safeer-pdf-generator
Version:
Framework-agnostic PDF generation library with chunking, merging, S3 upload, and email delivery
344 lines (339 loc) • 14.1 kB
JavaScript
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