feat(mail): email attachments over SMTP
Email can now carry attachments (e.g. an invoice PDF). The kernel already stored
attachments as message parts + the media StoragePort holds the bytes; the gap was
the email envelope.
- EMAIL payload carries attachment REFS ({filename, contentRef, mimeType}), not
bytes — the ledger + T8 PII redaction stay small; bytes are fetched at send time.
- SmtpProvider takes an AttachmentResolver; resolves each ref via storage and attaches
(nodemailer). FAILS CLOSED if a declared attachment can't be resolved (or no resolver
is wired) — never send a receipt/invoice missing its file; a FAILED command retries.
- CapabilityProviderRegistry injects the resolver from STORAGE_PORT (@Optional);
MediaModule exports STORAGE_PORT, CapabilityModule imports MediaModule. No cycle.
- TemplatedSender + MailService + the /v1/mail/send DTO pass attachments through.
Verified: 6 new unit tests (attach, fail-closed x2, plain-unaffected, resolver,
pass-through) + a REAL Ethereal SMTP send WITH a PDF attachment (SENT). Full suite
294/294, boundary + build clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -67,6 +67,16 @@ describe('TemplatedSender.sendTemplated', () => {
|
||||
expect(cmd.renderedHash).toMatch(/^[a-f0-9]{64}$/);
|
||||
});
|
||||
|
||||
it('carries attachment refs into the EMAIL command payload', async () => {
|
||||
await seedReceipt();
|
||||
const cmd = await sender().sendTemplated({
|
||||
source: { key: 'payment.receipt' }, channel: 'EMAIL', target: 'dana@acme.com',
|
||||
vars: { firstName: 'Dana', amount: '$1' }, idempotencyKey: 'att:1',
|
||||
attachments: [{ filename: 'invoice.pdf', contentRef: 'obj/1', mimeType: 'application/pdf' }],
|
||||
});
|
||||
expect((cmd.payload as { attachments: unknown[] }).attachments).toEqual([{ filename: 'invoice.pdf', contentRef: 'obj/1', mimeType: 'application/pdf' }]);
|
||||
});
|
||||
|
||||
it('shapes an SMS send as text only', async () => {
|
||||
await prisma.iiosMessageTemplate.create({
|
||||
data: { key: 'payment.receipt', channel: 'SMS', locale: 'en', version: 1, bodyText: 'Paid {{amount}}', variables: ['amount'] },
|
||||
|
||||
Reference in New Issue
Block a user