fluxmail 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/README.md +1 -1
  2. package/dist/accounts/googleAuth.d.ts.map +1 -1
  3. package/dist/accounts/googleAuth.js +2 -1
  4. package/dist/accounts/googleAuth.js.map +1 -1
  5. package/dist/accounts/registry.d.ts.map +1 -1
  6. package/dist/accounts/registry.js +12 -5
  7. package/dist/accounts/registry.js.map +1 -1
  8. package/dist/accounts/telemetry.d.ts +3 -0
  9. package/dist/accounts/telemetry.d.ts.map +1 -1
  10. package/dist/accounts/telemetry.js +18 -0
  11. package/dist/accounts/telemetry.js.map +1 -1
  12. package/dist/cli.d.ts.map +1 -1
  13. package/dist/cli.js +83 -24
  14. package/dist/cli.js.map +1 -1
  15. package/dist/cliInstances.d.ts +5 -2
  16. package/dist/cliInstances.d.ts.map +1 -1
  17. package/dist/cliInstances.js +74 -22
  18. package/dist/cliInstances.js.map +1 -1
  19. package/dist/cliMail.d.ts +2 -0
  20. package/dist/cliMail.d.ts.map +1 -1
  21. package/dist/cliMail.js +106 -18
  22. package/dist/cliMail.js.map +1 -1
  23. package/dist/cliOutput.d.ts +9 -0
  24. package/dist/cliOutput.d.ts.map +1 -0
  25. package/dist/cliOutput.js +76 -0
  26. package/dist/cliOutput.js.map +1 -0
  27. package/dist/context.js +1 -1
  28. package/dist/context.js.map +1 -1
  29. package/dist/http/admin.d.ts.map +1 -1
  30. package/dist/http/admin.js +2 -0
  31. package/dist/http/admin.js.map +1 -1
  32. package/dist/http/app.d.ts.map +1 -1
  33. package/dist/http/app.js +4 -0
  34. package/dist/http/app.js.map +1 -1
  35. package/dist/http/identity.d.ts.map +1 -1
  36. package/dist/http/identity.js +2 -0
  37. package/dist/http/identity.js.map +1 -1
  38. package/dist/http/rest.d.ts.map +1 -1
  39. package/dist/http/rest.js +250 -48
  40. package/dist/http/rest.js.map +1 -1
  41. package/dist/licensing/entitlements.d.ts +23 -5
  42. package/dist/licensing/entitlements.d.ts.map +1 -1
  43. package/dist/licensing/entitlements.js +101 -22
  44. package/dist/licensing/entitlements.js.map +1 -1
  45. package/dist/licensing/lease.d.ts +1 -1
  46. package/dist/licensing/lease.d.ts.map +1 -1
  47. package/dist/licensing/refresher.d.ts.map +1 -1
  48. package/dist/licensing/refresher.js +5 -2
  49. package/dist/licensing/refresher.js.map +1 -1
  50. package/dist/mcp/buildServer.d.ts.map +1 -1
  51. package/dist/mcp/buildServer.js +230 -78
  52. package/dist/mcp/buildServer.js.map +1 -1
  53. package/dist/mcp/outputSchemas.d.ts +2923 -0
  54. package/dist/mcp/outputSchemas.d.ts.map +1 -0
  55. package/dist/mcp/outputSchemas.js +108 -0
  56. package/dist/mcp/outputSchemas.js.map +1 -0
  57. package/dist/scheduler/sendScheduler.d.ts +2 -0
  58. package/dist/scheduler/sendScheduler.d.ts.map +1 -1
  59. package/dist/scheduler/sendScheduler.js +13 -3
  60. package/dist/scheduler/sendScheduler.js.map +1 -1
  61. package/dist/service/deliveryCoordinator.d.ts +45 -0
  62. package/dist/service/deliveryCoordinator.d.ts.map +1 -0
  63. package/dist/service/deliveryCoordinator.js +296 -0
  64. package/dist/service/deliveryCoordinator.js.map +1 -0
  65. package/dist/service/emailService.d.ts +47 -5
  66. package/dist/service/emailService.d.ts.map +1 -1
  67. package/dist/service/emailService.js +245 -39
  68. package/dist/service/emailService.js.map +1 -1
  69. package/dist/service/publicErrors.d.ts +12 -0
  70. package/dist/service/publicErrors.d.ts.map +1 -0
  71. package/dist/service/publicErrors.js +41 -0
  72. package/dist/service/publicErrors.js.map +1 -0
  73. package/dist/service/searchCursor.d.ts.map +1 -1
  74. package/dist/service/searchCursor.js +2 -1
  75. package/dist/service/searchCursor.js.map +1 -1
  76. package/dist/storage/db.d.ts +252 -2
  77. package/dist/storage/db.d.ts.map +1 -1
  78. package/dist/storage/db.js +57 -1
  79. package/dist/storage/db.js.map +1 -1
  80. package/dist/storage/members.d.ts.map +1 -1
  81. package/dist/storage/members.js +3 -1
  82. package/dist/storage/members.js.map +1 -1
  83. package/dist/storage/restIdempotency.d.ts +6 -0
  84. package/dist/storage/restIdempotency.d.ts.map +1 -1
  85. package/dist/storage/restIdempotency.js +8 -0
  86. package/dist/storage/restIdempotency.js.map +1 -1
  87. package/dist/storage/scheduledSends.d.ts +2 -1
  88. package/dist/storage/scheduledSends.d.ts.map +1 -1
  89. package/dist/storage/scheduledSends.js +6 -0
  90. package/dist/storage/scheduledSends.js.map +1 -1
  91. package/package.json +5 -5
@@ -4,6 +4,7 @@ import { type Entitlements } from '../licensing/entitlements.js';
4
4
  import type { FluxmailDb } from '../storage/db.js';
5
5
  import type { Principal } from '../auth.js';
6
6
  import { type ScheduledSendStatus } from '../storage/scheduledSends.js';
7
+ import { type DeliveryOperation } from './deliveryCoordinator.js';
7
8
  import { type ConfiguredSendAsInput } from '../storage/sendAs.js';
8
9
  export interface SendInput extends DraftInput {
9
10
  /** With replyToMessageId: compute recipients from the original (reply-all semantics). */
@@ -41,6 +42,7 @@ export interface ServiceStatus {
41
42
  }
42
43
  export interface ScheduledSendInfo {
43
44
  scheduleId: string;
45
+ operationId?: string;
44
46
  accountId: string;
45
47
  draftId: string;
46
48
  /** ISO 8601 UTC. */
@@ -79,6 +81,30 @@ export interface BatchSearchResult {
79
81
  groups: BatchSearchGroup[];
80
82
  exhausted: boolean;
81
83
  }
84
+ export interface ModifyResult {
85
+ action: ModifyAction;
86
+ succeededIds: string[];
87
+ failed: Array<{
88
+ messageId: string;
89
+ code: string;
90
+ }>;
91
+ uncertainIds: string[];
92
+ }
93
+ export interface SendPreview {
94
+ accountId: string;
95
+ from: string;
96
+ to: EmailAddress[];
97
+ cc: EmailAddress[];
98
+ bcc: EmailAddress[];
99
+ subject: string;
100
+ attachments: Array<{
101
+ filename: string;
102
+ mimeType: string;
103
+ sizeBytes: number;
104
+ }>;
105
+ bodyTextChars: number;
106
+ bodyHtmlChars: number;
107
+ }
82
108
  /** Parse and validate a sendAt timestamp; returns epoch ms. */
83
109
  export declare function resolveSendAt(sendAtIso: string, now?: number): number;
84
110
  /**
@@ -90,11 +116,12 @@ export declare class EmailService {
90
116
  private readonly registry;
91
117
  private readonly db;
92
118
  private readonly principal?;
119
+ private readonly maxAttachmentBytes;
93
120
  /** Wired to SendScheduler.wake() in long-lived processes; a no-op for one-shot CLI commands. */
94
121
  onScheduleChanged: () => void;
95
122
  private readonly cursorSecret;
96
123
  private readonly cursorCodec;
97
- constructor(registry: AccountRegistry, db: FluxmailDb, principal?: Principal | undefined, cursorSecret?: Buffer);
124
+ constructor(registry: AccountRegistry, db: FluxmailDb, principal?: Principal | undefined, cursorSecret?: Buffer, maxAttachmentBytes?: number);
98
125
  /**
99
126
  * A view restricted to a member's mailbox grants and optional connection
100
127
  * allowlist. Internal background workers use the default service instead.
@@ -118,9 +145,9 @@ export declare class EmailService {
118
145
  listAccounts(): Account[];
119
146
  status(): Promise<ServiceStatus>;
120
147
  /**
121
- * Gate for MCP tool calls: throws once a lapsed license leaves the instance
122
- * over the entitled caps; returns a renewal warning to attach to results
123
- * while the license is in its grace period or has lapsed.
148
+ * Gate for MCP tool calls: throws once the instance is over the entitled
149
+ * caps, after a lapse or once a cap reduction's grace period ends; returns a
150
+ * warning to attach to results while a grace period runs or after a lapse.
124
151
  */
125
152
  enforceQuota(): string | undefined;
126
153
  private scheduledStatus;
@@ -135,6 +162,19 @@ export declare class EmailService {
135
162
  createDraft(accountId: string | undefined, d: SendInput): Promise<Message>;
136
163
  updateDraft(accountId: string | undefined, draftId: string, d: SendInput): Promise<Message>;
137
164
  deleteDraft(accountId: string | undefined, draftId: string): Promise<void>;
165
+ getDraft(accountId: string | undefined, draftId: string): Promise<Message>;
166
+ previewSend(accountId: string | undefined, input: SendInput | {
167
+ draftId: string;
168
+ }): Promise<SendPreview>;
169
+ getDelivery(accountId: string, operationId: string): DeliveryOperation;
170
+ deliver(accountId: string | undefined, input: SendInput | {
171
+ draftId: string;
172
+ }, key: string): Promise<DeliveryOperation>;
173
+ deliverForward(accountId: string | undefined, input: ForwardInput, key: string): Promise<DeliveryOperation>;
174
+ deliverScheduled(accountId: string, draftId: string, scheduleId: string): Promise<DeliveryOperation>;
175
+ scheduleDelivery(accountId: string | undefined, input: SendInput | {
176
+ draftId: string;
177
+ }, sendAt: string, key: string): Promise<DeliveryOperation>;
138
178
  send(accountId: string | undefined, input: SendInput | {
139
179
  draftId: string;
140
180
  }): Promise<SendResult>;
@@ -145,6 +185,7 @@ export declare class EmailService {
145
185
  scheduleSend(accountId: string | undefined, input: SendInput | {
146
186
  draftId: string;
147
187
  }, sendAtIso: string): Promise<ScheduledSendInfo>;
188
+ private createSchedule;
148
189
  listScheduled(accountId?: string): ScheduledSendInfo[];
149
190
  /** Cancels a pending schedule; the provider draft is kept. */
150
191
  cancelScheduled(scheduleId: string): {
@@ -162,11 +203,12 @@ export declare class EmailService {
162
203
  private validateSender;
163
204
  private selectReplySender;
164
205
  forward(accountId: string | undefined, input: ForwardInput): Promise<SendResult>;
165
- modify(accountId: string | undefined, ids: string[], action: ModifyAction): Promise<void>;
206
+ modify(accountId: string | undefined, ids: string[], action: ModifyAction): Promise<ModifyResult>;
166
207
  getAttachment(accountId: string | undefined, messageId: string, attachmentId: string, maxBytes?: number): Promise<{
167
208
  meta: AttachmentMeta;
168
209
  content: Buffer;
169
210
  }>;
211
+ private assertMailContent;
170
212
  }
171
213
  export declare function buildForwardBody(original: Message, comment?: string): {
172
214
  text?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"emailService.d.ts","sourceRoot":"","sources":["../../src/service/emailService.ts"],"names":[],"mappings":"AAAA,OAAO,EAUL,KAAK,OAAO,EAEZ,KAAK,cAAc,EACnB,KAAK,UAAU,EACf,KAAK,YAAY,EACjB,KAAK,UAAU,EACf,KAAK,MAAM,EACX,KAAK,KAAK,EACV,KAAK,OAAO,EACZ,KAAK,iBAAiB,EACtB,KAAK,YAAY,EACjB,KAAK,QAAQ,EACb,KAAK,kBAAkB,EAEvB,KAAK,UAAU,EACf,KAAK,cAAc,EACnB,KAAK,MAAM,EACZ,MAAM,gBAAgB,CAAC;AAExB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC/D,OAAO,EAAwC,KAAK,YAAY,EAAE,MAAM,8BAA8B,CAAC;AACvG,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C,OAAO,EASL,KAAK,mBAAmB,EACzB,MAAM,8BAA8B,CAAC;AAEtC,OAAO,EAAiD,KAAK,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAEjH,MAAM,WAAW,SAAU,SAAQ,UAAU;IAC3C,yFAAyF;IACzF,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,EAAE,EAAE,YAAY,EAAE,CAAC;IACnB,EAAE,CAAC,EAAE,YAAY,EAAE,CAAC;IACpB,wDAAwD;IACxD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,KAAK,CACb,IAAI,CAAC,OAAO,EAAE,IAAI,GAAG,UAAU,GAAG,OAAO,GAAG,QAAQ,GAAG,eAAe,CAAC,GAAG;QACxE,KAAK,CAAC,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,OAAO,EAAE,MAAM,CAAA;SAAE,CAAC;QAC1C,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;KACrB,CACF,CAAC;IACF,gEAAgE;IAChE,OAAO,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5B,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,mEAAmE;IACnE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,kBAAkB,EAAE,MAAM,EAAE,CAAC;IAC7B,SAAS,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CACrD;AAED,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,oBAAoB;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,mBAAmB,CAAC;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,kBAAkB,EAAE,CAAC;IAC/B,KAAK,EAAE,kBAAkB,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,SAAS,EAAE,KAAK,CAAC;CAClB;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,iBAAiB,CAAC;IACzB,KAAK,CAAC,EAAE,gBAAgB,CAAC;CAC1B;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,gBAAgB,EAAE,CAAC;IAC3B,SAAS,EAAE,OAAO,CAAC;CACpB;AAkED,+DAA+D;AAC/D,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,SAAa,GAAG,MAAM,CAazE;AAkBD;;;;GAIG;AACH,qBAAa,YAAY;IAQrB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,EAAE;IACnB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC;IAT7B,gGAAgG;IAChG,iBAAiB,EAAE,MAAM,IAAI,CAAY;IAEzC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAoB;gBAG7B,QAAQ,EAAE,eAAe,EACzB,EAAE,EAAE,UAAU,EACd,SAAS,CAAC,EAAE,SAAS,YAAA,EACtC,YAAY,CAAC,EAAE,MAAM;IAMvB;;;OAGG;IACH,aAAa,CAAC,SAAS,EAAE,SAAS,GAAG,YAAY;IAMjD,6FAA6F;IAC7F,OAAO,CAAC,UAAU;IAIlB,OAAO,CAAC,SAAS;IAIjB,OAAO,CAAC,kBAAkB;IAK1B,OAAO,CAAC,gBAAgB;IAMxB;;;;OAIG;IACH,OAAO,CAAC,sBAAsB;IAwB9B,4DAA4D;IAC5D,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAI5C,kFAAkF;YACpE,YAAY;IAqB1B,YAAY,IAAI,OAAO,EAAE;IAInB,MAAM,IAAI,OAAO,CAAC,aAAa,CAAC;IA8DtC;;;;OAIG;IACH,YAAY,IAAI,MAAM,GAAG,SAAS;IAelC,OAAO,CAAC,eAAe;IAmBvB,WAAW,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAIlD,UAAU,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC;IAIhD,UAAU,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;IAMnD,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,qBAAqB,EAAE,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;IAsChG,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,EAAE,CAAC,EAAE,UAAU,EAAE,IAAI,GAAE,QAAa,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAiF3G,mBAAmB,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,iBAAiB,CAAC;IA4D9E,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAIvE,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAI3E,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,EAAE,CAAC,EAAE,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC;IAM1E,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC;IAM3F,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI1E,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,EAAE,KAAK,EAAE,SAAS,GAAG;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,UAAU,CAAC;IAoBhG;;;OAGG;IACG,YAAY,CAChB,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,KAAK,EAAE,SAAS,GAAG;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,EACtC,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,iBAAiB,CAAC;IA6B7B,aAAa,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,iBAAiB,EAAE;IAWtD,8DAA8D;IAC9D,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,IAAI,CAAA;KAAE;IAgB7F;;;;OAIG;YACW,iBAAiB;YA+BjB,gBAAgB;YAkChB,cAAc;IAwB5B,OAAO,CAAC,iBAAiB;IAWzB,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,EAAE,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,UAAU,CAAC;IA+BhF,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAIzF,aAAa,CACX,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,EACpB,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC;QAAE,IAAI,EAAE,cAAc,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;CAGtD;AAED,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAwBtG"}
1
+ {"version":3,"file":"emailService.d.ts","sourceRoot":"","sources":["../../src/service/emailService.ts"],"names":[],"mappings":"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"}
@@ -1,10 +1,13 @@
1
- import { computeReplyRecipients, EmailError, formatAddressList, forwardSubject, isEmailError, isPortableFolderRole, normalizeEmailQuery, parseSingleAddress, supportsPortableEmailQuery, } from '@fluxmail/core';
1
+ import { computeReplyRecipients, EmailError, formatAddressList, forwardSubject, isEmailError, isPortableFolderRole, normalizeEmailQuery, parseSingleAddress, replySubject, supportsPortableEmailQuery, } from '@fluxmail/core';
2
2
  import { randomBytes } from 'node:crypto';
3
3
  import { assertWithinQuota, checkLicenseState } from '../licensing/entitlements.js';
4
4
  import { listMembers } from '../storage/members.js';
5
5
  import { canAccessAccount, canAdminister, canSeeAccountMetadata } from '../authorization.js';
6
6
  import { cancelScheduledSend, countPending, createScheduledSend, findPendingByDraft, getScheduledSend, listScheduledSends, markSent, } from '../storage/scheduledSends.js';
7
7
  import { SearchCursorCodec } from './searchCursor.js';
8
+ import { DeliveryCoordinator, isDefiniteDeliveryFailure } from './deliveryCoordinator.js';
9
+ import { ClientInputError } from './publicErrors.js';
10
+ import { DEFAULT_MAX_ATTACHMENT_BYTES } from '../config.js';
8
11
  import { listConfiguredSendAs, replaceConfiguredSendAs } from '../storage/sendAs.js';
9
12
  const SCHEDULE_GRACE_MS = 60_000;
10
13
  const SCHEDULE_MAX_HORIZON_MS = 365 * 24 * 3_600_000;
@@ -64,13 +67,13 @@ export function resolveSendAt(sendAtIso, now = Date.now()) {
64
67
  // Require an ISO 8601 shape: Date.parse alone is lenient enough to accept junk.
65
68
  const sendAt = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/.test(sendAtIso) ? Date.parse(sendAtIso) : NaN;
66
69
  if (Number.isNaN(sendAt)) {
67
- throw new EmailError('invalid_request', `Could not parse sendAt: "${sendAtIso}" (expected ISO 8601)`);
70
+ throw new ClientInputError('invalid_request', `Could not parse sendAt: "${sendAtIso}" (expected ISO 8601)`);
68
71
  }
69
72
  if (sendAt < now - SCHEDULE_GRACE_MS) {
70
- throw new EmailError('invalid_request', 'sendAt is in the past; leave it out to send now');
73
+ throw new ClientInputError('invalid_request', 'sendAt is in the past; leave it out to send now');
71
74
  }
72
75
  if (sendAt > now + SCHEDULE_MAX_HORIZON_MS) {
73
- throw new EmailError('invalid_request', 'sendAt is more than a year away');
76
+ throw new ClientInputError('invalid_request', 'sendAt is more than a year away');
74
77
  }
75
78
  return sendAt;
76
79
  }
@@ -98,14 +101,16 @@ export class EmailService {
98
101
  registry;
99
102
  db;
100
103
  principal;
104
+ maxAttachmentBytes;
101
105
  /** Wired to SendScheduler.wake() in long-lived processes; a no-op for one-shot CLI commands. */
102
106
  onScheduleChanged = () => { };
103
107
  cursorSecret;
104
108
  cursorCodec;
105
- constructor(registry, db, principal, cursorSecret) {
109
+ constructor(registry, db, principal, cursorSecret, maxAttachmentBytes = DEFAULT_MAX_ATTACHMENT_BYTES) {
106
110
  this.registry = registry;
107
111
  this.db = db;
108
112
  this.principal = principal;
113
+ this.maxAttachmentBytes = maxAttachmentBytes;
109
114
  this.cursorSecret = cursorSecret ?? randomBytes(32);
110
115
  this.cursorCodec = new SearchCursorCodec(this.cursorSecret);
111
116
  }
@@ -114,7 +119,7 @@ export class EmailService {
114
119
  * allowlist. Internal background workers use the default service instead.
115
120
  */
116
121
  withPrincipal(principal) {
117
- const scoped = new EmailService(this.registry, this.db, principal, this.cursorSecret);
122
+ const scoped = new EmailService(this.registry, this.db, principal, this.cursorSecret, this.maxAttachmentBytes);
118
123
  scoped.onScheduleChanged = () => this.onScheduleChanged();
119
124
  return scoped;
120
125
  }
@@ -151,10 +156,10 @@ export class EmailService {
151
156
  return this.registry.resolveAccountId();
152
157
  const accessible = this.accessibleAccounts();
153
158
  if (accessible.length === 0) {
154
- throw new EmailError('invalid_request', 'No email accounts are available for this member.');
159
+ throw new ClientInputError('invalid_request', 'No email accounts are available for this member.');
155
160
  }
156
161
  if (accessible.length > 1) {
157
- throw new EmailError('invalid_request', `Multiple accounts are available; specify accountId. Available: ${accessible
162
+ throw new ClientInputError('invalid_request', `Multiple accounts are available; specify accountId. Available: ${accessible
158
163
  .map((a) => `${a.id} (${a.email})`)
159
164
  .join(', ')}`);
160
165
  }
@@ -169,7 +174,7 @@ export class EmailService {
169
174
  const resolvedId = this.resolveScopedAccountId(accountId);
170
175
  const account = this.registry.getAccount(resolvedId);
171
176
  if (account.status === 'disabled') {
172
- throw new EmailError('invalid_request', `Account ${resolvedId} is disabled`);
177
+ throw new ClientInputError('invalid_request', `Account ${resolvedId} is disabled`);
173
178
  }
174
179
  try {
175
180
  const result = await fn(this.registry.getProvider(resolvedId), resolvedId, account);
@@ -240,15 +245,17 @@ export class EmailService {
240
245
  };
241
246
  }
242
247
  /**
243
- * Gate for MCP tool calls: throws once a lapsed license leaves the instance
244
- * over the entitled caps; returns a renewal warning to attach to results
245
- * while the license is in its grace period or has lapsed.
248
+ * Gate for MCP tool calls: throws once the instance is over the entitled
249
+ * caps, after a lapse or once a cap reduction's grace period ends; returns a
250
+ * warning to attach to results while a grace period runs or after a lapse.
246
251
  */
247
252
  enforceQuota() {
248
253
  if (!this.isInternal() && (!this.principal || !canAdminister(this.principal, 'admin.license'))) {
249
254
  const state = checkLicenseState(this.db);
250
- if (state.overQuota) {
251
- throw new EmailError('entitlement_exceeded', 'This Fluxmail instance is over its plan limits. Ask an administrator to renew the license or reduce usage.');
255
+ if (state.blocked) {
256
+ throw new EmailError('entitlement_exceeded', 'This Fluxmail instance is over its plan limits. Ask an administrator to ' +
257
+ (state.entitlements.licensed ? 'upgrade the plan' : 'renew the license') +
258
+ ' or reduce usage.');
252
259
  }
253
260
  // License dates and renewal state are instance administration details.
254
261
  return undefined;
@@ -296,14 +303,14 @@ export class EmailService {
296
303
  const normalized = identities.map((identity) => {
297
304
  const parsed = parseSingleAddress(identity.email.trim());
298
305
  if (!parsed)
299
- throw new EmailError('invalid_request', `Could not parse sender address: "${identity.email}"`);
306
+ throw new ClientInputError('invalid_request', `Could not parse sender address: "${identity.email}"`);
300
307
  const email = parsed.email;
301
308
  const key = email.toLowerCase();
302
309
  if (key === account.email.toLowerCase()) {
303
- throw new EmailError('invalid_request', 'Do not include the connected account address in configured aliases.');
310
+ throw new ClientInputError('invalid_request', 'Do not include the connected account address in configured aliases.');
304
311
  }
305
312
  if (seen.has(key))
306
- throw new EmailError('invalid_request', `Duplicate sender address: "${email}"`);
313
+ throw new ClientInputError('invalid_request', `Duplicate sender address: "${email}"`);
307
314
  seen.add(key);
308
315
  const name = identity.name?.trim();
309
316
  return { email, ...(name ? { name } : {}) };
@@ -322,13 +329,13 @@ export class EmailService {
322
329
  async listMessages(accountId, q, page = {}) {
323
330
  const normalized = normalizeEmailQuery(q);
324
331
  if (!normalized.success) {
325
- throw new EmailError('invalid_request', normalized.diagnostics.map((item) => item.message).join(' '), {
332
+ throw new ClientInputError('invalid_request', normalized.diagnostics.map((item) => item.message).join(' '), {
326
333
  diagnostics: normalized.diagnostics,
327
334
  });
328
335
  }
329
336
  const query = normalized.query;
330
337
  if (page.includeSearchContext && !query.text) {
331
- throw new EmailError('invalid_request', 'includeSearchContext requires a portable text query.');
338
+ throw new ClientInputError('invalid_request', 'includeSearchContext requires a portable text query.');
332
339
  }
333
340
  const pageSize = Math.min(Math.max(page.pageSize ?? 25, 1), 100);
334
341
  const ownsController = page.signal === undefined;
@@ -407,23 +414,23 @@ export class EmailService {
407
414
  }
408
415
  async searchMessagesBatch(input) {
409
416
  if (input.accounts.length < 1 || input.accounts.length > 20) {
410
- throw new EmailError('invalid_request', 'Batch search requires between 1 and 20 accounts.');
417
+ throw new ClientInputError('invalid_request', 'Batch search requires between 1 and 20 accounts.');
411
418
  }
412
419
  const seen = new Set();
413
420
  for (const account of input.accounts) {
414
421
  if (seen.has(account.accountId))
415
- throw new EmailError('invalid_request', 'Batch account IDs must be distinct.');
422
+ throw new ClientInputError('invalid_request', 'Batch account IDs must be distinct.');
416
423
  seen.add(account.accountId);
417
424
  }
418
425
  const normalized = normalizeEmailQuery(input.query);
419
426
  if (!normalized.success) {
420
- throw new EmailError('invalid_request', normalized.diagnostics.map((item) => item.message).join(' '), {
427
+ throw new ClientInputError('invalid_request', normalized.diagnostics.map((item) => item.message).join(' '), {
421
428
  diagnostics: normalized.diagnostics,
422
429
  });
423
430
  }
424
431
  const pageSize = Math.min(Math.max(input.pageSize ?? 25, 1), 100);
425
432
  if (input.includeSearchContext && !normalized.query.text) {
426
- throw new EmailError('invalid_request', 'includeSearchContext requires a portable text query.');
433
+ throw new ClientInputError('invalid_request', 'includeSearchContext requires a portable text query.');
427
434
  }
428
435
  const controller = new AbortController();
429
436
  const startedAt = Date.now();
@@ -472,12 +479,14 @@ export class EmailService {
472
479
  getThread(accountId, threadId) {
473
480
  return this.withProvider(accountId, (p) => p.getThread(threadId));
474
481
  }
475
- createDraft(accountId, d) {
482
+ async createDraft(accountId, d) {
483
+ this.assertMailContent(d);
476
484
  return this.withProvider(accountId, async (p, _id, account) => {
477
485
  return p.createDraft(await this.resolveRecipients(p, account, d));
478
486
  });
479
487
  }
480
- updateDraft(accountId, draftId, d) {
488
+ async updateDraft(accountId, draftId, d) {
489
+ this.assertMailContent(d);
481
490
  return this.withProvider(accountId, async (p, _id, account) => {
482
491
  return p.updateDraft(draftId, await this.resolveRecipients(p, account, d));
483
492
  });
@@ -485,7 +494,125 @@ export class EmailService {
485
494
  deleteDraft(accountId, draftId) {
486
495
  return this.withProvider(accountId, (p) => p.deleteDraft(draftId));
487
496
  }
488
- send(accountId, input) {
497
+ getDraft(accountId, draftId) {
498
+ return this.withProvider(accountId, (p) => p.getDraft(draftId));
499
+ }
500
+ previewSend(accountId, input) {
501
+ return this.withProvider(accountId, async (provider, resolvedId, account) => {
502
+ if ('draftId' in input) {
503
+ const draft = await provider.getDraft(input.draftId);
504
+ if (draft.from?.email && draft.from.email.toLowerCase() !== account.email.toLowerCase()) {
505
+ await this.validateSender(provider, resolvedId, account, draft.from.email);
506
+ }
507
+ return {
508
+ accountId: resolvedId,
509
+ from: draft.from?.email ?? account.email,
510
+ to: draft.to,
511
+ cc: draft.cc ?? [],
512
+ bcc: draft.bcc ?? [],
513
+ subject: draft.subject,
514
+ attachments: (draft.attachments ?? []).map(({ filename, mimeType, sizeBytes }) => ({
515
+ filename,
516
+ mimeType,
517
+ sizeBytes,
518
+ })),
519
+ bodyTextChars: draft.body?.text?.length ?? 0,
520
+ bodyHtmlChars: draft.body?.html?.length ?? 0,
521
+ };
522
+ }
523
+ this.assertMailContent(input);
524
+ const resolved = await this.resolveRecipients(provider, account, input);
525
+ const subject = resolved.subject ??
526
+ (input.replyToMessageId ? replySubject((await provider.getMessage(input.replyToMessageId)).subject) : '');
527
+ return {
528
+ accountId: resolvedId,
529
+ from: resolved.from ?? account.email,
530
+ to: resolved.to ?? [],
531
+ cc: resolved.cc ?? [],
532
+ bcc: resolved.bcc ?? [],
533
+ subject,
534
+ attachments: (resolved.attachments ?? []).map(({ filename, mimeType, content }) => ({
535
+ filename,
536
+ mimeType,
537
+ sizeBytes: Buffer.byteLength(content, 'base64'),
538
+ })),
539
+ bodyTextChars: resolved.body.text?.length ?? 0,
540
+ bodyHtmlChars: resolved.body.html?.length ?? 0,
541
+ };
542
+ });
543
+ }
544
+ getDelivery(accountId, operationId) {
545
+ this.assertAccountAccess(accountId);
546
+ if (!this.principal)
547
+ throw new EmailError('permission_denied', 'A member session is required.');
548
+ return new DeliveryCoordinator(this.db).get(this.principal.principalId, this.principal.memberId, accountId, operationId);
549
+ }
550
+ deliver(accountId, input, key) {
551
+ const resolvedId = this.resolveScopedAccountId(accountId);
552
+ if (!this.principal)
553
+ throw new EmailError('permission_denied', 'A member session is required.');
554
+ return new DeliveryCoordinator(this.db).run({
555
+ principalId: this.principal.principalId,
556
+ memberId: this.principal.memberId,
557
+ accountId: resolvedId,
558
+ key,
559
+ kind: 'send',
560
+ request: input,
561
+ }, () => this.send(resolvedId, input));
562
+ }
563
+ deliverForward(accountId, input, key) {
564
+ const resolvedId = this.resolveScopedAccountId(accountId);
565
+ if (!this.principal)
566
+ throw new EmailError('permission_denied', 'A member session is required.');
567
+ return new DeliveryCoordinator(this.db).run({
568
+ principalId: this.principal.principalId,
569
+ memberId: this.principal.memberId,
570
+ accountId: resolvedId,
571
+ key,
572
+ kind: 'forward',
573
+ request: input,
574
+ }, () => this.forward(resolvedId, input));
575
+ }
576
+ deliverScheduled(accountId, draftId, scheduleId) {
577
+ return new DeliveryCoordinator(this.db).fireScheduled(accountId, draftId, scheduleId, () => this.withProvider(accountId, async (provider, resolvedId, account) => {
578
+ const draft = await provider.getDraft(draftId);
579
+ if (draft.from?.email && draft.from.email.toLowerCase() !== account.email.toLowerCase()) {
580
+ await this.validateSender(provider, resolvedId, account, draft.from.email);
581
+ }
582
+ }), () => this.withProvider(accountId, async (provider, resolvedId) => {
583
+ const result = await provider.send({ draftId });
584
+ const pending = findPendingByDraft(this.db, resolvedId, draftId);
585
+ if (pending) {
586
+ markSent(this.db, pending.id, result);
587
+ this.onScheduleChanged();
588
+ }
589
+ return result;
590
+ }));
591
+ }
592
+ scheduleDelivery(accountId, input, sendAt, key) {
593
+ const resolvedId = this.resolveScopedAccountId(accountId);
594
+ if (!this.principal)
595
+ throw new EmailError('permission_denied', 'A member session is required.');
596
+ let createdSchedule = false;
597
+ const scheduled = new DeliveryCoordinator(this.db).schedule({
598
+ principalId: this.principal.principalId,
599
+ memberId: this.principal.memberId,
600
+ accountId: resolvedId,
601
+ key,
602
+ request: { input, sendAt },
603
+ }, async () => {
604
+ const result = await this.createSchedule(resolvedId, input, sendAt);
605
+ createdSchedule = true;
606
+ return result;
607
+ });
608
+ return scheduled.finally(() => {
609
+ if (createdSchedule)
610
+ this.onScheduleChanged();
611
+ });
612
+ }
613
+ async send(accountId, input) {
614
+ if (!('draftId' in input))
615
+ this.assertMailContent(input);
489
616
  return this.withProvider(accountId, async (p, resolvedId, account) => {
490
617
  if ('draftId' in input) {
491
618
  const draft = await p.getDraft(input.draftId);
@@ -509,6 +636,13 @@ export class EmailService {
509
636
  * immediately; only the schedule (draft id + fire time) is stored locally.
510
637
  */
511
638
  async scheduleSend(accountId, input, sendAtIso) {
639
+ const scheduled = await this.createSchedule(accountId, input, sendAtIso);
640
+ this.onScheduleChanged();
641
+ return scheduled;
642
+ }
643
+ async createSchedule(accountId, input, sendAtIso) {
644
+ if (!('draftId' in input))
645
+ this.assertMailContent(input);
512
646
  const sendAt = resolveSendAt(sendAtIso);
513
647
  const { draft, resolvedId } = await this.withProvider(accountId, async (p, id, account) => {
514
648
  let message;
@@ -518,7 +652,7 @@ export class EmailService {
518
652
  else {
519
653
  const resolved = await this.resolveRecipients(p, account, input);
520
654
  if (!resolved.to?.length && !resolved.cc?.length && !resolved.bcc?.length) {
521
- throw new EmailError('invalid_request', 'Cannot schedule a message with no recipients');
655
+ throw new ClientInputError('invalid_request', 'Cannot schedule a message with no recipients');
522
656
  }
523
657
  message = await p.createDraft(resolved);
524
658
  }
@@ -534,19 +668,23 @@ export class EmailService {
534
668
  ...(draft.subject !== undefined ? { subject: draft.subject } : {}),
535
669
  ...(draft.to?.length ? { toRecipients: formatAddressList(draft.to) } : {}),
536
670
  });
537
- this.onScheduleChanged();
538
671
  return toScheduledInfo(row);
539
672
  }
540
673
  listScheduled(accountId) {
674
+ const coordinator = new DeliveryCoordinator(this.db);
675
+ const withOperation = (row) => {
676
+ const operationId = coordinator.findScheduled(row.accountId, row.id)?.operationId;
677
+ return { ...toScheduledInfo(row), ...(operationId ? { operationId } : {}) };
678
+ };
541
679
  if (accountId !== undefined) {
542
- return listScheduledSends(this.db, this.resolveScopedAccountId(accountId)).map(toScheduledInfo);
680
+ return listScheduledSends(this.db, this.resolveScopedAccountId(accountId)).map(withOperation);
543
681
  }
544
682
  // Listing must work across accounts, but a member key only sees its own mailboxes'.
545
683
  const rows = listScheduledSends(this.db);
546
684
  if (this.isInternal())
547
- return rows.map(toScheduledInfo);
685
+ return rows.map(withOperation);
548
686
  const accessible = new Set(this.accessibleAccounts().map((a) => a.id));
549
- return rows.filter((r) => accessible.has(r.accountId)).map(toScheduledInfo);
687
+ return rows.filter((r) => accessible.has(r.accountId)).map(withOperation);
550
688
  }
551
689
  /** Cancels a pending schedule; the provider draft is kept. */
552
690
  cancelScheduled(scheduleId) {
@@ -556,11 +694,12 @@ export class EmailService {
556
694
  throw new EmailError('not_found', `No scheduled send with id ${scheduleId}`);
557
695
  }
558
696
  if (row.status !== 'pending') {
559
- throw new EmailError('invalid_request', `Scheduled send ${scheduleId} is already ${row.status}`);
697
+ throw new ClientInputError('invalid_request', `Scheduled send ${scheduleId} is already ${row.status}`);
560
698
  }
561
699
  if (!cancelScheduledSend(this.db, scheduleId)) {
562
- throw new EmailError('invalid_request', `Scheduled send ${scheduleId} has already started sending`);
700
+ throw new ClientInputError('invalid_request', `Scheduled send ${scheduleId} has already started sending`);
563
701
  }
702
+ new DeliveryCoordinator(this.db).cancelScheduled(row.accountId, scheduleId);
564
703
  this.onScheduleChanged();
565
704
  return { scheduleId, draftId: row.draftId, draftKept: true };
566
705
  }
@@ -624,7 +763,7 @@ export class EmailService {
624
763
  async validateSender(provider, accountId, account, sender, known) {
625
764
  const parsed = parseSingleAddress(sender.trim());
626
765
  if (!parsed)
627
- throw new EmailError('invalid_request', `Could not parse sender address: "${sender}"`);
766
+ throw new ClientInputError('invalid_request', `Could not parse sender address: "${sender}"`);
628
767
  if (parsed.email.toLowerCase() === account.email.toLowerCase()) {
629
768
  return {
630
769
  email: account.email,
@@ -635,7 +774,7 @@ export class EmailService {
635
774
  }
636
775
  const identity = (known ?? (await this.sendAsIdentities(provider, accountId, account))).find((candidate) => candidate.email.toLowerCase() === parsed.email.toLowerCase());
637
776
  if (!identity)
638
- throw new EmailError('invalid_request', `Sender address "${parsed.email}" is not available.`);
777
+ throw new ClientInputError('invalid_request', `Sender address "${parsed.email}" is not available.`);
639
778
  return identity;
640
779
  }
641
780
  selectReplySender(original, identities) {
@@ -650,13 +789,22 @@ export class EmailService {
650
789
  }
651
790
  return identities.find((identity) => identity.isPrimary) ?? identities[0];
652
791
  }
653
- forward(accountId, input) {
792
+ async forward(accountId, input) {
793
+ if (Buffer.byteLength(input.comment ?? '', 'utf8') > 1024 * 1024) {
794
+ throw new ClientInputError('invalid_request', 'Forward comment exceeds the 1 MiB body limit.');
795
+ }
654
796
  return this.withProvider(accountId, async (p, resolvedId, account) => {
655
797
  const original = await p.getMessage(input.messageId);
656
798
  const includeAttachments = input.includeAttachments ?? true;
657
799
  const attachments = [];
658
800
  if (includeAttachments) {
659
- for (const meta of original.attachments ?? []) {
801
+ const metadata = original.attachments ?? [];
802
+ if (metadata.length > 20)
803
+ throw new ClientInputError('invalid_request', 'A message may have at most 20 attachments.');
804
+ if (metadata.reduce((total, item) => total + item.sizeBytes, 0) > this.maxAttachmentBytes) {
805
+ throw new ClientInputError('invalid_request', 'Combined attachments exceed the configured size limit.');
806
+ }
807
+ for (const meta of metadata) {
660
808
  const { content } = await p.getAttachment(original.id, meta.id);
661
809
  attachments.push({
662
810
  filename: meta.filename,
@@ -675,15 +823,73 @@ export class EmailService {
675
823
  body: buildForwardBody(original, input.comment),
676
824
  ...(attachments.length ? { attachments } : {}),
677
825
  };
826
+ this.assertMailContent(draft);
678
827
  return p.send(draft);
679
828
  });
680
829
  }
681
- modify(accountId, ids, action) {
682
- return this.withProvider(accountId, (p) => p.modify(ids, action));
830
+ async modify(accountId, ids, action) {
831
+ const unique = [...new Set(ids)];
832
+ if (unique.length < 1 || unique.length > 100) {
833
+ throw new ClientInputError('invalid_request', 'Modify between 1 and 100 distinct messages.');
834
+ }
835
+ const resolvedId = this.resolveScopedAccountId(accountId);
836
+ const result = await this.withProvider(resolvedId, async (provider) => {
837
+ const outcomes = Array.from({ length: unique.length }, () => 'uncertain');
838
+ let next = 0;
839
+ const worker = async () => {
840
+ for (;;) {
841
+ const index = next++;
842
+ if (index >= unique.length)
843
+ return;
844
+ try {
845
+ await provider.modify([unique[index]], action);
846
+ outcomes[index] = 'succeeded';
847
+ }
848
+ catch (error) {
849
+ outcomes[index] = isDefiniteDeliveryFailure(error)
850
+ ? { code: isEmailError(error) ? error.code : 'internal' }
851
+ : 'uncertain';
852
+ }
853
+ }
854
+ };
855
+ await Promise.all(Array.from({ length: Math.min(4, unique.length) }, () => worker()));
856
+ return {
857
+ action,
858
+ succeededIds: unique.filter((_, index) => outcomes[index] === 'succeeded'),
859
+ failed: unique.flatMap((messageId, index) => {
860
+ const outcome = outcomes[index];
861
+ return typeof outcome === 'object' ? [{ messageId, code: outcome.code }] : [];
862
+ }),
863
+ uncertainIds: unique.filter((_, index) => outcomes[index] === 'uncertain'),
864
+ };
865
+ });
866
+ if (result.failed.some(({ code }) => code === 'auth_expired')) {
867
+ this.registry.markStatus(resolvedId, 'auth_error');
868
+ }
869
+ return result;
683
870
  }
684
871
  getAttachment(accountId, messageId, attachmentId, maxBytes) {
685
872
  return this.withProvider(accountId, (p) => p.getAttachment(messageId, attachmentId, { maxBytes }));
686
873
  }
874
+ assertMailContent(input) {
875
+ const bodyBytes = Buffer.byteLength(input.body.text ?? '', 'utf8') + Buffer.byteLength(input.body.html ?? '', 'utf8');
876
+ if (bodyBytes > 1024 * 1024) {
877
+ throw new ClientInputError('invalid_request', 'Text and HTML bodies together must be at most 1 MiB.');
878
+ }
879
+ const attachments = input.attachments ?? [];
880
+ if (attachments.length > 20)
881
+ throw new ClientInputError('invalid_request', 'A message may have at most 20 attachments.');
882
+ let total = 0;
883
+ for (const attachment of attachments) {
884
+ if (!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(attachment.content)) {
885
+ throw new ClientInputError('invalid_request', 'Attachment content must be base64.');
886
+ }
887
+ total += Buffer.byteLength(attachment.content, 'base64');
888
+ if (total > this.maxAttachmentBytes) {
889
+ throw new ClientInputError('invalid_request', 'Combined attachments exceed the configured size limit.');
890
+ }
891
+ }
892
+ }
687
893
  }
688
894
  export function buildForwardBody(original, comment) {
689
895
  const headerLines = [