feat(email): system/user senderType + attachments

Composer validator now takes senderType (system|user) and an
attachments[] array, and the service dispatches across two paths:
the system path uses lib/email/index.ts with port-config noreply
identity and logs signed_doc_emailed when an attachment matches a
document's signed PDF; the user path stays on the existing personal-
account flow but is gated by the new email.allowPersonalAccountSends
toggle and the attachment fileIds are persisted on email_messages.
sendEmail in lib/email accepts attachments and resolves them from
MinIO with cross-port enforcement.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt Ciaccio
2026-04-28 02:48:11 +02:00
parent 9e69c13202
commit 1151768159
4 changed files with 210 additions and 25 deletions

View File

@@ -19,6 +19,7 @@ export const SETTING_KEYS = {
emailReplyTo: 'email_reply_to',
emailSignatureHtml: 'email_signature_html',
emailFooterHtml: 'email_footer_html',
emailAllowPersonalAccountSends: 'email_allow_personal_account_sends',
smtpHostOverride: 'smtp_host_override',
smtpPortOverride: 'smtp_port_override',
smtpUserOverride: 'smtp_user_override',
@@ -66,6 +67,12 @@ export interface PortEmailConfig {
smtpPort: number;
smtpUser: string | null;
smtpPass: string | null;
/**
* When false, only the system (port-config) sender identity is allowed.
* When true, admins/users may send via their connected personal email
* account. Defaults to false for safety.
*/
allowPersonalAccountSends: boolean;
}
export async function getPortEmailConfig(portId: string): Promise<PortEmailConfig> {
@@ -79,6 +86,7 @@ export async function getPortEmailConfig(portId: string): Promise<PortEmailConfi
smtpPort,
smtpUser,
smtpPass,
allowPersonalAccountSends,
] = await Promise.all([
readSetting<string>(SETTING_KEYS.emailFromName, portId),
readSetting<string>(SETTING_KEYS.emailFromAddress, portId),
@@ -89,6 +97,7 @@ export async function getPortEmailConfig(portId: string): Promise<PortEmailConfi
readSetting<number>(SETTING_KEYS.smtpPortOverride, portId),
readSetting<string>(SETTING_KEYS.smtpUserOverride, portId),
readSetting<string>(SETTING_KEYS.smtpPassOverride, portId),
readSetting<boolean>(SETTING_KEYS.emailAllowPersonalAccountSends, portId),
]);
// Parse env.SMTP_FROM into name + address if no port override
@@ -114,6 +123,7 @@ export async function getPortEmailConfig(portId: string): Promise<PortEmailConfi
smtpPort: smtpPort ?? env.SMTP_PORT,
smtpUser: smtpUser ?? env.SMTP_USER ?? null,
smtpPass: smtpPass ?? env.SMTP_PASS ?? null,
allowPersonalAccountSends: allowPersonalAccountSends ?? false,
};
}