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:
@@ -1,7 +1,14 @@
|
||||
import { Type } from 'class-transformer';
|
||||
import { IsInt, IsNotEmpty, IsObject, IsOptional, IsString, ValidateNested } from 'class-validator';
|
||||
import { IsArray, IsInt, IsNotEmpty, IsObject, IsOptional, IsString, ValidateNested } from 'class-validator';
|
||||
import { InlineTemplateDto } from '../templates/template.dto';
|
||||
|
||||
/** An email attachment reference — bytes live in the media store under `contentRef`. */
|
||||
export class AttachmentDto {
|
||||
@IsOptional() @IsString() filename?: string;
|
||||
@IsString() @IsNotEmpty() contentRef!: string;
|
||||
@IsOptional() @IsString() mimeType?: string;
|
||||
}
|
||||
|
||||
/** POST /v1/mail/internal — app-to-app mail (no SMTP). Provide EITHER `key` OR `inline`. */
|
||||
export class MailInternalDto {
|
||||
@IsOptional() @IsString() @IsNotEmpty() key?: string;
|
||||
@@ -25,6 +32,7 @@ export class MailSendDto {
|
||||
@IsOptional() @IsString() locale?: string;
|
||||
@IsString() @IsNotEmpty() idempotencyKey!: string;
|
||||
@IsOptional() @IsString() purpose?: string;
|
||||
@IsOptional() @IsArray() @ValidateNested({ each: true }) @Type(() => AttachmentDto) attachments?: AttachmentDto[];
|
||||
/** The registered recipient to mirror to; omit for a pre-registration send (email only). */
|
||||
@IsOptional() @IsString() mirrorToUserId?: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user