@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.
@@ -3,6 +3,6 @@ export declare function findPostmarkEmail(deliveryId: string, options: {
3
3
  postmarkToken?: string;
4
4
  }): Promise<{
5
5
  httpStatus: number;
6
- messageId: any;
7
- submittedAt: any;
6
+ messageId: string;
7
+ submittedAt: string;
8
8
  } | undefined>;
@@ -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
- if (!providerResponse ||
31
- typeof providerResponse !== 'object' ||
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 (providerResponse.TotalCount === 0)
43
+ if (result.data.TotalCount === 0)
39
44
  return undefined;
40
- const message = providerResponse.Messages.find((value) => value &&
41
- typeof value === 'object' &&
42
- 'Metadata' in value &&
43
- value.Metadata &&
44
- typeof value.Metadata === 'object' &&
45
- 'deliveryId' in value.Metadata &&
46
- value.Metadata.deliveryId === deliveryId);
47
- const messageId = message &&
48
- typeof message === 'object' &&
49
- 'MessageID' in message &&
50
- typeof message.MessageID === 'string' &&
51
- message.MessageID
52
- ? message.MessageID
53
- : undefined;
54
- const submittedAt = message &&
55
- typeof message === 'object' &&
56
- 'ReceivedAt' in message &&
57
- typeof message.ReceivedAt === 'string' &&
58
- (0, utils_1.day)(message.ReceivedAt).isValid()
59
- ? message.ReceivedAt
60
- : undefined;
61
- if (!messageId || !submittedAt) {
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 { httpStatus: response.status, messageId, submittedAt };
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":"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","sourcesContent":["import { day } from '@vrplatform/utils';\nimport { EmailDeliveryError } from './send-email';\n\nexport async function findPostmarkEmail(\n deliveryId: string,\n options: {\n fetch?: (\n input: string | URL | Request,\n init?: RequestInit\n ) => Promise<Response>;\n postmarkToken?: string;\n }\n) {\n if (!options.postmarkToken) {\n throw new EmailDeliveryError(\n 'Postmark is not configured',\n undefined,\n false,\n false\n );\n }\n\n const url = new URL('https://api.postmarkapp.com/messages/outbound');\n url.searchParams.set('count', '1');\n url.searchParams.set('offset', '0');\n url.searchParams.set('messagestream', 'outbound');\n url.searchParams.set('metadata_deliveryId', deliveryId);\n const response = await (options.fetch ?? fetch)(url, {\n method: 'GET',\n headers: {\n Accept: 'application/json',\n 'X-Postmark-Server-Token': options.postmarkToken,\n },\n });\n const providerResponse: unknown = await response.json().catch(() => ({}));\n\n if (!response.ok) {\n throw new EmailDeliveryError(\n 'Postmark message lookup failed',\n {\n provider: 'postmark',\n providerCategory: 'rejected',\n httpStatus: response.status,\n },\n false,\n response.status === 429 || response.status >= 500\n );\n }\n\n if (\n !providerResponse ||\n typeof providerResponse !== 'object' ||\n !('TotalCount' in providerResponse) ||\n typeof providerResponse.TotalCount !== 'number' ||\n !('Messages' in providerResponse) ||\n !Array.isArray(providerResponse.Messages)\n ) {\n throw new EmailDeliveryError('Postmark returned an invalid response');\n }\n if (providerResponse.TotalCount === 0) return undefined;\n\n const message = providerResponse.Messages.find(\n (value) =>\n value &&\n typeof value === 'object' &&\n 'Metadata' in value &&\n value.Metadata &&\n typeof value.Metadata === 'object' &&\n 'deliveryId' in value.Metadata &&\n value.Metadata.deliveryId === deliveryId\n );\n const messageId =\n message &&\n typeof message === 'object' &&\n 'MessageID' in message &&\n typeof message.MessageID === 'string' &&\n message.MessageID\n ? message.MessageID\n : undefined;\n const submittedAt =\n message &&\n typeof message === 'object' &&\n 'ReceivedAt' in message &&\n typeof message.ReceivedAt === 'string' &&\n day(message.ReceivedAt).isValid()\n ? message.ReceivedAt\n : undefined;\n if (!messageId || !submittedAt) {\n throw new EmailDeliveryError('Postmark returned an invalid response');\n }\n\n return { httpStatus: response.status, messageId, submittedAt };\n}\n"]}
1
+ {"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,3 +1,2 @@
1
1
  export * from './find-postmark-email';
2
- export * from './find-postmark-email';
3
2
  export * from './send-email';
@@ -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,wDAAsC;AACtC,+CAA6B","sourcesContent":["export * from './find-postmark-email';\nexport * from './find-postmark-email';\nexport * 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"]}
@@ -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 responseErrorCode = providerResponse !== null &&
42
- typeof providerResponse === 'object' &&
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 messageId = providerResponse !== null &&
72
- typeof providerResponse === 'object' &&
73
- 'MessageID' in providerResponse &&
74
- typeof providerResponse.MessageID === 'string' &&
75
- providerResponse.MessageID
76
- ? providerResponse.MessageID
77
- : undefined;
78
- const submittedAt = providerResponse !== null &&
79
- typeof providerResponse === 'object' &&
80
- 'SubmittedAt' in providerResponse &&
81
- typeof providerResponse.SubmittedAt === 'string' &&
82
- (0, utils_1.day)(providerResponse.SubmittedAt).isValid()
83
- ? providerResponse.SubmittedAt
84
- : undefined;
85
- if (!messageId || !submittedAt || postmarkErrorCode !== 0) {
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":"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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":";;;AAiDA,8BAuGC;AAED,sDAkEC;AA5ND,6CAAwC;AACxC,6BAAwB;AAExB,MAAM,uBAAuB,GAAG,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE,CAAC;AAC/D,MAAM,+BAA+B,GAAG,OAAC;KACtC,MAAM,CAAC,EAAE,SAAS,EAAE,uBAAuB,EAAE,CAAC;KAC9C,SAAS,CAAC,CAAC,EAAE,SAAS,EAAE,EAAE,EAAE,CAAC,SAAS,CAAC,CAAC;AAC3C,MAAM,6BAA6B,GAAG,OAAC;KACpC,MAAM,EAAE;KACR,KAAK,CAAC,OAAO,CAAC;KACd,SAAS,CAAC,MAAM,CAAC;KACjB,IAAI,CAAC,uBAAuB,CAAC,CAAC;AACjC,MAAM,6BAA6B,GAAG,OAAC,CAAC,MAAM,CAAC;IAC7C,SAAS,EAAE,OAAC,CAAC,OAAO,CAAC,CAAC,CAAC;IACvB,SAAS,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC5B,WAAW,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,IAAA,WAAG,EAAC,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC;CAChE,CAAC,CAAC;AAEH,SAAS,aAAa,CAAI,MAAoB,EAAE,KAAc;IAC5D,MAAM,MAAM,GAAG,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IACvC,IAAI,MAAM,CAAC,OAAO;QAAE,OAAO,MAAM,CAAC,IAAI,CAAC;AACzC,CAAC;AAgBD,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,aAAa,CAAC,+BAA+B,EAAE,gBAAgB,CAAC;QAChE,aAAa,CACX,6BAA6B,EAC7B,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAC1C,CAAC;IAEJ,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,MAAM,GAAG,6BAA6B,CAAC,SAAS,CAAC,gBAAgB,CAAC,CAAC;IACzE,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACpB,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,EAAE,MAAM,CAAC,IAAI,CAAC,SAAS;QAChC,WAAW,EAAE,MAAM,CAAC,IAAI,CAAC,WAAW;KACrC,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';\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"]}
@@ -3,6 +3,6 @@ export declare function findPostmarkEmail(deliveryId: string, options: {
3
3
  postmarkToken?: string;
4
4
  }): Promise<{
5
5
  httpStatus: number;
6
- messageId: any;
7
- submittedAt: any;
6
+ messageId: string;
7
+ submittedAt: string;
8
8
  } | undefined>;
@@ -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
- if (!providerResponse ||
28
- typeof providerResponse !== 'object' ||
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 (providerResponse.TotalCount === 0)
40
+ if (result.data.TotalCount === 0)
36
41
  return undefined;
37
- const message = providerResponse.Messages.find((value) => value &&
38
- typeof value === 'object' &&
39
- 'Metadata' in value &&
40
- value.Metadata &&
41
- typeof value.Metadata === 'object' &&
42
- 'deliveryId' in value.Metadata &&
43
- value.Metadata.deliveryId === deliveryId);
44
- const messageId = message &&
45
- typeof message === 'object' &&
46
- 'MessageID' in message &&
47
- typeof message.MessageID === 'string' &&
48
- message.MessageID
49
- ? message.MessageID
50
- : undefined;
51
- const submittedAt = message &&
52
- typeof message === 'object' &&
53
- 'ReceivedAt' in message &&
54
- typeof message.ReceivedAt === 'string' &&
55
- day(message.ReceivedAt).isValid()
56
- ? message.ReceivedAt
57
- : undefined;
58
- if (!messageId || !submittedAt) {
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 { httpStatus: response.status, messageId, submittedAt };
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":"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","sourcesContent":["import { day } from '@vrplatform/utils';\nimport { EmailDeliveryError } from './send-email';\n\nexport async function findPostmarkEmail(\n deliveryId: string,\n options: {\n fetch?: (\n input: string | URL | Request,\n init?: RequestInit\n ) => Promise<Response>;\n postmarkToken?: string;\n }\n) {\n if (!options.postmarkToken) {\n throw new EmailDeliveryError(\n 'Postmark is not configured',\n undefined,\n false,\n false\n );\n }\n\n const url = new URL('https://api.postmarkapp.com/messages/outbound');\n url.searchParams.set('count', '1');\n url.searchParams.set('offset', '0');\n url.searchParams.set('messagestream', 'outbound');\n url.searchParams.set('metadata_deliveryId', deliveryId);\n const response = await (options.fetch ?? fetch)(url, {\n method: 'GET',\n headers: {\n Accept: 'application/json',\n 'X-Postmark-Server-Token': options.postmarkToken,\n },\n });\n const providerResponse: unknown = await response.json().catch(() => ({}));\n\n if (!response.ok) {\n throw new EmailDeliveryError(\n 'Postmark message lookup failed',\n {\n provider: 'postmark',\n providerCategory: 'rejected',\n httpStatus: response.status,\n },\n false,\n response.status === 429 || response.status >= 500\n );\n }\n\n if (\n !providerResponse ||\n typeof providerResponse !== 'object' ||\n !('TotalCount' in providerResponse) ||\n typeof providerResponse.TotalCount !== 'number' ||\n !('Messages' in providerResponse) ||\n !Array.isArray(providerResponse.Messages)\n ) {\n throw new EmailDeliveryError('Postmark returned an invalid response');\n }\n if (providerResponse.TotalCount === 0) return undefined;\n\n const message = providerResponse.Messages.find(\n (value) =>\n value &&\n typeof value === 'object' &&\n 'Metadata' in value &&\n value.Metadata &&\n typeof value.Metadata === 'object' &&\n 'deliveryId' in value.Metadata &&\n value.Metadata.deliveryId === deliveryId\n );\n const messageId =\n message &&\n typeof message === 'object' &&\n 'MessageID' in message &&\n typeof message.MessageID === 'string' &&\n message.MessageID\n ? message.MessageID\n : undefined;\n const submittedAt =\n message &&\n typeof message === 'object' &&\n 'ReceivedAt' in message &&\n typeof message.ReceivedAt === 'string' &&\n day(message.ReceivedAt).isValid()\n ? message.ReceivedAt\n : undefined;\n if (!messageId || !submittedAt) {\n throw new EmailDeliveryError('Postmark returned an invalid response');\n }\n\n return { httpStatus: response.status, messageId, submittedAt };\n}\n"]}
1
+ {"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,3 +1,2 @@
1
1
  export * from './find-postmark-email';
2
- export * from './find-postmark-email';
3
2
  export * from './send-email';
@@ -1,4 +1,3 @@
1
1
  export * from './find-postmark-email';
2
- export * from './find-postmark-email';
3
2
  export * from './send-email';
4
3
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"src/","sources":["utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC;AACtC,cAAc,cAAc,CAAC","sourcesContent":["export * from './find-postmark-email';\nexport * from './find-postmark-email';\nexport * from './send-email';\n"]}
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 responseErrorCode = providerResponse !== null &&
39
- typeof providerResponse === 'object' &&
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 messageId = providerResponse !== null &&
69
- typeof providerResponse === 'object' &&
70
- 'MessageID' in providerResponse &&
71
- typeof providerResponse.MessageID === 'string' &&
72
- providerResponse.MessageID
73
- ? providerResponse.MessageID
74
- : undefined;
75
- const submittedAt = providerResponse !== null &&
76
- typeof providerResponse === 'object' &&
77
- 'SubmittedAt' in providerResponse &&
78
- typeof providerResponse.SubmittedAt === 'string' &&
79
- day(providerResponse.SubmittedAt).isValid()
80
- ? providerResponse.SubmittedAt
81
- : undefined;
82
- if (!messageId || !submittedAt || postmarkErrorCode !== 0) {
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.557",
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
- if (
51
- !providerResponse ||
52
- typeof providerResponse !== 'object' ||
53
- !('TotalCount' in providerResponse) ||
54
- typeof providerResponse.TotalCount !== 'number' ||
55
- !('Messages' in providerResponse) ||
56
- !Array.isArray(providerResponse.Messages)
57
- ) {
62
+ const result = postmarkMessagesResponseSchema.safeParse(providerResponse);
63
+ if (!result.success) {
58
64
  throw new EmailDeliveryError('Postmark returned an invalid response');
59
65
  }
60
- if (providerResponse.TotalCount === 0) return undefined;
66
+ if (result.data.TotalCount === 0) return undefined;
61
67
 
62
- const message = providerResponse.Messages.find(
63
- (value) =>
64
- value &&
65
- typeof value === 'object' &&
66
- 'Metadata' in value &&
67
- value.Metadata &&
68
- typeof value.Metadata === 'object' &&
69
- 'deliveryId' in value.Metadata &&
70
- value.Metadata.deliveryId === deliveryId
68
+ const message = result.data.Messages.find(
69
+ ({ Metadata }) => Metadata.deliveryId === deliveryId
71
70
  );
72
- const messageId =
73
- message &&
74
- typeof message === 'object' &&
75
- 'MessageID' in message &&
76
- typeof message.MessageID === 'string' &&
77
- message.MessageID
78
- ? message.MessageID
79
- : undefined;
80
- const submittedAt =
81
- message &&
82
- typeof message === 'object' &&
83
- 'ReceivedAt' in message &&
84
- typeof message.ReceivedAt === 'string' &&
85
- day(message.ReceivedAt).isValid()
86
- ? message.ReceivedAt
87
- : undefined;
88
- if (!messageId || !submittedAt) {
71
+ if (!message) {
89
72
  throw new EmailDeliveryError('Postmark returned an invalid response');
90
73
  }
91
74
 
92
- return { httpStatus: response.status, messageId, submittedAt };
75
+ return {
76
+ httpStatus: response.status,
77
+ messageId: message.MessageID,
78
+ submittedAt: message.ReceivedAt,
79
+ };
93
80
  }
@@ -1,3 +1,2 @@
1
1
  export * from './find-postmark-email';
2
- export * from './find-postmark-email';
3
2
  export * from './send-email';
@@ -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
- typeof errorCode === 'number' &&
95
- Number.isSafeInteger(errorCode) &&
96
- errorCode >= 0
97
- ? errorCode
98
- : undefined;
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 messageId =
131
- providerResponse !== null &&
132
- typeof providerResponse === 'object' &&
133
- 'MessageID' in providerResponse &&
134
- typeof providerResponse.MessageID === 'string' &&
135
- providerResponse.MessageID
136
- ? providerResponse.MessageID
137
- : undefined;
138
- const submittedAt =
139
- providerResponse !== null &&
140
- typeof providerResponse === 'object' &&
141
- 'SubmittedAt' in providerResponse &&
142
- typeof providerResponse.SubmittedAt === 'string' &&
143
- day(providerResponse.SubmittedAt).isValid()
144
- ? providerResponse.SubmittedAt
145
- : undefined;
146
- if (!messageId || !submittedAt || postmarkErrorCode !== 0) {
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