@c9up/rover 0.1.5 → 0.1.7
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/README.md +3 -1
- package/dist/BaseMail.d.ts +2 -1
- package/dist/BaseMail.d.ts.map +1 -1
- package/dist/BaseMail.js +9 -4
- package/dist/BaseMail.js.map +1 -1
- package/dist/Mail.d.ts +65 -11
- package/dist/Mail.d.ts.map +1 -1
- package/dist/Mail.js +142 -49
- package/dist/Mail.js.map +1 -1
- package/dist/MessageBuilder.d.ts +44 -7
- package/dist/MessageBuilder.d.ts.map +1 -1
- package/dist/MessageBuilder.js +57 -10
- package/dist/MessageBuilder.js.map +1 -1
- package/dist/format.d.ts +12 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/format.js +14 -0
- package/dist/format.js.map +1 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/queue/MailJob.d.ts.map +1 -1
- package/dist/queue/MailJob.js.map +1 -1
- package/dist/queue/MemoryMailMessenger.d.ts +15 -0
- package/dist/queue/MemoryMailMessenger.d.ts.map +1 -0
- package/dist/queue/MemoryMailMessenger.js +42 -0
- package/dist/queue/MemoryMailMessenger.js.map +1 -0
- package/dist/testing/FakeMail.d.ts +30 -3
- package/dist/testing/FakeMail.d.ts.map +1 -1
- package/dist/testing/FakeMail.js +106 -28
- package/dist/testing/FakeMail.js.map +1 -1
- package/dist/transports/BrevoTransport.d.ts +14 -0
- package/dist/transports/BrevoTransport.d.ts.map +1 -0
- package/dist/transports/BrevoTransport.js +134 -0
- package/dist/transports/BrevoTransport.js.map +1 -0
- package/dist/transports/MailgunTransport.d.ts.map +1 -1
- package/dist/transports/MailgunTransport.js +3 -1
- package/dist/transports/MailgunTransport.js.map +1 -1
- package/dist/transports/ResendTransport.d.ts.map +1 -1
- package/dist/transports/ResendTransport.js +4 -2
- package/dist/transports/ResendTransport.js.map +1 -1
- package/dist/transports/SendGridTransport.d.ts.map +1 -1
- package/dist/transports/SendGridTransport.js +2 -2
- package/dist/transports/SendGridTransport.js.map +1 -1
- package/dist/transports/SesTransport.js +1 -1
- package/dist/transports/SesTransport.js.map +1 -1
- package/dist/transports/SparkPostTransport.d.ts +14 -0
- package/dist/transports/SparkPostTransport.d.ts.map +1 -0
- package/dist/transports/SparkPostTransport.js +144 -0
- package/dist/transports/SparkPostTransport.js.map +1 -0
- package/index.darwin-arm64.node +0 -0
- package/index.darwin-x64.node +0 -0
- package/index.linux-arm64-gnu.node +0 -0
- package/index.linux-x64-gnu.node +0 -0
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +10 -1
- package/src/BaseMail.ts +17 -5
- package/src/Mail.ts +205 -62
- package/src/MessageBuilder.ts +103 -12
- package/src/format.ts +14 -0
- package/src/index.ts +5 -1
- package/src/queue/MailJob.ts +1 -1
- package/src/queue/MemoryMailMessenger.ts +45 -0
- package/src/testing/FakeMail.ts +232 -31
- package/src/transports/BrevoTransport.ts +174 -0
- package/src/transports/MailgunTransport.ts +3 -1
- package/src/transports/ResendTransport.ts +4 -2
- package/src/transports/SendGridTransport.ts +2 -2
- package/src/transports/SesTransport.ts +3 -1
- package/src/transports/SparkPostTransport.ts +182 -0
package/src/BaseMail.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type MailMessage, MessageBuilder } from "./MessageBuilder.js";
|
|
2
2
|
|
|
3
|
-
export type MailAddress = string | { address: string; name
|
|
3
|
+
export type MailAddress = string | { address: string; name?: string };
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Abstract base for class-based mail messages (Adonis parity).
|
|
@@ -20,6 +20,7 @@ export abstract class BaseMail {
|
|
|
20
20
|
protected readonly message: MessageBuilder = new MessageBuilder();
|
|
21
21
|
|
|
22
22
|
from?: MailAddress;
|
|
23
|
+
replyTo?: MailAddress;
|
|
23
24
|
subject?: string;
|
|
24
25
|
|
|
25
26
|
constructor() {
|
|
@@ -34,7 +35,14 @@ export abstract class BaseMail {
|
|
|
34
35
|
|
|
35
36
|
async build(viewsRoot?: string): Promise<MailMessage> {
|
|
36
37
|
if (this.from !== undefined) {
|
|
37
|
-
this.
|
|
38
|
+
applyAddress(this.from, (address, name) =>
|
|
39
|
+
this.message.from(address, name),
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
if (this.replyTo !== undefined) {
|
|
43
|
+
applyAddress(this.replyTo, (address, name) =>
|
|
44
|
+
this.message.replyTo(address, name),
|
|
45
|
+
);
|
|
38
46
|
}
|
|
39
47
|
if (this.subject !== undefined) {
|
|
40
48
|
this.message.subject(this.subject);
|
|
@@ -46,9 +54,13 @@ export abstract class BaseMail {
|
|
|
46
54
|
}
|
|
47
55
|
}
|
|
48
56
|
|
|
49
|
-
function
|
|
57
|
+
function applyAddress(
|
|
58
|
+
addr: MailAddress,
|
|
59
|
+
set: (address: string, name?: string) => void,
|
|
60
|
+
): void {
|
|
50
61
|
if (typeof addr === "string") {
|
|
51
|
-
|
|
62
|
+
set(addr);
|
|
63
|
+
return;
|
|
52
64
|
}
|
|
53
|
-
|
|
65
|
+
set(addr.address, addr.name);
|
|
54
66
|
}
|
package/src/Mail.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
MAIL_JOB_NAME,
|
|
12
12
|
MailJobHandler,
|
|
13
13
|
} from "./queue/MailJob.js";
|
|
14
|
+
import { MemoryMailMessenger } from "./queue/MemoryMailMessenger.js";
|
|
14
15
|
import { RoverError } from "./RoverError.js";
|
|
15
16
|
import {
|
|
16
17
|
computeBackoffMs,
|
|
@@ -68,6 +69,18 @@ export interface EmitterLike {
|
|
|
68
69
|
emit(event: string, data: unknown): void;
|
|
69
70
|
}
|
|
70
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Emitted (`mail:sending`) right before the transport `send` runs — no
|
|
74
|
+
* `messageId` yet, since the provider hasn't accepted the message.
|
|
75
|
+
*/
|
|
76
|
+
export interface MailSendingEvent {
|
|
77
|
+
to: string[];
|
|
78
|
+
cc: string[];
|
|
79
|
+
bcc: string[];
|
|
80
|
+
transportName: string;
|
|
81
|
+
timestamp: number;
|
|
82
|
+
}
|
|
83
|
+
|
|
71
84
|
export interface MailSentEvent {
|
|
72
85
|
messageId: string;
|
|
73
86
|
to: string[];
|
|
@@ -77,6 +90,21 @@ export interface MailSentEvent {
|
|
|
77
90
|
timestamp: number;
|
|
78
91
|
}
|
|
79
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Emitted for the queue lifecycle (`mail:queueing` / `mail:queued`). `jobId` is
|
|
95
|
+
* only present on `mail:queued` (once the job has been accepted by the queue /
|
|
96
|
+
* in-memory messenger).
|
|
97
|
+
*/
|
|
98
|
+
export interface MailQueueEvent {
|
|
99
|
+
to: string[];
|
|
100
|
+
cc: string[];
|
|
101
|
+
bcc: string[];
|
|
102
|
+
transportName: string;
|
|
103
|
+
queue: string;
|
|
104
|
+
jobId?: string;
|
|
105
|
+
timestamp: number;
|
|
106
|
+
}
|
|
107
|
+
|
|
80
108
|
export interface MailFailedEvent {
|
|
81
109
|
messageId: string;
|
|
82
110
|
to: string[];
|
|
@@ -110,12 +138,16 @@ export interface MailConfig {
|
|
|
110
138
|
|
|
111
139
|
/**
|
|
112
140
|
* Event hooks invoked by the internal dispatch loop. Default implementations
|
|
113
|
-
* are no-ops; when an event-bus `EmitterLike` is wired, the hooks emit
|
|
114
|
-
* `mail
|
|
141
|
+
* are no-ops; when an event-bus `EmitterLike` is wired, the hooks emit the
|
|
142
|
+
* `mail:*` events (colon-namespaced, `@adonisjs/mail` parity). Tests inject
|
|
143
|
+
* spies.
|
|
115
144
|
*/
|
|
116
145
|
export interface MailHooks {
|
|
146
|
+
onSending?(event: MailSendingEvent): void;
|
|
117
147
|
onSent?(event: MailSentEvent): void;
|
|
118
148
|
onFailed?(event: MailFailedEvent): void;
|
|
149
|
+
onQueueing?(event: MailQueueEvent): void;
|
|
150
|
+
onQueued?(event: MailQueueEvent): void;
|
|
119
151
|
}
|
|
120
152
|
|
|
121
153
|
/**
|
|
@@ -204,6 +236,10 @@ export class SmtpTransport implements MailTransport {
|
|
|
204
236
|
subject: message.subject,
|
|
205
237
|
html: message.html,
|
|
206
238
|
text: message.text,
|
|
239
|
+
priority: message.priority,
|
|
240
|
+
messageId: message.messageId,
|
|
241
|
+
inReplyTo: message.inReplyTo,
|
|
242
|
+
references: message.references,
|
|
207
243
|
headers: Object.keys(message.headers).length
|
|
208
244
|
? message.headers
|
|
209
245
|
: undefined,
|
|
@@ -212,6 +248,7 @@ export class SmtpTransport implements MailTransport {
|
|
|
212
248
|
filename: att.filename,
|
|
213
249
|
content: att.content,
|
|
214
250
|
contentType: att.contentType,
|
|
251
|
+
cid: att.cid,
|
|
215
252
|
}))
|
|
216
253
|
: undefined,
|
|
217
254
|
});
|
|
@@ -297,11 +334,14 @@ export function registerTransport(
|
|
|
297
334
|
*/
|
|
298
335
|
export class Mail {
|
|
299
336
|
#transports: Map<string, MailTransport> = new Map();
|
|
337
|
+
#mailers: Map<string, Mailer> = new Map();
|
|
300
338
|
#defaultTransport: string;
|
|
301
339
|
#defaultFrom: string;
|
|
302
|
-
|
|
303
|
-
|
|
340
|
+
/** Active `FakeMail`, when `fake()` mode is on. Manager-level (captures both `send` and `sendLater`), not a transport swap. */
|
|
341
|
+
#fake: FakeMail | null = null;
|
|
304
342
|
#queue: BayQueueLike | null = null;
|
|
343
|
+
/** Default in-memory messenger for `sendLater()` when no Bay queue is wired (Adonis MemoryQueueMessenger parity). */
|
|
344
|
+
#memoryMessenger: MemoryMailMessenger;
|
|
305
345
|
#queueName: string;
|
|
306
346
|
#queueMaxAttempts: number;
|
|
307
347
|
#globalRetry: RetryConfig | undefined;
|
|
@@ -328,6 +368,7 @@ export class Mail {
|
|
|
328
368
|
this.#globalRetry = config.retry;
|
|
329
369
|
this.#hooks = options?.hooks ?? {};
|
|
330
370
|
this.#emitter = options?.emitter ?? null;
|
|
371
|
+
this.#memoryMessenger = new MemoryMailMessenger(this, this.#emitter);
|
|
331
372
|
this.#viewsRoot = config.viewsRoot;
|
|
332
373
|
// Keep mutating the process-wide global too: standalone MessageBuilder
|
|
333
374
|
// usage (not routed through this Mail) still reads it. The per-instance
|
|
@@ -371,6 +412,23 @@ export class Mail {
|
|
|
371
412
|
transport?: string,
|
|
372
413
|
): Promise<void> {
|
|
373
414
|
const transportName = transport ?? this.#defaultTransport;
|
|
415
|
+
// Fake mode is manager-level: capture the built message (and the source
|
|
416
|
+
// BaseMail, for constructor-based assertions) instead of touching a
|
|
417
|
+
// transport. Still fire the send lifecycle so event wiring stays testable.
|
|
418
|
+
if (this.#fake !== null) {
|
|
419
|
+
const message = await this.#buildMessage(arg);
|
|
420
|
+
this.#fake.trackSent(message, arg instanceof BaseMail ? arg : undefined);
|
|
421
|
+
const base = {
|
|
422
|
+
to: message.to.slice(),
|
|
423
|
+
cc: message.cc.slice(),
|
|
424
|
+
bcc: message.bcc.slice(),
|
|
425
|
+
transportName,
|
|
426
|
+
timestamp: Date.now(),
|
|
427
|
+
};
|
|
428
|
+
this.#fireSending(base);
|
|
429
|
+
this.#fireSent({ ...base, messageId: randomBytes(16).toString("hex") });
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
374
432
|
// Validate transport up-front before running any callback / prepare() side effects.
|
|
375
433
|
if (!this.#transports.has(transportName)) {
|
|
376
434
|
throw new Error(`Mail transport '${transportName}' not configured`);
|
|
@@ -381,30 +439,50 @@ export class Mail {
|
|
|
381
439
|
}
|
|
382
440
|
|
|
383
441
|
/**
|
|
384
|
-
* Enqueue a send
|
|
385
|
-
*
|
|
386
|
-
*
|
|
442
|
+
* Enqueue a send. Returns the job id. When a `@c9up/bay` `QueueManager` was
|
|
443
|
+
* wired it enqueues there; otherwise it falls back to the default in-memory
|
|
444
|
+
* messenger (immediate microtask dispatch), matching `@adonisjs/mail`'s
|
|
445
|
+
* `MemoryQueueMessenger` — `sendLater()` never throws for a missing queue.
|
|
387
446
|
*/
|
|
388
447
|
async sendLater(
|
|
389
448
|
arg: ((message: MessageBuilder) => void) | BaseMail,
|
|
390
449
|
options?: { transport?: string; queue?: string },
|
|
391
450
|
): Promise<string> {
|
|
392
|
-
if (this.#queue === null) {
|
|
393
|
-
throw new RoverError(
|
|
394
|
-
"MAIL_QUEUE_REQUIRED",
|
|
395
|
-
"mail.sendLater() requires @c9up/bay QueueManager",
|
|
396
|
-
{
|
|
397
|
-
hint: "Register @c9up/bay and pass the QueueManager to Mail via RoverProvider, or use mail.send() for synchronous delivery.",
|
|
398
|
-
},
|
|
399
|
-
);
|
|
400
|
-
}
|
|
401
451
|
const message = await this.#buildMessage(arg);
|
|
402
452
|
const queueName = options?.queue ?? this.#queueName;
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
453
|
+
const transportName = options?.transport ?? this.#defaultTransport;
|
|
454
|
+
const base: MailQueueEvent = {
|
|
455
|
+
to: message.to.slice(),
|
|
456
|
+
cc: message.cc.slice(),
|
|
457
|
+
bcc: message.bcc.slice(),
|
|
458
|
+
transportName,
|
|
459
|
+
queue: queueName,
|
|
460
|
+
timestamp: Date.now(),
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
// Fake mode: capture into the queued bucket, don't dispatch.
|
|
464
|
+
if (this.#fake !== null) {
|
|
465
|
+
this.#fake.trackQueued(
|
|
466
|
+
message,
|
|
467
|
+
arg instanceof BaseMail ? arg : undefined,
|
|
468
|
+
);
|
|
469
|
+
const jobId = `fake_${randomBytes(12).toString("hex")}`;
|
|
470
|
+
this.#fireQueueing(base);
|
|
471
|
+
this.#fireQueued({ ...base, jobId });
|
|
472
|
+
return jobId;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
this.#fireQueueing(base);
|
|
476
|
+
const jobId =
|
|
477
|
+
this.#queue !== null
|
|
478
|
+
? await this.#queue.dispatch(
|
|
479
|
+
queueName,
|
|
480
|
+
{ message, transport: options?.transport },
|
|
481
|
+
{ maxAttempts: this.#queueMaxAttempts },
|
|
482
|
+
)
|
|
483
|
+
: this.#memoryMessenger.queue(message, options?.transport);
|
|
484
|
+
this.#fireQueued({ ...base, jobId });
|
|
485
|
+
return jobId;
|
|
408
486
|
}
|
|
409
487
|
|
|
410
488
|
/**
|
|
@@ -437,6 +515,16 @@ export class Mail {
|
|
|
437
515
|
const generatedId = randomBytes(16).toString("hex");
|
|
438
516
|
let lastError: unknown;
|
|
439
517
|
|
|
518
|
+
// Fire once before the first attempt — `mail:sending` signals intent, not
|
|
519
|
+
// per-retry, matching @adonisjs/mail.
|
|
520
|
+
this.#fireSending({
|
|
521
|
+
to: message.to.slice(),
|
|
522
|
+
cc: message.cc.slice(),
|
|
523
|
+
bcc: message.bcc.slice(),
|
|
524
|
+
transportName: name,
|
|
525
|
+
timestamp: Date.now(),
|
|
526
|
+
});
|
|
527
|
+
|
|
440
528
|
for (let attempt = 1; attempt <= retry.maxAttempts; attempt += 1) {
|
|
441
529
|
let sendResult: MailSendOutcome;
|
|
442
530
|
try {
|
|
@@ -507,36 +595,46 @@ export class Mail {
|
|
|
507
595
|
return clone;
|
|
508
596
|
}
|
|
509
597
|
|
|
598
|
+
#fireSending(event: MailSendingEvent): void {
|
|
599
|
+
this.#fire("mail:sending", event, this.#hooks.onSending);
|
|
600
|
+
}
|
|
601
|
+
|
|
510
602
|
#fireSent(event: MailSentEvent): void {
|
|
511
|
-
|
|
512
|
-
// delivery outcome. Emitter errors are already defensively swallowed.
|
|
513
|
-
try {
|
|
514
|
-
this.#hooks.onSent?.(event);
|
|
515
|
-
} catch (err) {
|
|
516
|
-
process.stderr.write(
|
|
517
|
-
`[rover] onSent hook threw: ${err instanceof Error ? err.message : String(err)}\n`,
|
|
518
|
-
);
|
|
519
|
-
}
|
|
520
|
-
if (this.#emitter) {
|
|
521
|
-
try {
|
|
522
|
-
this.#emitter.emit("mail.sent", event);
|
|
523
|
-
} catch {
|
|
524
|
-
// Event bus failure ≠ mail delivery failure — swallow.
|
|
525
|
-
}
|
|
526
|
-
}
|
|
603
|
+
this.#fire("mail:sent", event, this.#hooks.onSent);
|
|
527
604
|
}
|
|
528
605
|
|
|
529
606
|
#fireFailed(event: MailFailedEvent): void {
|
|
607
|
+
this.#fire("mail:failed", event, this.#hooks.onFailed);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
#fireQueueing(event: MailQueueEvent): void {
|
|
611
|
+
this.#fire("mail:queueing", event, this.#hooks.onQueueing);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
#fireQueued(event: MailQueueEvent): void {
|
|
615
|
+
this.#fire("mail:queued", event, this.#hooks.onQueued);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Fan a lifecycle event out to the (optional, user-land, may-throw) hook and
|
|
620
|
+
* the (optional) event bus. Hook and bus failures are isolated: neither can
|
|
621
|
+
* poison the delivery outcome.
|
|
622
|
+
*/
|
|
623
|
+
#fire<T>(
|
|
624
|
+
name: string,
|
|
625
|
+
event: T,
|
|
626
|
+
hook: ((event: T) => void) | undefined,
|
|
627
|
+
): void {
|
|
530
628
|
try {
|
|
531
|
-
|
|
629
|
+
hook?.(event);
|
|
532
630
|
} catch (err) {
|
|
533
631
|
process.stderr.write(
|
|
534
|
-
`[rover]
|
|
632
|
+
`[rover] ${name} hook threw: ${err instanceof Error ? err.message : String(err)}\n`,
|
|
535
633
|
);
|
|
536
634
|
}
|
|
537
635
|
if (this.#emitter) {
|
|
538
636
|
try {
|
|
539
|
-
this.#emitter.emit(
|
|
637
|
+
this.#emitter.emit(name, event);
|
|
540
638
|
} catch {
|
|
541
639
|
// Event bus failure ≠ mail delivery failure — swallow.
|
|
542
640
|
}
|
|
@@ -560,41 +658,86 @@ export class Mail {
|
|
|
560
658
|
return result;
|
|
561
659
|
}
|
|
562
660
|
|
|
563
|
-
/**
|
|
564
|
-
|
|
661
|
+
/**
|
|
662
|
+
* Get a `Mailer` bound to a named transport, so `mail.use('mailgun').send(cb)`
|
|
663
|
+
* routes through that transport (Adonis parity). Mailers are cached per name.
|
|
664
|
+
*/
|
|
665
|
+
use(name: string): Mailer {
|
|
666
|
+
if (!this.#transports.has(name)) {
|
|
667
|
+
throw new Error(`Mail transport '${name}' not configured`);
|
|
668
|
+
}
|
|
669
|
+
let mailer = this.#mailers.get(name);
|
|
670
|
+
if (mailer === undefined) {
|
|
671
|
+
mailer = new Mailer(this, name);
|
|
672
|
+
this.#mailers.set(name, mailer);
|
|
673
|
+
}
|
|
674
|
+
return mailer;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/** @internal Resolve the raw transport instance behind a mailer name. */
|
|
678
|
+
transportFor(name: string): MailTransport {
|
|
565
679
|
const t = this.#transports.get(name);
|
|
566
680
|
if (!t) throw new Error(`Mail transport '${name}' not configured`);
|
|
567
681
|
return t;
|
|
568
682
|
}
|
|
569
683
|
|
|
570
684
|
/**
|
|
571
|
-
*
|
|
572
|
-
*
|
|
573
|
-
*
|
|
685
|
+
* Enter fake mode. Every subsequent `send()` / `sendLater()` — including via
|
|
686
|
+
* `use(name)` — is captured by the returned `FakeMail` instead of hitting a
|
|
687
|
+
* transport or the queue. Call `restore()` to exit. Throws if already faking
|
|
688
|
+
* — nested fakes always indicate a forgotten `restore()`.
|
|
574
689
|
*/
|
|
575
690
|
fake(): FakeMail {
|
|
576
|
-
if (this.#
|
|
691
|
+
if (this.#fake !== null) {
|
|
577
692
|
throw new Error("Mail.fake() already active — call restore() first");
|
|
578
693
|
}
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
if (!original) {
|
|
582
|
-
throw new Error(
|
|
583
|
-
`Cannot fake default transport '${transportName}' — not configured`,
|
|
584
|
-
);
|
|
585
|
-
}
|
|
586
|
-
const fake = new FakeMail();
|
|
587
|
-
this.#fakeSnapshot = { transportName, original };
|
|
588
|
-
this.#transports.set(transportName, fake);
|
|
589
|
-
return fake;
|
|
694
|
+
this.#fake = new FakeMail();
|
|
695
|
+
return this.#fake;
|
|
590
696
|
}
|
|
591
697
|
|
|
592
|
-
/**
|
|
698
|
+
/** Exit fake mode. No-op if not currently faking. */
|
|
593
699
|
restore(): void {
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
700
|
+
this.#fake = null;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* A `Mailer` binds a named transport to the `send` / `sendLater` API so
|
|
706
|
+
* `mail.use('mailgun').send(cb)` routes through that transport (Adonis parity).
|
|
707
|
+
* It delegates back to the owning `Mail`, so fake mode and lifecycle events are
|
|
708
|
+
* honoured uniformly.
|
|
709
|
+
*/
|
|
710
|
+
export class Mailer {
|
|
711
|
+
#mail: Mail;
|
|
712
|
+
#name: string;
|
|
713
|
+
|
|
714
|
+
constructor(mail: Mail, name: string) {
|
|
715
|
+
this.#mail = mail;
|
|
716
|
+
this.#name = name;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/** The transport name this mailer is bound to. */
|
|
720
|
+
get name(): string {
|
|
721
|
+
return this.#name;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/** The underlying transport instance. */
|
|
725
|
+
get transport(): MailTransport {
|
|
726
|
+
return this.#mail.transportFor(this.#name);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
send(arg: ((message: MessageBuilder) => void) | BaseMail): Promise<void> {
|
|
730
|
+
// Narrow so the overloaded `Mail.send` resolves without a union cast.
|
|
731
|
+
return arg instanceof BaseMail
|
|
732
|
+
? this.#mail.send(arg, this.#name)
|
|
733
|
+
: this.#mail.send(arg, this.#name);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
sendLater(
|
|
737
|
+
arg: ((message: MessageBuilder) => void) | BaseMail,
|
|
738
|
+
options?: { queue?: string },
|
|
739
|
+
): Promise<string> {
|
|
740
|
+
return this.#mail.sendLater(arg, { ...options, transport: this.#name });
|
|
598
741
|
}
|
|
599
742
|
}
|
|
600
743
|
|
package/src/MessageBuilder.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { formatAddress } from "./format.js";
|
|
1
2
|
import { renderFile as renderTemplateFile } from "./templating/SimpleTemplate.js";
|
|
2
3
|
|
|
3
4
|
export interface MailMessage {
|
|
@@ -10,15 +11,36 @@ export interface MailMessage {
|
|
|
10
11
|
html?: string;
|
|
11
12
|
text?: string;
|
|
12
13
|
attachments: MailAttachment[];
|
|
13
|
-
headers: Record<string, string>;
|
|
14
|
+
headers: Record<string, string | string[]>;
|
|
15
|
+
/** Email priority hint (nodemailer `priority`). */
|
|
16
|
+
priority?: "low" | "normal" | "high";
|
|
17
|
+
/** Custom `Message-ID` header (threading / idempotency). */
|
|
18
|
+
messageId?: string;
|
|
19
|
+
/** `In-Reply-To` header — the message id this email replies to. */
|
|
20
|
+
inReplyTo?: string;
|
|
21
|
+
/** `References` header — the thread's message ids. */
|
|
22
|
+
references?: string[];
|
|
14
23
|
}
|
|
15
24
|
|
|
16
25
|
export interface MailAttachment {
|
|
17
26
|
filename: string;
|
|
18
27
|
content: Buffer | string;
|
|
19
28
|
contentType?: string;
|
|
29
|
+
/** Content-ID for inline (CID) embedding — set via `embedData()`. */
|
|
30
|
+
cid?: string;
|
|
20
31
|
}
|
|
21
32
|
|
|
33
|
+
/**
|
|
34
|
+
* A recipient in object form (Adonis parity). `name` is optional and, when
|
|
35
|
+
* present, produces the `"Name" <address>` display form.
|
|
36
|
+
*/
|
|
37
|
+
export interface RecipientObject {
|
|
38
|
+
address: string;
|
|
39
|
+
name?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type Recipient = string | RecipientObject;
|
|
43
|
+
|
|
22
44
|
export class MessageBuilder {
|
|
23
45
|
#msg: MailMessage = {
|
|
24
46
|
from: "",
|
|
@@ -31,26 +53,37 @@ export class MessageBuilder {
|
|
|
31
53
|
};
|
|
32
54
|
#pendingView: { path: string; data: Record<string, unknown> } | null = null;
|
|
33
55
|
|
|
34
|
-
from(address: string): this {
|
|
35
|
-
this.#msg.from = address;
|
|
56
|
+
from(address: string, name?: string): this {
|
|
57
|
+
this.#msg.from = formatAddress(address, name);
|
|
36
58
|
return this;
|
|
37
59
|
}
|
|
38
|
-
|
|
39
|
-
|
|
60
|
+
|
|
61
|
+
to(address: string, name?: string): this;
|
|
62
|
+
to(addresses: Recipient[]): this;
|
|
63
|
+
to(address: string | Recipient[], name?: string): this {
|
|
64
|
+
addRecipients(this.#msg.to, address, name);
|
|
40
65
|
return this;
|
|
41
66
|
}
|
|
42
|
-
|
|
43
|
-
|
|
67
|
+
|
|
68
|
+
cc(address: string, name?: string): this;
|
|
69
|
+
cc(addresses: Recipient[]): this;
|
|
70
|
+
cc(address: string | Recipient[], name?: string): this {
|
|
71
|
+
addRecipients(this.#msg.cc, address, name);
|
|
44
72
|
return this;
|
|
45
73
|
}
|
|
46
|
-
|
|
47
|
-
|
|
74
|
+
|
|
75
|
+
bcc(address: string, name?: string): this;
|
|
76
|
+
bcc(addresses: Recipient[]): this;
|
|
77
|
+
bcc(address: string | Recipient[], name?: string): this {
|
|
78
|
+
addRecipients(this.#msg.bcc, address, name);
|
|
48
79
|
return this;
|
|
49
80
|
}
|
|
50
|
-
|
|
51
|
-
|
|
81
|
+
|
|
82
|
+
replyTo(address: string, name?: string): this {
|
|
83
|
+
this.#msg.replyTo = formatAddress(address, name);
|
|
52
84
|
return this;
|
|
53
85
|
}
|
|
86
|
+
|
|
54
87
|
subject(text: string): this {
|
|
55
88
|
this.#msg.subject = text;
|
|
56
89
|
return this;
|
|
@@ -64,6 +97,30 @@ export class MessageBuilder {
|
|
|
64
97
|
return this;
|
|
65
98
|
}
|
|
66
99
|
|
|
100
|
+
/** Email priority (`low` | `normal` | `high`). */
|
|
101
|
+
priority(priority: "low" | "normal" | "high"): this {
|
|
102
|
+
this.#msg.priority = priority;
|
|
103
|
+
return this;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Set a custom `Message-ID` header. */
|
|
107
|
+
messageId(messageId: string): this {
|
|
108
|
+
this.#msg.messageId = messageId;
|
|
109
|
+
return this;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Set the `In-Reply-To` header for threading replies. */
|
|
113
|
+
inReplyTo(messageId: string): this {
|
|
114
|
+
this.#msg.inReplyTo = messageId;
|
|
115
|
+
return this;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Set the `References` header (thread message ids). */
|
|
119
|
+
references(messageIds: string[]): this {
|
|
120
|
+
this.#msg.references = messageIds.slice();
|
|
121
|
+
return this;
|
|
122
|
+
}
|
|
123
|
+
|
|
67
124
|
attach(
|
|
68
125
|
filename: string,
|
|
69
126
|
content: Buffer | string,
|
|
@@ -73,7 +130,18 @@ export class MessageBuilder {
|
|
|
73
130
|
return this;
|
|
74
131
|
}
|
|
75
132
|
|
|
76
|
-
|
|
133
|
+
/**
|
|
134
|
+
* Embed inline content referenced by a Content-ID. Use `cid:<cid>` inside the
|
|
135
|
+
* HTML body to reference it. Content-based (rover is agnostic / no-fs): the
|
|
136
|
+
* path-based `embed(file, cid)` form from `@adonisjs/mail` is a deliberate
|
|
137
|
+
* divergence — pass the bytes directly instead.
|
|
138
|
+
*/
|
|
139
|
+
embedData(content: Buffer | string, cid: string, contentType?: string): this {
|
|
140
|
+
this.#msg.attachments.push({ filename: cid, content, contentType, cid });
|
|
141
|
+
return this;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
header(key: string, value: string | string[]): this {
|
|
77
145
|
this.#msg.headers[key] = value;
|
|
78
146
|
return this;
|
|
79
147
|
}
|
|
@@ -107,3 +175,26 @@ export class MessageBuilder {
|
|
|
107
175
|
return this.#msg;
|
|
108
176
|
}
|
|
109
177
|
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Append one recipient, an array of recipients, or a `(address, name)` pair to
|
|
181
|
+
* a recipient list, formatting each into the `"Name" <address>` display form
|
|
182
|
+
* when a name is present.
|
|
183
|
+
*/
|
|
184
|
+
function addRecipients(
|
|
185
|
+
list: string[],
|
|
186
|
+
address: string | Recipient[],
|
|
187
|
+
name?: string,
|
|
188
|
+
): void {
|
|
189
|
+
if (Array.isArray(address)) {
|
|
190
|
+
for (const entry of address) {
|
|
191
|
+
list.push(
|
|
192
|
+
typeof entry === "string"
|
|
193
|
+
? entry
|
|
194
|
+
: formatAddress(entry.address, entry.name),
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
list.push(formatAddress(address, name));
|
|
200
|
+
}
|
package/src/format.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared address formatting helper. Kept dependency-free and in its own module
|
|
3
|
+
* so both `BaseMail` and `MessageBuilder` can import it without re-introducing
|
|
4
|
+
* the BaseMail ↔ MessageBuilder value cycle.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Format a recipient address with an optional display name into the
|
|
9
|
+
* `"Name" <address>` form (RFC 5322 quoted display name). A bare address —
|
|
10
|
+
* or an empty/whitespace-only name — is returned unchanged.
|
|
11
|
+
*/
|
|
12
|
+
export function formatAddress(address: string, name?: string): string {
|
|
13
|
+
return name !== undefined && name !== "" ? `"${name}" <${address}>` : address;
|
|
14
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
export type { MailAddress } from "./BaseMail.js";
|
|
2
2
|
export { BaseMail } from "./BaseMail.js";
|
|
3
3
|
export { defineConfig } from "./config.js";
|
|
4
|
-
export { RoverError } from "./RoverError.js";
|
|
5
4
|
export type {
|
|
6
5
|
EmitterLike,
|
|
7
6
|
MailAttachment,
|
|
@@ -9,6 +8,8 @@ export type {
|
|
|
9
8
|
MailFailedEvent,
|
|
10
9
|
MailHooks,
|
|
11
10
|
MailMessage,
|
|
11
|
+
MailQueueEvent,
|
|
12
|
+
MailSendingEvent,
|
|
12
13
|
MailSendOutcome,
|
|
13
14
|
MailSendResult,
|
|
14
15
|
MailSentEvent,
|
|
@@ -18,10 +19,13 @@ export type {
|
|
|
18
19
|
export {
|
|
19
20
|
LogTransport,
|
|
20
21
|
Mail,
|
|
22
|
+
Mailer,
|
|
21
23
|
MessageBuilder,
|
|
22
24
|
registerTransport,
|
|
23
25
|
SmtpTransport,
|
|
24
26
|
} from "./Mail.js";
|
|
27
|
+
export type { Recipient, RecipientObject } from "./MessageBuilder.js";
|
|
28
|
+
export { RoverError } from "./RoverError.js";
|
|
25
29
|
export { default as RoverProvider } from "./RoverProvider.js";
|
|
26
30
|
export {
|
|
27
31
|
DEFAULT_RETRY_CONFIG,
|
package/src/queue/MailJob.ts
CHANGED