@objectstack/plugin-email 4.0.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.
@@ -0,0 +1,94 @@
1
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
+
3
+ import type {
4
+ IEmailTransport,
5
+ NormalizedEmailMessage,
6
+ TransportSendResult,
7
+ } from '@objectstack/spec/contracts';
8
+
9
+ /**
10
+ * PostmarkTransport — SaaS delivery via https://postmarkapp.com
11
+ *
12
+ * Implements `IEmailTransport` using the Postmark HTTPS API. Zero
13
+ * external dependencies (uses fetch). API docs:
14
+ * https://postmarkapp.com/developer/user-guide/send-email-with-api
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * new EmailServicePlugin({
19
+ * transport: new PostmarkTransport({
20
+ * apiKey: process.env.POSTMARK_TOKEN!,
21
+ * messageStream: 'outbound',
22
+ * }),
23
+ * defaultFrom: { name: 'Acme', address: 'no-reply@acme.com' },
24
+ * });
25
+ * ```
26
+ */
27
+ export interface PostmarkTransportOptions {
28
+ apiKey: string;
29
+ /** Postmark message stream (default 'outbound'). */
30
+ messageStream?: string;
31
+ /** Override the API host. */
32
+ endpoint?: string;
33
+ }
34
+
35
+ export class PostmarkTransport implements IEmailTransport {
36
+ private readonly apiKey: string;
37
+ private readonly endpoint: string;
38
+ private readonly messageStream: string;
39
+
40
+ constructor(opts: PostmarkTransportOptions) {
41
+ if (!opts?.apiKey) throw new Error('PostmarkTransport: apiKey is required');
42
+ this.apiKey = opts.apiKey;
43
+ this.endpoint = opts.endpoint || 'https://api.postmarkapp.com/email';
44
+ this.messageStream = opts.messageStream || 'outbound';
45
+ }
46
+
47
+ async send(message: NormalizedEmailMessage): Promise<TransportSendResult> {
48
+ const body: Record<string, unknown> = {
49
+ From: message.from,
50
+ To: message.to.join(', '),
51
+ Subject: message.subject,
52
+ MessageStream: this.messageStream,
53
+ };
54
+ if (message.html !== undefined) body.HtmlBody = message.html;
55
+ if (message.text !== undefined) body.TextBody = message.text;
56
+ if (message.cc?.length) body.Cc = message.cc.join(', ');
57
+ if (message.bcc?.length) body.Bcc = message.bcc.join(', ');
58
+ if (message.replyTo) body.ReplyTo = message.replyTo;
59
+ if (message.headers && Object.keys(message.headers).length > 0) {
60
+ body.Headers = Object.entries(message.headers).map(([Name, Value]) => ({ Name, Value }));
61
+ }
62
+ if (message.attachments?.length) {
63
+ body.Attachments = message.attachments.map((a) => ({
64
+ Name: a.filename,
65
+ Content: typeof a.content === 'string'
66
+ ? a.content
67
+ : (a.content as any)?.toString?.('base64') ?? String(a.content),
68
+ ContentType: a.contentType || 'application/octet-stream',
69
+ ...(a.cid ? { ContentID: `cid:${a.cid}` } : {}),
70
+ }));
71
+ }
72
+
73
+ const res = await fetch(this.endpoint, {
74
+ method: 'POST',
75
+ headers: {
76
+ 'X-Postmark-Server-Token': this.apiKey,
77
+ 'Content-Type': 'application/json',
78
+ Accept: 'application/json',
79
+ },
80
+ body: JSON.stringify(body),
81
+ });
82
+
83
+ if (!res.ok) {
84
+ const errText = await res.text().catch(() => '');
85
+ throw new Error(`Postmark ${res.status}: ${errText.slice(0, 500)}`);
86
+ }
87
+ const json: any = await res.json().catch(() => ({}));
88
+ const messageId = String(json?.MessageID ?? '');
89
+ if (!messageId) {
90
+ throw new Error('Postmark: response missing `MessageID` field');
91
+ }
92
+ return { messageId, response: 'postmark:ok' };
93
+ }
94
+ }
@@ -0,0 +1,89 @@
1
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
+
3
+ import type {
4
+ IEmailTransport,
5
+ NormalizedEmailMessage,
6
+ TransportSendResult,
7
+ } from '@objectstack/spec/contracts';
8
+
9
+ /**
10
+ * ResendTransport — SaaS delivery via https://resend.com
11
+ *
12
+ * Implements `IEmailTransport` using the Resend HTTPS API. Zero
13
+ * external dependencies (uses fetch). API docs:
14
+ * https://resend.com/docs/api-reference/emails/send-email
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * new EmailServicePlugin({
19
+ * transport: new ResendTransport(process.env.RESEND_API_KEY!),
20
+ * defaultFrom: { name: 'Acme', address: 'no-reply@acme.com' },
21
+ * });
22
+ * ```
23
+ */
24
+ export interface ResendTransportOptions {
25
+ apiKey: string;
26
+ /** Override the API host (used by tests / proxies). */
27
+ endpoint?: string;
28
+ }
29
+
30
+ export class ResendTransport implements IEmailTransport {
31
+ private readonly apiKey: string;
32
+ private readonly endpoint: string;
33
+
34
+ constructor(apiKeyOrOptions: string | ResendTransportOptions) {
35
+ if (typeof apiKeyOrOptions === 'string') {
36
+ this.apiKey = apiKeyOrOptions;
37
+ this.endpoint = 'https://api.resend.com/emails';
38
+ } else {
39
+ this.apiKey = apiKeyOrOptions.apiKey;
40
+ this.endpoint = apiKeyOrOptions.endpoint || 'https://api.resend.com/emails';
41
+ }
42
+ if (!this.apiKey) {
43
+ throw new Error('ResendTransport: apiKey is required');
44
+ }
45
+ }
46
+
47
+ async send(message: NormalizedEmailMessage): Promise<TransportSendResult> {
48
+ const body: Record<string, unknown> = {
49
+ from: message.from,
50
+ to: message.to,
51
+ subject: message.subject,
52
+ };
53
+ if (message.html !== undefined) body.html = message.html;
54
+ if (message.text !== undefined) body.text = message.text;
55
+ if (message.cc?.length) body.cc = message.cc;
56
+ if (message.bcc?.length) body.bcc = message.bcc;
57
+ if (message.replyTo) body.reply_to = message.replyTo;
58
+ if (message.headers && Object.keys(message.headers).length > 0) body.headers = message.headers;
59
+ if (message.attachments?.length) {
60
+ body.attachments = message.attachments.map((a) => ({
61
+ filename: a.filename,
62
+ content: typeof a.content === 'string'
63
+ ? a.content
64
+ : (a.content as any)?.toString?.('base64') ?? String(a.content),
65
+ ...(a.contentType ? { content_type: a.contentType } : {}),
66
+ }));
67
+ }
68
+
69
+ const res = await fetch(this.endpoint, {
70
+ method: 'POST',
71
+ headers: {
72
+ Authorization: `Bearer ${this.apiKey}`,
73
+ 'Content-Type': 'application/json',
74
+ },
75
+ body: JSON.stringify(body),
76
+ });
77
+
78
+ if (!res.ok) {
79
+ const errText = await res.text().catch(() => '');
80
+ throw new Error(`Resend ${res.status}: ${errText.slice(0, 500)}`);
81
+ }
82
+ const json = await res.json().catch(() => ({} as any));
83
+ const messageId = String((json as any)?.id ?? '');
84
+ if (!messageId) {
85
+ throw new Error('Resend: response missing `id` field');
86
+ }
87
+ return { messageId, response: 'resend:ok' };
88
+ }
89
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "../../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src",
6
+ "types": ["node"]
7
+ },
8
+ "include": ["src/**/*"],
9
+ "exclude": ["dist", "node_modules", "**/*.test.ts"]
10
+ }