mail-time 5.1.0 → 5.2.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.
package/index.d.cts CHANGED
@@ -50,15 +50,103 @@ export type MailTimeScheduler = {
50
50
  ping: () => Promise<MailTimePingResult>;
51
51
  setInterval: (func: (...args: any[]) => unknown, delay: number, uid: string) => Promise<string>;
52
52
  destroy: () => boolean;
53
+ shutdown: (opts?: {
54
+ timeout?: number;
55
+ }) => Promise<boolean>;
53
56
  pause: (timerId?: string) => boolean;
54
57
  resume: (timerId?: string) => boolean;
55
58
  };
59
+ export type MailTimeMailbox = string | {
60
+ address: string;
61
+ name?: string;
62
+ };
63
+ export type MailTimePolicyRecipient = {
64
+ address: string;
65
+ sources: Array<"envelope" | "to" | "cc" | "bcc">;
66
+ };
67
+ export type MailTimePolicyDecision = {
68
+ address: string;
69
+ status: "suppressed" | "rejected" | "retry";
70
+ reason: string;
71
+ };
72
+ export type MailTimePolicyResult = {
73
+ decisions?: MailTimePolicyDecision[];
74
+ };
75
+ export type MailTimePolicyTransport = {
76
+ index: number;
77
+ name?: string;
78
+ };
79
+ export type MailTimePolicyEnvelope = {
80
+ from?: string;
81
+ to: string[];
82
+ };
83
+ export type MailTimePolicyContext = {
84
+ task: MailTimeTask;
85
+ attempt: number;
86
+ transport: MailTimePolicyTransport;
87
+ envelope: MailTimePolicyEnvelope;
88
+ recipients: MailTimePolicyRecipient[];
89
+ };
90
+ export type MailTimeBeforeSendPolicyContext = MailTimePolicyContext;
91
+ export type MailTimeStructuredRejection = {
92
+ address: string | null;
93
+ command?: string;
94
+ responseCode?: number;
95
+ response?: string;
96
+ message?: string;
97
+ transportIndex: number;
98
+ transportName?: string;
99
+ };
100
+ export type MailTimeRejectionPolicyContext = MailTimePolicyContext & {
101
+ error?: unknown;
102
+ info?: unknown;
103
+ rejections: MailTimeStructuredRejection[];
104
+ };
105
+ export type MailTimeRecipientResult = {
106
+ address: string;
107
+ status: "sent" | "error" | "suppressed" | "rejected";
108
+ sources?: Array<"envelope" | "to" | "cc" | "bcc">;
109
+ reasons: Array<{
110
+ provider: string;
111
+ reason: string;
112
+ }>;
113
+ attempt: number;
114
+ transportIndex?: number;
115
+ transportName?: string;
116
+ command?: string;
117
+ responseCode?: number;
118
+ response?: string;
119
+ message?: string;
120
+ };
121
+ export type MailTimeRecipientAttemptContext = MailTimeRejectionPolicyContext & {
122
+ decisions: MailTimeRecipientResult[];
123
+ };
124
+ export type MailTimeRecipientSummary = {
125
+ uuid: string;
126
+ tries: number;
127
+ isSettled: boolean;
128
+ recipients: {
129
+ sent: MailTimeRecipientResult[];
130
+ error: MailTimeRecipientResult[];
131
+ suppressed: MailTimeRecipientResult[];
132
+ rejected: MailTimeRecipientResult[];
133
+ };
134
+ };
135
+ export type MailTimeRecipientPolicy = {
136
+ name: string;
137
+ failureMode?: "retry" | "continue";
138
+ beforeSend?: (context: MailTimeBeforeSendPolicyContext) => void | MailTimePolicyResult | Promise<void | MailTimePolicyResult>;
139
+ classifyRejections?: (context: MailTimeRejectionPolicyContext) => void | MailTimePolicyResult | Promise<void | MailTimePolicyResult>;
140
+ observeAttempt?: (context: MailTimeRecipientAttemptContext) => void | Promise<void>;
141
+ };
56
142
  export type MailTimeTask = {
57
143
  uuid: string;
58
- to?: string | string[];
144
+ to?: MailTimeMailbox | MailTimeMailbox[];
59
145
  tries: number;
60
146
  sendAt: number;
61
147
  isSent: boolean;
148
+ isSettled?: boolean;
149
+ recipientResults?: MailTimeRecipientResult[];
62
150
  isCancelled: boolean;
63
151
  isFailed: boolean;
64
152
  isSending?: boolean;
@@ -84,6 +172,7 @@ export type CustomQueue = {
84
172
  }) => Promise<boolean>;
85
173
  update: (email: MailTimeTask | object, updateObj: object) => Promise<boolean>;
86
174
  ready?: () => Promise<void>;
175
+ supportsRecipientPolicies?: boolean;
87
176
  };
88
177
  export type MailTimeRejectedRecipient = {
89
178
  address: string;
@@ -91,7 +180,9 @@ export type MailTimeRejectedRecipient = {
91
180
  };
92
181
  export type MailTimeMailOptions = {
93
182
  [key: string]: any;
94
- to: string | string[];
183
+ to: MailTimeMailbox | MailTimeMailbox[];
184
+ cc?: MailTimeMailbox | MailTimeMailbox[];
185
+ bcc?: MailTimeMailbox | MailTimeMailbox[];
95
186
  sendAt?: Date | number;
96
187
  template?: string;
97
188
  concatSubject?: string;
@@ -139,8 +230,11 @@ export type MailTimeOptions = {
139
230
  prefix?: string;
140
231
  debug?: boolean;
141
232
  josk?: MailTimeJoSkOptions;
142
- onError?: (error: unknown, email: MailTimeTask | null, details?: object) => void;
143
- onSent?: (email: MailTimeTask, details?: object) => void;
233
+ recipientPolicies?: MailTimeRecipientPolicy[];
234
+ onError?: (error: unknown, email: MailTimeTask | null, details?: object, recipients?: MailTimeRecipientResult[], summary?: MailTimeRecipientSummary) => void | Promise<void>;
235
+ onSent?: (email: MailTimeTask, details?: object, recipients?: MailTimeRecipientResult[], summary?: MailTimeRecipientSummary) => void | Promise<void>;
236
+ onSuppressed?: (email: MailTimeTask, recipients: MailTimeRecipientResult[], summary: MailTimeRecipientSummary) => void | Promise<void>;
237
+ onRejected?: (email: MailTimeTask, recipients: MailTimeRecipientResult[], summary: MailTimeRecipientSummary) => void | Promise<void>;
144
238
  };
145
239
  /**
146
240
  * @typedef {import('./presets.js').MailTimePresetName} MailTimePresetName
@@ -167,22 +261,41 @@ export type MailTimeOptions = {
167
261
  * @typedef {{ [key: string]: any, adapter: MailTimeJoSkAdapterOptions | object, debug?: boolean, autoClear?: boolean, zombieTime?: number, lockLeaseTime?: number, minRevolvingDelay?: number, maxRevolvingDelay?: number, execute?: 'batch' | 'one', concurrency?: number, lockOwnerId?: string, resetOnInit?: boolean, onError?: (title: string, details: object) => void, onExecuted?: (uid: string, details: object) => void }} MailTimeJoSkOptions
168
262
  */
169
263
  /**
170
- * @typedef {{ [key: string]: any, ping: () => Promise<MailTimePingResult>, setInterval: (func: (...args: any[]) => unknown, delay: number, uid: string) => Promise<string>, destroy: () => boolean, pause: (timerId?: string) => boolean, resume: (timerId?: string) => boolean }} MailTimeScheduler
264
+ * @typedef {{ [key: string]: any, ping: () => Promise<MailTimePingResult>, setInterval: (func: (...args: any[]) => unknown, delay: number, uid: string) => Promise<string>, destroy: () => boolean, shutdown: (opts?: { timeout?: number }) => Promise<boolean>, pause: (timerId?: string) => boolean, resume: (timerId?: string) => boolean }} MailTimeScheduler
265
+ */
266
+ /**
267
+ * @typedef {string | { address: string, name?: string }} MailTimeMailbox
171
268
  */
172
269
  /**
173
- * @typedef {{ uuid: string, to?: string | string[], tries: number, sendAt: number, isSent: boolean, isCancelled: boolean, isFailed: boolean, isSending?: boolean, sendingAt?: number, template?: string | false, transport: number, concatSubject?: string | false, mailOptions: MailTimeMailOptions[] }} MailTimeTask
270
+ * @typedef {{ address: string, sources: Array<'envelope' | 'to' | 'cc' | 'bcc'> }} MailTimePolicyRecipient
271
+ * @typedef {{ address: string, status: 'suppressed' | 'rejected' | 'retry', reason: string }} MailTimePolicyDecision
272
+ * @typedef {{ decisions?: MailTimePolicyDecision[] }} MailTimePolicyResult
273
+ * @typedef {{ index: number, name?: string }} MailTimePolicyTransport
274
+ * @typedef {{ from?: string, to: string[] }} MailTimePolicyEnvelope
275
+ * @typedef {{ task: MailTimeTask, attempt: number, transport: MailTimePolicyTransport, envelope: MailTimePolicyEnvelope, recipients: MailTimePolicyRecipient[] }} MailTimePolicyContext
276
+ * @typedef {MailTimePolicyContext} MailTimeBeforeSendPolicyContext
277
+ * @typedef {{ address: string | null, command?: string, responseCode?: number, response?: string, message?: string, transportIndex: number, transportName?: string }} MailTimeStructuredRejection
278
+ * @typedef {MailTimePolicyContext & { error?: unknown, info?: unknown, rejections: MailTimeStructuredRejection[] }} MailTimeRejectionPolicyContext
279
+ * @typedef {{ address: string, status: 'sent' | 'error' | 'suppressed' | 'rejected', sources?: Array<'envelope' | 'to' | 'cc' | 'bcc'>, reasons: Array<{ provider: string, reason: string }>, attempt: number, transportIndex?: number, transportName?: string, command?: string, responseCode?: number, response?: string, message?: string }} MailTimeRecipientResult
280
+ * @typedef {MailTimeRejectionPolicyContext & { decisions: MailTimeRecipientResult[] }} MailTimeRecipientAttemptContext
281
+ * @typedef {{ uuid: string, tries: number, isSettled: boolean, recipients: { sent: MailTimeRecipientResult[], error: MailTimeRecipientResult[], suppressed: MailTimeRecipientResult[], rejected: MailTimeRecipientResult[] } }} MailTimeRecipientSummary
282
+ * @typedef {{ name: string, failureMode?: 'retry' | 'continue', beforeSend?: (context: MailTimeBeforeSendPolicyContext) => void | MailTimePolicyResult | Promise<void | MailTimePolicyResult>, classifyRejections?: (context: MailTimeRejectionPolicyContext) => void | MailTimePolicyResult | Promise<void | MailTimePolicyResult>, observeAttempt?: (context: MailTimeRecipientAttemptContext) => void | Promise<void> }} MailTimeRecipientPolicy
283
+ * @description Policy contexts are read-only by contract. Providers must bound their own I/O; renewal budgets do not limit hook duration.
284
+ */
285
+ /**
286
+ * @typedef {{ uuid: string, to?: MailTimeMailbox | MailTimeMailbox[], tries: number, sendAt: number, isSent: boolean, isSettled?: boolean, recipientResults?: MailTimeRecipientResult[], isCancelled: boolean, isFailed: boolean, isSending?: boolean, sendingAt?: number, template?: string | false, transport: number, concatSubject?: string | false, mailOptions: MailTimeMailOptions[] }} MailTimeTask
174
287
  */
175
288
  /**
176
289
  * @typedef {{ limit?: number, sendingTimeout?: number }} MailTimeIterateOptions
177
290
  */
178
291
  /**
179
- * @typedef {{ ping: () => Promise<MailTimePingResult>, iterate: (opts?: MailTimeIterateOptions) => Promise<void> | void, getPendingTo: (to: string, sendAt: number) => Promise<MailTimeTask | object | null>, push: (email: MailTimeTask) => Promise<void> | void, cancel: (uuid: string) => Promise<boolean>, remove: (email: MailTimeTask | object, opts?: { leaseTries: number, leaseSendingAt: number }) => Promise<boolean>, update: (email: MailTimeTask | object, updateObj: object) => Promise<boolean>, ready?: () => Promise<void> }} CustomQueue
292
+ * @typedef {{ ping: () => Promise<MailTimePingResult>, iterate: (opts?: MailTimeIterateOptions) => Promise<void> | void, getPendingTo: (to: string, sendAt: number) => Promise<MailTimeTask | object | null>, push: (email: MailTimeTask) => Promise<void> | void, cancel: (uuid: string) => Promise<boolean>, remove: (email: MailTimeTask | object, opts?: { leaseTries: number, leaseSendingAt: number }) => Promise<boolean>, update: (email: MailTimeTask | object, updateObj: object) => Promise<boolean>, ready?: () => Promise<void>, supportsRecipientPolicies?: boolean }} CustomQueue
180
293
  */
181
294
  /**
182
295
  * @typedef {{ address: string, error: string }} MailTimeRejectedRecipient
183
296
  */
184
297
  /**
185
- * @typedef {{ [key: string]: any, to: string | string[], sendAt?: Date | number, template?: string, concatSubject?: string, text?: string | false, html?: string | false, subject?: string, accepted?: string[], rejected?: MailTimeRejectedRecipient[] }} MailTimeMailOptions
298
+ * @typedef {{ [key: string]: any, to: MailTimeMailbox | MailTimeMailbox[], cc?: MailTimeMailbox | MailTimeMailbox[], bcc?: MailTimeMailbox | MailTimeMailbox[], sendAt?: Date | number, template?: string, concatSubject?: string, text?: string | false, html?: string | false, subject?: string, accepted?: string[], rejected?: MailTimeRejectedRecipient[] }} MailTimeMailOptions
186
299
  */
187
300
  /**
188
301
  * @typedef {{ subject?: string }} MailTimeConcatEmailsOptions
@@ -191,9 +304,15 @@ export type MailTimeOptions = {
191
304
  * @typedef {{ index: number, from: string | undefined }} MailTimeFromDetails
192
305
  */
193
306
  /**
194
- * @typedef {{ queue: RedisQueue | MongoQueue | PostgresQueue | CustomQueue, type?: 'server' | 'client', from?: string | ((transport: MailTimeTransport, details: MailTimeFromDetails) => string), transports?: MailTimeTransport[], strategy?: 'backup' | 'balancer', failsToNext?: number, shouldFailOver?: (error: unknown, info: object | undefined, email: MailTimeTask) => boolean, retries?: number, maxTries?: number, retryDelay?: number, interval?: number, keepHistory?: boolean, concatEmails?: boolean | MailTimeConcatEmailsOptions, concatSubject?: string, concatDelimiter?: string, concatDelay?: number, concatThrottling?: number, revolvingInterval?: number, mode?: 'one' | 'batch', concurrency?: number, sendingTimeout?: number, renewClaim?: boolean | number, maxRenewals?: number, strictPayload?: boolean, allowedMailFields?: string[], verifyTransports?: boolean, template?: string, prefix?: string, debug?: boolean, josk?: MailTimeJoSkOptions, onError?: (error: unknown, email: MailTimeTask | null, details?: object) => void, onSent?: (email: MailTimeTask, details?: object) => void }} MailTimeOptions
307
+ * @typedef {{ queue: RedisQueue | MongoQueue | PostgresQueue | CustomQueue, type?: 'server' | 'client', from?: string | ((transport: MailTimeTransport, details: MailTimeFromDetails) => string), transports?: MailTimeTransport[], strategy?: 'backup' | 'balancer', failsToNext?: number, shouldFailOver?: (error: unknown, info: object | undefined, email: MailTimeTask) => boolean, retries?: number, maxTries?: number, retryDelay?: number, interval?: number, keepHistory?: boolean, concatEmails?: boolean | MailTimeConcatEmailsOptions, concatSubject?: string, concatDelimiter?: string, concatDelay?: number, concatThrottling?: number, revolvingInterval?: number, mode?: 'one' | 'batch', concurrency?: number, sendingTimeout?: number, renewClaim?: boolean | number, maxRenewals?: number, strictPayload?: boolean, allowedMailFields?: string[], verifyTransports?: boolean, template?: string, prefix?: string, debug?: boolean, josk?: MailTimeJoSkOptions, recipientPolicies?: MailTimeRecipientPolicy[], onError?: (error: unknown, email: MailTimeTask | null, details?: object, recipients?: MailTimeRecipientResult[], summary?: MailTimeRecipientSummary) => void | Promise<void>, onSent?: (email: MailTimeTask, details?: object, recipients?: MailTimeRecipientResult[], summary?: MailTimeRecipientSummary) => void | Promise<void>, onSuppressed?: (email: MailTimeTask, recipients: MailTimeRecipientResult[], summary: MailTimeRecipientSummary) => void | Promise<void>, onRejected?: (email: MailTimeTask, recipients: MailTimeRecipientResult[], summary: MailTimeRecipientSummary) => void | Promise<void> }} MailTimeOptions
308
+ */
309
+ /**
310
+ * Class of MailTime.
311
+ * With recipientPolicies, terminal callbacks receive grouped recipients and a complete summary after persistence.
312
+ * isSettled includes exhausted errors; isSent means all accepted; isFailed means an error or permanent rejection.
313
+ * Unparseable terminal preparation failures invoke onError with an empty recipient group.
314
+ * Hooks must bound their own I/O. Callback notifications are best-effort, not durable exactly-once delivery.
195
315
  */
196
- /** Class of MailTime */
197
316
  export class MailTime {
198
317
  static set Template(newVal: string);
199
318
  static get Template(): string;
@@ -223,8 +342,10 @@ export class MailTime {
223
342
  retryDelay: number;
224
343
  template: string;
225
344
  keepHistory: boolean;
226
- onSent: (email: MailTimeTask, details?: object) => void;
227
- onError: (error: unknown, email: MailTimeTask | null, details?: object) => void;
345
+ onSent: (email: MailTimeTask, details?: object, recipients?: MailTimeRecipientResult[], summary?: MailTimeRecipientSummary) => void | Promise<void>;
346
+ onError: (error: unknown, email: MailTimeTask | null, details?: object, recipients?: MailTimeRecipientResult[], summary?: MailTimeRecipientSummary) => void | Promise<void>;
347
+ onSuppressed: (email: MailTimeTask, recipients: MailTimeRecipientResult[], summary: MailTimeRecipientSummary) => void | Promise<void>;
348
+ onRejected: (email: MailTimeTask, recipients: MailTimeRecipientResult[], summary: MailTimeRecipientSummary) => void | Promise<void>;
228
349
  revolvingInterval: number;
229
350
  mode: "one" | "batch";
230
351
  concurrency: number;
@@ -283,12 +404,13 @@ export class MailTime {
283
404
  /**
284
405
  * @memberOf MailTime
285
406
  * @name destroy
286
- * @description Stop the scheduler and block future dispatches. Without `{ drain: true }`, any in-flight SMTP attempts are neutralized on completion — they perform no storage writes, no `onSent`/`onError` callbacks, and no logging once `destroy()` returns; their claimed rows are recovered by stale-lock timeout (`sendingTimeout`). Pass `{ drain: true }` to instead let in-flight attempts run to completion (returns a Promise that resolves once they settle).
287
- * @param {{ drain?: boolean }} [opts]
407
+ * @description Stop the scheduler and block future dispatches. Without `{ drain: true }`, in-flight SMTP attempts are neutralized and their claims recover after `sendingTimeout`. With `{ drain: true }`, await JoSk shutdown and in-flight SMTP; resolves false if a scheduler handler exceeds `schedulerTimeout` (default 10000ms) or JoSk shutdown throws (logged); never rejects. The timeout does not bound SMTP drain time.
408
+ * @param {{ drain?: boolean, schedulerTimeout?: number }} [opts] - schedulerTimeout must be finite and non-negative; used only with drain
288
409
  * @returns {boolean | Promise<boolean>}
289
410
  */
290
411
  destroy(opts?: {
291
412
  drain?: boolean;
413
+ schedulerTimeout?: number;
292
414
  }): boolean | Promise<boolean>;
293
415
  /**
294
416
  * @async
package/index.d.ts CHANGED
@@ -50,15 +50,103 @@ export type MailTimeScheduler = {
50
50
  ping: () => Promise<MailTimePingResult>;
51
51
  setInterval: (func: (...args: any[]) => unknown, delay: number, uid: string) => Promise<string>;
52
52
  destroy: () => boolean;
53
+ shutdown: (opts?: {
54
+ timeout?: number;
55
+ }) => Promise<boolean>;
53
56
  pause: (timerId?: string) => boolean;
54
57
  resume: (timerId?: string) => boolean;
55
58
  };
59
+ export type MailTimeMailbox = string | {
60
+ address: string;
61
+ name?: string;
62
+ };
63
+ export type MailTimePolicyRecipient = {
64
+ address: string;
65
+ sources: Array<"envelope" | "to" | "cc" | "bcc">;
66
+ };
67
+ export type MailTimePolicyDecision = {
68
+ address: string;
69
+ status: "suppressed" | "rejected" | "retry";
70
+ reason: string;
71
+ };
72
+ export type MailTimePolicyResult = {
73
+ decisions?: MailTimePolicyDecision[];
74
+ };
75
+ export type MailTimePolicyTransport = {
76
+ index: number;
77
+ name?: string;
78
+ };
79
+ export type MailTimePolicyEnvelope = {
80
+ from?: string;
81
+ to: string[];
82
+ };
83
+ export type MailTimePolicyContext = {
84
+ task: MailTimeTask;
85
+ attempt: number;
86
+ transport: MailTimePolicyTransport;
87
+ envelope: MailTimePolicyEnvelope;
88
+ recipients: MailTimePolicyRecipient[];
89
+ };
90
+ export type MailTimeBeforeSendPolicyContext = MailTimePolicyContext;
91
+ export type MailTimeStructuredRejection = {
92
+ address: string | null;
93
+ command?: string;
94
+ responseCode?: number;
95
+ response?: string;
96
+ message?: string;
97
+ transportIndex: number;
98
+ transportName?: string;
99
+ };
100
+ export type MailTimeRejectionPolicyContext = MailTimePolicyContext & {
101
+ error?: unknown;
102
+ info?: unknown;
103
+ rejections: MailTimeStructuredRejection[];
104
+ };
105
+ export type MailTimeRecipientResult = {
106
+ address: string;
107
+ status: "sent" | "error" | "suppressed" | "rejected";
108
+ sources?: Array<"envelope" | "to" | "cc" | "bcc">;
109
+ reasons: Array<{
110
+ provider: string;
111
+ reason: string;
112
+ }>;
113
+ attempt: number;
114
+ transportIndex?: number;
115
+ transportName?: string;
116
+ command?: string;
117
+ responseCode?: number;
118
+ response?: string;
119
+ message?: string;
120
+ };
121
+ export type MailTimeRecipientAttemptContext = MailTimeRejectionPolicyContext & {
122
+ decisions: MailTimeRecipientResult[];
123
+ };
124
+ export type MailTimeRecipientSummary = {
125
+ uuid: string;
126
+ tries: number;
127
+ isSettled: boolean;
128
+ recipients: {
129
+ sent: MailTimeRecipientResult[];
130
+ error: MailTimeRecipientResult[];
131
+ suppressed: MailTimeRecipientResult[];
132
+ rejected: MailTimeRecipientResult[];
133
+ };
134
+ };
135
+ export type MailTimeRecipientPolicy = {
136
+ name: string;
137
+ failureMode?: "retry" | "continue";
138
+ beforeSend?: (context: MailTimeBeforeSendPolicyContext) => void | MailTimePolicyResult | Promise<void | MailTimePolicyResult>;
139
+ classifyRejections?: (context: MailTimeRejectionPolicyContext) => void | MailTimePolicyResult | Promise<void | MailTimePolicyResult>;
140
+ observeAttempt?: (context: MailTimeRecipientAttemptContext) => void | Promise<void>;
141
+ };
56
142
  export type MailTimeTask = {
57
143
  uuid: string;
58
- to?: string | string[];
144
+ to?: MailTimeMailbox | MailTimeMailbox[];
59
145
  tries: number;
60
146
  sendAt: number;
61
147
  isSent: boolean;
148
+ isSettled?: boolean;
149
+ recipientResults?: MailTimeRecipientResult[];
62
150
  isCancelled: boolean;
63
151
  isFailed: boolean;
64
152
  isSending?: boolean;
@@ -84,6 +172,7 @@ export type CustomQueue = {
84
172
  }) => Promise<boolean>;
85
173
  update: (email: MailTimeTask | object, updateObj: object) => Promise<boolean>;
86
174
  ready?: () => Promise<void>;
175
+ supportsRecipientPolicies?: boolean;
87
176
  };
88
177
  export type MailTimeRejectedRecipient = {
89
178
  address: string;
@@ -91,7 +180,9 @@ export type MailTimeRejectedRecipient = {
91
180
  };
92
181
  export type MailTimeMailOptions = {
93
182
  [key: string]: any;
94
- to: string | string[];
183
+ to: MailTimeMailbox | MailTimeMailbox[];
184
+ cc?: MailTimeMailbox | MailTimeMailbox[];
185
+ bcc?: MailTimeMailbox | MailTimeMailbox[];
95
186
  sendAt?: Date | number;
96
187
  template?: string;
97
188
  concatSubject?: string;
@@ -139,8 +230,11 @@ export type MailTimeOptions = {
139
230
  prefix?: string;
140
231
  debug?: boolean;
141
232
  josk?: MailTimeJoSkOptions;
142
- onError?: (error: unknown, email: MailTimeTask | null, details?: object) => void;
143
- onSent?: (email: MailTimeTask, details?: object) => void;
233
+ recipientPolicies?: MailTimeRecipientPolicy[];
234
+ onError?: (error: unknown, email: MailTimeTask | null, details?: object, recipients?: MailTimeRecipientResult[], summary?: MailTimeRecipientSummary) => void | Promise<void>;
235
+ onSent?: (email: MailTimeTask, details?: object, recipients?: MailTimeRecipientResult[], summary?: MailTimeRecipientSummary) => void | Promise<void>;
236
+ onSuppressed?: (email: MailTimeTask, recipients: MailTimeRecipientResult[], summary: MailTimeRecipientSummary) => void | Promise<void>;
237
+ onRejected?: (email: MailTimeTask, recipients: MailTimeRecipientResult[], summary: MailTimeRecipientSummary) => void | Promise<void>;
144
238
  };
145
239
  /**
146
240
  * @typedef {import('./presets.js').MailTimePresetName} MailTimePresetName
@@ -167,22 +261,41 @@ export type MailTimeOptions = {
167
261
  * @typedef {{ [key: string]: any, adapter: MailTimeJoSkAdapterOptions | object, debug?: boolean, autoClear?: boolean, zombieTime?: number, lockLeaseTime?: number, minRevolvingDelay?: number, maxRevolvingDelay?: number, execute?: 'batch' | 'one', concurrency?: number, lockOwnerId?: string, resetOnInit?: boolean, onError?: (title: string, details: object) => void, onExecuted?: (uid: string, details: object) => void }} MailTimeJoSkOptions
168
262
  */
169
263
  /**
170
- * @typedef {{ [key: string]: any, ping: () => Promise<MailTimePingResult>, setInterval: (func: (...args: any[]) => unknown, delay: number, uid: string) => Promise<string>, destroy: () => boolean, pause: (timerId?: string) => boolean, resume: (timerId?: string) => boolean }} MailTimeScheduler
264
+ * @typedef {{ [key: string]: any, ping: () => Promise<MailTimePingResult>, setInterval: (func: (...args: any[]) => unknown, delay: number, uid: string) => Promise<string>, destroy: () => boolean, shutdown: (opts?: { timeout?: number }) => Promise<boolean>, pause: (timerId?: string) => boolean, resume: (timerId?: string) => boolean }} MailTimeScheduler
265
+ */
266
+ /**
267
+ * @typedef {string | { address: string, name?: string }} MailTimeMailbox
171
268
  */
172
269
  /**
173
- * @typedef {{ uuid: string, to?: string | string[], tries: number, sendAt: number, isSent: boolean, isCancelled: boolean, isFailed: boolean, isSending?: boolean, sendingAt?: number, template?: string | false, transport: number, concatSubject?: string | false, mailOptions: MailTimeMailOptions[] }} MailTimeTask
270
+ * @typedef {{ address: string, sources: Array<'envelope' | 'to' | 'cc' | 'bcc'> }} MailTimePolicyRecipient
271
+ * @typedef {{ address: string, status: 'suppressed' | 'rejected' | 'retry', reason: string }} MailTimePolicyDecision
272
+ * @typedef {{ decisions?: MailTimePolicyDecision[] }} MailTimePolicyResult
273
+ * @typedef {{ index: number, name?: string }} MailTimePolicyTransport
274
+ * @typedef {{ from?: string, to: string[] }} MailTimePolicyEnvelope
275
+ * @typedef {{ task: MailTimeTask, attempt: number, transport: MailTimePolicyTransport, envelope: MailTimePolicyEnvelope, recipients: MailTimePolicyRecipient[] }} MailTimePolicyContext
276
+ * @typedef {MailTimePolicyContext} MailTimeBeforeSendPolicyContext
277
+ * @typedef {{ address: string | null, command?: string, responseCode?: number, response?: string, message?: string, transportIndex: number, transportName?: string }} MailTimeStructuredRejection
278
+ * @typedef {MailTimePolicyContext & { error?: unknown, info?: unknown, rejections: MailTimeStructuredRejection[] }} MailTimeRejectionPolicyContext
279
+ * @typedef {{ address: string, status: 'sent' | 'error' | 'suppressed' | 'rejected', sources?: Array<'envelope' | 'to' | 'cc' | 'bcc'>, reasons: Array<{ provider: string, reason: string }>, attempt: number, transportIndex?: number, transportName?: string, command?: string, responseCode?: number, response?: string, message?: string }} MailTimeRecipientResult
280
+ * @typedef {MailTimeRejectionPolicyContext & { decisions: MailTimeRecipientResult[] }} MailTimeRecipientAttemptContext
281
+ * @typedef {{ uuid: string, tries: number, isSettled: boolean, recipients: { sent: MailTimeRecipientResult[], error: MailTimeRecipientResult[], suppressed: MailTimeRecipientResult[], rejected: MailTimeRecipientResult[] } }} MailTimeRecipientSummary
282
+ * @typedef {{ name: string, failureMode?: 'retry' | 'continue', beforeSend?: (context: MailTimeBeforeSendPolicyContext) => void | MailTimePolicyResult | Promise<void | MailTimePolicyResult>, classifyRejections?: (context: MailTimeRejectionPolicyContext) => void | MailTimePolicyResult | Promise<void | MailTimePolicyResult>, observeAttempt?: (context: MailTimeRecipientAttemptContext) => void | Promise<void> }} MailTimeRecipientPolicy
283
+ * @description Policy contexts are read-only by contract. Providers must bound their own I/O; renewal budgets do not limit hook duration.
284
+ */
285
+ /**
286
+ * @typedef {{ uuid: string, to?: MailTimeMailbox | MailTimeMailbox[], tries: number, sendAt: number, isSent: boolean, isSettled?: boolean, recipientResults?: MailTimeRecipientResult[], isCancelled: boolean, isFailed: boolean, isSending?: boolean, sendingAt?: number, template?: string | false, transport: number, concatSubject?: string | false, mailOptions: MailTimeMailOptions[] }} MailTimeTask
174
287
  */
175
288
  /**
176
289
  * @typedef {{ limit?: number, sendingTimeout?: number }} MailTimeIterateOptions
177
290
  */
178
291
  /**
179
- * @typedef {{ ping: () => Promise<MailTimePingResult>, iterate: (opts?: MailTimeIterateOptions) => Promise<void> | void, getPendingTo: (to: string, sendAt: number) => Promise<MailTimeTask | object | null>, push: (email: MailTimeTask) => Promise<void> | void, cancel: (uuid: string) => Promise<boolean>, remove: (email: MailTimeTask | object, opts?: { leaseTries: number, leaseSendingAt: number }) => Promise<boolean>, update: (email: MailTimeTask | object, updateObj: object) => Promise<boolean>, ready?: () => Promise<void> }} CustomQueue
292
+ * @typedef {{ ping: () => Promise<MailTimePingResult>, iterate: (opts?: MailTimeIterateOptions) => Promise<void> | void, getPendingTo: (to: string, sendAt: number) => Promise<MailTimeTask | object | null>, push: (email: MailTimeTask) => Promise<void> | void, cancel: (uuid: string) => Promise<boolean>, remove: (email: MailTimeTask | object, opts?: { leaseTries: number, leaseSendingAt: number }) => Promise<boolean>, update: (email: MailTimeTask | object, updateObj: object) => Promise<boolean>, ready?: () => Promise<void>, supportsRecipientPolicies?: boolean }} CustomQueue
180
293
  */
181
294
  /**
182
295
  * @typedef {{ address: string, error: string }} MailTimeRejectedRecipient
183
296
  */
184
297
  /**
185
- * @typedef {{ [key: string]: any, to: string | string[], sendAt?: Date | number, template?: string, concatSubject?: string, text?: string | false, html?: string | false, subject?: string, accepted?: string[], rejected?: MailTimeRejectedRecipient[] }} MailTimeMailOptions
298
+ * @typedef {{ [key: string]: any, to: MailTimeMailbox | MailTimeMailbox[], cc?: MailTimeMailbox | MailTimeMailbox[], bcc?: MailTimeMailbox | MailTimeMailbox[], sendAt?: Date | number, template?: string, concatSubject?: string, text?: string | false, html?: string | false, subject?: string, accepted?: string[], rejected?: MailTimeRejectedRecipient[] }} MailTimeMailOptions
186
299
  */
187
300
  /**
188
301
  * @typedef {{ subject?: string }} MailTimeConcatEmailsOptions
@@ -191,9 +304,15 @@ export type MailTimeOptions = {
191
304
  * @typedef {{ index: number, from: string | undefined }} MailTimeFromDetails
192
305
  */
193
306
  /**
194
- * @typedef {{ queue: RedisQueue | MongoQueue | PostgresQueue | CustomQueue, type?: 'server' | 'client', from?: string | ((transport: MailTimeTransport, details: MailTimeFromDetails) => string), transports?: MailTimeTransport[], strategy?: 'backup' | 'balancer', failsToNext?: number, shouldFailOver?: (error: unknown, info: object | undefined, email: MailTimeTask) => boolean, retries?: number, maxTries?: number, retryDelay?: number, interval?: number, keepHistory?: boolean, concatEmails?: boolean | MailTimeConcatEmailsOptions, concatSubject?: string, concatDelimiter?: string, concatDelay?: number, concatThrottling?: number, revolvingInterval?: number, mode?: 'one' | 'batch', concurrency?: number, sendingTimeout?: number, renewClaim?: boolean | number, maxRenewals?: number, strictPayload?: boolean, allowedMailFields?: string[], verifyTransports?: boolean, template?: string, prefix?: string, debug?: boolean, josk?: MailTimeJoSkOptions, onError?: (error: unknown, email: MailTimeTask | null, details?: object) => void, onSent?: (email: MailTimeTask, details?: object) => void }} MailTimeOptions
307
+ * @typedef {{ queue: RedisQueue | MongoQueue | PostgresQueue | CustomQueue, type?: 'server' | 'client', from?: string | ((transport: MailTimeTransport, details: MailTimeFromDetails) => string), transports?: MailTimeTransport[], strategy?: 'backup' | 'balancer', failsToNext?: number, shouldFailOver?: (error: unknown, info: object | undefined, email: MailTimeTask) => boolean, retries?: number, maxTries?: number, retryDelay?: number, interval?: number, keepHistory?: boolean, concatEmails?: boolean | MailTimeConcatEmailsOptions, concatSubject?: string, concatDelimiter?: string, concatDelay?: number, concatThrottling?: number, revolvingInterval?: number, mode?: 'one' | 'batch', concurrency?: number, sendingTimeout?: number, renewClaim?: boolean | number, maxRenewals?: number, strictPayload?: boolean, allowedMailFields?: string[], verifyTransports?: boolean, template?: string, prefix?: string, debug?: boolean, josk?: MailTimeJoSkOptions, recipientPolicies?: MailTimeRecipientPolicy[], onError?: (error: unknown, email: MailTimeTask | null, details?: object, recipients?: MailTimeRecipientResult[], summary?: MailTimeRecipientSummary) => void | Promise<void>, onSent?: (email: MailTimeTask, details?: object, recipients?: MailTimeRecipientResult[], summary?: MailTimeRecipientSummary) => void | Promise<void>, onSuppressed?: (email: MailTimeTask, recipients: MailTimeRecipientResult[], summary: MailTimeRecipientSummary) => void | Promise<void>, onRejected?: (email: MailTimeTask, recipients: MailTimeRecipientResult[], summary: MailTimeRecipientSummary) => void | Promise<void> }} MailTimeOptions
308
+ */
309
+ /**
310
+ * Class of MailTime.
311
+ * With recipientPolicies, terminal callbacks receive grouped recipients and a complete summary after persistence.
312
+ * isSettled includes exhausted errors; isSent means all accepted; isFailed means an error or permanent rejection.
313
+ * Unparseable terminal preparation failures invoke onError with an empty recipient group.
314
+ * Hooks must bound their own I/O. Callback notifications are best-effort, not durable exactly-once delivery.
195
315
  */
196
- /** Class of MailTime */
197
316
  export class MailTime {
198
317
  static set Template(newVal: string);
199
318
  static get Template(): string;
@@ -223,8 +342,10 @@ export class MailTime {
223
342
  retryDelay: number;
224
343
  template: string;
225
344
  keepHistory: boolean;
226
- onSent: (email: MailTimeTask, details?: object) => void;
227
- onError: (error: unknown, email: MailTimeTask | null, details?: object) => void;
345
+ onSent: (email: MailTimeTask, details?: object, recipients?: MailTimeRecipientResult[], summary?: MailTimeRecipientSummary) => void | Promise<void>;
346
+ onError: (error: unknown, email: MailTimeTask | null, details?: object, recipients?: MailTimeRecipientResult[], summary?: MailTimeRecipientSummary) => void | Promise<void>;
347
+ onSuppressed: (email: MailTimeTask, recipients: MailTimeRecipientResult[], summary: MailTimeRecipientSummary) => void | Promise<void>;
348
+ onRejected: (email: MailTimeTask, recipients: MailTimeRecipientResult[], summary: MailTimeRecipientSummary) => void | Promise<void>;
228
349
  revolvingInterval: number;
229
350
  mode: "one" | "batch";
230
351
  concurrency: number;
@@ -283,12 +404,13 @@ export class MailTime {
283
404
  /**
284
405
  * @memberOf MailTime
285
406
  * @name destroy
286
- * @description Stop the scheduler and block future dispatches. Without `{ drain: true }`, any in-flight SMTP attempts are neutralized on completion — they perform no storage writes, no `onSent`/`onError` callbacks, and no logging once `destroy()` returns; their claimed rows are recovered by stale-lock timeout (`sendingTimeout`). Pass `{ drain: true }` to instead let in-flight attempts run to completion (returns a Promise that resolves once they settle).
287
- * @param {{ drain?: boolean }} [opts]
407
+ * @description Stop the scheduler and block future dispatches. Without `{ drain: true }`, in-flight SMTP attempts are neutralized and their claims recover after `sendingTimeout`. With `{ drain: true }`, await JoSk shutdown and in-flight SMTP; resolves false if a scheduler handler exceeds `schedulerTimeout` (default 10000ms) or JoSk shutdown throws (logged); never rejects. The timeout does not bound SMTP drain time.
408
+ * @param {{ drain?: boolean, schedulerTimeout?: number }} [opts] - schedulerTimeout must be finite and non-negative; used only with drain
288
409
  * @returns {boolean | Promise<boolean>}
289
410
  */
290
411
  destroy(opts?: {
291
412
  drain?: boolean;
413
+ schedulerTimeout?: number;
292
414
  }): boolean | Promise<boolean>;
293
415
  /**
294
416
  * @async