@vrplatform/email 1.0.2-stage.11 → 1.0.2-stage.1104

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.
Files changed (56) hide show
  1. package/build/main/components/ActionEmailTemplate.js +1 -3
  2. package/build/main/components/ActionEmailTemplate.js.map +1 -1
  3. package/build/main/components/EmailContainer.d.ts +3 -1
  4. package/build/main/components/EmailContainer.js +4 -4
  5. package/build/main/components/EmailContainer.js.map +1 -1
  6. package/build/main/components/utils/parse-email-html.js +13 -4
  7. package/build/main/components/utils/parse-email-html.js.map +1 -1
  8. package/build/main/templates/InviteOwnerEmailTemplate.d.ts +3 -1
  9. package/build/main/templates/InviteOwnerEmailTemplate.js +10 -6
  10. package/build/main/templates/InviteOwnerEmailTemplate.js.map +1 -1
  11. package/build/main/templates/InvitePmEmailTemplate.js +7 -3
  12. package/build/main/templates/InvitePmEmailTemplate.js.map +1 -1
  13. package/build/main/types.d.ts +1 -0
  14. package/build/main/types.js.map +1 -1
  15. package/build/main/utils/find-postmark-email.d.ts +8 -0
  16. package/build/main/utils/find-postmark-email.js +55 -0
  17. package/build/main/utils/find-postmark-email.js.map +1 -0
  18. package/build/main/utils/index.d.ts +1 -0
  19. package/build/main/utils/index.js +1 -0
  20. package/build/main/utils/index.js.map +1 -1
  21. package/build/main/utils/send-email.d.ts +27 -3
  22. package/build/main/utils/send-email.js +103 -22
  23. package/build/main/utils/send-email.js.map +1 -1
  24. package/build/module/components/ActionEmailTemplate.js +1 -3
  25. package/build/module/components/ActionEmailTemplate.js.map +1 -1
  26. package/build/module/components/EmailContainer.d.ts +3 -1
  27. package/build/module/components/EmailContainer.js +4 -4
  28. package/build/module/components/EmailContainer.js.map +1 -1
  29. package/build/module/components/utils/parse-email-html.js +13 -4
  30. package/build/module/components/utils/parse-email-html.js.map +1 -1
  31. package/build/module/templates/InviteOwnerEmailTemplate.d.ts +3 -1
  32. package/build/module/templates/InviteOwnerEmailTemplate.js +10 -6
  33. package/build/module/templates/InviteOwnerEmailTemplate.js.map +1 -1
  34. package/build/module/templates/InvitePmEmailTemplate.js +7 -3
  35. package/build/module/templates/InvitePmEmailTemplate.js.map +1 -1
  36. package/build/module/types.d.ts +1 -0
  37. package/build/module/types.js.map +1 -1
  38. package/build/module/utils/find-postmark-email.d.ts +8 -0
  39. package/build/module/utils/find-postmark-email.js +52 -0
  40. package/build/module/utils/find-postmark-email.js.map +1 -0
  41. package/build/module/utils/index.d.ts +1 -0
  42. package/build/module/utils/index.js +1 -0
  43. package/build/module/utils/index.js.map +1 -1
  44. package/build/module/utils/send-email.d.ts +27 -3
  45. package/build/module/utils/send-email.js +102 -22
  46. package/build/module/utils/send-email.js.map +1 -1
  47. package/package.json +5 -4
  48. package/src/components/ActionEmailTemplate.tsx +1 -3
  49. package/src/components/EmailContainer.tsx +10 -4
  50. package/src/components/utils/parse-email-html.ts +20 -6
  51. package/src/templates/InviteOwnerEmailTemplate.tsx +20 -7
  52. package/src/templates/InvitePmEmailTemplate.tsx +7 -3
  53. package/src/types.ts +1 -0
  54. package/src/utils/find-postmark-email.ts +80 -0
  55. package/src/utils/index.ts +1 -0
  56. package/src/utils/send-email.ts +177 -28
@@ -1,14 +1,65 @@
1
+ import { day } from '@vrplatform/utils';
2
+ import { z } from 'zod';
3
+
4
+ const postmarkErrorCodeSchema = z.number().int().nonnegative();
5
+ const postmarkResponseErrorCodeSchema = z
6
+ .object({ ErrorCode: postmarkErrorCodeSchema })
7
+ .transform(({ ErrorCode }) => ErrorCode);
8
+ const postmarkHeaderErrorCodeSchema = z
9
+ .string()
10
+ .regex(/^\d+$/)
11
+ .transform(Number)
12
+ .pipe(postmarkErrorCodeSchema);
13
+ const postmarkSuccessResponseSchema = z.object({
14
+ ErrorCode: z.literal(0),
15
+ MessageID: z.string().min(1),
16
+ SubmittedAt: z.string().refine((value) => day(value).isValid()),
17
+ });
18
+
19
+ function parseOptional<T>(schema: z.ZodType<T>, value: unknown) {
20
+ const result = schema.safeParse(value);
21
+ if (result.success) return result.data;
22
+ }
23
+
24
+ type EmailDeliveryFailureDetails = {
25
+ httpStatus: number;
26
+ postmarkErrorCode?: number;
27
+ provider: 'postmark';
28
+ providerCategory: 'inactiveRecipient' | 'rejected';
29
+ };
30
+
31
+ type EmailDeliveryErrorDetails =
32
+ | EmailDeliveryFailureDetails
33
+ | {
34
+ failureCount: number;
35
+ failures: EmailDeliveryFailureDetails[];
36
+ };
37
+
1
38
  export class EmailDeliveryError extends Error {
2
39
  constructor(
3
40
  message: string,
4
- public readonly details?: Record<string, unknown>,
5
- public readonly expected = false
41
+ public readonly details?: EmailDeliveryErrorDetails,
42
+ public readonly expected = false,
43
+ public readonly retryable = true
6
44
  ) {
7
45
  super(message);
8
46
  this.name = 'EmailDeliveryError';
9
47
  }
10
48
  }
11
49
 
50
+ /**
51
+ * Formats a verified sender address with a display name for the From header.
52
+ * Registrations store bare addresses; recipients should still see the company
53
+ * name the way they see `VRPlatform <notifications@vrplatform.app>`.
54
+ */
55
+ export function formatSenderIdentity(
56
+ name: string | null | undefined,
57
+ address: string
58
+ ) {
59
+ const display = (name ?? '').replace(/[\r\n"<>]/g, '').trim();
60
+ return display ? `"${display}" <${address}>` : address;
61
+ }
62
+
12
63
  export async function sendEmail(
13
64
  data: {
14
65
  to: string;
@@ -16,6 +67,9 @@ export async function sendEmail(
16
67
  text: string;
17
68
  html: string;
18
69
  replyTo?: string;
70
+ tag?: string;
71
+ metadata?: Record<string, string>;
72
+ from?: string;
19
73
  },
20
74
  options: {
21
75
  fetch?: (
@@ -26,7 +80,12 @@ export async function sendEmail(
26
80
  }
27
81
  ) {
28
82
  if (!options.postmarkToken) {
29
- throw new EmailDeliveryError('Postmark is not configured');
83
+ throw new EmailDeliveryError(
84
+ 'Postmark is not configured',
85
+ undefined,
86
+ false,
87
+ false
88
+ );
30
89
  }
31
90
 
32
91
  const response = await (options.fetch ?? fetch)(
@@ -39,48 +98,138 @@ export async function sendEmail(
39
98
  'X-Postmark-Server-Token': options.postmarkToken,
40
99
  },
41
100
  body: JSON.stringify({
42
- From: 'VRPlatform <notifications@vrplatform.app>',
101
+ From: data.from ?? 'VRPlatform <notifications@vrplatform.app>',
43
102
  To: data.to,
44
103
  Subject: data.subject,
45
104
  ReplyTo: data.replyTo,
46
105
  TextBody: data.text,
47
106
  HtmlBody: data.html,
48
107
  MessageStream: 'outbound',
108
+ Tag: data.tag,
109
+ Metadata: data.metadata,
49
110
  }),
50
111
  }
51
112
  );
52
113
 
53
114
  const providerResponse: unknown = await response.json().catch(() => ({}));
54
- const providerMessage =
55
- providerResponse !== null &&
56
- typeof providerResponse === 'object' &&
57
- 'Message' in providerResponse &&
58
- typeof providerResponse.Message === 'string'
59
- ? providerResponse.Message
60
- : undefined;
61
-
62
- if (!response.ok) {
63
- if (
64
- response.status === 422 &&
65
- providerMessage?.includes('marked as inactive')
66
- ) {
115
+ const postmarkErrorCode =
116
+ parseOptional(postmarkResponseErrorCodeSchema, providerResponse) ??
117
+ parseOptional(
118
+ postmarkHeaderErrorCodeSchema,
119
+ response.headers.get('X-PM-ApiErrorCode')
120
+ );
121
+
122
+ if (
123
+ !response.ok ||
124
+ (postmarkErrorCode !== undefined && postmarkErrorCode !== 0)
125
+ ) {
126
+ const inactiveRecipient =
127
+ response.status === 422 && postmarkErrorCode === 406;
128
+ const details: EmailDeliveryFailureDetails = {
129
+ provider: 'postmark',
130
+ providerCategory: inactiveRecipient ? 'inactiveRecipient' : 'rejected',
131
+ httpStatus: response.status,
132
+ ...(postmarkErrorCode !== undefined ? { postmarkErrorCode } : {}),
133
+ };
134
+
135
+ if (inactiveRecipient) {
67
136
  throw new EmailDeliveryError(
68
137
  'Postmark recipient is inactive',
69
- {
70
- providerMessage,
71
- status: response.status,
72
- statusText: response.statusText,
73
- },
74
- true
138
+ details,
139
+ true,
140
+ false
75
141
  );
76
142
  }
77
143
 
78
- throw new EmailDeliveryError('Postmark rejected the email', {
79
- providerResponse,
80
- status: response.status,
81
- statusText: response.statusText,
144
+ throw new EmailDeliveryError(
145
+ 'Postmark rejected the email',
146
+ details,
147
+ false,
148
+ response.status === 429 || response.status >= 500
149
+ );
150
+ }
151
+
152
+ const result = postmarkSuccessResponseSchema.safeParse(providerResponse);
153
+ if (!result.success) {
154
+ throw new EmailDeliveryError('Postmark returned an invalid response', {
155
+ provider: 'postmark',
156
+ providerCategory: 'rejected',
157
+ httpStatus: response.status,
158
+ ...(postmarkErrorCode !== undefined ? { postmarkErrorCode } : {}),
82
159
  });
83
160
  }
84
161
 
85
- return { status: response.status };
162
+ return {
163
+ httpStatus: response.status,
164
+ messageId: result.data.MessageID,
165
+ submittedAt: result.data.SubmittedAt,
166
+ };
167
+ }
168
+
169
+ export async function settleEmailDeliveries(deliveries: Promise<unknown>[]) {
170
+ const failures = (await Promise.allSettled(deliveries)).filter(
171
+ (result) => result.status === 'rejected'
172
+ );
173
+ if (failures.length === 0) return;
174
+
175
+ const unexpectedDeliveryError = failures.find(
176
+ (failure) =>
177
+ failure.reason instanceof EmailDeliveryError && !failure.reason.expected
178
+ )?.reason;
179
+ const allExpected = failures.every(
180
+ (failure) =>
181
+ failure.reason instanceof EmailDeliveryError && failure.reason.expected
182
+ );
183
+ const firstDeliveryError = failures.find(
184
+ (failure) => failure.reason instanceof EmailDeliveryError
185
+ )?.reason;
186
+ const failureDetails = failures.flatMap((failure) => {
187
+ if (
188
+ !(failure.reason instanceof EmailDeliveryError) ||
189
+ !failure.reason.details ||
190
+ !('provider' in failure.reason.details)
191
+ ) {
192
+ return [];
193
+ }
194
+ const { httpStatus, postmarkErrorCode, provider, providerCategory } =
195
+ failure.reason.details;
196
+ return [
197
+ {
198
+ httpStatus,
199
+ provider,
200
+ providerCategory,
201
+ ...(postmarkErrorCode !== undefined ? { postmarkErrorCode } : {}),
202
+ },
203
+ ];
204
+ });
205
+
206
+ const details: EmailDeliveryErrorDetails = {
207
+ failureCount: failures.length,
208
+ failures: failureDetails,
209
+ };
210
+ if (unexpectedDeliveryError instanceof EmailDeliveryError) {
211
+ if (unexpectedDeliveryError.message === 'Postmark is not configured') {
212
+ throw new EmailDeliveryError(
213
+ 'Postmark is not configured',
214
+ details,
215
+ false,
216
+ false
217
+ );
218
+ }
219
+ throw new EmailDeliveryError(
220
+ 'Postmark rejected the email',
221
+ details,
222
+ false,
223
+ unexpectedDeliveryError.retryable
224
+ );
225
+ }
226
+ if (allExpected && firstDeliveryError instanceof EmailDeliveryError) {
227
+ throw new EmailDeliveryError(
228
+ 'Postmark recipient is inactive',
229
+ details,
230
+ true,
231
+ false
232
+ );
233
+ }
234
+ throw new EmailDeliveryError('Email delivery failed', details);
86
235
  }