2 * @file Email Service - Multi-Provider Email Delivery
3 * @module services/email
5 * Unified email sending service supporting multiple providers (SMTP, Mailgun). Provides
6 * automatic fallback to console logging when no transport is configured (development mode).
7 * Supports per-tenant email configuration and environment variable defaults.
9 * **Supported Providers:**
10 * - **SMTP**: Universal email protocol via nodemailer (default)
11 * - **Mailgun**: Transactional email API for high deliverability
13 * **Configuration Priority:**
14 * 1. Runtime settings parameter (tenant-specific)
15 * 2. Environment variables (global defaults)
16 * 3. Console fallback (no configuration)
18 * **SMTP Environment Variables:**
19 * - `SMTP_HOST` - SMTP server hostname
20 * - `SMTP_PORT` - SMTP server port (25, 465, 587)
21 * - `SMTP_USER` - SMTP authentication username
22 * - `SMTP_PASS` - SMTP authentication password
23 * - `SMTP_SECURE` - Use TLS (true for port 465)
24 * - `FROM_EMAIL` - Default sender address
26 * **Mailgun Configuration:**
27 * - `settings.mailgun.apiKey` - Mailgun API key
28 * - `settings.mailgun.domain` - Mailgun domain
31 * - Password reset emails
33 * - Alert notifications
35 * - Ticket notifications
37 * // Send email using SMTP (env vars)
38 * const { sendEmail } = require('./services/email');
40 * to: 'user@example.com',
41 * subject: 'Welcome to RMM',
42 * html: '<h1>Welcome!</h1><p>Thanks for signing up.</p>',
43 * text: 'Welcome! Thanks for signing up.'
46 * // Send via Mailgun with custom settings
48 * to: 'customer@example.com',
49 * subject: 'Invoice #12345',
51 * provider: 'mailgun',
54 * apiKey: 'key-abc123',
55 * domain: 'mg.example.com'
57 * from: 'billing@example.com'
61 * // Development fallback (no SMTP configured)
62 * // Logs email to console instead of sending
64 * to: 'test@test.com',
69 * // [Email:FALLBACK] To: test@test.com
70 * // [Email:FALLBACK] Subject: Test
71 * // [Email:FALLBACK] Text: Testing
72 * @requires nodemailer - SMTP email client
73 * @requires @mailgun-js/mailgun.js - Mailgun API client
74 * @requires form-data - Multipart form data for Mailgun
77const nodemailer = require('nodemailer');
78const formData = require('form-data');
79const Mailgun = require('@mailgun-js/mailgun.js');
80const pool = require('./db');
81const crypto = require('crypto');
85 * Check if email configuration is valid for a tenant
86 * @description Validates that tenant has complete email configuration (SMTP or Mailgun)
87 * before attempting to send. Prevents connection errors and logs issues.
88 * @param {number} tenantId - Tenant ID to check configuration for
89 * @param {object} [settings] - Runtime settings to validate (optional, checks DB if not provided)
90 * @returns {Promise<object>} {isConfigured: boolean, provider: string, error: string}
92async function checkEmailConfig(tenantId, settings = null) {
94 // If settings provided, validate them
96 if (settings.provider === 'mailgun') {
97 const valid = settings.mailgun?.apiKey && settings.mailgun?.domain;
101 error: valid ? null : 'Missing Mailgun API key or domain'
105 const smtp = settings.smtp || {};
106 const host = smtp.host || process.env.SMTP_HOST;
107 const port = smtp.port || process.env.SMTP_PORT;
108 const user = smtp.user || process.env.SMTP_USER;
109 const pass = smtp.pass || process.env.SMTP_PASS;
111 if (!host || !port) {
115 error: 'Missing SMTP host or port'
119 return { isConfigured: true, provider: 'smtp', error: null };
122 // Check database for tenant config
123 const result = await pool.query(
124 'SELECT provider, is_configured, smtp_host, smtp_port, mailgun_api_key, mailgun_domain FROM tenant_email_config WHERE tenant_id = $1',
128 if (!result.rows.length) {
129 // No tenant config, check global env vars
130 const hasGlobalSMTP = process.env.SMTP_HOST && process.env.SMTP_PORT;
132 isConfigured: hasGlobalSMTP,
134 error: hasGlobalSMTP ? null : 'No email configuration found (tenant or global)'
138 const config = result.rows[0];
140 isConfigured: config.is_configured,
141 provider: config.provider,
142 error: config.is_configured ? null : 'Email configuration incomplete or not tested'
145 console.error('[Email] Config check error:', error);
146 return { isConfigured: false, provider: 'unknown', error: error.message };
152 * Create email tracking record in database
153 * @description Generates tracking pixel ID and stores email metadata for open tracking
154 * @param {object} params - Tracking parameters
155 * @param {number} params.tenantId - Tenant ID
156 * @param {string} params.to - Recipient email
157 * @param {string} params.subject - Email subject
158 * @param {string} [params.emailType] - Email type (invoice, ticket, notification, etc.)
159 * @param {string} [params.referenceType] - Reference type (invoice, ticket, user, etc.)
160 * @param {number} [params.referenceId] - Reference ID
161 * @param {string} [params.provider] - Email provider (smtp, mailgun)
162 * @returns {Promise<string>} Tracking ID (UUID)
164async function createEmailTracking({ tenantId, to, subject, emailType, referenceType, referenceId, provider = 'smtp' }) {
166 const result = await pool.query(
167 `INSERT INTO email_tracking
168 (tenant_id, recipient_email, subject, email_type, reference_type, reference_id, provider)
169 VALUES ($1, $2, $3, $4, $5, $6, $7)
170 RETURNING tracking_id`,
171 [tenantId, to, subject, emailType, referenceType, referenceId, provider]
173 return result.rows[0].tracking_id;
175 console.error('[Email] Failed to create tracking record:', error);
176 return null; // Non-fatal, email can still send without tracking
182 * Inject tracking pixel into HTML email
183 * @description Adds 1x1 transparent tracking pixel at end of HTML body for open tracking
184 * @param {string} html - HTML email content
185 * @param {string} trackingId - Tracking UUID to embed in pixel URL
186 * @param {string} [backendUrl] - Backend URL (defaults to env var or relative)
187 * @returns {string} HTML with tracking pixel injected
189function injectTrackingPixel(html, trackingId, backendUrl = null) {
190 if (!html || !trackingId) return html;
192 const baseUrl = backendUrl || process.env.BACKEND_URL || 'https://rmm-psa-backend-t9f7k.ondigitalocean.app';
193 const pixelUrl = `${baseUrl}/api/email/track/${trackingId}/pixel.gif`;
195 // Inject pixel before closing body tag (or at end if no body tag)
196 const pixel = `<img src="${pixelUrl}" width="1" height="1" alt="" style="display:block;width:1px;height:1px;border:0;" />`;
198 if (html.toLowerCase().includes('</body>')) {
199 return html.replace(/<\/body>/i, `${pixel}</body>`);
207 * Build email transport client for SMTP or Mailgun
208 * @description Creates nodemailer transport for SMTP or Mailgun client based on settings.
209 * Returns null if SMTP not configured (triggers console fallback). Supports both
210 * runtime settings and environment variable configuration.
211 * @param {object} [settings] - Transport configuration object
212 * @param {string} [settings.provider] - Transport provider: 'smtp' (default) or 'mailgun'
213 * @param {object} [settings.smtp] - SMTP configuration
214 * @param {string} [settings.smtp.host] - SMTP server hostname
215 * @param {number} [settings.smtp.port] - SMTP server port (25, 465, 587)
216 * @param {string} [settings.smtp.user] - SMTP authentication username
217 * @param {string} [settings.smtp.pass] - SMTP authentication password
218 * @param {boolean} [settings.smtp.secure] - Use TLS (true for port 465)
219 * @param {object} [settings.mailgun] - Mailgun configuration
220 * @param {string} [settings.mailgun.apiKey] - Mailgun API key
221 * @param {string} [settings.mailgun.domain] - Mailgun sending domain
222 * @returns {object|null} Nodemailer transport, Mailgun client, or null if unconfigured
224function buildTransport(settings) {
225 // settings: { smtp: {...}, mailgun: {...}, provider: 'smtp'|'mailgun' }
226 if (settings && settings.provider === 'mailgun' && settings.mailgun) {
227 // Mailgun transport (new SDK)
228 const mg = new Mailgun(formData);
231 key: settings.mailgun.apiKey
235 const smtp = settings && settings.smtp ? settings.smtp : {};
236 const host = smtp.host || process.env.SMTP_HOST;
237 const port = smtp.port || process.env.SMTP_PORT;
238 const user = smtp.user || process.env.SMTP_USER;
239 const pass = smtp.pass || process.env.SMTP_PASS;
240 const secure = smtp.secure !== undefined ? smtp.secure : (process.env.SMTP_SECURE === 'true');
241 if (!host || !port) {
242 return null; // No SMTP configured; fallback to console
244 return nodemailer.createTransport({
248 auth: user && pass ? { user, pass } : undefined,
254 * Send email via SMTP or Mailgun with automatic fallback
255 * @description Sends email using configured provider (SMTP by default). Falls back to
256 * console logging in development if SMTP is not configured. Supports HTML and plain text
257 * content with automatic multipart handling. Now includes tracking pixel injection and
258 * configuration validation to prevent connection errors.
259 * @param {object} param0 - Email sending options
260 * @param {string} param0.to - Recipient email address
261 * @param {string} param0.subject - Email subject line
262 * @param {string} [param0.html] - HTML email content
263 * @param {string} [param0.text] - Plain text email content
264 * @param {string} [param0.provider] - Email provider: 'smtp' (default) or 'mailgun'
265 * @param {object} [param0.settings] - Provider configuration with from/smtp/mailgun properties
266 * @param {number} [param0.tenantId] - Tenant ID for tracking and config validation
267 * @param {string} [param0.emailType] - Email type for tracking (invoice, ticket, notification, etc.)
268 * @param {string} [param0.referenceType] - Reference type (invoice, ticket, user, etc.)
269 * @param {number} [param0.referenceId] - Reference ID for tracking
270 * @param {boolean} [param0.skipTracking] - Skip tracking pixel injection (default: false)
271 * @returns {Promise<object>} Email send result with tracking info: {success, trackingId, messageId, fallback}
272 * @throws {Error} Throws if email sending fails (network, auth, etc.) or if config is invalid
274async function sendEmail({
283 referenceType = null,
287 const from = (settings.from || process.env.FROM_EMAIL) || 'no-reply@ibg.local';
289 // Validate email configuration before attempting to send
291 const configCheck = await checkEmailConfig(tenantId, settings);
292 if (!configCheck.isConfigured) {
293 console.warn(`[Email] Tenant ${tenantId}: Email not configured - ${configCheck.error}`);
294 console.log('[Email:FALLBACK] To:', to);
295 console.log('[Email:FALLBACK] Subject:', subject);
296 console.log('[Email:FALLBACK] Reason: Email not configured for tenant');
300 error: configCheck.error,
301 message: 'Email configuration incomplete - email not sent'
306 // Create tracking record and inject pixel (if enabled)
307 let trackingId = null;
308 if (tenantId && !skipTracking) {
309 trackingId = await createEmailTracking({
319 // Inject tracking pixel into HTML
320 if (trackingId && html) {
321 html = injectTrackingPixel(html, trackingId);
326 if (provider === 'mailgun' && settings.mailgun) {
328 const mg = buildTransport({ provider: 'mailgun', mailgun: settings.mailgun });
333 text: text || undefined,
334 html: html || undefined
337 const result = await mg.messages.create(settings.mailgun.domain, data);
338 console.log(`[Email:Mailgun] Sent to ${to} - Message ID: ${result.id}`);
340 // Update tracking with provider message ID
343 'UPDATE email_tracking SET provider_message_id = $1, delivery_status = $2 WHERE tracking_id = $3',
344 [result.id, 'sent', trackingId]
350 messageId: result.id,
355 console.error('[Mailgun] Error:', error);
357 // Update tracking with error
360 'UPDATE email_tracking SET delivery_status = $1, delivery_error = $2 WHERE tracking_id = $3',
361 ['failed', error.message, trackingId]
369 const transporter = buildTransport({ provider: 'smtp', smtp: settings.smtp });
371 console.log('[Email:FALLBACK] To:', to);
372 console.log('[Email:FALLBACK] Subject:', subject);
373 console.log('[Email:FALLBACK] Text:', text || '');
374 console.log('[Email:FALLBACK] HTML length:', html ? html.length : 0);
375 console.log('[Email:FALLBACK] Reason: SMTP not configured (missing SMTP_HOST or SMTP_PORT)');
380 message: 'SMTP not configured - email logged to console only'
385 const info = await transporter.sendMail({ from, to, subject, text, html });
386 console.log(`[Email:SMTP] Sent to ${to} - Message ID: ${info.messageId}`);
388 // Update tracking with provider message ID
391 'UPDATE email_tracking SET provider_message_id = $1, delivery_status = $2 WHERE tracking_id = $3',
392 [info.messageId, 'sent', trackingId]
398 messageId: info.messageId,
403 console.error('[SMTP] Error:', error);
405 // Update tracking with error
408 'UPDATE email_tracking SET delivery_status = $1, delivery_error = $2 WHERE tracking_id = $3',
409 ['failed', error.message, trackingId]
418module.exports = { sendEmail, checkEmailConfig, createEmailTracking, injectTrackingPixel };