@c9up/rover 0.1.14 → 0.1.16
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/dist/Mail.d.ts +8 -4
- package/dist/Mail.d.ts.map +1 -1
- package/dist/Mail.js +7 -6
- package/dist/Mail.js.map +1 -1
- package/dist/MessageBuilder.js +1 -1
- package/dist/MessageBuilder.js.map +1 -1
- package/dist/RoverProvider.d.ts +1 -0
- package/dist/RoverProvider.d.ts.map +1 -1
- package/dist/RoverProvider.js +9 -3
- package/dist/RoverProvider.js.map +1 -1
- package/dist/augmentations.d.ts +29 -0
- package/dist/augmentations.d.ts.map +1 -0
- package/dist/augmentations.js +18 -0
- package/dist/augmentations.js.map +1 -0
- package/dist/emitSafely.d.ts +18 -0
- package/dist/emitSafely.d.ts.map +1 -0
- package/dist/emitSafely.js +22 -0
- package/dist/emitSafely.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/queue/MemoryMailMessenger.d.ts.map +1 -1
- package/dist/queue/MemoryMailMessenger.js +14 -15
- package/dist/queue/MemoryMailMessenger.js.map +1 -1
- package/dist/retry.d.ts +10 -1
- package/dist/retry.d.ts.map +1 -1
- package/dist/retry.js +9 -0
- package/dist/retry.js.map +1 -1
- package/dist/testing/FakeMail.d.ts +4 -2
- package/dist/testing/FakeMail.d.ts.map +1 -1
- package/dist/testing/FakeMail.js +4 -19
- package/dist/testing/FakeMail.js.map +1 -1
- package/dist/transports/BrevoTransport.js +6 -3
- package/dist/transports/BrevoTransport.js.map +1 -1
- package/dist/transports/MailgunTransport.d.ts.map +1 -1
- package/dist/transports/MailgunTransport.js +59 -45
- package/dist/transports/MailgunTransport.js.map +1 -1
- package/dist/transports/SendGridTransport.d.ts.map +1 -1
- package/dist/transports/SendGridTransport.js +80 -82
- package/dist/transports/SendGridTransport.js.map +1 -1
- package/dist/transports/SparkPostTransport.js +6 -3
- package/dist/transports/SparkPostTransport.js.map +1 -1
- package/dist/webhooks/context.d.ts +9 -1
- package/dist/webhooks/context.d.ts.map +1 -1
- package/dist/webhooks/mailgun.d.ts.map +1 -1
- package/dist/webhooks/mailgun.js +2 -1
- package/dist/webhooks/mailgun.js.map +1 -1
- package/dist/webhooks/resend.d.ts.map +1 -1
- package/dist/webhooks/resend.js +2 -1
- package/dist/webhooks/resend.js.map +1 -1
- package/dist/webhooks/sendgrid.d.ts.map +1 -1
- package/dist/webhooks/sendgrid.js +2 -1
- package/dist/webhooks/sendgrid.js.map +1 -1
- 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 +8 -7
- package/scripts/build-napi-types.mjs +4 -3
- package/scripts/generate-napi-types.mjs +9 -6
- package/src/Mail.ts +21 -11
- package/src/MessageBuilder.ts +1 -1
- package/src/RoverProvider.ts +9 -3
- package/src/augmentations.ts +33 -0
- package/src/emitSafely.ts +31 -0
- package/src/index.ts +2 -0
- package/src/queue/MemoryMailMessenger.ts +15 -14
- package/src/retry.ts +11 -2
- package/src/testing/FakeMail.ts +12 -3
- package/src/transports/BrevoTransport.ts +6 -3
- package/src/transports/MailgunTransport.ts +64 -56
- package/src/transports/SendGridTransport.ts +86 -117
- package/src/transports/SparkPostTransport.ts +6 -3
- package/src/webhooks/context.ts +9 -1
- package/src/webhooks/mailgun.ts +2 -1
- package/src/webhooks/resend.ts +2 -1
- package/src/webhooks/sendgrid.ts +2 -1
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import formData from "form-data";
|
|
2
|
-
// mailgun.js is UMD-bundled; the class lives on `.default` under NodeNext.
|
|
3
|
-
import MailgunModule from "mailgun.js";
|
|
4
1
|
import {
|
|
5
2
|
type MailMessage,
|
|
6
3
|
type MailSendOutcome,
|
|
@@ -9,6 +6,7 @@ import {
|
|
|
9
6
|
} from "../Mail.js";
|
|
10
7
|
import { attachmentsFor, headerValue } from "../MessageBuilder.js";
|
|
11
8
|
import { RoverError } from "../RoverError.js";
|
|
9
|
+
import { fetchWithTimeout, wrapFetchNetworkError } from "./fetchError.js";
|
|
12
10
|
|
|
13
11
|
const stripCrlf = (v: string): string => v.replace(/[\r\n]/g, "");
|
|
14
12
|
const normalizeConfig = (v: string): string => stripCrlf(v).trim();
|
|
@@ -23,19 +21,10 @@ const redactSecrets = (s: string): string =>
|
|
|
23
21
|
.replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/g, "Bearer [REDACTED]")
|
|
24
22
|
.replace(/Basic\s+[A-Za-z0-9+/=]+/g, "Basic [REDACTED]");
|
|
25
23
|
|
|
26
|
-
/** Minimal mailgun.js client surface — matches the subset we need. */
|
|
27
|
-
interface MailgunClientLike {
|
|
28
|
-
messages: {
|
|
29
|
-
create(
|
|
30
|
-
domain: string,
|
|
31
|
-
data: Record<string, unknown>,
|
|
32
|
-
): Promise<{ id?: string; message?: string; status?: number }>;
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
|
|
36
24
|
export class MailgunTransport implements MailTransport {
|
|
37
|
-
#
|
|
25
|
+
#apiKey: string;
|
|
38
26
|
#domain: string;
|
|
27
|
+
#baseUrl: string;
|
|
39
28
|
|
|
40
29
|
constructor(config: Record<string, unknown>) {
|
|
41
30
|
const apiKey =
|
|
@@ -78,24 +67,11 @@ export class MailgunTransport implements MailTransport {
|
|
|
78
67
|
? "https://api.eu.mailgun.net"
|
|
79
68
|
: "https://api.mailgun.net";
|
|
80
69
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
injected &&
|
|
87
|
-
typeof injected === "object" &&
|
|
88
|
-
"messages" in (injected as object) &&
|
|
89
|
-
typeof (injected as MailgunClientLike).messages?.create === "function"
|
|
90
|
-
) {
|
|
91
|
-
this.#client = injected as MailgunClientLike;
|
|
92
|
-
} else {
|
|
93
|
-
// mailgun.js ships a UMD-style default export; the class constructor
|
|
94
|
-
// lives on `.default` in the typings (`static get default`).
|
|
95
|
-
const MailgunCtor = MailgunModule.default;
|
|
96
|
-
const mailgun = new MailgunCtor(formData);
|
|
97
|
-
this.#client = mailgun.client({ username: "api", key: apiKey, url });
|
|
98
|
-
}
|
|
70
|
+
this.#apiKey = apiKey;
|
|
71
|
+
this.#baseUrl =
|
|
72
|
+
typeof config.baseUrl === "string" && config.baseUrl.length > 0
|
|
73
|
+
? normalizeConfig(config.baseUrl).replace(/\/+$/, "")
|
|
74
|
+
: url;
|
|
99
75
|
}
|
|
100
76
|
|
|
101
77
|
async send(message: MailMessage): Promise<MailSendOutcome> {
|
|
@@ -134,38 +110,70 @@ export class MailgunTransport implements MailTransport {
|
|
|
134
110
|
? v.map(stripCrlf).join(", ")
|
|
135
111
|
: stripCrlf(v);
|
|
136
112
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
|
|
113
|
+
// Mailgun takes `multipart/form-data`; Node builds it, and `fetch` sets
|
|
114
|
+
// the boundary. Scalars first, then one file part per attachment.
|
|
115
|
+
const form = new FormData();
|
|
116
|
+
for (const [key, value] of Object.entries(data)) {
|
|
117
|
+
form.append(key, Array.isArray(value) ? value.join(", ") : String(value));
|
|
118
|
+
}
|
|
119
|
+
for (const att of attachmentsFor(message)) {
|
|
120
|
+
const bytes = Buffer.from(att.content as Buffer | string);
|
|
121
|
+
const blob = att.contentType
|
|
122
|
+
? new Blob([bytes], { type: stripCrlf(att.contentType) })
|
|
123
|
+
: new Blob([bytes]);
|
|
124
|
+
form.append("attachment", blob, stripCrlf(att.filename));
|
|
149
125
|
}
|
|
150
126
|
|
|
127
|
+
let res: Response;
|
|
151
128
|
try {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
129
|
+
res = await fetchWithTimeout(
|
|
130
|
+
"Mailgun",
|
|
131
|
+
`${this.#baseUrl}/v3/${encodeURIComponent(this.#domain)}/messages`,
|
|
132
|
+
{
|
|
133
|
+
method: "POST",
|
|
134
|
+
headers: {
|
|
135
|
+
// HTTP Basic, the username is literally `api`.
|
|
136
|
+
Authorization: `Basic ${Buffer.from(`api:${this.#apiKey}`).toString("base64")}`,
|
|
137
|
+
Accept: "application/json",
|
|
138
|
+
},
|
|
139
|
+
body: form,
|
|
140
|
+
},
|
|
141
|
+
);
|
|
155
142
|
} catch (err) {
|
|
156
|
-
throw
|
|
143
|
+
throw wrapFetchNetworkError("mailgun", err);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const raw = await res.text();
|
|
147
|
+
if (!res.ok) {
|
|
148
|
+
throw wrapMailgunError({
|
|
149
|
+
status: res.status,
|
|
150
|
+
message: raw,
|
|
151
|
+
headers: Object.fromEntries(res.headers.entries()),
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
// A 200 carries `{ id, message }`; anything unparseable is still a
|
|
155
|
+
// success on the wire, so it is not turned into an error.
|
|
156
|
+
try {
|
|
157
|
+
const parsed: unknown = JSON.parse(raw);
|
|
158
|
+
const id =
|
|
159
|
+
typeof parsed === "object" && parsed !== null
|
|
160
|
+
? Reflect.get(parsed, "id")
|
|
161
|
+
: undefined;
|
|
162
|
+
return typeof id === "string" ? { providerId: id } : undefined;
|
|
163
|
+
} catch {
|
|
164
|
+
return undefined;
|
|
157
165
|
}
|
|
158
166
|
}
|
|
159
167
|
}
|
|
160
168
|
|
|
161
169
|
/**
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
* consumers don't need to branch on provider.
|
|
170
|
+
* Map an upstream refusal onto the uniform `E_MAIL_PROVIDER_ERROR` every
|
|
171
|
+
* transport raises, so retry and observability never branch on the provider.
|
|
165
172
|
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
173
|
+
* The shape is what the HTTP response gives: a status, the body as the
|
|
174
|
+
* message, and the response headers — `Retry-After` among them, which the
|
|
175
|
+
* backoff honours. A network failure never reaches here; `wrapFetchNetworkError`
|
|
176
|
+
* carries the errno so the retry predicate can still see it.
|
|
169
177
|
*/
|
|
170
178
|
function wrapMailgunError(err: unknown): RoverError {
|
|
171
179
|
if (err instanceof RoverError) return err;
|
|
@@ -176,8 +184,8 @@ function wrapMailgunError(err: unknown): RoverError {
|
|
|
176
184
|
code?: string;
|
|
177
185
|
headers?: Record<string, string | string[]>;
|
|
178
186
|
};
|
|
179
|
-
// Coerce string-typed status
|
|
180
|
-
//
|
|
187
|
+
// Coerce a string-typed status into a number: the field is built here, but
|
|
188
|
+
// a caller injecting a fake response may still hand one over as text.
|
|
181
189
|
const statusNum = Number(anyErr.status);
|
|
182
190
|
const status = Number.isFinite(statusNum) ? statusNum : 0;
|
|
183
191
|
const providerMessage = redactSecrets(
|
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
import sgMail, {
|
|
2
|
-
type MailDataRequired,
|
|
3
|
-
type MailService,
|
|
4
|
-
} from "@sendgrid/mail";
|
|
5
1
|
import {
|
|
6
2
|
type MailMessage,
|
|
7
3
|
type MailSendOutcome,
|
|
@@ -10,6 +6,7 @@ import {
|
|
|
10
6
|
} from "../Mail.js";
|
|
11
7
|
import { attachmentsFor, headerValue } from "../MessageBuilder.js";
|
|
12
8
|
import { RoverError } from "../RoverError.js";
|
|
9
|
+
import { fetchWithTimeout, wrapFetchNetworkError } from "./fetchError.js";
|
|
13
10
|
|
|
14
11
|
const stripCrlf = (v: string): string => v.replace(/[\r\n]/g, "");
|
|
15
12
|
const normalizeConfig = (v: string): string => stripCrlf(v).trim();
|
|
@@ -24,26 +21,9 @@ const redactSecrets = (s: string): string =>
|
|
|
24
21
|
.replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/g, "Bearer [REDACTED]")
|
|
25
22
|
.replace(/Basic\s+[A-Za-z0-9+/=]+/g, "Basic [REDACTED]");
|
|
26
23
|
|
|
27
|
-
/**
|
|
28
|
-
* Minimal slice of the SendGrid client we depend on. A per-transport client
|
|
29
|
-
* instance is created in the constructor so concurrent transports with
|
|
30
|
-
* different API keys cannot race each other — the original module-level
|
|
31
|
-
* `sgMail.setApiKey()` would have been a multi-tenant foot-gun.
|
|
32
|
-
*/
|
|
33
|
-
interface SendGridClientLike {
|
|
34
|
-
setApiKey(apiKey: string): void;
|
|
35
|
-
send(
|
|
36
|
-
data: MailDataRequired,
|
|
37
|
-
): Promise<
|
|
38
|
-
[
|
|
39
|
-
{ statusCode: number; headers: Record<string, string | string[]> },
|
|
40
|
-
unknown,
|
|
41
|
-
]
|
|
42
|
-
>;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
24
|
export class SendGridTransport implements MailTransport {
|
|
46
|
-
#
|
|
25
|
+
#apiKey: string;
|
|
26
|
+
#baseUrl: string;
|
|
47
27
|
|
|
48
28
|
constructor(config: Record<string, unknown>) {
|
|
49
29
|
const apiKey =
|
|
@@ -56,86 +36,96 @@ export class SendGridTransport implements MailTransport {
|
|
|
56
36
|
);
|
|
57
37
|
}
|
|
58
38
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
typeof injected === "object" &&
|
|
65
|
-
typeof (injected as SendGridClientLike).send === "function" &&
|
|
66
|
-
typeof (injected as SendGridClientLike).setApiKey === "function"
|
|
67
|
-
) {
|
|
68
|
-
this.#client = injected as SendGridClientLike;
|
|
69
|
-
} else {
|
|
70
|
-
// Per-instance MailService (not the shared `sgMail` singleton) so
|
|
71
|
-
// `setApiKey` can't race across multiple transports.
|
|
72
|
-
this.#client = new (resolveMailServiceCtor(sgMail))();
|
|
73
|
-
}
|
|
74
|
-
this.#client.setApiKey(apiKey);
|
|
39
|
+
this.#apiKey = apiKey;
|
|
40
|
+
this.#baseUrl =
|
|
41
|
+
typeof config.baseUrl === "string" && config.baseUrl.length > 0
|
|
42
|
+
? normalizeConfig(config.baseUrl).replace(/\/+$/, "")
|
|
43
|
+
: "https://api.sendgrid.com";
|
|
75
44
|
}
|
|
76
45
|
|
|
77
46
|
async send(message: MailMessage): Promise<MailSendOutcome> {
|
|
78
47
|
assertHasRecipients(message);
|
|
79
|
-
const content = buildSendGridContent(message);
|
|
80
48
|
|
|
81
|
-
// CRLF
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
49
|
+
// CRLF is stripped at the wire boundary regardless of what the provider
|
|
50
|
+
// promises — a header injected through a recipient or a subject is the
|
|
51
|
+
// one thing a transport must never pass on.
|
|
52
|
+
const address = (value: string): { email: string } => ({
|
|
53
|
+
email: stripCrlf(value),
|
|
54
|
+
});
|
|
55
|
+
const personalization: Record<string, unknown> = {
|
|
56
|
+
// `to` may be empty when a message is bcc-only, which SendGrid allows.
|
|
57
|
+
to: message.to.map(address),
|
|
58
|
+
};
|
|
59
|
+
if (message.cc.length) personalization.cc = message.cc.map(address);
|
|
60
|
+
if (message.bcc.length) personalization.bcc = message.bcc.map(address);
|
|
61
|
+
|
|
62
|
+
const body: Record<string, unknown> = {
|
|
63
|
+
// The v3 API groups recipients under `personalizations`; the SDK's
|
|
64
|
+
// flat shape was its own, and this is what actually goes on the wire.
|
|
65
|
+
personalizations: [personalization],
|
|
66
|
+
from: address(message.from),
|
|
86
67
|
subject: stripCrlf(message.subject),
|
|
87
|
-
content,
|
|
88
|
-
...(message.cc.length ? { cc: message.cc.map(stripCrlf) } : {}),
|
|
89
|
-
...(message.bcc.length ? { bcc: message.bcc.map(stripCrlf) } : {}),
|
|
90
|
-
...(message.replyTo ? { replyTo: stripCrlf(message.replyTo) } : {}),
|
|
91
|
-
...(Object.keys(message.headers).length
|
|
92
|
-
? {
|
|
93
|
-
headers: Object.fromEntries(
|
|
94
|
-
Object.entries(message.headers).map(([k, raw]) => {
|
|
95
|
-
const v = headerValue(raw);
|
|
96
|
-
return [
|
|
97
|
-
stripCrlf(k),
|
|
98
|
-
Array.isArray(v) ? v.map(stripCrlf).join(", ") : stripCrlf(v),
|
|
99
|
-
];
|
|
100
|
-
}),
|
|
101
|
-
),
|
|
102
|
-
}
|
|
103
|
-
: {}),
|
|
104
|
-
...(attachmentsFor(message).length
|
|
105
|
-
? {
|
|
106
|
-
attachments: attachmentsFor(message).map((att) => {
|
|
107
|
-
const entry: {
|
|
108
|
-
filename: string;
|
|
109
|
-
content: string;
|
|
110
|
-
type?: string;
|
|
111
|
-
disposition: "attachment";
|
|
112
|
-
} = {
|
|
113
|
-
filename: stripCrlf(att.filename),
|
|
114
|
-
content: Buffer.from(att.content as Buffer | string).toString(
|
|
115
|
-
"base64",
|
|
116
|
-
),
|
|
117
|
-
disposition: "attachment" as const,
|
|
118
|
-
};
|
|
119
|
-
if (att.contentType) entry.type = stripCrlf(att.contentType);
|
|
120
|
-
return entry;
|
|
121
|
-
}),
|
|
122
|
-
}
|
|
123
|
-
: {}),
|
|
68
|
+
content: buildSendGridContent(message),
|
|
124
69
|
};
|
|
70
|
+
if (message.replyTo) body.reply_to = address(message.replyTo);
|
|
71
|
+
if (Object.keys(message.headers).length) {
|
|
72
|
+
body.headers = Object.fromEntries(
|
|
73
|
+
Object.entries(message.headers).map(([k, raw]) => {
|
|
74
|
+
const v = headerValue(raw);
|
|
75
|
+
return [
|
|
76
|
+
stripCrlf(k),
|
|
77
|
+
Array.isArray(v) ? v.map(stripCrlf).join(", ") : stripCrlf(v),
|
|
78
|
+
];
|
|
79
|
+
}),
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
const attachments = attachmentsFor(message);
|
|
83
|
+
if (attachments.length > 0) {
|
|
84
|
+
body.attachments = attachments.map((att) => {
|
|
85
|
+
const entry: Record<string, string> = {
|
|
86
|
+
filename: stripCrlf(att.filename),
|
|
87
|
+
content: Buffer.from(att.content as Buffer | string).toString(
|
|
88
|
+
"base64",
|
|
89
|
+
),
|
|
90
|
+
disposition: "attachment",
|
|
91
|
+
};
|
|
92
|
+
if (att.contentType) entry.type = stripCrlf(att.contentType);
|
|
93
|
+
return entry;
|
|
94
|
+
});
|
|
95
|
+
}
|
|
125
96
|
|
|
97
|
+
let res: Response;
|
|
126
98
|
try {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
99
|
+
res = await fetchWithTimeout(
|
|
100
|
+
"SendGrid",
|
|
101
|
+
`${this.#baseUrl}/v3/mail/send`,
|
|
102
|
+
{
|
|
103
|
+
method: "POST",
|
|
104
|
+
headers: {
|
|
105
|
+
Authorization: `Bearer ${this.#apiKey}`,
|
|
106
|
+
"Content-Type": "application/json",
|
|
107
|
+
},
|
|
108
|
+
body: JSON.stringify(body),
|
|
109
|
+
},
|
|
110
|
+
);
|
|
136
111
|
} catch (err) {
|
|
137
|
-
throw
|
|
112
|
+
throw wrapFetchNetworkError("sendgrid", err);
|
|
138
113
|
}
|
|
114
|
+
|
|
115
|
+
if (!res.ok) {
|
|
116
|
+
// Shaped as the mapper already reads it, so the mapping stays one
|
|
117
|
+
// piece of logic rather than two that can drift.
|
|
118
|
+
throw wrapSendGridError({
|
|
119
|
+
response: {
|
|
120
|
+
statusCode: res.status,
|
|
121
|
+
body: await res.text(),
|
|
122
|
+
headers: Object.fromEntries(res.headers.entries()),
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
// A 202 carries no body; the id is in the header.
|
|
127
|
+
const id = res.headers.get("x-message-id");
|
|
128
|
+
return id !== null && id.length > 0 ? { providerId: id } : undefined;
|
|
139
129
|
}
|
|
140
130
|
}
|
|
141
131
|
|
|
@@ -158,7 +148,7 @@ function assertHasRecipients(message: MailMessage): void {
|
|
|
158
148
|
/**
|
|
159
149
|
* Build the SendGrid v3 `content[]`: text/plain when text is set, text/html when
|
|
160
150
|
* html is set, always at least one entry (SendGrid rejects empty content). The
|
|
161
|
-
*
|
|
151
|
+
* At least one entry always: SendGrid refuses a message with empty content.
|
|
162
152
|
*/
|
|
163
153
|
function buildSendGridContent(
|
|
164
154
|
message: MailMessage,
|
|
@@ -177,7 +167,9 @@ function buildSendGridContent(
|
|
|
177
167
|
|
|
178
168
|
function wrapSendGridError(err: unknown): RoverError {
|
|
179
169
|
if (err instanceof RoverError) return err;
|
|
180
|
-
//
|
|
170
|
+
// Built from the HTTP response here, but the older SDK shape
|
|
171
|
+
// (`{ code, message, response: { body, headers, statusCode } }`) is still
|
|
172
|
+
// accepted so an injected fake or a wrapped error maps the same way.
|
|
181
173
|
const anyErr = err as {
|
|
182
174
|
code?: number | string;
|
|
183
175
|
message?: string;
|
|
@@ -224,27 +216,4 @@ function wrapSendGridError(err: unknown): RoverError {
|
|
|
224
216
|
);
|
|
225
217
|
}
|
|
226
218
|
|
|
227
|
-
/**
|
|
228
|
-
* Type-guard resolver for the `MailService` constructor attached to the
|
|
229
|
-
* module's default export at runtime. The cerebrum forbids `as unknown as T`;
|
|
230
|
-
* here we receive `sgMail` through a parameter typed `unknown`, narrow with
|
|
231
|
-
* runtime `typeof` checks, and return a single cast to a precise callable
|
|
232
|
-
* type. No double-cast chain, no `as unknown` anchor.
|
|
233
|
-
*/
|
|
234
|
-
function resolveMailServiceCtor(mod: unknown): new () => MailService {
|
|
235
|
-
if (mod && typeof mod === "object" && "MailService" in mod) {
|
|
236
|
-
const candidate = (mod as { MailService: unknown }).MailService;
|
|
237
|
-
if (typeof candidate === "function") {
|
|
238
|
-
return candidate as new () => MailService;
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
throw new RoverError(
|
|
242
|
-
"E_MAIL_PROVIDER_CONFIG",
|
|
243
|
-
"@sendgrid/mail runtime does not expose `.MailService` — upgrade to v8+",
|
|
244
|
-
{
|
|
245
|
-
hint: "Expected `module.exports.MailService` to be the MailService class (index.js attaches it).",
|
|
246
|
-
},
|
|
247
|
-
);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
219
|
registerTransport("sendgrid", (config) => new SendGridTransport(config));
|
|
@@ -177,9 +177,12 @@ export class SparkPostTransport implements MailTransport {
|
|
|
177
177
|
function parseAddress(input: string): { email: string; name?: string } {
|
|
178
178
|
const s = stripCrlf(input).trim();
|
|
179
179
|
const match = s.match(/^(.*)<([^>]+)>\s*$/);
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
180
|
+
// Both groups are required by the pattern, so a match carries both —
|
|
181
|
+
// named rather than indexed, which is what says so.
|
|
182
|
+
const [, rawName, rawEmail] = match ?? [];
|
|
183
|
+
if (rawEmail !== undefined) {
|
|
184
|
+
const email = rawEmail.trim();
|
|
185
|
+
const name = (rawName ?? "").trim().replace(/^"|"$/g, "").trim();
|
|
183
186
|
return name ? { email, name } : { email };
|
|
184
187
|
}
|
|
185
188
|
return { email: s };
|
package/src/webhooks/context.ts
CHANGED
|
@@ -37,5 +37,13 @@ export type WebhookMiddleware = (
|
|
|
37
37
|
) => Promise<void>;
|
|
38
38
|
|
|
39
39
|
export interface WebhookEmitter {
|
|
40
|
-
|
|
40
|
+
/**
|
|
41
|
+
* `unknown`, not `void`. `@adonisjs/events` declares
|
|
42
|
+
* `emit(): Promise<void>` and rethrows when a listener fails and no error
|
|
43
|
+
* handler is registered — and a `void` return ACCEPTS a promise-returning
|
|
44
|
+
* function, so the call site reads as if there were nothing to handle.
|
|
45
|
+
* Duck-typed, so it stays wider than Adonis's own class: a Node
|
|
46
|
+
* EventEmitter returns boolean.
|
|
47
|
+
*/
|
|
48
|
+
emit(event: string, data: unknown): unknown;
|
|
41
49
|
}
|
package/src/webhooks/mailgun.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Buffer } from "node:buffer";
|
|
2
2
|
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
3
|
+
import { emitSafely } from "../emitSafely.js";
|
|
3
4
|
import type {
|
|
4
5
|
WebhookEmitter,
|
|
5
6
|
WebhookHttpContext,
|
|
@@ -94,7 +95,7 @@ export function createMailgunWebhookHandler(
|
|
|
94
95
|
const eventName = EVENT_MAP[data.event];
|
|
95
96
|
if (eventName) {
|
|
96
97
|
try {
|
|
97
|
-
emitter
|
|
98
|
+
emitSafely(emitter, eventName, {
|
|
98
99
|
messageId: data.message?.headers?.["message-id"] ?? "",
|
|
99
100
|
to: data.recipient ?? "",
|
|
100
101
|
reason: data.reason,
|
package/src/webhooks/resend.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Buffer } from "node:buffer";
|
|
2
2
|
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
3
|
+
import { emitSafely } from "../emitSafely.js";
|
|
3
4
|
import type {
|
|
4
5
|
WebhookEmitter,
|
|
5
6
|
WebhookHttpContext,
|
|
@@ -176,7 +177,7 @@ function emitMappedEvent(
|
|
|
176
177
|
? (payload.data.to[0] ?? "")
|
|
177
178
|
: (payload.data?.to ?? "");
|
|
178
179
|
try {
|
|
179
|
-
emitter
|
|
180
|
+
emitSafely(emitter, mapped, {
|
|
180
181
|
messageId: payload.data?.email_id ?? "",
|
|
181
182
|
to,
|
|
182
183
|
reason: payload.data?.reason,
|
package/src/webhooks/sendgrid.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Buffer } from "node:buffer";
|
|
2
2
|
import { createPublicKey, verify } from "node:crypto";
|
|
3
|
+
import { emitSafely } from "../emitSafely.js";
|
|
3
4
|
import type {
|
|
4
5
|
WebhookEmitter,
|
|
5
6
|
WebhookHttpContext,
|
|
@@ -101,7 +102,7 @@ export function createSendGridWebhookHandler(
|
|
|
101
102
|
const mapped = EVENT_MAP[ev.event];
|
|
102
103
|
if (!mapped) continue;
|
|
103
104
|
try {
|
|
104
|
-
emitter
|
|
105
|
+
emitSafely(emitter, mapped, {
|
|
105
106
|
messageId: ev.sg_message_id ?? "",
|
|
106
107
|
to: ev.email ?? "",
|
|
107
108
|
reason: ev.reason,
|