@vrplatform/email 1.0.2-stage.513 → 1.0.2-stage.557
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/build/main/utils/find-postmark-email.d.ts +8 -0
- package/build/main/utils/find-postmark-email.js +66 -0
- package/build/main/utils/find-postmark-email.js.map +1 -0
- package/build/main/utils/index.d.ts +2 -0
- package/build/main/utils/index.js +2 -0
- package/build/main/utils/index.js.map +1 -1
- package/build/main/utils/send-email.d.ts +7 -2
- package/build/main/utils/send-email.js +40 -9
- package/build/main/utils/send-email.js.map +1 -1
- package/build/module/utils/find-postmark-email.d.ts +8 -0
- package/build/module/utils/find-postmark-email.js +63 -0
- package/build/module/utils/find-postmark-email.js.map +1 -0
- package/build/module/utils/index.d.ts +2 -0
- package/build/module/utils/index.js +2 -0
- package/build/module/utils/index.js.map +1 -1
- package/build/module/utils/send-email.d.ts +7 -2
- package/build/module/utils/send-email.js +41 -9
- package/build/module/utils/send-email.js.map +1 -1
- package/package.json +1 -1
- package/src/utils/find-postmark-email.ts +93 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/send-email.ts +70 -9
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare function findPostmarkEmail(deliveryId: string, options: {
|
|
2
|
+
fetch?: (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
3
|
+
postmarkToken?: string;
|
|
4
|
+
}): Promise<{
|
|
5
|
+
httpStatus: number;
|
|
6
|
+
messageId: any;
|
|
7
|
+
submittedAt: any;
|
|
8
|
+
} | undefined>;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.findPostmarkEmail = findPostmarkEmail;
|
|
4
|
+
const utils_1 = require("@vrplatform/utils");
|
|
5
|
+
const send_email_1 = require("./send-email");
|
|
6
|
+
async function findPostmarkEmail(deliveryId, options) {
|
|
7
|
+
if (!options.postmarkToken) {
|
|
8
|
+
throw new send_email_1.EmailDeliveryError('Postmark is not configured', undefined, false, false);
|
|
9
|
+
}
|
|
10
|
+
const url = new URL('https://api.postmarkapp.com/messages/outbound');
|
|
11
|
+
url.searchParams.set('count', '1');
|
|
12
|
+
url.searchParams.set('offset', '0');
|
|
13
|
+
url.searchParams.set('messagestream', 'outbound');
|
|
14
|
+
url.searchParams.set('metadata_deliveryId', deliveryId);
|
|
15
|
+
const response = await (options.fetch ?? fetch)(url, {
|
|
16
|
+
method: 'GET',
|
|
17
|
+
headers: {
|
|
18
|
+
Accept: 'application/json',
|
|
19
|
+
'X-Postmark-Server-Token': options.postmarkToken,
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
const providerResponse = await response.json().catch(() => ({}));
|
|
23
|
+
if (!response.ok) {
|
|
24
|
+
throw new send_email_1.EmailDeliveryError('Postmark message lookup failed', {
|
|
25
|
+
provider: 'postmark',
|
|
26
|
+
providerCategory: 'rejected',
|
|
27
|
+
httpStatus: response.status,
|
|
28
|
+
}, false, response.status === 429 || response.status >= 500);
|
|
29
|
+
}
|
|
30
|
+
if (!providerResponse ||
|
|
31
|
+
typeof providerResponse !== 'object' ||
|
|
32
|
+
!('TotalCount' in providerResponse) ||
|
|
33
|
+
typeof providerResponse.TotalCount !== 'number' ||
|
|
34
|
+
!('Messages' in providerResponse) ||
|
|
35
|
+
!Array.isArray(providerResponse.Messages)) {
|
|
36
|
+
throw new send_email_1.EmailDeliveryError('Postmark returned an invalid response');
|
|
37
|
+
}
|
|
38
|
+
if (providerResponse.TotalCount === 0)
|
|
39
|
+
return undefined;
|
|
40
|
+
const message = providerResponse.Messages.find((value) => value &&
|
|
41
|
+
typeof value === 'object' &&
|
|
42
|
+
'Metadata' in value &&
|
|
43
|
+
value.Metadata &&
|
|
44
|
+
typeof value.Metadata === 'object' &&
|
|
45
|
+
'deliveryId' in value.Metadata &&
|
|
46
|
+
value.Metadata.deliveryId === deliveryId);
|
|
47
|
+
const messageId = message &&
|
|
48
|
+
typeof message === 'object' &&
|
|
49
|
+
'MessageID' in message &&
|
|
50
|
+
typeof message.MessageID === 'string' &&
|
|
51
|
+
message.MessageID
|
|
52
|
+
? message.MessageID
|
|
53
|
+
: undefined;
|
|
54
|
+
const submittedAt = message &&
|
|
55
|
+
typeof message === 'object' &&
|
|
56
|
+
'ReceivedAt' in message &&
|
|
57
|
+
typeof message.ReceivedAt === 'string' &&
|
|
58
|
+
(0, utils_1.day)(message.ReceivedAt).isValid()
|
|
59
|
+
? message.ReceivedAt
|
|
60
|
+
: undefined;
|
|
61
|
+
if (!messageId || !submittedAt) {
|
|
62
|
+
throw new send_email_1.EmailDeliveryError('Postmark returned an invalid response');
|
|
63
|
+
}
|
|
64
|
+
return { httpStatus: response.status, messageId, submittedAt };
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=find-postmark-email.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"find-postmark-email.js","sourceRoot":"src/","sources":["utils/find-postmark-email.ts"],"names":[],"mappings":"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","sourcesContent":["import { day } from '@vrplatform/utils';\nimport { EmailDeliveryError } from './send-email';\n\nexport async function findPostmarkEmail(\n deliveryId: string,\n options: {\n fetch?: (\n input: string | URL | Request,\n init?: RequestInit\n ) => Promise<Response>;\n postmarkToken?: string;\n }\n) {\n if (!options.postmarkToken) {\n throw new EmailDeliveryError(\n 'Postmark is not configured',\n undefined,\n false,\n false\n );\n }\n\n const url = new URL('https://api.postmarkapp.com/messages/outbound');\n url.searchParams.set('count', '1');\n url.searchParams.set('offset', '0');\n url.searchParams.set('messagestream', 'outbound');\n url.searchParams.set('metadata_deliveryId', deliveryId);\n const response = await (options.fetch ?? fetch)(url, {\n method: 'GET',\n headers: {\n Accept: 'application/json',\n 'X-Postmark-Server-Token': options.postmarkToken,\n },\n });\n const providerResponse: unknown = await response.json().catch(() => ({}));\n\n if (!response.ok) {\n throw new EmailDeliveryError(\n 'Postmark message lookup failed',\n {\n provider: 'postmark',\n providerCategory: 'rejected',\n httpStatus: response.status,\n },\n false,\n response.status === 429 || response.status >= 500\n );\n }\n\n if (\n !providerResponse ||\n typeof providerResponse !== 'object' ||\n !('TotalCount' in providerResponse) ||\n typeof providerResponse.TotalCount !== 'number' ||\n !('Messages' in providerResponse) ||\n !Array.isArray(providerResponse.Messages)\n ) {\n throw new EmailDeliveryError('Postmark returned an invalid response');\n }\n if (providerResponse.TotalCount === 0) return undefined;\n\n const message = providerResponse.Messages.find(\n (value) =>\n value &&\n typeof value === 'object' &&\n 'Metadata' in value &&\n value.Metadata &&\n typeof value.Metadata === 'object' &&\n 'deliveryId' in value.Metadata &&\n value.Metadata.deliveryId === deliveryId\n );\n const messageId =\n message &&\n typeof message === 'object' &&\n 'MessageID' in message &&\n typeof message.MessageID === 'string' &&\n message.MessageID\n ? message.MessageID\n : undefined;\n const submittedAt =\n message &&\n typeof message === 'object' &&\n 'ReceivedAt' in message &&\n typeof message.ReceivedAt === 'string' &&\n day(message.ReceivedAt).isValid()\n ? message.ReceivedAt\n : undefined;\n if (!messageId || !submittedAt) {\n throw new EmailDeliveryError('Postmark returned an invalid response');\n }\n\n return { httpStatus: response.status, messageId, submittedAt };\n}\n"]}
|
|
@@ -14,5 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./find-postmark-email"), exports);
|
|
18
|
+
__exportStar(require("./find-postmark-email"), exports);
|
|
17
19
|
__exportStar(require("./send-email"), exports);
|
|
18
20
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"src/","sources":["utils/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,+CAA6B","sourcesContent":["export * from './send-email';\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"src/","sources":["utils/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,wDAAsC;AACtC,wDAAsC;AACtC,+CAA6B","sourcesContent":["export * from './find-postmark-email';\nexport * from './find-postmark-email';\nexport * from './send-email';\n"]}
|
|
@@ -11,7 +11,8 @@ type EmailDeliveryErrorDetails = EmailDeliveryFailureDetails | {
|
|
|
11
11
|
export declare class EmailDeliveryError extends Error {
|
|
12
12
|
readonly details?: EmailDeliveryErrorDetails | undefined;
|
|
13
13
|
readonly expected: boolean;
|
|
14
|
-
|
|
14
|
+
readonly retryable: boolean;
|
|
15
|
+
constructor(message: string, details?: EmailDeliveryErrorDetails | undefined, expected?: boolean, retryable?: boolean);
|
|
15
16
|
}
|
|
16
17
|
export declare function sendEmail(data: {
|
|
17
18
|
to: string;
|
|
@@ -19,11 +20,15 @@ export declare function sendEmail(data: {
|
|
|
19
20
|
text: string;
|
|
20
21
|
html: string;
|
|
21
22
|
replyTo?: string;
|
|
23
|
+
tag?: string;
|
|
24
|
+
metadata?: Record<string, string>;
|
|
22
25
|
}, options: {
|
|
23
26
|
fetch?: (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
24
27
|
postmarkToken?: string;
|
|
25
28
|
}): Promise<{
|
|
26
|
-
|
|
29
|
+
httpStatus: number;
|
|
30
|
+
messageId: string;
|
|
31
|
+
submittedAt: string;
|
|
27
32
|
}>;
|
|
28
33
|
export declare function settleEmailDeliveries(deliveries: Promise<unknown>[]): Promise<void>;
|
|
29
34
|
export {};
|
|
@@ -3,18 +3,20 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.EmailDeliveryError = void 0;
|
|
4
4
|
exports.sendEmail = sendEmail;
|
|
5
5
|
exports.settleEmailDeliveries = settleEmailDeliveries;
|
|
6
|
+
const utils_1 = require("@vrplatform/utils");
|
|
6
7
|
class EmailDeliveryError extends Error {
|
|
7
|
-
constructor(message, details, expected = false) {
|
|
8
|
+
constructor(message, details, expected = false, retryable = true) {
|
|
8
9
|
super(message);
|
|
9
10
|
this.details = details;
|
|
10
11
|
this.expected = expected;
|
|
12
|
+
this.retryable = retryable;
|
|
11
13
|
this.name = 'EmailDeliveryError';
|
|
12
14
|
}
|
|
13
15
|
}
|
|
14
16
|
exports.EmailDeliveryError = EmailDeliveryError;
|
|
15
17
|
async function sendEmail(data, options) {
|
|
16
18
|
if (!options.postmarkToken) {
|
|
17
|
-
throw new EmailDeliveryError('Postmark is not configured');
|
|
19
|
+
throw new EmailDeliveryError('Postmark is not configured', undefined, false, false);
|
|
18
20
|
}
|
|
19
21
|
const response = await (options.fetch ?? fetch)('https://api.postmarkapp.com/email', {
|
|
20
22
|
method: 'POST',
|
|
@@ -31,6 +33,8 @@ async function sendEmail(data, options) {
|
|
|
31
33
|
TextBody: data.text,
|
|
32
34
|
HtmlBody: data.html,
|
|
33
35
|
MessageStream: 'outbound',
|
|
36
|
+
Tag: data.tag,
|
|
37
|
+
Metadata: data.metadata,
|
|
34
38
|
}),
|
|
35
39
|
});
|
|
36
40
|
const providerResponse = await response.json().catch(() => ({}));
|
|
@@ -50,7 +54,8 @@ async function sendEmail(data, options) {
|
|
|
50
54
|
errorCode >= 0
|
|
51
55
|
? errorCode
|
|
52
56
|
: undefined;
|
|
53
|
-
if (!response.ok
|
|
57
|
+
if (!response.ok ||
|
|
58
|
+
(postmarkErrorCode !== undefined && postmarkErrorCode !== 0)) {
|
|
54
59
|
const inactiveRecipient = response.status === 422 && postmarkErrorCode === 406;
|
|
55
60
|
const details = {
|
|
56
61
|
provider: 'postmark',
|
|
@@ -59,11 +64,37 @@ async function sendEmail(data, options) {
|
|
|
59
64
|
...(postmarkErrorCode !== undefined ? { postmarkErrorCode } : {}),
|
|
60
65
|
};
|
|
61
66
|
if (inactiveRecipient) {
|
|
62
|
-
throw new EmailDeliveryError('Postmark recipient is inactive', details, true);
|
|
67
|
+
throw new EmailDeliveryError('Postmark recipient is inactive', details, true, false);
|
|
63
68
|
}
|
|
64
|
-
throw new EmailDeliveryError('Postmark rejected the email', details);
|
|
69
|
+
throw new EmailDeliveryError('Postmark rejected the email', details, false, response.status === 429 || response.status >= 500);
|
|
65
70
|
}
|
|
66
|
-
|
|
71
|
+
const messageId = providerResponse !== null &&
|
|
72
|
+
typeof providerResponse === 'object' &&
|
|
73
|
+
'MessageID' in providerResponse &&
|
|
74
|
+
typeof providerResponse.MessageID === 'string' &&
|
|
75
|
+
providerResponse.MessageID
|
|
76
|
+
? providerResponse.MessageID
|
|
77
|
+
: undefined;
|
|
78
|
+
const submittedAt = providerResponse !== null &&
|
|
79
|
+
typeof providerResponse === 'object' &&
|
|
80
|
+
'SubmittedAt' in providerResponse &&
|
|
81
|
+
typeof providerResponse.SubmittedAt === 'string' &&
|
|
82
|
+
(0, utils_1.day)(providerResponse.SubmittedAt).isValid()
|
|
83
|
+
? providerResponse.SubmittedAt
|
|
84
|
+
: undefined;
|
|
85
|
+
if (!messageId || !submittedAt || postmarkErrorCode !== 0) {
|
|
86
|
+
throw new EmailDeliveryError('Postmark returned an invalid response', {
|
|
87
|
+
provider: 'postmark',
|
|
88
|
+
providerCategory: 'rejected',
|
|
89
|
+
httpStatus: response.status,
|
|
90
|
+
...(postmarkErrorCode !== undefined ? { postmarkErrorCode } : {}),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
httpStatus: response.status,
|
|
95
|
+
messageId,
|
|
96
|
+
submittedAt,
|
|
97
|
+
};
|
|
67
98
|
}
|
|
68
99
|
async function settleEmailDeliveries(deliveries) {
|
|
69
100
|
const failures = (await Promise.allSettled(deliveries)).filter((result) => result.status === 'rejected');
|
|
@@ -94,12 +125,12 @@ async function settleEmailDeliveries(deliveries) {
|
|
|
94
125
|
};
|
|
95
126
|
if (unexpectedDeliveryError instanceof EmailDeliveryError) {
|
|
96
127
|
if (unexpectedDeliveryError.message === 'Postmark is not configured') {
|
|
97
|
-
throw new EmailDeliveryError('Postmark is not configured', details);
|
|
128
|
+
throw new EmailDeliveryError('Postmark is not configured', details, false, false);
|
|
98
129
|
}
|
|
99
|
-
throw new EmailDeliveryError('Postmark rejected the email', details);
|
|
130
|
+
throw new EmailDeliveryError('Postmark rejected the email', details, false, unexpectedDeliveryError.retryable);
|
|
100
131
|
}
|
|
101
132
|
if (allExpected && firstDeliveryError instanceof EmailDeliveryError) {
|
|
102
|
-
throw new EmailDeliveryError('Postmark recipient is inactive', details, true);
|
|
133
|
+
throw new EmailDeliveryError('Postmark recipient is inactive', details, true, false);
|
|
103
134
|
}
|
|
104
135
|
throw new EmailDeliveryError('Email delivery failed', details);
|
|
105
136
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"send-email.js","sourceRoot":"src/","sources":["utils/send-email.ts"],"names":[],"mappings":"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","sourcesContent":["type EmailDeliveryFailureDetails = {\n httpStatus: number;\n postmarkErrorCode?: number;\n provider: 'postmark';\n providerCategory: 'inactiveRecipient' | 'rejected';\n};\n\ntype EmailDeliveryErrorDetails =\n | EmailDeliveryFailureDetails\n | {\n failureCount: number;\n failures: EmailDeliveryFailureDetails[];\n };\n\nexport class EmailDeliveryError extends Error {\n constructor(\n message: string,\n public readonly details?: EmailDeliveryErrorDetails,\n public readonly expected = false\n ) {\n super(message);\n this.name = 'EmailDeliveryError';\n }\n}\n\nexport async function sendEmail(\n data: {\n to: string;\n subject: string;\n text: string;\n html: string;\n replyTo?: string;\n },\n options: {\n fetch?: (\n input: string | URL | Request,\n init?: RequestInit\n ) => Promise<Response>;\n postmarkToken?: string;\n }\n) {\n if (!options.postmarkToken) {\n throw new EmailDeliveryError('Postmark is not configured');\n }\n\n const response = await (options.fetch ?? fetch)(\n 'https://api.postmarkapp.com/email',\n {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n 'X-Postmark-Server-Token': options.postmarkToken,\n },\n body: JSON.stringify({\n From: 'VRPlatform <notifications@vrplatform.app>',\n To: data.to,\n Subject: data.subject,\n ReplyTo: data.replyTo,\n TextBody: data.text,\n HtmlBody: data.html,\n MessageStream: 'outbound',\n }),\n }\n );\n\n const providerResponse: unknown = await response.json().catch(() => ({}));\n const responseErrorCode =\n providerResponse !== null &&\n typeof providerResponse === 'object' &&\n 'ErrorCode' in providerResponse &&\n typeof providerResponse.ErrorCode === 'number'\n ? providerResponse.ErrorCode\n : undefined;\n const headerErrorCode = response.headers.get('X-PM-ApiErrorCode');\n const errorCode =\n responseErrorCode ??\n (headerErrorCode && /^\\d+$/.test(headerErrorCode)\n ? Number(headerErrorCode)\n : undefined);\n const postmarkErrorCode =\n typeof errorCode === 'number' &&\n Number.isSafeInteger(errorCode) &&\n errorCode >= 0\n ? errorCode\n : undefined;\n\n if (!response.ok) {\n const inactiveRecipient =\n response.status === 422 && postmarkErrorCode === 406;\n const details: EmailDeliveryFailureDetails = {\n provider: 'postmark',\n providerCategory: inactiveRecipient ? 'inactiveRecipient' : 'rejected',\n httpStatus: response.status,\n ...(postmarkErrorCode !== undefined ? { postmarkErrorCode } : {}),\n };\n\n if (inactiveRecipient) {\n throw new EmailDeliveryError(\n 'Postmark recipient is inactive',\n details,\n true\n );\n }\n\n throw new EmailDeliveryError('Postmark rejected the email', details);\n }\n\n return { status: response.status };\n}\n\nexport async function settleEmailDeliveries(deliveries: Promise<unknown>[]) {\n const failures = (await Promise.allSettled(deliveries)).filter(\n (result) => result.status === 'rejected'\n );\n if (failures.length === 0) return;\n\n const unexpectedDeliveryError = failures.find(\n (failure) =>\n failure.reason instanceof EmailDeliveryError && !failure.reason.expected\n )?.reason;\n const allExpected = failures.every(\n (failure) =>\n failure.reason instanceof EmailDeliveryError && failure.reason.expected\n );\n const firstDeliveryError = failures.find(\n (failure) => failure.reason instanceof EmailDeliveryError\n )?.reason;\n const failureDetails = failures.flatMap((failure) => {\n if (\n !(failure.reason instanceof EmailDeliveryError) ||\n !failure.reason.details ||\n !('provider' in failure.reason.details)\n ) {\n return [];\n }\n const { httpStatus, postmarkErrorCode, provider, providerCategory } =\n failure.reason.details;\n return [\n {\n httpStatus,\n provider,\n providerCategory,\n ...(postmarkErrorCode !== undefined ? { postmarkErrorCode } : {}),\n },\n ];\n });\n\n const details: EmailDeliveryErrorDetails = {\n failureCount: failures.length,\n failures: failureDetails,\n };\n if (unexpectedDeliveryError instanceof EmailDeliveryError) {\n if (unexpectedDeliveryError.message === 'Postmark is not configured') {\n throw new EmailDeliveryError('Postmark is not configured', details);\n }\n throw new EmailDeliveryError('Postmark rejected the email', details);\n }\n if (allExpected && firstDeliveryError instanceof EmailDeliveryError) {\n throw new EmailDeliveryError(\n 'Postmark recipient is inactive',\n details,\n true\n );\n }\n throw new EmailDeliveryError('Email delivery failed', details);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"send-email.js","sourceRoot":"src/","sources":["utils/send-email.ts"],"names":[],"mappings":"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","sourcesContent":["import { day } from '@vrplatform/utils';\n\ntype EmailDeliveryFailureDetails = {\n httpStatus: number;\n postmarkErrorCode?: number;\n provider: 'postmark';\n providerCategory: 'inactiveRecipient' | 'rejected';\n};\n\ntype EmailDeliveryErrorDetails =\n | EmailDeliveryFailureDetails\n | {\n failureCount: number;\n failures: EmailDeliveryFailureDetails[];\n };\n\nexport class EmailDeliveryError extends Error {\n constructor(\n message: string,\n public readonly details?: EmailDeliveryErrorDetails,\n public readonly expected = false,\n public readonly retryable = true\n ) {\n super(message);\n this.name = 'EmailDeliveryError';\n }\n}\n\nexport async function sendEmail(\n data: {\n to: string;\n subject: string;\n text: string;\n html: string;\n replyTo?: string;\n tag?: string;\n metadata?: Record<string, string>;\n },\n options: {\n fetch?: (\n input: string | URL | Request,\n init?: RequestInit\n ) => Promise<Response>;\n postmarkToken?: string;\n }\n) {\n if (!options.postmarkToken) {\n throw new EmailDeliveryError(\n 'Postmark is not configured',\n undefined,\n false,\n false\n );\n }\n\n const response = await (options.fetch ?? fetch)(\n 'https://api.postmarkapp.com/email',\n {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n 'X-Postmark-Server-Token': options.postmarkToken,\n },\n body: JSON.stringify({\n From: 'VRPlatform <notifications@vrplatform.app>',\n To: data.to,\n Subject: data.subject,\n ReplyTo: data.replyTo,\n TextBody: data.text,\n HtmlBody: data.html,\n MessageStream: 'outbound',\n Tag: data.tag,\n Metadata: data.metadata,\n }),\n }\n );\n\n const providerResponse: unknown = await response.json().catch(() => ({}));\n const responseErrorCode =\n providerResponse !== null &&\n typeof providerResponse === 'object' &&\n 'ErrorCode' in providerResponse &&\n typeof providerResponse.ErrorCode === 'number'\n ? providerResponse.ErrorCode\n : undefined;\n const headerErrorCode = response.headers.get('X-PM-ApiErrorCode');\n const errorCode =\n responseErrorCode ??\n (headerErrorCode && /^\\d+$/.test(headerErrorCode)\n ? Number(headerErrorCode)\n : undefined);\n const postmarkErrorCode =\n typeof errorCode === 'number' &&\n Number.isSafeInteger(errorCode) &&\n errorCode >= 0\n ? errorCode\n : undefined;\n\n if (\n !response.ok ||\n (postmarkErrorCode !== undefined && postmarkErrorCode !== 0)\n ) {\n const inactiveRecipient =\n response.status === 422 && postmarkErrorCode === 406;\n const details: EmailDeliveryFailureDetails = {\n provider: 'postmark',\n providerCategory: inactiveRecipient ? 'inactiveRecipient' : 'rejected',\n httpStatus: response.status,\n ...(postmarkErrorCode !== undefined ? { postmarkErrorCode } : {}),\n };\n\n if (inactiveRecipient) {\n throw new EmailDeliveryError(\n 'Postmark recipient is inactive',\n details,\n true,\n false\n );\n }\n\n throw new EmailDeliveryError(\n 'Postmark rejected the email',\n details,\n false,\n response.status === 429 || response.status >= 500\n );\n }\n\n const messageId =\n providerResponse !== null &&\n typeof providerResponse === 'object' &&\n 'MessageID' in providerResponse &&\n typeof providerResponse.MessageID === 'string' &&\n providerResponse.MessageID\n ? providerResponse.MessageID\n : undefined;\n const submittedAt =\n providerResponse !== null &&\n typeof providerResponse === 'object' &&\n 'SubmittedAt' in providerResponse &&\n typeof providerResponse.SubmittedAt === 'string' &&\n day(providerResponse.SubmittedAt).isValid()\n ? providerResponse.SubmittedAt\n : undefined;\n if (!messageId || !submittedAt || postmarkErrorCode !== 0) {\n throw new EmailDeliveryError('Postmark returned an invalid response', {\n provider: 'postmark',\n providerCategory: 'rejected',\n httpStatus: response.status,\n ...(postmarkErrorCode !== undefined ? { postmarkErrorCode } : {}),\n });\n }\n\n return {\n httpStatus: response.status,\n messageId,\n submittedAt,\n };\n}\n\nexport async function settleEmailDeliveries(deliveries: Promise<unknown>[]) {\n const failures = (await Promise.allSettled(deliveries)).filter(\n (result) => result.status === 'rejected'\n );\n if (failures.length === 0) return;\n\n const unexpectedDeliveryError = failures.find(\n (failure) =>\n failure.reason instanceof EmailDeliveryError && !failure.reason.expected\n )?.reason;\n const allExpected = failures.every(\n (failure) =>\n failure.reason instanceof EmailDeliveryError && failure.reason.expected\n );\n const firstDeliveryError = failures.find(\n (failure) => failure.reason instanceof EmailDeliveryError\n )?.reason;\n const failureDetails = failures.flatMap((failure) => {\n if (\n !(failure.reason instanceof EmailDeliveryError) ||\n !failure.reason.details ||\n !('provider' in failure.reason.details)\n ) {\n return [];\n }\n const { httpStatus, postmarkErrorCode, provider, providerCategory } =\n failure.reason.details;\n return [\n {\n httpStatus,\n provider,\n providerCategory,\n ...(postmarkErrorCode !== undefined ? { postmarkErrorCode } : {}),\n },\n ];\n });\n\n const details: EmailDeliveryErrorDetails = {\n failureCount: failures.length,\n failures: failureDetails,\n };\n if (unexpectedDeliveryError instanceof EmailDeliveryError) {\n if (unexpectedDeliveryError.message === 'Postmark is not configured') {\n throw new EmailDeliveryError(\n 'Postmark is not configured',\n details,\n false,\n false\n );\n }\n throw new EmailDeliveryError(\n 'Postmark rejected the email',\n details,\n false,\n unexpectedDeliveryError.retryable\n );\n }\n if (allExpected && firstDeliveryError instanceof EmailDeliveryError) {\n throw new EmailDeliveryError(\n 'Postmark recipient is inactive',\n details,\n true,\n false\n );\n }\n throw new EmailDeliveryError('Email delivery failed', details);\n}\n"]}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare function findPostmarkEmail(deliveryId: string, options: {
|
|
2
|
+
fetch?: (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
3
|
+
postmarkToken?: string;
|
|
4
|
+
}): Promise<{
|
|
5
|
+
httpStatus: number;
|
|
6
|
+
messageId: any;
|
|
7
|
+
submittedAt: any;
|
|
8
|
+
} | undefined>;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { day } from '@vrplatform/utils';
|
|
2
|
+
import { EmailDeliveryError } from './send-email';
|
|
3
|
+
export async function findPostmarkEmail(deliveryId, options) {
|
|
4
|
+
if (!options.postmarkToken) {
|
|
5
|
+
throw new EmailDeliveryError('Postmark is not configured', undefined, false, false);
|
|
6
|
+
}
|
|
7
|
+
const url = new URL('https://api.postmarkapp.com/messages/outbound');
|
|
8
|
+
url.searchParams.set('count', '1');
|
|
9
|
+
url.searchParams.set('offset', '0');
|
|
10
|
+
url.searchParams.set('messagestream', 'outbound');
|
|
11
|
+
url.searchParams.set('metadata_deliveryId', deliveryId);
|
|
12
|
+
const response = await (options.fetch ?? fetch)(url, {
|
|
13
|
+
method: 'GET',
|
|
14
|
+
headers: {
|
|
15
|
+
Accept: 'application/json',
|
|
16
|
+
'X-Postmark-Server-Token': options.postmarkToken,
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
const providerResponse = await response.json().catch(() => ({}));
|
|
20
|
+
if (!response.ok) {
|
|
21
|
+
throw new EmailDeliveryError('Postmark message lookup failed', {
|
|
22
|
+
provider: 'postmark',
|
|
23
|
+
providerCategory: 'rejected',
|
|
24
|
+
httpStatus: response.status,
|
|
25
|
+
}, false, response.status === 429 || response.status >= 500);
|
|
26
|
+
}
|
|
27
|
+
if (!providerResponse ||
|
|
28
|
+
typeof providerResponse !== 'object' ||
|
|
29
|
+
!('TotalCount' in providerResponse) ||
|
|
30
|
+
typeof providerResponse.TotalCount !== 'number' ||
|
|
31
|
+
!('Messages' in providerResponse) ||
|
|
32
|
+
!Array.isArray(providerResponse.Messages)) {
|
|
33
|
+
throw new EmailDeliveryError('Postmark returned an invalid response');
|
|
34
|
+
}
|
|
35
|
+
if (providerResponse.TotalCount === 0)
|
|
36
|
+
return undefined;
|
|
37
|
+
const message = providerResponse.Messages.find((value) => value &&
|
|
38
|
+
typeof value === 'object' &&
|
|
39
|
+
'Metadata' in value &&
|
|
40
|
+
value.Metadata &&
|
|
41
|
+
typeof value.Metadata === 'object' &&
|
|
42
|
+
'deliveryId' in value.Metadata &&
|
|
43
|
+
value.Metadata.deliveryId === deliveryId);
|
|
44
|
+
const messageId = message &&
|
|
45
|
+
typeof message === 'object' &&
|
|
46
|
+
'MessageID' in message &&
|
|
47
|
+
typeof message.MessageID === 'string' &&
|
|
48
|
+
message.MessageID
|
|
49
|
+
? message.MessageID
|
|
50
|
+
: undefined;
|
|
51
|
+
const submittedAt = message &&
|
|
52
|
+
typeof message === 'object' &&
|
|
53
|
+
'ReceivedAt' in message &&
|
|
54
|
+
typeof message.ReceivedAt === 'string' &&
|
|
55
|
+
day(message.ReceivedAt).isValid()
|
|
56
|
+
? message.ReceivedAt
|
|
57
|
+
: undefined;
|
|
58
|
+
if (!messageId || !submittedAt) {
|
|
59
|
+
throw new EmailDeliveryError('Postmark returned an invalid response');
|
|
60
|
+
}
|
|
61
|
+
return { httpStatus: response.status, messageId, submittedAt };
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=find-postmark-email.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"find-postmark-email.js","sourceRoot":"src/","sources":["utils/find-postmark-email.ts"],"names":[],"mappings":"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","sourcesContent":["import { day } from '@vrplatform/utils';\nimport { EmailDeliveryError } from './send-email';\n\nexport async function findPostmarkEmail(\n deliveryId: string,\n options: {\n fetch?: (\n input: string | URL | Request,\n init?: RequestInit\n ) => Promise<Response>;\n postmarkToken?: string;\n }\n) {\n if (!options.postmarkToken) {\n throw new EmailDeliveryError(\n 'Postmark is not configured',\n undefined,\n false,\n false\n );\n }\n\n const url = new URL('https://api.postmarkapp.com/messages/outbound');\n url.searchParams.set('count', '1');\n url.searchParams.set('offset', '0');\n url.searchParams.set('messagestream', 'outbound');\n url.searchParams.set('metadata_deliveryId', deliveryId);\n const response = await (options.fetch ?? fetch)(url, {\n method: 'GET',\n headers: {\n Accept: 'application/json',\n 'X-Postmark-Server-Token': options.postmarkToken,\n },\n });\n const providerResponse: unknown = await response.json().catch(() => ({}));\n\n if (!response.ok) {\n throw new EmailDeliveryError(\n 'Postmark message lookup failed',\n {\n provider: 'postmark',\n providerCategory: 'rejected',\n httpStatus: response.status,\n },\n false,\n response.status === 429 || response.status >= 500\n );\n }\n\n if (\n !providerResponse ||\n typeof providerResponse !== 'object' ||\n !('TotalCount' in providerResponse) ||\n typeof providerResponse.TotalCount !== 'number' ||\n !('Messages' in providerResponse) ||\n !Array.isArray(providerResponse.Messages)\n ) {\n throw new EmailDeliveryError('Postmark returned an invalid response');\n }\n if (providerResponse.TotalCount === 0) return undefined;\n\n const message = providerResponse.Messages.find(\n (value) =>\n value &&\n typeof value === 'object' &&\n 'Metadata' in value &&\n value.Metadata &&\n typeof value.Metadata === 'object' &&\n 'deliveryId' in value.Metadata &&\n value.Metadata.deliveryId === deliveryId\n );\n const messageId =\n message &&\n typeof message === 'object' &&\n 'MessageID' in message &&\n typeof message.MessageID === 'string' &&\n message.MessageID\n ? message.MessageID\n : undefined;\n const submittedAt =\n message &&\n typeof message === 'object' &&\n 'ReceivedAt' in message &&\n typeof message.ReceivedAt === 'string' &&\n day(message.ReceivedAt).isValid()\n ? message.ReceivedAt\n : undefined;\n if (!messageId || !submittedAt) {\n throw new EmailDeliveryError('Postmark returned an invalid response');\n }\n\n return { httpStatus: response.status, messageId, submittedAt };\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"src/","sources":["utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC","sourcesContent":["export * from './send-email';\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"src/","sources":["utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC;AACtC,cAAc,cAAc,CAAC","sourcesContent":["export * from './find-postmark-email';\nexport * from './find-postmark-email';\nexport * from './send-email';\n"]}
|
|
@@ -11,7 +11,8 @@ type EmailDeliveryErrorDetails = EmailDeliveryFailureDetails | {
|
|
|
11
11
|
export declare class EmailDeliveryError extends Error {
|
|
12
12
|
readonly details?: EmailDeliveryErrorDetails | undefined;
|
|
13
13
|
readonly expected: boolean;
|
|
14
|
-
|
|
14
|
+
readonly retryable: boolean;
|
|
15
|
+
constructor(message: string, details?: EmailDeliveryErrorDetails | undefined, expected?: boolean, retryable?: boolean);
|
|
15
16
|
}
|
|
16
17
|
export declare function sendEmail(data: {
|
|
17
18
|
to: string;
|
|
@@ -19,11 +20,15 @@ export declare function sendEmail(data: {
|
|
|
19
20
|
text: string;
|
|
20
21
|
html: string;
|
|
21
22
|
replyTo?: string;
|
|
23
|
+
tag?: string;
|
|
24
|
+
metadata?: Record<string, string>;
|
|
22
25
|
}, options: {
|
|
23
26
|
fetch?: (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
24
27
|
postmarkToken?: string;
|
|
25
28
|
}): Promise<{
|
|
26
|
-
|
|
29
|
+
httpStatus: number;
|
|
30
|
+
messageId: string;
|
|
31
|
+
submittedAt: string;
|
|
27
32
|
}>;
|
|
28
33
|
export declare function settleEmailDeliveries(deliveries: Promise<unknown>[]): Promise<void>;
|
|
29
34
|
export {};
|
|
@@ -1,16 +1,19 @@
|
|
|
1
|
+
import { day } from '@vrplatform/utils';
|
|
1
2
|
export class EmailDeliveryError extends Error {
|
|
2
3
|
details;
|
|
3
4
|
expected;
|
|
4
|
-
|
|
5
|
+
retryable;
|
|
6
|
+
constructor(message, details, expected = false, retryable = true) {
|
|
5
7
|
super(message);
|
|
6
8
|
this.details = details;
|
|
7
9
|
this.expected = expected;
|
|
10
|
+
this.retryable = retryable;
|
|
8
11
|
this.name = 'EmailDeliveryError';
|
|
9
12
|
}
|
|
10
13
|
}
|
|
11
14
|
export async function sendEmail(data, options) {
|
|
12
15
|
if (!options.postmarkToken) {
|
|
13
|
-
throw new EmailDeliveryError('Postmark is not configured');
|
|
16
|
+
throw new EmailDeliveryError('Postmark is not configured', undefined, false, false);
|
|
14
17
|
}
|
|
15
18
|
const response = await (options.fetch ?? fetch)('https://api.postmarkapp.com/email', {
|
|
16
19
|
method: 'POST',
|
|
@@ -27,6 +30,8 @@ export async function sendEmail(data, options) {
|
|
|
27
30
|
TextBody: data.text,
|
|
28
31
|
HtmlBody: data.html,
|
|
29
32
|
MessageStream: 'outbound',
|
|
33
|
+
Tag: data.tag,
|
|
34
|
+
Metadata: data.metadata,
|
|
30
35
|
}),
|
|
31
36
|
});
|
|
32
37
|
const providerResponse = await response.json().catch(() => ({}));
|
|
@@ -46,7 +51,8 @@ export async function sendEmail(data, options) {
|
|
|
46
51
|
errorCode >= 0
|
|
47
52
|
? errorCode
|
|
48
53
|
: undefined;
|
|
49
|
-
if (!response.ok
|
|
54
|
+
if (!response.ok ||
|
|
55
|
+
(postmarkErrorCode !== undefined && postmarkErrorCode !== 0)) {
|
|
50
56
|
const inactiveRecipient = response.status === 422 && postmarkErrorCode === 406;
|
|
51
57
|
const details = {
|
|
52
58
|
provider: 'postmark',
|
|
@@ -55,11 +61,37 @@ export async function sendEmail(data, options) {
|
|
|
55
61
|
...(postmarkErrorCode !== undefined ? { postmarkErrorCode } : {}),
|
|
56
62
|
};
|
|
57
63
|
if (inactiveRecipient) {
|
|
58
|
-
throw new EmailDeliveryError('Postmark recipient is inactive', details, true);
|
|
64
|
+
throw new EmailDeliveryError('Postmark recipient is inactive', details, true, false);
|
|
59
65
|
}
|
|
60
|
-
throw new EmailDeliveryError('Postmark rejected the email', details);
|
|
66
|
+
throw new EmailDeliveryError('Postmark rejected the email', details, false, response.status === 429 || response.status >= 500);
|
|
61
67
|
}
|
|
62
|
-
|
|
68
|
+
const messageId = providerResponse !== null &&
|
|
69
|
+
typeof providerResponse === 'object' &&
|
|
70
|
+
'MessageID' in providerResponse &&
|
|
71
|
+
typeof providerResponse.MessageID === 'string' &&
|
|
72
|
+
providerResponse.MessageID
|
|
73
|
+
? providerResponse.MessageID
|
|
74
|
+
: undefined;
|
|
75
|
+
const submittedAt = providerResponse !== null &&
|
|
76
|
+
typeof providerResponse === 'object' &&
|
|
77
|
+
'SubmittedAt' in providerResponse &&
|
|
78
|
+
typeof providerResponse.SubmittedAt === 'string' &&
|
|
79
|
+
day(providerResponse.SubmittedAt).isValid()
|
|
80
|
+
? providerResponse.SubmittedAt
|
|
81
|
+
: undefined;
|
|
82
|
+
if (!messageId || !submittedAt || postmarkErrorCode !== 0) {
|
|
83
|
+
throw new EmailDeliveryError('Postmark returned an invalid response', {
|
|
84
|
+
provider: 'postmark',
|
|
85
|
+
providerCategory: 'rejected',
|
|
86
|
+
httpStatus: response.status,
|
|
87
|
+
...(postmarkErrorCode !== undefined ? { postmarkErrorCode } : {}),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
httpStatus: response.status,
|
|
92
|
+
messageId,
|
|
93
|
+
submittedAt,
|
|
94
|
+
};
|
|
63
95
|
}
|
|
64
96
|
export async function settleEmailDeliveries(deliveries) {
|
|
65
97
|
const failures = (await Promise.allSettled(deliveries)).filter((result) => result.status === 'rejected');
|
|
@@ -90,12 +122,12 @@ export async function settleEmailDeliveries(deliveries) {
|
|
|
90
122
|
};
|
|
91
123
|
if (unexpectedDeliveryError instanceof EmailDeliveryError) {
|
|
92
124
|
if (unexpectedDeliveryError.message === 'Postmark is not configured') {
|
|
93
|
-
throw new EmailDeliveryError('Postmark is not configured', details);
|
|
125
|
+
throw new EmailDeliveryError('Postmark is not configured', details, false, false);
|
|
94
126
|
}
|
|
95
|
-
throw new EmailDeliveryError('Postmark rejected the email', details);
|
|
127
|
+
throw new EmailDeliveryError('Postmark rejected the email', details, false, unexpectedDeliveryError.retryable);
|
|
96
128
|
}
|
|
97
129
|
if (allExpected && firstDeliveryError instanceof EmailDeliveryError) {
|
|
98
|
-
throw new EmailDeliveryError('Postmark recipient is inactive', details, true);
|
|
130
|
+
throw new EmailDeliveryError('Postmark recipient is inactive', details, true, false);
|
|
99
131
|
}
|
|
100
132
|
throw new EmailDeliveryError('Email delivery failed', details);
|
|
101
133
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"send-email.js","sourceRoot":"src/","sources":["utils/send-email.ts"],"names":[],"mappings":"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","sourcesContent":["type EmailDeliveryFailureDetails = {\n httpStatus: number;\n postmarkErrorCode?: number;\n provider: 'postmark';\n providerCategory: 'inactiveRecipient' | 'rejected';\n};\n\ntype EmailDeliveryErrorDetails =\n | EmailDeliveryFailureDetails\n | {\n failureCount: number;\n failures: EmailDeliveryFailureDetails[];\n };\n\nexport class EmailDeliveryError extends Error {\n constructor(\n message: string,\n public readonly details?: EmailDeliveryErrorDetails,\n public readonly expected = false\n ) {\n super(message);\n this.name = 'EmailDeliveryError';\n }\n}\n\nexport async function sendEmail(\n data: {\n to: string;\n subject: string;\n text: string;\n html: string;\n replyTo?: string;\n },\n options: {\n fetch?: (\n input: string | URL | Request,\n init?: RequestInit\n ) => Promise<Response>;\n postmarkToken?: string;\n }\n) {\n if (!options.postmarkToken) {\n throw new EmailDeliveryError('Postmark is not configured');\n }\n\n const response = await (options.fetch ?? fetch)(\n 'https://api.postmarkapp.com/email',\n {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n 'X-Postmark-Server-Token': options.postmarkToken,\n },\n body: JSON.stringify({\n From: 'VRPlatform <notifications@vrplatform.app>',\n To: data.to,\n Subject: data.subject,\n ReplyTo: data.replyTo,\n TextBody: data.text,\n HtmlBody: data.html,\n MessageStream: 'outbound',\n }),\n }\n );\n\n const providerResponse: unknown = await response.json().catch(() => ({}));\n const responseErrorCode =\n providerResponse !== null &&\n typeof providerResponse === 'object' &&\n 'ErrorCode' in providerResponse &&\n typeof providerResponse.ErrorCode === 'number'\n ? providerResponse.ErrorCode\n : undefined;\n const headerErrorCode = response.headers.get('X-PM-ApiErrorCode');\n const errorCode =\n responseErrorCode ??\n (headerErrorCode && /^\\d+$/.test(headerErrorCode)\n ? Number(headerErrorCode)\n : undefined);\n const postmarkErrorCode =\n typeof errorCode === 'number' &&\n Number.isSafeInteger(errorCode) &&\n errorCode >= 0\n ? errorCode\n : undefined;\n\n if (!response.ok) {\n const inactiveRecipient =\n response.status === 422 && postmarkErrorCode === 406;\n const details: EmailDeliveryFailureDetails = {\n provider: 'postmark',\n providerCategory: inactiveRecipient ? 'inactiveRecipient' : 'rejected',\n httpStatus: response.status,\n ...(postmarkErrorCode !== undefined ? { postmarkErrorCode } : {}),\n };\n\n if (inactiveRecipient) {\n throw new EmailDeliveryError(\n 'Postmark recipient is inactive',\n details,\n true\n );\n }\n\n throw new EmailDeliveryError('Postmark rejected the email', details);\n }\n\n return { status: response.status };\n}\n\nexport async function settleEmailDeliveries(deliveries: Promise<unknown>[]) {\n const failures = (await Promise.allSettled(deliveries)).filter(\n (result) => result.status === 'rejected'\n );\n if (failures.length === 0) return;\n\n const unexpectedDeliveryError = failures.find(\n (failure) =>\n failure.reason instanceof EmailDeliveryError && !failure.reason.expected\n )?.reason;\n const allExpected = failures.every(\n (failure) =>\n failure.reason instanceof EmailDeliveryError && failure.reason.expected\n );\n const firstDeliveryError = failures.find(\n (failure) => failure.reason instanceof EmailDeliveryError\n )?.reason;\n const failureDetails = failures.flatMap((failure) => {\n if (\n !(failure.reason instanceof EmailDeliveryError) ||\n !failure.reason.details ||\n !('provider' in failure.reason.details)\n ) {\n return [];\n }\n const { httpStatus, postmarkErrorCode, provider, providerCategory } =\n failure.reason.details;\n return [\n {\n httpStatus,\n provider,\n providerCategory,\n ...(postmarkErrorCode !== undefined ? { postmarkErrorCode } : {}),\n },\n ];\n });\n\n const details: EmailDeliveryErrorDetails = {\n failureCount: failures.length,\n failures: failureDetails,\n };\n if (unexpectedDeliveryError instanceof EmailDeliveryError) {\n if (unexpectedDeliveryError.message === 'Postmark is not configured') {\n throw new EmailDeliveryError('Postmark is not configured', details);\n }\n throw new EmailDeliveryError('Postmark rejected the email', details);\n }\n if (allExpected && firstDeliveryError instanceof EmailDeliveryError) {\n throw new EmailDeliveryError(\n 'Postmark recipient is inactive',\n details,\n true\n );\n }\n throw new EmailDeliveryError('Email delivery failed', details);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"send-email.js","sourceRoot":"src/","sources":["utils/send-email.ts"],"names":[],"mappings":"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","sourcesContent":["import { day } from '@vrplatform/utils';\n\ntype EmailDeliveryFailureDetails = {\n httpStatus: number;\n postmarkErrorCode?: number;\n provider: 'postmark';\n providerCategory: 'inactiveRecipient' | 'rejected';\n};\n\ntype EmailDeliveryErrorDetails =\n | EmailDeliveryFailureDetails\n | {\n failureCount: number;\n failures: EmailDeliveryFailureDetails[];\n };\n\nexport class EmailDeliveryError extends Error {\n constructor(\n message: string,\n public readonly details?: EmailDeliveryErrorDetails,\n public readonly expected = false,\n public readonly retryable = true\n ) {\n super(message);\n this.name = 'EmailDeliveryError';\n }\n}\n\nexport async function sendEmail(\n data: {\n to: string;\n subject: string;\n text: string;\n html: string;\n replyTo?: string;\n tag?: string;\n metadata?: Record<string, string>;\n },\n options: {\n fetch?: (\n input: string | URL | Request,\n init?: RequestInit\n ) => Promise<Response>;\n postmarkToken?: string;\n }\n) {\n if (!options.postmarkToken) {\n throw new EmailDeliveryError(\n 'Postmark is not configured',\n undefined,\n false,\n false\n );\n }\n\n const response = await (options.fetch ?? fetch)(\n 'https://api.postmarkapp.com/email',\n {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n 'X-Postmark-Server-Token': options.postmarkToken,\n },\n body: JSON.stringify({\n From: 'VRPlatform <notifications@vrplatform.app>',\n To: data.to,\n Subject: data.subject,\n ReplyTo: data.replyTo,\n TextBody: data.text,\n HtmlBody: data.html,\n MessageStream: 'outbound',\n Tag: data.tag,\n Metadata: data.metadata,\n }),\n }\n );\n\n const providerResponse: unknown = await response.json().catch(() => ({}));\n const responseErrorCode =\n providerResponse !== null &&\n typeof providerResponse === 'object' &&\n 'ErrorCode' in providerResponse &&\n typeof providerResponse.ErrorCode === 'number'\n ? providerResponse.ErrorCode\n : undefined;\n const headerErrorCode = response.headers.get('X-PM-ApiErrorCode');\n const errorCode =\n responseErrorCode ??\n (headerErrorCode && /^\\d+$/.test(headerErrorCode)\n ? Number(headerErrorCode)\n : undefined);\n const postmarkErrorCode =\n typeof errorCode === 'number' &&\n Number.isSafeInteger(errorCode) &&\n errorCode >= 0\n ? errorCode\n : undefined;\n\n if (\n !response.ok ||\n (postmarkErrorCode !== undefined && postmarkErrorCode !== 0)\n ) {\n const inactiveRecipient =\n response.status === 422 && postmarkErrorCode === 406;\n const details: EmailDeliveryFailureDetails = {\n provider: 'postmark',\n providerCategory: inactiveRecipient ? 'inactiveRecipient' : 'rejected',\n httpStatus: response.status,\n ...(postmarkErrorCode !== undefined ? { postmarkErrorCode } : {}),\n };\n\n if (inactiveRecipient) {\n throw new EmailDeliveryError(\n 'Postmark recipient is inactive',\n details,\n true,\n false\n );\n }\n\n throw new EmailDeliveryError(\n 'Postmark rejected the email',\n details,\n false,\n response.status === 429 || response.status >= 500\n );\n }\n\n const messageId =\n providerResponse !== null &&\n typeof providerResponse === 'object' &&\n 'MessageID' in providerResponse &&\n typeof providerResponse.MessageID === 'string' &&\n providerResponse.MessageID\n ? providerResponse.MessageID\n : undefined;\n const submittedAt =\n providerResponse !== null &&\n typeof providerResponse === 'object' &&\n 'SubmittedAt' in providerResponse &&\n typeof providerResponse.SubmittedAt === 'string' &&\n day(providerResponse.SubmittedAt).isValid()\n ? providerResponse.SubmittedAt\n : undefined;\n if (!messageId || !submittedAt || postmarkErrorCode !== 0) {\n throw new EmailDeliveryError('Postmark returned an invalid response', {\n provider: 'postmark',\n providerCategory: 'rejected',\n httpStatus: response.status,\n ...(postmarkErrorCode !== undefined ? { postmarkErrorCode } : {}),\n });\n }\n\n return {\n httpStatus: response.status,\n messageId,\n submittedAt,\n };\n}\n\nexport async function settleEmailDeliveries(deliveries: Promise<unknown>[]) {\n const failures = (await Promise.allSettled(deliveries)).filter(\n (result) => result.status === 'rejected'\n );\n if (failures.length === 0) return;\n\n const unexpectedDeliveryError = failures.find(\n (failure) =>\n failure.reason instanceof EmailDeliveryError && !failure.reason.expected\n )?.reason;\n const allExpected = failures.every(\n (failure) =>\n failure.reason instanceof EmailDeliveryError && failure.reason.expected\n );\n const firstDeliveryError = failures.find(\n (failure) => failure.reason instanceof EmailDeliveryError\n )?.reason;\n const failureDetails = failures.flatMap((failure) => {\n if (\n !(failure.reason instanceof EmailDeliveryError) ||\n !failure.reason.details ||\n !('provider' in failure.reason.details)\n ) {\n return [];\n }\n const { httpStatus, postmarkErrorCode, provider, providerCategory } =\n failure.reason.details;\n return [\n {\n httpStatus,\n provider,\n providerCategory,\n ...(postmarkErrorCode !== undefined ? { postmarkErrorCode } : {}),\n },\n ];\n });\n\n const details: EmailDeliveryErrorDetails = {\n failureCount: failures.length,\n failures: failureDetails,\n };\n if (unexpectedDeliveryError instanceof EmailDeliveryError) {\n if (unexpectedDeliveryError.message === 'Postmark is not configured') {\n throw new EmailDeliveryError(\n 'Postmark is not configured',\n details,\n false,\n false\n );\n }\n throw new EmailDeliveryError(\n 'Postmark rejected the email',\n details,\n false,\n unexpectedDeliveryError.retryable\n );\n }\n if (allExpected && firstDeliveryError instanceof EmailDeliveryError) {\n throw new EmailDeliveryError(\n 'Postmark recipient is inactive',\n details,\n true,\n false\n );\n }\n throw new EmailDeliveryError('Email delivery failed', details);\n}\n"]}
|
package/package.json
CHANGED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { day } from '@vrplatform/utils';
|
|
2
|
+
import { EmailDeliveryError } from './send-email';
|
|
3
|
+
|
|
4
|
+
export async function findPostmarkEmail(
|
|
5
|
+
deliveryId: string,
|
|
6
|
+
options: {
|
|
7
|
+
fetch?: (
|
|
8
|
+
input: string | URL | Request,
|
|
9
|
+
init?: RequestInit
|
|
10
|
+
) => Promise<Response>;
|
|
11
|
+
postmarkToken?: string;
|
|
12
|
+
}
|
|
13
|
+
) {
|
|
14
|
+
if (!options.postmarkToken) {
|
|
15
|
+
throw new EmailDeliveryError(
|
|
16
|
+
'Postmark is not configured',
|
|
17
|
+
undefined,
|
|
18
|
+
false,
|
|
19
|
+
false
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const url = new URL('https://api.postmarkapp.com/messages/outbound');
|
|
24
|
+
url.searchParams.set('count', '1');
|
|
25
|
+
url.searchParams.set('offset', '0');
|
|
26
|
+
url.searchParams.set('messagestream', 'outbound');
|
|
27
|
+
url.searchParams.set('metadata_deliveryId', deliveryId);
|
|
28
|
+
const response = await (options.fetch ?? fetch)(url, {
|
|
29
|
+
method: 'GET',
|
|
30
|
+
headers: {
|
|
31
|
+
Accept: 'application/json',
|
|
32
|
+
'X-Postmark-Server-Token': options.postmarkToken,
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
const providerResponse: unknown = await response.json().catch(() => ({}));
|
|
36
|
+
|
|
37
|
+
if (!response.ok) {
|
|
38
|
+
throw new EmailDeliveryError(
|
|
39
|
+
'Postmark message lookup failed',
|
|
40
|
+
{
|
|
41
|
+
provider: 'postmark',
|
|
42
|
+
providerCategory: 'rejected',
|
|
43
|
+
httpStatus: response.status,
|
|
44
|
+
},
|
|
45
|
+
false,
|
|
46
|
+
response.status === 429 || response.status >= 500
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (
|
|
51
|
+
!providerResponse ||
|
|
52
|
+
typeof providerResponse !== 'object' ||
|
|
53
|
+
!('TotalCount' in providerResponse) ||
|
|
54
|
+
typeof providerResponse.TotalCount !== 'number' ||
|
|
55
|
+
!('Messages' in providerResponse) ||
|
|
56
|
+
!Array.isArray(providerResponse.Messages)
|
|
57
|
+
) {
|
|
58
|
+
throw new EmailDeliveryError('Postmark returned an invalid response');
|
|
59
|
+
}
|
|
60
|
+
if (providerResponse.TotalCount === 0) return undefined;
|
|
61
|
+
|
|
62
|
+
const message = providerResponse.Messages.find(
|
|
63
|
+
(value) =>
|
|
64
|
+
value &&
|
|
65
|
+
typeof value === 'object' &&
|
|
66
|
+
'Metadata' in value &&
|
|
67
|
+
value.Metadata &&
|
|
68
|
+
typeof value.Metadata === 'object' &&
|
|
69
|
+
'deliveryId' in value.Metadata &&
|
|
70
|
+
value.Metadata.deliveryId === deliveryId
|
|
71
|
+
);
|
|
72
|
+
const messageId =
|
|
73
|
+
message &&
|
|
74
|
+
typeof message === 'object' &&
|
|
75
|
+
'MessageID' in message &&
|
|
76
|
+
typeof message.MessageID === 'string' &&
|
|
77
|
+
message.MessageID
|
|
78
|
+
? message.MessageID
|
|
79
|
+
: undefined;
|
|
80
|
+
const submittedAt =
|
|
81
|
+
message &&
|
|
82
|
+
typeof message === 'object' &&
|
|
83
|
+
'ReceivedAt' in message &&
|
|
84
|
+
typeof message.ReceivedAt === 'string' &&
|
|
85
|
+
day(message.ReceivedAt).isValid()
|
|
86
|
+
? message.ReceivedAt
|
|
87
|
+
: undefined;
|
|
88
|
+
if (!messageId || !submittedAt) {
|
|
89
|
+
throw new EmailDeliveryError('Postmark returned an invalid response');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return { httpStatus: response.status, messageId, submittedAt };
|
|
93
|
+
}
|
package/src/utils/index.ts
CHANGED
package/src/utils/send-email.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { day } from '@vrplatform/utils';
|
|
2
|
+
|
|
1
3
|
type EmailDeliveryFailureDetails = {
|
|
2
4
|
httpStatus: number;
|
|
3
5
|
postmarkErrorCode?: number;
|
|
@@ -16,7 +18,8 @@ export class EmailDeliveryError extends Error {
|
|
|
16
18
|
constructor(
|
|
17
19
|
message: string,
|
|
18
20
|
public readonly details?: EmailDeliveryErrorDetails,
|
|
19
|
-
public readonly expected = false
|
|
21
|
+
public readonly expected = false,
|
|
22
|
+
public readonly retryable = true
|
|
20
23
|
) {
|
|
21
24
|
super(message);
|
|
22
25
|
this.name = 'EmailDeliveryError';
|
|
@@ -30,6 +33,8 @@ export async function sendEmail(
|
|
|
30
33
|
text: string;
|
|
31
34
|
html: string;
|
|
32
35
|
replyTo?: string;
|
|
36
|
+
tag?: string;
|
|
37
|
+
metadata?: Record<string, string>;
|
|
33
38
|
},
|
|
34
39
|
options: {
|
|
35
40
|
fetch?: (
|
|
@@ -40,7 +45,12 @@ export async function sendEmail(
|
|
|
40
45
|
}
|
|
41
46
|
) {
|
|
42
47
|
if (!options.postmarkToken) {
|
|
43
|
-
throw new EmailDeliveryError(
|
|
48
|
+
throw new EmailDeliveryError(
|
|
49
|
+
'Postmark is not configured',
|
|
50
|
+
undefined,
|
|
51
|
+
false,
|
|
52
|
+
false
|
|
53
|
+
);
|
|
44
54
|
}
|
|
45
55
|
|
|
46
56
|
const response = await (options.fetch ?? fetch)(
|
|
@@ -60,6 +70,8 @@ export async function sendEmail(
|
|
|
60
70
|
TextBody: data.text,
|
|
61
71
|
HtmlBody: data.html,
|
|
62
72
|
MessageStream: 'outbound',
|
|
73
|
+
Tag: data.tag,
|
|
74
|
+
Metadata: data.metadata,
|
|
63
75
|
}),
|
|
64
76
|
}
|
|
65
77
|
);
|
|
@@ -85,7 +97,10 @@ export async function sendEmail(
|
|
|
85
97
|
? errorCode
|
|
86
98
|
: undefined;
|
|
87
99
|
|
|
88
|
-
if (
|
|
100
|
+
if (
|
|
101
|
+
!response.ok ||
|
|
102
|
+
(postmarkErrorCode !== undefined && postmarkErrorCode !== 0)
|
|
103
|
+
) {
|
|
89
104
|
const inactiveRecipient =
|
|
90
105
|
response.status === 422 && postmarkErrorCode === 406;
|
|
91
106
|
const details: EmailDeliveryFailureDetails = {
|
|
@@ -99,14 +114,49 @@ export async function sendEmail(
|
|
|
99
114
|
throw new EmailDeliveryError(
|
|
100
115
|
'Postmark recipient is inactive',
|
|
101
116
|
details,
|
|
102
|
-
true
|
|
117
|
+
true,
|
|
118
|
+
false
|
|
103
119
|
);
|
|
104
120
|
}
|
|
105
121
|
|
|
106
|
-
throw new EmailDeliveryError(
|
|
122
|
+
throw new EmailDeliveryError(
|
|
123
|
+
'Postmark rejected the email',
|
|
124
|
+
details,
|
|
125
|
+
false,
|
|
126
|
+
response.status === 429 || response.status >= 500
|
|
127
|
+
);
|
|
107
128
|
}
|
|
108
129
|
|
|
109
|
-
|
|
130
|
+
const messageId =
|
|
131
|
+
providerResponse !== null &&
|
|
132
|
+
typeof providerResponse === 'object' &&
|
|
133
|
+
'MessageID' in providerResponse &&
|
|
134
|
+
typeof providerResponse.MessageID === 'string' &&
|
|
135
|
+
providerResponse.MessageID
|
|
136
|
+
? providerResponse.MessageID
|
|
137
|
+
: undefined;
|
|
138
|
+
const submittedAt =
|
|
139
|
+
providerResponse !== null &&
|
|
140
|
+
typeof providerResponse === 'object' &&
|
|
141
|
+
'SubmittedAt' in providerResponse &&
|
|
142
|
+
typeof providerResponse.SubmittedAt === 'string' &&
|
|
143
|
+
day(providerResponse.SubmittedAt).isValid()
|
|
144
|
+
? providerResponse.SubmittedAt
|
|
145
|
+
: undefined;
|
|
146
|
+
if (!messageId || !submittedAt || postmarkErrorCode !== 0) {
|
|
147
|
+
throw new EmailDeliveryError('Postmark returned an invalid response', {
|
|
148
|
+
provider: 'postmark',
|
|
149
|
+
providerCategory: 'rejected',
|
|
150
|
+
httpStatus: response.status,
|
|
151
|
+
...(postmarkErrorCode !== undefined ? { postmarkErrorCode } : {}),
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
httpStatus: response.status,
|
|
157
|
+
messageId,
|
|
158
|
+
submittedAt,
|
|
159
|
+
};
|
|
110
160
|
}
|
|
111
161
|
|
|
112
162
|
export async function settleEmailDeliveries(deliveries: Promise<unknown>[]) {
|
|
@@ -152,15 +202,26 @@ export async function settleEmailDeliveries(deliveries: Promise<unknown>[]) {
|
|
|
152
202
|
};
|
|
153
203
|
if (unexpectedDeliveryError instanceof EmailDeliveryError) {
|
|
154
204
|
if (unexpectedDeliveryError.message === 'Postmark is not configured') {
|
|
155
|
-
throw new EmailDeliveryError(
|
|
205
|
+
throw new EmailDeliveryError(
|
|
206
|
+
'Postmark is not configured',
|
|
207
|
+
details,
|
|
208
|
+
false,
|
|
209
|
+
false
|
|
210
|
+
);
|
|
156
211
|
}
|
|
157
|
-
throw new EmailDeliveryError(
|
|
212
|
+
throw new EmailDeliveryError(
|
|
213
|
+
'Postmark rejected the email',
|
|
214
|
+
details,
|
|
215
|
+
false,
|
|
216
|
+
unexpectedDeliveryError.retryable
|
|
217
|
+
);
|
|
158
218
|
}
|
|
159
219
|
if (allExpected && firstDeliveryError instanceof EmailDeliveryError) {
|
|
160
220
|
throw new EmailDeliveryError(
|
|
161
221
|
'Postmark recipient is inactive',
|
|
162
222
|
details,
|
|
163
|
-
true
|
|
223
|
+
true,
|
|
224
|
+
false
|
|
164
225
|
);
|
|
165
226
|
}
|
|
166
227
|
throw new EmailDeliveryError('Email delivery failed', details);
|