← back to Jill Website

src/services/emailService.ts

411 lines

import nodemailer, { Transporter } from 'nodemailer';
import { Inquiry } from '../models/Inquiry';

export interface EmailConfig {
  host: string;
  port: number;
  secure: boolean;
  auth: {
    user: string;
    pass: string;
  };
  from: string;
}

export interface EmailResult {
  success: boolean;
  messageId?: string;
  error?: string;
}

export interface EmailLog {
  timestamp: Date;
  to: string;
  subject: string;
  success: boolean;
  error?: string;
  retryCount: number;
}

function escapeHtml(value: unknown): string {
  return String(value ?? '')
    .replace(/&/g, '&')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#39;');
}

export class EmailService {
  private transporter: Transporter;
  private config: EmailConfig;
  private emailLogs: EmailLog[] = [];
  private maxRetries: number = 3;
  private retryDelay: number = 5000; // 5 seconds

  constructor(config?: EmailConfig) {
    // Use provided config or default to environment variables
    this.config = config || {
      host: process.env.SMTP_HOST || 'smtp.gmail.com',
      port: parseInt(process.env.SMTP_PORT || '587', 10),
      secure: process.env.SMTP_SECURE === 'true',
      auth: {
        user: process.env.SMTP_USER || '',
        pass: process.env.SMTP_PASS || ''
      },
      from: process.env.SMTP_FROM || 'noreply@nosarabeachfront.com'
    };

    this.transporter = nodemailer.createTransport({
      host: this.config.host,
      port: this.config.port,
      secure: this.config.secure,
      auth: this.config.auth
    });
  }

  /**
   * Generate unsubscribe URL with email parameter
   */
  private getUnsubscribeUrl(email: string): string {
    const baseUrl = process.env.BASE_URL || 'http://localhost:8200';
    return `${baseUrl}/api/unsubscribe?email=${encodeURIComponent(email)}`;
  }

  /**
   * Generate HTML email template for inquiry confirmation
   */
  private generateConfirmationEmail(inquiry: Inquiry, propertyDetails?: any): string {
    const checkInDate = new Date(inquiry.check_in_date).toLocaleDateString('en-US', {
      weekday: 'long',
      year: 'numeric',
      month: 'long',
      day: 'numeric'
    });

    const checkOutDate = new Date(inquiry.check_out_date).toLocaleDateString('en-US', {
      weekday: 'long',
      year: 'numeric',
      month: 'long',
      day: 'numeric'
    });

    const unsubscribeUrl = this.getUnsubscribeUrl(inquiry.email);
    const name = escapeHtml(inquiry.name);
    const email = escapeHtml(inquiry.email);
    const phone = escapeHtml(inquiry.phone);
    const guestCount = escapeHtml(inquiry.guest_count);
    const message = escapeHtml(inquiry.message);

    return `
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Inquiry Confirmation</title>
  <style>
    body {
      font-family: Arial, sans-serif;
      line-height: 1.6;
      color: #333;
      max-width: 600px;
      margin: 0 auto;
      padding: 20px;
    }
    .header {
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      color: white;
      padding: 30px 20px;
      text-align: center;
      border-radius: 8px 8px 0 0;
    }
    .content {
      background: #f9f9f9;
      padding: 30px 20px;
      border-left: 1px solid #ddd;
      border-right: 1px solid #ddd;
    }
    .detail-box {
      background: white;
      padding: 20px;
      margin: 20px 0;
      border-radius: 8px;
      border-left: 4px solid #667eea;
    }
    .detail-row {
      padding: 8px 0;
      border-bottom: 1px solid #eee;
    }
    .detail-row:last-child {
      border-bottom: none;
    }
    .label {
      font-weight: bold;
      color: #667eea;
      display: inline-block;
      width: 150px;
    }
    .footer {
      background: #333;
      color: #fff;
      padding: 20px;
      text-align: center;
      font-size: 12px;
      border-radius: 0 0 8px 8px;
    }
    .footer a {
      color: #667eea;
      text-decoration: none;
    }
    .message-box {
      background: #fff;
      padding: 15px;
      margin: 15px 0;
      border-radius: 8px;
      border: 1px solid #ddd;
      font-style: italic;
    }
  </style>
</head>
<body>
  <div class="header">
    <h1>Thank You for Your Inquiry!</h1>
    <p>We've received your reservation request</p>
  </div>

  <div class="content">
    <p>Dear ${name},</p>

    <p>Thank you for your interest in our luxury beachfront property in Nosara, Costa Rica. We have received your inquiry and our property management team will review it shortly.</p>

    <div class="detail-box">
      <h3 style="margin-top: 0; color: #667eea;">Your Inquiry Details</h3>

      <div class="detail-row">
        <span class="label">Name:</span>
        <span>${name}</span>
      </div>

      <div class="detail-row">
        <span class="label">Email:</span>
        <span>${email}</span>
      </div>

      <div class="detail-row">
        <span class="label">Phone:</span>
        <span>${phone}</span>
      </div>

      <div class="detail-row">
        <span class="label">Check-in:</span>
        <span>${checkInDate}</span>
      </div>

      <div class="detail-row">
        <span class="label">Check-out:</span>
        <span>${checkOutDate}</span>
      </div>

      <div class="detail-row">
        <span class="label">Guests:</span>
        <span>${guestCount}</span>
      </div>
    </div>

    ${inquiry.message ? `
    <div class="message-box">
      <strong>Your Message:</strong><br>
      ${message}
    </div>
    ` : ''}

    <p><strong>What happens next?</strong></p>
    <ul>
      <li>Our team will review your inquiry within 24 hours</li>
      <li>We'll check availability for your requested dates</li>
      <li>You'll receive a personalized response with pricing and availability</li>
      <li>We're here to answer any questions you may have</li>
    </ul>

    <p>If you have any immediate questions, please don't hesitate to contact us directly.</p>

    <p>We look forward to hosting you in paradise!</p>

    <p>Best regards,<br>
    <strong>Nosara Beachfront Rentals Team</strong></p>
  </div>

  <div class="footer">
    <p>Nosara Beachfront Rentals | Luxury Vacation Properties</p>
    <p>Nosara, Guanacaste, Costa Rica</p>
    <p style="margin-top: 15px;">
      <a href="mailto:info@nosarabeachfront.com">info@nosarabeachfront.com</a>
    </p>
    <p style="margin-top: 15px; font-size: 11px;">
      You received this email because you submitted an inquiry through our website.
      <br>
      <a href="${unsubscribeUrl}">Unsubscribe from future emails</a>
    </p>
  </div>
</body>
</html>
    `.trim();
  }

  /**
   * Send confirmation email with retry mechanism
   */
  async sendConfirmationEmail(inquiry: Inquiry, propertyDetails?: any): Promise<EmailResult> {
    let lastError: string = '';

    for (let attempt = 0; attempt < this.maxRetries; attempt++) {
      try {
        const htmlContent = this.generateConfirmationEmail(inquiry, propertyDetails);

        const info = await this.transporter.sendMail({
          from: this.config.from,
          to: inquiry.email,
          subject: 'Inquiry Confirmation - Nosara Beachfront Rentals',
          html: htmlContent,
          text: this.generatePlainTextEmail(inquiry)
        });

        // Log successful email
        this.logEmail({
          timestamp: new Date(),
          to: inquiry.email,
          subject: 'Inquiry Confirmation',
          success: true,
          retryCount: attempt
        });

        return {
          success: true,
          messageId: info.messageId
        };
      } catch (error) {
        lastError = error instanceof Error ? error.message : 'Unknown error';

        // Log failed attempt
        console.error(`Email delivery attempt ${attempt + 1} failed:`, lastError);

        // Wait before retry (except on last attempt)
        if (attempt < this.maxRetries - 1) {
          await this.sleep(this.retryDelay);
        }
      }
    }

    // All retries failed - log final failure
    this.logEmail({
      timestamp: new Date(),
      to: inquiry.email,
      subject: 'Inquiry Confirmation',
      success: false,
      error: lastError,
      retryCount: this.maxRetries
    });

    return {
      success: false,
      error: lastError
    };
  }

  /**
   * Generate plain text version of email
   */
  private generatePlainTextEmail(inquiry: Inquiry): string {
    const checkInDate = new Date(inquiry.check_in_date).toLocaleDateString('en-US');
    const checkOutDate = new Date(inquiry.check_out_date).toLocaleDateString('en-US');
    const unsubscribeUrl = this.getUnsubscribeUrl(inquiry.email);

    return `
Thank You for Your Inquiry!

Dear ${inquiry.name},

Thank you for your interest in our luxury beachfront property in Nosara, Costa Rica. We have received your inquiry and our property management team will review it shortly.

Your Inquiry Details:
- Name: ${inquiry.name}
- Email: ${inquiry.email}
- Phone: ${inquiry.phone}
- Check-in: ${checkInDate}
- Check-out: ${checkOutDate}
- Guests: ${inquiry.guest_count}

${inquiry.message ? `Your Message:\n${inquiry.message}\n` : ''}

What happens next?
- Our team will review your inquiry within 24 hours
- We'll check availability for your requested dates
- You'll receive a personalized response with pricing and availability
- We're here to answer any questions you may have

If you have any immediate questions, please don't hesitate to contact us directly.

We look forward to hosting you in paradise!

Best regards,
Nosara Beachfront Rentals Team

---
Nosara Beachfront Rentals | Luxury Vacation Properties
Nosara, Guanacaste, Costa Rica
info@nosarabeachfront.com

You received this email because you submitted an inquiry through our website.
To unsubscribe from future emails, visit: ${unsubscribeUrl}
    `.trim();
  }

  /**
   * Log email delivery attempts
   */
  private logEmail(log: EmailLog): void {
    this.emailLogs.push(log);

    // Keep only last 1000 logs
    if (this.emailLogs.length > 1000) {
      this.emailLogs = this.emailLogs.slice(-1000);
    }

    // Also log to console
    const status = log.success ? '✅ SUCCESS' : '❌ FAILED';
    console.log(`[EMAIL ${status}] To: ${log.to} | Subject: ${log.subject} | Retries: ${log.retryCount}${log.error ? ` | Error: ${log.error}` : ''}`);
  }

  /**
   * Get email logs
   */
  getEmailLogs(limit: number = 100): EmailLog[] {
    return this.emailLogs.slice(-limit);
  }

  /**
   * Helper function to sleep
   */
  private sleep(ms: number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, ms));
  }

  /**
   * Verify SMTP connection
   */
  async verifyConnection(): Promise<boolean> {
    try {
      await this.transporter.verify();
      console.log('✅ SMTP connection verified');
      return true;
    } catch (error) {
      console.error('❌ SMTP connection failed:', error);
      return false;
    }
  }
}

// Export singleton instance
export const emailService = new EmailService();