@vrplatform/email 1.0.2-stage.551 → 1.0.2-stage.560
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 +55 -0
- package/build/main/utils/find-postmark-email.js.map +1 -0
- package/build/main/utils/index.d.ts +1 -0
- package/build/main/utils/index.js +1 -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 +49 -25
- 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 +52 -0
- package/build/module/utils/find-postmark-email.js.map +1 -0
- package/build/module/utils/index.d.ts +1 -0
- package/build/module/utils/index.js +1 -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 +50 -25
- package/build/module/utils/send-email.js.map +1 -1
- package/package.json +3 -2
- package/src/utils/find-postmark-email.ts +80 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/send-email.ts +81 -27
|
@@ -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: string;
|
|
7
|
+
submittedAt: string;
|
|
8
|
+
} | undefined>;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.findPostmarkEmail = findPostmarkEmail;
|
|
4
|
+
const utils_1 = require("@vrplatform/utils");
|
|
5
|
+
const zod_1 = require("zod");
|
|
6
|
+
const send_email_1 = require("./send-email");
|
|
7
|
+
const postmarkMessagesResponseSchema = zod_1.z.object({
|
|
8
|
+
TotalCount: zod_1.z.number().int().nonnegative(),
|
|
9
|
+
Messages: zod_1.z.array(zod_1.z.object({
|
|
10
|
+
MessageID: zod_1.z.string().min(1),
|
|
11
|
+
ReceivedAt: zod_1.z.string().refine((value) => (0, utils_1.day)(value).isValid()),
|
|
12
|
+
Metadata: zod_1.z.object({ deliveryId: zod_1.z.string() }).passthrough(),
|
|
13
|
+
})),
|
|
14
|
+
});
|
|
15
|
+
async function findPostmarkEmail(deliveryId, options) {
|
|
16
|
+
if (!options.postmarkToken) {
|
|
17
|
+
throw new send_email_1.EmailDeliveryError('Postmark is not configured', undefined, false, false);
|
|
18
|
+
}
|
|
19
|
+
const url = new URL('https://api.postmarkapp.com/messages/outbound');
|
|
20
|
+
url.searchParams.set('count', '1');
|
|
21
|
+
url.searchParams.set('offset', '0');
|
|
22
|
+
url.searchParams.set('messagestream', 'outbound');
|
|
23
|
+
url.searchParams.set('metadata_deliveryId', deliveryId);
|
|
24
|
+
const response = await (options.fetch ?? fetch)(url, {
|
|
25
|
+
method: 'GET',
|
|
26
|
+
headers: {
|
|
27
|
+
Accept: 'application/json',
|
|
28
|
+
'X-Postmark-Server-Token': options.postmarkToken,
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
const providerResponse = await response.json().catch(() => ({}));
|
|
32
|
+
if (!response.ok) {
|
|
33
|
+
throw new send_email_1.EmailDeliveryError('Postmark message lookup failed', {
|
|
34
|
+
provider: 'postmark',
|
|
35
|
+
providerCategory: 'rejected',
|
|
36
|
+
httpStatus: response.status,
|
|
37
|
+
}, false, response.status === 429 || response.status >= 500);
|
|
38
|
+
}
|
|
39
|
+
const result = postmarkMessagesResponseSchema.safeParse(providerResponse);
|
|
40
|
+
if (!result.success) {
|
|
41
|
+
throw new send_email_1.EmailDeliveryError('Postmark returned an invalid response');
|
|
42
|
+
}
|
|
43
|
+
if (result.data.TotalCount === 0)
|
|
44
|
+
return undefined;
|
|
45
|
+
const message = result.data.Messages.find(({ Metadata }) => Metadata.deliveryId === deliveryId);
|
|
46
|
+
if (!message) {
|
|
47
|
+
throw new send_email_1.EmailDeliveryError('Postmark returned an invalid response');
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
httpStatus: response.status,
|
|
51
|
+
messageId: message.MessageID,
|
|
52
|
+
submittedAt: message.ReceivedAt,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
//# 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 { z } from 'zod';\nimport { EmailDeliveryError } from './send-email';\n\nconst postmarkMessagesResponseSchema = z.object({\n TotalCount: z.number().int().nonnegative(),\n Messages: z.array(\n z.object({\n MessageID: z.string().min(1),\n ReceivedAt: z.string().refine((value) => day(value).isValid()),\n Metadata: z.object({ deliveryId: z.string() }).passthrough(),\n })\n ),\n});\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 const result = postmarkMessagesResponseSchema.safeParse(providerResponse);\n if (!result.success) {\n throw new EmailDeliveryError('Postmark returned an invalid response');\n }\n if (result.data.TotalCount === 0) return undefined;\n\n const message = result.data.Messages.find(\n ({ Metadata }) => Metadata.deliveryId === deliveryId\n );\n if (!message) {\n throw new EmailDeliveryError('Postmark returned an invalid response');\n }\n\n return {\n httpStatus: response.status,\n messageId: message.MessageID,\n submittedAt: message.ReceivedAt,\n };\n}\n"]}
|
|
@@ -14,5 +14,6 @@ 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);
|
|
17
18
|
__exportStar(require("./send-email"), exports);
|
|
18
19
|
//# 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,+CAA6B","sourcesContent":["export * 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,40 @@ 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");
|
|
7
|
+
const zod_1 = require("zod");
|
|
8
|
+
const postmarkErrorCodeSchema = zod_1.z.number().int().nonnegative();
|
|
9
|
+
const postmarkResponseErrorCodeSchema = zod_1.z
|
|
10
|
+
.object({ ErrorCode: postmarkErrorCodeSchema })
|
|
11
|
+
.transform(({ ErrorCode }) => ErrorCode);
|
|
12
|
+
const postmarkHeaderErrorCodeSchema = zod_1.z
|
|
13
|
+
.string()
|
|
14
|
+
.regex(/^\d+$/)
|
|
15
|
+
.transform(Number)
|
|
16
|
+
.pipe(postmarkErrorCodeSchema);
|
|
17
|
+
const postmarkSuccessResponseSchema = zod_1.z.object({
|
|
18
|
+
ErrorCode: zod_1.z.literal(0),
|
|
19
|
+
MessageID: zod_1.z.string().min(1),
|
|
20
|
+
SubmittedAt: zod_1.z.string().refine((value) => (0, utils_1.day)(value).isValid()),
|
|
21
|
+
});
|
|
22
|
+
function parseOptional(schema, value) {
|
|
23
|
+
const result = schema.safeParse(value);
|
|
24
|
+
if (result.success)
|
|
25
|
+
return result.data;
|
|
26
|
+
}
|
|
6
27
|
class EmailDeliveryError extends Error {
|
|
7
|
-
constructor(message, details, expected = false) {
|
|
28
|
+
constructor(message, details, expected = false, retryable = true) {
|
|
8
29
|
super(message);
|
|
9
30
|
this.details = details;
|
|
10
31
|
this.expected = expected;
|
|
32
|
+
this.retryable = retryable;
|
|
11
33
|
this.name = 'EmailDeliveryError';
|
|
12
34
|
}
|
|
13
35
|
}
|
|
14
36
|
exports.EmailDeliveryError = EmailDeliveryError;
|
|
15
37
|
async function sendEmail(data, options) {
|
|
16
38
|
if (!options.postmarkToken) {
|
|
17
|
-
throw new EmailDeliveryError('Postmark is not configured');
|
|
39
|
+
throw new EmailDeliveryError('Postmark is not configured', undefined, false, false);
|
|
18
40
|
}
|
|
19
41
|
const response = await (options.fetch ?? fetch)('https://api.postmarkapp.com/email', {
|
|
20
42
|
method: 'POST',
|
|
@@ -31,26 +53,15 @@ async function sendEmail(data, options) {
|
|
|
31
53
|
TextBody: data.text,
|
|
32
54
|
HtmlBody: data.html,
|
|
33
55
|
MessageStream: 'outbound',
|
|
56
|
+
Tag: data.tag,
|
|
57
|
+
Metadata: data.metadata,
|
|
34
58
|
}),
|
|
35
59
|
});
|
|
36
60
|
const providerResponse = await response.json().catch(() => ({}));
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
? providerResponse.ErrorCode
|
|
42
|
-
: undefined;
|
|
43
|
-
const headerErrorCode = response.headers.get('X-PM-ApiErrorCode');
|
|
44
|
-
const errorCode = responseErrorCode ??
|
|
45
|
-
(headerErrorCode && /^\d+$/.test(headerErrorCode)
|
|
46
|
-
? Number(headerErrorCode)
|
|
47
|
-
: undefined);
|
|
48
|
-
const postmarkErrorCode = typeof errorCode === 'number' &&
|
|
49
|
-
Number.isSafeInteger(errorCode) &&
|
|
50
|
-
errorCode >= 0
|
|
51
|
-
? errorCode
|
|
52
|
-
: undefined;
|
|
53
|
-
if (!response.ok) {
|
|
61
|
+
const postmarkErrorCode = parseOptional(postmarkResponseErrorCodeSchema, providerResponse) ??
|
|
62
|
+
parseOptional(postmarkHeaderErrorCodeSchema, response.headers.get('X-PM-ApiErrorCode'));
|
|
63
|
+
if (!response.ok ||
|
|
64
|
+
(postmarkErrorCode !== undefined && postmarkErrorCode !== 0)) {
|
|
54
65
|
const inactiveRecipient = response.status === 422 && postmarkErrorCode === 406;
|
|
55
66
|
const details = {
|
|
56
67
|
provider: 'postmark',
|
|
@@ -59,11 +70,24 @@ async function sendEmail(data, options) {
|
|
|
59
70
|
...(postmarkErrorCode !== undefined ? { postmarkErrorCode } : {}),
|
|
60
71
|
};
|
|
61
72
|
if (inactiveRecipient) {
|
|
62
|
-
throw new EmailDeliveryError('Postmark recipient is inactive', details, true);
|
|
73
|
+
throw new EmailDeliveryError('Postmark recipient is inactive', details, true, false);
|
|
63
74
|
}
|
|
64
|
-
throw new EmailDeliveryError('Postmark rejected the email', details);
|
|
75
|
+
throw new EmailDeliveryError('Postmark rejected the email', details, false, response.status === 429 || response.status >= 500);
|
|
76
|
+
}
|
|
77
|
+
const result = postmarkSuccessResponseSchema.safeParse(providerResponse);
|
|
78
|
+
if (!result.success) {
|
|
79
|
+
throw new EmailDeliveryError('Postmark returned an invalid response', {
|
|
80
|
+
provider: 'postmark',
|
|
81
|
+
providerCategory: 'rejected',
|
|
82
|
+
httpStatus: response.status,
|
|
83
|
+
...(postmarkErrorCode !== undefined ? { postmarkErrorCode } : {}),
|
|
84
|
+
});
|
|
65
85
|
}
|
|
66
|
-
return {
|
|
86
|
+
return {
|
|
87
|
+
httpStatus: response.status,
|
|
88
|
+
messageId: result.data.MessageID,
|
|
89
|
+
submittedAt: result.data.SubmittedAt,
|
|
90
|
+
};
|
|
67
91
|
}
|
|
68
92
|
async function settleEmailDeliveries(deliveries) {
|
|
69
93
|
const failures = (await Promise.allSettled(deliveries)).filter((result) => result.status === 'rejected');
|
|
@@ -94,12 +118,12 @@ async function settleEmailDeliveries(deliveries) {
|
|
|
94
118
|
};
|
|
95
119
|
if (unexpectedDeliveryError instanceof EmailDeliveryError) {
|
|
96
120
|
if (unexpectedDeliveryError.message === 'Postmark is not configured') {
|
|
97
|
-
throw new EmailDeliveryError('Postmark is not configured', details);
|
|
121
|
+
throw new EmailDeliveryError('Postmark is not configured', details, false, false);
|
|
98
122
|
}
|
|
99
|
-
throw new EmailDeliveryError('Postmark rejected the email', details);
|
|
123
|
+
throw new EmailDeliveryError('Postmark rejected the email', details, false, unexpectedDeliveryError.retryable);
|
|
100
124
|
}
|
|
101
125
|
if (allExpected && firstDeliveryError instanceof EmailDeliveryError) {
|
|
102
|
-
throw new EmailDeliveryError('Postmark recipient is inactive', details, true);
|
|
126
|
+
throw new EmailDeliveryError('Postmark recipient is inactive', details, true, false);
|
|
103
127
|
}
|
|
104
128
|
throw new EmailDeliveryError('Email delivery failed', details);
|
|
105
129
|
}
|
|
@@ -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';\nimport { z } from 'zod';\n\nconst postmarkErrorCodeSchema = z.number().int().nonnegative();\nconst postmarkResponseErrorCodeSchema = z\n .object({ ErrorCode: postmarkErrorCodeSchema })\n .transform(({ ErrorCode }) => ErrorCode);\nconst postmarkHeaderErrorCodeSchema = z\n .string()\n .regex(/^\\d+$/)\n .transform(Number)\n .pipe(postmarkErrorCodeSchema);\nconst postmarkSuccessResponseSchema = z.object({\n ErrorCode: z.literal(0),\n MessageID: z.string().min(1),\n SubmittedAt: z.string().refine((value) => day(value).isValid()),\n});\n\nfunction parseOptional<T>(schema: z.ZodType<T>, value: unknown) {\n const result = schema.safeParse(value);\n if (result.success) return result.data;\n}\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 postmarkErrorCode =\n parseOptional(postmarkResponseErrorCodeSchema, providerResponse) ??\n parseOptional(\n postmarkHeaderErrorCodeSchema,\n response.headers.get('X-PM-ApiErrorCode')\n );\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 result = postmarkSuccessResponseSchema.safeParse(providerResponse);\n if (!result.success) {\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: result.data.MessageID,\n submittedAt: result.data.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: string;
|
|
7
|
+
submittedAt: string;
|
|
8
|
+
} | undefined>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { day } from '@vrplatform/utils';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { EmailDeliveryError } from './send-email';
|
|
4
|
+
const postmarkMessagesResponseSchema = z.object({
|
|
5
|
+
TotalCount: z.number().int().nonnegative(),
|
|
6
|
+
Messages: z.array(z.object({
|
|
7
|
+
MessageID: z.string().min(1),
|
|
8
|
+
ReceivedAt: z.string().refine((value) => day(value).isValid()),
|
|
9
|
+
Metadata: z.object({ deliveryId: z.string() }).passthrough(),
|
|
10
|
+
})),
|
|
11
|
+
});
|
|
12
|
+
export async function findPostmarkEmail(deliveryId, options) {
|
|
13
|
+
if (!options.postmarkToken) {
|
|
14
|
+
throw new EmailDeliveryError('Postmark is not configured', undefined, false, false);
|
|
15
|
+
}
|
|
16
|
+
const url = new URL('https://api.postmarkapp.com/messages/outbound');
|
|
17
|
+
url.searchParams.set('count', '1');
|
|
18
|
+
url.searchParams.set('offset', '0');
|
|
19
|
+
url.searchParams.set('messagestream', 'outbound');
|
|
20
|
+
url.searchParams.set('metadata_deliveryId', deliveryId);
|
|
21
|
+
const response = await (options.fetch ?? fetch)(url, {
|
|
22
|
+
method: 'GET',
|
|
23
|
+
headers: {
|
|
24
|
+
Accept: 'application/json',
|
|
25
|
+
'X-Postmark-Server-Token': options.postmarkToken,
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
const providerResponse = await response.json().catch(() => ({}));
|
|
29
|
+
if (!response.ok) {
|
|
30
|
+
throw new EmailDeliveryError('Postmark message lookup failed', {
|
|
31
|
+
provider: 'postmark',
|
|
32
|
+
providerCategory: 'rejected',
|
|
33
|
+
httpStatus: response.status,
|
|
34
|
+
}, false, response.status === 429 || response.status >= 500);
|
|
35
|
+
}
|
|
36
|
+
const result = postmarkMessagesResponseSchema.safeParse(providerResponse);
|
|
37
|
+
if (!result.success) {
|
|
38
|
+
throw new EmailDeliveryError('Postmark returned an invalid response');
|
|
39
|
+
}
|
|
40
|
+
if (result.data.TotalCount === 0)
|
|
41
|
+
return undefined;
|
|
42
|
+
const message = result.data.Messages.find(({ Metadata }) => Metadata.deliveryId === deliveryId);
|
|
43
|
+
if (!message) {
|
|
44
|
+
throw new EmailDeliveryError('Postmark returned an invalid response');
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
httpStatus: response.status,
|
|
48
|
+
messageId: message.MessageID,
|
|
49
|
+
submittedAt: message.ReceivedAt,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
//# 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":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,mBAAmB,CAAC;AACxC,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAElD,MAAM,8BAA8B,GAAG,CAAC,CAAC,MAAM,CAAC;IAC9C,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;IAC1C,QAAQ,EAAE,CAAC,CAAC,KAAK,CACf,CAAC,CAAC,MAAM,CAAC;QACP,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;QAC5B,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC;QAC9D,QAAQ,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,WAAW,EAAE;KAC7D,CAAC,CACH;CACF,CAAC,CAAC;AAEH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,UAAkB,EAClB,OAMC;IAED,IAAI,CAAC,OAAO,CAAC,aAAa,EAAE,CAAC;QAC3B,MAAM,IAAI,kBAAkB,CAC1B,4BAA4B,EAC5B,SAAS,EACT,KAAK,EACL,KAAK,CACN,CAAC;IACJ,CAAC;IAED,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,+CAA+C,CAAC,CAAC;IACrE,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;IACnC,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;IACpC,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,eAAe,EAAE,UAAU,CAAC,CAAC;IAClD,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,qBAAqB,EAAE,UAAU,CAAC,CAAC;IACxD,MAAM,QAAQ,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK,IAAI,KAAK,CAAC,CAAC,GAAG,EAAE;QACnD,MAAM,EAAE,KAAK;QACb,OAAO,EAAE;YACP,MAAM,EAAE,kBAAkB;YAC1B,yBAAyB,EAAE,OAAO,CAAC,aAAa;SACjD;KACF,CAAC,CAAC;IACH,MAAM,gBAAgB,GAAY,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAE1E,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,MAAM,IAAI,kBAAkB,CAC1B,gCAAgC,EAChC;YACE,QAAQ,EAAE,UAAU;YACpB,gBAAgB,EAAE,UAAU;YAC5B,UAAU,EAAE,QAAQ,CAAC,MAAM;SAC5B,EACD,KAAK,EACL,QAAQ,CAAC,MAAM,KAAK,GAAG,IAAI,QAAQ,CAAC,MAAM,IAAI,GAAG,CAClD,CAAC;IACJ,CAAC;IAED,MAAM,MAAM,GAAG,8BAA8B,CAAC,SAAS,CAAC,gBAAgB,CAAC,CAAC;IAC1E,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACpB,MAAM,IAAI,kBAAkB,CAAC,uCAAuC,CAAC,CAAC;IACxE,CAAC;IACD,IAAI,MAAM,CAAC,IAAI,CAAC,UAAU,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IAEnD,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CACvC,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC,QAAQ,CAAC,UAAU,KAAK,UAAU,CACrD,CAAC;IACF,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,kBAAkB,CAAC,uCAAuC,CAAC,CAAC;IACxE,CAAC;IAED,OAAO;QACL,UAAU,EAAE,QAAQ,CAAC,MAAM;QAC3B,SAAS,EAAE,OAAO,CAAC,SAAS;QAC5B,WAAW,EAAE,OAAO,CAAC,UAAU;KAChC,CAAC;AACJ,CAAC","sourcesContent":["import { day } from '@vrplatform/utils';\nimport { z } from 'zod';\nimport { EmailDeliveryError } from './send-email';\n\nconst postmarkMessagesResponseSchema = z.object({\n TotalCount: z.number().int().nonnegative(),\n Messages: z.array(\n z.object({\n MessageID: z.string().min(1),\n ReceivedAt: z.string().refine((value) => day(value).isValid()),\n Metadata: z.object({ deliveryId: z.string() }).passthrough(),\n })\n ),\n});\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 const result = postmarkMessagesResponseSchema.safeParse(providerResponse);\n if (!result.success) {\n throw new EmailDeliveryError('Postmark returned an invalid response');\n }\n if (result.data.TotalCount === 0) return undefined;\n\n const message = result.data.Messages.find(\n ({ Metadata }) => Metadata.deliveryId === deliveryId\n );\n if (!message) {\n throw new EmailDeliveryError('Postmark returned an invalid response');\n }\n\n return {\n httpStatus: response.status,\n messageId: message.MessageID,\n submittedAt: message.ReceivedAt,\n };\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,cAAc,CAAC","sourcesContent":["export * 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,39 @@
|
|
|
1
|
+
import { day } from '@vrplatform/utils';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
const postmarkErrorCodeSchema = z.number().int().nonnegative();
|
|
4
|
+
const postmarkResponseErrorCodeSchema = z
|
|
5
|
+
.object({ ErrorCode: postmarkErrorCodeSchema })
|
|
6
|
+
.transform(({ ErrorCode }) => ErrorCode);
|
|
7
|
+
const postmarkHeaderErrorCodeSchema = z
|
|
8
|
+
.string()
|
|
9
|
+
.regex(/^\d+$/)
|
|
10
|
+
.transform(Number)
|
|
11
|
+
.pipe(postmarkErrorCodeSchema);
|
|
12
|
+
const postmarkSuccessResponseSchema = z.object({
|
|
13
|
+
ErrorCode: z.literal(0),
|
|
14
|
+
MessageID: z.string().min(1),
|
|
15
|
+
SubmittedAt: z.string().refine((value) => day(value).isValid()),
|
|
16
|
+
});
|
|
17
|
+
function parseOptional(schema, value) {
|
|
18
|
+
const result = schema.safeParse(value);
|
|
19
|
+
if (result.success)
|
|
20
|
+
return result.data;
|
|
21
|
+
}
|
|
1
22
|
export class EmailDeliveryError extends Error {
|
|
2
23
|
details;
|
|
3
24
|
expected;
|
|
4
|
-
|
|
25
|
+
retryable;
|
|
26
|
+
constructor(message, details, expected = false, retryable = true) {
|
|
5
27
|
super(message);
|
|
6
28
|
this.details = details;
|
|
7
29
|
this.expected = expected;
|
|
30
|
+
this.retryable = retryable;
|
|
8
31
|
this.name = 'EmailDeliveryError';
|
|
9
32
|
}
|
|
10
33
|
}
|
|
11
34
|
export async function sendEmail(data, options) {
|
|
12
35
|
if (!options.postmarkToken) {
|
|
13
|
-
throw new EmailDeliveryError('Postmark is not configured');
|
|
36
|
+
throw new EmailDeliveryError('Postmark is not configured', undefined, false, false);
|
|
14
37
|
}
|
|
15
38
|
const response = await (options.fetch ?? fetch)('https://api.postmarkapp.com/email', {
|
|
16
39
|
method: 'POST',
|
|
@@ -27,26 +50,15 @@ export async function sendEmail(data, options) {
|
|
|
27
50
|
TextBody: data.text,
|
|
28
51
|
HtmlBody: data.html,
|
|
29
52
|
MessageStream: 'outbound',
|
|
53
|
+
Tag: data.tag,
|
|
54
|
+
Metadata: data.metadata,
|
|
30
55
|
}),
|
|
31
56
|
});
|
|
32
57
|
const providerResponse = await response.json().catch(() => ({}));
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
? providerResponse.ErrorCode
|
|
38
|
-
: undefined;
|
|
39
|
-
const headerErrorCode = response.headers.get('X-PM-ApiErrorCode');
|
|
40
|
-
const errorCode = responseErrorCode ??
|
|
41
|
-
(headerErrorCode && /^\d+$/.test(headerErrorCode)
|
|
42
|
-
? Number(headerErrorCode)
|
|
43
|
-
: undefined);
|
|
44
|
-
const postmarkErrorCode = typeof errorCode === 'number' &&
|
|
45
|
-
Number.isSafeInteger(errorCode) &&
|
|
46
|
-
errorCode >= 0
|
|
47
|
-
? errorCode
|
|
48
|
-
: undefined;
|
|
49
|
-
if (!response.ok) {
|
|
58
|
+
const postmarkErrorCode = parseOptional(postmarkResponseErrorCodeSchema, providerResponse) ??
|
|
59
|
+
parseOptional(postmarkHeaderErrorCodeSchema, response.headers.get('X-PM-ApiErrorCode'));
|
|
60
|
+
if (!response.ok ||
|
|
61
|
+
(postmarkErrorCode !== undefined && postmarkErrorCode !== 0)) {
|
|
50
62
|
const inactiveRecipient = response.status === 422 && postmarkErrorCode === 406;
|
|
51
63
|
const details = {
|
|
52
64
|
provider: 'postmark',
|
|
@@ -55,11 +67,24 @@ export async function sendEmail(data, options) {
|
|
|
55
67
|
...(postmarkErrorCode !== undefined ? { postmarkErrorCode } : {}),
|
|
56
68
|
};
|
|
57
69
|
if (inactiveRecipient) {
|
|
58
|
-
throw new EmailDeliveryError('Postmark recipient is inactive', details, true);
|
|
70
|
+
throw new EmailDeliveryError('Postmark recipient is inactive', details, true, false);
|
|
59
71
|
}
|
|
60
|
-
throw new EmailDeliveryError('Postmark rejected the email', details);
|
|
72
|
+
throw new EmailDeliveryError('Postmark rejected the email', details, false, response.status === 429 || response.status >= 500);
|
|
73
|
+
}
|
|
74
|
+
const result = postmarkSuccessResponseSchema.safeParse(providerResponse);
|
|
75
|
+
if (!result.success) {
|
|
76
|
+
throw new EmailDeliveryError('Postmark returned an invalid response', {
|
|
77
|
+
provider: 'postmark',
|
|
78
|
+
providerCategory: 'rejected',
|
|
79
|
+
httpStatus: response.status,
|
|
80
|
+
...(postmarkErrorCode !== undefined ? { postmarkErrorCode } : {}),
|
|
81
|
+
});
|
|
61
82
|
}
|
|
62
|
-
return {
|
|
83
|
+
return {
|
|
84
|
+
httpStatus: response.status,
|
|
85
|
+
messageId: result.data.MessageID,
|
|
86
|
+
submittedAt: result.data.SubmittedAt,
|
|
87
|
+
};
|
|
63
88
|
}
|
|
64
89
|
export async function settleEmailDeliveries(deliveries) {
|
|
65
90
|
const failures = (await Promise.allSettled(deliveries)).filter((result) => result.status === 'rejected');
|
|
@@ -90,12 +115,12 @@ export async function settleEmailDeliveries(deliveries) {
|
|
|
90
115
|
};
|
|
91
116
|
if (unexpectedDeliveryError instanceof EmailDeliveryError) {
|
|
92
117
|
if (unexpectedDeliveryError.message === 'Postmark is not configured') {
|
|
93
|
-
throw new EmailDeliveryError('Postmark is not configured', details);
|
|
118
|
+
throw new EmailDeliveryError('Postmark is not configured', details, false, false);
|
|
94
119
|
}
|
|
95
|
-
throw new EmailDeliveryError('Postmark rejected the email', details);
|
|
120
|
+
throw new EmailDeliveryError('Postmark rejected the email', details, false, unexpectedDeliveryError.retryable);
|
|
96
121
|
}
|
|
97
122
|
if (allExpected && firstDeliveryError instanceof EmailDeliveryError) {
|
|
98
|
-
throw new EmailDeliveryError('Postmark recipient is inactive', details, true);
|
|
123
|
+
throw new EmailDeliveryError('Postmark recipient is inactive', details, true, false);
|
|
99
124
|
}
|
|
100
125
|
throw new EmailDeliveryError('Email delivery failed', details);
|
|
101
126
|
}
|
|
@@ -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';\nimport { z } from 'zod';\n\nconst postmarkErrorCodeSchema = z.number().int().nonnegative();\nconst postmarkResponseErrorCodeSchema = z\n .object({ ErrorCode: postmarkErrorCodeSchema })\n .transform(({ ErrorCode }) => ErrorCode);\nconst postmarkHeaderErrorCodeSchema = z\n .string()\n .regex(/^\\d+$/)\n .transform(Number)\n .pipe(postmarkErrorCodeSchema);\nconst postmarkSuccessResponseSchema = z.object({\n ErrorCode: z.literal(0),\n MessageID: z.string().min(1),\n SubmittedAt: z.string().refine((value) => day(value).isValid()),\n});\n\nfunction parseOptional<T>(schema: z.ZodType<T>, value: unknown) {\n const result = schema.safeParse(value);\n if (result.success) return result.data;\n}\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 postmarkErrorCode =\n parseOptional(postmarkResponseErrorCodeSchema, providerResponse) ??\n parseOptional(\n postmarkHeaderErrorCodeSchema,\n response.headers.get('X-PM-ApiErrorCode')\n );\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 result = postmarkSuccessResponseSchema.safeParse(providerResponse);\n if (!result.success) {\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: result.data.MessageID,\n submittedAt: result.data.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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vrplatform/email",
|
|
3
|
-
"version": "1.0.2-stage.
|
|
3
|
+
"version": "1.0.2-stage.560",
|
|
4
4
|
"description": "VRTrust transactional email templates and Postmark transport",
|
|
5
5
|
"license": "ISC",
|
|
6
6
|
"main": "build/main/index.js",
|
|
@@ -30,7 +30,8 @@
|
|
|
30
30
|
"@react-email/img": "0.0.9",
|
|
31
31
|
"@react-email/preview": "0.0.10",
|
|
32
32
|
"@react-email/render": "0.0.17",
|
|
33
|
-
"@react-email/text": "0.0.9"
|
|
33
|
+
"@react-email/text": "0.0.9",
|
|
34
|
+
"zod": "^4.4.3"
|
|
34
35
|
},
|
|
35
36
|
"peerDependencies": {
|
|
36
37
|
"@vrplatform/utils": "*",
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { day } from '@vrplatform/utils';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { EmailDeliveryError } from './send-email';
|
|
4
|
+
|
|
5
|
+
const postmarkMessagesResponseSchema = z.object({
|
|
6
|
+
TotalCount: z.number().int().nonnegative(),
|
|
7
|
+
Messages: z.array(
|
|
8
|
+
z.object({
|
|
9
|
+
MessageID: z.string().min(1),
|
|
10
|
+
ReceivedAt: z.string().refine((value) => day(value).isValid()),
|
|
11
|
+
Metadata: z.object({ deliveryId: z.string() }).passthrough(),
|
|
12
|
+
})
|
|
13
|
+
),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export async function findPostmarkEmail(
|
|
17
|
+
deliveryId: string,
|
|
18
|
+
options: {
|
|
19
|
+
fetch?: (
|
|
20
|
+
input: string | URL | Request,
|
|
21
|
+
init?: RequestInit
|
|
22
|
+
) => Promise<Response>;
|
|
23
|
+
postmarkToken?: string;
|
|
24
|
+
}
|
|
25
|
+
) {
|
|
26
|
+
if (!options.postmarkToken) {
|
|
27
|
+
throw new EmailDeliveryError(
|
|
28
|
+
'Postmark is not configured',
|
|
29
|
+
undefined,
|
|
30
|
+
false,
|
|
31
|
+
false
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const url = new URL('https://api.postmarkapp.com/messages/outbound');
|
|
36
|
+
url.searchParams.set('count', '1');
|
|
37
|
+
url.searchParams.set('offset', '0');
|
|
38
|
+
url.searchParams.set('messagestream', 'outbound');
|
|
39
|
+
url.searchParams.set('metadata_deliveryId', deliveryId);
|
|
40
|
+
const response = await (options.fetch ?? fetch)(url, {
|
|
41
|
+
method: 'GET',
|
|
42
|
+
headers: {
|
|
43
|
+
Accept: 'application/json',
|
|
44
|
+
'X-Postmark-Server-Token': options.postmarkToken,
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
const providerResponse: unknown = await response.json().catch(() => ({}));
|
|
48
|
+
|
|
49
|
+
if (!response.ok) {
|
|
50
|
+
throw new EmailDeliveryError(
|
|
51
|
+
'Postmark message lookup failed',
|
|
52
|
+
{
|
|
53
|
+
provider: 'postmark',
|
|
54
|
+
providerCategory: 'rejected',
|
|
55
|
+
httpStatus: response.status,
|
|
56
|
+
},
|
|
57
|
+
false,
|
|
58
|
+
response.status === 429 || response.status >= 500
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const result = postmarkMessagesResponseSchema.safeParse(providerResponse);
|
|
63
|
+
if (!result.success) {
|
|
64
|
+
throw new EmailDeliveryError('Postmark returned an invalid response');
|
|
65
|
+
}
|
|
66
|
+
if (result.data.TotalCount === 0) return undefined;
|
|
67
|
+
|
|
68
|
+
const message = result.data.Messages.find(
|
|
69
|
+
({ Metadata }) => Metadata.deliveryId === deliveryId
|
|
70
|
+
);
|
|
71
|
+
if (!message) {
|
|
72
|
+
throw new EmailDeliveryError('Postmark returned an invalid response');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
httpStatus: response.status,
|
|
77
|
+
messageId: message.MessageID,
|
|
78
|
+
submittedAt: message.ReceivedAt,
|
|
79
|
+
};
|
|
80
|
+
}
|
package/src/utils/index.ts
CHANGED
package/src/utils/send-email.ts
CHANGED
|
@@ -1,3 +1,26 @@
|
|
|
1
|
+
import { day } from '@vrplatform/utils';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
const postmarkErrorCodeSchema = z.number().int().nonnegative();
|
|
5
|
+
const postmarkResponseErrorCodeSchema = z
|
|
6
|
+
.object({ ErrorCode: postmarkErrorCodeSchema })
|
|
7
|
+
.transform(({ ErrorCode }) => ErrorCode);
|
|
8
|
+
const postmarkHeaderErrorCodeSchema = z
|
|
9
|
+
.string()
|
|
10
|
+
.regex(/^\d+$/)
|
|
11
|
+
.transform(Number)
|
|
12
|
+
.pipe(postmarkErrorCodeSchema);
|
|
13
|
+
const postmarkSuccessResponseSchema = z.object({
|
|
14
|
+
ErrorCode: z.literal(0),
|
|
15
|
+
MessageID: z.string().min(1),
|
|
16
|
+
SubmittedAt: z.string().refine((value) => day(value).isValid()),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
function parseOptional<T>(schema: z.ZodType<T>, value: unknown) {
|
|
20
|
+
const result = schema.safeParse(value);
|
|
21
|
+
if (result.success) return result.data;
|
|
22
|
+
}
|
|
23
|
+
|
|
1
24
|
type EmailDeliveryFailureDetails = {
|
|
2
25
|
httpStatus: number;
|
|
3
26
|
postmarkErrorCode?: number;
|
|
@@ -16,7 +39,8 @@ export class EmailDeliveryError extends Error {
|
|
|
16
39
|
constructor(
|
|
17
40
|
message: string,
|
|
18
41
|
public readonly details?: EmailDeliveryErrorDetails,
|
|
19
|
-
public readonly expected = false
|
|
42
|
+
public readonly expected = false,
|
|
43
|
+
public readonly retryable = true
|
|
20
44
|
) {
|
|
21
45
|
super(message);
|
|
22
46
|
this.name = 'EmailDeliveryError';
|
|
@@ -30,6 +54,8 @@ export async function sendEmail(
|
|
|
30
54
|
text: string;
|
|
31
55
|
html: string;
|
|
32
56
|
replyTo?: string;
|
|
57
|
+
tag?: string;
|
|
58
|
+
metadata?: Record<string, string>;
|
|
33
59
|
},
|
|
34
60
|
options: {
|
|
35
61
|
fetch?: (
|
|
@@ -40,7 +66,12 @@ export async function sendEmail(
|
|
|
40
66
|
}
|
|
41
67
|
) {
|
|
42
68
|
if (!options.postmarkToken) {
|
|
43
|
-
throw new EmailDeliveryError(
|
|
69
|
+
throw new EmailDeliveryError(
|
|
70
|
+
'Postmark is not configured',
|
|
71
|
+
undefined,
|
|
72
|
+
false,
|
|
73
|
+
false
|
|
74
|
+
);
|
|
44
75
|
}
|
|
45
76
|
|
|
46
77
|
const response = await (options.fetch ?? fetch)(
|
|
@@ -60,32 +91,24 @@ export async function sendEmail(
|
|
|
60
91
|
TextBody: data.text,
|
|
61
92
|
HtmlBody: data.html,
|
|
62
93
|
MessageStream: 'outbound',
|
|
94
|
+
Tag: data.tag,
|
|
95
|
+
Metadata: data.metadata,
|
|
63
96
|
}),
|
|
64
97
|
}
|
|
65
98
|
);
|
|
66
99
|
|
|
67
100
|
const providerResponse: unknown = await response.json().catch(() => ({}));
|
|
68
|
-
const responseErrorCode =
|
|
69
|
-
providerResponse !== null &&
|
|
70
|
-
typeof providerResponse === 'object' &&
|
|
71
|
-
'ErrorCode' in providerResponse &&
|
|
72
|
-
typeof providerResponse.ErrorCode === 'number'
|
|
73
|
-
? providerResponse.ErrorCode
|
|
74
|
-
: undefined;
|
|
75
|
-
const headerErrorCode = response.headers.get('X-PM-ApiErrorCode');
|
|
76
|
-
const errorCode =
|
|
77
|
-
responseErrorCode ??
|
|
78
|
-
(headerErrorCode && /^\d+$/.test(headerErrorCode)
|
|
79
|
-
? Number(headerErrorCode)
|
|
80
|
-
: undefined);
|
|
81
101
|
const postmarkErrorCode =
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
102
|
+
parseOptional(postmarkResponseErrorCodeSchema, providerResponse) ??
|
|
103
|
+
parseOptional(
|
|
104
|
+
postmarkHeaderErrorCodeSchema,
|
|
105
|
+
response.headers.get('X-PM-ApiErrorCode')
|
|
106
|
+
);
|
|
87
107
|
|
|
88
|
-
if (
|
|
108
|
+
if (
|
|
109
|
+
!response.ok ||
|
|
110
|
+
(postmarkErrorCode !== undefined && postmarkErrorCode !== 0)
|
|
111
|
+
) {
|
|
89
112
|
const inactiveRecipient =
|
|
90
113
|
response.status === 422 && postmarkErrorCode === 406;
|
|
91
114
|
const details: EmailDeliveryFailureDetails = {
|
|
@@ -99,14 +122,34 @@ export async function sendEmail(
|
|
|
99
122
|
throw new EmailDeliveryError(
|
|
100
123
|
'Postmark recipient is inactive',
|
|
101
124
|
details,
|
|
102
|
-
true
|
|
125
|
+
true,
|
|
126
|
+
false
|
|
103
127
|
);
|
|
104
128
|
}
|
|
105
129
|
|
|
106
|
-
throw new EmailDeliveryError(
|
|
130
|
+
throw new EmailDeliveryError(
|
|
131
|
+
'Postmark rejected the email',
|
|
132
|
+
details,
|
|
133
|
+
false,
|
|
134
|
+
response.status === 429 || response.status >= 500
|
|
135
|
+
);
|
|
107
136
|
}
|
|
108
137
|
|
|
109
|
-
|
|
138
|
+
const result = postmarkSuccessResponseSchema.safeParse(providerResponse);
|
|
139
|
+
if (!result.success) {
|
|
140
|
+
throw new EmailDeliveryError('Postmark returned an invalid response', {
|
|
141
|
+
provider: 'postmark',
|
|
142
|
+
providerCategory: 'rejected',
|
|
143
|
+
httpStatus: response.status,
|
|
144
|
+
...(postmarkErrorCode !== undefined ? { postmarkErrorCode } : {}),
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
httpStatus: response.status,
|
|
150
|
+
messageId: result.data.MessageID,
|
|
151
|
+
submittedAt: result.data.SubmittedAt,
|
|
152
|
+
};
|
|
110
153
|
}
|
|
111
154
|
|
|
112
155
|
export async function settleEmailDeliveries(deliveries: Promise<unknown>[]) {
|
|
@@ -152,15 +195,26 @@ export async function settleEmailDeliveries(deliveries: Promise<unknown>[]) {
|
|
|
152
195
|
};
|
|
153
196
|
if (unexpectedDeliveryError instanceof EmailDeliveryError) {
|
|
154
197
|
if (unexpectedDeliveryError.message === 'Postmark is not configured') {
|
|
155
|
-
throw new EmailDeliveryError(
|
|
198
|
+
throw new EmailDeliveryError(
|
|
199
|
+
'Postmark is not configured',
|
|
200
|
+
details,
|
|
201
|
+
false,
|
|
202
|
+
false
|
|
203
|
+
);
|
|
156
204
|
}
|
|
157
|
-
throw new EmailDeliveryError(
|
|
205
|
+
throw new EmailDeliveryError(
|
|
206
|
+
'Postmark rejected the email',
|
|
207
|
+
details,
|
|
208
|
+
false,
|
|
209
|
+
unexpectedDeliveryError.retryable
|
|
210
|
+
);
|
|
158
211
|
}
|
|
159
212
|
if (allExpected && firstDeliveryError instanceof EmailDeliveryError) {
|
|
160
213
|
throw new EmailDeliveryError(
|
|
161
214
|
'Postmark recipient is inactive',
|
|
162
215
|
details,
|
|
163
|
-
true
|
|
216
|
+
true,
|
|
217
|
+
false
|
|
164
218
|
);
|
|
165
219
|
}
|
|
166
220
|
throw new EmailDeliveryError('Email delivery failed', details);
|