@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.
@@ -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"]}
@@ -1 +1,2 @@
1
+ export * from './find-postmark-email';
1
2
  export * from './send-email';
@@ -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
- constructor(message: string, details?: EmailDeliveryErrorDetails | undefined, expected?: boolean);
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
- status: number;
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 responseErrorCode = providerResponse !== null &&
38
- typeof providerResponse === 'object' &&
39
- 'ErrorCode' in providerResponse &&
40
- typeof providerResponse.ErrorCode === 'number'
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 { status: response.status };
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":"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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,2 @@
1
+ export * from './find-postmark-email';
1
2
  export * from './send-email';
@@ -1,2 +1,3 @@
1
+ export * from './find-postmark-email';
1
2
  export * from './send-email';
2
3
  //# sourceMappingURL=index.js.map
@@ -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
- constructor(message: string, details?: EmailDeliveryErrorDetails | undefined, expected?: boolean);
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
- status: number;
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
- constructor(message, details, expected = false) {
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 responseErrorCode = providerResponse !== null &&
34
- typeof providerResponse === 'object' &&
35
- 'ErrorCode' in providerResponse &&
36
- typeof providerResponse.ErrorCode === 'number'
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 { status: response.status };
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.551",
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
+ }
@@ -1 +1,2 @@
1
+ export * from './find-postmark-email';
1
2
  export * from './send-email';
@@ -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('Postmark is not configured');
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
- typeof errorCode === 'number' &&
83
- Number.isSafeInteger(errorCode) &&
84
- errorCode >= 0
85
- ? errorCode
86
- : undefined;
102
+ parseOptional(postmarkResponseErrorCodeSchema, providerResponse) ??
103
+ parseOptional(
104
+ postmarkHeaderErrorCodeSchema,
105
+ response.headers.get('X-PM-ApiErrorCode')
106
+ );
87
107
 
88
- if (!response.ok) {
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('Postmark rejected the email', details);
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
- return { status: response.status };
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('Postmark is not configured', details);
198
+ throw new EmailDeliveryError(
199
+ 'Postmark is not configured',
200
+ details,
201
+ false,
202
+ false
203
+ );
156
204
  }
157
- throw new EmailDeliveryError('Postmark rejected the email', details);
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);