@softure-ai/mailing 0.0.0-stage → 0.1.6

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 (193) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/LICENSE +21 -0
  3. package/README.md +346 -2
  4. package/dist/address.d.ts +27 -0
  5. package/dist/address.d.ts.map +1 -0
  6. package/dist/address.js +76 -0
  7. package/dist/address.js.map +1 -0
  8. package/dist/cli/bin.d.ts +3 -0
  9. package/dist/cli/bin.d.ts.map +1 -0
  10. package/dist/cli/bin.js +5 -0
  11. package/dist/cli/bin.js.map +1 -0
  12. package/dist/cli/command.d.ts +9 -0
  13. package/dist/cli/command.d.ts.map +1 -0
  14. package/dist/cli/command.js +36 -0
  15. package/dist/cli/command.js.map +1 -0
  16. package/dist/cli/index.d.ts +3 -0
  17. package/dist/cli/index.d.ts.map +1 -0
  18. package/dist/cli/index.js +4 -0
  19. package/dist/cli/index.js.map +1 -0
  20. package/dist/cli/run.d.ts +31 -0
  21. package/dist/cli/run.d.ts.map +1 -0
  22. package/dist/cli/run.js +288 -0
  23. package/dist/cli/run.js.map +1 -0
  24. package/dist/contract.d.ts +102 -0
  25. package/dist/contract.d.ts.map +1 -0
  26. package/dist/contract.js +3 -0
  27. package/dist/contract.js.map +1 -0
  28. package/dist/index.d.ts +47 -0
  29. package/dist/index.d.ts.map +1 -0
  30. package/dist/index.js +59 -0
  31. package/dist/index.js.map +1 -0
  32. package/dist/messages/en.d.ts +26 -0
  33. package/dist/messages/en.d.ts.map +1 -0
  34. package/dist/messages/en.js +26 -0
  35. package/dist/messages/en.js.map +1 -0
  36. package/dist/messages/index.d.ts +59 -0
  37. package/dist/messages/index.d.ts.map +1 -0
  38. package/dist/messages/index.js +11 -0
  39. package/dist/messages/index.js.map +1 -0
  40. package/dist/messages/pl.d.ts +3 -0
  41. package/dist/messages/pl.d.ts.map +1 -0
  42. package/dist/messages/pl.js +27 -0
  43. package/dist/messages/pl.js.map +1 -0
  44. package/dist/next/actions.d.ts +2 -0
  45. package/dist/next/actions.d.ts.map +1 -0
  46. package/dist/next/actions.js +35 -0
  47. package/dist/next/actions.js.map +1 -0
  48. package/dist/next/context.d.ts +4 -0
  49. package/dist/next/context.d.ts.map +1 -0
  50. package/dist/next/context.js +14 -0
  51. package/dist/next/context.js.map +1 -0
  52. package/dist/next/deliver-once.d.ts +4 -0
  53. package/dist/next/deliver-once.d.ts.map +1 -0
  54. package/dist/next/deliver-once.js +7 -0
  55. package/dist/next/deliver-once.js.map +1 -0
  56. package/dist/next/index.d.ts +8 -0
  57. package/dist/next/index.d.ts.map +1 -0
  58. package/dist/next/index.js +11 -0
  59. package/dist/next/index.js.map +1 -0
  60. package/dist/next/messages.d.ts +5 -0
  61. package/dist/next/messages.d.ts.map +1 -0
  62. package/dist/next/messages.js +7 -0
  63. package/dist/next/messages.js.map +1 -0
  64. package/dist/next/pages.d.ts +7 -0
  65. package/dist/next/pages.d.ts.map +1 -0
  66. package/dist/next/pages.js +40 -0
  67. package/dist/next/pages.js.map +1 -0
  68. package/dist/next/params.d.ts +4 -0
  69. package/dist/next/params.d.ts.map +1 -0
  70. package/dist/next/params.js +4 -0
  71. package/dist/next/params.js.map +1 -0
  72. package/dist/next/route.d.ts +14 -0
  73. package/dist/next/route.d.ts.map +1 -0
  74. package/dist/next/route.js +44 -0
  75. package/dist/next/route.js.map +1 -0
  76. package/dist/next/send-mail.d.ts +4 -0
  77. package/dist/next/send-mail.d.ts.map +1 -0
  78. package/dist/next/send-mail.js +16 -0
  79. package/dist/next/send-mail.js.map +1 -0
  80. package/dist/options.d.ts +13 -0
  81. package/dist/options.d.ts.map +1 -0
  82. package/dist/options.js +29 -0
  83. package/dist/options.js.map +1 -0
  84. package/dist/providers/resend.d.ts +17 -0
  85. package/dist/providers/resend.d.ts.map +1 -0
  86. package/dist/providers/resend.js +64 -0
  87. package/dist/providers/resend.js.map +1 -0
  88. package/dist/schema.d.ts +350 -0
  89. package/dist/schema.d.ts.map +1 -0
  90. package/dist/schema.js +30 -0
  91. package/dist/schema.js.map +1 -0
  92. package/dist/server/campaign-file.d.ts +28 -0
  93. package/dist/server/campaign-file.d.ts.map +1 -0
  94. package/dist/server/campaign-file.js +87 -0
  95. package/dist/server/campaign-file.js.map +1 -0
  96. package/dist/server/campaigns.d.ts +58 -0
  97. package/dist/server/campaigns.d.ts.map +1 -0
  98. package/dist/server/campaigns.js +118 -0
  99. package/dist/server/campaigns.js.map +1 -0
  100. package/dist/server/deliveries.d.ts +56 -0
  101. package/dist/server/deliveries.d.ts.map +1 -0
  102. package/dist/server/deliveries.js +100 -0
  103. package/dist/server/deliveries.js.map +1 -0
  104. package/dist/server/dns.d.ts +33 -0
  105. package/dist/server/dns.d.ts.map +1 -0
  106. package/dist/server/dns.js +113 -0
  107. package/dist/server/dns.js.map +1 -0
  108. package/dist/server/health.d.ts +3 -0
  109. package/dist/server/health.d.ts.map +1 -0
  110. package/dist/server/health.js +13 -0
  111. package/dist/server/health.js.map +1 -0
  112. package/dist/server/index.d.ts +12 -0
  113. package/dist/server/index.d.ts.map +1 -0
  114. package/dist/server/index.js +14 -0
  115. package/dist/server/index.js.map +1 -0
  116. package/dist/server/list-mail.d.ts +20 -0
  117. package/dist/server/list-mail.d.ts.map +1 -0
  118. package/dist/server/list-mail.js +31 -0
  119. package/dist/server/list-mail.js.map +1 -0
  120. package/dist/server/options.d.ts +14 -0
  121. package/dist/server/options.d.ts.map +1 -0
  122. package/dist/server/options.js +22 -0
  123. package/dist/server/options.js.map +1 -0
  124. package/dist/server/send-mail.d.ts +19 -0
  125. package/dist/server/send-mail.d.ts.map +1 -0
  126. package/dist/server/send-mail.js +129 -0
  127. package/dist/server/send-mail.js.map +1 -0
  128. package/dist/server/suppressions.d.ts +27 -0
  129. package/dist/server/suppressions.d.ts.map +1 -0
  130. package/dist/server/suppressions.js +64 -0
  131. package/dist/server/suppressions.js.map +1 -0
  132. package/dist/server/unsubscribe-link.d.ts +51 -0
  133. package/dist/server/unsubscribe-link.d.ts.map +1 -0
  134. package/dist/server/unsubscribe-link.js +78 -0
  135. package/dist/server/unsubscribe-link.js.map +1 -0
  136. package/dist/server/validate-mail.d.ts +23 -0
  137. package/dist/server/validate-mail.d.ts.map +1 -0
  138. package/dist/server/validate-mail.js +52 -0
  139. package/dist/server/validate-mail.js.map +1 -0
  140. package/dist/testing/fake-provider.d.ts +24 -0
  141. package/dist/testing/fake-provider.d.ts.map +1 -0
  142. package/dist/testing/fake-provider.js +43 -0
  143. package/dist/testing/fake-provider.js.map +1 -0
  144. package/dist/testing/index.d.ts +3 -0
  145. package/dist/testing/index.d.ts.map +1 -0
  146. package/dist/testing/index.js +5 -0
  147. package/dist/testing/index.js.map +1 -0
  148. package/dist/testing/outbox.d.ts +6 -0
  149. package/dist/testing/outbox.d.ts.map +1 -0
  150. package/dist/testing/outbox.js +32 -0
  151. package/dist/testing/outbox.js.map +1 -0
  152. package/migrations/0001_create_suppressions.sql +13 -0
  153. package/migrations/0002_create_campaigns_and_deliveries.sql +41 -0
  154. package/module.json +32 -0
  155. package/package.json +74 -4
  156. package/src/address.ts +89 -0
  157. package/src/cli/bin.ts +5 -0
  158. package/src/cli/command.ts +44 -0
  159. package/src/cli/index.ts +3 -0
  160. package/src/cli/run.ts +317 -0
  161. package/src/contract.ts +108 -0
  162. package/src/index.ts +85 -0
  163. package/src/messages/en.ts +25 -0
  164. package/src/messages/index.ts +15 -0
  165. package/src/messages/pl.ts +28 -0
  166. package/src/next/actions.ts +35 -0
  167. package/src/next/context.ts +15 -0
  168. package/src/next/deliver-once.ts +10 -0
  169. package/src/next/index.ts +10 -0
  170. package/src/next/messages.ts +9 -0
  171. package/src/next/next-modules.d.ts +12 -0
  172. package/src/next/pages.tsx +69 -0
  173. package/src/next/params.ts +6 -0
  174. package/src/next/route.ts +45 -0
  175. package/src/next/send-mail.ts +15 -0
  176. package/src/options.ts +34 -0
  177. package/src/providers/resend.ts +86 -0
  178. package/src/schema.ts +40 -0
  179. package/src/server/campaign-file.ts +101 -0
  180. package/src/server/campaigns.ts +175 -0
  181. package/src/server/deliveries.ts +155 -0
  182. package/src/server/dns.ts +153 -0
  183. package/src/server/health.ts +14 -0
  184. package/src/server/index.ts +56 -0
  185. package/src/server/list-mail.ts +42 -0
  186. package/src/server/options.ts +32 -0
  187. package/src/server/send-mail.ts +152 -0
  188. package/src/server/suppressions.ts +77 -0
  189. package/src/server/unsubscribe-link.ts +109 -0
  190. package/src/server/validate-mail.ts +68 -0
  191. package/src/testing/fake-provider.ts +66 -0
  192. package/src/testing/index.ts +4 -0
  193. package/src/testing/outbox.ts +32 -0
@@ -0,0 +1,59 @@
1
+ import type { MailingErrorCode } from "../contract.js";
2
+ import { en } from "./en.js";
3
+ /** Complete default dictionaries; apps pass partial overrides per locale. */
4
+ export declare const mailingMessages: {
5
+ en: {
6
+ errors: {
7
+ mailing: {
8
+ invalid_input: string;
9
+ rejected: string;
10
+ unavailable: string;
11
+ suppressed: string;
12
+ };
13
+ };
14
+ footer: {
15
+ text: string;
16
+ htmlLead: string;
17
+ htmlLink: string;
18
+ };
19
+ unsubscribe: {
20
+ title: string;
21
+ lead: string;
22
+ submit: string;
23
+ doneTitle: string;
24
+ doneBody: string;
25
+ invalidTitle: string;
26
+ invalidBody: string;
27
+ failed: string;
28
+ };
29
+ };
30
+ pl: {
31
+ errors: {
32
+ mailing: {
33
+ invalid_input: string;
34
+ rejected: string;
35
+ unavailable: string;
36
+ suppressed: string;
37
+ };
38
+ };
39
+ footer: {
40
+ text: string;
41
+ htmlLead: string;
42
+ htmlLink: string;
43
+ };
44
+ unsubscribe: {
45
+ title: string;
46
+ lead: string;
47
+ submit: string;
48
+ doneTitle: string;
49
+ doneBody: string;
50
+ invalidTitle: string;
51
+ invalidBody: string;
52
+ failed: string;
53
+ };
54
+ };
55
+ };
56
+ export type MailingMessages = typeof en;
57
+ /** The copy for a mailing error code; a code the dictionary does not know reads as `unavailable`. */
58
+ export declare function getMailingErrorMessage(messages: MailingMessages, code: MailingErrorCode): string;
59
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/messages/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AACvD,OAAO,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAG7B,6EAA6E;AAC7E,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAAa,CAAC;AAE1C,MAAM,MAAM,eAAe,GAAG,OAAO,EAAE,CAAC;AAExC,qGAAqG;AACrG,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,eAAe,EAAE,IAAI,EAAE,gBAAgB,GAAG,MAAM,CAIhG"}
@@ -0,0 +1,11 @@
1
+ import { en } from "./en.js";
2
+ import { pl } from "./pl.js";
3
+ /** Complete default dictionaries; apps pass partial overrides per locale. */
4
+ export const mailingMessages = { en, pl };
5
+ /** The copy for a mailing error code; a code the dictionary does not know reads as `unavailable`. */
6
+ export function getMailingErrorMessage(messages, code) {
7
+ const group = messages.errors.mailing;
8
+ const name = code.slice("mailing.".length);
9
+ return (Object.hasOwn(group, name) ? group[name] : undefined) ?? messages.errors.mailing.unavailable;
10
+ }
11
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/messages/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAC7B,OAAO,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAE7B,6EAA6E;AAC7E,MAAM,CAAC,MAAM,eAAe,GAAG,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC;AAI1C,qGAAqG;AACrG,MAAM,UAAU,sBAAsB,CAAC,QAAyB,EAAE,IAAsB;IACtF,MAAM,KAAK,GAAqC,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC;IACxE,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;IAC3C,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,WAAW,CAAC;AACvG,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { en } from "./en.js";
2
+ export declare const pl: typeof en;
3
+ //# sourceMappingURL=pl.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pl.d.ts","sourceRoot":"","sources":["../../src/messages/pl.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAGlC,eAAO,MAAM,EAAE,EAAE,OAAO,EAwBvB,CAAC"}
@@ -0,0 +1,27 @@
1
+ // Polish copy lives only in message dictionaries; the language gate exempts `messages/` folders.
2
+ export const pl = {
3
+ errors: {
4
+ mailing: {
5
+ invalid_input: "Tej wiadomości nie da się wysłać. Sprawdź adres i spróbuj ponownie.",
6
+ rejected: "Usługa pocztowa odrzuciła tę wiadomość.",
7
+ unavailable: "Usługa pocztowa nie odpowiada. Spróbuj ponownie za chwilę.",
8
+ suppressed: "Ten adres wypisał się z takich wiadomości, więc nic nie wysłano.",
9
+ },
10
+ },
11
+ footer: {
12
+ text: "Nie chcesz więcej takich wiadomości? Wypisz się tutaj:",
13
+ htmlLead: "Nie chcesz więcej takich wiadomości?",
14
+ htmlLink: "Wypisz się",
15
+ },
16
+ unsubscribe: {
17
+ title: "Wypisanie",
18
+ lead: "Przestaniesz dostawać od nas takie wiadomości. Poczta o Twoim koncie, na przykład reset hasła, nadal będzie przychodzić.",
19
+ submit: "Wypisz mnie",
20
+ doneTitle: "Wypisano",
21
+ doneBody: "Nie wyślemy Ci już takich wiadomości.",
22
+ invalidTitle: "Ten odnośnik nie działa",
23
+ invalidBody: "Otwórz jeszcze raz odnośnik z wiadomości, w całości. Jeśli dalej nie działa, odpisz na wiadomość i poproś o wypisanie.",
24
+ failed: "Nie udało się tego zapisać. Spróbuj ponownie za chwilę.",
25
+ },
26
+ };
27
+ //# sourceMappingURL=pl.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pl.js","sourceRoot":"","sources":["../../src/messages/pl.ts"],"names":[],"mappings":"AAEA,iGAAiG;AACjG,MAAM,CAAC,MAAM,EAAE,GAAc;IAC3B,MAAM,EAAE;QACN,OAAO,EAAE;YACP,aAAa,EAAE,qEAAqE;YACpF,QAAQ,EAAE,yCAAyC;YACnD,WAAW,EAAE,4DAA4D;YACzE,UAAU,EAAE,kEAAkE;SAC/E;KACF;IACD,MAAM,EAAE;QACN,IAAI,EAAE,wDAAwD;QAC9D,QAAQ,EAAE,sCAAsC;QAChD,QAAQ,EAAE,YAAY;KACvB;IACD,WAAW,EAAE;QACX,KAAK,EAAE,WAAW;QAClB,IAAI,EAAE,0HAA0H;QAChI,MAAM,EAAE,aAAa;QACrB,SAAS,EAAE,UAAU;QACrB,QAAQ,EAAE,uCAAuC;QACjD,YAAY,EAAE,yBAAyB;QACvC,WAAW,EAAE,wHAAwH;QACrI,MAAM,EAAE,yDAAyD;KAClE;CACF,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare function unsubscribeAction(formData: FormData): Promise<void>;
2
+ //# sourceMappingURL=actions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"actions.d.ts","sourceRoot":"","sources":["../../src/next/actions.ts"],"names":[],"mappings":"AAeA,wBAAsB,iBAAiB,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAmBzE"}
@@ -0,0 +1,35 @@
1
+ "use server";
2
+ // The unsubscribe page's server action. It takes the signed link from the form (the same values
3
+ // the page got in its URL), records the opt-out and redirects to the page's outcome. No session is
4
+ // involved: the signature is the authorization. Next refuses an action whose Origin does not match
5
+ // the host.
6
+ import { errorLogLabel } from "@softure-ai/core";
7
+ import { getSoftureConfig } from "@softure-ai/core/next";
8
+ import { redirect } from "next/navigation";
9
+ import { getMailingRoutes } from "../server/options.js";
10
+ import { unsubscribe } from "../server/suppressions.js";
11
+ import { RECIPIENT_PARAM, readUnsubscribeToken, SIGNATURE_PARAM } from "../server/unsubscribe-link.js";
12
+ import { getMailingContext } from "./context.js";
13
+ import { UNSUBSCRIBE_STATUS_PARAM } from "./params.js";
14
+ export async function unsubscribeAction(formData) {
15
+ const config = getSoftureConfig();
16
+ const token = readUnsubscribeToken(formData);
17
+ let status;
18
+ try {
19
+ const result = await unsubscribe(await getMailingContext(config), token, "page");
20
+ status = result.ok ? "done" : "invalid";
21
+ }
22
+ catch (error) {
23
+ // The label names the kind of failure, never its text: that can carry the link's values.
24
+ console.error(`@softure-ai/mailing: unsubscribe failed: ${errorLogLabel(error)}`);
25
+ status = "failed";
26
+ }
27
+ const query = new URLSearchParams({ [UNSUBSCRIBE_STATUS_PARAM]: status });
28
+ if (status === "failed" && token !== null) {
29
+ // The form comes back with the same link, so the person can try again.
30
+ query.set(RECIPIENT_PARAM, token.recipientKey);
31
+ query.set(SIGNATURE_PARAM, token.signature);
32
+ }
33
+ redirect(`${getMailingRoutes(config).unsubscribe}?${query.toString()}`);
34
+ }
35
+ //# sourceMappingURL=actions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"actions.js","sourceRoot":"","sources":["../../src/next/actions.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,gGAAgG;AAChG,mGAAmG;AACnG,mGAAmG;AACnG,YAAY;AACZ,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAC3C,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AACxD,OAAO,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AACxD,OAAO,EAAE,eAAe,EAAE,oBAAoB,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AACvG,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,EAAE,wBAAwB,EAA0B,MAAM,aAAa,CAAC;AAE/E,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,QAAkB;IACxD,MAAM,MAAM,GAAG,gBAAgB,EAAE,CAAC;IAClC,MAAM,KAAK,GAAG,oBAAoB,CAAC,QAAQ,CAAC,CAAC;IAC7C,IAAI,MAAyB,CAAC;IAC9B,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,WAAW,CAAC,MAAM,iBAAiB,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC;QACjF,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC;IAC1C,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,yFAAyF;QACzF,OAAO,CAAC,KAAK,CAAC,4CAA4C,aAAa,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QAClF,MAAM,GAAG,QAAQ,CAAC;IACpB,CAAC;IACD,MAAM,KAAK,GAAG,IAAI,eAAe,CAAC,EAAE,CAAC,wBAAwB,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC;IAC1E,IAAI,MAAM,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QAC1C,uEAAuE;QACvE,KAAK,CAAC,GAAG,CAAC,eAAe,EAAE,KAAK,CAAC,YAAY,CAAC,CAAC;QAC/C,KAAK,CAAC,GAAG,CAAC,eAAe,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;IAC9C,CAAC;IACD,QAAQ,CAAC,GAAG,gBAAgB,CAAC,MAAM,CAAC,CAAC,WAAW,IAAI,KAAK,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;AAC1E,CAAC"}
@@ -0,0 +1,4 @@
1
+ import { type SoftureConfig } from "@softure-ai/core";
2
+ import type { SuppressionContext } from "../server/suppressions.js";
3
+ export declare function getMailingContext(config?: SoftureConfig): Promise<SuppressionContext>;
4
+ //# sourceMappingURL=context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/next/context.ts"],"names":[],"mappings":"AAEA,OAAO,EAAe,KAAK,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAGnE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAEpE,wBAAsB,iBAAiB,CAAC,MAAM,GAAE,aAAkC,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAO/G"}
@@ -0,0 +1,14 @@
1
+ // What the adapter hands the server functions: the registered config, the process-wide database
2
+ // handle and the wall clock.
3
+ import { systemClock } from "@softure-ai/core";
4
+ import { getSoftureConfig } from "@softure-ai/core/next";
5
+ import { getConfiguredDatabase } from "@softure-ai/db";
6
+ export async function getMailingContext(config = getSoftureConfig()) {
7
+ if (config.database === null) {
8
+ // Unreachable for a validated config: the module has a database schema.
9
+ throw new Error("@softure-ai/mailing: softure.config.ts has no database; mailing needs one");
10
+ }
11
+ const { db } = await getConfiguredDatabase(config.database);
12
+ return { db, clock: systemClock, config };
13
+ }
14
+ //# sourceMappingURL=context.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.js","sourceRoot":"","sources":["../../src/next/context.ts"],"names":[],"mappings":"AAAA,gGAAgG;AAChG,6BAA6B;AAC7B,OAAO,EAAE,WAAW,EAAsB,MAAM,kBAAkB,CAAC;AACnE,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AAGvD,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,SAAwB,gBAAgB,EAAE;IAChF,IAAI,MAAM,CAAC,QAAQ,KAAK,IAAI,EAAE,CAAC;QAC7B,wEAAwE;QACxE,MAAM,IAAI,KAAK,CAAC,2EAA2E,CAAC,CAAC;IAC/F,CAAC;IACD,MAAM,EAAE,EAAE,EAAE,GAAG,MAAM,qBAAqB,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IAC5D,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,EAAE,CAAC;AAC5C,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type { Delivery, DeliverOptions, DeliveryOutcome } from "../server/deliveries.js";
2
+ /** Sends one mail at most once per scope and recipient; see `deliverOnce` in `@softure-ai/mailing/server`. */
3
+ export declare function deliverOnce(delivery: Delivery, options?: DeliverOptions): Promise<DeliveryOutcome>;
4
+ //# sourceMappingURL=deliver-once.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deliver-once.d.ts","sourceRoot":"","sources":["../../src/next/deliver-once.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAIzF,8GAA8G;AAC9G,wBAAsB,WAAW,CAAC,QAAQ,EAAE,QAAQ,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,eAAe,CAAC,CAE5G"}
@@ -0,0 +1,7 @@
1
+ import { deliverOnce as deliverOnceWithContext } from "../server/deliveries.js";
2
+ import { getMailingContext } from "./context.js";
3
+ /** Sends one mail at most once per scope and recipient; see `deliverOnce` in `@softure-ai/mailing/server`. */
4
+ export async function deliverOnce(delivery, options = {}) {
5
+ return deliverOnceWithContext(await getMailingContext(), delivery, options);
6
+ }
7
+ //# sourceMappingURL=deliver-once.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deliver-once.js","sourceRoot":"","sources":["../../src/next/deliver-once.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,WAAW,IAAI,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AAChF,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAEjD,8GAA8G;AAC9G,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,QAAkB,EAAE,UAA0B,EAAE;IAChF,OAAO,sBAAsB,CAAC,MAAM,iBAAiB,EAAE,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC;AAC9E,CAAC"}
@@ -0,0 +1,8 @@
1
+ export { unsubscribeAction } from "./actions.js";
2
+ export { deliverOnce } from "./deliver-once.js";
3
+ export { getMailingMessages } from "./messages.js";
4
+ export { UnsubscribePage, type UnsubscribePageProps } from "./pages.js";
5
+ export { UNSUBSCRIBE_STATUS_PARAM, type UnsubscribeStatus } from "./params.js";
6
+ export { getUnsubscribeRoute, postUnsubscribeRoute } from "./route.js";
7
+ export { sendMail } from "./send-mail.js";
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/next/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,EAAE,eAAe,EAAE,KAAK,oBAAoB,EAAE,MAAM,YAAY,CAAC;AACxE,OAAO,EAAE,wBAAwB,EAAE,KAAK,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAC/E,OAAO,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AACvE,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC"}
@@ -0,0 +1,11 @@
1
+ // The Next.js adapter of @softure-ai/mailing: sending (and sending once, through the ledger) on the
2
+ // registered configuration, the unsubscribe page with its action, and the one-click route
3
+ // (docs/02-module-standard.md §8).
4
+ export { unsubscribeAction } from "./actions.js";
5
+ export { deliverOnce } from "./deliver-once.js";
6
+ export { getMailingMessages } from "./messages.js";
7
+ export { UnsubscribePage } from "./pages.js";
8
+ export { UNSUBSCRIBE_STATUS_PARAM } from "./params.js";
9
+ export { getUnsubscribeRoute, postUnsubscribeRoute } from "./route.js";
10
+ export { sendMail } from "./send-mail.js";
11
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/next/index.ts"],"names":[],"mappings":"AAAA,oGAAoG;AACpG,0FAA0F;AAC1F,mCAAmC;AACnC,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,EAAE,eAAe,EAA6B,MAAM,YAAY,CAAC;AACxE,OAAO,EAAE,wBAAwB,EAA0B,MAAM,aAAa,CAAC;AAC/E,OAAO,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AACvE,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC"}
@@ -0,0 +1,5 @@
1
+ import type { SoftureConfig } from "@softure-ai/core";
2
+ import type { MailingMessages } from "../messages/index.js";
3
+ /** The module's copy in the app's locale, with the app's overrides applied. */
4
+ export declare function getMailingMessages(config: SoftureConfig): MailingMessages;
5
+ //# sourceMappingURL=messages.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../../src/next/messages.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAG5D,+EAA+E;AAC/E,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,aAAa,GAAG,eAAe,CAGzE"}
@@ -0,0 +1,7 @@
1
+ import { getMailingModule } from "../server/options.js";
2
+ /** The module's copy in the app's locale, with the app's overrides applied. */
3
+ export function getMailingMessages(config) {
4
+ // The module factory merged the dictionaries; their shape is the module's own.
5
+ return getMailingModule(config).messages[config.locale];
6
+ }
7
+ //# sourceMappingURL=messages.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"messages.js","sourceRoot":"","sources":["../../src/next/messages.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAExD,+EAA+E;AAC/E,MAAM,UAAU,kBAAkB,CAAC,MAAqB;IACtD,+EAA+E;IAC/E,OAAO,gBAAgB,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAoB,CAAC;AAC7E,CAAC"}
@@ -0,0 +1,7 @@
1
+ type SearchParams = Promise<Record<string, string | string[] | undefined>>;
2
+ export interface UnsubscribePageProps {
3
+ readonly searchParams?: SearchParams;
4
+ }
5
+ export declare function UnsubscribePage({ searchParams }: UnsubscribePageProps): Promise<import("react").JSX.Element>;
6
+ export {};
7
+ //# sourceMappingURL=pages.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pages.d.ts","sourceRoot":"","sources":["../../src/next/pages.tsx"],"names":[],"mappings":"AAaA,KAAK,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC,CAAC;AAE3E,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,YAAY,CAAC,EAAE,YAAY,CAAC;CACtC;AAyBD,wBAAsB,eAAe,CAAC,EAAE,YAAY,EAAE,EAAE,oBAAoB,wCA0B3E"}
@@ -0,0 +1,40 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // The unsubscribe page, ready to mount with one line:
3
+ // `export { UnsubscribePage as default } from "@softure-ai/mailing/next"`.
4
+ // A server component without client JavaScript: the button is a plain form posting a server
5
+ // action, so it works in any browser a mail client opens. Opening the page changes nothing (mail
6
+ // scanners open links too) and does not check the signature: the submit does.
7
+ import { getSoftureConfig } from "@softure-ai/core/next";
8
+ import { Button, Card, FormError } from "@softure-ai/ui";
9
+ import { RECIPIENT_PARAM, SIGNATURE_PARAM } from "../server/unsubscribe-link.js";
10
+ import { unsubscribeAction } from "./actions.js";
11
+ import { getMailingMessages } from "./messages.js";
12
+ import { UNSUBSCRIBE_STATUS_PARAM } from "./params.js";
13
+ const LAYOUT_CLASS = "sft:mx-auto sft:box-border sft:w-full sft:sm:max-w-md sft:px-4 sft:py-4";
14
+ const FORM_CLASS = "sft:flex sft:flex-col sft:gap-3 sft:font-sans";
15
+ function readParam(params, name) {
16
+ const value = params[name];
17
+ return Array.isArray(value) ? value[0] : value;
18
+ }
19
+ /**
20
+ * The link's token stays out of other sites' Referer headers; `same-origin` (not `no-referrer`)
21
+ * keeps the Origin header Next checks on server actions.
22
+ */
23
+ function UnsubscribeLayout({ title, lead, children }) {
24
+ return (_jsxs("main", { className: LAYOUT_CLASS, children: [_jsx("meta", { name: "referrer", content: "same-origin" }), _jsx(Card, { title: title, subtitle: lead, children: children })] }));
25
+ }
26
+ export async function UnsubscribePage({ searchParams }) {
27
+ const config = getSoftureConfig();
28
+ const messages = getMailingMessages(config).unsubscribe;
29
+ const params = (await searchParams) ?? {};
30
+ const status = readParam(params, UNSUBSCRIBE_STATUS_PARAM);
31
+ if (status === "done")
32
+ return _jsx(UnsubscribeLayout, { title: messages.doneTitle, lead: messages.doneBody });
33
+ const recipientKey = readParam(params, RECIPIENT_PARAM) ?? "";
34
+ const signature = readParam(params, SIGNATURE_PARAM) ?? "";
35
+ if (status === "invalid" || recipientKey === "" || signature === "") {
36
+ return _jsx(UnsubscribeLayout, { title: messages.invalidTitle, lead: messages.invalidBody });
37
+ }
38
+ return (_jsx(UnsubscribeLayout, { title: messages.title, lead: messages.lead, children: _jsxs("form", { action: unsubscribeAction, className: FORM_CLASS, children: [_jsx("input", { type: "hidden", name: RECIPIENT_PARAM, value: recipientKey }), _jsx("input", { type: "hidden", name: SIGNATURE_PARAM, value: signature }), _jsx(FormError, { message: status === "failed" ? messages.failed : undefined }), _jsx("div", { children: _jsx(Button, { type: "submit", variant: "primary", children: messages.submit }) })] }) }));
39
+ }
40
+ //# sourceMappingURL=pages.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pages.js","sourceRoot":"","sources":["../../src/next/pages.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ /** Set by the action after a submit: `done`, `invalid` or `failed`. */
2
+ export declare const UNSUBSCRIBE_STATUS_PARAM = "status";
3
+ export type UnsubscribeStatus = "done" | "invalid" | "failed";
4
+ //# sourceMappingURL=params.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"params.d.ts","sourceRoot":"","sources":["../../src/next/params.ts"],"names":[],"mappings":"AAEA,uEAAuE;AACvE,eAAO,MAAM,wBAAwB,WAAW,CAAC;AAEjD,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,SAAS,GAAG,QAAQ,CAAC"}
@@ -0,0 +1,4 @@
1
+ // The query parameter the unsubscribe page and its action pass between each other.
2
+ /** Set by the action after a submit: `done`, `invalid` or `failed`. */
3
+ export const UNSUBSCRIBE_STATUS_PARAM = "status";
4
+ //# sourceMappingURL=params.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"params.js","sourceRoot":"","sources":["../../src/next/params.ts"],"names":[],"mappings":"AAAA,mFAAmF;AAEnF,uEAAuE;AACvE,MAAM,CAAC,MAAM,wBAAwB,GAAG,QAAQ,CAAC"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Records the opt-out of the link in the URL. The body (`List-Unsubscribe=One-Click`) is not read:
3
+ * anyone can send it, the signature is the proof. 200 once recorded (also again), 400 for a link
4
+ * that does not verify, 500 when the database fails: the client may retry, and 200 would claim an
5
+ * opt-out that did not happen.
6
+ */
7
+ export declare function postUnsubscribeRoute(request: Request): Promise<Response>;
8
+ /**
9
+ * A client without RFC 8058 opens the header's URL in a browser: send it to the page with the
10
+ * same link (303, so nothing changes on GET). A 405 would also put the token in access logs that
11
+ * record failed requests.
12
+ */
13
+ export declare function getUnsubscribeRoute(request: Request): Response;
14
+ //# sourceMappingURL=route.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route.d.ts","sourceRoot":"","sources":["../../src/next/route.ts"],"names":[],"mappings":"AAmBA;;;;;GAKG;AACH,wBAAsB,oBAAoB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAS9E;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,GAAG,QAAQ,CAG9D"}
@@ -0,0 +1,44 @@
1
+ // The RFC 8058 one-click route. Mount both handlers with a rename in
2
+ // app/api/mailing/unsubscribe/route.ts:
3
+ // `export { postUnsubscribeRoute as POST, getUnsubscribeRoute as GET } from "@softure-ai/mailing/next"`.
4
+ //
5
+ // Mail clients (Gmail, Outlook, Apple Mail) POST here from their own servers: no cookies, no
6
+ // server action id, a foreign Origin. That is why this is a route handler and not an action. It
7
+ // needs no session and must stay outside any auth guard. There is no per-IP rate limit on
8
+ // purpose: a few provider addresses act for millions of people, so a bucket would refuse real
9
+ // unsubscribes. The link is checked by shape and signature before the database is touched, and
10
+ // the only write is an idempotent insert.
11
+ import { errorLogLabel } from "@softure-ai/core";
12
+ import { getSoftureConfig } from "@softure-ai/core/next";
13
+ import { getMailingRoutes } from "../server/options.js";
14
+ import { unsubscribe } from "../server/suppressions.js";
15
+ import { readUnsubscribeToken } from "../server/unsubscribe-link.js";
16
+ import { getMailingContext } from "./context.js";
17
+ const NO_STORE = { "cache-control": "no-store" };
18
+ /**
19
+ * Records the opt-out of the link in the URL. The body (`List-Unsubscribe=One-Click`) is not read:
20
+ * anyone can send it, the signature is the proof. 200 once recorded (also again), 400 for a link
21
+ * that does not verify, 500 when the database fails: the client may retry, and 200 would claim an
22
+ * opt-out that did not happen.
23
+ */
24
+ export async function postUnsubscribeRoute(request) {
25
+ const token = readUnsubscribeToken(new URL(request.url).searchParams);
26
+ try {
27
+ const result = await unsubscribe(await getMailingContext(), token, "one-click");
28
+ return new Response(null, { status: result.ok ? 200 : 400, headers: NO_STORE });
29
+ }
30
+ catch (error) {
31
+ console.error(`@softure-ai/mailing: one-click unsubscribe failed: ${errorLogLabel(error)}`);
32
+ return new Response(null, { status: 500, headers: NO_STORE });
33
+ }
34
+ }
35
+ /**
36
+ * A client without RFC 8058 opens the header's URL in a browser: send it to the page with the
37
+ * same link (303, so nothing changes on GET). A 405 would also put the token in access logs that
38
+ * record failed requests.
39
+ */
40
+ export function getUnsubscribeRoute(request) {
41
+ const { search } = new URL(request.url);
42
+ return new Response(null, { status: 303, headers: { ...NO_STORE, location: `${getMailingRoutes(getSoftureConfig()).unsubscribe}${search}` } });
43
+ }
44
+ //# sourceMappingURL=route.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route.js","sourceRoot":"","sources":["../../src/next/route.ts"],"names":[],"mappings":"AAAA,qEAAqE;AACrE,wCAAwC;AACxC,yGAAyG;AACzG,EAAE;AACF,6FAA6F;AAC7F,gGAAgG;AAChG,0FAA0F;AAC1F,8FAA8F;AAC9F,+FAA+F;AAC/F,0CAA0C;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AACxD,OAAO,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AACxD,OAAO,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AACrE,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAEjD,MAAM,QAAQ,GAAG,EAAE,eAAe,EAAE,UAAU,EAAE,CAAC;AAEjD;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAC,OAAgB;IACzD,MAAM,KAAK,GAAG,oBAAoB,CAAC,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,YAAY,CAAC,CAAC;IACtE,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,WAAW,CAAC,MAAM,iBAAiB,EAAE,EAAE,KAAK,EAAE,WAAW,CAAC,CAAC;QAChF,OAAO,IAAI,QAAQ,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,CAAC;IAClF,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,CAAC,sDAAsD,aAAa,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QAC5F,OAAO,IAAI,QAAQ,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,CAAC;IAChE,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CAAC,OAAgB;IAClD,MAAM,EAAE,MAAM,EAAE,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACxC,OAAO,IAAI,QAAQ,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,EAAE,GAAG,QAAQ,EAAE,QAAQ,EAAE,GAAG,gBAAgB,CAAC,gBAAgB,EAAE,CAAC,CAAC,WAAW,GAAG,MAAM,EAAE,EAAE,EAAE,CAAC,CAAC;AACjJ,CAAC"}
@@ -0,0 +1,4 @@
1
+ import { type OutgoingMail, type SendMailOptions, type SendMailResult } from "../contract.js";
2
+ /** Sends one mail with the registered config; see `sendMail` in `@softure-ai/mailing/server`. */
3
+ export declare function sendMail(mail: OutgoingMail, options?: SendMailOptions): Promise<SendMailResult>;
4
+ //# sourceMappingURL=send-mail.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"send-mail.d.ts","sourceRoot":"","sources":["../../src/next/send-mail.ts"],"names":[],"mappings":"AAIA,OAAO,EAAsB,KAAK,YAAY,EAAE,KAAK,eAAe,EAAE,KAAK,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAIlH,iGAAiG;AACjG,wBAAsB,QAAQ,CAAC,IAAI,EAAE,YAAY,EAAE,OAAO,GAAE,eAAoB,GAAG,OAAO,CAAC,cAAc,CAAC,CAKzG"}
@@ -0,0 +1,16 @@
1
+ // `sendMail` on the app's registered configuration, for server actions and route handlers. List
2
+ // mail also gets the shared database handle (its suppression check); transactional mail never
3
+ // touches the database.
4
+ import { getSoftureConfig } from "@softure-ai/core/next";
5
+ import { TRANSACTIONAL_KIND } from "../contract.js";
6
+ import { sendMail as sendMailWithContext } from "../server/send-mail.js";
7
+ import { getMailingContext } from "./context.js";
8
+ /** Sends one mail with the registered config; see `sendMail` in `@softure-ai/mailing/server`. */
9
+ export async function sendMail(mail, options = {}) {
10
+ const config = getSoftureConfig();
11
+ if ((mail.kind ?? TRANSACTIONAL_KIND) === TRANSACTIONAL_KIND)
12
+ return sendMailWithContext({ config }, mail, options);
13
+ const { db } = await getMailingContext(config);
14
+ return sendMailWithContext({ config, db }, mail, options);
15
+ }
16
+ //# sourceMappingURL=send-mail.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"send-mail.js","sourceRoot":"","sources":["../../src/next/send-mail.ts"],"names":[],"mappings":"AAAA,gGAAgG;AAChG,8FAA8F;AAC9F,wBAAwB;AACxB,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,EAAE,kBAAkB,EAAgE,MAAM,gBAAgB,CAAC;AAClH,OAAO,EAAE,QAAQ,IAAI,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AACzE,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAEjD,iGAAiG;AACjG,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,IAAkB,EAAE,UAA2B,EAAE;IAC9E,MAAM,MAAM,GAAG,gBAAgB,EAAE,CAAC;IAClC,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,kBAAkB,CAAC,KAAK,kBAAkB;QAAE,OAAO,mBAAmB,CAAC,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;IACpH,MAAM,EAAE,EAAE,EAAE,GAAG,MAAM,iBAAiB,CAAC,MAAM,CAAC,CAAC;IAC/C,OAAO,mBAAmB,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;AAC5D,CAAC"}
@@ -0,0 +1,13 @@
1
+ import { z } from "zod";
2
+ import type { MailProvider, OnUnsubscribedHook } from "./contract.js";
3
+ export declare const DEFAULT_TIMEOUT_MS = 10000;
4
+ export declare const mailingOptionsSchema: z.ZodObject<{
5
+ from: z.ZodString;
6
+ replyTo: z.ZodOptional<z.ZodString>;
7
+ provider: z.ZodCustom<MailProvider, MailProvider>;
8
+ timeoutMs: z.ZodDefault<z.ZodNumber>;
9
+ onUnsubscribed: z.ZodOptional<z.ZodCustom<OnUnsubscribedHook, OnUnsubscribedHook>>;
10
+ }, z.core.$strict>;
11
+ export type MailingOptionsInput = z.input<typeof mailingOptionsSchema>;
12
+ export type MailingOptions = z.output<typeof mailingOptionsSchema>;
13
+ //# sourceMappingURL=options.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../src/options.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,KAAK,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAEtE,eAAO,MAAM,kBAAkB,QAAS,CAAC;AAQzC,eAAO,MAAM,oBAAoB;;;;;;kBAiB/B,CAAC;AAEH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AACvE,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,oBAAoB,CAAC,CAAC"}
@@ -0,0 +1,29 @@
1
+ // The options an app passes to `mailing({ ... })` in softure.config.ts, parsed at startup.
2
+ import { z } from "zod";
3
+ import { isMailbox, isSingleAddress } from "./address.js";
4
+ export const DEFAULT_TIMEOUT_MS = 10_000;
5
+ function isMailProvider(value) {
6
+ if (typeof value !== "object" || value === null)
7
+ return false;
8
+ const candidate = value;
9
+ return typeof candidate.name === "string" && candidate.name !== "" && typeof candidate.send === "function";
10
+ }
11
+ export const mailingOptionsSchema = z.strictObject({
12
+ /**
13
+ * The sender of every mail: `hello@example.com` or `Plan <hello@example.com>`. It must be on a
14
+ * domain the provider signs (DKIM), or DMARC rejects the mail.
15
+ */
16
+ from: z.string().trim().refine(isMailbox, "must be an address or Name <address>, e.g. Plan <hello@example.com>"),
17
+ /** Where replies go; may sit on another domain than `from` (DMARC does not check it). */
18
+ replyTo: z.string().trim().refine(isSingleAddress, "must be one address, e.g. support@example.com").optional(),
19
+ /** The adapter that delivers mail: `resend()`, or `fakeMailProvider()` from `/testing`. */
20
+ provider: z.custom(isMailProvider, "must be a mail provider such as resend()"),
21
+ /** How long one send may take before it reads as `mailing.unavailable`. */
22
+ timeoutMs: z.number().int().min(1_000).max(60_000).default(DEFAULT_TIMEOUT_MS),
23
+ /**
24
+ * Reacts to an unsubscribe in its transaction, e.g. `withdrawWaitlistConsents` of
25
+ * `@softure-ai/waitlist/server`, which records the withdrawal in privacy's consent ledger.
26
+ */
27
+ onUnsubscribed: z.custom((value) => typeof value === "function", "must be a function").optional(),
28
+ });
29
+ //# sourceMappingURL=options.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"options.js","sourceRoot":"","sources":["../src/options.ts"],"names":[],"mappings":"AAAA,2FAA2F;AAC3F,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAG1D,MAAM,CAAC,MAAM,kBAAkB,GAAG,MAAM,CAAC;AAEzC,SAAS,cAAc,CAAC,KAAc;IACpC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IAC9D,MAAM,SAAS,GAAG,KAA2C,CAAC;IAC9D,OAAO,OAAO,SAAS,CAAC,IAAI,KAAK,QAAQ,IAAI,SAAS,CAAC,IAAI,KAAK,EAAE,IAAI,OAAO,SAAS,CAAC,IAAI,KAAK,UAAU,CAAC;AAC7G,CAAC;AAED,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,CAAC,YAAY,CAAC;IACjD;;;OAGG;IACH,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,MAAM,CAAC,SAAS,EAAE,qEAAqE,CAAC;IAChH,yFAAyF;IACzF,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,MAAM,CAAC,eAAe,EAAE,+CAA+C,CAAC,CAAC,QAAQ,EAAE;IAC9G,2FAA2F;IAC3F,QAAQ,EAAE,CAAC,CAAC,MAAM,CAAe,cAAc,EAAE,0CAA0C,CAAC;IAC5F,2EAA2E;IAC3E,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,kBAAkB,CAAC;IAC9E;;;OAGG;IACH,cAAc,EAAE,CAAC,CAAC,MAAM,CAAqB,CAAC,KAAK,EAAE,EAAE,CAAC,OAAO,KAAK,KAAK,UAAU,EAAE,oBAAoB,CAAC,CAAC,QAAQ,EAAE;CACtH,CAAC,CAAC"}
@@ -0,0 +1,17 @@
1
+ import type { MailProvider } from "../contract.js";
2
+ export declare const RESEND_ENDPOINT = "https://api.resend.com/emails";
3
+ export declare const RESEND_API_KEY_ENV = "RESEND_API_KEY";
4
+ export interface ResendOptions {
5
+ /**
6
+ * The API key. Default: `process.env.RESEND_API_KEY`, read on every send, so the configuration
7
+ * loads at build time without the secret.
8
+ */
9
+ readonly apiKey?: string;
10
+ /** Default: Resend's `POST /emails`. For a proxy or a test server. */
11
+ readonly endpoint?: string;
12
+ /** Injected in tests; default: the global `fetch`. */
13
+ readonly fetch?: typeof fetch;
14
+ }
15
+ /** Sends through Resend's HTTP API. */
16
+ export declare function resend(options?: ResendOptions): MailProvider;
17
+ //# sourceMappingURL=resend.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resend.d.ts","sourceRoot":"","sources":["../../src/providers/resend.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAoC,MAAM,gBAAgB,CAAC;AAErF,eAAO,MAAM,eAAe,kCAAkC,CAAC;AAC/D,eAAO,MAAM,kBAAkB,mBAAmB,CAAC;AAEnD,MAAM,WAAW,aAAa;IAC5B;;;OAGG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,sEAAsE;IACtE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,sDAAsD;IACtD,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;CAC/B;AAQD,uCAAuC;AACvC,wBAAgB,MAAM,CAAC,OAAO,GAAE,aAAkB,GAAG,YAAY,CA0BhE"}
@@ -0,0 +1,64 @@
1
+ export const RESEND_ENDPOINT = "https://api.resend.com/emails";
2
+ export const RESEND_API_KEY_ENV = "RESEND_API_KEY";
3
+ /** Resend's name for an idempotency key that another request is still using: retry later. */
4
+ const CONCURRENT_IDEMPOTENT_REQUESTS = "concurrent_idempotent_requests";
5
+ /** Statuses below 500 that mean "try again later", not "this mail is refused". */
6
+ const RETRYABLE_CLIENT_STATUSES = new Set([408, 429]);
7
+ /** Sends through Resend's HTTP API. */
8
+ export function resend(options = {}) {
9
+ const endpoint = options.endpoint ?? RESEND_ENDPOINT;
10
+ return {
11
+ name: "resend",
12
+ send: async (message, { signal }) => {
13
+ const apiKey = (options.apiKey ?? process.env[RESEND_API_KEY_ENV] ?? "").trim();
14
+ if (apiKey === "") {
15
+ // A deployment without the key cannot send anything until it is set; never a caller error.
16
+ console.error(`mailing: resend has no API key; set ${RESEND_API_KEY_ENV} or pass resend({ apiKey })`);
17
+ return { status: "unavailable" };
18
+ }
19
+ const fetchImpl = options.fetch ?? fetch;
20
+ const response = await fetchImpl(endpoint, {
21
+ method: "POST",
22
+ headers: {
23
+ Authorization: `Bearer ${apiKey}`,
24
+ "Content-Type": "application/json",
25
+ ...(message.idempotencyKey === null ? {} : { "Idempotency-Key": message.idempotencyKey }),
26
+ },
27
+ body: JSON.stringify(toResendBody(message)),
28
+ signal,
29
+ cache: "no-store",
30
+ });
31
+ return readOutcome(response);
32
+ },
33
+ };
34
+ }
35
+ function toResendBody(message) {
36
+ return {
37
+ from: message.from,
38
+ to: [message.to],
39
+ subject: message.subject,
40
+ text: message.text,
41
+ ...(message.html === null ? {} : { html: message.html }),
42
+ ...(message.replyTo === null ? {} : { reply_to: message.replyTo }),
43
+ ...(Object.keys(message.headers).length === 0 ? {} : { headers: message.headers }),
44
+ };
45
+ }
46
+ async function readOutcome(response) {
47
+ const payload = await response.json().catch(() => null);
48
+ if (response.ok) {
49
+ const id = readString(payload, "id");
50
+ return id === null ? { status: "unavailable", httpStatus: response.status } : { status: "sent", id };
51
+ }
52
+ // Only the error's `name` (an enum) is read: its `message` can echo the address back.
53
+ const isRetryable = response.status >= 500 ||
54
+ RETRYABLE_CLIENT_STATUSES.has(response.status) ||
55
+ (response.status === 409 && readString(payload, "name") === CONCURRENT_IDEMPOTENT_REQUESTS);
56
+ return { status: isRetryable ? "unavailable" : "rejected", httpStatus: response.status };
57
+ }
58
+ function readString(payload, key) {
59
+ if (typeof payload !== "object" || payload === null || !Object.hasOwn(payload, key))
60
+ return null;
61
+ const value = payload[key];
62
+ return typeof value === "string" && value !== "" ? value : null;
63
+ }
64
+ //# sourceMappingURL=resend.js.map