@vrplatform/email 1.0.2-stage.446 → 1.0.2-stage.458

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.
@@ -1,7 +1,17 @@
1
+ type EmailDeliveryFailureDetails = {
2
+ httpStatus: number;
3
+ postmarkErrorCode?: number;
4
+ provider: 'postmark';
5
+ providerCategory: 'inactiveRecipient' | 'rejected';
6
+ };
7
+ type EmailDeliveryErrorDetails = EmailDeliveryFailureDetails | {
8
+ failureCount: number;
9
+ failures: EmailDeliveryFailureDetails[];
10
+ };
1
11
  export declare class EmailDeliveryError extends Error {
2
- readonly details?: Record<string, unknown> | undefined;
12
+ readonly details?: EmailDeliveryErrorDetails | undefined;
3
13
  readonly expected: boolean;
4
- constructor(message: string, details?: Record<string, unknown> | undefined, expected?: boolean);
14
+ constructor(message: string, details?: EmailDeliveryErrorDetails | undefined, expected?: boolean);
5
15
  }
6
16
  export declare function sendEmail(data: {
7
17
  to: string;
@@ -15,3 +25,5 @@ export declare function sendEmail(data: {
15
25
  }): Promise<{
16
26
  status: number;
17
27
  }>;
28
+ export declare function settleEmailDeliveries(deliveries: Promise<unknown>[]): Promise<void>;
29
+ export {};
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.EmailDeliveryError = void 0;
4
4
  exports.sendEmail = sendEmail;
5
+ exports.settleEmailDeliveries = settleEmailDeliveries;
5
6
  class EmailDeliveryError extends Error {
6
7
  constructor(message, details, expected = false) {
7
8
  super(message);
@@ -33,27 +34,73 @@ async function sendEmail(data, options) {
33
34
  }),
34
35
  });
35
36
  const providerResponse = await response.json().catch(() => ({}));
36
- const providerMessage = providerResponse !== null &&
37
+ const responseErrorCode = providerResponse !== null &&
37
38
  typeof providerResponse === 'object' &&
38
- 'Message' in providerResponse &&
39
- typeof providerResponse.Message === 'string'
40
- ? providerResponse.Message
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
41
52
  : undefined;
42
53
  if (!response.ok) {
43
- if (response.status === 422 &&
44
- providerMessage?.includes('marked as inactive')) {
45
- throw new EmailDeliveryError('Postmark recipient is inactive', {
46
- providerMessage,
47
- status: response.status,
48
- statusText: response.statusText,
49
- }, true);
54
+ const inactiveRecipient = response.status === 422 && postmarkErrorCode === 406;
55
+ const details = {
56
+ provider: 'postmark',
57
+ providerCategory: inactiveRecipient ? 'inactiveRecipient' : 'rejected',
58
+ httpStatus: response.status,
59
+ ...(postmarkErrorCode !== undefined ? { postmarkErrorCode } : {}),
60
+ };
61
+ if (inactiveRecipient) {
62
+ throw new EmailDeliveryError('Postmark recipient is inactive', details, true);
50
63
  }
51
- throw new EmailDeliveryError('Postmark rejected the email', {
52
- providerResponse,
53
- status: response.status,
54
- statusText: response.statusText,
55
- });
64
+ throw new EmailDeliveryError('Postmark rejected the email', details);
56
65
  }
57
66
  return { status: response.status };
58
67
  }
68
+ async function settleEmailDeliveries(deliveries) {
69
+ const failures = (await Promise.allSettled(deliveries)).filter((result) => result.status === 'rejected');
70
+ if (failures.length === 0)
71
+ return;
72
+ const unexpectedDeliveryError = failures.find((failure) => failure.reason instanceof EmailDeliveryError && !failure.reason.expected)?.reason;
73
+ const allExpected = failures.every((failure) => failure.reason instanceof EmailDeliveryError && failure.reason.expected);
74
+ const firstDeliveryError = failures.find((failure) => failure.reason instanceof EmailDeliveryError)?.reason;
75
+ const failureDetails = failures.flatMap((failure) => {
76
+ if (!(failure.reason instanceof EmailDeliveryError) ||
77
+ !failure.reason.details ||
78
+ !('provider' in failure.reason.details)) {
79
+ return [];
80
+ }
81
+ const { httpStatus, postmarkErrorCode, provider, providerCategory } = failure.reason.details;
82
+ return [
83
+ {
84
+ httpStatus,
85
+ provider,
86
+ providerCategory,
87
+ ...(postmarkErrorCode !== undefined ? { postmarkErrorCode } : {}),
88
+ },
89
+ ];
90
+ });
91
+ const details = {
92
+ failureCount: failures.length,
93
+ failures: failureDetails,
94
+ };
95
+ if (unexpectedDeliveryError instanceof EmailDeliveryError) {
96
+ if (unexpectedDeliveryError.message === 'Postmark is not configured') {
97
+ throw new EmailDeliveryError('Postmark is not configured', details);
98
+ }
99
+ throw new EmailDeliveryError('Postmark rejected the email', details);
100
+ }
101
+ if (allExpected && firstDeliveryError instanceof EmailDeliveryError) {
102
+ throw new EmailDeliveryError('Postmark recipient is inactive', details, true);
103
+ }
104
+ throw new EmailDeliveryError('Email delivery failed', details);
105
+ }
59
106
  //# sourceMappingURL=send-email.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"send-email.js","sourceRoot":"src/","sources":["utils/send-email.ts"],"names":[],"mappings":";;;AAWA,8BA0EC;AArFD,MAAa,kBAAmB,SAAQ,KAAK;IAC3C,YACE,OAAe,EACC,OAAiC,EACjC,WAAW,KAAK;QAEhC,KAAK,CAAC,OAAO,CAAC,CAAC;QAHC,YAAO,GAAP,OAAO,CAA0B;QACjC,aAAQ,GAAR,QAAQ,CAAQ;QAGhC,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAC;IACnC,CAAC;CACF;AATD,gDASC;AAEM,KAAK,UAAU,SAAS,CAC7B,IAMC,EACD,OAMC;IAED,IAAI,CAAC,OAAO,CAAC,aAAa,EAAE,CAAC;QAC3B,MAAM,IAAI,kBAAkB,CAAC,4BAA4B,CAAC,CAAC;IAC7D,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;SAC1B,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,eAAe,GACnB,gBAAgB,KAAK,IAAI;QACzB,OAAO,gBAAgB,KAAK,QAAQ;QACpC,SAAS,IAAI,gBAAgB;QAC7B,OAAO,gBAAgB,CAAC,OAAO,KAAK,QAAQ;QAC1C,CAAC,CAAC,gBAAgB,CAAC,OAAO;QAC1B,CAAC,CAAC,SAAS,CAAC;IAEhB,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,IACE,QAAQ,CAAC,MAAM,KAAK,GAAG;YACvB,eAAe,EAAE,QAAQ,CAAC,oBAAoB,CAAC,EAC/C,CAAC;YACD,MAAM,IAAI,kBAAkB,CAC1B,gCAAgC,EAChC;gBACE,eAAe;gBACf,MAAM,EAAE,QAAQ,CAAC,MAAM;gBACvB,UAAU,EAAE,QAAQ,CAAC,UAAU;aAChC,EACD,IAAI,CACL,CAAC;QACJ,CAAC;QAED,MAAM,IAAI,kBAAkB,CAAC,6BAA6B,EAAE;YAC1D,gBAAgB;YAChB,MAAM,EAAE,QAAQ,CAAC,MAAM;YACvB,UAAU,EAAE,QAAQ,CAAC,UAAU;SAChC,CAAC,CAAC;IACL,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,CAAC;AACrC,CAAC","sourcesContent":["export class EmailDeliveryError extends Error {\n constructor(\n message: string,\n public readonly details?: Record<string, unknown>,\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 providerMessage =\n providerResponse !== null &&\n typeof providerResponse === 'object' &&\n 'Message' in providerResponse &&\n typeof providerResponse.Message === 'string'\n ? providerResponse.Message\n : undefined;\n\n if (!response.ok) {\n if (\n response.status === 422 &&\n providerMessage?.includes('marked as inactive')\n ) {\n throw new EmailDeliveryError(\n 'Postmark recipient is inactive',\n {\n providerMessage,\n status: response.status,\n statusText: response.statusText,\n },\n true\n );\n }\n\n throw new EmailDeliveryError('Postmark rejected the email', {\n providerResponse,\n status: response.status,\n statusText: response.statusText,\n });\n }\n\n return { status: response.status };\n}\n"]}
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,7 +1,17 @@
1
+ type EmailDeliveryFailureDetails = {
2
+ httpStatus: number;
3
+ postmarkErrorCode?: number;
4
+ provider: 'postmark';
5
+ providerCategory: 'inactiveRecipient' | 'rejected';
6
+ };
7
+ type EmailDeliveryErrorDetails = EmailDeliveryFailureDetails | {
8
+ failureCount: number;
9
+ failures: EmailDeliveryFailureDetails[];
10
+ };
1
11
  export declare class EmailDeliveryError extends Error {
2
- readonly details?: Record<string, unknown> | undefined;
12
+ readonly details?: EmailDeliveryErrorDetails | undefined;
3
13
  readonly expected: boolean;
4
- constructor(message: string, details?: Record<string, unknown> | undefined, expected?: boolean);
14
+ constructor(message: string, details?: EmailDeliveryErrorDetails | undefined, expected?: boolean);
5
15
  }
6
16
  export declare function sendEmail(data: {
7
17
  to: string;
@@ -15,3 +25,5 @@ export declare function sendEmail(data: {
15
25
  }): Promise<{
16
26
  status: number;
17
27
  }>;
28
+ export declare function settleEmailDeliveries(deliveries: Promise<unknown>[]): Promise<void>;
29
+ export {};
@@ -30,27 +30,73 @@ export async function sendEmail(data, options) {
30
30
  }),
31
31
  });
32
32
  const providerResponse = await response.json().catch(() => ({}));
33
- const providerMessage = providerResponse !== null &&
33
+ const responseErrorCode = providerResponse !== null &&
34
34
  typeof providerResponse === 'object' &&
35
- 'Message' in providerResponse &&
36
- typeof providerResponse.Message === 'string'
37
- ? providerResponse.Message
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
38
48
  : undefined;
39
49
  if (!response.ok) {
40
- if (response.status === 422 &&
41
- providerMessage?.includes('marked as inactive')) {
42
- throw new EmailDeliveryError('Postmark recipient is inactive', {
43
- providerMessage,
44
- status: response.status,
45
- statusText: response.statusText,
46
- }, true);
50
+ const inactiveRecipient = response.status === 422 && postmarkErrorCode === 406;
51
+ const details = {
52
+ provider: 'postmark',
53
+ providerCategory: inactiveRecipient ? 'inactiveRecipient' : 'rejected',
54
+ httpStatus: response.status,
55
+ ...(postmarkErrorCode !== undefined ? { postmarkErrorCode } : {}),
56
+ };
57
+ if (inactiveRecipient) {
58
+ throw new EmailDeliveryError('Postmark recipient is inactive', details, true);
47
59
  }
48
- throw new EmailDeliveryError('Postmark rejected the email', {
49
- providerResponse,
50
- status: response.status,
51
- statusText: response.statusText,
52
- });
60
+ throw new EmailDeliveryError('Postmark rejected the email', details);
53
61
  }
54
62
  return { status: response.status };
55
63
  }
64
+ export async function settleEmailDeliveries(deliveries) {
65
+ const failures = (await Promise.allSettled(deliveries)).filter((result) => result.status === 'rejected');
66
+ if (failures.length === 0)
67
+ return;
68
+ const unexpectedDeliveryError = failures.find((failure) => failure.reason instanceof EmailDeliveryError && !failure.reason.expected)?.reason;
69
+ const allExpected = failures.every((failure) => failure.reason instanceof EmailDeliveryError && failure.reason.expected);
70
+ const firstDeliveryError = failures.find((failure) => failure.reason instanceof EmailDeliveryError)?.reason;
71
+ const failureDetails = failures.flatMap((failure) => {
72
+ if (!(failure.reason instanceof EmailDeliveryError) ||
73
+ !failure.reason.details ||
74
+ !('provider' in failure.reason.details)) {
75
+ return [];
76
+ }
77
+ const { httpStatus, postmarkErrorCode, provider, providerCategory } = failure.reason.details;
78
+ return [
79
+ {
80
+ httpStatus,
81
+ provider,
82
+ providerCategory,
83
+ ...(postmarkErrorCode !== undefined ? { postmarkErrorCode } : {}),
84
+ },
85
+ ];
86
+ });
87
+ const details = {
88
+ failureCount: failures.length,
89
+ failures: failureDetails,
90
+ };
91
+ if (unexpectedDeliveryError instanceof EmailDeliveryError) {
92
+ if (unexpectedDeliveryError.message === 'Postmark is not configured') {
93
+ throw new EmailDeliveryError('Postmark is not configured', details);
94
+ }
95
+ throw new EmailDeliveryError('Postmark rejected the email', details);
96
+ }
97
+ if (allExpected && firstDeliveryError instanceof EmailDeliveryError) {
98
+ throw new EmailDeliveryError('Postmark recipient is inactive', details, true);
99
+ }
100
+ throw new EmailDeliveryError('Email delivery failed', details);
101
+ }
56
102
  //# sourceMappingURL=send-email.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"send-email.js","sourceRoot":"src/","sources":["utils/send-email.ts"],"names":[],"mappings":"AAAA,MAAM,OAAO,kBAAmB,SAAQ,KAAK;IAGzB;IACA;IAHlB,YACE,OAAe,EACC,OAAiC,EACjC,WAAW,KAAK;QAEhC,KAAK,CAAC,OAAO,CAAC,CAAC;QAHC,YAAO,GAAP,OAAO,CAA0B;QACjC,aAAQ,GAAR,QAAQ,CAAQ;QAGhC,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAC;IACnC,CAAC;CACF;AAED,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,IAMC,EACD,OAMC;IAED,IAAI,CAAC,OAAO,CAAC,aAAa,EAAE,CAAC;QAC3B,MAAM,IAAI,kBAAkB,CAAC,4BAA4B,CAAC,CAAC;IAC7D,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;SAC1B,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,eAAe,GACnB,gBAAgB,KAAK,IAAI;QACzB,OAAO,gBAAgB,KAAK,QAAQ;QACpC,SAAS,IAAI,gBAAgB;QAC7B,OAAO,gBAAgB,CAAC,OAAO,KAAK,QAAQ;QAC1C,CAAC,CAAC,gBAAgB,CAAC,OAAO;QAC1B,CAAC,CAAC,SAAS,CAAC;IAEhB,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,IACE,QAAQ,CAAC,MAAM,KAAK,GAAG;YACvB,eAAe,EAAE,QAAQ,CAAC,oBAAoB,CAAC,EAC/C,CAAC;YACD,MAAM,IAAI,kBAAkB,CAC1B,gCAAgC,EAChC;gBACE,eAAe;gBACf,MAAM,EAAE,QAAQ,CAAC,MAAM;gBACvB,UAAU,EAAE,QAAQ,CAAC,UAAU;aAChC,EACD,IAAI,CACL,CAAC;QACJ,CAAC;QAED,MAAM,IAAI,kBAAkB,CAAC,6BAA6B,EAAE;YAC1D,gBAAgB;YAChB,MAAM,EAAE,QAAQ,CAAC,MAAM;YACvB,UAAU,EAAE,QAAQ,CAAC,UAAU;SAChC,CAAC,CAAC;IACL,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,CAAC;AACrC,CAAC","sourcesContent":["export class EmailDeliveryError extends Error {\n constructor(\n message: string,\n public readonly details?: Record<string, unknown>,\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 providerMessage =\n providerResponse !== null &&\n typeof providerResponse === 'object' &&\n 'Message' in providerResponse &&\n typeof providerResponse.Message === 'string'\n ? providerResponse.Message\n : undefined;\n\n if (!response.ok) {\n if (\n response.status === 422 &&\n providerMessage?.includes('marked as inactive')\n ) {\n throw new EmailDeliveryError(\n 'Postmark recipient is inactive',\n {\n providerMessage,\n status: response.status,\n statusText: response.statusText,\n },\n true\n );\n }\n\n throw new EmailDeliveryError('Postmark rejected the email', {\n providerResponse,\n status: response.status,\n statusText: response.statusText,\n });\n }\n\n return { status: response.status };\n}\n"]}
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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vrplatform/email",
3
- "version": "1.0.2-stage.446",
3
+ "version": "1.0.2-stage.458",
4
4
  "description": "VRTrust transactional email templates and Postmark transport",
5
5
  "license": "ISC",
6
6
  "main": "build/main/index.js",
@@ -1,7 +1,21 @@
1
+ type EmailDeliveryFailureDetails = {
2
+ httpStatus: number;
3
+ postmarkErrorCode?: number;
4
+ provider: 'postmark';
5
+ providerCategory: 'inactiveRecipient' | 'rejected';
6
+ };
7
+
8
+ type EmailDeliveryErrorDetails =
9
+ | EmailDeliveryFailureDetails
10
+ | {
11
+ failureCount: number;
12
+ failures: EmailDeliveryFailureDetails[];
13
+ };
14
+
1
15
  export class EmailDeliveryError extends Error {
2
16
  constructor(
3
17
  message: string,
4
- public readonly details?: Record<string, unknown>,
18
+ public readonly details?: EmailDeliveryErrorDetails,
5
19
  public readonly expected = false
6
20
  ) {
7
21
  super(message);
@@ -51,36 +65,103 @@ export async function sendEmail(
51
65
  );
52
66
 
53
67
  const providerResponse: unknown = await response.json().catch(() => ({}));
54
- const providerMessage =
68
+ const responseErrorCode =
55
69
  providerResponse !== null &&
56
70
  typeof providerResponse === 'object' &&
57
- 'Message' in providerResponse &&
58
- typeof providerResponse.Message === 'string'
59
- ? providerResponse.Message
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
+ const postmarkErrorCode =
82
+ typeof errorCode === 'number' &&
83
+ Number.isSafeInteger(errorCode) &&
84
+ errorCode >= 0
85
+ ? errorCode
60
86
  : undefined;
61
87
 
62
88
  if (!response.ok) {
63
- if (
64
- response.status === 422 &&
65
- providerMessage?.includes('marked as inactive')
66
- ) {
89
+ const inactiveRecipient =
90
+ response.status === 422 && postmarkErrorCode === 406;
91
+ const details: EmailDeliveryFailureDetails = {
92
+ provider: 'postmark',
93
+ providerCategory: inactiveRecipient ? 'inactiveRecipient' : 'rejected',
94
+ httpStatus: response.status,
95
+ ...(postmarkErrorCode !== undefined ? { postmarkErrorCode } : {}),
96
+ };
97
+
98
+ if (inactiveRecipient) {
67
99
  throw new EmailDeliveryError(
68
100
  'Postmark recipient is inactive',
69
- {
70
- providerMessage,
71
- status: response.status,
72
- statusText: response.statusText,
73
- },
101
+ details,
74
102
  true
75
103
  );
76
104
  }
77
105
 
78
- throw new EmailDeliveryError('Postmark rejected the email', {
79
- providerResponse,
80
- status: response.status,
81
- statusText: response.statusText,
82
- });
106
+ throw new EmailDeliveryError('Postmark rejected the email', details);
83
107
  }
84
108
 
85
109
  return { status: response.status };
86
110
  }
111
+
112
+ export async function settleEmailDeliveries(deliveries: Promise<unknown>[]) {
113
+ const failures = (await Promise.allSettled(deliveries)).filter(
114
+ (result) => result.status === 'rejected'
115
+ );
116
+ if (failures.length === 0) return;
117
+
118
+ const unexpectedDeliveryError = failures.find(
119
+ (failure) =>
120
+ failure.reason instanceof EmailDeliveryError && !failure.reason.expected
121
+ )?.reason;
122
+ const allExpected = failures.every(
123
+ (failure) =>
124
+ failure.reason instanceof EmailDeliveryError && failure.reason.expected
125
+ );
126
+ const firstDeliveryError = failures.find(
127
+ (failure) => failure.reason instanceof EmailDeliveryError
128
+ )?.reason;
129
+ const failureDetails = failures.flatMap((failure) => {
130
+ if (
131
+ !(failure.reason instanceof EmailDeliveryError) ||
132
+ !failure.reason.details ||
133
+ !('provider' in failure.reason.details)
134
+ ) {
135
+ return [];
136
+ }
137
+ const { httpStatus, postmarkErrorCode, provider, providerCategory } =
138
+ failure.reason.details;
139
+ return [
140
+ {
141
+ httpStatus,
142
+ provider,
143
+ providerCategory,
144
+ ...(postmarkErrorCode !== undefined ? { postmarkErrorCode } : {}),
145
+ },
146
+ ];
147
+ });
148
+
149
+ const details: EmailDeliveryErrorDetails = {
150
+ failureCount: failures.length,
151
+ failures: failureDetails,
152
+ };
153
+ if (unexpectedDeliveryError instanceof EmailDeliveryError) {
154
+ if (unexpectedDeliveryError.message === 'Postmark is not configured') {
155
+ throw new EmailDeliveryError('Postmark is not configured', details);
156
+ }
157
+ throw new EmailDeliveryError('Postmark rejected the email', details);
158
+ }
159
+ if (allExpected && firstDeliveryError instanceof EmailDeliveryError) {
160
+ throw new EmailDeliveryError(
161
+ 'Postmark recipient is inactive',
162
+ details,
163
+ true
164
+ );
165
+ }
166
+ throw new EmailDeliveryError('Email delivery failed', details);
167
+ }