132 lines
3.6 KiB
TypeScript
132 lines
3.6 KiB
TypeScript
/**
|
||
* 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<string, any>
|
||
): Promise<void> {
|
||
// 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();
|
||
}
|
||
}
|