48hr.email/application/smtp-service.js
ClaraCrazy 8daa0fefe9
[Feat]: Add email forwarding
Uses seperate SMTP credentials for forwarding. This is just the raw system, validation will be in the next commit.
2026-01-02 16:11:29 +01:00

222 lines
6.8 KiB
JavaScript

const nodemailer = require('nodemailer')
const debug = require('debug')('48hr-email:smtp-service')
/**
* SMTP Service for forwarding emails
* Uses nodemailer to send forwarded emails via configured SMTP server
*/
class SmtpService {
constructor(config) {
this.config = config
this.transporter = null
// Only initialize transporter if SMTP is configured
if (this._isConfigured()) {
this._initializeTransporter()
} else {
debug('SMTP not configured - forwarding functionality will be unavailable')
}
}
/**
* Check if SMTP is properly configured
* @returns {boolean}
*/
_isConfigured() {
return !!(
this.config.smtp.enabled &&
this.config.smtp.host &&
this.config.smtp.user &&
this.config.smtp.password
)
}
/**
* Initialize the nodemailer transporter
* @private
*/
_initializeTransporter() {
try {
this.transporter = nodemailer.createTransport({
host: this.config.smtp.host,
port: this.config.smtp.port,
secure: this.config.smtp.secure,
auth: {
user: this.config.smtp.user,
pass: this.config.smtp.password
},
tls: {
// Allow self-signed certificates and skip verification
// This is useful for development or internal SMTP servers
rejectUnauthorized: false
}
})
debug(`SMTP transporter initialized: ${this.config.smtp.host}:${this.config.smtp.port}`)
} catch (error) {
debug('Failed to initialize SMTP transporter:', error.message)
throw new Error(`SMTP initialization failed: ${error.message}`)
}
}
/**
* Forward an email to a destination address
* @param {Object} mail - Parsed email object from mailparser
* @param {string} destinationEmail - Email address to forward to
* @returns {Promise<{success: boolean, error?: string, messageId?: string}>}
*/
async forwardMail(mail, destinationEmail) {
if (!this.transporter) {
return {
success: false,
error: 'SMTP is not configured. Please configure SMTP settings to enable forwarding.'
}
}
if (!mail) {
return {
success: false,
error: 'Email not found'
}
}
try {
debug(`Forwarding email (Subject: "${mail.subject}") to ${destinationEmail}`)
const forwardMessage = this._buildForwardMessage(mail, destinationEmail)
const info = await this.transporter.sendMail(forwardMessage)
debug(`Email forwarded successfully. MessageId: ${info.messageId}`)
return {
success: true,
messageId: info.messageId
}
} catch (error) {
debug('Failed to forward email:', error.message)
return {
success: false,
error: `Failed to send email: ${error.message}`
}
}
}
/**
* Build the forward message structure
* @param {Object} mail - Parsed email object
* @param {string} destinationEmail - Destination address
* @returns {Object} - Nodemailer message object
* @private
*/
_buildForwardMessage(mail, destinationEmail) {
// Extract original sender info
const originalFrom = (mail.from && mail.from.text) || 'Unknown Sender'
const originalTo = (mail.to && mail.to.text) || 'Unknown Recipient'
const originalDate = mail.date ? new Date(mail.date).toLocaleString() : 'Unknown Date'
const originalSubject = mail.subject || '(no subject)'
// Build forwarded message body
let forwardedBody = `
---------- Forwarded message ----------
From: ${originalFrom}
Date: ${originalDate}
Subject: ${originalSubject}
To: ${originalTo}
`
// Add original text body if available
if (mail.text) {
forwardedBody += mail.text
} else if (mail.html) {
// If only HTML is available, mention it
forwardedBody += '[This email contains HTML content. See attachment or HTML version below.]\n\n'
}
// Build the message object
const message = {
from: {
name: this.config.smtp.fromName,
address: this.config.smtp.user
},
to: destinationEmail,
subject: `Fwd: ${originalSubject}`,
text: forwardedBody,
replyTo: originalFrom
}
// Add HTML body if available
if (mail.html) {
const htmlForwardedBody = `
<div style="border-left: 2px solid #ccc; padding-left: 10px; margin: 10px 0;">
<p><strong>---------- Forwarded message ----------</strong><br>
<strong>From:</strong> ${this._escapeHtml(originalFrom)}<br>
<strong>Date:</strong> ${this._escapeHtml(originalDate)}<br>
<strong>Subject:</strong> ${this._escapeHtml(originalSubject)}<br>
<strong>To:</strong> ${this._escapeHtml(originalTo)}</p>
</div>
${mail.html}
`
message.html = htmlForwardedBody
}
// Add attachments if present
if (mail.attachments && mail.attachments.length > 0) {
message.attachments = mail.attachments.map(att => ({
filename: att.filename || 'attachment',
content: att.content,
contentType: att.contentType,
contentDisposition: att.contentDisposition || 'attachment'
}))
debug(`Including ${mail.attachments.length} attachment(s) in forwarded email`)
}
return message
}
/**
* Simple HTML escape for email headers
* @param {string} text
* @returns {string}
* @private
*/
_escapeHtml(text) {
if (!text) return ''
return String(text)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;')
}
/**
* Verify SMTP connection
* @returns {Promise<{success: boolean, error?: string}>}
*/
async verifyConnection() {
if (!this.transporter) {
return {
success: false,
error: 'SMTP is not configured'
}
}
try {
await this.transporter.verify()
debug('SMTP connection verified successfully')
return { success: true }
} catch (error) {
debug('SMTP connection verification failed:', error.message)
return {
success: false,
error: error.message
}
}
}
}
module.exports = SmtpService