emdash-smtp-core 0.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/LICENSE +21 -0
- package/dist/index.d.mts +397 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +2153 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +45 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,2153 @@
|
|
|
1
|
+
//#region src/storage.ts
|
|
2
|
+
const GLOBAL_SETTINGS_KEY = "settings:global";
|
|
3
|
+
const SELECTED_PROVIDER_KEY = "state:selectedProviderId";
|
|
4
|
+
const LAST_TEST_RESULT_KEY = "state:lastTestResult";
|
|
5
|
+
function providerSettingsKey(providerId) {
|
|
6
|
+
return `settings:provider:${providerId}`;
|
|
7
|
+
}
|
|
8
|
+
function trimString(value) {
|
|
9
|
+
if (typeof value !== "string") return void 0;
|
|
10
|
+
const next = value.trim();
|
|
11
|
+
return next === "" ? void 0 : next;
|
|
12
|
+
}
|
|
13
|
+
function numberValue$2(value) {
|
|
14
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
15
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
16
|
+
const next = Number(value);
|
|
17
|
+
if (Number.isFinite(next)) return next;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function booleanValue$1(value) {
|
|
21
|
+
if (typeof value === "boolean") return value;
|
|
22
|
+
if (typeof value === "string") return value === "true" || value === "1" || value === "on";
|
|
23
|
+
return Boolean(value);
|
|
24
|
+
}
|
|
25
|
+
function normalizeLogLevel(value) {
|
|
26
|
+
if (value === "errors" || value === "off") return value;
|
|
27
|
+
return "all";
|
|
28
|
+
}
|
|
29
|
+
function cleanRecord(record) {
|
|
30
|
+
return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== void 0));
|
|
31
|
+
}
|
|
32
|
+
async function getGlobalSettings(ctx) {
|
|
33
|
+
const saved = await ctx.kv.get(GLOBAL_SETTINGS_KEY) ?? {};
|
|
34
|
+
return {
|
|
35
|
+
primaryProviderId: trimString(saved.primaryProviderId),
|
|
36
|
+
fallbackProviderId: trimString(saved.fallbackProviderId),
|
|
37
|
+
fromEmail: trimString(saved.fromEmail),
|
|
38
|
+
fromName: trimString(saved.fromName),
|
|
39
|
+
replyTo: trimString(saved.replyTo),
|
|
40
|
+
logLevel: normalizeLogLevel(saved.logLevel)
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
async function saveGlobalSettingsFromValues(ctx, values) {
|
|
44
|
+
const next = {
|
|
45
|
+
primaryProviderId: trimString(values.primaryProviderId),
|
|
46
|
+
fallbackProviderId: trimString(values.fallbackProviderId),
|
|
47
|
+
fromEmail: trimString(values.fromEmail),
|
|
48
|
+
fromName: trimString(values.fromName),
|
|
49
|
+
replyTo: trimString(values.replyTo),
|
|
50
|
+
logLevel: normalizeLogLevel(values.logLevel)
|
|
51
|
+
};
|
|
52
|
+
await ctx.kv.set(GLOBAL_SETTINGS_KEY, next);
|
|
53
|
+
return next;
|
|
54
|
+
}
|
|
55
|
+
async function getProviderSettings(ctx, providerId) {
|
|
56
|
+
return await ctx.kv.get(providerSettingsKey(providerId)) ?? {};
|
|
57
|
+
}
|
|
58
|
+
async function patchProviderSettings(ctx, providerId, patch) {
|
|
59
|
+
const cleaned = cleanRecord({
|
|
60
|
+
...await getProviderSettings(ctx, providerId),
|
|
61
|
+
...patch
|
|
62
|
+
});
|
|
63
|
+
await ctx.kv.set(providerSettingsKey(providerId), cleaned);
|
|
64
|
+
return cleaned;
|
|
65
|
+
}
|
|
66
|
+
async function saveProviderSettingsFromValues(ctx, provider, values) {
|
|
67
|
+
const existing = await getProviderSettings(ctx, provider.id);
|
|
68
|
+
const next = {};
|
|
69
|
+
for (const field of provider.fields) {
|
|
70
|
+
const raw = values[field.key];
|
|
71
|
+
if (field.type === "secret") {
|
|
72
|
+
const secret = trimString(raw);
|
|
73
|
+
if (secret !== void 0) next[field.key] = secret;
|
|
74
|
+
else if (existing[field.key] !== void 0) next[field.key] = existing[field.key];
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (field.type === "number") {
|
|
78
|
+
const numeric = numberValue$2(raw);
|
|
79
|
+
if (numeric !== void 0) next[field.key] = numeric;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (field.type === "toggle") {
|
|
83
|
+
next[field.key] = booleanValue$1(raw);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const text = trimString(raw);
|
|
87
|
+
if (text !== void 0) next[field.key] = text;
|
|
88
|
+
}
|
|
89
|
+
const cleaned = cleanRecord(next);
|
|
90
|
+
await ctx.kv.set(providerSettingsKey(provider.id), cleaned);
|
|
91
|
+
return cleaned;
|
|
92
|
+
}
|
|
93
|
+
async function clearProviderSecret(ctx, providerId, fieldKey) {
|
|
94
|
+
const next = { ...await getProviderSettings(ctx, providerId) };
|
|
95
|
+
delete next[fieldKey];
|
|
96
|
+
await ctx.kv.set(providerSettingsKey(providerId), cleanRecord(next));
|
|
97
|
+
}
|
|
98
|
+
async function getSelectedProviderId(ctx) {
|
|
99
|
+
return trimString(await ctx.kv.get(SELECTED_PROVIDER_KEY));
|
|
100
|
+
}
|
|
101
|
+
async function setSelectedProviderId(ctx, providerId) {
|
|
102
|
+
await ctx.kv.set(SELECTED_PROVIDER_KEY, providerId);
|
|
103
|
+
}
|
|
104
|
+
async function getLastTestResult(ctx) {
|
|
105
|
+
return await ctx.kv.get(LAST_TEST_RESULT_KEY);
|
|
106
|
+
}
|
|
107
|
+
async function setLastTestResult(ctx, result) {
|
|
108
|
+
await ctx.kv.set(LAST_TEST_RESULT_KEY, result);
|
|
109
|
+
}
|
|
110
|
+
function createDeliveryLogRecord(input) {
|
|
111
|
+
return {
|
|
112
|
+
...input,
|
|
113
|
+
createdAt: input.createdAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function createLogId(record) {
|
|
117
|
+
const base = record.createdAt.replace(/[^0-9]/g, "").slice(0, 14);
|
|
118
|
+
const random = Math.random().toString(36).slice(2, 10);
|
|
119
|
+
return `${base}-${record.providerId}-${random}`;
|
|
120
|
+
}
|
|
121
|
+
async function writeDeliveryLog(ctx, record) {
|
|
122
|
+
const collection = ctx.storage?.deliveryLogs;
|
|
123
|
+
if (!collection?.put) return;
|
|
124
|
+
const logLevel = (await getGlobalSettings(ctx)).logLevel ?? "all";
|
|
125
|
+
if (logLevel === "off") return;
|
|
126
|
+
if (logLevel === "errors" && record.status !== "failed") return;
|
|
127
|
+
const id = record.id ?? createLogId(record);
|
|
128
|
+
await collection.put(id, {
|
|
129
|
+
...record,
|
|
130
|
+
id
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
async function queryRecentDeliveryLogs(ctx, limit = 25) {
|
|
134
|
+
const collection = ctx.storage?.deliveryLogs;
|
|
135
|
+
if (!collection?.query) return [];
|
|
136
|
+
return (await collection.query({
|
|
137
|
+
orderBy: { createdAt: "desc" },
|
|
138
|
+
limit
|
|
139
|
+
})).items ?? [];
|
|
140
|
+
}
|
|
141
|
+
async function countDeliveryLogs(ctx, status) {
|
|
142
|
+
const collection = ctx.storage?.deliveryLogs;
|
|
143
|
+
if (collection?.count) return collection.count({ status });
|
|
144
|
+
if (!collection?.query) return 0;
|
|
145
|
+
return (await collection.query({
|
|
146
|
+
where: { status },
|
|
147
|
+
limit: 1e3
|
|
148
|
+
})).items.length;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
//#endregion
|
|
152
|
+
//#region src/providers.ts
|
|
153
|
+
function unique(values) {
|
|
154
|
+
return [...new Set(values.filter(Boolean))].sort();
|
|
155
|
+
}
|
|
156
|
+
function stringValue$1(settings, key) {
|
|
157
|
+
const value = settings[key];
|
|
158
|
+
if (typeof value !== "string") return void 0;
|
|
159
|
+
const trimmed = value.trim();
|
|
160
|
+
return trimmed === "" ? void 0 : trimmed;
|
|
161
|
+
}
|
|
162
|
+
function numberValue$1(settings, key, fallback) {
|
|
163
|
+
const value = settings[key];
|
|
164
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
165
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
166
|
+
const parsed = Number(value);
|
|
167
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
168
|
+
}
|
|
169
|
+
return fallback;
|
|
170
|
+
}
|
|
171
|
+
function requireString(settings, key, label) {
|
|
172
|
+
const value = stringValue$1(settings, key);
|
|
173
|
+
if (!value) throw new Error(`${label} is required.`);
|
|
174
|
+
return value;
|
|
175
|
+
}
|
|
176
|
+
function sanitizeHeaderText(value) {
|
|
177
|
+
return value.replace(/[\r\n]+/g, " ").replace(/\s+/g, " ").trim();
|
|
178
|
+
}
|
|
179
|
+
function sanitizeEmailAddress(value, label) {
|
|
180
|
+
const trimmed = value.trim();
|
|
181
|
+
if (!trimmed) throw new Error(`${label} is required.`);
|
|
182
|
+
if (/[\r\n]/.test(trimmed)) throw new Error(`${label} contains invalid newline characters.`);
|
|
183
|
+
return trimmed;
|
|
184
|
+
}
|
|
185
|
+
function encodeHeaderValue(value) {
|
|
186
|
+
const sanitized = sanitizeHeaderText(value);
|
|
187
|
+
if (/^[\x20-\x7E]*$/.test(sanitized) && !/[",]/.test(sanitized)) return sanitized;
|
|
188
|
+
return `=?UTF-8?B?${toBase64(new TextEncoder().encode(sanitized))}?=`;
|
|
189
|
+
}
|
|
190
|
+
function formatAddress(email, name) {
|
|
191
|
+
const safeEmail = sanitizeEmailAddress(email, "Email address");
|
|
192
|
+
const safeName = name ? encodeHeaderValue(name) : void 0;
|
|
193
|
+
return safeName ? `${safeName} <${safeEmail}>` : safeEmail;
|
|
194
|
+
}
|
|
195
|
+
function ensureFetch(runtime) {
|
|
196
|
+
if (!runtime.fetch) throw new Error("This provider requires network fetch support in the current runtime.");
|
|
197
|
+
return runtime.fetch;
|
|
198
|
+
}
|
|
199
|
+
function ensureSmtp(runtime) {
|
|
200
|
+
if (!runtime.smtpSend) throw new Error("Custom SMTP is only available in the trusted package.");
|
|
201
|
+
return runtime.smtpSend;
|
|
202
|
+
}
|
|
203
|
+
function ensureSendmail(runtime) {
|
|
204
|
+
if (!runtime.sendmailSend) throw new Error("Local sendmail is only available in the trusted package.");
|
|
205
|
+
return runtime.sendmailSend;
|
|
206
|
+
}
|
|
207
|
+
async function readResponse(response) {
|
|
208
|
+
const text = await response.text();
|
|
209
|
+
try {
|
|
210
|
+
return {
|
|
211
|
+
text,
|
|
212
|
+
json: text ? JSON.parse(text) : void 0
|
|
213
|
+
};
|
|
214
|
+
} catch {
|
|
215
|
+
return { text };
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function asRecord(value) {
|
|
219
|
+
return typeof value === "object" && value !== null ? value : void 0;
|
|
220
|
+
}
|
|
221
|
+
function asArray(value) {
|
|
222
|
+
return Array.isArray(value) ? value : void 0;
|
|
223
|
+
}
|
|
224
|
+
function asString(value) {
|
|
225
|
+
return typeof value === "string" ? value : void 0;
|
|
226
|
+
}
|
|
227
|
+
function getPath(value, ...path) {
|
|
228
|
+
let current = value;
|
|
229
|
+
for (const segment of path) {
|
|
230
|
+
if (typeof segment === "number") {
|
|
231
|
+
const items = asArray(current);
|
|
232
|
+
if (!items) return void 0;
|
|
233
|
+
current = items[segment];
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
const record = asRecord(current);
|
|
237
|
+
if (!record) return void 0;
|
|
238
|
+
current = record[segment];
|
|
239
|
+
}
|
|
240
|
+
return current;
|
|
241
|
+
}
|
|
242
|
+
var HttpError = class extends Error {
|
|
243
|
+
status;
|
|
244
|
+
bodyText;
|
|
245
|
+
bodyJson;
|
|
246
|
+
constructor(status, bodyText, bodyJson) {
|
|
247
|
+
super(bodyText || `HTTP ${status}`);
|
|
248
|
+
this.name = "HttpError";
|
|
249
|
+
this.status = status;
|
|
250
|
+
this.bodyText = bodyText;
|
|
251
|
+
this.bodyJson = bodyJson;
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
function hasAllSettings(settings, keys) {
|
|
255
|
+
return keys.every((key) => Boolean(stringValue$1(settings, key)));
|
|
256
|
+
}
|
|
257
|
+
function shouldRetryAfterRefresh(error) {
|
|
258
|
+
return error instanceof HttpError && (error.status === 401 || error.status === 403);
|
|
259
|
+
}
|
|
260
|
+
async function refreshProviderAccessToken(args, tokenUrl, body, label) {
|
|
261
|
+
const { json } = await requestJson({
|
|
262
|
+
url: tokenUrl,
|
|
263
|
+
runtime: args.runtime,
|
|
264
|
+
ok: [200],
|
|
265
|
+
init: {
|
|
266
|
+
method: "POST",
|
|
267
|
+
headers: {
|
|
268
|
+
accept: "application/json",
|
|
269
|
+
"content-type": "application/x-www-form-urlencoded"
|
|
270
|
+
},
|
|
271
|
+
body: body.toString()
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
const accessToken = asString(getPath(json, "access_token"));
|
|
275
|
+
if (!accessToken) throw new Error(`${label} refresh did not return an access token.`);
|
|
276
|
+
const refreshToken = asString(getPath(json, "refresh_token"));
|
|
277
|
+
await patchProviderSettings(args.ctx, args.providerId, {
|
|
278
|
+
accessToken,
|
|
279
|
+
...refreshToken ? { refreshToken } : {}
|
|
280
|
+
});
|
|
281
|
+
return accessToken;
|
|
282
|
+
}
|
|
283
|
+
async function requestJson(opts) {
|
|
284
|
+
const response = await ensureFetch(opts.runtime)(opts.url, opts.init);
|
|
285
|
+
const result = await readResponse(response);
|
|
286
|
+
if (!(opts.ok ?? [
|
|
287
|
+
200,
|
|
288
|
+
201,
|
|
289
|
+
202
|
|
290
|
+
]).includes(response.status)) throw new HttpError(response.status, result.text || `HTTP ${response.status}`, result.json);
|
|
291
|
+
return {
|
|
292
|
+
response,
|
|
293
|
+
...result
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
function toBase64(bytes) {
|
|
297
|
+
if (typeof Buffer !== "undefined") return Buffer.from(bytes).toString("base64");
|
|
298
|
+
let binary = "";
|
|
299
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
300
|
+
return btoa(binary);
|
|
301
|
+
}
|
|
302
|
+
function toBase64Url(text) {
|
|
303
|
+
return toBase64(new TextEncoder().encode(text)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
304
|
+
}
|
|
305
|
+
function toHex(bytes) {
|
|
306
|
+
return Array.from(bytes).map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
307
|
+
}
|
|
308
|
+
async function sha256Hex(payload) {
|
|
309
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(payload));
|
|
310
|
+
return toHex(new Uint8Array(digest));
|
|
311
|
+
}
|
|
312
|
+
async function hmacSha256Raw(key, data) {
|
|
313
|
+
const rawKey = typeof key === "string" ? new TextEncoder().encode(key) : Uint8Array.from(key);
|
|
314
|
+
const imported = await crypto.subtle.importKey("raw", rawKey.buffer, {
|
|
315
|
+
name: "HMAC",
|
|
316
|
+
hash: "SHA-256"
|
|
317
|
+
}, false, ["sign"]);
|
|
318
|
+
const signature = await crypto.subtle.sign("HMAC", imported, new TextEncoder().encode(data));
|
|
319
|
+
return new Uint8Array(signature);
|
|
320
|
+
}
|
|
321
|
+
function stripHtml$1(html) {
|
|
322
|
+
return html.replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
|
|
323
|
+
}
|
|
324
|
+
function buildMimeMessage(message) {
|
|
325
|
+
const from = formatAddress(message.fromEmail || "", message.fromName);
|
|
326
|
+
const to = message.to.map((email) => sanitizeEmailAddress(email, "Recipient email address")).join(", ");
|
|
327
|
+
const replyTo = message.replyTo?.length ? message.replyTo.map((email) => sanitizeEmailAddress(email, "Reply-to email address")).join(", ") : void 0;
|
|
328
|
+
const subject = encodeHeaderValue(message.subject);
|
|
329
|
+
const headers = [
|
|
330
|
+
`From: ${from}`,
|
|
331
|
+
`To: ${to}`,
|
|
332
|
+
`Subject: ${subject}`,
|
|
333
|
+
...replyTo ? [`Reply-To: ${replyTo}`] : [],
|
|
334
|
+
"MIME-Version: 1.0"
|
|
335
|
+
];
|
|
336
|
+
if (message.html) {
|
|
337
|
+
const boundary = `emdash-${Math.random().toString(36).slice(2, 12)}`;
|
|
338
|
+
return [
|
|
339
|
+
...headers,
|
|
340
|
+
`Content-Type: multipart/alternative; boundary="${boundary}"`,
|
|
341
|
+
"",
|
|
342
|
+
`--${boundary}`,
|
|
343
|
+
"Content-Type: text/plain; charset=\"UTF-8\"",
|
|
344
|
+
"",
|
|
345
|
+
message.text,
|
|
346
|
+
"",
|
|
347
|
+
`--${boundary}`,
|
|
348
|
+
"Content-Type: text/html; charset=\"UTF-8\"",
|
|
349
|
+
"",
|
|
350
|
+
message.html,
|
|
351
|
+
"",
|
|
352
|
+
`--${boundary}--`,
|
|
353
|
+
""
|
|
354
|
+
].join("\r\n");
|
|
355
|
+
}
|
|
356
|
+
return [
|
|
357
|
+
...headers,
|
|
358
|
+
"Content-Type: text/plain; charset=\"UTF-8\"",
|
|
359
|
+
"",
|
|
360
|
+
message.text,
|
|
361
|
+
""
|
|
362
|
+
].join("\r\n");
|
|
363
|
+
}
|
|
364
|
+
async function signAwsRequest(opts) {
|
|
365
|
+
const amzDate = (/* @__PURE__ */ new Date()).toISOString().replace(/[:-]|\.\d{3}/g, "");
|
|
366
|
+
const dateStamp = amzDate.slice(0, 8);
|
|
367
|
+
const service = "ses";
|
|
368
|
+
const payloadHash = await sha256Hex(opts.payload);
|
|
369
|
+
const baseHeaders = {
|
|
370
|
+
"content-type": "application/x-www-form-urlencoded; charset=utf-8",
|
|
371
|
+
"host": opts.url.host,
|
|
372
|
+
"x-amz-content-sha256": payloadHash,
|
|
373
|
+
"x-amz-date": amzDate,
|
|
374
|
+
...opts.headers ?? {}
|
|
375
|
+
};
|
|
376
|
+
const sortedHeaderKeys = Object.keys(baseHeaders).sort();
|
|
377
|
+
const canonicalHeaders = sortedHeaderKeys.map((key) => `${key}:${(baseHeaders[key] ?? "").trim()}\n`).join("");
|
|
378
|
+
const signedHeaders = sortedHeaderKeys.join(";");
|
|
379
|
+
const canonicalRequest = [
|
|
380
|
+
opts.method,
|
|
381
|
+
opts.url.pathname || "/",
|
|
382
|
+
opts.url.search.startsWith("?") ? opts.url.search.slice(1) : opts.url.search,
|
|
383
|
+
canonicalHeaders,
|
|
384
|
+
signedHeaders,
|
|
385
|
+
payloadHash
|
|
386
|
+
].join("\n");
|
|
387
|
+
const credentialScope = `${dateStamp}/${opts.region}/${service}/aws4_request`;
|
|
388
|
+
const stringToSign = [
|
|
389
|
+
"AWS4-HMAC-SHA256",
|
|
390
|
+
amzDate,
|
|
391
|
+
credentialScope,
|
|
392
|
+
await sha256Hex(canonicalRequest)
|
|
393
|
+
].join("\n");
|
|
394
|
+
const signature = toHex(await hmacSha256Raw(await hmacSha256Raw(await hmacSha256Raw(await hmacSha256Raw(await hmacSha256Raw(`AWS4${opts.secretAccessKey}`, dateStamp), opts.region), service), "aws4_request"), stringToSign));
|
|
395
|
+
return {
|
|
396
|
+
...baseHeaders,
|
|
397
|
+
Authorization: `AWS4-HMAC-SHA256 Credential=${opts.accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
function getMessageText(message) {
|
|
401
|
+
return message.text || (message.html ? stripHtml$1(message.html) : "");
|
|
402
|
+
}
|
|
403
|
+
async function sendViaAmazon(args) {
|
|
404
|
+
const accessKeyId = requireString(args.settings, "accessKeyId", "Access Key ID");
|
|
405
|
+
const secretAccessKey = requireString(args.settings, "secretAccessKey", "Secret Access Key");
|
|
406
|
+
const region = requireString(args.settings, "region", "Region");
|
|
407
|
+
const payload = new URLSearchParams({
|
|
408
|
+
Action: "SendRawEmail",
|
|
409
|
+
Version: "2010-12-01",
|
|
410
|
+
"RawMessage.Data": toBase64(new TextEncoder().encode(buildMimeMessage(args.message)))
|
|
411
|
+
}).toString();
|
|
412
|
+
const url = new URL(`https://email.${region}.amazonaws.com/`);
|
|
413
|
+
const headers = await signAwsRequest({
|
|
414
|
+
accessKeyId,
|
|
415
|
+
secretAccessKey,
|
|
416
|
+
region,
|
|
417
|
+
url,
|
|
418
|
+
method: "POST",
|
|
419
|
+
payload
|
|
420
|
+
});
|
|
421
|
+
const { text } = await requestJson({
|
|
422
|
+
url: url.toString(),
|
|
423
|
+
runtime: args.runtime,
|
|
424
|
+
ok: [200],
|
|
425
|
+
init: {
|
|
426
|
+
method: "POST",
|
|
427
|
+
headers,
|
|
428
|
+
body: payload
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
return { remoteMessageId: text.match(/<MessageId>([^<]+)<\/MessageId>/i)?.[1] };
|
|
432
|
+
}
|
|
433
|
+
async function sendViaBrevo(args) {
|
|
434
|
+
const apiKey = requireString(args.settings, "apiKey", "API Key");
|
|
435
|
+
const body = {
|
|
436
|
+
sender: {
|
|
437
|
+
email: args.message.fromEmail,
|
|
438
|
+
...args.message.fromName ? { name: args.message.fromName } : {}
|
|
439
|
+
},
|
|
440
|
+
to: args.message.to.map((email) => ({ email })),
|
|
441
|
+
subject: args.message.subject,
|
|
442
|
+
...args.message.html ? { htmlContent: args.message.html } : { textContent: args.message.text },
|
|
443
|
+
...args.message.replyTo?.[0] ? { replyTo: { email: args.message.replyTo[0] } } : {}
|
|
444
|
+
};
|
|
445
|
+
const { json } = await requestJson({
|
|
446
|
+
url: "https://api.brevo.com/v3/smtp/email",
|
|
447
|
+
runtime: args.runtime,
|
|
448
|
+
init: {
|
|
449
|
+
method: "POST",
|
|
450
|
+
headers: {
|
|
451
|
+
accept: "application/json",
|
|
452
|
+
"content-type": "application/json",
|
|
453
|
+
"api-key": apiKey
|
|
454
|
+
},
|
|
455
|
+
body: JSON.stringify(body)
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
return { remoteMessageId: asString(asRecord(json)?.messageId) };
|
|
459
|
+
}
|
|
460
|
+
async function sendViaElasticEmail(args) {
|
|
461
|
+
const apiKey = requireString(args.settings, "apiKey", "API Key");
|
|
462
|
+
const body = {
|
|
463
|
+
Recipients: { To: [...args.message.to] },
|
|
464
|
+
Content: {
|
|
465
|
+
From: formatAddress(args.message.fromEmail || "", args.message.fromName),
|
|
466
|
+
Subject: args.message.subject,
|
|
467
|
+
Body: [{
|
|
468
|
+
Charset: "utf-8",
|
|
469
|
+
Content: args.message.html || args.message.text,
|
|
470
|
+
ContentType: args.message.html ? "HTML" : "PlainText"
|
|
471
|
+
}],
|
|
472
|
+
...args.message.replyTo?.[0] ? { ReplyTo: args.message.replyTo[0] } : {}
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
const { json } = await requestJson({
|
|
476
|
+
url: "https://api.elasticemail.com/v4/emails/transactional",
|
|
477
|
+
runtime: args.runtime,
|
|
478
|
+
init: {
|
|
479
|
+
method: "POST",
|
|
480
|
+
headers: {
|
|
481
|
+
accept: "application/json",
|
|
482
|
+
"content-type": "application/json",
|
|
483
|
+
"X-ElasticEmail-ApiKey": apiKey
|
|
484
|
+
},
|
|
485
|
+
body: JSON.stringify(body)
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
const record = asRecord(json);
|
|
489
|
+
return { remoteMessageId: asString(record?.TransactionID) ?? asString(record?.MessageID) };
|
|
490
|
+
}
|
|
491
|
+
async function sendViaEmailit(args) {
|
|
492
|
+
const apiKey = requireString(args.settings, "apiKey", "API Key");
|
|
493
|
+
const body = {
|
|
494
|
+
to: args.message.to.join(", "),
|
|
495
|
+
from: formatAddress(args.message.fromEmail || "", args.message.fromName),
|
|
496
|
+
subject: args.message.subject,
|
|
497
|
+
headers: { ...args.message.replyTo?.[0] ? { "reply-to": args.message.replyTo[0] } : {} },
|
|
498
|
+
...args.message.html ? {
|
|
499
|
+
html: args.message.html,
|
|
500
|
+
text: getMessageText(args.message)
|
|
501
|
+
} : { text: args.message.text }
|
|
502
|
+
};
|
|
503
|
+
const { json } = await requestJson({
|
|
504
|
+
url: "https://api.emailit.com/v1/emails",
|
|
505
|
+
runtime: args.runtime,
|
|
506
|
+
init: {
|
|
507
|
+
method: "POST",
|
|
508
|
+
headers: {
|
|
509
|
+
accept: "application/json",
|
|
510
|
+
"content-type": "application/json",
|
|
511
|
+
Authorization: `Bearer ${apiKey}`
|
|
512
|
+
},
|
|
513
|
+
body: JSON.stringify(body)
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
const record = asRecord(json);
|
|
517
|
+
return { remoteMessageId: asString(record?.id) ?? asString(record?.message_id) };
|
|
518
|
+
}
|
|
519
|
+
async function sendViaGenericSmtp(args) {
|
|
520
|
+
return ensureSmtp(args.runtime)({
|
|
521
|
+
host: requireString(args.settings, "host", "SMTP Hostname"),
|
|
522
|
+
port: numberValue$1(args.settings, "port", 587) ?? 587,
|
|
523
|
+
secure: (stringValue$1(args.settings, "security") ?? "starttls") === "ssl",
|
|
524
|
+
username: stringValue$1(args.settings, "username"),
|
|
525
|
+
password: stringValue$1(args.settings, "password")
|
|
526
|
+
}, args.message);
|
|
527
|
+
}
|
|
528
|
+
async function resolveGoogleAccessToken(args, forceRefresh = false) {
|
|
529
|
+
const storedAccessToken = forceRefresh ? void 0 : stringValue$1(args.settings, "accessToken");
|
|
530
|
+
if (storedAccessToken) return storedAccessToken;
|
|
531
|
+
if (!hasAllSettings(args.settings, [
|
|
532
|
+
"clientId",
|
|
533
|
+
"clientSecret",
|
|
534
|
+
"refreshToken"
|
|
535
|
+
])) throw new Error("Google requires an access token or client ID, client secret, and refresh token.");
|
|
536
|
+
return refreshProviderAccessToken(args, "https://oauth2.googleapis.com/token", new URLSearchParams({
|
|
537
|
+
client_id: requireString(args.settings, "clientId", "Client ID"),
|
|
538
|
+
client_secret: requireString(args.settings, "clientSecret", "Client Secret"),
|
|
539
|
+
refresh_token: requireString(args.settings, "refreshToken", "Refresh Token"),
|
|
540
|
+
grant_type: "refresh_token"
|
|
541
|
+
}), "Google");
|
|
542
|
+
}
|
|
543
|
+
async function resolveMicrosoftAccessToken(args, forceRefresh = false) {
|
|
544
|
+
const storedAccessToken = forceRefresh ? void 0 : stringValue$1(args.settings, "accessToken");
|
|
545
|
+
if (storedAccessToken) return storedAccessToken;
|
|
546
|
+
if (!hasAllSettings(args.settings, [
|
|
547
|
+
"clientId",
|
|
548
|
+
"clientSecret",
|
|
549
|
+
"refreshToken"
|
|
550
|
+
])) throw new Error("Microsoft requires an access token or application ID, client secret, and refresh token.");
|
|
551
|
+
return refreshProviderAccessToken(args, `https://login.microsoftonline.com/${stringValue$1(args.settings, "tenantId") ?? "common"}/oauth2/v2.0/token`, new URLSearchParams({
|
|
552
|
+
client_id: requireString(args.settings, "clientId", "Application ID"),
|
|
553
|
+
client_secret: requireString(args.settings, "clientSecret", "Client Secret"),
|
|
554
|
+
refresh_token: requireString(args.settings, "refreshToken", "Refresh Token"),
|
|
555
|
+
grant_type: "refresh_token",
|
|
556
|
+
scope: "email Mail.Send User.Read profile openid offline_access"
|
|
557
|
+
}), "Microsoft");
|
|
558
|
+
}
|
|
559
|
+
async function resolveZohoAccessToken(args, forceRefresh = false) {
|
|
560
|
+
const storedAccessToken = forceRefresh ? void 0 : stringValue$1(args.settings, "accessToken");
|
|
561
|
+
if (storedAccessToken) return storedAccessToken;
|
|
562
|
+
if (!hasAllSettings(args.settings, [
|
|
563
|
+
"clientId",
|
|
564
|
+
"clientSecret",
|
|
565
|
+
"refreshToken"
|
|
566
|
+
])) throw new Error("Zoho requires an access token or client ID, client secret, and refresh token.");
|
|
567
|
+
const body = new URLSearchParams({
|
|
568
|
+
client_id: requireString(args.settings, "clientId", "Client ID"),
|
|
569
|
+
client_secret: requireString(args.settings, "clientSecret", "Client Secret"),
|
|
570
|
+
refresh_token: requireString(args.settings, "refreshToken", "Refresh Token"),
|
|
571
|
+
grant_type: "refresh_token"
|
|
572
|
+
});
|
|
573
|
+
const redirectUri = stringValue$1(args.settings, "redirectUri");
|
|
574
|
+
if (redirectUri) body.set("redirect_uri", redirectUri);
|
|
575
|
+
return refreshProviderAccessToken(args, "https://accounts.zoho.com/oauth/v2/token", body, "Zoho");
|
|
576
|
+
}
|
|
577
|
+
async function sendViaGoogle(args) {
|
|
578
|
+
const raw = toBase64Url(buildMimeMessage(args.message));
|
|
579
|
+
const sendRequest = async (accessToken) => {
|
|
580
|
+
const { json } = await requestJson({
|
|
581
|
+
url: "https://gmail.googleapis.com/gmail/v1/users/me/messages/send",
|
|
582
|
+
runtime: args.runtime,
|
|
583
|
+
init: {
|
|
584
|
+
method: "POST",
|
|
585
|
+
headers: {
|
|
586
|
+
Authorization: `Bearer ${accessToken}`,
|
|
587
|
+
"Content-Type": "application/json"
|
|
588
|
+
},
|
|
589
|
+
body: JSON.stringify({ raw })
|
|
590
|
+
},
|
|
591
|
+
ok: [200]
|
|
592
|
+
});
|
|
593
|
+
return { remoteMessageId: asString(asRecord(json)?.id) };
|
|
594
|
+
};
|
|
595
|
+
const canRefresh = hasAllSettings(args.settings, [
|
|
596
|
+
"clientId",
|
|
597
|
+
"clientSecret",
|
|
598
|
+
"refreshToken"
|
|
599
|
+
]);
|
|
600
|
+
try {
|
|
601
|
+
return await sendRequest(await resolveGoogleAccessToken(args));
|
|
602
|
+
} catch (error) {
|
|
603
|
+
if (!canRefresh || !shouldRetryAfterRefresh(error)) throw error;
|
|
604
|
+
return sendRequest(await resolveGoogleAccessToken(args, true));
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
async function sendViaMailchimp(args) {
|
|
608
|
+
const body = {
|
|
609
|
+
key: requireString(args.settings, "apiKey", "API Key"),
|
|
610
|
+
message: {
|
|
611
|
+
subject: args.message.subject,
|
|
612
|
+
from_email: args.message.fromEmail,
|
|
613
|
+
...args.message.fromName ? { from_name: args.message.fromName } : {},
|
|
614
|
+
to: args.message.to.map((email) => ({
|
|
615
|
+
email,
|
|
616
|
+
type: "to"
|
|
617
|
+
})),
|
|
618
|
+
...args.message.html ? {
|
|
619
|
+
html: args.message.html,
|
|
620
|
+
text: getMessageText(args.message)
|
|
621
|
+
} : { text: args.message.text },
|
|
622
|
+
...args.message.replyTo?.[0] ? { headers: { "reply-to": args.message.replyTo[0] } } : {}
|
|
623
|
+
}
|
|
624
|
+
};
|
|
625
|
+
const { json } = await requestJson({
|
|
626
|
+
url: "https://mandrillapp.com/api/1.0/messages/send",
|
|
627
|
+
runtime: args.runtime,
|
|
628
|
+
init: {
|
|
629
|
+
method: "POST",
|
|
630
|
+
headers: { "Content-Type": "application/json" },
|
|
631
|
+
body: JSON.stringify(body)
|
|
632
|
+
}
|
|
633
|
+
});
|
|
634
|
+
const record = asRecord(Array.isArray(json) ? json[0] : json);
|
|
635
|
+
return { remoteMessageId: asString(record?._id) ?? asString(record?.id) };
|
|
636
|
+
}
|
|
637
|
+
async function sendViaMailerSend(args) {
|
|
638
|
+
const apiKey = requireString(args.settings, "apiKey", "API Key");
|
|
639
|
+
const body = {
|
|
640
|
+
subject: args.message.subject,
|
|
641
|
+
from: {
|
|
642
|
+
email: args.message.fromEmail,
|
|
643
|
+
...args.message.fromName ? { name: args.message.fromName } : {}
|
|
644
|
+
},
|
|
645
|
+
to: args.message.to.map((email) => ({ email })),
|
|
646
|
+
...args.message.html ? { html: args.message.html } : { text: args.message.text },
|
|
647
|
+
...args.message.replyTo?.[0] ? { reply_to: { email: args.message.replyTo[0] } } : {}
|
|
648
|
+
};
|
|
649
|
+
const { json } = await requestJson({
|
|
650
|
+
url: "https://api.mailersend.com/v1/email",
|
|
651
|
+
runtime: args.runtime,
|
|
652
|
+
init: {
|
|
653
|
+
method: "POST",
|
|
654
|
+
headers: {
|
|
655
|
+
accept: "application/json",
|
|
656
|
+
"content-type": "application/json",
|
|
657
|
+
Authorization: `Bearer ${apiKey}`
|
|
658
|
+
},
|
|
659
|
+
body: JSON.stringify(body)
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
const record = asRecord(json);
|
|
663
|
+
return { remoteMessageId: asString(record?.message_id) ?? asString(record?.id) };
|
|
664
|
+
}
|
|
665
|
+
async function sendViaMailgun(args) {
|
|
666
|
+
const apiKey = requireString(args.settings, "apiKey", "Mailgun API Key");
|
|
667
|
+
const domain = requireString(args.settings, "domain", "Sending Domain");
|
|
668
|
+
const base = (stringValue$1(args.settings, "region") ?? "us") === "eu" ? "https://api.eu.mailgun.net/v3" : "https://api.mailgun.net/v3";
|
|
669
|
+
const body = new URLSearchParams({
|
|
670
|
+
from: formatAddress(args.message.fromEmail || "", args.message.fromName),
|
|
671
|
+
to: args.message.to.join(", "),
|
|
672
|
+
subject: args.message.subject,
|
|
673
|
+
...args.message.html ? { html: args.message.html } : {},
|
|
674
|
+
text: getMessageText(args.message)
|
|
675
|
+
});
|
|
676
|
+
if (args.message.replyTo?.[0]) body.set("h:Reply-To", args.message.replyTo[0]);
|
|
677
|
+
const basic = toBase64(new TextEncoder().encode(`api:${apiKey}`));
|
|
678
|
+
const { json } = await requestJson({
|
|
679
|
+
url: `${base}/${domain}/messages`,
|
|
680
|
+
runtime: args.runtime,
|
|
681
|
+
init: {
|
|
682
|
+
method: "POST",
|
|
683
|
+
headers: { Authorization: `Basic ${basic}` },
|
|
684
|
+
body
|
|
685
|
+
},
|
|
686
|
+
ok: [200]
|
|
687
|
+
});
|
|
688
|
+
return { remoteMessageId: asString(asRecord(json)?.id) };
|
|
689
|
+
}
|
|
690
|
+
async function sendViaMailjet(args) {
|
|
691
|
+
const apiKey = requireString(args.settings, "apiKey", "API Key");
|
|
692
|
+
const apiSecret = requireString(args.settings, "apiSecret", "API Secret Key");
|
|
693
|
+
const body = { Messages: [{
|
|
694
|
+
To: args.message.to.map((email) => ({ Email: email })),
|
|
695
|
+
From: {
|
|
696
|
+
Email: args.message.fromEmail,
|
|
697
|
+
...args.message.fromName ? { Name: args.message.fromName } : {}
|
|
698
|
+
},
|
|
699
|
+
Subject: args.message.subject,
|
|
700
|
+
...args.message.html ? {
|
|
701
|
+
HTMLPart: args.message.html,
|
|
702
|
+
TextPart: getMessageText(args.message)
|
|
703
|
+
} : { TextPart: args.message.text },
|
|
704
|
+
...args.message.replyTo?.[0] ? { Headers: { "Reply-To": args.message.replyTo[0] } } : {}
|
|
705
|
+
}] };
|
|
706
|
+
const basic = toBase64(new TextEncoder().encode(`${apiKey}:${apiSecret}`));
|
|
707
|
+
const { json } = await requestJson({
|
|
708
|
+
url: "https://api.mailjet.com/v3.1/send",
|
|
709
|
+
runtime: args.runtime,
|
|
710
|
+
init: {
|
|
711
|
+
method: "POST",
|
|
712
|
+
headers: {
|
|
713
|
+
accept: "application/json",
|
|
714
|
+
"content-type": "application/json",
|
|
715
|
+
Authorization: `Basic ${basic}`
|
|
716
|
+
},
|
|
717
|
+
body: JSON.stringify(body)
|
|
718
|
+
}
|
|
719
|
+
});
|
|
720
|
+
return { remoteMessageId: asString(getPath(asRecord(json), "Messages", 0, "To", 0, "MessageUUID")) };
|
|
721
|
+
}
|
|
722
|
+
async function sendViaMicrosoft(args) {
|
|
723
|
+
const sendRequest = async (accessToken) => {
|
|
724
|
+
await requestJson({
|
|
725
|
+
url: "https://graph.microsoft.com/v1.0/me/sendMail",
|
|
726
|
+
runtime: args.runtime,
|
|
727
|
+
ok: [202],
|
|
728
|
+
init: {
|
|
729
|
+
method: "POST",
|
|
730
|
+
headers: {
|
|
731
|
+
Authorization: `Bearer ${accessToken}`,
|
|
732
|
+
"content-type": "application/json"
|
|
733
|
+
},
|
|
734
|
+
body: JSON.stringify({
|
|
735
|
+
message: {
|
|
736
|
+
subject: args.message.subject,
|
|
737
|
+
body: {
|
|
738
|
+
contentType: args.message.html ? "HTML" : "Text",
|
|
739
|
+
content: args.message.html || args.message.text
|
|
740
|
+
},
|
|
741
|
+
toRecipients: args.message.to.map((email) => ({ emailAddress: { address: email } })),
|
|
742
|
+
...args.message.replyTo?.[0] ? { replyTo: [{ emailAddress: { address: args.message.replyTo[0] } }] } : {}
|
|
743
|
+
},
|
|
744
|
+
saveToSentItems: false
|
|
745
|
+
})
|
|
746
|
+
}
|
|
747
|
+
});
|
|
748
|
+
return {};
|
|
749
|
+
};
|
|
750
|
+
const canRefresh = hasAllSettings(args.settings, [
|
|
751
|
+
"clientId",
|
|
752
|
+
"clientSecret",
|
|
753
|
+
"refreshToken"
|
|
754
|
+
]);
|
|
755
|
+
try {
|
|
756
|
+
return await sendRequest(await resolveMicrosoftAccessToken(args));
|
|
757
|
+
} catch (error) {
|
|
758
|
+
if (!canRefresh || !shouldRetryAfterRefresh(error)) throw error;
|
|
759
|
+
return sendRequest(await resolveMicrosoftAccessToken(args, true));
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
async function sendViaPhpMail(args) {
|
|
763
|
+
return ensureSendmail(args.runtime)({
|
|
764
|
+
sendmailPath: stringValue$1(args.settings, "sendmailPath") ?? "sendmail",
|
|
765
|
+
fromEmail: args.message.fromEmail || "",
|
|
766
|
+
fromName: args.message.fromName
|
|
767
|
+
}, args.message);
|
|
768
|
+
}
|
|
769
|
+
async function sendViaPostmark(args) {
|
|
770
|
+
const serverApiToken = requireString(args.settings, "serverApiToken", "Server API Token");
|
|
771
|
+
const body = {
|
|
772
|
+
from: formatAddress(args.message.fromEmail || "", args.message.fromName),
|
|
773
|
+
to: args.message.to.join(","),
|
|
774
|
+
subject: args.message.subject,
|
|
775
|
+
textBody: getMessageText(args.message),
|
|
776
|
+
...args.message.html ? { htmlBody: args.message.html } : {},
|
|
777
|
+
...args.message.replyTo?.[0] ? { ReplyTo: args.message.replyTo[0] } : {}
|
|
778
|
+
};
|
|
779
|
+
const { json } = await requestJson({
|
|
780
|
+
url: "https://api.postmarkapp.com/email",
|
|
781
|
+
runtime: args.runtime,
|
|
782
|
+
ok: [200],
|
|
783
|
+
init: {
|
|
784
|
+
method: "POST",
|
|
785
|
+
headers: {
|
|
786
|
+
Accept: "application/json",
|
|
787
|
+
"Content-Type": "application/json",
|
|
788
|
+
"X-Postmark-Server-Token": serverApiToken
|
|
789
|
+
},
|
|
790
|
+
body: JSON.stringify(body)
|
|
791
|
+
}
|
|
792
|
+
});
|
|
793
|
+
return { remoteMessageId: asString(asRecord(json)?.MessageID) };
|
|
794
|
+
}
|
|
795
|
+
async function sendViaResend(args) {
|
|
796
|
+
const apiKey = requireString(args.settings, "apiKey", "API Key");
|
|
797
|
+
const body = {
|
|
798
|
+
to: [...args.message.to],
|
|
799
|
+
from: formatAddress(args.message.fromEmail || "", args.message.fromName),
|
|
800
|
+
subject: args.message.subject,
|
|
801
|
+
...args.message.html ? {
|
|
802
|
+
html: args.message.html,
|
|
803
|
+
text: getMessageText(args.message)
|
|
804
|
+
} : { text: args.message.text },
|
|
805
|
+
...args.message.replyTo?.[0] ? { reply_to: args.message.replyTo[0] } : {}
|
|
806
|
+
};
|
|
807
|
+
const { json } = await requestJson({
|
|
808
|
+
url: "https://api.resend.com/emails",
|
|
809
|
+
runtime: args.runtime,
|
|
810
|
+
init: {
|
|
811
|
+
method: "POST",
|
|
812
|
+
headers: {
|
|
813
|
+
accept: "application/json",
|
|
814
|
+
"content-type": "application/json",
|
|
815
|
+
Authorization: `Bearer ${apiKey}`
|
|
816
|
+
},
|
|
817
|
+
body: JSON.stringify(body)
|
|
818
|
+
}
|
|
819
|
+
});
|
|
820
|
+
return { remoteMessageId: asString(asRecord(json)?.id) };
|
|
821
|
+
}
|
|
822
|
+
async function sendViaSendgrid(args) {
|
|
823
|
+
const apiKey = requireString(args.settings, "apiKey", "SendGrid API Key");
|
|
824
|
+
await requestJson({
|
|
825
|
+
url: "https://api.sendgrid.com/v3/mail/send",
|
|
826
|
+
runtime: args.runtime,
|
|
827
|
+
ok: [202],
|
|
828
|
+
init: {
|
|
829
|
+
method: "POST",
|
|
830
|
+
headers: {
|
|
831
|
+
Authorization: `Bearer ${apiKey}`,
|
|
832
|
+
"Content-Type": "application/json"
|
|
833
|
+
},
|
|
834
|
+
body: JSON.stringify({
|
|
835
|
+
from: {
|
|
836
|
+
email: args.message.fromEmail,
|
|
837
|
+
...args.message.fromName ? { name: args.message.fromName } : {}
|
|
838
|
+
},
|
|
839
|
+
personalizations: [{ to: args.message.to.map((email) => ({ email })) }],
|
|
840
|
+
subject: args.message.subject,
|
|
841
|
+
content: [{
|
|
842
|
+
type: args.message.html ? "text/html" : "text/plain",
|
|
843
|
+
value: args.message.html || args.message.text
|
|
844
|
+
}],
|
|
845
|
+
...args.message.replyTo?.length ? { reply_to_list: args.message.replyTo.map((email) => ({ email })) } : {}
|
|
846
|
+
})
|
|
847
|
+
}
|
|
848
|
+
});
|
|
849
|
+
return {};
|
|
850
|
+
}
|
|
851
|
+
async function sendViaSmtp2go(args) {
|
|
852
|
+
const apiKey = requireString(args.settings, "apiKey", "API Key");
|
|
853
|
+
const body = {
|
|
854
|
+
sender: formatAddress(args.message.fromEmail || "", args.message.fromName),
|
|
855
|
+
subject: args.message.subject,
|
|
856
|
+
to: [...args.message.to],
|
|
857
|
+
...args.message.html ? { html_body: args.message.html } : { text_body: args.message.text }
|
|
858
|
+
};
|
|
859
|
+
if (args.message.replyTo?.[0]) body.custom_headers = [{
|
|
860
|
+
header: "Reply-To",
|
|
861
|
+
value: args.message.replyTo[0]
|
|
862
|
+
}];
|
|
863
|
+
const { json } = await requestJson({
|
|
864
|
+
url: "https://api.smtp2go.com/v3/email/send",
|
|
865
|
+
runtime: args.runtime,
|
|
866
|
+
init: {
|
|
867
|
+
method: "POST",
|
|
868
|
+
headers: {
|
|
869
|
+
accept: "application/json",
|
|
870
|
+
"content-type": "application/json",
|
|
871
|
+
"X-Smtp2go-Api-Key": apiKey
|
|
872
|
+
},
|
|
873
|
+
body: JSON.stringify(body)
|
|
874
|
+
}
|
|
875
|
+
});
|
|
876
|
+
const record = asRecord(json);
|
|
877
|
+
return { remoteMessageId: asString(getPath(record, "data", "email_id")) ?? asString(record?.request_id) };
|
|
878
|
+
}
|
|
879
|
+
async function sendViaSparkpost(args) {
|
|
880
|
+
const apiKey = requireString(args.settings, "apiKey", "API Key");
|
|
881
|
+
const base = (stringValue$1(args.settings, "accountLocation") ?? "us") === "eu" ? "https://api.eu.sparkpost.com/api/v1" : "https://api.sparkpost.com/api/v1";
|
|
882
|
+
const body = {
|
|
883
|
+
recipients: args.message.to.map((email) => ({ address: { email } })),
|
|
884
|
+
content: {
|
|
885
|
+
from: {
|
|
886
|
+
email: args.message.fromEmail,
|
|
887
|
+
...args.message.fromName ? { name: args.message.fromName } : {}
|
|
888
|
+
},
|
|
889
|
+
subject: args.message.subject,
|
|
890
|
+
...args.message.html ? { html: args.message.html } : { text: args.message.text },
|
|
891
|
+
...args.message.replyTo?.[0] ? { reply_to: args.message.replyTo[0] } : {}
|
|
892
|
+
},
|
|
893
|
+
options: { transactional: true }
|
|
894
|
+
};
|
|
895
|
+
const { json } = await requestJson({
|
|
896
|
+
url: `${base}/transmissions/`,
|
|
897
|
+
runtime: args.runtime,
|
|
898
|
+
init: {
|
|
899
|
+
method: "POST",
|
|
900
|
+
headers: {
|
|
901
|
+
accept: "application/json",
|
|
902
|
+
"content-type": "application/json",
|
|
903
|
+
Authorization: apiKey
|
|
904
|
+
},
|
|
905
|
+
body: JSON.stringify(body)
|
|
906
|
+
}
|
|
907
|
+
});
|
|
908
|
+
return { remoteMessageId: asString(getPath(asRecord(json), "results", "id")) };
|
|
909
|
+
}
|
|
910
|
+
function getZohoBaseUrl(region) {
|
|
911
|
+
switch (region) {
|
|
912
|
+
case "eu": return "https://mail.zoho.eu";
|
|
913
|
+
case "in": return "https://mail.zoho.in";
|
|
914
|
+
case "com.au": return "https://mail.zoho.com.au";
|
|
915
|
+
case "jp": return "https://mail.zoho.jp";
|
|
916
|
+
case "sa": return "https://mail.zoho.sa";
|
|
917
|
+
case "ca": return "https://mail.zohocloud.ca";
|
|
918
|
+
default: return "https://mail.zoho.com";
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
async function sendViaZoho(args) {
|
|
922
|
+
const accountId = requireString(args.settings, "accountId", "Account ID");
|
|
923
|
+
const region = stringValue$1(args.settings, "dataCenterRegion") ?? "us";
|
|
924
|
+
const body = {
|
|
925
|
+
fromAddress: args.message.fromEmail,
|
|
926
|
+
toAddress: args.message.to.join(","),
|
|
927
|
+
subject: args.message.subject,
|
|
928
|
+
content: args.message.html || args.message.text,
|
|
929
|
+
encoding: "UTF-8",
|
|
930
|
+
mailFormat: args.message.html ? "html" : "plaintext",
|
|
931
|
+
...args.message.replyTo?.[0] ? { replyToAddress: args.message.replyTo[0] } : {}
|
|
932
|
+
};
|
|
933
|
+
const sendRequest = async (accessToken) => {
|
|
934
|
+
const { json } = await requestJson({
|
|
935
|
+
url: `${getZohoBaseUrl(region)}/api/accounts/${accountId}/messages`,
|
|
936
|
+
runtime: args.runtime,
|
|
937
|
+
init: {
|
|
938
|
+
method: "POST",
|
|
939
|
+
headers: {
|
|
940
|
+
Authorization: `Zoho-oauthtoken ${accessToken}`,
|
|
941
|
+
"Content-Type": "application/json"
|
|
942
|
+
},
|
|
943
|
+
body: JSON.stringify(body)
|
|
944
|
+
}
|
|
945
|
+
});
|
|
946
|
+
const record = asRecord(json);
|
|
947
|
+
return { remoteMessageId: asString(getPath(record, "data", "messageId")) ?? asString(getPath(record, "data", "message_id")) };
|
|
948
|
+
};
|
|
949
|
+
const canRefresh = hasAllSettings(args.settings, [
|
|
950
|
+
"clientId",
|
|
951
|
+
"clientSecret",
|
|
952
|
+
"refreshToken"
|
|
953
|
+
]);
|
|
954
|
+
try {
|
|
955
|
+
return await sendRequest(await resolveZohoAccessToken(args));
|
|
956
|
+
} catch (error) {
|
|
957
|
+
if (!canRefresh || !shouldRetryAfterRefresh(error)) throw error;
|
|
958
|
+
return sendRequest(await resolveZohoAccessToken(args, true));
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
function option(label, value) {
|
|
962
|
+
return {
|
|
963
|
+
label,
|
|
964
|
+
value
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
function fields(...defs) {
|
|
968
|
+
return defs;
|
|
969
|
+
}
|
|
970
|
+
const SMTP_PROVIDER_DEFINITIONS = [
|
|
971
|
+
{
|
|
972
|
+
id: "amazon",
|
|
973
|
+
label: "Amazon SES",
|
|
974
|
+
description: "Amazon Simple Email Service using signed AWS API requests.",
|
|
975
|
+
availability: {
|
|
976
|
+
trusted: true,
|
|
977
|
+
marketplace: true
|
|
978
|
+
},
|
|
979
|
+
allowedHosts: ["*.amazonaws.com"],
|
|
980
|
+
fields: fields({
|
|
981
|
+
key: "accessKeyId",
|
|
982
|
+
label: "Access Key ID",
|
|
983
|
+
type: "secret",
|
|
984
|
+
required: true
|
|
985
|
+
}, {
|
|
986
|
+
key: "secretAccessKey",
|
|
987
|
+
label: "Secret Access Key",
|
|
988
|
+
type: "secret",
|
|
989
|
+
required: true
|
|
990
|
+
}, {
|
|
991
|
+
key: "region",
|
|
992
|
+
label: "Region",
|
|
993
|
+
type: "select",
|
|
994
|
+
required: true,
|
|
995
|
+
defaultValue: "us-east-1",
|
|
996
|
+
options: [
|
|
997
|
+
option("US East (N. Virginia)", "us-east-1"),
|
|
998
|
+
option("US East (Ohio)", "us-east-2"),
|
|
999
|
+
option("US West (N. California)", "us-west-1"),
|
|
1000
|
+
option("US West (Oregon)", "us-west-2"),
|
|
1001
|
+
option("Europe (Ireland)", "eu-west-1"),
|
|
1002
|
+
option("Europe (Frankfurt)", "eu-central-1"),
|
|
1003
|
+
option("Europe (London)", "eu-west-2"),
|
|
1004
|
+
option("Asia Pacific (Sydney)", "ap-southeast-2"),
|
|
1005
|
+
option("Asia Pacific (Singapore)", "ap-southeast-1")
|
|
1006
|
+
]
|
|
1007
|
+
}),
|
|
1008
|
+
send: sendViaAmazon
|
|
1009
|
+
},
|
|
1010
|
+
{
|
|
1011
|
+
id: "brevo",
|
|
1012
|
+
label: "Brevo",
|
|
1013
|
+
description: "Transactional email via the Brevo SMTP API.",
|
|
1014
|
+
availability: {
|
|
1015
|
+
trusted: true,
|
|
1016
|
+
marketplace: true
|
|
1017
|
+
},
|
|
1018
|
+
allowedHosts: ["api.brevo.com"],
|
|
1019
|
+
fields: fields({
|
|
1020
|
+
key: "apiKey",
|
|
1021
|
+
label: "API Key",
|
|
1022
|
+
type: "secret",
|
|
1023
|
+
required: true
|
|
1024
|
+
}),
|
|
1025
|
+
send: sendViaBrevo
|
|
1026
|
+
},
|
|
1027
|
+
{
|
|
1028
|
+
id: "elastic_email",
|
|
1029
|
+
label: "Elastic Email",
|
|
1030
|
+
description: "Transactional email via the Elastic Email v4 API.",
|
|
1031
|
+
availability: {
|
|
1032
|
+
trusted: true,
|
|
1033
|
+
marketplace: true
|
|
1034
|
+
},
|
|
1035
|
+
allowedHosts: ["api.elasticemail.com"],
|
|
1036
|
+
fields: fields({
|
|
1037
|
+
key: "apiKey",
|
|
1038
|
+
label: "API Key",
|
|
1039
|
+
type: "secret",
|
|
1040
|
+
required: true
|
|
1041
|
+
}),
|
|
1042
|
+
send: sendViaElasticEmail
|
|
1043
|
+
},
|
|
1044
|
+
{
|
|
1045
|
+
id: "emailit",
|
|
1046
|
+
label: "Emailit",
|
|
1047
|
+
description: "Transactional email via the Emailit API.",
|
|
1048
|
+
availability: {
|
|
1049
|
+
trusted: true,
|
|
1050
|
+
marketplace: true
|
|
1051
|
+
},
|
|
1052
|
+
allowedHosts: ["api.emailit.com"],
|
|
1053
|
+
fields: fields({
|
|
1054
|
+
key: "apiKey",
|
|
1055
|
+
label: "API Key",
|
|
1056
|
+
type: "secret",
|
|
1057
|
+
required: true
|
|
1058
|
+
}),
|
|
1059
|
+
send: sendViaEmailit
|
|
1060
|
+
},
|
|
1061
|
+
{
|
|
1062
|
+
id: "generic",
|
|
1063
|
+
label: "Generic SMTP",
|
|
1064
|
+
description: "Custom SMTP server using Nodemailer in the trusted package.",
|
|
1065
|
+
availability: {
|
|
1066
|
+
trusted: true,
|
|
1067
|
+
marketplace: false
|
|
1068
|
+
},
|
|
1069
|
+
allowedHosts: [],
|
|
1070
|
+
fields: fields({
|
|
1071
|
+
key: "host",
|
|
1072
|
+
label: "SMTP Hostname",
|
|
1073
|
+
type: "text",
|
|
1074
|
+
required: true,
|
|
1075
|
+
placeholder: "smtp.example.com"
|
|
1076
|
+
}, {
|
|
1077
|
+
key: "port",
|
|
1078
|
+
label: "SMTP Port",
|
|
1079
|
+
type: "number",
|
|
1080
|
+
required: true,
|
|
1081
|
+
defaultValue: 587
|
|
1082
|
+
}, {
|
|
1083
|
+
key: "security",
|
|
1084
|
+
label: "Encryption",
|
|
1085
|
+
type: "select",
|
|
1086
|
+
defaultValue: "starttls",
|
|
1087
|
+
options: [
|
|
1088
|
+
option("STARTTLS / Auto", "starttls"),
|
|
1089
|
+
option("SSL/TLS", "ssl"),
|
|
1090
|
+
option("None", "none")
|
|
1091
|
+
]
|
|
1092
|
+
}, {
|
|
1093
|
+
key: "username",
|
|
1094
|
+
label: "Authentication Username",
|
|
1095
|
+
type: "text"
|
|
1096
|
+
}, {
|
|
1097
|
+
key: "password",
|
|
1098
|
+
label: "Authentication Password",
|
|
1099
|
+
type: "secret"
|
|
1100
|
+
}),
|
|
1101
|
+
send: sendViaGenericSmtp
|
|
1102
|
+
},
|
|
1103
|
+
{
|
|
1104
|
+
id: "google",
|
|
1105
|
+
label: "Google / Gmail",
|
|
1106
|
+
description: "Gmail API delivery using an access token or refresh-token credentials.",
|
|
1107
|
+
availability: {
|
|
1108
|
+
trusted: true,
|
|
1109
|
+
marketplace: true
|
|
1110
|
+
},
|
|
1111
|
+
allowedHosts: ["gmail.googleapis.com", "oauth2.googleapis.com"],
|
|
1112
|
+
fields: fields({
|
|
1113
|
+
key: "accessToken",
|
|
1114
|
+
label: "Access Token",
|
|
1115
|
+
type: "secret"
|
|
1116
|
+
}, {
|
|
1117
|
+
key: "refreshToken",
|
|
1118
|
+
label: "Refresh Token",
|
|
1119
|
+
type: "secret"
|
|
1120
|
+
}, {
|
|
1121
|
+
key: "clientId",
|
|
1122
|
+
label: "Client ID",
|
|
1123
|
+
type: "text"
|
|
1124
|
+
}, {
|
|
1125
|
+
key: "clientSecret",
|
|
1126
|
+
label: "Client Secret",
|
|
1127
|
+
type: "secret"
|
|
1128
|
+
}),
|
|
1129
|
+
isConfigured: (settings) => Boolean(stringValue$1(settings, "accessToken")) || hasAllSettings(settings, [
|
|
1130
|
+
"clientId",
|
|
1131
|
+
"clientSecret",
|
|
1132
|
+
"refreshToken"
|
|
1133
|
+
]),
|
|
1134
|
+
send: sendViaGoogle
|
|
1135
|
+
},
|
|
1136
|
+
{
|
|
1137
|
+
id: "mailchimp",
|
|
1138
|
+
label: "Mailchimp Transactional",
|
|
1139
|
+
description: "Mandrill/Mailchimp Transactional email delivery.",
|
|
1140
|
+
availability: {
|
|
1141
|
+
trusted: true,
|
|
1142
|
+
marketplace: true
|
|
1143
|
+
},
|
|
1144
|
+
allowedHosts: ["mandrillapp.com"],
|
|
1145
|
+
fields: fields({
|
|
1146
|
+
key: "apiKey",
|
|
1147
|
+
label: "API Key",
|
|
1148
|
+
type: "secret",
|
|
1149
|
+
required: true
|
|
1150
|
+
}),
|
|
1151
|
+
send: sendViaMailchimp
|
|
1152
|
+
},
|
|
1153
|
+
{
|
|
1154
|
+
id: "mailersend",
|
|
1155
|
+
label: "MailerSend",
|
|
1156
|
+
description: "Transactional email via the MailerSend API.",
|
|
1157
|
+
availability: {
|
|
1158
|
+
trusted: true,
|
|
1159
|
+
marketplace: true
|
|
1160
|
+
},
|
|
1161
|
+
allowedHosts: ["api.mailersend.com"],
|
|
1162
|
+
fields: fields({
|
|
1163
|
+
key: "apiKey",
|
|
1164
|
+
label: "API Key",
|
|
1165
|
+
type: "secret",
|
|
1166
|
+
required: true
|
|
1167
|
+
}),
|
|
1168
|
+
send: sendViaMailerSend
|
|
1169
|
+
},
|
|
1170
|
+
{
|
|
1171
|
+
id: "mailgun",
|
|
1172
|
+
label: "Mailgun",
|
|
1173
|
+
description: "Transactional email via the Mailgun Messages API.",
|
|
1174
|
+
availability: {
|
|
1175
|
+
trusted: true,
|
|
1176
|
+
marketplace: true
|
|
1177
|
+
},
|
|
1178
|
+
allowedHosts: ["api.mailgun.net", "api.eu.mailgun.net"],
|
|
1179
|
+
fields: fields({
|
|
1180
|
+
key: "apiKey",
|
|
1181
|
+
label: "Mailgun API Key",
|
|
1182
|
+
type: "secret",
|
|
1183
|
+
required: true
|
|
1184
|
+
}, {
|
|
1185
|
+
key: "domain",
|
|
1186
|
+
label: "Sending Domain",
|
|
1187
|
+
type: "text",
|
|
1188
|
+
required: true,
|
|
1189
|
+
placeholder: "mg.example.com"
|
|
1190
|
+
}, {
|
|
1191
|
+
key: "region",
|
|
1192
|
+
label: "Region",
|
|
1193
|
+
type: "select",
|
|
1194
|
+
defaultValue: "us",
|
|
1195
|
+
options: [option("US", "us"), option("EU", "eu")]
|
|
1196
|
+
}),
|
|
1197
|
+
send: sendViaMailgun
|
|
1198
|
+
},
|
|
1199
|
+
{
|
|
1200
|
+
id: "mailjet",
|
|
1201
|
+
label: "Mailjet",
|
|
1202
|
+
description: "Transactional email via the Mailjet v3.1 API.",
|
|
1203
|
+
availability: {
|
|
1204
|
+
trusted: true,
|
|
1205
|
+
marketplace: true
|
|
1206
|
+
},
|
|
1207
|
+
allowedHosts: ["api.mailjet.com"],
|
|
1208
|
+
fields: fields({
|
|
1209
|
+
key: "apiKey",
|
|
1210
|
+
label: "API Key",
|
|
1211
|
+
type: "secret",
|
|
1212
|
+
required: true
|
|
1213
|
+
}, {
|
|
1214
|
+
key: "apiSecret",
|
|
1215
|
+
label: "API Secret Key",
|
|
1216
|
+
type: "secret",
|
|
1217
|
+
required: true
|
|
1218
|
+
}),
|
|
1219
|
+
send: sendViaMailjet
|
|
1220
|
+
},
|
|
1221
|
+
{
|
|
1222
|
+
id: "microsoft",
|
|
1223
|
+
label: "365 / Outlook",
|
|
1224
|
+
description: "Microsoft Graph delivery using an access token or refresh-token credentials.",
|
|
1225
|
+
availability: {
|
|
1226
|
+
trusted: true,
|
|
1227
|
+
marketplace: true
|
|
1228
|
+
},
|
|
1229
|
+
allowedHosts: ["graph.microsoft.com", "login.microsoftonline.com"],
|
|
1230
|
+
fields: fields({
|
|
1231
|
+
key: "accessToken",
|
|
1232
|
+
label: "Access Token",
|
|
1233
|
+
type: "secret"
|
|
1234
|
+
}, {
|
|
1235
|
+
key: "refreshToken",
|
|
1236
|
+
label: "Refresh Token",
|
|
1237
|
+
type: "secret"
|
|
1238
|
+
}, {
|
|
1239
|
+
key: "clientId",
|
|
1240
|
+
label: "Application ID",
|
|
1241
|
+
type: "text"
|
|
1242
|
+
}, {
|
|
1243
|
+
key: "clientSecret",
|
|
1244
|
+
label: "Client Secret",
|
|
1245
|
+
type: "secret"
|
|
1246
|
+
}, {
|
|
1247
|
+
key: "tenantId",
|
|
1248
|
+
label: "Tenant ID",
|
|
1249
|
+
type: "text",
|
|
1250
|
+
defaultValue: "common",
|
|
1251
|
+
placeholder: "common"
|
|
1252
|
+
}),
|
|
1253
|
+
isConfigured: (settings) => Boolean(stringValue$1(settings, "accessToken")) || hasAllSettings(settings, [
|
|
1254
|
+
"clientId",
|
|
1255
|
+
"clientSecret",
|
|
1256
|
+
"refreshToken"
|
|
1257
|
+
]),
|
|
1258
|
+
send: sendViaMicrosoft
|
|
1259
|
+
},
|
|
1260
|
+
{
|
|
1261
|
+
id: "phpmail",
|
|
1262
|
+
label: "PHP Mail / local sendmail",
|
|
1263
|
+
description: "Local sendmail transport for trusted installs.",
|
|
1264
|
+
availability: {
|
|
1265
|
+
trusted: true,
|
|
1266
|
+
marketplace: false
|
|
1267
|
+
},
|
|
1268
|
+
allowedHosts: [],
|
|
1269
|
+
fields: fields({
|
|
1270
|
+
key: "sendmailPath",
|
|
1271
|
+
label: "Sendmail Path",
|
|
1272
|
+
type: "text",
|
|
1273
|
+
defaultValue: "sendmail"
|
|
1274
|
+
}),
|
|
1275
|
+
send: sendViaPhpMail
|
|
1276
|
+
},
|
|
1277
|
+
{
|
|
1278
|
+
id: "postmark",
|
|
1279
|
+
label: "Postmark",
|
|
1280
|
+
description: "Transactional email via the Postmark send email endpoint.",
|
|
1281
|
+
availability: {
|
|
1282
|
+
trusted: true,
|
|
1283
|
+
marketplace: true
|
|
1284
|
+
},
|
|
1285
|
+
allowedHosts: ["api.postmarkapp.com"],
|
|
1286
|
+
fields: fields({
|
|
1287
|
+
key: "serverApiToken",
|
|
1288
|
+
label: "Server API Token",
|
|
1289
|
+
type: "secret",
|
|
1290
|
+
required: true
|
|
1291
|
+
}),
|
|
1292
|
+
send: sendViaPostmark
|
|
1293
|
+
},
|
|
1294
|
+
{
|
|
1295
|
+
id: "resend",
|
|
1296
|
+
label: "Resend",
|
|
1297
|
+
description: "Transactional email via the Resend API.",
|
|
1298
|
+
availability: {
|
|
1299
|
+
trusted: true,
|
|
1300
|
+
marketplace: true
|
|
1301
|
+
},
|
|
1302
|
+
allowedHosts: ["api.resend.com"],
|
|
1303
|
+
fields: fields({
|
|
1304
|
+
key: "apiKey",
|
|
1305
|
+
label: "API Key",
|
|
1306
|
+
type: "secret",
|
|
1307
|
+
required: true
|
|
1308
|
+
}),
|
|
1309
|
+
send: sendViaResend
|
|
1310
|
+
},
|
|
1311
|
+
{
|
|
1312
|
+
id: "sendgrid",
|
|
1313
|
+
label: "SendGrid",
|
|
1314
|
+
description: "Transactional email via Twilio SendGrid.",
|
|
1315
|
+
availability: {
|
|
1316
|
+
trusted: true,
|
|
1317
|
+
marketplace: true
|
|
1318
|
+
},
|
|
1319
|
+
allowedHosts: ["api.sendgrid.com"],
|
|
1320
|
+
fields: fields({
|
|
1321
|
+
key: "apiKey",
|
|
1322
|
+
label: "SendGrid API Key",
|
|
1323
|
+
type: "secret",
|
|
1324
|
+
required: true
|
|
1325
|
+
}),
|
|
1326
|
+
send: sendViaSendgrid
|
|
1327
|
+
},
|
|
1328
|
+
{
|
|
1329
|
+
id: "smtp2go",
|
|
1330
|
+
label: "SMTP2GO",
|
|
1331
|
+
description: "Transactional email via SMTP2GO's HTTP API.",
|
|
1332
|
+
availability: {
|
|
1333
|
+
trusted: true,
|
|
1334
|
+
marketplace: true
|
|
1335
|
+
},
|
|
1336
|
+
allowedHosts: ["api.smtp2go.com"],
|
|
1337
|
+
fields: fields({
|
|
1338
|
+
key: "apiKey",
|
|
1339
|
+
label: "API Key",
|
|
1340
|
+
type: "secret",
|
|
1341
|
+
required: true
|
|
1342
|
+
}),
|
|
1343
|
+
send: sendViaSmtp2go
|
|
1344
|
+
},
|
|
1345
|
+
{
|
|
1346
|
+
id: "sparkpost",
|
|
1347
|
+
label: "SparkPost",
|
|
1348
|
+
description: "Transactional email via the SparkPost Transmissions API.",
|
|
1349
|
+
availability: {
|
|
1350
|
+
trusted: true,
|
|
1351
|
+
marketplace: true
|
|
1352
|
+
},
|
|
1353
|
+
allowedHosts: ["api.sparkpost.com", "api.eu.sparkpost.com"],
|
|
1354
|
+
fields: fields({
|
|
1355
|
+
key: "accountLocation",
|
|
1356
|
+
label: "Account Location",
|
|
1357
|
+
type: "select",
|
|
1358
|
+
defaultValue: "us",
|
|
1359
|
+
options: [option("United States", "us"), option("Europe", "eu")]
|
|
1360
|
+
}, {
|
|
1361
|
+
key: "apiKey",
|
|
1362
|
+
label: "API Key",
|
|
1363
|
+
type: "secret",
|
|
1364
|
+
required: true
|
|
1365
|
+
}),
|
|
1366
|
+
send: sendViaSparkpost
|
|
1367
|
+
},
|
|
1368
|
+
{
|
|
1369
|
+
id: "zoho",
|
|
1370
|
+
label: "Zoho Mail",
|
|
1371
|
+
description: "Zoho Mail API delivery using an access token or refresh-token credentials.",
|
|
1372
|
+
availability: {
|
|
1373
|
+
trusted: true,
|
|
1374
|
+
marketplace: true
|
|
1375
|
+
},
|
|
1376
|
+
allowedHosts: [
|
|
1377
|
+
"mail.zoho.com",
|
|
1378
|
+
"mail.zoho.eu",
|
|
1379
|
+
"mail.zoho.in",
|
|
1380
|
+
"mail.zoho.com.au",
|
|
1381
|
+
"mail.zoho.jp",
|
|
1382
|
+
"mail.zoho.sa",
|
|
1383
|
+
"mail.zohocloud.ca",
|
|
1384
|
+
"accounts.zoho.com"
|
|
1385
|
+
],
|
|
1386
|
+
fields: fields({
|
|
1387
|
+
key: "dataCenterRegion",
|
|
1388
|
+
label: "Datacenter Region",
|
|
1389
|
+
type: "select",
|
|
1390
|
+
defaultValue: "us",
|
|
1391
|
+
options: [
|
|
1392
|
+
option("United States", "us"),
|
|
1393
|
+
option("Europe", "eu"),
|
|
1394
|
+
option("India", "in"),
|
|
1395
|
+
option("Australia", "com.au"),
|
|
1396
|
+
option("Japan", "jp"),
|
|
1397
|
+
option("Saudi Arabia", "sa"),
|
|
1398
|
+
option("Canada", "ca")
|
|
1399
|
+
]
|
|
1400
|
+
}, {
|
|
1401
|
+
key: "clientId",
|
|
1402
|
+
label: "Client ID",
|
|
1403
|
+
type: "text"
|
|
1404
|
+
}, {
|
|
1405
|
+
key: "clientSecret",
|
|
1406
|
+
label: "Client Secret",
|
|
1407
|
+
type: "secret"
|
|
1408
|
+
}, {
|
|
1409
|
+
key: "refreshToken",
|
|
1410
|
+
label: "Refresh Token",
|
|
1411
|
+
type: "secret"
|
|
1412
|
+
}, {
|
|
1413
|
+
key: "accessToken",
|
|
1414
|
+
label: "Access Token",
|
|
1415
|
+
type: "secret"
|
|
1416
|
+
}, {
|
|
1417
|
+
key: "redirectUri",
|
|
1418
|
+
label: "Redirect URI",
|
|
1419
|
+
type: "text",
|
|
1420
|
+
placeholder: "Optional unless required by your token setup"
|
|
1421
|
+
}, {
|
|
1422
|
+
key: "accountId",
|
|
1423
|
+
label: "Account ID",
|
|
1424
|
+
type: "text",
|
|
1425
|
+
required: true
|
|
1426
|
+
}),
|
|
1427
|
+
isConfigured: (settings) => Boolean(stringValue$1(settings, "accountId")) && (Boolean(stringValue$1(settings, "accessToken")) || hasAllSettings(settings, [
|
|
1428
|
+
"clientId",
|
|
1429
|
+
"clientSecret",
|
|
1430
|
+
"refreshToken"
|
|
1431
|
+
])),
|
|
1432
|
+
send: sendViaZoho
|
|
1433
|
+
}
|
|
1434
|
+
];
|
|
1435
|
+
function getProviderById(providerId) {
|
|
1436
|
+
return SMTP_PROVIDER_DEFINITIONS.find((provider) => provider.id === providerId);
|
|
1437
|
+
}
|
|
1438
|
+
function isProviderAvailable(provider, variant) {
|
|
1439
|
+
return provider.availability[variant];
|
|
1440
|
+
}
|
|
1441
|
+
function isProviderConfigured(provider, settings) {
|
|
1442
|
+
if (provider.isConfigured) return provider.isConfigured(settings);
|
|
1443
|
+
return provider.fields.every((field) => {
|
|
1444
|
+
if (!field.required) return true;
|
|
1445
|
+
if (field.type === "number") return numberValue$1(settings, field.key) !== void 0;
|
|
1446
|
+
if (field.type === "toggle") return settings[field.key] !== void 0;
|
|
1447
|
+
return Boolean(stringValue$1(settings, field.key));
|
|
1448
|
+
});
|
|
1449
|
+
}
|
|
1450
|
+
function getProviderLabel(providerId) {
|
|
1451
|
+
if (!providerId) return "Not configured";
|
|
1452
|
+
return getProviderById(providerId)?.label ?? providerId;
|
|
1453
|
+
}
|
|
1454
|
+
function getAvailableProviderSelectOptions(variant) {
|
|
1455
|
+
return SMTP_PROVIDER_DEFINITIONS.filter((provider) => isProviderAvailable(provider, variant)).map((provider) => ({
|
|
1456
|
+
label: provider.label,
|
|
1457
|
+
value: provider.id
|
|
1458
|
+
}));
|
|
1459
|
+
}
|
|
1460
|
+
function getProviderPickerOptions(variant) {
|
|
1461
|
+
return SMTP_PROVIDER_DEFINITIONS.map((provider) => ({
|
|
1462
|
+
label: isProviderAvailable(provider, variant) ? provider.label : `${provider.label} (trusted-only)`,
|
|
1463
|
+
value: provider.id
|
|
1464
|
+
}));
|
|
1465
|
+
}
|
|
1466
|
+
function collectAllowedHosts(variant) {
|
|
1467
|
+
return unique(SMTP_PROVIDER_DEFINITIONS.filter((provider) => isProviderAvailable(provider, variant)).flatMap((provider) => provider.allowedHosts));
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
//#endregion
|
|
1471
|
+
//#region src/delivery.ts
|
|
1472
|
+
function trimmed(value) {
|
|
1473
|
+
if (!value) return void 0;
|
|
1474
|
+
const next = value.trim();
|
|
1475
|
+
return next === "" ? void 0 : next;
|
|
1476
|
+
}
|
|
1477
|
+
function stripHtml(html) {
|
|
1478
|
+
return html.replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
|
|
1479
|
+
}
|
|
1480
|
+
function normalizeMessageInput(message, settings) {
|
|
1481
|
+
const to = (Array.isArray(message.to) ? message.to : String(message.to).split(",")).map((entry) => entry.trim()).filter(Boolean);
|
|
1482
|
+
if (to.length === 0) throw new Error("At least one recipient email address is required.");
|
|
1483
|
+
const fromEmail = trimmed(settings.fromEmail);
|
|
1484
|
+
if (!fromEmail) throw new Error("A default from email must be configured before sending.");
|
|
1485
|
+
const text = trimmed(message.text) ?? (message.html ? stripHtml(message.html) : "");
|
|
1486
|
+
if (!text && !trimmed(message.html)) throw new Error("A message body is required.");
|
|
1487
|
+
return {
|
|
1488
|
+
to,
|
|
1489
|
+
subject: message.subject,
|
|
1490
|
+
text,
|
|
1491
|
+
html: trimmed(message.html),
|
|
1492
|
+
fromEmail,
|
|
1493
|
+
fromName: trimmed(settings.fromName),
|
|
1494
|
+
replyTo: trimmed(settings.replyTo) ? [settings.replyTo.trim()] : void 0
|
|
1495
|
+
};
|
|
1496
|
+
}
|
|
1497
|
+
async function resolvePrimaryProvider(ctx, runtime, settings) {
|
|
1498
|
+
const preferred = settings.primaryProviderId ? getProviderById(settings.primaryProviderId) : void 0;
|
|
1499
|
+
if (preferred && isProviderAvailable(preferred, runtime.variant)) {
|
|
1500
|
+
const providerSettings = await getProviderSettings(ctx, preferred.id);
|
|
1501
|
+
if (isProviderConfigured(preferred, providerSettings)) return {
|
|
1502
|
+
provider: preferred,
|
|
1503
|
+
providerSettings
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
for (const provider of SMTP_PROVIDER_DEFINITIONS) {
|
|
1507
|
+
if (!isProviderAvailable(provider, runtime.variant)) continue;
|
|
1508
|
+
const providerSettings = await getProviderSettings(ctx, provider.id);
|
|
1509
|
+
if (isProviderConfigured(provider, providerSettings)) return {
|
|
1510
|
+
provider,
|
|
1511
|
+
providerSettings
|
|
1512
|
+
};
|
|
1513
|
+
}
|
|
1514
|
+
const variantLabel = runtime.variant === "marketplace" ? "marketplace-compatible" : "trusted";
|
|
1515
|
+
throw new Error(`No configured ${variantLabel} SMTP provider is available.`);
|
|
1516
|
+
}
|
|
1517
|
+
async function resolveFallbackProvider(ctx, runtime, settings, primaryProviderId) {
|
|
1518
|
+
if (!settings.fallbackProviderId || settings.fallbackProviderId === primaryProviderId) return void 0;
|
|
1519
|
+
const provider = getProviderById(settings.fallbackProviderId);
|
|
1520
|
+
if (!provider || !isProviderAvailable(provider, runtime.variant)) return void 0;
|
|
1521
|
+
const providerSettings = await getProviderSettings(ctx, provider.id);
|
|
1522
|
+
if (!isProviderConfigured(provider, providerSettings)) return void 0;
|
|
1523
|
+
return {
|
|
1524
|
+
provider,
|
|
1525
|
+
providerSettings
|
|
1526
|
+
};
|
|
1527
|
+
}
|
|
1528
|
+
async function sendWithProvider(ctx, provider, providerSettings, message, runtime) {
|
|
1529
|
+
const start = Date.now();
|
|
1530
|
+
const result = await provider.send({
|
|
1531
|
+
ctx,
|
|
1532
|
+
providerId: provider.id,
|
|
1533
|
+
settings: providerSettings,
|
|
1534
|
+
message,
|
|
1535
|
+
runtime
|
|
1536
|
+
});
|
|
1537
|
+
return {
|
|
1538
|
+
providerId: provider.id,
|
|
1539
|
+
remoteMessageId: result.remoteMessageId,
|
|
1540
|
+
durationMs: Date.now() - start
|
|
1541
|
+
};
|
|
1542
|
+
}
|
|
1543
|
+
async function deliverWithConfiguredProvider(args) {
|
|
1544
|
+
const settings = await getGlobalSettings(args.ctx);
|
|
1545
|
+
const normalizedMessage = normalizeMessageInput(args.message, settings);
|
|
1546
|
+
const primary = await resolvePrimaryProvider(args.ctx, args.runtime, settings);
|
|
1547
|
+
const fallback = await resolveFallbackProvider(args.ctx, args.runtime, settings, primary.provider.id);
|
|
1548
|
+
try {
|
|
1549
|
+
return await sendWithProvider(args.ctx, primary.provider, primary.providerSettings, normalizedMessage, args.runtime);
|
|
1550
|
+
} catch (primaryError) {
|
|
1551
|
+
if (!fallback) throw primaryError;
|
|
1552
|
+
args.ctx.log?.warn("Primary SMTP provider failed, attempting fallback provider.", {
|
|
1553
|
+
primaryProviderId: primary.provider.id,
|
|
1554
|
+
fallbackProviderId: fallback.provider.id,
|
|
1555
|
+
error: primaryError instanceof Error ? primaryError.message : String(primaryError)
|
|
1556
|
+
});
|
|
1557
|
+
return sendWithProvider(args.ctx, fallback.provider, fallback.providerSettings, normalizedMessage, args.runtime);
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
async function isDeliveryReady(args) {
|
|
1561
|
+
const settings = await getGlobalSettings(args.ctx);
|
|
1562
|
+
if (!trimmed(settings.fromEmail)) return false;
|
|
1563
|
+
try {
|
|
1564
|
+
await resolvePrimaryProvider(args.ctx, args.runtime, settings);
|
|
1565
|
+
return true;
|
|
1566
|
+
} catch {
|
|
1567
|
+
return false;
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
//#endregion
|
|
1572
|
+
//#region src/admin.ts
|
|
1573
|
+
const SMTP_PLUGIN_ID = "emdash-smtp";
|
|
1574
|
+
const SMTP_PLUGIN_VERSION = "0.2.0";
|
|
1575
|
+
const SMTP_ADMIN_PAGES = [{
|
|
1576
|
+
path: "/providers",
|
|
1577
|
+
label: "SMTP Providers",
|
|
1578
|
+
icon: "mail"
|
|
1579
|
+
}, {
|
|
1580
|
+
path: "/logs",
|
|
1581
|
+
label: "SMTP Logs",
|
|
1582
|
+
icon: "activity"
|
|
1583
|
+
}];
|
|
1584
|
+
const SMTP_ADMIN_WIDGETS = [{
|
|
1585
|
+
id: "smtp-overview",
|
|
1586
|
+
title: "SMTP",
|
|
1587
|
+
size: "third"
|
|
1588
|
+
}];
|
|
1589
|
+
function header(text) {
|
|
1590
|
+
return {
|
|
1591
|
+
type: "header",
|
|
1592
|
+
text
|
|
1593
|
+
};
|
|
1594
|
+
}
|
|
1595
|
+
function divider() {
|
|
1596
|
+
return { type: "divider" };
|
|
1597
|
+
}
|
|
1598
|
+
function context(text) {
|
|
1599
|
+
return {
|
|
1600
|
+
type: "context",
|
|
1601
|
+
text
|
|
1602
|
+
};
|
|
1603
|
+
}
|
|
1604
|
+
function banner(title, description, variant = "default") {
|
|
1605
|
+
return {
|
|
1606
|
+
type: "banner",
|
|
1607
|
+
title,
|
|
1608
|
+
description,
|
|
1609
|
+
variant
|
|
1610
|
+
};
|
|
1611
|
+
}
|
|
1612
|
+
function stats(summary) {
|
|
1613
|
+
return {
|
|
1614
|
+
type: "stats",
|
|
1615
|
+
items: [
|
|
1616
|
+
{
|
|
1617
|
+
label: "Active provider",
|
|
1618
|
+
value: summary.activeProviderLabel
|
|
1619
|
+
},
|
|
1620
|
+
{
|
|
1621
|
+
label: "Sent",
|
|
1622
|
+
value: summary.sentCount,
|
|
1623
|
+
trend: summary.sentCount > 0 ? "up" : "neutral"
|
|
1624
|
+
},
|
|
1625
|
+
{
|
|
1626
|
+
label: "Failed",
|
|
1627
|
+
value: summary.failedCount,
|
|
1628
|
+
trend: summary.failedCount > 0 ? "down" : "neutral"
|
|
1629
|
+
}
|
|
1630
|
+
]
|
|
1631
|
+
};
|
|
1632
|
+
}
|
|
1633
|
+
function actions(elements) {
|
|
1634
|
+
return {
|
|
1635
|
+
type: "actions",
|
|
1636
|
+
elements
|
|
1637
|
+
};
|
|
1638
|
+
}
|
|
1639
|
+
function button(actionId, label, opts) {
|
|
1640
|
+
return {
|
|
1641
|
+
type: "button",
|
|
1642
|
+
action_id: actionId,
|
|
1643
|
+
label,
|
|
1644
|
+
...opts?.style ? { style: opts.style } : {},
|
|
1645
|
+
...opts?.value !== void 0 ? { value: opts.value } : {},
|
|
1646
|
+
...opts?.confirm ? { confirm: opts.confirm } : {}
|
|
1647
|
+
};
|
|
1648
|
+
}
|
|
1649
|
+
function textField(field, value) {
|
|
1650
|
+
return {
|
|
1651
|
+
type: "text_input",
|
|
1652
|
+
action_id: field.key,
|
|
1653
|
+
label: field.label,
|
|
1654
|
+
...field.placeholder ? { placeholder: field.placeholder } : {},
|
|
1655
|
+
...value !== void 0 ? { initial_value: value } : {},
|
|
1656
|
+
...field.type === "textarea" || field.multiline ? { multiline: true } : {}
|
|
1657
|
+
};
|
|
1658
|
+
}
|
|
1659
|
+
function secretField(field, hasValue) {
|
|
1660
|
+
return {
|
|
1661
|
+
type: "secret_input",
|
|
1662
|
+
action_id: field.key,
|
|
1663
|
+
label: field.label,
|
|
1664
|
+
...field.placeholder ? { placeholder: field.placeholder } : {},
|
|
1665
|
+
has_value: hasValue
|
|
1666
|
+
};
|
|
1667
|
+
}
|
|
1668
|
+
function numberField(field, value) {
|
|
1669
|
+
return {
|
|
1670
|
+
type: "number_input",
|
|
1671
|
+
action_id: field.key,
|
|
1672
|
+
label: field.label,
|
|
1673
|
+
...value !== void 0 ? { initial_value: value } : {}
|
|
1674
|
+
};
|
|
1675
|
+
}
|
|
1676
|
+
function selectField(field, value, overrideOptions) {
|
|
1677
|
+
return {
|
|
1678
|
+
type: "select",
|
|
1679
|
+
action_id: field.key,
|
|
1680
|
+
label: field.label,
|
|
1681
|
+
options: overrideOptions ?? field.options ?? [],
|
|
1682
|
+
...value !== void 0 ? { initial_value: value } : {}
|
|
1683
|
+
};
|
|
1684
|
+
}
|
|
1685
|
+
function toggleField(field, value) {
|
|
1686
|
+
return {
|
|
1687
|
+
type: "toggle",
|
|
1688
|
+
action_id: field.key,
|
|
1689
|
+
label: field.label,
|
|
1690
|
+
...field.description ? { description: field.description } : {},
|
|
1691
|
+
...value !== void 0 ? { initial_value: value } : {}
|
|
1692
|
+
};
|
|
1693
|
+
}
|
|
1694
|
+
function stringValue(value) {
|
|
1695
|
+
if (typeof value !== "string") return void 0;
|
|
1696
|
+
const next = value.trim();
|
|
1697
|
+
return next === "" ? void 0 : next;
|
|
1698
|
+
}
|
|
1699
|
+
function numberValue(value) {
|
|
1700
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
1701
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
1702
|
+
const parsed = Number(value);
|
|
1703
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
function booleanValue(value) {
|
|
1707
|
+
if (typeof value === "boolean") return value;
|
|
1708
|
+
if (typeof value === "string") return value === "true" || value === "1" || value === "on";
|
|
1709
|
+
return Boolean(value);
|
|
1710
|
+
}
|
|
1711
|
+
async function buildSummary(ctx) {
|
|
1712
|
+
const settings = await getGlobalSettings(ctx);
|
|
1713
|
+
const sentCount = await countDeliveryLogs(ctx, "sent");
|
|
1714
|
+
const failedCount = await countDeliveryLogs(ctx, "failed");
|
|
1715
|
+
return {
|
|
1716
|
+
activeProviderLabel: getProviderLabel(settings.primaryProviderId),
|
|
1717
|
+
sentCount,
|
|
1718
|
+
failedCount
|
|
1719
|
+
};
|
|
1720
|
+
}
|
|
1721
|
+
async function getCurrentProvider(ctx, variant) {
|
|
1722
|
+
const selected = await getSelectedProviderId(ctx);
|
|
1723
|
+
const preferred = selected ? getProviderById(selected) : void 0;
|
|
1724
|
+
if (preferred) return preferred;
|
|
1725
|
+
return SMTP_PROVIDER_DEFINITIONS.find((provider) => isProviderAvailable(provider, variant)) ?? SMTP_PROVIDER_DEFINITIONS[0];
|
|
1726
|
+
}
|
|
1727
|
+
function buildGlobalSettingsForm(settings, variant) {
|
|
1728
|
+
const availableOptions = getAvailableProviderSelectOptions(variant);
|
|
1729
|
+
const logLevelOptions = [
|
|
1730
|
+
{
|
|
1731
|
+
label: "All deliveries",
|
|
1732
|
+
value: "all"
|
|
1733
|
+
},
|
|
1734
|
+
{
|
|
1735
|
+
label: "Errors only",
|
|
1736
|
+
value: "errors"
|
|
1737
|
+
},
|
|
1738
|
+
{
|
|
1739
|
+
label: "Disabled",
|
|
1740
|
+
value: "off"
|
|
1741
|
+
}
|
|
1742
|
+
];
|
|
1743
|
+
return {
|
|
1744
|
+
type: "form",
|
|
1745
|
+
block_id: "global-settings",
|
|
1746
|
+
fields: [
|
|
1747
|
+
selectField({
|
|
1748
|
+
key: "primaryProviderId",
|
|
1749
|
+
label: "Primary Provider",
|
|
1750
|
+
type: "select",
|
|
1751
|
+
options: availableOptions
|
|
1752
|
+
}, settings.primaryProviderId, availableOptions),
|
|
1753
|
+
selectField({
|
|
1754
|
+
key: "fallbackProviderId",
|
|
1755
|
+
label: "Fallback Provider",
|
|
1756
|
+
type: "select",
|
|
1757
|
+
options: [{
|
|
1758
|
+
label: "None",
|
|
1759
|
+
value: ""
|
|
1760
|
+
}, ...availableOptions]
|
|
1761
|
+
}, settings.fallbackProviderId, [{
|
|
1762
|
+
label: "None",
|
|
1763
|
+
value: ""
|
|
1764
|
+
}, ...availableOptions]),
|
|
1765
|
+
textField({
|
|
1766
|
+
key: "fromEmail",
|
|
1767
|
+
label: "Default From Email",
|
|
1768
|
+
type: "text",
|
|
1769
|
+
required: true,
|
|
1770
|
+
placeholder: "noreply@example.com"
|
|
1771
|
+
}, settings.fromEmail),
|
|
1772
|
+
textField({
|
|
1773
|
+
key: "fromName",
|
|
1774
|
+
label: "Default From Name",
|
|
1775
|
+
type: "text",
|
|
1776
|
+
placeholder: "Example Site"
|
|
1777
|
+
}, settings.fromName),
|
|
1778
|
+
textField({
|
|
1779
|
+
key: "replyTo",
|
|
1780
|
+
label: "Default Reply-To Email",
|
|
1781
|
+
type: "text",
|
|
1782
|
+
placeholder: "support@example.com"
|
|
1783
|
+
}, settings.replyTo),
|
|
1784
|
+
selectField({
|
|
1785
|
+
key: "logLevel",
|
|
1786
|
+
label: "Log Level",
|
|
1787
|
+
type: "select",
|
|
1788
|
+
options: logLevelOptions
|
|
1789
|
+
}, settings.logLevel ?? "all", logLevelOptions)
|
|
1790
|
+
],
|
|
1791
|
+
submit: {
|
|
1792
|
+
label: "Save Global Settings",
|
|
1793
|
+
action_id: "save_global"
|
|
1794
|
+
}
|
|
1795
|
+
};
|
|
1796
|
+
}
|
|
1797
|
+
function buildProviderPickerForm(providerId, variant) {
|
|
1798
|
+
const options = getProviderPickerOptions(variant);
|
|
1799
|
+
return {
|
|
1800
|
+
type: "form",
|
|
1801
|
+
block_id: "provider-picker",
|
|
1802
|
+
fields: [selectField({
|
|
1803
|
+
key: "providerId",
|
|
1804
|
+
label: "Provider",
|
|
1805
|
+
type: "select",
|
|
1806
|
+
options
|
|
1807
|
+
}, providerId, options)],
|
|
1808
|
+
submit: {
|
|
1809
|
+
label: "Load Provider",
|
|
1810
|
+
action_id: "select_provider"
|
|
1811
|
+
}
|
|
1812
|
+
};
|
|
1813
|
+
}
|
|
1814
|
+
function buildProviderDetails(provider, variant, configured) {
|
|
1815
|
+
const available = isProviderAvailable(provider, variant);
|
|
1816
|
+
return [{
|
|
1817
|
+
type: "fields",
|
|
1818
|
+
fields: [
|
|
1819
|
+
{
|
|
1820
|
+
label: "Provider",
|
|
1821
|
+
value: provider.label
|
|
1822
|
+
},
|
|
1823
|
+
{
|
|
1824
|
+
label: "Availability",
|
|
1825
|
+
value: available ? "Available" : "Trusted-only"
|
|
1826
|
+
},
|
|
1827
|
+
{
|
|
1828
|
+
label: "Configured",
|
|
1829
|
+
value: configured ? "Yes" : "No"
|
|
1830
|
+
},
|
|
1831
|
+
{
|
|
1832
|
+
label: "Allowed Hosts",
|
|
1833
|
+
value: provider.allowedHosts.length ? provider.allowedHosts.join(", ") : "None"
|
|
1834
|
+
}
|
|
1835
|
+
]
|
|
1836
|
+
}, context(provider.description)];
|
|
1837
|
+
}
|
|
1838
|
+
function buildProviderSettingsForm(provider, settings) {
|
|
1839
|
+
return {
|
|
1840
|
+
type: "form",
|
|
1841
|
+
block_id: "provider-settings",
|
|
1842
|
+
fields: provider.fields.map((field) => {
|
|
1843
|
+
if (field.type === "secret") return secretField(field, Boolean(stringValue(settings[field.key])));
|
|
1844
|
+
if (field.type === "number") return numberField(field, numberValue(settings[field.key]) ?? (typeof field.defaultValue === "number" ? field.defaultValue : void 0));
|
|
1845
|
+
if (field.type === "select") return selectField(field, stringValue(settings[field.key]) ?? (typeof field.defaultValue === "string" ? field.defaultValue : void 0));
|
|
1846
|
+
if (field.type === "toggle") return toggleField(field, typeof settings[field.key] === "boolean" ? booleanValue(settings[field.key]) : typeof field.defaultValue === "boolean" ? field.defaultValue : void 0);
|
|
1847
|
+
return textField(field, stringValue(settings[field.key]) ?? (typeof field.defaultValue === "string" ? field.defaultValue : void 0));
|
|
1848
|
+
}),
|
|
1849
|
+
submit: {
|
|
1850
|
+
label: "Save Provider Settings",
|
|
1851
|
+
action_id: "save_provider"
|
|
1852
|
+
}
|
|
1853
|
+
};
|
|
1854
|
+
}
|
|
1855
|
+
function buildProviderSecretActions(provider, settings) {
|
|
1856
|
+
const elements = provider.fields.filter((field) => field.type === "secret" && Boolean(stringValue(settings[field.key]))).map((field) => button(`clear_secret:${provider.id}:${field.key}`, `Clear ${field.label}`, {
|
|
1857
|
+
style: "danger",
|
|
1858
|
+
confirm: {
|
|
1859
|
+
title: `Clear ${field.label}?`,
|
|
1860
|
+
text: `This will remove the stored ${field.label.toLowerCase()} from ${provider.label}.`,
|
|
1861
|
+
confirm: "Clear",
|
|
1862
|
+
deny: "Cancel",
|
|
1863
|
+
style: "danger"
|
|
1864
|
+
}
|
|
1865
|
+
}));
|
|
1866
|
+
return elements.length ? actions(elements) : null;
|
|
1867
|
+
}
|
|
1868
|
+
function buildTestSendForm(lastResult) {
|
|
1869
|
+
const blocks = [{
|
|
1870
|
+
type: "form",
|
|
1871
|
+
block_id: "test-send",
|
|
1872
|
+
fields: [
|
|
1873
|
+
{
|
|
1874
|
+
type: "text_input",
|
|
1875
|
+
action_id: "to",
|
|
1876
|
+
label: "Recipient Email",
|
|
1877
|
+
placeholder: "you@example.com"
|
|
1878
|
+
},
|
|
1879
|
+
{
|
|
1880
|
+
type: "text_input",
|
|
1881
|
+
action_id: "subject",
|
|
1882
|
+
label: "Subject",
|
|
1883
|
+
initial_value: "EmDash SMTP test email"
|
|
1884
|
+
},
|
|
1885
|
+
{
|
|
1886
|
+
type: "text_input",
|
|
1887
|
+
action_id: "text",
|
|
1888
|
+
label: "Message",
|
|
1889
|
+
multiline: true,
|
|
1890
|
+
initial_value: "This is a test email sent from EmDash SMTP."
|
|
1891
|
+
}
|
|
1892
|
+
],
|
|
1893
|
+
submit: {
|
|
1894
|
+
label: "Send Test Email",
|
|
1895
|
+
action_id: "send_test"
|
|
1896
|
+
}
|
|
1897
|
+
}];
|
|
1898
|
+
if (lastResult) blocks.push(banner(lastResult.status === "sent" ? "Last test succeeded" : "Last test failed", `${lastResult.createdAt}: ${lastResult.message}`, lastResult.status === "sent" ? "default" : "error"));
|
|
1899
|
+
return blocks;
|
|
1900
|
+
}
|
|
1901
|
+
async function buildLogsTable(ctx) {
|
|
1902
|
+
return {
|
|
1903
|
+
type: "table",
|
|
1904
|
+
page_action_id: "go_logs",
|
|
1905
|
+
empty_text: "No delivery logs yet.",
|
|
1906
|
+
columns: [
|
|
1907
|
+
{
|
|
1908
|
+
key: "createdAt",
|
|
1909
|
+
label: "Created",
|
|
1910
|
+
format: "relative_time",
|
|
1911
|
+
sortable: true
|
|
1912
|
+
},
|
|
1913
|
+
{
|
|
1914
|
+
key: "status",
|
|
1915
|
+
label: "Status",
|
|
1916
|
+
format: "badge"
|
|
1917
|
+
},
|
|
1918
|
+
{
|
|
1919
|
+
key: "provider",
|
|
1920
|
+
label: "Provider"
|
|
1921
|
+
},
|
|
1922
|
+
{
|
|
1923
|
+
key: "to",
|
|
1924
|
+
label: "To"
|
|
1925
|
+
},
|
|
1926
|
+
{
|
|
1927
|
+
key: "subject",
|
|
1928
|
+
label: "Subject"
|
|
1929
|
+
},
|
|
1930
|
+
{
|
|
1931
|
+
key: "source",
|
|
1932
|
+
label: "Source"
|
|
1933
|
+
},
|
|
1934
|
+
{
|
|
1935
|
+
key: "details",
|
|
1936
|
+
label: "Details",
|
|
1937
|
+
format: "code"
|
|
1938
|
+
}
|
|
1939
|
+
],
|
|
1940
|
+
rows: (await queryRecentDeliveryLogs(ctx, 25)).map(({ data }) => ({
|
|
1941
|
+
createdAt: data.createdAt,
|
|
1942
|
+
status: data.status,
|
|
1943
|
+
provider: getProviderLabel(data.providerId),
|
|
1944
|
+
to: data.message.to,
|
|
1945
|
+
subject: data.message.subject,
|
|
1946
|
+
source: data.source,
|
|
1947
|
+
details: data.errorMessage ?? data.remoteMessageId ?? "—"
|
|
1948
|
+
}))
|
|
1949
|
+
};
|
|
1950
|
+
}
|
|
1951
|
+
async function buildProvidersPage(ctx, variant, runtime, toast) {
|
|
1952
|
+
const summary = await buildSummary(ctx);
|
|
1953
|
+
const settings = await getGlobalSettings(ctx);
|
|
1954
|
+
const currentProvider = await getCurrentProvider(ctx, variant);
|
|
1955
|
+
const currentProviderSettings = await getProviderSettings(ctx, currentProvider.id);
|
|
1956
|
+
const configured = isProviderConfigured(currentProvider, currentProviderSettings);
|
|
1957
|
+
const lastTestResult = await getLastTestResult(ctx);
|
|
1958
|
+
const secretActions = buildProviderSecretActions(currentProvider, currentProviderSettings);
|
|
1959
|
+
const providerRows = await Promise.all(SMTP_PROVIDER_DEFINITIONS.map(async (provider) => {
|
|
1960
|
+
const providerSettings = await getProviderSettings(ctx, provider.id);
|
|
1961
|
+
return {
|
|
1962
|
+
provider: provider.label,
|
|
1963
|
+
id: provider.id,
|
|
1964
|
+
availability: isProviderAvailable(provider, variant) ? "available" : "trusted-only",
|
|
1965
|
+
configured: isProviderConfigured(provider, providerSettings) ? "yes" : "no",
|
|
1966
|
+
selected: provider.id === currentProvider.id ? "current" : ""
|
|
1967
|
+
};
|
|
1968
|
+
}));
|
|
1969
|
+
const blocks = [
|
|
1970
|
+
header("SMTP Providers"),
|
|
1971
|
+
banner(variant === "marketplace" ? "Marketplace-safe variant" : "Trusted variant", variant === "marketplace" ? "This install can use HTTP API providers. Generic SMTP and local sendmail remain visible for parity but are not available here." : `This install can use all providers, including Generic SMTP and local sendmail. Allowed hosts: ${collectAllowedHosts("trusted").join(", ")}`, variant === "marketplace" ? "alert" : "default"),
|
|
1972
|
+
stats(summary),
|
|
1973
|
+
actions([button("go_providers", "Providers", { style: "secondary" }), button("go_logs", "View Logs", { style: "primary" })]),
|
|
1974
|
+
{
|
|
1975
|
+
type: "table",
|
|
1976
|
+
page_action_id: "go_providers",
|
|
1977
|
+
empty_text: "No providers available.",
|
|
1978
|
+
columns: [
|
|
1979
|
+
{
|
|
1980
|
+
key: "provider",
|
|
1981
|
+
label: "Provider"
|
|
1982
|
+
},
|
|
1983
|
+
{
|
|
1984
|
+
key: "id",
|
|
1985
|
+
label: "ID",
|
|
1986
|
+
format: "code"
|
|
1987
|
+
},
|
|
1988
|
+
{
|
|
1989
|
+
key: "availability",
|
|
1990
|
+
label: "Availability",
|
|
1991
|
+
format: "badge"
|
|
1992
|
+
},
|
|
1993
|
+
{
|
|
1994
|
+
key: "configured",
|
|
1995
|
+
label: "Configured",
|
|
1996
|
+
format: "badge"
|
|
1997
|
+
},
|
|
1998
|
+
{
|
|
1999
|
+
key: "selected",
|
|
2000
|
+
label: "Selected",
|
|
2001
|
+
format: "badge"
|
|
2002
|
+
}
|
|
2003
|
+
],
|
|
2004
|
+
rows: providerRows
|
|
2005
|
+
},
|
|
2006
|
+
divider(),
|
|
2007
|
+
buildProviderPickerForm(currentProvider.id, variant),
|
|
2008
|
+
...buildProviderDetails(currentProvider, variant, configured),
|
|
2009
|
+
buildGlobalSettingsForm(settings, variant)
|
|
2010
|
+
];
|
|
2011
|
+
if (!isProviderAvailable(currentProvider, variant)) blocks.push(banner(`${currentProvider.label} is not available in the marketplace variant`, "Use the trusted emdash-smtp package in astro.config.mjs if you need this transport.", "alert"));
|
|
2012
|
+
else {
|
|
2013
|
+
blocks.push(buildProviderSettingsForm(currentProvider, currentProviderSettings));
|
|
2014
|
+
if (secretActions) blocks.push(secretActions);
|
|
2015
|
+
blocks.push(...buildTestSendForm(lastTestResult));
|
|
2016
|
+
}
|
|
2017
|
+
return {
|
|
2018
|
+
blocks,
|
|
2019
|
+
...toast ? { toast } : {}
|
|
2020
|
+
};
|
|
2021
|
+
}
|
|
2022
|
+
async function buildLogsPage(ctx, toast) {
|
|
2023
|
+
const summary = await buildSummary(ctx);
|
|
2024
|
+
return {
|
|
2025
|
+
blocks: [
|
|
2026
|
+
header("SMTP Logs"),
|
|
2027
|
+
stats(summary),
|
|
2028
|
+
actions([button("go_providers", "Providers", { style: "primary" }), button("go_logs", "Refresh Logs", { style: "secondary" })]),
|
|
2029
|
+
await buildLogsTable(ctx)
|
|
2030
|
+
],
|
|
2031
|
+
...toast ? { toast } : {}
|
|
2032
|
+
};
|
|
2033
|
+
}
|
|
2034
|
+
async function buildWidgetPage(ctx) {
|
|
2035
|
+
return { blocks: [stats(await buildSummary(ctx)), context("EmDash SMTP monitors the active provider and recent delivery outcomes.")] };
|
|
2036
|
+
}
|
|
2037
|
+
async function handleAdminInteraction(args) {
|
|
2038
|
+
const { ctx, interaction, variant, runtime } = args;
|
|
2039
|
+
if (interaction.type === "page_load") {
|
|
2040
|
+
if (interaction.page === "/logs") return buildLogsPage(ctx);
|
|
2041
|
+
if (interaction.page === "widget:smtp-overview") return buildWidgetPage(ctx);
|
|
2042
|
+
return buildProvidersPage(ctx, variant, runtime);
|
|
2043
|
+
}
|
|
2044
|
+
if (interaction.type === "block_action" || interaction.type === "action") {
|
|
2045
|
+
if (interaction.action_id === "go_logs") return buildLogsPage(ctx);
|
|
2046
|
+
if (interaction.action_id === "go_providers") return buildProvidersPage(ctx, variant, runtime);
|
|
2047
|
+
if (interaction.action_id.startsWith("clear_secret:")) {
|
|
2048
|
+
const [, providerId, fieldKey] = interaction.action_id.split(":");
|
|
2049
|
+
if (providerId && fieldKey) {
|
|
2050
|
+
await clearProviderSecret(ctx, providerId, fieldKey);
|
|
2051
|
+
return buildProvidersPage(ctx, variant, runtime, {
|
|
2052
|
+
message: `Cleared stored secret for ${fieldKey}.`,
|
|
2053
|
+
type: "success"
|
|
2054
|
+
});
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
return buildProvidersPage(ctx, variant, runtime);
|
|
2058
|
+
}
|
|
2059
|
+
if (interaction.type === "form_submit") {
|
|
2060
|
+
if (interaction.action_id === "save_global") {
|
|
2061
|
+
await saveGlobalSettingsFromValues(ctx, interaction.values);
|
|
2062
|
+
return buildProvidersPage(ctx, variant, runtime, {
|
|
2063
|
+
message: "Global SMTP settings saved.",
|
|
2064
|
+
type: "success"
|
|
2065
|
+
});
|
|
2066
|
+
}
|
|
2067
|
+
if (interaction.action_id === "select_provider") {
|
|
2068
|
+
const providerId = stringValue(interaction.values.providerId);
|
|
2069
|
+
if (providerId) await setSelectedProviderId(ctx, providerId);
|
|
2070
|
+
return buildProvidersPage(ctx, variant, runtime, {
|
|
2071
|
+
message: "Provider selection updated.",
|
|
2072
|
+
type: "info"
|
|
2073
|
+
});
|
|
2074
|
+
}
|
|
2075
|
+
if (interaction.action_id === "save_provider") {
|
|
2076
|
+
const provider = await getCurrentProvider(ctx, variant);
|
|
2077
|
+
await saveProviderSettingsFromValues(ctx, provider, interaction.values);
|
|
2078
|
+
return buildProvidersPage(ctx, variant, runtime, {
|
|
2079
|
+
message: `${provider.label} settings saved.`,
|
|
2080
|
+
type: "success"
|
|
2081
|
+
});
|
|
2082
|
+
}
|
|
2083
|
+
if (interaction.action_id === "send_test") {
|
|
2084
|
+
const to = stringValue(interaction.values.to);
|
|
2085
|
+
const subject = stringValue(interaction.values.subject) ?? "EmDash SMTP test email";
|
|
2086
|
+
const text = stringValue(interaction.values.text) ?? "This is a test email sent from EmDash SMTP.";
|
|
2087
|
+
if (!to) return buildProvidersPage(ctx, variant, runtime, {
|
|
2088
|
+
message: "A recipient email address is required for test sends.",
|
|
2089
|
+
type: "error"
|
|
2090
|
+
});
|
|
2091
|
+
try {
|
|
2092
|
+
const result = await deliverWithConfiguredProvider({
|
|
2093
|
+
ctx,
|
|
2094
|
+
runtime,
|
|
2095
|
+
message: {
|
|
2096
|
+
to,
|
|
2097
|
+
subject,
|
|
2098
|
+
text
|
|
2099
|
+
},
|
|
2100
|
+
source: `${ctx.plugin.id}:test`
|
|
2101
|
+
});
|
|
2102
|
+
await writeDeliveryLog(ctx, createDeliveryLogRecord({
|
|
2103
|
+
providerId: result.providerId,
|
|
2104
|
+
status: "sent",
|
|
2105
|
+
source: `${ctx.plugin.id}:test`,
|
|
2106
|
+
durationMs: result.durationMs,
|
|
2107
|
+
message: {
|
|
2108
|
+
to,
|
|
2109
|
+
subject
|
|
2110
|
+
},
|
|
2111
|
+
remoteMessageId: result.remoteMessageId
|
|
2112
|
+
}));
|
|
2113
|
+
await setLastTestResult(ctx, {
|
|
2114
|
+
status: "sent",
|
|
2115
|
+
providerId: result.providerId,
|
|
2116
|
+
message: `Sent with ${getProviderLabel(result.providerId)}${result.remoteMessageId ? ` (${result.remoteMessageId})` : ""}.`,
|
|
2117
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2118
|
+
});
|
|
2119
|
+
return buildProvidersPage(ctx, variant, runtime, {
|
|
2120
|
+
message: `Test email sent with ${getProviderLabel(result.providerId)}.`,
|
|
2121
|
+
type: "success"
|
|
2122
|
+
});
|
|
2123
|
+
} catch (error) {
|
|
2124
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2125
|
+
await writeDeliveryLog(ctx, createDeliveryLogRecord({
|
|
2126
|
+
providerId: "unknown",
|
|
2127
|
+
status: "failed",
|
|
2128
|
+
source: `${ctx.plugin.id}:test`,
|
|
2129
|
+
durationMs: 0,
|
|
2130
|
+
message: {
|
|
2131
|
+
to,
|
|
2132
|
+
subject
|
|
2133
|
+
},
|
|
2134
|
+
errorMessage: message
|
|
2135
|
+
}));
|
|
2136
|
+
await setLastTestResult(ctx, {
|
|
2137
|
+
status: "failed",
|
|
2138
|
+
message,
|
|
2139
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2140
|
+
});
|
|
2141
|
+
return buildProvidersPage(ctx, variant, runtime, {
|
|
2142
|
+
message,
|
|
2143
|
+
type: "error"
|
|
2144
|
+
});
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
return buildProvidersPage(ctx, variant, runtime);
|
|
2149
|
+
}
|
|
2150
|
+
|
|
2151
|
+
//#endregion
|
|
2152
|
+
export { GLOBAL_SETTINGS_KEY, LAST_TEST_RESULT_KEY, SELECTED_PROVIDER_KEY, SMTP_ADMIN_PAGES, SMTP_ADMIN_WIDGETS, SMTP_PLUGIN_ID, SMTP_PLUGIN_VERSION, SMTP_PROVIDER_DEFINITIONS, clearProviderSecret, collectAllowedHosts, countDeliveryLogs, createDeliveryLogRecord, deliverWithConfiguredProvider, getAvailableProviderSelectOptions, getGlobalSettings, getLastTestResult, getProviderById, getProviderLabel, getProviderPickerOptions, getProviderSettings, getSelectedProviderId, handleAdminInteraction, isDeliveryReady, isProviderAvailable, isProviderConfigured, patchProviderSettings, queryRecentDeliveryLogs, saveGlobalSettingsFromValues, saveProviderSettingsFromValues, setLastTestResult, setSelectedProviderId, writeDeliveryLog };
|
|
2153
|
+
//# sourceMappingURL=index.mjs.map
|