@upyo/ses 0.2.0-dev.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +255 -0
- package/dist/index.cjs +433 -0
- package/dist/index.d.cts +258 -0
- package/dist/index.d.ts +258 -0
- package/dist/index.js +432 -0
- package/package.json +72 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
//#region src/config.ts
|
|
2
|
+
/**
|
|
3
|
+
* Creates a resolved SES configuration with default values applied.
|
|
4
|
+
*
|
|
5
|
+
* This function takes a partial SES configuration and fills in default values
|
|
6
|
+
* for all optional fields, ensuring the transport has all necessary configuration.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* const config = createSesConfig({
|
|
11
|
+
* authentication: {
|
|
12
|
+
* type: "credentials",
|
|
13
|
+
* accessKeyId: "AKIA...",
|
|
14
|
+
* secretAccessKey: "wJal..",
|
|
15
|
+
* },
|
|
16
|
+
* region: "eu-west-1",
|
|
17
|
+
* });
|
|
18
|
+
*
|
|
19
|
+
* // config.timeout is now 30000 (default)
|
|
20
|
+
* // config.retries is now 3 (default)
|
|
21
|
+
* // config.batchSize is now 50 (default)
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* @param config The SES configuration object.
|
|
25
|
+
* @returns A resolved configuration with all defaults applied.
|
|
26
|
+
*/
|
|
27
|
+
function createSesConfig(config) {
|
|
28
|
+
return {
|
|
29
|
+
authentication: config.authentication,
|
|
30
|
+
region: config.region ?? "us-east-1",
|
|
31
|
+
timeout: config.timeout ?? 3e4,
|
|
32
|
+
retries: config.retries ?? 3,
|
|
33
|
+
validateSsl: config.validateSsl ?? true,
|
|
34
|
+
headers: config.headers ?? {},
|
|
35
|
+
configurationSetName: config.configurationSetName,
|
|
36
|
+
defaultTags: config.defaultTags ?? {},
|
|
37
|
+
batchSize: config.batchSize ?? 50
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
//#endregion
|
|
42
|
+
//#region src/http-client.ts
|
|
43
|
+
var SesHttpClient = class {
|
|
44
|
+
config;
|
|
45
|
+
constructor(config) {
|
|
46
|
+
this.config = config;
|
|
47
|
+
}
|
|
48
|
+
sendMessage(messageData, signal) {
|
|
49
|
+
const region = this.config.region;
|
|
50
|
+
const url = `https://email.${region}.amazonaws.com/v2/email/outbound-emails`;
|
|
51
|
+
return this.makeRequest(url, {
|
|
52
|
+
method: "POST",
|
|
53
|
+
headers: { "Content-Type": "application/json" },
|
|
54
|
+
body: JSON.stringify(messageData),
|
|
55
|
+
signal
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
async makeRequest(url, options) {
|
|
59
|
+
let lastError = null;
|
|
60
|
+
for (let attempt = 0; attempt <= this.config.retries; attempt++) try {
|
|
61
|
+
const response = await this.fetchWithAuth(url, options);
|
|
62
|
+
const text = await response.text();
|
|
63
|
+
if (response.status === 200) return {
|
|
64
|
+
statusCode: response.status,
|
|
65
|
+
body: text,
|
|
66
|
+
headers: this.headersToRecord(response.headers)
|
|
67
|
+
};
|
|
68
|
+
let errorData;
|
|
69
|
+
try {
|
|
70
|
+
errorData = JSON.parse(text);
|
|
71
|
+
} catch {
|
|
72
|
+
errorData = { message: text || `HTTP ${response.status}` };
|
|
73
|
+
}
|
|
74
|
+
throw new SesApiError(errorData.message || `HTTP ${response.status}`, response.status, errorData.errors);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
77
|
+
if (error instanceof SesApiError && error.statusCode && error.statusCode >= 400 && error.statusCode < 500) throw error;
|
|
78
|
+
if (attempt === this.config.retries) throw error;
|
|
79
|
+
const delay = Math.pow(2, attempt) * 1e3;
|
|
80
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
81
|
+
}
|
|
82
|
+
throw lastError || /* @__PURE__ */ new Error("Request failed after all retries");
|
|
83
|
+
}
|
|
84
|
+
async fetchWithAuth(url, options) {
|
|
85
|
+
const credentials = await this.getCredentials();
|
|
86
|
+
const headers = new Headers(options.headers);
|
|
87
|
+
const signedHeaders = await this.signRequest(url, {
|
|
88
|
+
...options,
|
|
89
|
+
headers
|
|
90
|
+
}, credentials);
|
|
91
|
+
for (const [key, value] of Object.entries(this.config.headers)) signedHeaders.set(key, value);
|
|
92
|
+
const controller = new AbortController();
|
|
93
|
+
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
|
|
94
|
+
let signal = controller.signal;
|
|
95
|
+
if (options.signal) {
|
|
96
|
+
signal = options.signal;
|
|
97
|
+
if (options.signal.aborted) controller.abort();
|
|
98
|
+
else options.signal.addEventListener("abort", () => controller.abort());
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
return await globalThis.fetch(url, {
|
|
102
|
+
...options,
|
|
103
|
+
headers: signedHeaders,
|
|
104
|
+
signal
|
|
105
|
+
});
|
|
106
|
+
} finally {
|
|
107
|
+
clearTimeout(timeoutId);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async getCredentials() {
|
|
111
|
+
const auth = this.config.authentication;
|
|
112
|
+
switch (auth.type) {
|
|
113
|
+
case "credentials": return {
|
|
114
|
+
accessKeyId: auth.accessKeyId,
|
|
115
|
+
secretAccessKey: auth.secretAccessKey
|
|
116
|
+
};
|
|
117
|
+
case "session": return {
|
|
118
|
+
accessKeyId: auth.accessKeyId,
|
|
119
|
+
secretAccessKey: auth.secretAccessKey,
|
|
120
|
+
sessionToken: auth.sessionToken
|
|
121
|
+
};
|
|
122
|
+
default: throw new Error(`Unsupported authentication type: ${auth.type}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async signRequest(url, options, credentials) {
|
|
126
|
+
const parsedUrl = new URL(url);
|
|
127
|
+
const method = options.method || "GET";
|
|
128
|
+
const headers = new Headers(options.headers);
|
|
129
|
+
const host = parsedUrl.host;
|
|
130
|
+
const path = parsedUrl.pathname + parsedUrl.search;
|
|
131
|
+
const service = "ses";
|
|
132
|
+
const region = this.config.region;
|
|
133
|
+
const now = /* @__PURE__ */ new Date();
|
|
134
|
+
const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, "");
|
|
135
|
+
const dateStamp = amzDate.substring(0, 8);
|
|
136
|
+
headers.set("Host", host);
|
|
137
|
+
headers.set("X-Amz-Date", amzDate);
|
|
138
|
+
if (credentials.sessionToken) headers.set("X-Amz-Security-Token", credentials.sessionToken);
|
|
139
|
+
const signedHeaders = "host;x-amz-date" + (credentials.sessionToken ? ";x-amz-security-token" : "");
|
|
140
|
+
const canonicalHeaders = `host:${host}\nx-amz-date:${amzDate}\n` + (credentials.sessionToken ? `x-amz-security-token:${credentials.sessionToken}\n` : "");
|
|
141
|
+
const payloadHash = await this.sha256(options.body || "");
|
|
142
|
+
const canonicalRequest = `${method}\n${path}\n\n${canonicalHeaders}\n${signedHeaders}\n${payloadHash}`;
|
|
143
|
+
const algorithm = "AWS4-HMAC-SHA256";
|
|
144
|
+
const credentialScope = `${dateStamp}/${region}/${service}/aws4_request`;
|
|
145
|
+
const stringToSign = `${algorithm}\n${amzDate}\n${credentialScope}\n${await this.sha256(canonicalRequest)}`;
|
|
146
|
+
const signingKey = await this.getSignatureKey(credentials.secretAccessKey, dateStamp, region, service);
|
|
147
|
+
const signature = await this.hmacSha256(signingKey, stringToSign);
|
|
148
|
+
const authorizationHeader = `${algorithm} Credential=${credentials.accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
|
|
149
|
+
headers.set("Authorization", authorizationHeader);
|
|
150
|
+
return headers;
|
|
151
|
+
}
|
|
152
|
+
async sha256(data) {
|
|
153
|
+
const encoder = new TextEncoder();
|
|
154
|
+
const dataBuffer = encoder.encode(data);
|
|
155
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", dataBuffer);
|
|
156
|
+
return Array.from(new Uint8Array(hashBuffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
157
|
+
}
|
|
158
|
+
async hmacSha256(key, data) {
|
|
159
|
+
const encoder = new TextEncoder();
|
|
160
|
+
const dataBuffer = encoder.encode(data);
|
|
161
|
+
const cryptoKey = await crypto.subtle.importKey("raw", key, {
|
|
162
|
+
name: "HMAC",
|
|
163
|
+
hash: "SHA-256"
|
|
164
|
+
}, false, ["sign"]);
|
|
165
|
+
const signature = await crypto.subtle.sign("HMAC", cryptoKey, dataBuffer);
|
|
166
|
+
return Array.from(new Uint8Array(signature)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
167
|
+
}
|
|
168
|
+
async getSignatureKey(key, dateStamp, regionName, serviceName) {
|
|
169
|
+
const encoder = new TextEncoder();
|
|
170
|
+
let keyBuffer = new Uint8Array(encoder.encode("AWS4" + key)).buffer;
|
|
171
|
+
keyBuffer = await this.hmacSha256Buffer(keyBuffer, dateStamp);
|
|
172
|
+
keyBuffer = await this.hmacSha256Buffer(keyBuffer, regionName);
|
|
173
|
+
keyBuffer = await this.hmacSha256Buffer(keyBuffer, serviceName);
|
|
174
|
+
keyBuffer = await this.hmacSha256Buffer(keyBuffer, "aws4_request");
|
|
175
|
+
return keyBuffer;
|
|
176
|
+
}
|
|
177
|
+
async hmacSha256Buffer(key, data) {
|
|
178
|
+
const encoder = new TextEncoder();
|
|
179
|
+
const dataBuffer = encoder.encode(data);
|
|
180
|
+
const cryptoKey = await crypto.subtle.importKey("raw", key, {
|
|
181
|
+
name: "HMAC",
|
|
182
|
+
hash: "SHA-256"
|
|
183
|
+
}, false, ["sign"]);
|
|
184
|
+
return await crypto.subtle.sign("HMAC", cryptoKey, dataBuffer);
|
|
185
|
+
}
|
|
186
|
+
headersToRecord(headers) {
|
|
187
|
+
const record = {};
|
|
188
|
+
for (const [key, value] of headers.entries()) record[key] = value;
|
|
189
|
+
return record;
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
var SesApiError = class extends Error {
|
|
193
|
+
statusCode;
|
|
194
|
+
errors;
|
|
195
|
+
constructor(message, statusCode, errors) {
|
|
196
|
+
super(message);
|
|
197
|
+
this.name = "SesApiError";
|
|
198
|
+
this.statusCode = statusCode;
|
|
199
|
+
this.errors = errors;
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
//#endregion
|
|
204
|
+
//#region src/message-converter.ts
|
|
205
|
+
async function convertMessage(message, config) {
|
|
206
|
+
const destination = {};
|
|
207
|
+
if (message.recipients.length > 0) destination.ToAddresses = message.recipients.map(formatAddress);
|
|
208
|
+
if (message.ccRecipients.length > 0) destination.CcAddresses = message.ccRecipients.map(formatAddress);
|
|
209
|
+
if (message.bccRecipients.length > 0) destination.BccAddresses = message.bccRecipients.map(formatAddress);
|
|
210
|
+
const sesMessage = {
|
|
211
|
+
Destination: destination,
|
|
212
|
+
Content: {},
|
|
213
|
+
FromEmailAddress: formatAddress(message.sender)
|
|
214
|
+
};
|
|
215
|
+
if (message.replyRecipients.length > 0) sesMessage.ReplyToAddresses = message.replyRecipients.map(formatAddress);
|
|
216
|
+
if (config.configurationSetName) sesMessage.ConfigurationSetName = config.configurationSetName;
|
|
217
|
+
sesMessage.Content.Simple = await createSimpleContent(message);
|
|
218
|
+
const tags = [];
|
|
219
|
+
if (message.tags.length > 0) for (const tag of message.tags) tags.push({
|
|
220
|
+
Name: "category",
|
|
221
|
+
Value: tag
|
|
222
|
+
});
|
|
223
|
+
if (message.priority !== "normal") tags.push({
|
|
224
|
+
Name: "priority",
|
|
225
|
+
Value: message.priority
|
|
226
|
+
});
|
|
227
|
+
for (const [name, value] of Object.entries(config.defaultTags)) tags.push({
|
|
228
|
+
Name: name,
|
|
229
|
+
Value: value
|
|
230
|
+
});
|
|
231
|
+
if (tags.length > 0) sesMessage.Tags = tags;
|
|
232
|
+
return sesMessage;
|
|
233
|
+
}
|
|
234
|
+
async function createSimpleContent(message) {
|
|
235
|
+
const content = {
|
|
236
|
+
Subject: {
|
|
237
|
+
Data: message.subject,
|
|
238
|
+
Charset: "UTF-8"
|
|
239
|
+
},
|
|
240
|
+
Body: {}
|
|
241
|
+
};
|
|
242
|
+
if ("html" in message.content) {
|
|
243
|
+
content.Body.Html = {
|
|
244
|
+
Data: message.content.html,
|
|
245
|
+
Charset: "UTF-8"
|
|
246
|
+
};
|
|
247
|
+
if (message.content.text) content.Body.Text = {
|
|
248
|
+
Data: message.content.text,
|
|
249
|
+
Charset: "UTF-8"
|
|
250
|
+
};
|
|
251
|
+
} else content.Body.Text = {
|
|
252
|
+
Data: message.content.text,
|
|
253
|
+
Charset: "UTF-8"
|
|
254
|
+
};
|
|
255
|
+
if (message.attachments.length > 0) content.Attachments = await Promise.all(message.attachments.map(async (attachment) => {
|
|
256
|
+
const contentBytes = await attachment.content;
|
|
257
|
+
const base64Content = btoa(String.fromCharCode(...contentBytes));
|
|
258
|
+
return {
|
|
259
|
+
FileName: attachment.filename,
|
|
260
|
+
ContentType: attachment.contentType || "application/octet-stream",
|
|
261
|
+
ContentDisposition: attachment.inline ? "INLINE" : "ATTACHMENT",
|
|
262
|
+
ContentId: attachment.contentId,
|
|
263
|
+
ContentTransferEncoding: "BASE64",
|
|
264
|
+
RawContent: base64Content
|
|
265
|
+
};
|
|
266
|
+
}));
|
|
267
|
+
return content;
|
|
268
|
+
}
|
|
269
|
+
function formatAddress(address) {
|
|
270
|
+
if (address.name) return `"${address.name}" <${address.address}>`;
|
|
271
|
+
return address.address;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
//#endregion
|
|
275
|
+
//#region src/ses-transport.ts
|
|
276
|
+
/**
|
|
277
|
+
* Amazon SES email transport implementation.
|
|
278
|
+
*
|
|
279
|
+
* This transport sends emails through the AWS Simple Email Service (SES) using
|
|
280
|
+
* the v2 API. It supports AWS Signature v4 authentication, configurable
|
|
281
|
+
* retries, and concurrent batch sending.
|
|
282
|
+
*
|
|
283
|
+
* @example
|
|
284
|
+
* ```typescript
|
|
285
|
+
* import { SesTransport } from "@upyo/ses";
|
|
286
|
+
* import { createMessage } from "@upyo/core";
|
|
287
|
+
*
|
|
288
|
+
* const transport = new SesTransport({
|
|
289
|
+
* authentication: {
|
|
290
|
+
* type: "credentials",
|
|
291
|
+
* accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
|
|
292
|
+
* secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
|
|
293
|
+
* },
|
|
294
|
+
* region: "us-east-1",
|
|
295
|
+
* });
|
|
296
|
+
*
|
|
297
|
+
* const message = createMessage({
|
|
298
|
+
* from: "sender@example.com",
|
|
299
|
+
* to: "recipient@example.com",
|
|
300
|
+
* subject: "Hello from SES",
|
|
301
|
+
* content: { text: "This is a test message" },
|
|
302
|
+
* });
|
|
303
|
+
*
|
|
304
|
+
* const receipt = await transport.send(message);
|
|
305
|
+
* if (receipt.successful) {
|
|
306
|
+
* console.log("Email sent with ID:", receipt.messageId);
|
|
307
|
+
* }
|
|
308
|
+
* ```
|
|
309
|
+
*/
|
|
310
|
+
var SesTransport = class {
|
|
311
|
+
/** Resolved configuration with defaults applied */
|
|
312
|
+
config;
|
|
313
|
+
/** HTTP client for SES API requests */
|
|
314
|
+
httpClient;
|
|
315
|
+
/**
|
|
316
|
+
* Creates a new SES transport instance.
|
|
317
|
+
*
|
|
318
|
+
* @param config SES configuration options.
|
|
319
|
+
*/
|
|
320
|
+
constructor(config) {
|
|
321
|
+
this.config = createSesConfig(config);
|
|
322
|
+
this.httpClient = new SesHttpClient(this.config);
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Sends a single email message through Amazon SES.
|
|
326
|
+
*
|
|
327
|
+
* This method converts the message to SES format, sends it via the SES v2 API,
|
|
328
|
+
* and returns a receipt indicating success or failure.
|
|
329
|
+
*
|
|
330
|
+
* @example
|
|
331
|
+
* ```typescript
|
|
332
|
+
* const message = createMessage({
|
|
333
|
+
* from: "sender@example.com",
|
|
334
|
+
* to: "recipient@example.com",
|
|
335
|
+
* subject: "Hello",
|
|
336
|
+
* content: { text: "Hello, world!" },
|
|
337
|
+
* attachments: [{
|
|
338
|
+
* filename: "document.pdf",
|
|
339
|
+
* content: Promise.resolve(pdfBytes),
|
|
340
|
+
* contentType: "application/pdf",
|
|
341
|
+
* }],
|
|
342
|
+
* });
|
|
343
|
+
*
|
|
344
|
+
* const receipt = await transport.send(message);
|
|
345
|
+
* ```
|
|
346
|
+
*
|
|
347
|
+
* @param message The email message to send.
|
|
348
|
+
* @param options Optional transport options (e.g., abort signal).
|
|
349
|
+
* @returns A promise that resolves to a receipt with the result.
|
|
350
|
+
*/
|
|
351
|
+
async send(message, options) {
|
|
352
|
+
options?.signal?.throwIfAborted();
|
|
353
|
+
try {
|
|
354
|
+
const sesMessage = await convertMessage(message, this.config);
|
|
355
|
+
options?.signal?.throwIfAborted();
|
|
356
|
+
const response = await this.httpClient.sendMessage(sesMessage, options?.signal);
|
|
357
|
+
const messageId = this.extractMessageId(response);
|
|
358
|
+
return {
|
|
359
|
+
successful: true,
|
|
360
|
+
messageId
|
|
361
|
+
};
|
|
362
|
+
} catch (error) {
|
|
363
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
364
|
+
return {
|
|
365
|
+
successful: false,
|
|
366
|
+
errorMessages: [errorMessage]
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Sends multiple email messages concurrently through Amazon SES.
|
|
372
|
+
*
|
|
373
|
+
* This method processes messages in batches (configurable via `batchSize`)
|
|
374
|
+
* and sends each batch concurrently to improve performance. It yields
|
|
375
|
+
* receipts as they become available, allowing for streaming processing of
|
|
376
|
+
* results.
|
|
377
|
+
*
|
|
378
|
+
* @example
|
|
379
|
+
* ```typescript
|
|
380
|
+
* const messages = [
|
|
381
|
+
* createMessage({ from: "sender@example.com", to: "user1@example.com", subject: "Hello 1", content: { text: "Message 1" } }),
|
|
382
|
+
* createMessage({ from: "sender@example.com", to: "user2@example.com", subject: "Hello 2", content: { text: "Message 2" } }),
|
|
383
|
+
* createMessage({ from: "sender@example.com", to: "user3@example.com", subject: "Hello 3", content: { text: "Message 3" } }),
|
|
384
|
+
* ];
|
|
385
|
+
*
|
|
386
|
+
* for await (const receipt of transport.sendMany(messages)) {
|
|
387
|
+
* if (receipt.successful) {
|
|
388
|
+
* console.log("Sent:", receipt.messageId);
|
|
389
|
+
* } else {
|
|
390
|
+
* console.error("Failed:", receipt.errorMessages);
|
|
391
|
+
* }
|
|
392
|
+
* }
|
|
393
|
+
* ```
|
|
394
|
+
*
|
|
395
|
+
* @param messages An iterable or async iterable of messages to send.
|
|
396
|
+
* @param options Optional transport options (e.g., abort signal).
|
|
397
|
+
* @returns Individual receipts for each message as they complete.
|
|
398
|
+
*/
|
|
399
|
+
async *sendMany(messages, options) {
|
|
400
|
+
options?.signal?.throwIfAborted();
|
|
401
|
+
const isAsyncIterable = Symbol.asyncIterator in messages;
|
|
402
|
+
const messageArray = [];
|
|
403
|
+
if (isAsyncIterable) for await (const message of messages) messageArray.push(message);
|
|
404
|
+
else for (const message of messages) messageArray.push(message);
|
|
405
|
+
const batchSize = this.config.batchSize;
|
|
406
|
+
for (let i = 0; i < messageArray.length; i += batchSize) {
|
|
407
|
+
options?.signal?.throwIfAborted();
|
|
408
|
+
const batch = messageArray.slice(i, i + batchSize);
|
|
409
|
+
const receipts = await this.sendConcurrent(batch, options);
|
|
410
|
+
for (const receipt of receipts) yield receipt;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
async sendConcurrent(messages, options) {
|
|
414
|
+
options?.signal?.throwIfAborted();
|
|
415
|
+
const sendPromises = messages.map((message) => this.send(message, options));
|
|
416
|
+
return await Promise.all(sendPromises);
|
|
417
|
+
}
|
|
418
|
+
extractMessageId(response) {
|
|
419
|
+
if (response.body) try {
|
|
420
|
+
const parsed = JSON.parse(response.body);
|
|
421
|
+
if (parsed.MessageId) return parsed.MessageId;
|
|
422
|
+
} catch {}
|
|
423
|
+
const messageIdHeader = response.headers?.["x-amzn-requestid"] || response.headers?.["X-Amzn-RequestId"];
|
|
424
|
+
if (messageIdHeader) return messageIdHeader;
|
|
425
|
+
const timestamp = Date.now();
|
|
426
|
+
const random = Math.random().toString(36).substring(2, 8);
|
|
427
|
+
return `ses-${timestamp}-${random}`;
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
//#endregion
|
|
432
|
+
export { SesTransport };
|
package/package.json
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@upyo/ses",
|
|
3
|
+
"version": "0.2.0-dev.19+e02d7bd8",
|
|
4
|
+
"description": "Amazon SES transport for Upyo email library",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"email",
|
|
7
|
+
"mail",
|
|
8
|
+
"sendmail",
|
|
9
|
+
"ses",
|
|
10
|
+
"amazon",
|
|
11
|
+
"aws"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"author": {
|
|
15
|
+
"name": "Hong Minhee",
|
|
16
|
+
"email": "hong@minhee.org",
|
|
17
|
+
"url": "https://hongminhee.org/"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://upyo.org/",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/dahlia/upyo.git",
|
|
23
|
+
"directory": "packages/ses/"
|
|
24
|
+
},
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/dahlia/upyo/issues"
|
|
27
|
+
},
|
|
28
|
+
"funding": [
|
|
29
|
+
"https://github.com/sponsors/dahlia"
|
|
30
|
+
],
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=20.0.0",
|
|
33
|
+
"bun": ">=1.2.0",
|
|
34
|
+
"deno": ">=2.3.0"
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"dist/",
|
|
38
|
+
"package.json",
|
|
39
|
+
"README.md"
|
|
40
|
+
],
|
|
41
|
+
"type": "module",
|
|
42
|
+
"module": "./dist/index.js",
|
|
43
|
+
"main": "./dist/index.cjs",
|
|
44
|
+
"types": "./dist/index.d.ts",
|
|
45
|
+
"exports": {
|
|
46
|
+
".": {
|
|
47
|
+
"types": {
|
|
48
|
+
"import": "./dist/index.d.ts",
|
|
49
|
+
"require": "./dist/index.d.cts"
|
|
50
|
+
},
|
|
51
|
+
"import": "./dist/index.js",
|
|
52
|
+
"require": "./dist/index.cjs"
|
|
53
|
+
},
|
|
54
|
+
"./package.json": "./package.json"
|
|
55
|
+
},
|
|
56
|
+
"sideEffects": false,
|
|
57
|
+
"peerDependencies": {
|
|
58
|
+
"@upyo/core": "0.2.0-dev.19+e02d7bd8"
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@dotenvx/dotenvx": "^1.47.3",
|
|
62
|
+
"tsdown": "^0.12.7",
|
|
63
|
+
"typescript": "5.8.3"
|
|
64
|
+
},
|
|
65
|
+
"scripts": {
|
|
66
|
+
"build": "tsdown",
|
|
67
|
+
"prepublish": "tsdown",
|
|
68
|
+
"test": "tsdown && dotenvx run --ignore=MISSING_ENV_FILE -- node --experimental-transform-types --test",
|
|
69
|
+
"test:bun": "tsdown && bun test --timeout=30000 --env-file=.env",
|
|
70
|
+
"test:deno": "deno test --allow-env --allow-net --env-file=.env"
|
|
71
|
+
}
|
|
72
|
+
}
|