@brilab-mailer/contracts 0.0.5-5 → 0.1.0

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/index.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  export * from './lib/mailer-contracts.js';
2
2
  export * from './lib/mailer-provider.js';
3
+ export * from './lib/mailer-send-result.js';
4
+ export * from './lib/mailer-send-error.js';
3
5
  export * from './lib/mailer-templates.js';
4
6
  export * from './lib/mailer-config-properties.js';
5
7
  export * from './lib/mailer-messages.js';
package/index.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,2BAA2B,CAAC;AAC1C,cAAc,0BAA0B,CAAC;AACzC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,mCAAmC,CAAC;AAClD,cAAc,0BAA0B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,2BAA2B,CAAC;AAC1C,cAAc,0BAA0B,CAAC;AACzC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,mCAAmC,CAAC;AAClD,cAAc,0BAA0B,CAAC"}
package/index.js CHANGED
@@ -1,5 +1,7 @@
1
1
  export * from './lib/mailer-contracts.js';
2
2
  export * from './lib/mailer-provider.js';
3
+ export * from './lib/mailer-send-result.js';
4
+ export * from './lib/mailer-send-error.js';
3
5
  export * from './lib/mailer-templates.js';
4
6
  export * from './lib/mailer-config-properties.js';
5
7
  export * from './lib/mailer-messages.js';
@@ -1,15 +1,35 @@
1
- type MailerSender = {
1
+ /** An email address with an optional display name. */
2
+ export type MailerAddress = {
2
3
  email: string;
3
4
  name?: string;
4
5
  };
5
- type MailerRecipient = {
6
- email: string;
6
+ /** Backwards-compatible aliases. */
7
+ export type MailerSender = MailerAddress;
8
+ export type MailerRecipient = MailerAddress;
9
+ export type MailerAttachment = {
10
+ filename: string;
11
+ /** File content as a Buffer or base64 string. Mutually exclusive with `path`. */
12
+ content?: string | Buffer;
13
+ /** Local/remote path to a hosted file. Mutually exclusive with `content`. */
14
+ path?: string;
15
+ contentType?: string;
16
+ /** Content-ID for inline images referenced via `cid:`. */
17
+ contentId?: string;
18
+ disposition?: 'attachment' | 'inline';
7
19
  };
8
20
  export type MailerMessagesBuilderDraft = {
9
- from?: MailerSender;
10
- to?: MailerRecipient[];
21
+ from?: MailerAddress;
22
+ to?: MailerAddress[];
23
+ cc?: MailerAddress[];
24
+ bcc?: MailerAddress[];
25
+ replyTo?: MailerAddress;
11
26
  subject?: string;
12
27
  headers?: Record<string, string>;
28
+ attachments?: MailerAttachment[];
29
+ /** Provider-level tags/categories (e.g. Resend tags, SendGrid categories). */
30
+ tags?: Record<string, string>;
31
+ /** Stable key for safe retries on providers that support idempotency. */
32
+ idempotencyKey?: string;
13
33
  meta: Record<string, any>;
14
34
  text?: string | Buffer;
15
35
  html?: string | Buffer;
@@ -26,8 +46,8 @@ export type MailerMessagesContent = {
26
46
  html?: string | Buffer;
27
47
  };
28
48
  export type MailerMessagesResolved = Omit<MailerMessagesBuilderDraft, 'text' | 'html' | 'from' | 'to'> & {
29
- from: MailerSender;
30
- to: MailerRecipient[];
49
+ from: MailerAddress;
50
+ to: MailerAddress[];
31
51
  subject: string;
32
52
  content: MailerMessagesContent;
33
53
  };
@@ -35,25 +55,41 @@ export declare class MailerMessagesBuilder {
35
55
  private readonly draft;
36
56
  protected generateDraft(): MailerMessagesBuilderDraft;
37
57
  protected prepare(): MailerMessagesResolved;
38
- setFrom(email: MailerSender["email"], name?: MailerSender['name']): this;
39
- setFrom(from: MailerSender): this;
40
- private resolveRecipientToItem;
41
- addTo(email: MailerRecipient["email"]): this;
42
- addTo(emails: MailerRecipient["email"][]): this;
43
- addTo(recipient: MailerRecipient): this;
44
- addTo(recipients: MailerRecipient[]): this;
45
- setTo(email: MailerRecipient["email"]): this;
46
- setTo(emails: MailerRecipient["email"][]): this;
47
- setTo(recipient: MailerRecipient): this;
48
- setTo(recipients: MailerRecipient[]): this;
58
+ setFrom(email: MailerAddress["email"], name?: MailerAddress['name']): this;
59
+ setFrom(from: MailerAddress): this;
60
+ setReplyTo(email: MailerAddress["email"], name?: MailerAddress['name']): this;
61
+ setReplyTo(replyTo: MailerAddress): this;
62
+ private resolveAddress;
63
+ private toAddressList;
64
+ /** Merge new recipients into an existing list, de-duplicating by email. */
65
+ private mergeAddresses;
66
+ addTo(input: MailerAddress[] | MailerAddress | string[] | string): this;
67
+ setTo(input: MailerAddress[] | MailerAddress | string[] | string): this;
68
+ addCc(input: MailerAddress[] | MailerAddress | string[] | string): this;
69
+ setCc(input: MailerAddress[] | MailerAddress | string[] | string): this;
70
+ addBcc(input: MailerAddress[] | MailerAddress | string[] | string): this;
71
+ setBcc(input: MailerAddress[] | MailerAddress | string[] | string): this;
49
72
  setSubject(subject: string): this;
73
+ setHeader(name: string, value: string): this;
74
+ setHeaders(headers: Record<string, string>): this;
75
+ addAttachment(attachment: MailerAttachment): this;
76
+ setTag(name: string, value: string): this;
77
+ setIdempotencyKey(key: string): this;
50
78
  setText(text: string | Buffer): this;
51
79
  setHtml(html: string | Buffer): this;
52
80
  }
81
+ /** Normalizes a string/Buffer body to a plain string. */
53
82
  export declare const normalizeDataBodyString: <Body extends string | Buffer>(body?: Body) => string;
54
- export declare const segmentedDataContentPayload: <Response extends Base, Keys extends {
55
- text: keyof Response;
56
- html: keyof Response;
57
- }, Base extends Partial<Response> = Partial<Response>>(resolved: MailerMessagesResolved, keys: Keys | null, normalizer: null | (<Body extends string | Buffer>(body?: Body) => string), base?: Base) => Response;
58
- export {};
83
+ /** Resolves the message content into plain `text`/`html` strings. */
84
+ export declare const resolveBodies: (resolved: MailerMessagesResolved) => {
85
+ text?: string;
86
+ html?: string;
87
+ };
88
+ /**
89
+ * Formats an address into an RFC 5322 string, quoting the display name when it
90
+ * contains special characters. Used by providers that take `"Name <email>"`.
91
+ */
92
+ export declare const formatAddress: (address: MailerAddress) => string;
93
+ /** Formats a list of addresses into a comma-separated RFC 5322 string. */
94
+ export declare const formatAddressList: (addresses?: MailerAddress[]) => string | undefined;
59
95
  //# sourceMappingURL=mailer-messages.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"mailer-messages.d.ts","sourceRoot":"","sources":["../../src/lib/mailer-messages.ts"],"names":[],"mappings":"AAAA,KAAK,YAAY,GAAG;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACd,CAAA;AAED,KAAK,eAAe,GAAG;IACtB,KAAK,EAAE,MAAM,CAAC;CACd,CAAA;AAED,MAAM,MAAM,0BAA0B,GAAG;IACxC,IAAI,CAAC,EAAE,YAAY,CAAC;IACpB,EAAE,CAAC,EAAE,eAAe,EAAE,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC1B,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACvB,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAC9B;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;CAAE,GACvC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;CAAE,GACvC;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;CAAE,CAAC;AAErE,MAAM,MAAM,sBAAsB,GACjC,IAAI,CAAC,0BAA0B,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC,GAC/D;IACF,IAAI,EAAE,YAAY,CAAC;IACnB,EAAE,EAAE,eAAe,EAAE,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,qBAAqB,CAAC;CAC/B,CAAC;AAEF,qBAAa,qBAAqB;IACjC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAIpB;IAEF,SAAS,CAAC,aAAa,IAAI,0BAA0B;IAIrD,SAAS,CAAC,OAAO,IAAI,sBAAsB;IAwBpC,OAAO,CAAC,KAAK,EAAE,YAAY,CAAC,OAAO,CAAC,EAAE,IAAI,CAAC,EAAE,YAAY,CAAC,MAAM,CAAC,GAAG,IAAI;IACxE,OAAO,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI;IAWxC,OAAO,CAAC,sBAAsB;IAKvB,KAAK,CAAC,KAAK,EAAE,eAAe,CAAC,OAAO,CAAC,GAAG,IAAI;IAC5C,KAAK,CAAC,MAAM,EAAE,eAAe,CAAC,OAAO,CAAC,EAAE,GAAG,IAAI;IAC/C,KAAK,CAAC,SAAS,EAAE,eAAe,GAAG,IAAI;IACvC,KAAK,CAAC,UAAU,EAAE,eAAe,EAAE,GAAG,IAAI;IA2B1C,KAAK,CAAC,KAAK,EAAE,eAAe,CAAC,OAAO,CAAC,GAAG,IAAI;IAC5C,KAAK,CAAC,MAAM,EAAE,eAAe,CAAC,OAAO,CAAC,EAAE,GAAG,IAAI;IAC/C,KAAK,CAAC,SAAS,EAAE,eAAe,GAAG,IAAI;IACvC,KAAK,CAAC,UAAU,EAAE,eAAe,EAAE,GAAG,IAAI;IAc1C,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAKjC,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAKpC,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;CAI3C;AAED,eAAO,MAAM,uBAAuB,GAAI,IAAI,SAAS,MAAM,GAAG,MAAM,EAAE,OAAO,IAAI,KAAG,MAInF,CAAA;AAED,eAAO,MAAM,2BAA2B,GACvC,QAAQ,SAAS,IAAI,EACrB,IAAI,SAAS;IAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;IAAC,IAAI,EAAE,MAAM,QAAQ,CAAA;CAAE,EAC3D,IAAI,SAAS,OAAO,CAAC,QAAQ,CAAC,GAAG,OAAO,CAAC,QAAQ,CAAC,EAElD,UAAU,sBAAsB,EAChC,MAAM,IAAI,GAAG,IAAI,EACjB,YAAY,IAAI,GAAG,CAAC,CAAC,IAAI,SAAS,MAAM,GAAG,MAAM,EAAE,IAAI,CAAC,EAAE,IAAI,KAAK,MAAM,CAAC,EAC1E,OAAM,IAAiB,KACrB,QAyBF,CAAC"}
1
+ {"version":3,"file":"mailer-messages.d.ts","sourceRoot":"","sources":["../../src/lib/mailer-messages.ts"],"names":[],"mappings":"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"}
@@ -1,8 +1,12 @@
1
1
  export class MailerMessagesBuilder {
2
2
  draft = {
3
3
  to: [],
4
+ cc: [],
5
+ bcc: [],
4
6
  meta: {},
5
7
  headers: {},
8
+ attachments: [],
9
+ tags: {},
6
10
  };
7
11
  generateDraft() {
8
12
  return this.draft;
@@ -10,13 +14,15 @@ export class MailerMessagesBuilder {
10
14
  prepare() {
11
15
  const draft = this.generateDraft();
12
16
  if (!draft.from?.email)
13
- throw new Error('from.email is required');
17
+ throw new Error('[Mailer] from.email is required');
14
18
  if (!draft.to?.length)
15
- throw new Error('to[] required');
19
+ throw new Error('[Mailer] to[] is required');
16
20
  if (!draft.subject)
17
- throw new Error('subject required');
21
+ throw new Error('[Mailer] subject is required');
18
22
  const text = typeof draft.text === 'string' ? draft.text : draft.text?.toString();
19
23
  const html = typeof draft.html === 'string' ? draft.html : draft.html?.toString();
24
+ if (!text && !html)
25
+ throw new Error('[Mailer] message has no content (text or html)');
20
26
  const content = text && html
21
27
  ? { kind: 'mixed', text, html }
22
28
  : html
@@ -33,50 +39,82 @@ export class MailerMessagesBuilder {
33
39
  setFrom(fromOrEmail, name) {
34
40
  const fromEmail = typeof fromOrEmail === 'string' ? fromOrEmail : fromOrEmail.email;
35
41
  const fromName = typeof fromOrEmail === 'string' ? name : fromOrEmail.name;
36
- this.draft.from = {
37
- email: fromEmail,
38
- name: fromName
39
- };
42
+ this.draft.from = { email: fromEmail, name: fromName };
43
+ return this;
44
+ }
45
+ setReplyTo(replyToOrEmail, name) {
46
+ this.draft.replyTo = typeof replyToOrEmail === 'string'
47
+ ? { email: replyToOrEmail, name }
48
+ : replyToOrEmail;
40
49
  return this;
41
50
  }
42
- resolveRecipientToItem(emailOrRecipient) {
43
- if (typeof emailOrRecipient === 'string')
44
- return { email: emailOrRecipient };
45
- return emailOrRecipient;
51
+ resolveAddress(input) {
52
+ return typeof input === 'string' ? { email: input } : input;
53
+ }
54
+ toAddressList(input) {
55
+ const arr = Array.isArray(input) ? input : [input];
56
+ return arr.map((i) => this.resolveAddress(i));
57
+ }
58
+ /** Merge new recipients into an existing list, de-duplicating by email. */
59
+ mergeAddresses(prev, next) {
60
+ const result = [...prev];
61
+ for (const addr of next) {
62
+ const idx = result.findIndex((p) => p.email === addr.email);
63
+ if (idx !== -1)
64
+ result[idx] = { ...result[idx], ...addr };
65
+ else
66
+ result.push(addr);
67
+ }
68
+ return result;
46
69
  }
47
70
  addTo(input) {
48
- const prevTo = this.draft.to || [];
49
- const newTo = (() => {
50
- const isArr = Array.isArray(input);
51
- const recipients = isArr
52
- ? input.map(this.resolveRecipientToItem)
53
- : [this.resolveRecipientToItem(input)];
54
- const remaining = [];
55
- for (const r of recipients) {
56
- const idx = prevTo.findIndex(p => p.email === r.email);
57
- if (idx !== -1) {
58
- prevTo[idx] = { ...prevTo[idx], ...r };
59
- }
60
- else {
61
- remaining.push(r);
62
- }
63
- }
64
- return remaining;
65
- })();
66
- this.draft.to = [...prevTo, ...newTo];
71
+ this.draft.to = this.mergeAddresses(this.draft.to || [], this.toAddressList(input));
67
72
  return this;
68
73
  }
69
74
  setTo(input) {
70
- const isArr = Array.isArray(input);
71
- this.draft.to = isArr
72
- ? input.map(this.resolveRecipientToItem)
73
- : [this.resolveRecipientToItem(input)];
75
+ this.draft.to = this.toAddressList(input);
76
+ return this;
77
+ }
78
+ addCc(input) {
79
+ this.draft.cc = this.mergeAddresses(this.draft.cc || [], this.toAddressList(input));
80
+ return this;
81
+ }
82
+ setCc(input) {
83
+ this.draft.cc = this.toAddressList(input);
84
+ return this;
85
+ }
86
+ addBcc(input) {
87
+ this.draft.bcc = this.mergeAddresses(this.draft.bcc || [], this.toAddressList(input));
88
+ return this;
89
+ }
90
+ setBcc(input) {
91
+ this.draft.bcc = this.toAddressList(input);
74
92
  return this;
75
93
  }
76
94
  setSubject(subject) {
77
95
  this.draft.subject = subject;
78
96
  return this;
79
97
  }
98
+ setHeader(name, value) {
99
+ this.draft.headers = { ...(this.draft.headers || {}), [name]: value };
100
+ return this;
101
+ }
102
+ setHeaders(headers) {
103
+ this.draft.headers = { ...(this.draft.headers || {}), ...headers };
104
+ return this;
105
+ }
106
+ addAttachment(attachment) {
107
+ this.draft.attachments = [...(this.draft.attachments || []), attachment];
108
+ return this;
109
+ }
110
+ setTag(name, value) {
111
+ this.draft.tags = { ...(this.draft.tags || {}), [name]: value };
112
+ return this;
113
+ }
114
+ setIdempotencyKey(key) {
115
+ this.draft.idempotencyKey = key;
116
+ return this;
117
+ }
80
118
  setText(text) {
81
119
  this.draft.text = text;
82
120
  return this;
@@ -86,6 +124,7 @@ export class MailerMessagesBuilder {
86
124
  return this;
87
125
  }
88
126
  }
127
+ /** Normalizes a string/Buffer body to a plain string. */
89
128
  export const normalizeDataBodyString = (body) => {
90
129
  if (!body)
91
130
  return '';
@@ -93,25 +132,37 @@ export const normalizeDataBodyString = (body) => {
93
132
  return body;
94
133
  return body.toString();
95
134
  };
96
- export const segmentedDataContentPayload = (resolved, keys, normalizer, base = {}) => {
97
- const { text: keyText, html: keyHtml } = keys || { text: 'text', html: 'html' };
98
- const normalize = normalizer || normalizeDataBodyString;
99
- switch (resolved.content.kind) {
135
+ /** Resolves the message content into plain `text`/`html` strings. */
136
+ export const resolveBodies = (resolved) => {
137
+ const content = resolved.content;
138
+ switch (content.kind) {
100
139
  case 'text':
101
- return { ...base, [keyText]: normalize(resolved.content.text) };
140
+ return { text: normalizeDataBodyString(content.text) };
102
141
  case 'html':
103
- return { ...base, [keyHtml]: normalize(resolved.content.html) };
142
+ return { html: normalizeDataBodyString(content.html) };
104
143
  case 'mixed':
105
144
  return {
106
- ...base,
107
- [keyText]: normalize(resolved.content.text),
108
- [keyHtml]: normalize(resolved.content.html),
145
+ text: normalizeDataBodyString(content.text),
146
+ html: normalizeDataBodyString(content.html),
109
147
  };
110
148
  default:
111
- return {
112
- ...base,
113
- [keyText]: normalize(''),
114
- [keyHtml]: normalize(''),
115
- };
149
+ return {};
116
150
  }
117
151
  };
152
+ /**
153
+ * Formats an address into an RFC 5322 string, quoting the display name when it
154
+ * contains special characters. Used by providers that take `"Name <email>"`.
155
+ */
156
+ export const formatAddress = (address) => {
157
+ if (!address.name)
158
+ return address.email;
159
+ const needsQuote = /[",:;<>@\[\]\\]/.test(address.name);
160
+ const name = needsQuote ? `"${address.name.replace(/"/g, '\\"')}"` : address.name;
161
+ return `${name} <${address.email}>`;
162
+ };
163
+ /** Formats a list of addresses into a comma-separated RFC 5322 string. */
164
+ export const formatAddressList = (addresses) => {
165
+ if (!addresses?.length)
166
+ return undefined;
167
+ return addresses.map(formatAddress).join(', ');
168
+ };
@@ -1,11 +1,15 @@
1
1
  import { MailerMessagesResolved } from "./mailer-messages.js";
2
+ import { MailerSendResult, MailerSendOptions } from "./mailer-send-result.js";
2
3
  export type MailerProviderOptionsType = Record<string, any> | undefined;
3
4
  export type MailerNativeClientType<P> = P extends {
4
5
  getClient: (...args: any[]) => infer C;
5
6
  } ? C : never;
6
7
  export interface MailerProvider<Options = object, NativeClient = unknown, Payload = object> {
8
+ /** Returns the underlying SDK client. Always available across providers. */
7
9
  getClient(): NativeClient;
10
+ /** Maps the normalized message onto the provider-native payload. */
8
11
  createPayload(resolved: MailerMessagesResolved): Payload;
9
- send(payload: Payload): Promise<void>;
12
+ /** Sends the payload and normalizes the provider response. */
13
+ send(payload: Payload, options?: MailerSendOptions): Promise<MailerSendResult>;
10
14
  }
11
15
  //# sourceMappingURL=mailer-provider.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"mailer-provider.d.ts","sourceRoot":"","sources":["../../src/lib/mailer-provider.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAE9D,MAAM,MAAM,yBAAyB,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,CAAC;AAExE,MAAM,MAAM,sBAAsB,CAAC,CAAC,IAAI,CAAC,SAAS;IAAE,SAAS,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,MAAM,CAAC,CAAA;CAAE,GAAG,CAAC,GAAG,KAAK,CAAC;AAEzG,MAAM,WAAW,cAAc,CAC9B,OAAO,GAAG,MAAM,EAChB,YAAY,GAAG,OAAO,EACtB,OAAO,GAAG,MAAM;IAEhB,SAAS,IAAI,YAAY,CAAC;IAC1B,aAAa,CAAC,QAAQ,EAAE,sBAAsB,GAAG,OAAO,CAAC;IACzD,IAAI,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACtC"}
1
+ {"version":3,"file":"mailer-provider.d.ts","sourceRoot":"","sources":["../../src/lib/mailer-provider.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAE9E,MAAM,MAAM,yBAAyB,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,CAAC;AAExE,MAAM,MAAM,sBAAsB,CAAC,CAAC,IAAI,CAAC,SAAS;IAAE,SAAS,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,MAAM,CAAC,CAAA;CAAE,GAAG,CAAC,GAAG,KAAK,CAAC;AAEzG,MAAM,WAAW,cAAc,CAC9B,OAAO,GAAG,MAAM,EAChB,YAAY,GAAG,OAAO,EACtB,OAAO,GAAG,MAAM;IAEhB,4EAA4E;IAC5E,SAAS,IAAI,YAAY,CAAC;IAC1B,oEAAoE;IACpE,aAAa,CAAC,QAAQ,EAAE,sBAAsB,GAAG,OAAO,CAAC;IACzD,8DAA8D;IAC9D,IAAI,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;CAC/E"}
@@ -0,0 +1,25 @@
1
+ export interface MailerSendErrorParams {
2
+ provider: string;
3
+ message: string;
4
+ code?: string | number;
5
+ statusCode?: number;
6
+ /** Explicit override; when omitted it is derived from `statusCode`. */
7
+ retryable?: boolean;
8
+ cause?: unknown;
9
+ }
10
+ /**
11
+ * Normalized provider error. Carries a `retryable` flag so the retry layer can
12
+ * skip permanent failures (4xx validation) and only retry transient ones
13
+ * (network, 429, 5xx).
14
+ */
15
+ export declare class MailerSendError extends Error {
16
+ readonly provider: string;
17
+ readonly code?: string | number;
18
+ readonly statusCode?: number;
19
+ readonly retryable: boolean;
20
+ readonly cause?: unknown;
21
+ constructor(params: MailerSendErrorParams);
22
+ }
23
+ /** Wraps any thrown SDK error into a normalized `MailerSendError`. */
24
+ export declare const toMailerSendError: (provider: string, err: unknown) => MailerSendError;
25
+ //# sourceMappingURL=mailer-send-error.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mailer-send-error.d.ts","sourceRoot":"","sources":["../../src/lib/mailer-send-error.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,qBAAqB;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,uEAAuE;IACvE,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,KAAK,CAAC,EAAE,OAAO,CAAC;CAChB;AAED;;;;GAIG;AACH,qBAAa,eAAgB,SAAQ,KAAK;IACzC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAChC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,SAAkB,KAAK,CAAC,EAAE,OAAO,CAAC;gBAEtB,MAAM,EAAE,qBAAqB;CASzC;AAoBD,sEAAsE;AACtE,eAAO,MAAM,iBAAiB,GAAI,UAAU,MAAM,EAAE,KAAK,OAAO,KAAG,eAWlE,CAAC"}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Normalized provider error. Carries a `retryable` flag so the retry layer can
3
+ * skip permanent failures (4xx validation) and only retry transient ones
4
+ * (network, 429, 5xx).
5
+ */
6
+ export class MailerSendError extends Error {
7
+ provider;
8
+ code;
9
+ statusCode;
10
+ retryable;
11
+ cause;
12
+ constructor(params) {
13
+ super(`[${params.provider}] ${params.message}`);
14
+ this.name = 'MailerSendError';
15
+ this.provider = params.provider;
16
+ this.code = params.code;
17
+ this.statusCode = params.statusCode;
18
+ this.cause = params.cause;
19
+ this.retryable = params.retryable ?? deriveRetryable(params.statusCode);
20
+ }
21
+ }
22
+ /** 429 and 5xx are transient; other classified statuses are permanent. */
23
+ const deriveRetryable = (statusCode) => {
24
+ if (statusCode === undefined)
25
+ return true; // network/unknown → retry
26
+ if (statusCode === 429)
27
+ return true;
28
+ return statusCode >= 500;
29
+ };
30
+ /** Best-effort extraction of a status code from an arbitrary SDK error. */
31
+ const extractStatusCode = (err) => {
32
+ const candidate = err?.statusCode ??
33
+ err?.status ??
34
+ err?.response?.statusCode ??
35
+ err?.response?.status ??
36
+ err?.$metadata?.httpStatusCode;
37
+ return typeof candidate === 'number' ? candidate : undefined;
38
+ };
39
+ /** Wraps any thrown SDK error into a normalized `MailerSendError`. */
40
+ export const toMailerSendError = (provider, err) => {
41
+ if (err instanceof MailerSendError)
42
+ return err;
43
+ const e = err;
44
+ const message = e?.message ?? String(err);
45
+ return new MailerSendError({
46
+ provider,
47
+ message,
48
+ code: e?.code ?? e?.name,
49
+ statusCode: extractStatusCode(e),
50
+ cause: err,
51
+ });
52
+ };
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Provider-agnostic result of a single send.
3
+ *
4
+ * Every provider maps its native response onto this shape so callers can
5
+ * rely on one contract regardless of the underlying transport.
6
+ */
7
+ export interface MailerSendResult {
8
+ /** Provider message identifier, when the provider exposes one. */
9
+ messageId?: string;
10
+ /** Recipients the provider/transport accepted (e.g. SMTP `accepted`). */
11
+ accepted?: string[];
12
+ /** Recipients the provider/transport rejected (e.g. SMTP `rejected`). */
13
+ rejected?: string[];
14
+ /** Raw, untyped provider response for advanced/debug access. */
15
+ raw?: unknown;
16
+ }
17
+ /**
18
+ * Per-send request options, separate from the message payload.
19
+ * Threaded from `MailerService.send` into `MailerProvider.send`.
20
+ */
21
+ export interface MailerSendOptions {
22
+ /**
23
+ * Stable key for safe retries — providers that support it (e.g. Resend)
24
+ * use it to deduplicate identical requests. A key is generated per logical
25
+ * send so the retry loop never produces duplicate emails.
26
+ */
27
+ idempotencyKey?: string;
28
+ }
29
+ //# sourceMappingURL=mailer-send-result.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mailer-send-result.d.ts","sourceRoot":"","sources":["../../src/lib/mailer-send-result.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAChC,kEAAkE;IAClE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,yEAAyE;IACzE,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,yEAAyE;IACzE,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,gEAAgE;IAChE,GAAG,CAAC,EAAE,OAAO,CAAC;CACd;AAED;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IACjC;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB"}
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brilab-mailer/contracts",
3
- "version": "0.0.5-5",
3
+ "version": "0.1.0",
4
4
  "author": "Bohdan Radchenko <radchenkobs@gmail.com>",
5
5
  "type": "module",
6
6
  "main": "./index.js",