@vrplatform/email 1.0.2-stage.557 → 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 +2 -2
- package/build/main/utils/find-postmark-email.js +19 -30
- package/build/main/utils/find-postmark-email.js.map +1 -1
- package/build/main/utils/index.d.ts +0 -1
- package/build/main/utils/index.js +0 -1
- package/build/main/utils/index.js.map +1 -1
- package/build/main/utils/send-email.js +26 -33
- package/build/main/utils/send-email.js.map +1 -1
- package/build/module/utils/find-postmark-email.d.ts +2 -2
- package/build/module/utils/find-postmark-email.js +19 -30
- package/build/module/utils/find-postmark-email.js.map +1 -1
- package/build/module/utils/index.d.ts +0 -1
- package/build/module/utils/index.js +0 -1
- package/build/module/utils/index.js.map +1 -1
- package/build/module/utils/send-email.js +26 -33
- package/build/module/utils/send-email.js.map +1 -1
- package/package.json +3 -2
- package/src/utils/find-postmark-email.ts +23 -36
- package/src/utils/index.ts +0 -1
- package/src/utils/send-email.ts +30 -37
|
@@ -2,7 +2,16 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.findPostmarkEmail = findPostmarkEmail;
|
|
4
4
|
const utils_1 = require("@vrplatform/utils");
|
|
5
|
+
const zod_1 = require("zod");
|
|
5
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
|
+
});
|
|
6
15
|
async function findPostmarkEmail(deliveryId, options) {
|
|
7
16
|
if (!options.postmarkToken) {
|
|
8
17
|
throw new send_email_1.EmailDeliveryError('Postmark is not configured', undefined, false, false);
|
|
@@ -27,40 +36,20 @@ async function findPostmarkEmail(deliveryId, options) {
|
|
|
27
36
|
httpStatus: response.status,
|
|
28
37
|
}, false, response.status === 429 || response.status >= 500);
|
|
29
38
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
!('TotalCount' in providerResponse) ||
|
|
33
|
-
typeof providerResponse.TotalCount !== 'number' ||
|
|
34
|
-
!('Messages' in providerResponse) ||
|
|
35
|
-
!Array.isArray(providerResponse.Messages)) {
|
|
39
|
+
const result = postmarkMessagesResponseSchema.safeParse(providerResponse);
|
|
40
|
+
if (!result.success) {
|
|
36
41
|
throw new send_email_1.EmailDeliveryError('Postmark returned an invalid response');
|
|
37
42
|
}
|
|
38
|
-
if (
|
|
43
|
+
if (result.data.TotalCount === 0)
|
|
39
44
|
return undefined;
|
|
40
|
-
const message =
|
|
41
|
-
|
|
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) {
|
|
45
|
+
const message = result.data.Messages.find(({ Metadata }) => Metadata.deliveryId === deliveryId);
|
|
46
|
+
if (!message) {
|
|
62
47
|
throw new send_email_1.EmailDeliveryError('Postmark returned an invalid response');
|
|
63
48
|
}
|
|
64
|
-
return {
|
|
49
|
+
return {
|
|
50
|
+
httpStatus: response.status,
|
|
51
|
+
messageId: message.MessageID,
|
|
52
|
+
submittedAt: message.ReceivedAt,
|
|
53
|
+
};
|
|
65
54
|
}
|
|
66
55
|
//# sourceMappingURL=find-postmark-email.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"find-postmark-email.js","sourceRoot":"src/","sources":["utils/find-postmark-email.ts"],"names":[],"mappings":";;
|
|
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"]}
|
|
@@ -15,6 +15,5 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
__exportStar(require("./find-postmark-email"), exports);
|
|
18
|
-
__exportStar(require("./find-postmark-email"), exports);
|
|
19
18
|
__exportStar(require("./send-email"), exports);
|
|
20
19
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"src/","sources":["utils/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,wDAAsC;AACtC
|
|
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"]}
|
|
@@ -4,6 +4,26 @@ exports.EmailDeliveryError = void 0;
|
|
|
4
4
|
exports.sendEmail = sendEmail;
|
|
5
5
|
exports.settleEmailDeliveries = settleEmailDeliveries;
|
|
6
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
|
+
}
|
|
7
27
|
class EmailDeliveryError extends Error {
|
|
8
28
|
constructor(message, details, expected = false, retryable = true) {
|
|
9
29
|
super(message);
|
|
@@ -38,22 +58,8 @@ async function sendEmail(data, options) {
|
|
|
38
58
|
}),
|
|
39
59
|
});
|
|
40
60
|
const providerResponse = await response.json().catch(() => ({}));
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
'ErrorCode' in providerResponse &&
|
|
44
|
-
typeof providerResponse.ErrorCode === 'number'
|
|
45
|
-
? providerResponse.ErrorCode
|
|
46
|
-
: undefined;
|
|
47
|
-
const headerErrorCode = response.headers.get('X-PM-ApiErrorCode');
|
|
48
|
-
const errorCode = responseErrorCode ??
|
|
49
|
-
(headerErrorCode && /^\d+$/.test(headerErrorCode)
|
|
50
|
-
? Number(headerErrorCode)
|
|
51
|
-
: undefined);
|
|
52
|
-
const postmarkErrorCode = typeof errorCode === 'number' &&
|
|
53
|
-
Number.isSafeInteger(errorCode) &&
|
|
54
|
-
errorCode >= 0
|
|
55
|
-
? errorCode
|
|
56
|
-
: undefined;
|
|
61
|
+
const postmarkErrorCode = parseOptional(postmarkResponseErrorCodeSchema, providerResponse) ??
|
|
62
|
+
parseOptional(postmarkHeaderErrorCodeSchema, response.headers.get('X-PM-ApiErrorCode'));
|
|
57
63
|
if (!response.ok ||
|
|
58
64
|
(postmarkErrorCode !== undefined && postmarkErrorCode !== 0)) {
|
|
59
65
|
const inactiveRecipient = response.status === 422 && postmarkErrorCode === 406;
|
|
@@ -68,21 +74,8 @@ async function sendEmail(data, options) {
|
|
|
68
74
|
}
|
|
69
75
|
throw new EmailDeliveryError('Postmark rejected the email', details, false, response.status === 429 || response.status >= 500);
|
|
70
76
|
}
|
|
71
|
-
const
|
|
72
|
-
|
|
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) {
|
|
77
|
+
const result = postmarkSuccessResponseSchema.safeParse(providerResponse);
|
|
78
|
+
if (!result.success) {
|
|
86
79
|
throw new EmailDeliveryError('Postmark returned an invalid response', {
|
|
87
80
|
provider: 'postmark',
|
|
88
81
|
providerCategory: 'rejected',
|
|
@@ -92,8 +85,8 @@ async function sendEmail(data, options) {
|
|
|
92
85
|
}
|
|
93
86
|
return {
|
|
94
87
|
httpStatus: response.status,
|
|
95
|
-
messageId,
|
|
96
|
-
submittedAt,
|
|
88
|
+
messageId: result.data.MessageID,
|
|
89
|
+
submittedAt: result.data.SubmittedAt,
|
|
97
90
|
};
|
|
98
91
|
}
|
|
99
92
|
async function settleEmailDeliveries(deliveries) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"send-email.js","sourceRoot":"src/","sources":["utils/send-email.ts"],"names":[],"mappings":";;;AA4BA,8BAmIC;AAED,sDAkEC;AAnOD,6CAAwC;AAgBxC,MAAa,kBAAmB,SAAQ,KAAK;IAC3C,YACE,OAAe,EACC,OAAmC,EACnC,WAAW,KAAK,EAChB,YAAY,IAAI;QAEhC,KAAK,CAAC,OAAO,CAAC,CAAC;QAJC,YAAO,GAAP,OAAO,CAA4B;QACnC,aAAQ,GAAR,QAAQ,CAAQ;QAChB,cAAS,GAAT,SAAS,CAAO;QAGhC,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAC;IACnC,CAAC;CACF;AAVD,gDAUC;AAEM,KAAK,UAAU,SAAS,CAC7B,IAQC,EACD,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,QAAQ,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK,IAAI,KAAK,CAAC,CAC7C,mCAAmC,EACnC;QACE,MAAM,EAAE,MAAM;QACd,OAAO,EAAE;YACP,MAAM,EAAE,kBAAkB;YAC1B,cAAc,EAAE,kBAAkB;YAClC,yBAAyB,EAAE,OAAO,CAAC,aAAa;SACjD;QACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;YACnB,IAAI,EAAE,2CAA2C;YACjD,EAAE,EAAE,IAAI,CAAC,EAAE;YACX,OAAO,EAAE,IAAI,CAAC,OAAO;YACrB,OAAO,EAAE,IAAI,CAAC,OAAO;YACrB,QAAQ,EAAE,IAAI,CAAC,IAAI;YACnB,QAAQ,EAAE,IAAI,CAAC,IAAI;YACnB,aAAa,EAAE,UAAU;YACzB,GAAG,EAAE,IAAI,CAAC,GAAG;YACb,QAAQ,EAAE,IAAI,CAAC,QAAQ;SACxB,CAAC;KACH,CACF,CAAC;IAEF,MAAM,gBAAgB,GAAY,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAC1E,MAAM,iBAAiB,GACrB,gBAAgB,KAAK,IAAI;QACzB,OAAO,gBAAgB,KAAK,QAAQ;QACpC,WAAW,IAAI,gBAAgB;QAC/B,OAAO,gBAAgB,CAAC,SAAS,KAAK,QAAQ;QAC5C,CAAC,CAAC,gBAAgB,CAAC,SAAS;QAC5B,CAAC,CAAC,SAAS,CAAC;IAChB,MAAM,eAAe,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC;IAClE,MAAM,SAAS,GACb,iBAAiB;QACjB,CAAC,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,eAAe,CAAC;YAC/C,CAAC,CAAC,MAAM,CAAC,eAAe,CAAC;YACzB,CAAC,CAAC,SAAS,CAAC,CAAC;IACjB,MAAM,iBAAiB,GACrB,OAAO,SAAS,KAAK,QAAQ;QAC7B,MAAM,CAAC,aAAa,CAAC,SAAS,CAAC;QAC/B,SAAS,IAAI,CAAC;QACZ,CAAC,CAAC,SAAS;QACX,CAAC,CAAC,SAAS,CAAC;IAEhB,IACE,CAAC,QAAQ,CAAC,EAAE;QACZ,CAAC,iBAAiB,KAAK,SAAS,IAAI,iBAAiB,KAAK,CAAC,CAAC,EAC5D,CAAC;QACD,MAAM,iBAAiB,GACrB,QAAQ,CAAC,MAAM,KAAK,GAAG,IAAI,iBAAiB,KAAK,GAAG,CAAC;QACvD,MAAM,OAAO,GAAgC;YAC3C,QAAQ,EAAE,UAAU;YACpB,gBAAgB,EAAE,iBAAiB,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,UAAU;YACtE,UAAU,EAAE,QAAQ,CAAC,MAAM;YAC3B,GAAG,CAAC,iBAAiB,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,iBAAiB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAClE,CAAC;QAEF,IAAI,iBAAiB,EAAE,CAAC;YACtB,MAAM,IAAI,kBAAkB,CAC1B,gCAAgC,EAChC,OAAO,EACP,IAAI,EACJ,KAAK,CACN,CAAC;QACJ,CAAC;QAED,MAAM,IAAI,kBAAkB,CAC1B,6BAA6B,EAC7B,OAAO,EACP,KAAK,EACL,QAAQ,CAAC,MAAM,KAAK,GAAG,IAAI,QAAQ,CAAC,MAAM,IAAI,GAAG,CAClD,CAAC;IACJ,CAAC;IAED,MAAM,SAAS,GACb,gBAAgB,KAAK,IAAI;QACzB,OAAO,gBAAgB,KAAK,QAAQ;QACpC,WAAW,IAAI,gBAAgB;QAC/B,OAAO,gBAAgB,CAAC,SAAS,KAAK,QAAQ;QAC9C,gBAAgB,CAAC,SAAS;QACxB,CAAC,CAAC,gBAAgB,CAAC,SAAS;QAC5B,CAAC,CAAC,SAAS,CAAC;IAChB,MAAM,WAAW,GACf,gBAAgB,KAAK,IAAI;QACzB,OAAO,gBAAgB,KAAK,QAAQ;QACpC,aAAa,IAAI,gBAAgB;QACjC,OAAO,gBAAgB,CAAC,WAAW,KAAK,QAAQ;QAChD,IAAA,WAAG,EAAC,gBAAgB,CAAC,WAAW,CAAC,CAAC,OAAO,EAAE;QACzC,CAAC,CAAC,gBAAgB,CAAC,WAAW;QAC9B,CAAC,CAAC,SAAS,CAAC;IAChB,IAAI,CAAC,SAAS,IAAI,CAAC,WAAW,IAAI,iBAAiB,KAAK,CAAC,EAAE,CAAC;QAC1D,MAAM,IAAI,kBAAkB,CAAC,uCAAuC,EAAE;YACpE,QAAQ,EAAE,UAAU;YACpB,gBAAgB,EAAE,UAAU;YAC5B,UAAU,EAAE,QAAQ,CAAC,MAAM;YAC3B,GAAG,CAAC,iBAAiB,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,iBAAiB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAClE,CAAC,CAAC;IACL,CAAC;IAED,OAAO;QACL,UAAU,EAAE,QAAQ,CAAC,MAAM;QAC3B,SAAS;QACT,WAAW;KACZ,CAAC;AACJ,CAAC;AAEM,KAAK,UAAU,qBAAqB,CAAC,UAA8B;IACxE,MAAM,QAAQ,GAAG,CAAC,MAAM,OAAO,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC,CAAC,MAAM,CAC5D,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,KAAK,UAAU,CACzC,CAAC;IACF,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO;IAElC,MAAM,uBAAuB,GAAG,QAAQ,CAAC,IAAI,CAC3C,CAAC,OAAO,EAAE,EAAE,CACV,OAAO,CAAC,MAAM,YAAY,kBAAkB,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAC3E,EAAE,MAAM,CAAC;IACV,MAAM,WAAW,GAAG,QAAQ,CAAC,KAAK,CAChC,CAAC,OAAO,EAAE,EAAE,CACV,OAAO,CAAC,MAAM,YAAY,kBAAkB,IAAI,OAAO,CAAC,MAAM,CAAC,QAAQ,CAC1E,CAAC;IACF,MAAM,kBAAkB,GAAG,QAAQ,CAAC,IAAI,CACtC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,YAAY,kBAAkB,CAC1D,EAAE,MAAM,CAAC;IACV,MAAM,cAAc,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAClD,IACE,CAAC,CAAC,OAAO,CAAC,MAAM,YAAY,kBAAkB,CAAC;YAC/C,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO;YACvB,CAAC,CAAC,UAAU,IAAI,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,EACvC,CAAC;YACD,OAAO,EAAE,CAAC;QACZ,CAAC;QACD,MAAM,EAAE,UAAU,EAAE,iBAAiB,EAAE,QAAQ,EAAE,gBAAgB,EAAE,GACjE,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC;QACzB,OAAO;YACL;gBACE,UAAU;gBACV,QAAQ;gBACR,gBAAgB;gBAChB,GAAG,CAAC,iBAAiB,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,iBAAiB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aAClE;SACF,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,MAAM,OAAO,GAA8B;QACzC,YAAY,EAAE,QAAQ,CAAC,MAAM;QAC7B,QAAQ,EAAE,cAAc;KACzB,CAAC;IACF,IAAI,uBAAuB,YAAY,kBAAkB,EAAE,CAAC;QAC1D,IAAI,uBAAuB,CAAC,OAAO,KAAK,4BAA4B,EAAE,CAAC;YACrE,MAAM,IAAI,kBAAkB,CAC1B,4BAA4B,EAC5B,OAAO,EACP,KAAK,EACL,KAAK,CACN,CAAC;QACJ,CAAC;QACD,MAAM,IAAI,kBAAkB,CAC1B,6BAA6B,EAC7B,OAAO,EACP,KAAK,EACL,uBAAuB,CAAC,SAAS,CAClC,CAAC;IACJ,CAAC;IACD,IAAI,WAAW,IAAI,kBAAkB,YAAY,kBAAkB,EAAE,CAAC;QACpE,MAAM,IAAI,kBAAkB,CAC1B,gCAAgC,EAChC,OAAO,EACP,IAAI,EACJ,KAAK,CACN,CAAC;IACJ,CAAC;IACD,MAAM,IAAI,kBAAkB,CAAC,uBAAuB,EAAE,OAAO,CAAC,CAAC;AACjE,CAAC","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"]}
|
|
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"]}
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import { day } from '@vrplatform/utils';
|
|
2
|
+
import { z } from 'zod';
|
|
2
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
|
+
});
|
|
3
12
|
export async function findPostmarkEmail(deliveryId, options) {
|
|
4
13
|
if (!options.postmarkToken) {
|
|
5
14
|
throw new EmailDeliveryError('Postmark is not configured', undefined, false, false);
|
|
@@ -24,40 +33,20 @@ export async function findPostmarkEmail(deliveryId, options) {
|
|
|
24
33
|
httpStatus: response.status,
|
|
25
34
|
}, false, response.status === 429 || response.status >= 500);
|
|
26
35
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
!('TotalCount' in providerResponse) ||
|
|
30
|
-
typeof providerResponse.TotalCount !== 'number' ||
|
|
31
|
-
!('Messages' in providerResponse) ||
|
|
32
|
-
!Array.isArray(providerResponse.Messages)) {
|
|
36
|
+
const result = postmarkMessagesResponseSchema.safeParse(providerResponse);
|
|
37
|
+
if (!result.success) {
|
|
33
38
|
throw new EmailDeliveryError('Postmark returned an invalid response');
|
|
34
39
|
}
|
|
35
|
-
if (
|
|
40
|
+
if (result.data.TotalCount === 0)
|
|
36
41
|
return undefined;
|
|
37
|
-
const message =
|
|
38
|
-
|
|
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) {
|
|
42
|
+
const message = result.data.Messages.find(({ Metadata }) => Metadata.deliveryId === deliveryId);
|
|
43
|
+
if (!message) {
|
|
59
44
|
throw new EmailDeliveryError('Postmark returned an invalid response');
|
|
60
45
|
}
|
|
61
|
-
return {
|
|
46
|
+
return {
|
|
47
|
+
httpStatus: response.status,
|
|
48
|
+
messageId: message.MessageID,
|
|
49
|
+
submittedAt: message.ReceivedAt,
|
|
50
|
+
};
|
|
62
51
|
}
|
|
63
52
|
//# sourceMappingURL=find-postmark-email.js.map
|
|
@@ -1 +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,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAElD,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,
|
|
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"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"src/","sources":["utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAC;AACtC,cAAc,
|
|
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"]}
|
|
@@ -1,4 +1,24 @@
|
|
|
1
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
|
+
}
|
|
2
22
|
export class EmailDeliveryError extends Error {
|
|
3
23
|
details;
|
|
4
24
|
expected;
|
|
@@ -35,22 +55,8 @@ export async function sendEmail(data, options) {
|
|
|
35
55
|
}),
|
|
36
56
|
});
|
|
37
57
|
const providerResponse = await response.json().catch(() => ({}));
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
'ErrorCode' in providerResponse &&
|
|
41
|
-
typeof providerResponse.ErrorCode === 'number'
|
|
42
|
-
? providerResponse.ErrorCode
|
|
43
|
-
: undefined;
|
|
44
|
-
const headerErrorCode = response.headers.get('X-PM-ApiErrorCode');
|
|
45
|
-
const errorCode = responseErrorCode ??
|
|
46
|
-
(headerErrorCode && /^\d+$/.test(headerErrorCode)
|
|
47
|
-
? Number(headerErrorCode)
|
|
48
|
-
: undefined);
|
|
49
|
-
const postmarkErrorCode = typeof errorCode === 'number' &&
|
|
50
|
-
Number.isSafeInteger(errorCode) &&
|
|
51
|
-
errorCode >= 0
|
|
52
|
-
? errorCode
|
|
53
|
-
: undefined;
|
|
58
|
+
const postmarkErrorCode = parseOptional(postmarkResponseErrorCodeSchema, providerResponse) ??
|
|
59
|
+
parseOptional(postmarkHeaderErrorCodeSchema, response.headers.get('X-PM-ApiErrorCode'));
|
|
54
60
|
if (!response.ok ||
|
|
55
61
|
(postmarkErrorCode !== undefined && postmarkErrorCode !== 0)) {
|
|
56
62
|
const inactiveRecipient = response.status === 422 && postmarkErrorCode === 406;
|
|
@@ -65,21 +71,8 @@ export async function sendEmail(data, options) {
|
|
|
65
71
|
}
|
|
66
72
|
throw new EmailDeliveryError('Postmark rejected the email', details, false, response.status === 429 || response.status >= 500);
|
|
67
73
|
}
|
|
68
|
-
const
|
|
69
|
-
|
|
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) {
|
|
74
|
+
const result = postmarkSuccessResponseSchema.safeParse(providerResponse);
|
|
75
|
+
if (!result.success) {
|
|
83
76
|
throw new EmailDeliveryError('Postmark returned an invalid response', {
|
|
84
77
|
provider: 'postmark',
|
|
85
78
|
providerCategory: 'rejected',
|
|
@@ -89,8 +82,8 @@ export async function sendEmail(data, options) {
|
|
|
89
82
|
}
|
|
90
83
|
return {
|
|
91
84
|
httpStatus: response.status,
|
|
92
|
-
messageId,
|
|
93
|
-
submittedAt,
|
|
85
|
+
messageId: result.data.MessageID,
|
|
86
|
+
submittedAt: result.data.SubmittedAt,
|
|
94
87
|
};
|
|
95
88
|
}
|
|
96
89
|
export async function settleEmailDeliveries(deliveries) {
|
|
@@ -1 +1 @@
|
|
|
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"]}
|
|
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": "*",
|
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
import { day } from '@vrplatform/utils';
|
|
2
|
+
import { z } from 'zod';
|
|
2
3
|
import { EmailDeliveryError } from './send-email';
|
|
3
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
|
+
|
|
4
16
|
export async function findPostmarkEmail(
|
|
5
17
|
deliveryId: string,
|
|
6
18
|
options: {
|
|
@@ -47,47 +59,22 @@ export async function findPostmarkEmail(
|
|
|
47
59
|
);
|
|
48
60
|
}
|
|
49
61
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
typeof providerResponse !== 'object' ||
|
|
53
|
-
!('TotalCount' in providerResponse) ||
|
|
54
|
-
typeof providerResponse.TotalCount !== 'number' ||
|
|
55
|
-
!('Messages' in providerResponse) ||
|
|
56
|
-
!Array.isArray(providerResponse.Messages)
|
|
57
|
-
) {
|
|
62
|
+
const result = postmarkMessagesResponseSchema.safeParse(providerResponse);
|
|
63
|
+
if (!result.success) {
|
|
58
64
|
throw new EmailDeliveryError('Postmark returned an invalid response');
|
|
59
65
|
}
|
|
60
|
-
if (
|
|
66
|
+
if (result.data.TotalCount === 0) return undefined;
|
|
61
67
|
|
|
62
|
-
const message =
|
|
63
|
-
(
|
|
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
|
|
68
|
+
const message = result.data.Messages.find(
|
|
69
|
+
({ Metadata }) => Metadata.deliveryId === deliveryId
|
|
71
70
|
);
|
|
72
|
-
|
|
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) {
|
|
71
|
+
if (!message) {
|
|
89
72
|
throw new EmailDeliveryError('Postmark returned an invalid response');
|
|
90
73
|
}
|
|
91
74
|
|
|
92
|
-
return {
|
|
75
|
+
return {
|
|
76
|
+
httpStatus: response.status,
|
|
77
|
+
messageId: message.MessageID,
|
|
78
|
+
submittedAt: message.ReceivedAt,
|
|
79
|
+
};
|
|
93
80
|
}
|
package/src/utils/index.ts
CHANGED
package/src/utils/send-email.ts
CHANGED
|
@@ -1,4 +1,25 @@
|
|
|
1
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
|
+
}
|
|
2
23
|
|
|
3
24
|
type EmailDeliveryFailureDetails = {
|
|
4
25
|
httpStatus: number;
|
|
@@ -77,25 +98,12 @@ export async function sendEmail(
|
|
|
77
98
|
);
|
|
78
99
|
|
|
79
100
|
const providerResponse: unknown = await response.json().catch(() => ({}));
|
|
80
|
-
const responseErrorCode =
|
|
81
|
-
providerResponse !== null &&
|
|
82
|
-
typeof providerResponse === 'object' &&
|
|
83
|
-
'ErrorCode' in providerResponse &&
|
|
84
|
-
typeof providerResponse.ErrorCode === 'number'
|
|
85
|
-
? providerResponse.ErrorCode
|
|
86
|
-
: undefined;
|
|
87
|
-
const headerErrorCode = response.headers.get('X-PM-ApiErrorCode');
|
|
88
|
-
const errorCode =
|
|
89
|
-
responseErrorCode ??
|
|
90
|
-
(headerErrorCode && /^\d+$/.test(headerErrorCode)
|
|
91
|
-
? Number(headerErrorCode)
|
|
92
|
-
: undefined);
|
|
93
101
|
const postmarkErrorCode =
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
102
|
+
parseOptional(postmarkResponseErrorCodeSchema, providerResponse) ??
|
|
103
|
+
parseOptional(
|
|
104
|
+
postmarkHeaderErrorCodeSchema,
|
|
105
|
+
response.headers.get('X-PM-ApiErrorCode')
|
|
106
|
+
);
|
|
99
107
|
|
|
100
108
|
if (
|
|
101
109
|
!response.ok ||
|
|
@@ -127,23 +135,8 @@ export async function sendEmail(
|
|
|
127
135
|
);
|
|
128
136
|
}
|
|
129
137
|
|
|
130
|
-
const
|
|
131
|
-
|
|
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) {
|
|
138
|
+
const result = postmarkSuccessResponseSchema.safeParse(providerResponse);
|
|
139
|
+
if (!result.success) {
|
|
147
140
|
throw new EmailDeliveryError('Postmark returned an invalid response', {
|
|
148
141
|
provider: 'postmark',
|
|
149
142
|
providerCategory: 'rejected',
|
|
@@ -154,8 +147,8 @@ export async function sendEmail(
|
|
|
154
147
|
|
|
155
148
|
return {
|
|
156
149
|
httpStatus: response.status,
|
|
157
|
-
messageId,
|
|
158
|
-
submittedAt,
|
|
150
|
+
messageId: result.data.MessageID,
|
|
151
|
+
submittedAt: result.data.SubmittedAt,
|
|
159
152
|
};
|
|
160
153
|
}
|
|
161
154
|
|