/** * lib/smtp.ts * SMTP mail sending via nodemailer + IMAP Sent folder append. * Uses user's mailbox credentials for authenticated SMTP. */ import nodemailer from "nodemailer"; import { ImapFlow } from "imapflow"; import type { MailCredentials } from "./imap"; const SMTP_HOST = process.env.MAILCOW_API_URL ? new URL(process.env.MAILCOW_API_URL).hostname : "localhost"; const SMTP_PORT = parseInt(process.env.SMTP_PORT ?? "587"); const IMAP_HOST = SMTP_HOST; const IMAP_PORT = parseInt(process.env.IMAP_PORT ?? "993"); export interface SendMailOptions { to: string; cc?: string; subject: string; html: string; text?: string; inReplyTo?: string; references?: string; attachments?: { filename: string; content: Buffer | string; contentType?: string; }[]; } export async function sendMail( creds: MailCredentials, options: SendMailOptions ): Promise<{ success: boolean; messageId?: string; error?: string }> { try { const transporter = nodemailer.createTransport({ host: SMTP_HOST, port: SMTP_PORT, secure: SMTP_PORT === 465, auth: { user: creds.email, pass: creds.password, }, tls: { // Mailcow self-signed cert'e izin ver rejectUnauthorized: false, }, }); const mailOptions = { from: creds.email, to: options.to, cc: options.cc || undefined, subject: options.subject, html: options.html, text: options.text || undefined, inReplyTo: options.inReplyTo || undefined, references: options.references || undefined, attachments: options.attachments?.map((a) => ({ filename: a.filename, content: a.content, contentType: a.contentType, })), }; const result = await transporter.sendMail(mailOptions); // Gönderilen maili Sent klasörüne kaydet (IMAP APPEND) try { await appendToSent(creds, mailOptions); } catch (e) { // Sent'e kaydetme başarısız olursa mail yine de gitmiş olur console.error("Sent klasörüne kaydetme hatası:", e); } return { success: true, messageId: result.messageId }; } catch (err: any) { return { success: false, error: err?.message ?? "SMTP hatası" }; } } /** * Gönderilen maili IMAP Sent klasörüne kaydet. * Mailcow'da Sent klasörü "Sent" olarak adlandırılır. */ async function appendToSent( creds: MailCredentials, mailOptions: Record ): Promise { // nodemailer ile raw mesaj oluştur const transporter = nodemailer.createTransport({ jsonTransport: true }); const compiled = await transporter.sendMail(mailOptions); const rawMessage = JSON.parse(compiled.message); // Gerçek raw RFC822 mesajı oluştur const buildTransporter = nodemailer.createTransport({ streamTransport: true }); const built = await buildTransporter.sendMail(mailOptions); const chunks: Buffer[] = []; for await (const chunk of built.message as any) { chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); } const rawBuffer = Buffer.concat(chunks); const client = new ImapFlow({ host: IMAP_HOST, port: IMAP_PORT, secure: true, auth: { user: creds.email, pass: creds.password }, logger: false, }); await client.connect(); try { // Sent klasörünü bul const mailboxes = await client.list(); let sentPath = "Sent"; for (const mb of mailboxes) { if (mb.specialUse === "\\Sent") { sentPath = mb.path; break; } } // Mesajı Sent klasörüne ekle (Seen flag ile) await client.append(sentPath, rawBuffer, ["\\Seen"]); } finally { await client.logout(); } }