@vrplatform/email 1.0.2-373 → 1.0.2-37339970689

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 +3 -3
  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 +3 -3
  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 +9 -3
  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 +1 @@
1
- {"version":3,"file":"send-email.js","sourceRoot":"src/","sources":["utils/send-email.ts"],"names":[],"mappings":"AAAA,MAAM,OAAO,kBAAmB,SAAQ,KAAK;IAGzB;IACA;IAHlB,YACE,OAAe,EACC,OAAiC,EACjC,WAAW,KAAK;QAEhC,KAAK,CAAC,OAAO,CAAC,CAAC;QAHC,YAAO,GAAP,OAAO,CAA0B;QACjC,aAAQ,GAAR,QAAQ,CAAQ;QAGhC,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAC;IACnC,CAAC;CACF;AAED,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,IAMC,EACD,OAMC;IAED,IAAI,CAAC,OAAO,CAAC,aAAa,EAAE,CAAC;QAC3B,MAAM,IAAI,kBAAkB,CAAC,4BAA4B,CAAC,CAAC;IAC7D,CAAC;IAED,MAAM,QAAQ,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK,IAAI,KAAK,CAAC,CAC7C,mCAAmC,EACnC;QACE,MAAM,EAAE,MAAM;QACd,OAAO,EAAE;YACP,MAAM,EAAE,kBAAkB;YAC1B,cAAc,EAAE,kBAAkB;YAClC,yBAAyB,EAAE,OAAO,CAAC,aAAa;SACjD;QACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;YACnB,IAAI,EAAE,2CAA2C;YACjD,EAAE,EAAE,IAAI,CAAC,EAAE;YACX,OAAO,EAAE,IAAI,CAAC,OAAO;YACrB,OAAO,EAAE,IAAI,CAAC,OAAO;YACrB,QAAQ,EAAE,IAAI,CAAC,IAAI;YACnB,QAAQ,EAAE,IAAI,CAAC,IAAI;YACnB,aAAa,EAAE,UAAU;SAC1B,CAAC;KACH,CACF,CAAC;IAEF,MAAM,gBAAgB,GAAY,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAC1E,MAAM,eAAe,GACnB,gBAAgB,KAAK,IAAI;QACzB,OAAO,gBAAgB,KAAK,QAAQ;QACpC,SAAS,IAAI,gBAAgB;QAC7B,OAAO,gBAAgB,CAAC,OAAO,KAAK,QAAQ;QAC1C,CAAC,CAAC,gBAAgB,CAAC,OAAO;QAC1B,CAAC,CAAC,SAAS,CAAC;IAEhB,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,IACE,QAAQ,CAAC,MAAM,KAAK,GAAG;YACvB,eAAe,EAAE,QAAQ,CAAC,oBAAoB,CAAC,EAC/C,CAAC;YACD,MAAM,IAAI,kBAAkB,CAC1B,gCAAgC,EAChC;gBACE,eAAe;gBACf,MAAM,EAAE,QAAQ,CAAC,MAAM;gBACvB,UAAU,EAAE,QAAQ,CAAC,UAAU;aAChC,EACD,IAAI,CACL,CAAC;QACJ,CAAC;QAED,MAAM,IAAI,kBAAkB,CAAC,6BAA6B,EAAE;YAC1D,gBAAgB;YAChB,MAAM,EAAE,QAAQ,CAAC,MAAM;YACvB,UAAU,EAAE,QAAQ,CAAC,UAAU;SAChC,CAAC,CAAC;IACL,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,CAAC;AACrC,CAAC","sourcesContent":["export class EmailDeliveryError extends Error {\n constructor(\n message: string,\n public readonly details?: Record<string, unknown>,\n public readonly expected = false\n ) {\n super(message);\n this.name = 'EmailDeliveryError';\n }\n}\n\nexport async function sendEmail(\n data: {\n to: string;\n subject: string;\n text: string;\n html: string;\n replyTo?: string;\n },\n options: {\n fetch?: (\n input: string | URL | Request,\n init?: RequestInit\n ) => Promise<Response>;\n postmarkToken?: string;\n }\n) {\n if (!options.postmarkToken) {\n throw new EmailDeliveryError('Postmark is not configured');\n }\n\n const response = await (options.fetch ?? fetch)(\n 'https://api.postmarkapp.com/email',\n {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n 'X-Postmark-Server-Token': options.postmarkToken,\n },\n body: JSON.stringify({\n From: 'VRPlatform <notifications@vrplatform.app>',\n To: data.to,\n Subject: data.subject,\n ReplyTo: data.replyTo,\n TextBody: data.text,\n HtmlBody: data.html,\n MessageStream: 'outbound',\n }),\n }\n );\n\n const providerResponse: unknown = await response.json().catch(() => ({}));\n const providerMessage =\n providerResponse !== null &&\n typeof providerResponse === 'object' &&\n 'Message' in providerResponse &&\n typeof providerResponse.Message === 'string'\n ? providerResponse.Message\n : undefined;\n\n if (!response.ok) {\n if (\n response.status === 422 &&\n providerMessage?.includes('marked as inactive')\n ) {\n throw new EmailDeliveryError(\n 'Postmark recipient is inactive',\n {\n providerMessage,\n status: response.status,\n statusText: response.statusText,\n },\n true\n );\n }\n\n throw new EmailDeliveryError('Postmark rejected the email', {\n providerResponse,\n status: response.status,\n statusText: response.statusText,\n });\n }\n\n return { status: response.status };\n}\n"]}
1
+ {"version":3,"file":"send-email.js","sourceRoot":"src/","sources":["utils/send-email.ts"],"names":[],"mappings":"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","sourcesContent":["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\n/**\n * Formats a verified sender address with a display name for the From header.\n * Registrations store bare addresses; recipients should still see the company\n * name the way they see `VRPlatform <notifications@vrplatform.app>`.\n */\nexport function formatSenderIdentity(\n name: string | null | undefined,\n address: string\n) {\n const display = (name ?? '').replace(/[\\r\\n\"<>]/g, '').trim();\n return display ? `\"${display}\" <${address}>` : address;\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 from?: 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: data.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-373",
3
+ "version": "1.0.2-37339970689",
4
4
  "description": "VRTrust transactional email templates and Postmark transport",
5
5
  "license": "ISC",
6
6
  "main": "build/main/index.js",
@@ -30,16 +30,17 @@
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
- "@vrplatform/utils": "*",
37
+ "@vrplatform/utils": "1.0.21",
37
38
  "react": "^19.0.0"
38
39
  },
39
40
  "devDependencies": {
40
41
  "@types/react": "19.2.17",
41
42
  "@vrplatform/bin": "*",
42
- "@vrplatform/utils": "*",
43
+ "@vrplatform/utils": "1.0.21",
43
44
  "react": "19.2.7"
44
45
  },
45
46
  "files": [
@@ -33,9 +33,7 @@ export const ActionEmailTemplate = ({
33
33
  <EmailContainer
34
34
  title={data.subject}
35
35
  shortDescription={data.shortDescription}
36
- tenant={{
37
- logo: tenant.logo,
38
- }}
36
+ tenant={tenant}
39
37
  mode={mode}
40
38
  >
41
39
  <Heading
@@ -12,7 +12,7 @@ import type { EmailBaseProps } from '../types';
12
12
  type Props = {
13
13
  children: React.ReactNode;
14
14
  title: string;
15
- tenant: Omit<EmailBaseProps['tenant'], 'name'>;
15
+ tenant: Omit<EmailBaseProps['tenant'], 'name'> & { name?: string };
16
16
  mode: EmailBaseProps['mode'];
17
17
  shortDescription?: string;
18
18
  };
@@ -68,7 +68,11 @@ export const EmailContainer = (props: Props) => {
68
68
  );
69
69
  };
70
70
 
71
- const Content = ({ children, tenant: { logo: tenantLogo }, mode }: Props) => {
71
+ const Content = ({
72
+ children,
73
+ tenant: { logo: tenantLogo, name, showPlatformBranding = true },
74
+ mode,
75
+ }: Props) => {
72
76
  return (
73
77
  <>
74
78
  {tenantLogo ? (
@@ -86,6 +90,8 @@ const Content = ({ children, tenant: { logo: tenantLogo }, mode }: Props) => {
86
90
  >
87
91
  <Img src={tenantLogo} alt={'Logo'} width={70} />
88
92
  </div>
93
+ ) : !showPlatformBranding ? (
94
+ <Text>{name}</Text>
89
95
  ) : mode === 'send' ? (
90
96
  <Img
91
97
  src={'https://staging.portal.vrplatform.app/logo.png'}
@@ -105,7 +111,7 @@ const Content = ({ children, tenant: { logo: tenantLogo }, mode }: Props) => {
105
111
 
106
112
  {children}
107
113
 
108
- <Footer />
114
+ {showPlatformBranding ? <Footer /> : <Text>{name}</Text>}
109
115
  </>
110
116
  );
111
117
  };
@@ -1,3 +1,11 @@
1
+ const escapeHtml = (value: string) =>
2
+ value
3
+ .replaceAll('&', '&amp;')
4
+ .replaceAll('<', '&lt;')
5
+ .replaceAll('>', '&gt;')
6
+ .replaceAll('"', '&quot;')
7
+ .replaceAll("'", '&#39;');
8
+
1
9
  export const parseEmailHtml = (
2
10
  html: string,
3
11
  args: {
@@ -10,16 +18,22 @@ export const parseEmailHtml = (
10
18
  // parse out any html scripts of type : <team-name-component></team-name-component> or <user-name-component></user-name-component>
11
19
  // and replace them with the appropriate component
12
20
  // return the parsed html
21
+ // Values are escaped because names come from users and integrations. Function
22
+ // replacers keep `$` sequences in a value from being read as patterns.
23
+ const href = escapeHtml(args.href);
13
24
 
14
25
  return html
15
26
  .replaceAll(
16
27
  /<action-link-component><\/action-link-component>/g,
17
- `<a href="${args.href}" target="_blank">${args.href}</a>`
28
+ () => `<a href="${href}" target="_blank">${href}</a>`
18
29
  )
19
- .replaceAll(/<user-name-component><\/user-name-component>/g, args.userName)
20
- .replaceAll(
21
- /<listing-name-component><\/listing-name-component>/g,
22
- args.listingName
30
+ .replaceAll(/<user-name-component><\/user-name-component>/g, () =>
31
+ escapeHtml(args.userName)
32
+ )
33
+ .replaceAll(/<listing-name-component><\/listing-name-component>/g, () =>
34
+ escapeHtml(args.listingName)
23
35
  )
24
- .replaceAll(/<date-component><\/date-component>/g, args.date);
36
+ .replaceAll(/<date-component><\/date-component>/g, () =>
37
+ escapeHtml(args.date)
38
+ );
25
39
  };
@@ -16,17 +16,30 @@ export const getDefaultInviteOwnerEmailData = (
16
16
  mode === 'tag' ? EMAIL_ACTION_LINK_COMPONENT : args.signInLink;
17
17
 
18
18
  return {
19
- bodyOne: `<p>You are invited to join us on ${args.appName}.</p><p>To start using ${args.appName}, accept the invitation and verify your email address.</p>`,
19
+ bodyOne: `<p>You are invited to access your Owner Portal on ${args.appName}.</p><p>Accept the invitation below to set up your access.</p>`,
20
20
  bodyTwo: `<p>If you have any trouble with the button, you can copy and paste the link below into your browser: ${signInLink}</p>`,
21
21
  buttonText: 'Accept Invitation',
22
- header: 'Welcome to the team!',
23
- shortDescription:
24
- 'You have been invited to join the team. To start using VRPlatform, verify your email address at link below.',
22
+ header: 'Welcome to your Owner Portal!',
23
+ shortDescription: 'Set up your Owner Portal access using your invitation.',
25
24
  href: args.signInLink,
26
- subject: `You are invited to join your team on ${args.appName}`,
25
+ subject: `Your invitation to the ${args.appName} Owner Portal`,
27
26
  };
28
27
  };
29
28
 
30
- export const InviteOwnerEmailTemplate = (props: ActionEmailTemplateProps) => {
31
- return <ActionEmailTemplate {...props} />;
29
+ export const InviteOwnerEmailTemplate = ({
30
+ ownerAccessRole = 'admin',
31
+ ...props
32
+ }: ActionEmailTemplateProps & {
33
+ ownerAccessRole?: 'admin' | 'viewer';
34
+ }) => {
35
+ return (
36
+ <ActionEmailTemplate
37
+ {...props}
38
+ data={{
39
+ ...props.data,
40
+ // Add guidance at render time so saved custom templates receive it too.
41
+ bodyTwo: `${props.data.bodyTwo}<hr style="width:100%;border:none;border-top:1px solid #eaeaea;margin:24px 0" /><p>This invitation link is for one-time setup of your Owner Portal access and expires after 7 days. Sign up using the exact email address this invitation was sent to.</p><p>If your invitation expires before setup is complete, contact your property manager for a new invitation.</p><p>For questions about ${ownerAccessRole === 'viewer' ? 'portal access' : 'your statements or portal access'}, contact your property manager.</p>`,
42
+ }}
43
+ />
44
+ );
32
45
  };
@@ -18,12 +18,11 @@ export const getDefaultInvitePropertyManagerEmailData = (
18
18
  : `<a href='${args.signInLink}' target='_blank'>${args.signInLink}</a>`;
19
19
 
20
20
  return {
21
- bodyOne: `<p>You are invited to join us on ${args.appName}.</p><p>To start using ${args.appName}, accept the invitation and verify your email address.</p>`,
21
+ bodyOne: `<p>You are invited to join us on ${args.appName}.</p><p>Accept the invitation below to set up your team access.</p>`,
22
22
  bodyTwo: `<p>If you have any trouble with the button, you can use the following link below: ${signInLink}</p>`,
23
23
  buttonText: 'Accept Invitation',
24
24
  header: 'Welcome to the team!',
25
- shortDescription:
26
- 'You have been invited to join the team. To start using VRPlatform, verify your email address at link below.',
25
+ shortDescription: `Accept your invitation to join your team on ${args.appName}.`,
27
26
  href: args.signInLink,
28
27
  subject: `You are invited to join your team on ${args.appName}`,
29
28
  };
@@ -33,6 +32,11 @@ export const InvitePmEmailTemplate = (props: ActionEmailTemplateProps) => {
33
32
  return (
34
33
  <ActionEmailTemplate
35
34
  {...props}
35
+ data={{
36
+ ...props.data,
37
+ // Add guidance at render time so saved custom templates receive it too.
38
+ bodyTwo: `${props.data.bodyTwo}<hr style="width:100%;border:none;border-top:1px solid #eaeaea;margin:24px 0" /><p>This invitation link is for one-time setup of your team access and expires after 7 days. Sign up using the exact email address this invitation was sent to.</p><p>If your invitation expires before setup is complete, contact your team administrator for a new invitation.</p><p>For questions about team access, contact your team administrator.</p>`,
39
+ }}
36
40
  tenant={{
37
41
  logo: undefined,
38
42
  name: props.tenant.name,
package/src/types.ts CHANGED
@@ -2,6 +2,7 @@ export type EmailBaseProps = {
2
2
  mode: 'edit' | 'preview' | 'send';
3
3
  tenant: {
4
4
  name: string;
5
+ showPlatformBranding?: boolean;
5
6
  logo: string | undefined | null;
6
7
  };
7
8
  listing: string | undefined;
@@ -0,0 +1,80 @@
1
+ import { day } from '@vrplatform/utils';
2
+ import { z } from 'zod';
3
+ import { EmailDeliveryError } from './send-email';
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
+
16
+ export async function findPostmarkEmail(
17
+ deliveryId: string,
18
+ options: {
19
+ fetch?: (
20
+ input: string | URL | Request,
21
+ init?: RequestInit
22
+ ) => Promise<Response>;
23
+ postmarkToken?: string;
24
+ }
25
+ ) {
26
+ if (!options.postmarkToken) {
27
+ throw new EmailDeliveryError(
28
+ 'Postmark is not configured',
29
+ undefined,
30
+ false,
31
+ false
32
+ );
33
+ }
34
+
35
+ const url = new URL('https://api.postmarkapp.com/messages/outbound');
36
+ url.searchParams.set('count', '1');
37
+ url.searchParams.set('offset', '0');
38
+ url.searchParams.set('messagestream', 'outbound');
39
+ url.searchParams.set('metadata_deliveryId', deliveryId);
40
+ const response = await (options.fetch ?? fetch)(url, {
41
+ method: 'GET',
42
+ headers: {
43
+ Accept: 'application/json',
44
+ 'X-Postmark-Server-Token': options.postmarkToken,
45
+ },
46
+ });
47
+ const providerResponse: unknown = await response.json().catch(() => ({}));
48
+
49
+ if (!response.ok) {
50
+ throw new EmailDeliveryError(
51
+ 'Postmark message lookup failed',
52
+ {
53
+ provider: 'postmark',
54
+ providerCategory: 'rejected',
55
+ httpStatus: response.status,
56
+ },
57
+ false,
58
+ response.status === 429 || response.status >= 500
59
+ );
60
+ }
61
+
62
+ const result = postmarkMessagesResponseSchema.safeParse(providerResponse);
63
+ if (!result.success) {
64
+ throw new EmailDeliveryError('Postmark returned an invalid response');
65
+ }
66
+ if (result.data.TotalCount === 0) return undefined;
67
+
68
+ const message = result.data.Messages.find(
69
+ ({ Metadata }) => Metadata.deliveryId === deliveryId
70
+ );
71
+ if (!message) {
72
+ throw new EmailDeliveryError('Postmark returned an invalid response');
73
+ }
74
+
75
+ return {
76
+ httpStatus: response.status,
77
+ messageId: message.MessageID,
78
+ submittedAt: message.ReceivedAt,
79
+ };
80
+ }
@@ -1 +1,2 @@
1
+ export * from './find-postmark-email';
1
2
  export * from './send-email';
@@ -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
  }