UNPKG

safeer-pdf-generator

Version:

Framework-agnostic PDF generation library with chunking, merging, S3 upload, and email delivery

344 lines (339 loc) 14.1 kB
import { EmailError } from '../../core/errors.js'; import { noOpLogger } from '../../config/defaults.js'; /** * Default email sender implementation using Nodemailer */ export class DefaultEmailSender { constructor(logger = noOpLogger) { this.logger = logger; } /** * Initialize transporter (lazy loading to avoid requiring nodemailer) */ async initTransporter(config) { if (this.transporter && config.transport) { // Use provided transporter return config.transport; } if (!config.smtp && !config.transport) { throw new EmailError('Either transport or SMTP configuration is required'); } try { // Dynamic import to avoid bundling nodemailer if not used const nodemailer = await import('nodemailer'); if (config.transport) { this.transporter = config.transport; this.logger.debug('Using provided email transporter'); } else if (config.smtp) { this.transporter = nodemailer.createTransport({ host: config.smtp.host, port: config.smtp.port, secure: config.smtp.secure || false, auth: { user: config.smtp.auth.user, pass: config.smtp.auth.pass, }, }); this.logger.debug(`Created SMTP transporter for ${config.smtp.host}:${config.smtp.port}`); } return this.transporter; } catch (error) { throw new EmailError('Failed to initialize email transporter. Make sure nodemailer is installed.', config.to, error); } } /** * Send PDF email */ async send(config, pdfBuffer, filename, downloadUrl) { const transporter = await this.initTransporter(config); this.logger.info(`Sending PDF email to ${config.to}`); try { const subject = config.subject || `Your PDF Report: ${filename.replace(/\.[^.]+$/, '')}`; const htmlContent = await this.renderEmailTemplate(config, { title: filename.replace(/\.[^.]+$/, ''), generatedAt: new Date().toLocaleString(), downloadUrl, fileSizeMB: Number((pdfBuffer.length / 1024 / 1024).toFixed(2)), user: { name: '' }, // Will be populated if available company: { name: '' }, // Will be populated if available }); const mailOptions = { from: config.from || 'noreply@pdfreporer.com', to: config.to, subject, html: htmlContent, }; // Add attachment if requested or no download URL provided if (config.attachmentMode !== 'link' || !downloadUrl) { mailOptions.attachments = [ { filename, content: pdfBuffer, contentType: 'application/pdf', }, ]; } const result = await transporter.sendMail(mailOptions); this.logger.info(`Email sent successfully to ${config.to} (MessageId: ${result.messageId})`); return { sent: true, messageId: result.messageId, }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; // Enhanced error messages with better context let enhancedMessage = errorMessage; if (errorMessage.includes('socket close') || errorMessage.includes('ECONNRESET')) { enhancedMessage = `Email sending failed: Unexpected socket close. ` + `This usually indicates SMTP port/security mismatch. ` + `Current config: ${config.smtp?.host}:${config.smtp?.port} (secure=${config.smtp?.secure}). ` + `For Gmail: Use port 587 with secure=false, or port 465 with secure=true.`; } else if (errorMessage.includes('ENOTFOUND')) { enhancedMessage = `Email sending failed: SMTP host not found (${config.smtp?.host}). ` + `Please verify the SMTP host address is correct.`; } else if (errorMessage.includes('ECONNREFUSED')) { enhancedMessage = `Email sending failed: Connection refused to ${config.smtp?.host}:${config.smtp?.port}. ` + `Please verify the port number and firewall settings.`; } else if (errorMessage.includes('Invalid login')) { enhancedMessage = `Email sending failed: Authentication failed. ` + `Please verify your username and password are correct. ` + `For Gmail, you may need to use an App Password instead of your regular password.`; } else if (errorMessage.includes('self signed certificate')) { enhancedMessage = `Email sending failed: Self-signed certificate error. ` + `Try setting 'rejectUnauthorized: false' in your SMTP configuration, ` + `though this reduces security.`; } this.logger.error(`Email sending failed: ${enhancedMessage}`); return { sent: false, error: enhancedMessage, }; } } /** * Render email template */ async renderEmailTemplate(config, context) { if (typeof config.template === 'function') { return config.template(context); } if (typeof config.template === 'string') { return this.renderTemplate(config.template, context); } // Use default template return this.renderTemplate(getDefaultEmailTemplate(), context); } /** * Simple template renderer (supports {{variable}} syntax) */ renderTemplate(template, context) { return template.replace(/\{\{([^}]+)\}\}/g, (match, path) => { const keys = path.trim().split('.'); let value = context; for (const key of keys) { if (value && typeof value === 'object' && key in value) { value = value[key]; } else { return match; // Return original if path not found } } return String(value || ''); }); } /** * Test email configuration */ async testConnection(config) { try { const transporter = await this.initTransporter(config); // Verify connection const verified = await transporter.verify(); return { success: verified }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Unknown error', }; } } /** * Send test email */ async sendTest(config) { try { const testBuffer = Buffer.from('This is a test PDF attachment'); const result = await this.send({ ...config, subject: 'PDF Reporter - Test Email', }, testBuffer, 'test-report.pdf'); return result; } catch (error) { return { sent: false, error: error instanceof Error ? error.message : 'Unknown error', }; } } /** * Get email provider info from SMTP config */ static getProviderInfo(smtpHost) { const providers = { 'smtp.gmail.com': { provider: 'Gmail', defaultPort: 587, requiresSSL: true }, 'smtp.outlook.com': { provider: 'Outlook', defaultPort: 587, requiresSSL: true }, 'smtp.office365.com': { provider: 'Office365', defaultPort: 587, requiresSSL: true }, 'smtp.yahoo.com': { provider: 'Yahoo', defaultPort: 587, requiresSSL: true }, 'smtp.mail.yahoo.com': { provider: 'Yahoo', defaultPort: 587, requiresSSL: true }, 'smtp.sendgrid.net': { provider: 'SendGrid', defaultPort: 587, requiresSSL: true }, 'smtp.mailgun.org': { provider: 'Mailgun', defaultPort: 587, requiresSSL: true }, }; return (providers[smtpHost.toLowerCase()] || { provider: 'Unknown', defaultPort: 587, requiresSSL: false, }); } } /** * Default email template */ export function getDefaultEmailTemplate() { return ` <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Your PDF Report is Ready</title> <style> body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; margin: 0; padding: 20px; background-color: #f4f4f4; } .container { max-width: 600px; margin: 0 auto; background-color: white; padding: 30px; border-radius: 10px; box-shadow: 0 0 10px rgba(0,0,0,0.1); } .header { text-align: center; margin-bottom: 30px; } .header h1 { color: #2c3e50; margin: 0; } .content { margin-bottom: 30px; } .download-button { display: inline-block; padding: 12px 24px; background-color: #3498db; color: white; text-decoration: none; border-radius: 5px; margin: 20px 0; } .download-button:hover { background-color: #2980b9; } .details { background-color: #f8f9fa; padding: 20px; border-radius: 5px; margin: 20px 0; } .details h3 { margin-top: 0; color: #555; } .details ul { margin: 0; padding-left: 20px; } .footer { text-align: center; font-size: 12px; color: #777; margin-top: 30px; border-top: 1px solid #eee; padding-top: 20px; } </style> </head> <body> <div class="container"> <div class="header"> <h1>Your PDF Report is Ready!</h1> </div> <div class="content"> <p>Hello,</p> <p>Your requested PDF report "<strong>{{title}}</strong>" has been generated successfully.</p> <p>You can download your report using the button below:</p> <p style="text-align: center;"> <a href="{{downloadUrl}}" class="download-button">Download PDF Report</a> </p> <div class="details"> <h3>Report Details:</h3> <ul> <li>Title: {{title}}</li> <li>Generated: {{generatedAt}}</li> <li>File Size: {{fileSizeMB}} MB</li> </ul> </div> <p>If you have any questions or need assistance, please don't hesitate to contact our support team.</p> </div> <div class="footer"> <p>Best regards,<br>{{company.name}} Team</p> <p>This is an automated message. Please do not reply to this email.</p> </div> </div> </body> </html> `; } /** * Create email sender */ export function createEmailSender(logger) { return new DefaultEmailSender(logger); } /** * Test email connection without creating a full EmailSender instance */ export async function testEmailConnection(config) { try { // Dynamic import to avoid bundling nodemailer if not used const nodemailer = await import('nodemailer'); let transporter; if (config.transport) { transporter = config.transport; } else if (config.smtp) { transporter = nodemailer.createTransport({ host: config.smtp.host, port: config.smtp.port, secure: config.smtp.secure || false, auth: { user: config.smtp.auth.user, pass: config.smtp.auth.pass, }, }); } else { throw new Error('Either transport or SMTP configuration is required'); } // Verify connection await transporter.verify(); const providerInfo = DefaultEmailSender.getProviderInfo(config.smtp?.host || 'unknown'); return { success: true, details: { host: config.smtp?.host || 'transport', port: config.smtp?.port || 0, secure: config.smtp?.secure || false, provider: providerInfo.provider, }, }; } catch (error) { let errorMessage = error instanceof Error ? error.message : 'Unknown error'; // Enhanced error messages for connection testing if (errorMessage.includes('ENOTFOUND')) { errorMessage = `SMTP host not found: ${config.smtp?.host}. Please verify the host address.`; } else if (errorMessage.includes('ECONNREFUSED')) { errorMessage = `Connection refused to ${config.smtp?.host}:${config.smtp?.port}. Check port and firewall.`; } else if (errorMessage.includes('Invalid login')) { errorMessage = `Authentication failed. Verify username/password. For Gmail, use App Password.`; } else if (errorMessage.includes('self signed certificate')) { errorMessage = `SSL certificate error. Consider setting rejectUnauthorized: false (reduces security).`; } return { success: false, error: errorMessage, details: config.smtp ? { host: config.smtp.host, port: config.smtp.port, secure: config.smtp.secure || false, provider: DefaultEmailSender.getProviderInfo(config.smtp.host).provider, } : undefined, }; } } //# sourceMappingURL=EmailSender.js.map