first commit
This commit is contained in:
131
lib/smtp.ts
Normal file
131
lib/smtp.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user