All files / connectors smtp.ts

100% Statements 10/10
50% Branches 2/4
80% Functions 4/5
100% Lines 10/10

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72                                          18x       18x                 18x       12x                   2x             12x               4x 4x 2x   2x        
import { createTransport, type Transporter } from "nodemailer";
import type { OutgoingMessage, SendConnector, SendResult } from "./types.js";
 
export interface SmtpConfig {
	host: string;
	port: number;
	secure: boolean;
	auth: {
		user: string;
		pass: string;
	};
}
 
/**
 * SendConnector implementation backed by SMTP via Nodemailer.
 *
 * Creates a transport on construction; call verify() to test credentials
 * before sending. Each send() creates a fresh connection (Nodemailer handles
 * pooling internally when pool:true is set, but we keep it simple for now).
 */
export class SmtpSendConnector implements SendConnector {
	readonly name = "smtp";
	private transport: Transporter;
 
	constructor(config: SmtpConfig) {
		this.transport = createTransport({
			host: config.host,
			port: config.port,
			secure: config.secure,
			auth: config.auth,
			tls: { rejectUnauthorized: config.secure },
		});
		// Suppress async transport errors (connection timeouts, socket resets)
		// that can fire outside of send()/verify() and cause unhandled rejections.
		this.transport.on("error", () => {});
	}
 
	async send(message: OutgoingMessage): Promise<SendResult> {
		const result = await this.transport.sendMail({
			from: message.from,
			to: message.to.join(", "),
			cc: message.cc?.join(", "),
			bcc: message.bcc?.join(", "),
			subject: message.subject,
			text: message.textBody,
			html: message.htmlBody,
			inReplyTo: message.inReplyTo,
			references: message.references?.join(" "),
			attachments: message.attachments?.map((a) => ({
				filename: a.filename,
				contentType: a.contentType,
				content: a.content,
			})),
		});
 
		return {
			messageId: result.messageId,
			accepted: Array.isArray(result.accepted) ? result.accepted.map(String) : [],
			rejected: Array.isArray(result.rejected) ? result.rejected.map(String) : [],
		};
	}
 
	async verify(): Promise<boolean> {
		try {
			await this.transport.verify();
			return true;
		} catch {
			return false;
		}
	}
}