arcane-os 0.15.2 → 0.16.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/CHANGELOG.md +22 -0
- package/README.md +9 -1
- package/docs/reference/cli.md +44 -30
- package/docs/reference/mail.md +166 -67
- package/docs/reviews/mail-server-purpose-review.md +350 -0
- package/package.json +1 -1
- package/runtime/arcane/modules/Mail.js +42 -71
- package/runtime/arcane/modules/MailOutbox.mjs +5 -5
- package/runtime/arcane/modules/MailTransport.mjs +12 -21
- package/src/cli/main.mjs +14 -46
- package/src/mail-server.mjs +333 -617
- package/src/mail.mjs +50 -110
package/src/mail-server.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import Is from 'strong-type';
|
|
2
|
-
import {
|
|
2
|
+
import {randomUUID} from 'node:crypto';
|
|
3
|
+
import {inspect} from 'node:util';
|
|
3
4
|
import {Server} from 'node-http-server';
|
|
4
5
|
|
|
5
6
|
const is = new Is(false);
|
|
@@ -8,18 +9,7 @@ export const RESEND_MAIL_SERVER_PROTOCOL='arcane-resend-mail-gateway/1';
|
|
|
8
9
|
export const RESEND_MAIL_PATH='/v1/mail';
|
|
9
10
|
|
|
10
11
|
const RESEND_EMAIL_ENDPOINT='https://api.resend.com/emails';
|
|
11
|
-
const APP_ID_PATTERN=/^[a-z0-9](?:[a-z0-9-]{0,62})$/u;
|
|
12
|
-
const EMAIL_PATTERN=/^[a-z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+$/iu;
|
|
13
|
-
const IDEMPOTENCY_KEY_PATTERN=/^[a-zA-Z0-9._:-]+$/u;
|
|
14
|
-
const PROVIDER_ID_PATTERN=/^[a-zA-Z0-9._:-]+$/u;
|
|
15
|
-
const PROVIDER_CODE_PATTERN=/^[a-z0-9_]+$/u;
|
|
16
12
|
const REQUEST_ID_PATTERN=/^[a-zA-Z0-9-]+$/u;
|
|
17
|
-
const JSON_CONTENT_TYPE_PATTERN=/^application\/json(?:\s*;\s*charset\s*=\s*"?utf-8"?)?$/iu;
|
|
18
|
-
const HEADER_NAME_PATTERN=/^[!#$%&'*+.^_`|~0-9a-z-]+$/iu;
|
|
19
|
-
const MAIL_TYPES=new Set(['error','report','crisis_detected']);
|
|
20
|
-
const PREFLIGHT_HEADERS=new Set([
|
|
21
|
-
'content-type','idempotency-key','x-mail-app','x-mail-key'
|
|
22
|
-
]);
|
|
23
13
|
const PERMANENT_RATE_CODES=new Set(['daily_quota_exceeded','monthly_quota_exceeded']);
|
|
24
14
|
const RETRYABLE_PROVIDER_STATUSES=new Set([408,425,429,500,502,503,504]);
|
|
25
15
|
const MAX_NODE_TIMER_DELAY_MS=2_147_483_647;
|
|
@@ -31,7 +21,7 @@ class MailGatewayFault extends Error {
|
|
|
31
21
|
this.code=code;
|
|
32
22
|
this.details=details;
|
|
33
23
|
this.retryable=Boolean(retryable);
|
|
34
|
-
this.retryAfterMs=
|
|
24
|
+
this.retryAfterMs=retryDelayOrZero(retryAfterMs);
|
|
35
25
|
this.statusCode=statusCode;
|
|
36
26
|
this.uncertain=Boolean(uncertain);
|
|
37
27
|
}
|
|
@@ -52,17 +42,27 @@ function completeErrorDetails(error){
|
|
|
52
42
|
...(is.string(error.code)?{code:error.code}:{}),
|
|
53
43
|
message:is.string(error.message)?error.message:String(error),
|
|
54
44
|
name:is.string(error.name)?error.name:'Error',
|
|
55
|
-
...(is.string(error.stack)?{stack:error.stack}:{})
|
|
45
|
+
...(is.string(error.stack)?{stack:error.stack}:{}),
|
|
46
|
+
...(error.cause===undefined?{}:{
|
|
47
|
+
cause:error.cause instanceof Error?completeErrorDetails(error.cause):error.cause
|
|
48
|
+
}),
|
|
49
|
+
...(error instanceof AggregateError?{errors:error.errors.map(completeErrorDetails)}:{})
|
|
56
50
|
};
|
|
57
51
|
}
|
|
58
52
|
|
|
59
|
-
function
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
53
|
+
function reportMailError(message,error){
|
|
54
|
+
console.error(message,inspect(error,{
|
|
55
|
+
depth:null,
|
|
56
|
+
maxArrayLength:null,
|
|
57
|
+
maxStringLength:null
|
|
58
|
+
}));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function readRetryDelayMs(retryDelayMs=1_000){
|
|
62
|
+
if(!is.safeInteger(retryDelayMs)||retryDelayMs<1){
|
|
63
|
+
throw configurationError('retryableDelayMs must be a positive integer.');
|
|
64
64
|
}
|
|
65
|
-
return
|
|
65
|
+
return retryDelayMs;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
function optionalTimeoutMs(value,label){
|
|
@@ -76,171 +76,17 @@ function optionalTimeoutMs(value,label){
|
|
|
76
76
|
return value;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
function
|
|
79
|
+
function retryDelayOrZero(value){
|
|
80
80
|
return is.safeInteger(value)&&value>0?value:0;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
function
|
|
84
|
-
const resolved=value===undefined?fallback:value;
|
|
85
|
-
if(!is.safeInteger(resolved)||resolved<0||resolved>65_535){
|
|
86
|
-
throw configurationError('port must be an integer between 0 and 65535.');
|
|
87
|
-
}
|
|
88
|
-
return resolved;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function validateSignal(signal){
|
|
92
|
-
if(signal!==undefined&&!(signal instanceof AbortSignal)){
|
|
93
|
-
throw new TypeError('Mail server signal must be an AbortSignal.');
|
|
94
|
-
}
|
|
95
|
-
return signal;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function validateApiKey(value){
|
|
99
|
-
if(!is.string(value)||value.length<1||!/^[\x21-\x7e]+$/u.test(value)){
|
|
100
|
-
throw configurationError('Resend API key must be a nonempty printable ASCII string.');
|
|
101
|
-
}
|
|
102
|
-
return value;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function validateAppId(value){
|
|
106
|
-
if(!is.string(value)||!APP_ID_PATTERN.test(value)){
|
|
107
|
-
throw configurationError('Mail application identity is invalid.');
|
|
108
|
-
}
|
|
109
|
-
return value;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function appKeyDigest(value){
|
|
113
|
-
return createHash('sha256').update(value,'utf8').digest();
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function normalizeCallerAuthentication(options){
|
|
117
|
-
const allowUnauthenticatedCaller=options.allowUnauthenticatedCaller??false;
|
|
118
|
-
if(!is.boolean(allowUnauthenticatedCaller)){
|
|
119
|
-
throw configurationError('allowUnauthenticatedCaller must be a boolean.');
|
|
120
|
-
}
|
|
121
|
-
if(allowUnauthenticatedCaller){
|
|
122
|
-
if(options.appKey!==undefined){
|
|
123
|
-
throw configurationError(
|
|
124
|
-
'appKey must be omitted when allowUnauthenticatedCaller is true.'
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
return {
|
|
128
|
-
appKeyDigest:null,
|
|
129
|
-
callerAuthentication:'origin-app-id-only'
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
if(!is.string(options.appKey)||!/^[\x21-\x7e]+$/u.test(options.appKey)){
|
|
133
|
-
throw configurationError(
|
|
134
|
-
'appKey must be a nonempty printable ASCII string unless unauthenticated caller mode is explicitly enabled.'
|
|
135
|
-
);
|
|
136
|
-
}
|
|
137
|
-
return {
|
|
138
|
-
appKeyDigest:appKeyDigest(options.appKey),
|
|
139
|
-
callerAuthentication:'app-key'
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function normalizedEmail(value,label){
|
|
144
|
-
if(!is.string(value)){
|
|
145
|
-
throw configurationError(`${label} must be an email address.`);
|
|
146
|
-
}
|
|
147
|
-
const normalized=value.trim().toLowerCase();
|
|
148
|
-
if(normalized.length<3||normalized.length>254||!EMAIL_PATTERN.test(normalized)){
|
|
149
|
-
throw configurationError(`${label} must be a valid email address.`);
|
|
150
|
-
}
|
|
151
|
-
return normalized;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function validateFrom(value){
|
|
155
|
-
if(!is.string(value)||value!==value.trim()||value.length<3||value.length>320
|
|
156
|
-
||/[\u0000-\u001f\u007f]/u.test(value)){
|
|
157
|
-
throw configurationError('Mail sender is invalid.');
|
|
158
|
-
}
|
|
159
|
-
if(EMAIL_PATTERN.test(value)){
|
|
160
|
-
return value.toLowerCase();
|
|
161
|
-
}
|
|
162
|
-
const match=/^([^<>]{1,64}) <([^<>]+)>$/u.exec(value);
|
|
163
|
-
if(!match||!match[1].trim()){
|
|
164
|
-
throw configurationError('Mail sender must be an email address or Name <email> value.');
|
|
165
|
-
}
|
|
166
|
-
const address=normalizedEmail(match[2],'Mail sender');
|
|
167
|
-
return `${match[1]} <${address}>`;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function normalizeEmailList(value,label,{allowEmpty=false}={}){
|
|
171
|
-
if(!is.array(value)||(!allowEmpty&&value.length===0)){
|
|
172
|
-
throw configurationError(`${label} must contain ${allowEmpty?'zero or more':'one or more'} addresses.`);
|
|
173
|
-
}
|
|
174
|
-
const result=[];
|
|
175
|
-
for(const entry of value){
|
|
176
|
-
const address=normalizedEmail(entry,label);
|
|
177
|
-
result.push(address);
|
|
178
|
-
}
|
|
179
|
-
return result;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function normalizeOrigin(value){
|
|
183
|
-
if(!is.string(value)||!value.trim()){
|
|
184
|
-
throw configurationError('Every allowed mail origin must be a URL origin.');
|
|
185
|
-
}
|
|
186
|
-
let url;
|
|
187
|
-
try{
|
|
188
|
-
url=new URL(value.trim());
|
|
189
|
-
}catch{
|
|
190
|
-
throw configurationError('Every allowed mail origin must be a valid URL origin.');
|
|
191
|
-
}
|
|
192
|
-
if(!['http:','https:'].includes(url.protocol)||url.username||url.password
|
|
193
|
-
||url.pathname!=='/'||url.search||url.hash){
|
|
194
|
-
throw configurationError('Every allowed mail origin must be an HTTP or HTTPS origin without credentials, path, query, or fragment.');
|
|
195
|
-
}
|
|
196
|
-
return url.origin;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function normalizeOrigins(value){
|
|
200
|
-
if(!is.array(value)||value.length===0){
|
|
201
|
-
throw configurationError('allowedOrigins must contain one or more exact origins.');
|
|
202
|
-
}
|
|
203
|
-
const origins=[];
|
|
204
|
-
const seen=new Set();
|
|
205
|
-
for(const entry of value){
|
|
206
|
-
const origin=normalizeOrigin(entry);
|
|
207
|
-
if(seen.has(origin)){
|
|
208
|
-
throw configurationError('allowedOrigins must not contain duplicate origins.');
|
|
209
|
-
}
|
|
210
|
-
seen.add(origin);
|
|
211
|
-
origins.push(origin);
|
|
212
|
-
}
|
|
213
|
-
return new Set(origins);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function validateLoopbackHost(value){
|
|
217
|
-
if(value!=='127.0.0.1'&&value!=='::1'){
|
|
218
|
-
throw configurationError('Mail server host must be the numeric loopback address 127.0.0.1 or ::1.');
|
|
219
|
-
}
|
|
220
|
-
return value;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function normalizeConfiguration(options={}){
|
|
83
|
+
function resolveMailServerConfiguration(options={}){
|
|
224
84
|
if(!options||!is.object(options)||is.array(options)){
|
|
225
85
|
throw configurationError('Mail server options must be an object.');
|
|
226
86
|
}
|
|
227
|
-
const
|
|
228
|
-
const
|
|
229
|
-
options.recipientAllowlist??[],
|
|
230
|
-
'recipientAllowlist',
|
|
231
|
-
{allowEmpty:true}
|
|
232
|
-
);
|
|
233
|
-
const errorRecipients=normalizeEmailList(
|
|
234
|
-
options.errorRecipients??[],
|
|
235
|
-
'errorRecipients',
|
|
236
|
-
{allowEmpty:true}
|
|
237
|
-
);
|
|
87
|
+
const recipientAllowlist=options.recipientAllowlist??[];
|
|
88
|
+
const errorRecipients=options.errorRecipients??[];
|
|
238
89
|
const allowedRecipients=new Set(recipientAllowlist);
|
|
239
|
-
if(recipientAllowlist.length>0&&errorRecipients.some(function errorRecipientIsNotAllowed(address){
|
|
240
|
-
return !allowedRecipients.has(address);
|
|
241
|
-
})){
|
|
242
|
-
throw configurationError('Every error recipient must also be in recipientAllowlist.');
|
|
243
|
-
}
|
|
244
90
|
const fetchImpl=options.fetchImpl??globalThis.fetch;
|
|
245
91
|
if(!is.function(fetchImpl)){
|
|
246
92
|
throw configurationError('A fetch implementation is required for Resend delivery.');
|
|
@@ -251,29 +97,28 @@ function normalizeConfiguration(options={}){
|
|
|
251
97
|
if(options.requestIdFactory!==undefined&&!is.function(options.requestIdFactory)){
|
|
252
98
|
throw configurationError('requestIdFactory must be a function when supplied.');
|
|
253
99
|
}
|
|
100
|
+
if(options.verifySubscription!==undefined&&!is.function(options.verifySubscription)){
|
|
101
|
+
throw configurationError('verifySubscription must be a function when supplied.');
|
|
102
|
+
}
|
|
254
103
|
return {
|
|
255
104
|
allowAnyRecipient:recipientAllowlist.length===0,
|
|
256
|
-
allowedOrigins:
|
|
105
|
+
allowedOrigins:new Set(options.allowedOrigins??[]),
|
|
257
106
|
allowedRecipients,
|
|
258
|
-
apiKey:
|
|
259
|
-
|
|
260
|
-
appId:validateAppId(options.appId),
|
|
107
|
+
apiKey:options.apiKey,
|
|
108
|
+
appId:options.appId,
|
|
261
109
|
bodyTimeoutMs:optionalTimeoutMs(options.bodyTimeoutMs,'bodyTimeoutMs'),
|
|
262
110
|
errorRecipients,
|
|
263
111
|
fetchImpl,
|
|
264
|
-
from:
|
|
265
|
-
host:
|
|
266
|
-
callerAuthentication:
|
|
112
|
+
from:options.from,
|
|
113
|
+
host:options.host??'0.0.0.0',
|
|
114
|
+
callerAuthentication:options.verifySubscription?'subscription':'none',
|
|
267
115
|
onEvent:options.onEvent,
|
|
268
|
-
port:
|
|
116
|
+
port:options.port??8025,
|
|
269
117
|
providerTimeoutMs:optionalTimeoutMs(options.providerTimeoutMs,'providerTimeoutMs'),
|
|
270
118
|
requestIdFactory:options.requestIdFactory??randomUUID,
|
|
271
|
-
retryableDelayMs:
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
{label:'retryableDelayMs'}
|
|
275
|
-
),
|
|
276
|
-
signal:validateSignal(options.signal)
|
|
119
|
+
retryableDelayMs:readRetryDelayMs(options.retryableDelayMs),
|
|
120
|
+
signal:options.signal,
|
|
121
|
+
verifySubscription:options.verifySubscription
|
|
277
122
|
};
|
|
278
123
|
}
|
|
279
124
|
|
|
@@ -289,148 +134,98 @@ function createRequestId(factory){
|
|
|
289
134
|
return randomUUID();
|
|
290
135
|
}
|
|
291
136
|
|
|
292
|
-
function
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
function headerValues(request,name){
|
|
297
|
-
const distinct=request.headersDistinct?.[name];
|
|
298
|
-
if(is.array(distinct)){
|
|
299
|
-
return distinct.map(function stringifyDistinctHeader(value){return String(value);});
|
|
300
|
-
}
|
|
301
|
-
const values=[];
|
|
302
|
-
for(let index=0;index<(request.rawHeaders?.length??0);index+=2){
|
|
303
|
-
if(String(request.rawHeaders[index]).toLowerCase()===name){
|
|
304
|
-
values.push(String(request.rawHeaders[index+1]??''));
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
if(values.length>0){
|
|
308
|
-
return values;
|
|
309
|
-
}
|
|
310
|
-
const fallback=request.headers?.[name];
|
|
311
|
-
if(fallback===undefined){
|
|
312
|
-
return [];
|
|
313
|
-
}
|
|
314
|
-
return is.array(fallback)?fallback.map(String):[String(fallback)];
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
function singleHeader(request,name,{required=true}={}){
|
|
318
|
-
const values=headerValues(request,name);
|
|
319
|
-
if(values.length===0&&!required){
|
|
320
|
-
return '';
|
|
321
|
-
}
|
|
322
|
-
if(values.length!==1||!values[0]){
|
|
137
|
+
function requireRequestHeader(request,headerName){
|
|
138
|
+
const headerValues=request.headersDistinct[headerName]??[];
|
|
139
|
+
if(headerValues.length!==1||!headerValues[0]){
|
|
323
140
|
throw new MailGatewayFault('mail_invalid_headers',{statusCode:400});
|
|
324
141
|
}
|
|
325
|
-
return
|
|
142
|
+
return headerValues[0];
|
|
326
143
|
}
|
|
327
144
|
|
|
328
|
-
function
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
145
|
+
async function verifyMailSubscription(request,configuration,signal){
|
|
146
|
+
const appName=requireRequestHeader(request,'x-mail-app');
|
|
147
|
+
const authorizationHeaders=request.headersDistinct.authorization??[];
|
|
148
|
+
const subscriptionKey=authorizationHeaders.length===1
|
|
149
|
+
?/^Bearer (.+)$/iu.exec(authorizationHeaders[0])?.[1]
|
|
150
|
+
:undefined;
|
|
151
|
+
if(!subscriptionKey){
|
|
152
|
+
throw new MailGatewayFault('mail_subscription_required',{statusCode:401});
|
|
332
153
|
}
|
|
333
|
-
|
|
334
|
-
let parsed;
|
|
154
|
+
let verified;
|
|
335
155
|
try{
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
throw new MailGatewayFault('mail_invalid_host',{statusCode:421});
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
function allowedOrigin(request,configuration){
|
|
353
|
-
const origin=singleHeader(request,'origin');
|
|
354
|
-
if(!configuration.allowedOrigins.has(origin)){
|
|
355
|
-
throw new MailGatewayFault('mail_origin_not_allowed',{statusCode:403});
|
|
356
|
-
}
|
|
357
|
-
return origin;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
function authenticateLocalCaller(request,configuration){
|
|
361
|
-
const values=headerValues(request,'x-mail-key');
|
|
362
|
-
if(configuration.callerAuthentication==='origin-app-id-only'){
|
|
363
|
-
if(values.length!==0){
|
|
364
|
-
throw new MailGatewayFault('mail_app_key_unexpected',{statusCode:403});
|
|
156
|
+
signal.throwIfAborted();
|
|
157
|
+
verified=await waitForResultOrAbort(configuration.verifySubscription({
|
|
158
|
+
appName,
|
|
159
|
+
subscriptionKey,
|
|
160
|
+
signal
|
|
161
|
+
}),signal);
|
|
162
|
+
}catch(error){
|
|
163
|
+
if(signal.aborted){
|
|
164
|
+
throw new MailGatewayFault('mail_request_cancelled',{
|
|
165
|
+
retryable:true,
|
|
166
|
+
statusCode:408
|
|
167
|
+
});
|
|
365
168
|
}
|
|
366
|
-
|
|
169
|
+
throw new MailGatewayFault('mail_subscription_verification_failed',{
|
|
170
|
+
details:completeErrorDetails(error),
|
|
171
|
+
retryable:true,
|
|
172
|
+
retryAfterMs:configuration.retryableDelayMs,
|
|
173
|
+
statusCode:503
|
|
174
|
+
});
|
|
367
175
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
const digest=appKeyDigest(candidateIsValid?candidate:'');
|
|
371
|
-
const authenticated=timingSafeEqual(configuration.appKeyDigest,digest);
|
|
372
|
-
if(values.length!==1||!candidateIsValid||!authenticated){
|
|
373
|
-
throw new MailGatewayFault('mail_app_key_invalid',{statusCode:401});
|
|
176
|
+
if(verified!==true){
|
|
177
|
+
throw new MailGatewayFault('mail_subscription_invalid',{statusCode:401});
|
|
374
178
|
}
|
|
179
|
+
return appName;
|
|
375
180
|
}
|
|
376
181
|
|
|
377
|
-
function
|
|
182
|
+
function createCorsResponseHeaders(origin){
|
|
378
183
|
if(!origin){
|
|
379
184
|
return {};
|
|
380
185
|
}
|
|
381
186
|
return {
|
|
382
|
-
'access-control-allow-headers':'Content-Type, Idempotency-Key, X-Mail-App,
|
|
187
|
+
'access-control-allow-headers':'Content-Type, Idempotency-Key, X-Mail-App, Authorization',
|
|
383
188
|
'access-control-allow-methods':'POST, OPTIONS',
|
|
384
189
|
'access-control-allow-origin':origin,
|
|
385
190
|
'access-control-expose-headers':'Retry-After',
|
|
386
191
|
'access-control-max-age':'600',
|
|
387
|
-
...(allowPrivateNetwork?{'access-control-allow-private-network':'true'}:{}),
|
|
388
192
|
'vary':'Origin'
|
|
389
193
|
};
|
|
390
194
|
}
|
|
391
195
|
|
|
392
|
-
function
|
|
393
|
-
return corsHeaders(origin);
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
function writeJson(response,statusCode,value,{origin='',retryAfterMs=0}={}){
|
|
196
|
+
function sendJsonResponse(response,statusCode,value,{origin='',retryAfterMs=0}={}){
|
|
397
197
|
if(response.destroyed||response.writableEnded){
|
|
398
198
|
return false;
|
|
399
199
|
}
|
|
400
200
|
const body=JSON.stringify(value);
|
|
401
201
|
const headers={
|
|
402
|
-
...
|
|
403
|
-
'content-length':String(Buffer.byteLength(body,'utf8')),
|
|
202
|
+
...createCorsResponseHeaders(origin),
|
|
404
203
|
'content-type':'application/json; charset=utf-8'
|
|
405
204
|
};
|
|
406
|
-
|
|
407
|
-
if(
|
|
408
|
-
headers['retry-after']=String(Math.
|
|
205
|
+
if(statusCode===401)headers['www-authenticate']='Bearer';
|
|
206
|
+
if(retryAfterMs){
|
|
207
|
+
headers['retry-after']=String(Math.ceil(retryAfterMs/1000));
|
|
409
208
|
}
|
|
410
209
|
response.writeHead(statusCode,headers);
|
|
411
210
|
response.end(body);
|
|
412
211
|
return true;
|
|
413
212
|
}
|
|
414
213
|
|
|
415
|
-
function
|
|
214
|
+
function sendCorsPreflightResponse(response,origin){
|
|
416
215
|
if(response.destroyed||response.writableEnded){
|
|
417
216
|
return false;
|
|
418
217
|
}
|
|
419
|
-
response.writeHead(204,
|
|
420
|
-
...baseResponseHeaders(origin),
|
|
421
|
-
...corsHeaders(origin,{allowPrivateNetwork}),
|
|
422
|
-
'content-length':'0'
|
|
423
|
-
});
|
|
218
|
+
response.writeHead(204,createCorsResponseHeaders(origin));
|
|
424
219
|
response.end();
|
|
425
220
|
return true;
|
|
426
221
|
}
|
|
427
222
|
|
|
428
|
-
function
|
|
429
|
-
const retryAfterMs=
|
|
430
|
-
return
|
|
223
|
+
function sendMailFailureResponse(response,requestId,fault,origin=''){
|
|
224
|
+
const retryAfterMs=fault.retryAfterMs;
|
|
225
|
+
return sendJsonResponse(response,fault.statusCode,{
|
|
431
226
|
requestId,
|
|
432
227
|
error:{
|
|
433
|
-
code:
|
|
228
|
+
code:fault.code,
|
|
434
229
|
message:fault.message,
|
|
435
230
|
details:fault.details,
|
|
436
231
|
retryable:Boolean(fault.retryable),
|
|
@@ -440,7 +235,7 @@ function writeFault(response,requestId,fault,origin=''){
|
|
|
440
235
|
},{origin,retryAfterMs});
|
|
441
236
|
}
|
|
442
237
|
|
|
443
|
-
function
|
|
238
|
+
function mailFaultFromError(error){
|
|
444
239
|
if(error instanceof MailGatewayFault){
|
|
445
240
|
return error;
|
|
446
241
|
}
|
|
@@ -450,85 +245,62 @@ function normalizeFault(error){
|
|
|
450
245
|
});
|
|
451
246
|
}
|
|
452
247
|
|
|
453
|
-
function
|
|
454
|
-
|
|
455
|
-
|
|
248
|
+
function createMailEventObserver(onEvent){
|
|
249
|
+
const pendingObserverTasks=new Set();
|
|
250
|
+
function reportObserverFailure(error){
|
|
251
|
+
reportMailError('Mail event observer failed.',error);
|
|
456
252
|
}
|
|
457
|
-
const rawHeaders=singleHeader(request,'access-control-request-headers');
|
|
458
|
-
const requestedHeaders=rawHeaders.split(',').map(function normalizeRequestedHeader(value){
|
|
459
|
-
return value.trim().toLowerCase();
|
|
460
|
-
});
|
|
461
|
-
if(requestedHeaders.length===0||requestedHeaders.some(function headerIsNotAllowed(value){
|
|
462
|
-
return !value||!HEADER_NAME_PATTERN.test(value)||!PREFLIGHT_HEADERS.has(value);
|
|
463
|
-
})){
|
|
464
|
-
throw new MailGatewayFault('mail_preflight_denied',{statusCode:403});
|
|
465
|
-
}
|
|
466
|
-
const privateNetwork=singleHeader(
|
|
467
|
-
request,
|
|
468
|
-
'access-control-request-private-network',
|
|
469
|
-
{required:false}
|
|
470
|
-
);
|
|
471
|
-
if(privateNetwork&&privateNetwork!=='true'){
|
|
472
|
-
throw new MailGatewayFault('mail_preflight_denied',{statusCode:403});
|
|
473
|
-
}
|
|
474
|
-
return {allowPrivateNetwork:privateNetwork==='true'};
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
function createObserver(onEvent){
|
|
478
|
-
const pending=new Set();
|
|
479
253
|
function observe(event){
|
|
480
|
-
|
|
481
|
-
return;
|
|
482
|
-
}
|
|
483
|
-
let result;
|
|
254
|
+
let observerResult;
|
|
484
255
|
try{
|
|
485
|
-
|
|
486
|
-
}catch{
|
|
256
|
+
observerResult=onEvent(event);
|
|
257
|
+
}catch(error){
|
|
258
|
+
reportObserverFailure(error);
|
|
487
259
|
return;
|
|
488
260
|
}
|
|
489
|
-
if(!
|
|
261
|
+
if(!observerResult||!is.function(observerResult.then)){
|
|
490
262
|
return;
|
|
491
263
|
}
|
|
492
|
-
const
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
.finally(function releaseObserverTask(){
|
|
264
|
+
const observerTask=Promise.resolve(observerResult);
|
|
265
|
+
pendingObserverTasks.add(observerTask);
|
|
266
|
+
observerTask.catch(reportObserverFailure)
|
|
267
|
+
.finally(function releaseObserverTask(){pendingObserverTasks.delete(observerTask);});
|
|
496
268
|
}
|
|
497
|
-
async function
|
|
498
|
-
await Promise.allSettled([...
|
|
269
|
+
async function drainObserverTasks(){
|
|
270
|
+
await Promise.allSettled([...pendingObserverTasks]);
|
|
499
271
|
}
|
|
500
|
-
return {drain,observe};
|
|
272
|
+
return {drain:drainObserverTasks,observe};
|
|
501
273
|
}
|
|
502
274
|
|
|
503
|
-
function
|
|
504
|
-
return new Promise(function
|
|
505
|
-
const
|
|
506
|
-
let
|
|
507
|
-
const
|
|
275
|
+
function readRequestBodyText(request,{timeoutMs,signal}){
|
|
276
|
+
return new Promise(function collectRequestBodyText(resolve,reject){
|
|
277
|
+
const bodyChunks=[];
|
|
278
|
+
let bodyReadSettled=false;
|
|
279
|
+
const bodyTimeout=timeoutMs==null
|
|
508
280
|
?null
|
|
509
281
|
:setTimeout(function expireRequestBody(){
|
|
510
|
-
|
|
282
|
+
settleBodyRead(new MailGatewayFault('mail_body_timeout',{
|
|
511
283
|
retryable:true,
|
|
512
284
|
statusCode:408
|
|
513
285
|
}));
|
|
514
286
|
request.resume();
|
|
515
287
|
},timeoutMs);
|
|
516
288
|
|
|
517
|
-
function
|
|
518
|
-
if(
|
|
519
|
-
request.removeListener('data',
|
|
520
|
-
request.removeListener('end',
|
|
521
|
-
request.removeListener('error',
|
|
522
|
-
request.removeListener('aborted',
|
|
523
|
-
signal?.removeEventListener('abort',
|
|
289
|
+
function releaseBodyReadResources(){
|
|
290
|
+
if(bodyTimeout!==null) clearTimeout(bodyTimeout);
|
|
291
|
+
request.removeListener('data',collectBodyChunk);
|
|
292
|
+
request.removeListener('end',completeBodyRead);
|
|
293
|
+
request.removeListener('error',rejectFailedBodyRead);
|
|
294
|
+
request.removeListener('aborted',rejectAbortedBodyRead);
|
|
295
|
+
signal?.removeEventListener('abort',cancelBodyReadFromSignal);
|
|
524
296
|
}
|
|
525
297
|
|
|
526
|
-
function
|
|
527
|
-
if(
|
|
298
|
+
function settleBodyRead(error,value){
|
|
299
|
+
if(bodyReadSettled){
|
|
528
300
|
return;
|
|
529
301
|
}
|
|
530
|
-
|
|
531
|
-
|
|
302
|
+
bodyReadSettled=true;
|
|
303
|
+
releaseBodyReadResources();
|
|
532
304
|
if(error){
|
|
533
305
|
reject(error);
|
|
534
306
|
}else{
|
|
@@ -536,154 +308,106 @@ function readRequestBody(request,{timeoutMs,signal}){
|
|
|
536
308
|
}
|
|
537
309
|
}
|
|
538
310
|
|
|
539
|
-
function
|
|
540
|
-
const
|
|
541
|
-
|
|
311
|
+
function collectBodyChunk(chunk){
|
|
312
|
+
const bodyChunk=Buffer.isBuffer(chunk)?chunk:Buffer.from(chunk);
|
|
313
|
+
bodyChunks.push(bodyChunk);
|
|
542
314
|
}
|
|
543
315
|
|
|
544
|
-
function
|
|
545
|
-
|
|
316
|
+
function completeBodyRead(){
|
|
317
|
+
settleBodyRead(null,Buffer.concat(bodyChunks).toString('utf8'));
|
|
546
318
|
}
|
|
547
319
|
|
|
548
|
-
function
|
|
549
|
-
|
|
320
|
+
function rejectFailedBodyRead(error){
|
|
321
|
+
settleBodyRead(new MailGatewayFault('mail_request_stream_failed',{
|
|
550
322
|
details:completeErrorDetails(error),
|
|
551
323
|
retryable:true,
|
|
552
324
|
statusCode:400
|
|
553
325
|
}));
|
|
554
326
|
}
|
|
555
327
|
|
|
556
|
-
function
|
|
557
|
-
|
|
328
|
+
function rejectAbortedBodyRead(){
|
|
329
|
+
settleBodyRead(new MailGatewayFault('mail_request_cancelled',{
|
|
558
330
|
retryable:true,
|
|
559
331
|
statusCode:408
|
|
560
332
|
}));
|
|
561
333
|
}
|
|
562
334
|
|
|
563
|
-
function
|
|
564
|
-
|
|
335
|
+
function cancelBodyReadFromSignal(){
|
|
336
|
+
settleBodyRead(new MailGatewayFault('mail_request_cancelled',{
|
|
565
337
|
retryable:true,
|
|
566
338
|
statusCode:408
|
|
567
339
|
}));
|
|
568
340
|
request.resume();
|
|
569
341
|
}
|
|
570
342
|
|
|
571
|
-
request.on('data',
|
|
572
|
-
request.once('end',
|
|
573
|
-
request.once('error',
|
|
574
|
-
request.once('aborted',
|
|
575
|
-
signal?.addEventListener('abort',
|
|
343
|
+
request.on('data',collectBodyChunk);
|
|
344
|
+
request.once('end',completeBodyRead);
|
|
345
|
+
request.once('error',rejectFailedBodyRead);
|
|
346
|
+
request.once('aborted',rejectAbortedBodyRead);
|
|
347
|
+
signal?.addEventListener('abort',cancelBodyReadFromSignal,{once:true});
|
|
576
348
|
if(signal?.aborted){
|
|
577
|
-
|
|
349
|
+
cancelBodyReadFromSignal();
|
|
578
350
|
}
|
|
579
351
|
});
|
|
580
352
|
}
|
|
581
353
|
|
|
582
|
-
function
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
function normalizeReportRecipients(report,configuration){
|
|
594
|
-
if(!is.array(report.to)){
|
|
595
|
-
throw new MailGatewayFault('mail_invalid_recipients',{statusCode:422});
|
|
596
|
-
}
|
|
597
|
-
const recipients=[];
|
|
598
|
-
for(const value of report.to){
|
|
599
|
-
const address=normalizedReportEmail(value);
|
|
600
|
-
if(!configuration.allowAnyRecipient&&!configuration.allowedRecipients.has(address)){
|
|
601
|
-
throw new MailGatewayFault('mail_recipient_not_allowed',{statusCode:403});
|
|
354
|
+
function resolveReportRecipients(report,configuration){
|
|
355
|
+
const recipients=is.array(report.to)&&report.to.length===0&&report.type==='error'
|
|
356
|
+
?configuration.errorRecipients
|
|
357
|
+
:report.to;
|
|
358
|
+
if(!configuration.allowAnyRecipient){
|
|
359
|
+
for(const recipientGroup of [recipients,report.cc,report.bcc]){
|
|
360
|
+
if(recipientGroup===undefined)continue;
|
|
361
|
+
for(const recipient of is.array(recipientGroup)?recipientGroup:[recipientGroup]){
|
|
362
|
+
if(configuration.allowedRecipients.has(recipient))continue;
|
|
363
|
+
throw new MailGatewayFault('mail_recipient_not_allowed',{statusCode:403});
|
|
364
|
+
}
|
|
602
365
|
}
|
|
603
|
-
recipients.push(address);
|
|
604
|
-
}
|
|
605
|
-
if(recipients.length===0&&report.type==='error'){
|
|
606
|
-
recipients.push(...configuration.errorRecipients);
|
|
607
|
-
}
|
|
608
|
-
if(recipients.length===0){
|
|
609
|
-
throw new MailGatewayFault('mail_recipients_required',{statusCode:422});
|
|
610
366
|
}
|
|
611
367
|
return recipients;
|
|
612
368
|
}
|
|
613
369
|
|
|
614
|
-
function
|
|
615
|
-
if(!
|
|
370
|
+
function prepareProviderDelivery(report,configuration){
|
|
371
|
+
if(!report||!is.object(report)||is.array(report)){
|
|
616
372
|
throw new MailGatewayFault('mail_invalid_report',{statusCode:422});
|
|
617
373
|
}
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
if(!is.string(value.type)||!MAIL_TYPES.has(value.type)){
|
|
623
|
-
throw new MailGatewayFault('mail_invalid_type',{statusCode:422});
|
|
624
|
-
}
|
|
625
|
-
if(!is.string(value.subject)){
|
|
626
|
-
throw new MailGatewayFault('mail_invalid_subject',{statusCode:422});
|
|
627
|
-
}
|
|
628
|
-
const hasText=Object.hasOwn(value,'text');
|
|
629
|
-
const hasHtml=Object.hasOwn(value,'html');
|
|
630
|
-
if(!hasText&&!hasHtml||(hasText&&!is.string(value.text))
|
|
631
|
-
||(hasHtml&&!is.string(value.html))){
|
|
632
|
-
throw new MailGatewayFault('mail_content_required',{statusCode:422});
|
|
633
|
-
}
|
|
634
|
-
const recipients=normalizeReportRecipients(value,configuration);
|
|
635
|
-
const providerFields={...value};
|
|
636
|
-
delete providerFields.type;
|
|
637
|
-
const providerBody={
|
|
638
|
-
...providerFields,
|
|
639
|
-
from:configuration.from,
|
|
640
|
-
to:recipients,
|
|
641
|
-
subject:value.subject,
|
|
642
|
-
...(hasText?{text:value.text}:{}),
|
|
643
|
-
...(hasHtml?{html:value.html}:{})
|
|
644
|
-
};
|
|
645
|
-
const serializedProviderBody=JSON.stringify(providerBody);
|
|
374
|
+
const recipients=resolveReportRecipients(report,configuration);
|
|
375
|
+
const providerRequest={...report,to:recipients};
|
|
376
|
+
delete providerRequest.type;
|
|
377
|
+
if(configuration.from!==undefined)providerRequest.from=configuration.from;
|
|
646
378
|
return {
|
|
647
|
-
report
|
|
648
|
-
|
|
649
|
-
recipientCount:recipients.length
|
|
379
|
+
report,
|
|
380
|
+
serializedProviderRequest:JSON.stringify(providerRequest),
|
|
381
|
+
recipientCount:is.array(recipients)?recipients.length:recipients?1:0
|
|
650
382
|
};
|
|
651
383
|
}
|
|
652
384
|
|
|
653
|
-
function
|
|
654
|
-
let
|
|
385
|
+
function parseMailRequest(requestText,configuration){
|
|
386
|
+
let report;
|
|
655
387
|
try{
|
|
656
|
-
|
|
388
|
+
report=JSON.parse(requestText);
|
|
657
389
|
}catch{
|
|
658
390
|
throw new MailGatewayFault('mail_invalid_json',{statusCode:400});
|
|
659
391
|
}
|
|
660
|
-
return
|
|
392
|
+
return prepareProviderDelivery(report,configuration);
|
|
661
393
|
}
|
|
662
394
|
|
|
663
|
-
function
|
|
664
|
-
if(!is.string(value)
|
|
395
|
+
function parseRetryAfterMilliseconds(value,now=Date.now()){
|
|
396
|
+
if(!is.string(value)){
|
|
665
397
|
return 0;
|
|
666
398
|
}
|
|
667
|
-
const
|
|
668
|
-
if(
|
|
669
|
-
|
|
399
|
+
const retryAfterValue=value.trim();
|
|
400
|
+
if(!retryAfterValue)return 0;
|
|
401
|
+
if(/^\d+(?:\.\d+)?$/u.test(retryAfterValue)){
|
|
402
|
+
return retryDelayOrZero(Math.ceil(Number(retryAfterValue)*1000));
|
|
670
403
|
}
|
|
671
|
-
const timestamp=Date.parse(
|
|
404
|
+
const timestamp=Date.parse(retryAfterValue);
|
|
672
405
|
return is.finite(timestamp)
|
|
673
|
-
?
|
|
406
|
+
? retryDelayOrZero(Math.max(0,timestamp-now))
|
|
674
407
|
: 0;
|
|
675
408
|
}
|
|
676
409
|
|
|
677
|
-
function
|
|
678
|
-
try{
|
|
679
|
-
const value=response.headers?.get?.(name);
|
|
680
|
-
return value===null||value===undefined?'':String(value);
|
|
681
|
-
}catch{
|
|
682
|
-
return '';
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
function awaitAbortable(value,signal){
|
|
410
|
+
function waitForResultOrAbort(value,signal){
|
|
687
411
|
return new Promise(function waitForAbortable(resolve,reject){
|
|
688
412
|
let settled=false;
|
|
689
413
|
function cleanup(){
|
|
@@ -736,7 +460,7 @@ function cancelProviderReader(reader){
|
|
|
736
460
|
}
|
|
737
461
|
}
|
|
738
462
|
|
|
739
|
-
async function
|
|
463
|
+
async function readProviderResponseText(response,signal){
|
|
740
464
|
if(response.body===null||response.body===undefined){
|
|
741
465
|
return '';
|
|
742
466
|
}
|
|
@@ -749,24 +473,28 @@ async function readProviderBody(response,signal){
|
|
|
749
473
|
}
|
|
750
474
|
const reader=response.body.getReader();
|
|
751
475
|
const decoder=new TextDecoder();
|
|
752
|
-
let
|
|
476
|
+
let providerResponseText='';
|
|
753
477
|
let fullyRead=false;
|
|
754
478
|
try{
|
|
755
479
|
while(true){
|
|
756
|
-
const result=await
|
|
480
|
+
const result=await waitForResultOrAbort(reader.read(),signal);
|
|
757
481
|
if(result.done){
|
|
758
482
|
fullyRead=true;
|
|
759
483
|
break;
|
|
760
484
|
}
|
|
761
|
-
|
|
762
|
-
throw new MailGatewayFault('resend_unreadable_response',{
|
|
763
|
-
statusCode:502,
|
|
764
|
-
uncertain:true
|
|
765
|
-
});
|
|
766
|
-
}
|
|
767
|
-
text+=decoder.decode(result.value,{stream:true});
|
|
485
|
+
providerResponseText+=decoder.decode(result.value,{stream:true});
|
|
768
486
|
}
|
|
769
|
-
return
|
|
487
|
+
return providerResponseText+decoder.decode();
|
|
488
|
+
}catch(error){
|
|
489
|
+
throw new MailGatewayFault('resend_unreadable_response',{
|
|
490
|
+
details:{
|
|
491
|
+
error:completeErrorDetails(error),
|
|
492
|
+
responseText:providerResponseText+decoder.decode(),
|
|
493
|
+
responseComplete:false
|
|
494
|
+
},
|
|
495
|
+
statusCode:502,
|
|
496
|
+
uncertain:true
|
|
497
|
+
});
|
|
770
498
|
}finally{
|
|
771
499
|
if(!fullyRead){
|
|
772
500
|
cancelProviderReader(reader);
|
|
@@ -774,40 +502,35 @@ async function readProviderBody(response,signal){
|
|
|
774
502
|
try{
|
|
775
503
|
reader.releaseLock();
|
|
776
504
|
}catch{
|
|
777
|
-
//
|
|
505
|
+
// Stream cleanup cannot replace the provider outcome.
|
|
778
506
|
}
|
|
779
507
|
}
|
|
780
508
|
}
|
|
781
509
|
|
|
782
|
-
function
|
|
783
|
-
if(!text){
|
|
784
|
-
return null;
|
|
785
|
-
}
|
|
510
|
+
function parseProviderResponse(providerResponseText){
|
|
786
511
|
try{
|
|
787
|
-
|
|
788
|
-
return value&&is.object(value)&&!is.array(value)?value:null;
|
|
512
|
+
return JSON.parse(providerResponseText);
|
|
789
513
|
}catch{
|
|
790
|
-
return
|
|
514
|
+
return providerResponseText;
|
|
791
515
|
}
|
|
792
516
|
}
|
|
793
517
|
|
|
794
|
-
function
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
return candidate;
|
|
518
|
+
function resolveProviderErrorCode(providerResponse,statusCode){
|
|
519
|
+
if(is.string(providerResponse?.name)&&providerResponse.name){
|
|
520
|
+
return providerResponse.name;
|
|
798
521
|
}
|
|
799
522
|
return `resend_http_${String(statusCode)}`;
|
|
800
523
|
}
|
|
801
524
|
|
|
802
|
-
function
|
|
803
|
-
const code=
|
|
525
|
+
function classifyProviderRejection(statusCode,value,retryAfterMs,defaultRetryAfterMs){
|
|
526
|
+
const code=resolveProviderErrorCode(value,statusCode);
|
|
804
527
|
const permanentRateLimit=PERMANENT_RATE_CODES.has(code);
|
|
805
528
|
const retryable=code==='concurrent_idempotent_requests'
|
|
806
529
|
||(statusCode===409&&code!=='invalid_idempotent_request')
|
|
807
530
|
||(!permanentRateLimit&&code!=='invalid_idempotent_request'
|
|
808
531
|
&&RETRYABLE_PROVIDER_STATUSES.has(statusCode));
|
|
809
532
|
const resolvedDelay=retryable
|
|
810
|
-
?
|
|
533
|
+
? retryAfterMs||defaultRetryAfterMs
|
|
811
534
|
: 0;
|
|
812
535
|
return {
|
|
813
536
|
kind:'rejected',
|
|
@@ -821,50 +544,50 @@ function providerRejection(statusCode,value,retryAfterMs,defaultRetryAfterMs){
|
|
|
821
544
|
};
|
|
822
545
|
}
|
|
823
546
|
|
|
824
|
-
function
|
|
547
|
+
function createUncertainProviderResult(code,retryAfterMs,providerStatus=0,details=null){
|
|
825
548
|
return {
|
|
826
549
|
code,
|
|
827
550
|
details,
|
|
828
551
|
kind:'ambiguous',
|
|
829
552
|
providerStatus,
|
|
830
|
-
retryAfterMs:
|
|
553
|
+
retryAfterMs:retryDelayOrZero(retryAfterMs)
|
|
831
554
|
};
|
|
832
555
|
}
|
|
833
556
|
|
|
834
|
-
async function
|
|
557
|
+
async function attemptResendDelivery(configuration,delivery,idempotencyKey,signal,requestId,observe,appId=configuration.appId){
|
|
835
558
|
const controller=new AbortController();
|
|
836
559
|
let outcome=null;
|
|
837
560
|
let timedOut=false;
|
|
838
|
-
function
|
|
561
|
+
function recordAttemptOutcome(result){
|
|
839
562
|
outcome=result;
|
|
840
563
|
return result;
|
|
841
564
|
}
|
|
842
|
-
function
|
|
565
|
+
function abortProviderRequest(){
|
|
843
566
|
controller.abort(signal?.reason??new Error('Mail request cancelled.'));
|
|
844
567
|
}
|
|
845
|
-
signal?.addEventListener('abort',
|
|
568
|
+
signal?.addEventListener('abort',abortProviderRequest,{once:true});
|
|
846
569
|
if(signal?.aborted){
|
|
847
|
-
|
|
570
|
+
abortProviderRequest();
|
|
848
571
|
}
|
|
849
572
|
const timeout=configuration.providerTimeoutMs==null
|
|
850
573
|
?null
|
|
851
|
-
:setTimeout(function
|
|
574
|
+
:setTimeout(function abortTimedOutProviderRequest(){
|
|
852
575
|
timedOut=true;
|
|
853
576
|
controller.abort(new Error('Resend request timed out.'));
|
|
854
577
|
},configuration.providerTimeoutMs);
|
|
855
|
-
const startedAt=Date.now();
|
|
856
|
-
observe({
|
|
578
|
+
const startedAt=configuration.onEvent?Date.now():0;
|
|
579
|
+
if(configuration.onEvent)observe({
|
|
857
580
|
type:'mail.provider.started',
|
|
858
|
-
appId
|
|
581
|
+
appId,
|
|
859
582
|
idempotencyKey,
|
|
860
|
-
providerRequest:JSON.parse(delivery.
|
|
583
|
+
providerRequest:JSON.parse(delivery.serializedProviderRequest),
|
|
861
584
|
report:delivery.report,
|
|
862
585
|
requestId
|
|
863
586
|
});
|
|
864
587
|
let response;
|
|
865
588
|
try{
|
|
866
589
|
try{
|
|
867
|
-
response=await
|
|
590
|
+
response=await waitForResultOrAbort(configuration.fetchImpl(RESEND_EMAIL_ENDPOINT,{
|
|
868
591
|
method:'POST',
|
|
869
592
|
headers:{
|
|
870
593
|
'Authorization':`Bearer ${configuration.apiKey}`,
|
|
@@ -872,13 +595,12 @@ async function performResendAttempt(configuration,delivery,idempotencyKey,signal
|
|
|
872
595
|
'Idempotency-Key':idempotencyKey,
|
|
873
596
|
'User-Agent':'arcane-os-sdk-mail/1'
|
|
874
597
|
},
|
|
875
|
-
body:delivery.
|
|
598
|
+
body:delivery.serializedProviderRequest,
|
|
876
599
|
redirect:'error',
|
|
877
|
-
referrerPolicy:'no-referrer',
|
|
878
600
|
signal:controller.signal
|
|
879
601
|
}),controller.signal);
|
|
880
602
|
}catch(error){
|
|
881
|
-
return
|
|
603
|
+
return recordAttemptOutcome(createUncertainProviderResult(
|
|
882
604
|
timedOut?'resend_timeout':'resend_transport_uncertain',
|
|
883
605
|
configuration.retryableDelayMs,
|
|
884
606
|
0,
|
|
@@ -887,64 +609,64 @@ async function performResendAttempt(configuration,delivery,idempotencyKey,signal
|
|
|
887
609
|
}
|
|
888
610
|
const statusCode=Number(response?.status);
|
|
889
611
|
if(!is.safeInteger(statusCode)||statusCode<100||statusCode>599){
|
|
890
|
-
return
|
|
612
|
+
return recordAttemptOutcome(createUncertainProviderResult(
|
|
891
613
|
'resend_invalid_response',
|
|
892
614
|
configuration.retryableDelayMs,
|
|
893
615
|
0,
|
|
894
616
|
{status:response?.status??null}
|
|
895
617
|
));
|
|
896
618
|
}
|
|
897
|
-
let
|
|
619
|
+
let providerResponseText='';
|
|
898
620
|
try{
|
|
899
|
-
|
|
621
|
+
providerResponseText=await readProviderResponseText(response,controller.signal);
|
|
900
622
|
}catch(error){
|
|
901
623
|
if(statusCode>=200&&statusCode<300||controller.signal.aborted){
|
|
902
|
-
return
|
|
624
|
+
return recordAttemptOutcome(createUncertainProviderResult(
|
|
903
625
|
error instanceof MailGatewayFault?error.code:'resend_transport_uncertain',
|
|
904
626
|
configuration.retryableDelayMs,
|
|
905
627
|
statusCode,
|
|
906
628
|
completeErrorDetails(error)
|
|
907
629
|
));
|
|
908
630
|
}
|
|
909
|
-
return
|
|
631
|
+
return recordAttemptOutcome(classifyProviderRejection(
|
|
910
632
|
statusCode,
|
|
911
|
-
|
|
912
|
-
|
|
633
|
+
completeErrorDetails(error),
|
|
634
|
+
parseRetryAfterMilliseconds(response.headers.get('retry-after')),
|
|
913
635
|
configuration.retryableDelayMs
|
|
914
636
|
));
|
|
915
637
|
}
|
|
916
|
-
const
|
|
638
|
+
const providerResponse=parseProviderResponse(providerResponseText);
|
|
917
639
|
if(statusCode>=200&&statusCode<300){
|
|
918
|
-
if(!
|
|
919
|
-
return
|
|
640
|
+
if(!is.string(providerResponse?.id)||!providerResponse.id){
|
|
641
|
+
return recordAttemptOutcome(createUncertainProviderResult(
|
|
920
642
|
'resend_invalid_success_response',
|
|
921
643
|
configuration.retryableDelayMs,
|
|
922
644
|
statusCode,
|
|
923
|
-
|
|
645
|
+
providerResponse
|
|
924
646
|
));
|
|
925
647
|
}
|
|
926
|
-
return
|
|
648
|
+
return recordAttemptOutcome({
|
|
927
649
|
kind:'accepted',
|
|
928
|
-
providerId:
|
|
929
|
-
providerResponse
|
|
650
|
+
providerId:providerResponse.id,
|
|
651
|
+
providerResponse,
|
|
930
652
|
providerStatus:statusCode
|
|
931
653
|
});
|
|
932
654
|
}
|
|
933
|
-
return
|
|
655
|
+
return recordAttemptOutcome(classifyProviderRejection(
|
|
934
656
|
statusCode,
|
|
935
|
-
|
|
936
|
-
|
|
657
|
+
providerResponse,
|
|
658
|
+
parseRetryAfterMilliseconds(response.headers.get('retry-after')),
|
|
937
659
|
configuration.retryableDelayMs
|
|
938
660
|
));
|
|
939
661
|
}finally{
|
|
940
662
|
if(timeout!==null) clearTimeout(timeout);
|
|
941
|
-
signal?.removeEventListener('abort',
|
|
942
|
-
observe({
|
|
663
|
+
signal?.removeEventListener('abort',abortProviderRequest);
|
|
664
|
+
if(configuration.onEvent)observe({
|
|
943
665
|
type:'mail.provider.completed',
|
|
944
|
-
appId
|
|
666
|
+
appId,
|
|
945
667
|
idempotencyKey,
|
|
946
668
|
outcome,
|
|
947
|
-
providerRequest:JSON.parse(delivery.
|
|
669
|
+
providerRequest:JSON.parse(delivery.serializedProviderRequest),
|
|
948
670
|
report:delivery.report,
|
|
949
671
|
durationMs:Math.max(0,Date.now()-startedAt),
|
|
950
672
|
requestId,
|
|
@@ -953,12 +675,12 @@ async function performResendAttempt(configuration,delivery,idempotencyKey,signal
|
|
|
953
675
|
}
|
|
954
676
|
}
|
|
955
677
|
|
|
956
|
-
function
|
|
678
|
+
function resolveDirectSendConfiguration(options){
|
|
957
679
|
if(!options||!is.object(options)||is.array(options)){
|
|
958
680
|
throw configurationError('Mail send options must be an object.');
|
|
959
681
|
}
|
|
960
|
-
if(!is.string(options.reportKey)||!
|
|
961
|
-
throw configurationError('reportKey
|
|
682
|
+
if(!is.string(options.reportKey)||!options.reportKey){
|
|
683
|
+
throw configurationError('reportKey is required to identify the mail attempt.');
|
|
962
684
|
}
|
|
963
685
|
const fetchImpl=options.fetchImpl??globalThis.fetch;
|
|
964
686
|
if(!is.function(fetchImpl)){
|
|
@@ -973,26 +695,22 @@ function normalizeDirectSendOptions(options){
|
|
|
973
695
|
return {
|
|
974
696
|
allowAnyRecipient:true,
|
|
975
697
|
allowedRecipients:null,
|
|
976
|
-
apiKey:
|
|
977
|
-
appId:
|
|
698
|
+
apiKey:options.apiKey,
|
|
699
|
+
appId:options.appId,
|
|
978
700
|
errorRecipients:[],
|
|
979
701
|
fetchImpl,
|
|
980
|
-
from:
|
|
702
|
+
from:options.from,
|
|
981
703
|
providerTimeoutMs:optionalTimeoutMs(options.providerTimeoutMs,'providerTimeoutMs'),
|
|
982
704
|
requestIdFactory:options.requestIdFactory??randomUUID,
|
|
983
|
-
retryableDelayMs:
|
|
984
|
-
|
|
985
|
-
1_000,
|
|
986
|
-
{label:'retryableDelayMs'}
|
|
987
|
-
),
|
|
988
|
-
signal:validateSignal(options.signal),
|
|
705
|
+
retryableDelayMs:readRetryDelayMs(options.retryableDelayMs),
|
|
706
|
+
signal:options.signal,
|
|
989
707
|
report:options.report,
|
|
990
708
|
reportKey:options.reportKey,
|
|
991
709
|
onEvent:options.onEvent
|
|
992
710
|
};
|
|
993
711
|
}
|
|
994
712
|
|
|
995
|
-
function
|
|
713
|
+
function createDirectSendResult(result,{delivery,requestId}){
|
|
996
714
|
const common={
|
|
997
715
|
...result,
|
|
998
716
|
provider:'resend',
|
|
@@ -1003,20 +721,16 @@ function directSendResult(result,{delivery,requestId}){
|
|
|
1003
721
|
?result.fault.retryable?'retryable':'permanent'
|
|
1004
722
|
:result.kind,
|
|
1005
723
|
requestId,
|
|
1006
|
-
|
|
1007
|
-
providerRequest:JSON.parse(delivery.providerBody),
|
|
724
|
+
providerRequest:JSON.parse(delivery.serializedProviderRequest),
|
|
1008
725
|
report:delivery.report,
|
|
1009
726
|
recipientCount:delivery.recipientCount
|
|
1010
727
|
};
|
|
1011
728
|
if(result.kind==='accepted'){
|
|
1012
|
-
return
|
|
729
|
+
return common;
|
|
1013
730
|
}
|
|
1014
731
|
if(result.kind==='ambiguous'){
|
|
1015
732
|
return {
|
|
1016
733
|
...common,
|
|
1017
|
-
code:result.code,
|
|
1018
|
-
details:result.details,
|
|
1019
|
-
...(result.retryAfterMs?{retryAfterMs:result.retryAfterMs}:{}),
|
|
1020
734
|
retryable:true,
|
|
1021
735
|
uncertain:true
|
|
1022
736
|
};
|
|
@@ -1033,8 +747,7 @@ function directSendResult(result,{delivery,requestId}){
|
|
|
1033
747
|
}
|
|
1034
748
|
|
|
1035
749
|
export async function sendResendMail(options={}){
|
|
1036
|
-
const configuration=
|
|
1037
|
-
const delivery=normalizeReport(configuration.report,configuration);
|
|
750
|
+
const configuration=resolveDirectSendConfiguration(options);
|
|
1038
751
|
if(configuration.signal?.aborted){
|
|
1039
752
|
const error=new Error('Mail send cancelled before provider attempt.',{
|
|
1040
753
|
cause:configuration.signal.reason
|
|
@@ -1042,29 +755,30 @@ export async function sendResendMail(options={}){
|
|
|
1042
755
|
error.code='ARCANE_CANCELLED';
|
|
1043
756
|
throw error;
|
|
1044
757
|
}
|
|
758
|
+
const delivery=prepareProviderDelivery(configuration.report,configuration);
|
|
1045
759
|
const requestId=createRequestId(configuration.requestIdFactory);
|
|
1046
|
-
const observer=
|
|
760
|
+
const observer=configuration.onEvent?createMailEventObserver(configuration.onEvent):null;
|
|
1047
761
|
try{
|
|
1048
|
-
const result=await
|
|
762
|
+
const result=await attemptResendDelivery(
|
|
1049
763
|
configuration,
|
|
1050
764
|
delivery,
|
|
1051
765
|
configuration.reportKey,
|
|
1052
766
|
configuration.signal,
|
|
1053
767
|
requestId,
|
|
1054
|
-
observer
|
|
768
|
+
observer?.observe
|
|
1055
769
|
);
|
|
1056
|
-
return
|
|
770
|
+
return createDirectSendResult(result,{
|
|
1057
771
|
delivery,
|
|
1058
772
|
requestId
|
|
1059
773
|
});
|
|
1060
774
|
}finally{
|
|
1061
|
-
await observer.drain();
|
|
775
|
+
if(observer)await observer.drain();
|
|
1062
776
|
}
|
|
1063
777
|
}
|
|
1064
778
|
|
|
1065
|
-
function
|
|
779
|
+
function writeProviderDeliveryResponse(response,result,{origin,requestId,recipientCount}){
|
|
1066
780
|
if(result.kind==='accepted'){
|
|
1067
|
-
return
|
|
781
|
+
return sendJsonResponse(response,202,{
|
|
1068
782
|
requestId,
|
|
1069
783
|
status:'accepted',
|
|
1070
784
|
accepted:recipientCount,
|
|
@@ -1074,7 +788,7 @@ function sendProviderResult(response,result,{origin,requestId,recipientCount}){
|
|
|
1074
788
|
},{origin});
|
|
1075
789
|
}
|
|
1076
790
|
if(result.kind==='ambiguous'){
|
|
1077
|
-
return
|
|
791
|
+
return sendJsonResponse(response,207,{
|
|
1078
792
|
requestId,
|
|
1079
793
|
status:'delivery_uncertain',
|
|
1080
794
|
accepted:0,
|
|
@@ -1083,120 +797,121 @@ function sendProviderResult(response,result,{origin,requestId,recipientCount}){
|
|
|
1083
797
|
...(result.retryAfterMs?{retryAfterMs:result.retryAfterMs}:{})
|
|
1084
798
|
},{origin,retryAfterMs:result.retryAfterMs});
|
|
1085
799
|
}
|
|
1086
|
-
return
|
|
800
|
+
return sendMailFailureResponse(response,requestId,result.fault,origin);
|
|
1087
801
|
}
|
|
1088
802
|
|
|
1089
803
|
export function createResendMailRequestHandler(options={}){
|
|
1090
|
-
|
|
804
|
+
return createConfiguredMailHandler(resolveMailServerConfiguration(options));
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
function createConfiguredMailHandler(configuration){
|
|
1091
808
|
const ownerController=new AbortController();
|
|
1092
809
|
const activeRequests=new Set();
|
|
1093
|
-
const observer=
|
|
810
|
+
const observer=configuration.onEvent?createMailEventObserver(configuration.onEvent):null;
|
|
1094
811
|
let closePromise=null;
|
|
1095
812
|
|
|
1096
|
-
function
|
|
813
|
+
function abortHandlerFromOwner(){
|
|
1097
814
|
ownerController.abort(configuration.signal?.reason??new Error('Mail server cancelled.'));
|
|
1098
815
|
}
|
|
1099
|
-
configuration.signal?.addEventListener('abort',
|
|
816
|
+
configuration.signal?.addEventListener('abort',abortHandlerFromOwner,{once:true});
|
|
1100
817
|
if(configuration.signal?.aborted){
|
|
1101
|
-
|
|
818
|
+
abortHandlerFromOwner();
|
|
1102
819
|
}
|
|
1103
820
|
|
|
1104
|
-
async function
|
|
821
|
+
async function handleMailRequest(request,response){
|
|
1105
822
|
const requestId=createRequestId(configuration.requestIdFactory);
|
|
1106
|
-
|
|
823
|
+
let appId=request.headers['x-mail-app']??configuration.appId;
|
|
824
|
+
const startedAt=configuration.onEvent?Date.now():0;
|
|
1107
825
|
const requestController=new AbortController();
|
|
1108
826
|
let delivery=null;
|
|
1109
827
|
let idempotencyKey=null;
|
|
1110
828
|
let origin='';
|
|
1111
829
|
let providerAttempted=false;
|
|
1112
830
|
let result=null;
|
|
1113
|
-
let serialized=null;
|
|
1114
831
|
|
|
1115
|
-
function
|
|
832
|
+
function abortRequestFromHandler(){
|
|
1116
833
|
requestController.abort(ownerController.signal.reason);
|
|
1117
834
|
}
|
|
1118
|
-
function
|
|
835
|
+
function abortDisconnectedRequest(){
|
|
1119
836
|
requestController.abort(new Error('Mail client disconnected.'));
|
|
1120
837
|
}
|
|
1121
|
-
function
|
|
838
|
+
function abortRequestOnPrematureResponseClose(){
|
|
1122
839
|
if(!response.writableEnded){
|
|
1123
|
-
|
|
840
|
+
abortDisconnectedRequest();
|
|
1124
841
|
}
|
|
1125
842
|
}
|
|
1126
|
-
function absorbResponseError(){
|
|
1127
|
-
abortFromRequest();
|
|
1128
|
-
}
|
|
1129
843
|
function releaseRequestListeners(){
|
|
1130
|
-
request.removeListener('aborted',
|
|
1131
|
-
request.removeListener('error',
|
|
844
|
+
request.removeListener('aborted',abortDisconnectedRequest);
|
|
845
|
+
request.removeListener('error',abortDisconnectedRequest);
|
|
1132
846
|
}
|
|
1133
847
|
function releaseResponseListeners(){
|
|
1134
|
-
response.removeListener('close',
|
|
1135
|
-
response.removeListener('error',
|
|
848
|
+
response.removeListener('close',abortRequestOnPrematureResponseClose);
|
|
849
|
+
response.removeListener('error',abortDisconnectedRequest);
|
|
1136
850
|
}
|
|
1137
851
|
|
|
1138
|
-
ownerController.signal.addEventListener('abort',
|
|
1139
|
-
request.once('aborted',
|
|
1140
|
-
request.once('error',
|
|
1141
|
-
response.once('close',
|
|
1142
|
-
response.once('error',
|
|
852
|
+
ownerController.signal.addEventListener('abort',abortRequestFromHandler,{once:true});
|
|
853
|
+
request.once('aborted',abortDisconnectedRequest);
|
|
854
|
+
request.once('error',abortDisconnectedRequest);
|
|
855
|
+
response.once('close',abortRequestOnPrematureResponseClose);
|
|
856
|
+
response.once('error',abortDisconnectedRequest);
|
|
1143
857
|
if(ownerController.signal.aborted){
|
|
1144
|
-
|
|
858
|
+
abortRequestFromHandler();
|
|
1145
859
|
}
|
|
1146
|
-
observer.observe({
|
|
860
|
+
if(configuration.onEvent)observer.observe({
|
|
1147
861
|
type:'mail.request.received',
|
|
1148
|
-
appId
|
|
862
|
+
appId,
|
|
1149
863
|
requestId
|
|
1150
864
|
});
|
|
1151
865
|
|
|
1152
866
|
try{
|
|
1153
|
-
|
|
1154
|
-
origin=allowedOrigin(request,configuration);
|
|
1155
|
-
if(request.url!==RESEND_MAIL_PATH){
|
|
867
|
+
if(request.url!==RESEND_MAIL_PATH&&!request.url?.startsWith(`${RESEND_MAIL_PATH}?`)){
|
|
1156
868
|
throw new MailGatewayFault('mail_route_not_found',{statusCode:404});
|
|
1157
869
|
}
|
|
870
|
+
const requestOrigin=request.headers.origin;
|
|
871
|
+
if(requestOrigin){
|
|
872
|
+
const originAllowed=configuration.allowedOrigins.size>0
|
|
873
|
+
?configuration.allowedOrigins.has(requestOrigin)
|
|
874
|
+
:requestOrigin===`https://${request.headers.host}`
|
|
875
|
+
||requestOrigin===`http://${request.headers.host}`;
|
|
876
|
+
if(!originAllowed){
|
|
877
|
+
throw new MailGatewayFault('mail_origin_not_allowed',{statusCode:403});
|
|
878
|
+
}
|
|
879
|
+
origin=requestOrigin;
|
|
880
|
+
}
|
|
1158
881
|
if(request.method==='OPTIONS'){
|
|
1159
|
-
|
|
1160
|
-
writePreflight(response,origin,preflight);
|
|
882
|
+
sendCorsPreflightResponse(response,origin);
|
|
1161
883
|
return;
|
|
1162
884
|
}
|
|
1163
885
|
if(request.method!=='POST'){
|
|
1164
886
|
throw new MailGatewayFault('mail_method_not_allowed',{statusCode:405});
|
|
1165
887
|
}
|
|
1166
|
-
if(
|
|
1167
|
-
|
|
1168
|
-
}
|
|
1169
|
-
authenticateLocalCaller(request,configuration);
|
|
1170
|
-
idempotencyKey=singleHeader(request,'idempotency-key');
|
|
1171
|
-
if(!IDEMPOTENCY_KEY_PATTERN.test(idempotencyKey)){
|
|
1172
|
-
throw new MailGatewayFault('invalid_idempotency_key',{statusCode:400});
|
|
1173
|
-
}
|
|
1174
|
-
const contentType=singleHeader(request,'content-type');
|
|
1175
|
-
if(!JSON_CONTENT_TYPE_PATTERN.test(contentType)){
|
|
1176
|
-
throw new MailGatewayFault('mail_unsupported_content_type',{statusCode:415});
|
|
888
|
+
if(configuration.verifySubscription){
|
|
889
|
+
appId=await verifyMailSubscription(request,configuration,requestController.signal);
|
|
1177
890
|
}
|
|
1178
|
-
|
|
891
|
+
idempotencyKey=requireRequestHeader(request,'idempotency-key');
|
|
892
|
+
const requestText=await readRequestBodyText(request,{
|
|
1179
893
|
signal:requestController.signal,
|
|
1180
894
|
timeoutMs:configuration.bodyTimeoutMs
|
|
1181
895
|
});
|
|
1182
|
-
delivery=
|
|
896
|
+
delivery=parseMailRequest(requestText,configuration);
|
|
1183
897
|
providerAttempted=true;
|
|
1184
|
-
result=await
|
|
898
|
+
result=await attemptResendDelivery(
|
|
1185
899
|
configuration,
|
|
1186
900
|
delivery,
|
|
1187
901
|
idempotencyKey,
|
|
1188
902
|
requestController.signal,
|
|
1189
903
|
requestId,
|
|
1190
|
-
observer
|
|
904
|
+
observer?.observe,
|
|
905
|
+
appId
|
|
1191
906
|
);
|
|
1192
|
-
|
|
907
|
+
writeProviderDeliveryResponse(response,result,{
|
|
1193
908
|
origin,
|
|
1194
909
|
recipientCount:delivery.recipientCount,
|
|
1195
910
|
requestId
|
|
1196
911
|
});
|
|
1197
|
-
observer.observe({
|
|
912
|
+
if(configuration.onEvent)observer.observe({
|
|
1198
913
|
type:'mail.request.completed',
|
|
1199
|
-
appId
|
|
914
|
+
appId,
|
|
1200
915
|
classification:result.kind,
|
|
1201
916
|
delivery,
|
|
1202
917
|
durationMs:Math.max(0,Date.now()-startedAt),
|
|
@@ -1206,14 +921,14 @@ export function createResendMailRequestHandler(options={}){
|
|
|
1206
921
|
requestId
|
|
1207
922
|
});
|
|
1208
923
|
}catch(error){
|
|
1209
|
-
const fault=
|
|
1210
|
-
|
|
924
|
+
const fault=mailFaultFromError(error);
|
|
925
|
+
sendMailFailureResponse(response,requestId,fault,origin);
|
|
1211
926
|
if(!request.readableEnded&&!request.destroyed){
|
|
1212
927
|
request.resume();
|
|
1213
928
|
}
|
|
1214
|
-
observer.observe({
|
|
929
|
+
if(configuration.onEvent)observer.observe({
|
|
1215
930
|
type:'mail.request.completed',
|
|
1216
|
-
appId
|
|
931
|
+
appId,
|
|
1217
932
|
classification:fault.uncertain?'ambiguous':fault.retryable?'retryable':'permanent',
|
|
1218
933
|
delivery,
|
|
1219
934
|
durationMs:Math.max(0,Date.now()-startedAt),
|
|
@@ -1231,7 +946,7 @@ export function createResendMailRequestHandler(options={}){
|
|
|
1231
946
|
requestId
|
|
1232
947
|
});
|
|
1233
948
|
}finally{
|
|
1234
|
-
ownerController.signal.removeEventListener('abort',
|
|
949
|
+
ownerController.signal.removeEventListener('abort',abortRequestFromHandler);
|
|
1235
950
|
if(request.readableEnded||request.destroyed){
|
|
1236
951
|
releaseRequestListeners();
|
|
1237
952
|
}else{
|
|
@@ -1245,30 +960,31 @@ export function createResendMailRequestHandler(options={}){
|
|
|
1245
960
|
}
|
|
1246
961
|
}
|
|
1247
962
|
|
|
1248
|
-
function
|
|
1249
|
-
const operation=
|
|
963
|
+
function dispatchMailRequest(request,response){
|
|
964
|
+
const operation=handleMailRequest(request,response);
|
|
1250
965
|
activeRequests.add(operation);
|
|
1251
|
-
operation.catch(function
|
|
966
|
+
operation.catch(function closeResponseAfterHandlerFailure(error){
|
|
967
|
+
reportMailError('Mail request handler failed.',error);
|
|
1252
968
|
if(!response.destroyed){
|
|
1253
|
-
response.destroy();
|
|
969
|
+
response.destroy(error);
|
|
1254
970
|
}
|
|
1255
971
|
}).finally(function releaseActiveRequest(){
|
|
1256
972
|
activeRequests.delete(operation);
|
|
1257
973
|
});
|
|
1258
974
|
}
|
|
1259
975
|
|
|
1260
|
-
async function
|
|
1261
|
-
configuration.signal?.removeEventListener('abort',
|
|
976
|
+
async function closeMailRequestHandler(){
|
|
977
|
+
configuration.signal?.removeEventListener('abort',abortHandlerFromOwner);
|
|
1262
978
|
if(!ownerController.signal.aborted){
|
|
1263
979
|
ownerController.abort(new Error('Mail server closed.'));
|
|
1264
980
|
}
|
|
1265
981
|
await Promise.allSettled([...activeRequests]);
|
|
1266
|
-
await observer.drain();
|
|
982
|
+
if(observer)await observer.drain();
|
|
1267
983
|
}
|
|
1268
984
|
|
|
1269
985
|
function close(){
|
|
1270
986
|
if(!closePromise){
|
|
1271
|
-
closePromise=
|
|
987
|
+
closePromise=closeMailRequestHandler();
|
|
1272
988
|
}
|
|
1273
989
|
return closePromise;
|
|
1274
990
|
}
|
|
@@ -1277,13 +993,13 @@ export function createResendMailRequestHandler(options={}){
|
|
|
1277
993
|
appId:configuration.appId,
|
|
1278
994
|
callerAuthentication:configuration.callerAuthentication,
|
|
1279
995
|
close,
|
|
1280
|
-
handle,
|
|
996
|
+
handle:dispatchMailRequest,
|
|
1281
997
|
path:RESEND_MAIL_PATH,
|
|
1282
998
|
protocol:RESEND_MAIL_SERVER_PROTOCOL
|
|
1283
999
|
};
|
|
1284
1000
|
}
|
|
1285
1001
|
|
|
1286
|
-
function
|
|
1002
|
+
function listenForMailRequests(mailServer){
|
|
1287
1003
|
return new Promise(function waitForMailListener(resolve,reject){
|
|
1288
1004
|
function onError(error){
|
|
1289
1005
|
reject(error);
|
|
@@ -1297,7 +1013,7 @@ function deployMailServer(mailServer){
|
|
|
1297
1013
|
}
|
|
1298
1014
|
|
|
1299
1015
|
export async function startResendMailServer(options={}){
|
|
1300
|
-
const configuration=
|
|
1016
|
+
const configuration=resolveMailServerConfiguration(options);
|
|
1301
1017
|
if(configuration.signal?.aborted){
|
|
1302
1018
|
throw configuration.signal.reason??new Error('Mail server start was cancelled.');
|
|
1303
1019
|
}
|
|
@@ -1306,23 +1022,23 @@ export async function startResendMailServer(options={}){
|
|
|
1306
1022
|
port:configuration.port,
|
|
1307
1023
|
server:{timeout:0}
|
|
1308
1024
|
});
|
|
1309
|
-
const requestHandler=
|
|
1310
|
-
mailServer.onRawRequest=function
|
|
1025
|
+
const requestHandler=createConfiguredMailHandler(configuration);
|
|
1026
|
+
mailServer.onRawRequest=function routeRawMailRequest(request,response){
|
|
1311
1027
|
requestHandler.handle(request,response);
|
|
1312
1028
|
return true;
|
|
1313
1029
|
};
|
|
1314
1030
|
|
|
1315
1031
|
let server;
|
|
1316
1032
|
try{
|
|
1317
|
-
server=await
|
|
1033
|
+
server=await listenForMailRequests(mailServer);
|
|
1318
1034
|
}catch(error){
|
|
1319
1035
|
await Promise.allSettled([mailServer.close(),requestHandler.close()]);
|
|
1320
1036
|
throw error;
|
|
1321
1037
|
}
|
|
1322
1038
|
const address=server.address();
|
|
1323
|
-
if(!address||is.string(address)
|
|
1039
|
+
if(!address||is.string(address)){
|
|
1324
1040
|
await Promise.allSettled([mailServer.close(),requestHandler.close()]);
|
|
1325
|
-
throw configurationError('Mail server
|
|
1041
|
+
throw configurationError('Mail server has no TCP listener address.');
|
|
1326
1042
|
}
|
|
1327
1043
|
const displayHost=address.address.includes(':')?`[${address.address}]`:address.address;
|
|
1328
1044
|
const origin=`http://${displayHost}:${String(address.port)}`;
|
|
@@ -1335,8 +1051,8 @@ export async function startResendMailServer(options={}){
|
|
|
1335
1051
|
});
|
|
1336
1052
|
lifecycle.catch(function observeMailLifecycleFailure(){});
|
|
1337
1053
|
|
|
1338
|
-
async function
|
|
1339
|
-
configuration.signal?.removeEventListener('abort',
|
|
1054
|
+
async function closeMailServer(){
|
|
1055
|
+
configuration.signal?.removeEventListener('abort',closeServerOnAbort);
|
|
1340
1056
|
const handlerClosing=requestHandler.close();
|
|
1341
1057
|
try{
|
|
1342
1058
|
await Promise.all([mailServer.close(),handlerClosing]);
|
|
@@ -1349,21 +1065,21 @@ export async function startResendMailServer(options={}){
|
|
|
1349
1065
|
|
|
1350
1066
|
function close(){
|
|
1351
1067
|
if(!closePromise){
|
|
1352
|
-
closePromise=
|
|
1068
|
+
closePromise=closeMailServer();
|
|
1353
1069
|
}
|
|
1354
1070
|
return closePromise;
|
|
1355
1071
|
}
|
|
1356
1072
|
|
|
1357
|
-
function
|
|
1073
|
+
function closeServerOnAbort(){
|
|
1358
1074
|
close().catch(function ignoreSignalCloseFailure(){});
|
|
1359
1075
|
}
|
|
1360
1076
|
|
|
1361
|
-
function
|
|
1077
|
+
function closeServerAfterError(error){
|
|
1362
1078
|
rejectLifecycle(error);
|
|
1363
1079
|
close().catch(function observeOperationalCloseFailure(){});
|
|
1364
1080
|
}
|
|
1365
1081
|
|
|
1366
|
-
server.once('close',function
|
|
1082
|
+
server.once('close',function finishMailServerAfterExternalClose(){
|
|
1367
1083
|
if(!closePromise){
|
|
1368
1084
|
closePromise=requestHandler.close().then(
|
|
1369
1085
|
function resolveExternalClose(){resolveLifecycle();},
|
|
@@ -1372,10 +1088,10 @@ export async function startResendMailServer(options={}){
|
|
|
1372
1088
|
closePromise.catch(function observeExternalCloseFailure(){});
|
|
1373
1089
|
}
|
|
1374
1090
|
});
|
|
1375
|
-
server.on('error',
|
|
1376
|
-
configuration.signal?.addEventListener('abort',
|
|
1091
|
+
server.on('error',closeServerAfterError);
|
|
1092
|
+
configuration.signal?.addEventListener('abort',closeServerOnAbort,{once:true});
|
|
1377
1093
|
if(configuration.signal?.aborted){
|
|
1378
|
-
|
|
1094
|
+
closeServerOnAbort();
|
|
1379
1095
|
}
|
|
1380
1096
|
|
|
1381
1097
|
return {
|