@vrplatform/email 1.0.2-stage.11 → 1.0.2-stage.1101
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.
- package/build/main/components/ActionEmailTemplate.js +1 -3
- package/build/main/components/ActionEmailTemplate.js.map +1 -1
- package/build/main/components/EmailContainer.d.ts +3 -1
- package/build/main/components/EmailContainer.js +4 -4
- package/build/main/components/EmailContainer.js.map +1 -1
- package/build/main/components/utils/parse-email-html.js +13 -4
- package/build/main/components/utils/parse-email-html.js.map +1 -1
- package/build/main/templates/InviteOwnerEmailTemplate.d.ts +3 -1
- package/build/main/templates/InviteOwnerEmailTemplate.js +10 -6
- package/build/main/templates/InviteOwnerEmailTemplate.js.map +1 -1
- package/build/main/templates/InvitePmEmailTemplate.js +7 -3
- package/build/main/templates/InvitePmEmailTemplate.js.map +1 -1
- package/build/main/types.d.ts +1 -0
- package/build/main/types.js.map +1 -1
- package/build/main/utils/find-postmark-email.d.ts +8 -0
- package/build/main/utils/find-postmark-email.js +55 -0
- package/build/main/utils/find-postmark-email.js.map +1 -0
- package/build/main/utils/index.d.ts +1 -0
- package/build/main/utils/index.js +1 -0
- package/build/main/utils/index.js.map +1 -1
- package/build/main/utils/send-email.d.ts +27 -3
- package/build/main/utils/send-email.js +103 -22
- package/build/main/utils/send-email.js.map +1 -1
- package/build/module/components/ActionEmailTemplate.js +1 -3
- package/build/module/components/ActionEmailTemplate.js.map +1 -1
- package/build/module/components/EmailContainer.d.ts +3 -1
- package/build/module/components/EmailContainer.js +4 -4
- package/build/module/components/EmailContainer.js.map +1 -1
- package/build/module/components/utils/parse-email-html.js +13 -4
- package/build/module/components/utils/parse-email-html.js.map +1 -1
- package/build/module/templates/InviteOwnerEmailTemplate.d.ts +3 -1
- package/build/module/templates/InviteOwnerEmailTemplate.js +10 -6
- package/build/module/templates/InviteOwnerEmailTemplate.js.map +1 -1
- package/build/module/templates/InvitePmEmailTemplate.js +7 -3
- package/build/module/templates/InvitePmEmailTemplate.js.map +1 -1
- package/build/module/types.d.ts +1 -0
- package/build/module/types.js.map +1 -1
- package/build/module/utils/find-postmark-email.d.ts +8 -0
- package/build/module/utils/find-postmark-email.js +52 -0
- package/build/module/utils/find-postmark-email.js.map +1 -0
- package/build/module/utils/index.d.ts +1 -0
- package/build/module/utils/index.js +1 -0
- package/build/module/utils/index.js.map +1 -1
- package/build/module/utils/send-email.d.ts +27 -3
- package/build/module/utils/send-email.js +102 -22
- package/build/module/utils/send-email.js.map +1 -1
- package/package.json +5 -4
- package/src/components/ActionEmailTemplate.tsx +1 -3
- package/src/components/EmailContainer.tsx +10 -4
- package/src/components/utils/parse-email-html.ts +20 -6
- package/src/templates/InviteOwnerEmailTemplate.tsx +20 -7
- package/src/templates/InvitePmEmailTemplate.tsx +7 -3
- package/src/types.ts +1 -0
- package/src/utils/find-postmark-email.ts +80 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/send-email.ts +177 -28
package/src/utils/send-email.ts
CHANGED
|
@@ -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?:
|
|
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(
|
|
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
|
|
55
|
-
providerResponse
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
statusText: response.statusText,
|
|
73
|
-
},
|
|
74
|
-
true
|
|
138
|
+
details,
|
|
139
|
+
true,
|
|
140
|
+
false
|
|
75
141
|
);
|
|
76
142
|
}
|
|
77
143
|
|
|
78
|
-
throw new EmailDeliveryError(
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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 {
|
|
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
|
}
|