@vrplatform/email 1.0.2-5 → 1.0.2-556

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 (39) hide show
  1. package/build/main/components/EmailContainer.js +1 -1
  2. package/build/main/components/EmailContainer.js.map +1 -1
  3. package/build/main/templates/InviteOwnerEmailTemplate.d.ts +3 -1
  4. package/build/main/templates/InviteOwnerEmailTemplate.js +10 -6
  5. package/build/main/templates/InviteOwnerEmailTemplate.js.map +1 -1
  6. package/build/main/templates/InvitePmEmailTemplate.js +7 -3
  7. package/build/main/templates/InvitePmEmailTemplate.js.map +1 -1
  8. package/build/main/utils/find-postmark-email.d.ts +8 -0
  9. package/build/main/utils/find-postmark-email.js +66 -0
  10. package/build/main/utils/find-postmark-email.js.map +1 -0
  11. package/build/main/utils/index.d.ts +2 -0
  12. package/build/main/utils/index.js +2 -0
  13. package/build/main/utils/index.js.map +1 -1
  14. package/build/main/utils/send-email.d.ts +20 -3
  15. package/build/main/utils/send-email.js +97 -19
  16. package/build/main/utils/send-email.js.map +1 -1
  17. package/build/module/components/EmailContainer.js +1 -1
  18. package/build/module/components/EmailContainer.js.map +1 -1
  19. package/build/module/templates/InviteOwnerEmailTemplate.d.ts +3 -1
  20. package/build/module/templates/InviteOwnerEmailTemplate.js +10 -6
  21. package/build/module/templates/InviteOwnerEmailTemplate.js.map +1 -1
  22. package/build/module/templates/InvitePmEmailTemplate.js +7 -3
  23. package/build/module/templates/InvitePmEmailTemplate.js.map +1 -1
  24. package/build/module/utils/find-postmark-email.d.ts +8 -0
  25. package/build/module/utils/find-postmark-email.js +63 -0
  26. package/build/module/utils/find-postmark-email.js.map +1 -0
  27. package/build/module/utils/index.d.ts +2 -0
  28. package/build/module/utils/index.js +2 -0
  29. package/build/module/utils/index.js.map +1 -1
  30. package/build/module/utils/send-email.d.ts +20 -3
  31. package/build/module/utils/send-email.js +97 -19
  32. package/build/module/utils/send-email.js.map +1 -1
  33. package/package.json +1 -1
  34. package/src/components/EmailContainer.tsx +1 -1
  35. package/src/templates/InviteOwnerEmailTemplate.tsx +20 -7
  36. package/src/templates/InvitePmEmailTemplate.tsx +7 -3
  37. package/src/utils/find-postmark-email.ts +93 -0
  38. package/src/utils/index.ts +2 -0
  39. package/src/utils/send-email.ts +165 -23
@@ -1,8 +1,25 @@
1
+ import { day } from '@vrplatform/utils';
2
+
3
+ type EmailDeliveryFailureDetails = {
4
+ httpStatus: number;
5
+ postmarkErrorCode?: number;
6
+ provider: 'postmark';
7
+ providerCategory: 'inactiveRecipient' | 'rejected';
8
+ };
9
+
10
+ type EmailDeliveryErrorDetails =
11
+ | EmailDeliveryFailureDetails
12
+ | {
13
+ failureCount: number;
14
+ failures: EmailDeliveryFailureDetails[];
15
+ };
16
+
1
17
  export class EmailDeliveryError extends Error {
2
18
  constructor(
3
19
  message: string,
4
- public readonly details?: Record<string, unknown>,
5
- public readonly expected = false
20
+ public readonly details?: EmailDeliveryErrorDetails,
21
+ public readonly expected = false,
22
+ public readonly retryable = true
6
23
  ) {
7
24
  super(message);
8
25
  this.name = 'EmailDeliveryError';
@@ -16,6 +33,8 @@ export async function sendEmail(
16
33
  text: string;
17
34
  html: string;
18
35
  replyTo?: string;
36
+ tag?: string;
37
+ metadata?: Record<string, string>;
19
38
  },
20
39
  options: {
21
40
  fetch?: (
@@ -26,7 +45,12 @@ export async function sendEmail(
26
45
  }
27
46
  ) {
28
47
  if (!options.postmarkToken) {
29
- throw new EmailDeliveryError('Postmark is not configured');
48
+ throw new EmailDeliveryError(
49
+ 'Postmark is not configured',
50
+ undefined,
51
+ false,
52
+ false
53
+ );
30
54
  }
31
55
 
32
56
  const response = await (options.fetch ?? fetch)(
@@ -46,41 +70,159 @@ export async function sendEmail(
46
70
  TextBody: data.text,
47
71
  HtmlBody: data.html,
48
72
  MessageStream: 'outbound',
73
+ Tag: data.tag,
74
+ Metadata: data.metadata,
49
75
  }),
50
76
  }
51
77
  );
52
78
 
53
79
  const providerResponse: unknown = await response.json().catch(() => ({}));
54
- const providerMessage =
80
+ const responseErrorCode =
55
81
  providerResponse !== null &&
56
82
  typeof providerResponse === 'object' &&
57
- 'Message' in providerResponse &&
58
- typeof providerResponse.Message === 'string'
59
- ? providerResponse.Message
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
+ const postmarkErrorCode =
94
+ typeof errorCode === 'number' &&
95
+ Number.isSafeInteger(errorCode) &&
96
+ errorCode >= 0
97
+ ? errorCode
60
98
  : undefined;
61
99
 
62
- if (!response.ok) {
63
- if (
64
- response.status === 422 &&
65
- providerMessage?.includes('marked as inactive')
66
- ) {
100
+ if (
101
+ !response.ok ||
102
+ (postmarkErrorCode !== undefined && postmarkErrorCode !== 0)
103
+ ) {
104
+ const inactiveRecipient =
105
+ response.status === 422 && postmarkErrorCode === 406;
106
+ const details: EmailDeliveryFailureDetails = {
107
+ provider: 'postmark',
108
+ providerCategory: inactiveRecipient ? 'inactiveRecipient' : 'rejected',
109
+ httpStatus: response.status,
110
+ ...(postmarkErrorCode !== undefined ? { postmarkErrorCode } : {}),
111
+ };
112
+
113
+ if (inactiveRecipient) {
67
114
  throw new EmailDeliveryError(
68
115
  'Postmark recipient is inactive',
69
- {
70
- providerMessage,
71
- status: response.status,
72
- statusText: response.statusText,
73
- },
74
- true
116
+ details,
117
+ true,
118
+ false
75
119
  );
76
120
  }
77
121
 
78
- throw new EmailDeliveryError('Postmark rejected the email', {
79
- providerResponse,
80
- status: response.status,
81
- statusText: response.statusText,
122
+ throw new EmailDeliveryError(
123
+ 'Postmark rejected the email',
124
+ details,
125
+ false,
126
+ response.status === 429 || response.status >= 500
127
+ );
128
+ }
129
+
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) {
147
+ throw new EmailDeliveryError('Postmark returned an invalid response', {
148
+ provider: 'postmark',
149
+ providerCategory: 'rejected',
150
+ httpStatus: response.status,
151
+ ...(postmarkErrorCode !== undefined ? { postmarkErrorCode } : {}),
82
152
  });
83
153
  }
84
154
 
85
- return { status: response.status };
155
+ return {
156
+ httpStatus: response.status,
157
+ messageId,
158
+ submittedAt,
159
+ };
160
+ }
161
+
162
+ export async function settleEmailDeliveries(deliveries: Promise<unknown>[]) {
163
+ const failures = (await Promise.allSettled(deliveries)).filter(
164
+ (result) => result.status === 'rejected'
165
+ );
166
+ if (failures.length === 0) return;
167
+
168
+ const unexpectedDeliveryError = failures.find(
169
+ (failure) =>
170
+ failure.reason instanceof EmailDeliveryError && !failure.reason.expected
171
+ )?.reason;
172
+ const allExpected = failures.every(
173
+ (failure) =>
174
+ failure.reason instanceof EmailDeliveryError && failure.reason.expected
175
+ );
176
+ const firstDeliveryError = failures.find(
177
+ (failure) => failure.reason instanceof EmailDeliveryError
178
+ )?.reason;
179
+ const failureDetails = failures.flatMap((failure) => {
180
+ if (
181
+ !(failure.reason instanceof EmailDeliveryError) ||
182
+ !failure.reason.details ||
183
+ !('provider' in failure.reason.details)
184
+ ) {
185
+ return [];
186
+ }
187
+ const { httpStatus, postmarkErrorCode, provider, providerCategory } =
188
+ failure.reason.details;
189
+ return [
190
+ {
191
+ httpStatus,
192
+ provider,
193
+ providerCategory,
194
+ ...(postmarkErrorCode !== undefined ? { postmarkErrorCode } : {}),
195
+ },
196
+ ];
197
+ });
198
+
199
+ const details: EmailDeliveryErrorDetails = {
200
+ failureCount: failures.length,
201
+ failures: failureDetails,
202
+ };
203
+ if (unexpectedDeliveryError instanceof EmailDeliveryError) {
204
+ if (unexpectedDeliveryError.message === 'Postmark is not configured') {
205
+ throw new EmailDeliveryError(
206
+ 'Postmark is not configured',
207
+ details,
208
+ false,
209
+ false
210
+ );
211
+ }
212
+ throw new EmailDeliveryError(
213
+ 'Postmark rejected the email',
214
+ details,
215
+ false,
216
+ unexpectedDeliveryError.retryable
217
+ );
218
+ }
219
+ if (allExpected && firstDeliveryError instanceof EmailDeliveryError) {
220
+ throw new EmailDeliveryError(
221
+ 'Postmark recipient is inactive',
222
+ details,
223
+ true,
224
+ false
225
+ );
226
+ }
227
+ throw new EmailDeliveryError('Email delivery failed', details);
86
228
  }