v_1.7
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
import nodemailer from 'nodemailer';
|
||||
import { ImapFlow } from 'imapflow';
|
||||
import { CONFIG_FILE } from './config.js';
|
||||
import { readJson, writeJson } from './store.js';
|
||||
import { logEvent } from './logger.js';
|
||||
|
||||
// Prefilled for PurelyMail. The admin only needs to add the account address and
|
||||
// password in User management → Mail.
|
||||
const DEFAULTS = {
|
||||
enabled: false,
|
||||
fromName: 'QR Presentation',
|
||||
username: '', // full PurelyMail email address, used as SMTP/IMAP login
|
||||
password: '',
|
||||
smtpHost: 'smtp.purelymail.com',
|
||||
smtpPort: 465,
|
||||
smtpSecure: true, // SSL/TLS on 465
|
||||
imapHost: 'imap.purelymail.com',
|
||||
imapPort: 993,
|
||||
imapSecure: true, // SSL/TLS on 993
|
||||
saveToSent: true,
|
||||
sentFolder: 'Sent'
|
||||
};
|
||||
|
||||
let cfg = { ...DEFAULTS };
|
||||
|
||||
export async function initMail() {
|
||||
const saved = await readJson(CONFIG_FILE, null);
|
||||
cfg = { ...DEFAULTS, ...(saved && saved.mail ? saved.mail : {}) };
|
||||
}
|
||||
|
||||
export function isAvailable() {
|
||||
return Boolean(cfg.enabled && cfg.username && cfg.password);
|
||||
}
|
||||
|
||||
// Never leak the stored password.
|
||||
export function getMailConfig() {
|
||||
const { password, ...safe } = cfg;
|
||||
return { ...safe, hasPassword: Boolean(password), available: isAvailable() };
|
||||
}
|
||||
|
||||
export async function saveMailConfig(patch = {}) {
|
||||
const next = { ...cfg, ...patch };
|
||||
// Blank password field means "keep what's stored".
|
||||
if (patch.password === undefined || patch.password === '') next.password = cfg.password;
|
||||
next.smtpPort = Number(next.smtpPort) || DEFAULTS.smtpPort;
|
||||
next.imapPort = Number(next.imapPort) || DEFAULTS.imapPort;
|
||||
next.enabled = Boolean(next.enabled);
|
||||
next.smtpSecure = Boolean(next.smtpSecure);
|
||||
next.imapSecure = Boolean(next.imapSecure);
|
||||
next.saveToSent = Boolean(next.saveToSent);
|
||||
cfg = next;
|
||||
await writeJson(CONFIG_FILE, { mail: cfg });
|
||||
return getMailConfig();
|
||||
}
|
||||
|
||||
function requireConfigured() {
|
||||
if (!cfg.username || !cfg.password) {
|
||||
throw new Error('Mail account is not set up. Add it under User management → Mail.');
|
||||
}
|
||||
}
|
||||
|
||||
// Compile a MIME message to a Buffer using nodemailer's stream transport, so we
|
||||
// can both send it over SMTP and APPEND the exact same bytes to the Sent folder.
|
||||
async function buildRaw({ to, subject, text, html, attachments }) {
|
||||
const composer = nodemailer.createTransport({ streamTransport: true, buffer: true, newline: 'crlf' });
|
||||
const built = await composer.sendMail({
|
||||
from: `"${cfg.fromName}" <${cfg.username}>`,
|
||||
to,
|
||||
subject,
|
||||
text,
|
||||
html,
|
||||
attachments
|
||||
});
|
||||
return built.message; // Buffer
|
||||
}
|
||||
|
||||
export async function sendMail({ to, subject, text, html, attachments, actor = 'system' }) {
|
||||
requireConfigured();
|
||||
const recipients = (Array.isArray(to) ? to : [to]).map((s) => String(s).trim()).filter(Boolean);
|
||||
if (!recipients.length) throw new Error('No recipient address provided.');
|
||||
|
||||
const raw = await buildRaw({ to: recipients.join(', '), subject, text, html, attachments });
|
||||
|
||||
const smtp = nodemailer.createTransport({
|
||||
host: cfg.smtpHost,
|
||||
port: cfg.smtpPort,
|
||||
secure: cfg.smtpSecure,
|
||||
auth: { user: cfg.username, pass: cfg.password }
|
||||
});
|
||||
|
||||
try {
|
||||
await smtp.sendMail({ envelope: { from: cfg.username, to: recipients }, raw });
|
||||
} catch (err) {
|
||||
await logEvent({ level: 'error', actor, action: 'mail_send',
|
||||
message: `Failed sending to ${recipients.join(', ')}: ${err.message}` });
|
||||
throw new Error(`Could not send the email: ${err.message}`);
|
||||
}
|
||||
|
||||
// Keep a copy in the account's Sent folder via IMAP APPEND.
|
||||
if (cfg.saveToSent) {
|
||||
try {
|
||||
const client = new ImapFlow({
|
||||
host: cfg.imapHost, port: cfg.imapPort, secure: cfg.imapSecure,
|
||||
auth: { user: cfg.username, pass: cfg.password }, logger: false
|
||||
});
|
||||
await client.connect();
|
||||
await client.append(cfg.sentFolder || 'Sent', raw, ['\\Seen']);
|
||||
await client.logout();
|
||||
} catch (err) {
|
||||
await logEvent({ level: 'warn', actor, action: 'mail_sent_copy',
|
||||
message: `Sent to ${recipients.join(', ')} but could not save to "${cfg.sentFolder}": ${err.message}` });
|
||||
}
|
||||
}
|
||||
|
||||
await logEvent({ actor, action: 'mail_send',
|
||||
message: `Email sent to ${recipients.join(', ')} — "${subject}"` });
|
||||
return { ok: true };
|
||||
}
|
||||
Reference in New Issue
Block a user