@softure-ai/mailing 0.2.0 → 0.2.1

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 (47) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +73 -5
  3. package/dist/contract.d.ts +13 -1
  4. package/dist/contract.d.ts.map +1 -1
  5. package/dist/contract.js.map +1 -1
  6. package/dist/index.d.ts +4 -2
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +3 -3
  9. package/dist/index.js.map +1 -1
  10. package/dist/jobs/delivery-job.d.ts +72 -0
  11. package/dist/jobs/delivery-job.d.ts.map +1 -0
  12. package/dist/jobs/delivery-job.js +85 -0
  13. package/dist/jobs/delivery-job.js.map +1 -0
  14. package/dist/jobs/index.d.ts +2 -0
  15. package/dist/jobs/index.d.ts.map +1 -0
  16. package/dist/jobs/index.js +4 -0
  17. package/dist/jobs/index.js.map +1 -0
  18. package/dist/messages/en.d.ts +2 -0
  19. package/dist/messages/en.d.ts.map +1 -1
  20. package/dist/messages/en.js +2 -0
  21. package/dist/messages/en.js.map +1 -1
  22. package/dist/messages/index.d.ts +4 -2
  23. package/dist/messages/index.d.ts.map +1 -1
  24. package/dist/messages/index.js.map +1 -1
  25. package/dist/providers/resend.d.ts +5 -0
  26. package/dist/providers/resend.d.ts.map +1 -1
  27. package/dist/providers/resend.js +23 -3
  28. package/dist/providers/resend.js.map +1 -1
  29. package/dist/server/deliveries.d.ts +6 -1
  30. package/dist/server/deliveries.d.ts.map +1 -1
  31. package/dist/server/deliveries.js +3 -2
  32. package/dist/server/deliveries.js.map +1 -1
  33. package/dist/server/send-mail.d.ts +2 -1
  34. package/dist/server/send-mail.d.ts.map +1 -1
  35. package/dist/server/send-mail.js +17 -5
  36. package/dist/server/send-mail.js.map +1 -1
  37. package/module.json +2 -2
  38. package/package.json +12 -1
  39. package/src/contract.ts +14 -3
  40. package/src/index.ts +4 -3
  41. package/src/jobs/delivery-job.ts +129 -0
  42. package/src/jobs/index.ts +12 -0
  43. package/src/messages/en.ts +2 -0
  44. package/src/messages/index.ts +2 -2
  45. package/src/providers/resend.ts +22 -3
  46. package/src/server/deliveries.ts +9 -5
  47. package/src/server/send-mail.ts +18 -5
@@ -37,7 +37,8 @@ export interface MailContext {
37
37
  * `invalid_input` (fix the input), `rejected` (do not retry), `unavailable` (retry later with the
38
38
  * same `idempotencyKey`), `suppressed` (a list mail to someone who unsubscribed; do not retry),
39
39
  * `provider_refused` / `quota_exceeded` (the account cannot send: stop, fix or wait, then retry). A
40
- * failure the provider answered with an HTTP status carries it as `httpStatus`.
40
+ * failure the provider answered with an HTTP status carries it as `httpStatus`, and the wait it asked for
41
+ * (`Retry-After`) as `retryAfterSeconds`.
41
42
  * Throws only when the module is not enabled, or for list mail without `context.db`.
42
43
  */
43
44
  export async function sendMail(context: MailContext, mail: OutgoingMail, options: SendMailOptions = {}): Promise<SendMailResult> {
@@ -64,7 +65,11 @@ export async function sendMail(context: MailContext, mail: OutgoingMail, options
64
65
  }
65
66
  const code = FAILURE_CODES[outcome.status];
66
67
  logFailure(provider, code, { status: outcome.httpStatus });
67
- return outcome.httpStatus === undefined ? err(code) : { ...err(code), httpStatus: outcome.httpStatus };
68
+ return {
69
+ ...err(code),
70
+ ...(outcome.httpStatus === undefined ? {} : { httpStatus: outcome.httpStatus }),
71
+ ...(outcome.retryAfterSeconds === undefined ? {} : { retryAfterSeconds: outcome.retryAfterSeconds }),
72
+ };
68
73
  }
69
74
 
70
75
  /** Why `previewMail` cannot render a mail: invalid input (with the fields), or list mail without the secret. */
@@ -171,17 +176,25 @@ async function callProvider(provider: MailProvider, message: Parameters<MailProv
171
176
  /** A provider's answer, narrowed: anything that is not a known outcome reads as `unavailable`. */
172
177
  function readOutcome(outcome: unknown): ProviderOutcome {
173
178
  if (typeof outcome !== "object" || outcome === null) return { status: "unavailable" };
174
- const { status, id, httpStatus } = outcome as { status?: unknown; id?: unknown; httpStatus?: unknown };
179
+ const { status, id, httpStatus, retryAfterSeconds } = outcome as { status?: unknown; id?: unknown; httpStatus?: unknown; retryAfterSeconds?: unknown };
175
180
  if (status === "sent") {
176
181
  return typeof id === "string" && id !== "" ? { status, id } : { status: "unavailable" };
177
182
  }
178
183
  if (typeof status === "string" && Object.hasOwn(FAILURE_CODES, status)) {
179
- const failure = status as ProviderFailureStatus;
180
- return isHttpStatus(httpStatus) ? { status: failure, httpStatus } : { status: failure };
184
+ return {
185
+ status: status as ProviderFailureStatus,
186
+ ...(isHttpStatus(httpStatus) ? { httpStatus } : {}),
187
+ ...(isRetryAfter(retryAfterSeconds) ? { retryAfterSeconds } : {}),
188
+ };
181
189
  }
182
190
  return { status: "unavailable" };
183
191
  }
184
192
 
193
+ /** A provider's `Retry-After` in whole seconds; anything else is dropped. */
194
+ function isRetryAfter(value: unknown): value is number {
195
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
196
+ }
197
+
185
198
  function isHttpStatus(value: unknown): value is number {
186
199
  return typeof value === "number" && Number.isInteger(value) && value >= 100 && value <= 599;
187
200
  }