@flink-app/email-plugin 2.0.0-alpha.109 → 2.0.0-alpha.111

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/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # @flink-app/email-plugin
2
2
 
3
+ ## 2.0.0-alpha.111
4
+
5
+ ### Minor Changes
6
+
7
+ - 00246e8: feat(email-plugin): add MailerSend provider
8
+
9
+ Adds `mailersendClient` to `@flink-app/email-plugin`, alongside the existing SendGrid, SMTP, Flowmailer, AWS SES and Postmark clients. Configure with `emailPlugin({ client: new mailersendClient({ apiKey }) })`.
10
+
11
+ - New `emailMailersend` type: addresses as plain strings, `"Name <email>"` strings or `{ email, name }` objects; supports `templateId`, `personalization`, `tags`, inline/regular base64 attachments and `sendAt`
12
+ - `sendEmail()` returns rich results (`messageId`, HTTP status and error details)
13
+ - Optional `defaultFrom` and `baseUrl` client options
14
+
15
+ ### Patch Changes
16
+
17
+ - @flink-app/flink@2.0.0-alpha.111
18
+
19
+ ## 2.0.0-alpha.110
20
+
21
+ ### Patch Changes
22
+
23
+ - @flink-app/flink@2.0.0-alpha.110
24
+
3
25
  ## 2.0.0-alpha.109
4
26
 
5
27
  ### Patch Changes
package/dist/index.d.ts CHANGED
@@ -5,9 +5,12 @@ export { smtpClient } from "./smtpClient";
5
5
  export { flowMailerClient } from "./flowmailerClient";
6
6
  export { sesClient } from "./sesClient";
7
7
  export { postmarkClient } from "./postmarkClient";
8
+ export { mailersendClient, toMailersendAddress } from "./mailersendClient";
9
+ export type { mailersendClientOptions, MailersendResult } from "./mailersendClient";
8
10
  export type { email } from "./schemas/email";
9
11
  export type { emailSes } from "./schemas/emailSes";
10
12
  export type { emailPostmark, PostmarkAttachment } from "./schemas/emailPostmark";
13
+ export type { emailMailersend, MailersendAddress, MailersendAttachment, MailersendPersonalization, } from "./schemas/emailMailersend";
11
14
  export * from "./ses-types";
12
15
  export type emailPluginOptions = {
13
16
  /**
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.emailPlugin = exports.postmarkClient = exports.sesClient = exports.flowMailerClient = exports.smtpClient = exports.sendgridClient = void 0;
17
+ exports.emailPlugin = exports.toMailersendAddress = exports.mailersendClient = exports.postmarkClient = exports.sesClient = exports.flowMailerClient = exports.smtpClient = exports.sendgridClient = void 0;
18
18
  var sendgridClient_1 = require("./sendgridClient");
19
19
  Object.defineProperty(exports, "sendgridClient", { enumerable: true, get: function () { return sendgridClient_1.sendgridClient; } });
20
20
  var smtpClient_1 = require("./smtpClient");
@@ -25,6 +25,9 @@ var sesClient_1 = require("./sesClient");
25
25
  Object.defineProperty(exports, "sesClient", { enumerable: true, get: function () { return sesClient_1.sesClient; } });
26
26
  var postmarkClient_1 = require("./postmarkClient");
27
27
  Object.defineProperty(exports, "postmarkClient", { enumerable: true, get: function () { return postmarkClient_1.postmarkClient; } });
28
+ var mailersendClient_1 = require("./mailersendClient");
29
+ Object.defineProperty(exports, "mailersendClient", { enumerable: true, get: function () { return mailersendClient_1.mailersendClient; } });
30
+ Object.defineProperty(exports, "toMailersendAddress", { enumerable: true, get: function () { return mailersendClient_1.toMailersendAddress; } });
28
31
  __exportStar(require("./ses-types"), exports);
29
32
  var emailPlugin = function (options) {
30
33
  return {
@@ -0,0 +1,61 @@
1
+ import { AxiosInstance } from "axios";
2
+ import { client } from "./schemas/client";
3
+ import { emailMailersend as email, MailersendAddress } from "./schemas/emailMailersend";
4
+ export interface mailersendClientOptions {
5
+ /**
6
+ * MailerSend API token
7
+ */
8
+ apiKey: string;
9
+ /**
10
+ * Default "from" address when not specified per-email
11
+ */
12
+ defaultFrom?: MailersendAddress;
13
+ /**
14
+ * Override API base url, defaults to "https://api.mailersend.com/v1"
15
+ */
16
+ baseUrl?: string;
17
+ }
18
+ export interface MailersendResult {
19
+ success: boolean;
20
+ /**
21
+ * Message id returned by MailerSend (from the `x-message-id` response header)
22
+ */
23
+ messageId?: string;
24
+ error?: {
25
+ /**
26
+ * HTTP status code from MailerSend, if the request reached the API
27
+ */
28
+ status?: number;
29
+ message: string;
30
+ /**
31
+ * Raw error body returned by MailerSend, if any
32
+ */
33
+ details?: any;
34
+ };
35
+ }
36
+ type MailersendAddressPayload = {
37
+ email: string;
38
+ name?: string;
39
+ };
40
+ /**
41
+ * Parse an address string such as "Jane Doe <jane@example.com>" into
42
+ * MailerSend's `{ email, name }` shape. Plain addresses are passed through.
43
+ */
44
+ export declare function toMailersendAddress(address: MailersendAddress): MailersendAddressPayload;
45
+ export declare class mailersendClient implements client {
46
+ private http;
47
+ private defaultFrom?;
48
+ constructor(options: mailersendClientOptions, http?: AxiosInstance);
49
+ /**
50
+ * Send an email via MailerSend. Implements the base `client` interface.
51
+ * Returns `true` on success, `false` on failure.
52
+ * For richer results (messageId, error details), use `sendEmail()` instead.
53
+ */
54
+ send(email: email): Promise<boolean>;
55
+ /**
56
+ * Send an email via MailerSend with rich result including messageId and error details.
57
+ */
58
+ sendEmail(email: email): Promise<MailersendResult>;
59
+ private buildPayload;
60
+ }
61
+ export {};
@@ -0,0 +1,179 @@
1
+ "use strict";
2
+ var __assign = (this && this.__assign) || function () {
3
+ __assign = Object.assign || function(t) {
4
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
5
+ s = arguments[i];
6
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
7
+ t[p] = s[p];
8
+ }
9
+ return t;
10
+ };
11
+ return __assign.apply(this, arguments);
12
+ };
13
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
14
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
15
+ return new (P || (P = Promise))(function (resolve, reject) {
16
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
17
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
18
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
19
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
20
+ });
21
+ };
22
+ var __generator = (this && this.__generator) || function (thisArg, body) {
23
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
24
+ return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
25
+ function verb(n) { return function (v) { return step([n, v]); }; }
26
+ function step(op) {
27
+ if (f) throw new TypeError("Generator is already executing.");
28
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
29
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
30
+ if (y = 0, t) op = [op[0] & 2, t.value];
31
+ switch (op[0]) {
32
+ case 0: case 1: t = op; break;
33
+ case 4: _.label++; return { value: op[1], done: false };
34
+ case 5: _.label++; y = op[1]; op = [0]; continue;
35
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
36
+ default:
37
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
38
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
39
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
40
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
41
+ if (t[2]) _.ops.pop();
42
+ _.trys.pop(); continue;
43
+ }
44
+ op = body.call(thisArg, _);
45
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
46
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
47
+ }
48
+ };
49
+ var __importDefault = (this && this.__importDefault) || function (mod) {
50
+ return (mod && mod.__esModule) ? mod : { "default": mod };
51
+ };
52
+ Object.defineProperty(exports, "__esModule", { value: true });
53
+ exports.mailersendClient = void 0;
54
+ exports.toMailersendAddress = toMailersendAddress;
55
+ var axios_1 = __importDefault(require("axios"));
56
+ /**
57
+ * Parse an address string such as "Jane Doe <jane@example.com>" into
58
+ * MailerSend's `{ email, name }` shape. Plain addresses are passed through.
59
+ */
60
+ function toMailersendAddress(address) {
61
+ if (typeof address !== "string") {
62
+ return address.name ? { email: address.email, name: address.name } : { email: address.email };
63
+ }
64
+ var match = address.trim().match(/^(.*?)\s*<([^<>]+)>$/);
65
+ if (match) {
66
+ var name = match[1].replace(/^"|"$/g, "").trim();
67
+ return name ? { email: match[2].trim(), name: name } : { email: match[2].trim() };
68
+ }
69
+ return { email: address.trim() };
70
+ }
71
+ var mailersendClient = /** @class */ (function () {
72
+ function mailersendClient(options, http) {
73
+ var _a;
74
+ this.defaultFrom = options.defaultFrom;
75
+ this.http =
76
+ http !== null && http !== void 0 ? http : axios_1.default.create({
77
+ baseURL: (_a = options.baseUrl) !== null && _a !== void 0 ? _a : "https://api.mailersend.com/v1",
78
+ headers: {
79
+ Authorization: "Bearer ".concat(options.apiKey),
80
+ "Content-Type": "application/json",
81
+ "X-Requested-With": "XMLHttpRequest",
82
+ },
83
+ });
84
+ }
85
+ /**
86
+ * Send an email via MailerSend. Implements the base `client` interface.
87
+ * Returns `true` on success, `false` on failure.
88
+ * For richer results (messageId, error details), use `sendEmail()` instead.
89
+ */
90
+ mailersendClient.prototype.send = function (email) {
91
+ return __awaiter(this, void 0, void 0, function () {
92
+ var result;
93
+ return __generator(this, function (_a) {
94
+ switch (_a.label) {
95
+ case 0: return [4 /*yield*/, this.sendEmail(email)];
96
+ case 1:
97
+ result = _a.sent();
98
+ return [2 /*return*/, result.success];
99
+ }
100
+ });
101
+ });
102
+ };
103
+ /**
104
+ * Send an email via MailerSend with rich result including messageId and error details.
105
+ */
106
+ mailersendClient.prototype.sendEmail = function (email) {
107
+ return __awaiter(this, void 0, void 0, function () {
108
+ var from, payload, resp, messageId, ex_1, status, details, message;
109
+ var _a, _b, _c;
110
+ return __generator(this, function (_d) {
111
+ switch (_d.label) {
112
+ case 0:
113
+ from = email.from || this.defaultFrom;
114
+ if (!from) {
115
+ return [2 /*return*/, {
116
+ success: false,
117
+ error: {
118
+ message: "No 'from' address specified. Set it on the email or use defaultFrom in mailersendClientOptions.",
119
+ },
120
+ }];
121
+ }
122
+ payload = this.buildPayload(email, from);
123
+ _d.label = 1;
124
+ case 1:
125
+ _d.trys.push([1, 3, , 4]);
126
+ return [4 /*yield*/, this.http.post("/email", payload)];
127
+ case 2:
128
+ resp = _d.sent();
129
+ messageId = (_a = resp.headers) === null || _a === void 0 ? void 0 : _a["x-message-id"];
130
+ return [2 /*return*/, { success: true, messageId: messageId }];
131
+ case 3:
132
+ ex_1 = _d.sent();
133
+ status = (_b = ex_1 === null || ex_1 === void 0 ? void 0 : ex_1.response) === null || _b === void 0 ? void 0 : _b.status;
134
+ details = (_c = ex_1 === null || ex_1 === void 0 ? void 0 : ex_1.response) === null || _c === void 0 ? void 0 : _c.data;
135
+ message = (details === null || details === void 0 ? void 0 : details.message) || (ex_1 === null || ex_1 === void 0 ? void 0 : ex_1.message) || "Unknown error";
136
+ console.log(JSON.stringify({ error: "MailerSend send failed", status: status, message: message, details: details }));
137
+ return [2 /*return*/, { success: false, error: { status: status, message: message, details: details } }];
138
+ case 4: return [2 /*return*/];
139
+ }
140
+ });
141
+ });
142
+ };
143
+ mailersendClient.prototype.buildPayload = function (email, from) {
144
+ var _a, _b, _c, _d, _e;
145
+ var payload = {
146
+ from: toMailersendAddress(from),
147
+ to: email.to.map(toMailersendAddress),
148
+ };
149
+ if ((_a = email.cc) === null || _a === void 0 ? void 0 : _a.length)
150
+ payload.cc = email.cc.map(toMailersendAddress);
151
+ if ((_b = email.bcc) === null || _b === void 0 ? void 0 : _b.length)
152
+ payload.bcc = email.bcc.map(toMailersendAddress);
153
+ if (email.replyTo)
154
+ payload.reply_to = toMailersendAddress(email.replyTo);
155
+ if (email.subject !== undefined)
156
+ payload.subject = email.subject;
157
+ if (email.text !== undefined)
158
+ payload.text = email.text;
159
+ if (email.html !== undefined)
160
+ payload.html = email.html;
161
+ if (email.templateId)
162
+ payload.template_id = email.templateId;
163
+ if ((_c = email.personalization) === null || _c === void 0 ? void 0 : _c.length)
164
+ payload.personalization = email.personalization;
165
+ if ((_d = email.tags) === null || _d === void 0 ? void 0 : _d.length)
166
+ payload.tags = email.tags;
167
+ if ((_e = email.attachments) === null || _e === void 0 ? void 0 : _e.length) {
168
+ payload.attachments = email.attachments.map(function (a) {
169
+ var _a;
170
+ return (__assign({ content: a.content, filename: a.filename, disposition: (_a = a.disposition) !== null && _a !== void 0 ? _a : "attachment" }, (a.id ? { id: a.id } : {})));
171
+ });
172
+ }
173
+ if (email.sendAt !== undefined)
174
+ payload.send_at = email.sendAt;
175
+ return payload;
176
+ };
177
+ return mailersendClient;
178
+ }());
179
+ exports.mailersendClient = mailersendClient;
@@ -1,8 +1,9 @@
1
1
  import { email } from "./email";
2
2
  import { emailFlowmailer } from "./emailFlowmailer";
3
+ import { emailMailersend } from "./emailMailersend";
3
4
  import { emailPostmark } from "./emailPostmark";
4
5
  import { emailSendgrid } from "./emailSendgrid";
5
6
  import { emailSes } from "./emailSes";
6
7
  export interface client {
7
- send(email: email | emailSendgrid | emailFlowmailer | emailSes | emailPostmark): Promise<boolean>;
8
+ send(email: email | emailSendgrid | emailFlowmailer | emailSes | emailPostmark | emailMailersend): Promise<boolean>;
8
9
  }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * An email address, either as a plain string ("user@example.com" or
3
+ * "Display Name <user@example.com>") or as an object with email and optional name.
4
+ */
5
+ export type MailersendAddress = string | {
6
+ email: string;
7
+ name?: string;
8
+ };
9
+ export interface MailersendAttachment {
10
+ /**
11
+ * Base64 encoded content
12
+ */
13
+ content: string;
14
+ /**
15
+ * Filename shown to recipient
16
+ */
17
+ filename: string;
18
+ /**
19
+ * "attachment" (default) or "inline". Inline attachments can be referenced
20
+ * from HTML via the `id`, e.g. `<img src="cid:image1">`
21
+ */
22
+ disposition?: "attachment" | "inline";
23
+ /**
24
+ * Content-ID used to reference inline attachments from the HTML body
25
+ */
26
+ id?: string;
27
+ }
28
+ export interface MailersendPersonalization {
29
+ /**
30
+ * Recipient email address this personalization data applies to
31
+ */
32
+ email: string;
33
+ /**
34
+ * Variables available to the template / `{{ var }}` substitutions
35
+ */
36
+ data: Record<string, any>;
37
+ }
38
+ type emailMailersendBase = {
39
+ /**
40
+ * From address used to send the email
41
+ */
42
+ from: MailersendAddress;
43
+ /**
44
+ * Email addresses to send to
45
+ */
46
+ to: MailersendAddress[];
47
+ /**
48
+ * CC addresses
49
+ */
50
+ cc?: MailersendAddress[];
51
+ /**
52
+ * BCC addresses
53
+ */
54
+ bcc?: MailersendAddress[];
55
+ /**
56
+ * Reply-to address
57
+ */
58
+ replyTo?: MailersendAddress;
59
+ /**
60
+ * MailerSend tags attached to the message
61
+ */
62
+ tags?: string[];
63
+ /**
64
+ * Per-recipient template/substitution variables
65
+ */
66
+ personalization?: MailersendPersonalization[];
67
+ /**
68
+ * File attachments
69
+ */
70
+ attachments?: MailersendAttachment[];
71
+ /**
72
+ * Unix timestamp (seconds) to schedule the email for later delivery (max 72h ahead)
73
+ */
74
+ sendAt?: number;
75
+ };
76
+ export type emailMailersend = emailMailersendBase & ({
77
+ subject: string;
78
+ text: string;
79
+ html?: string;
80
+ templateId?: string;
81
+ } | {
82
+ subject: string;
83
+ html: string;
84
+ text?: string;
85
+ templateId?: string;
86
+ } | {
87
+ /**
88
+ * MailerSend template id. When set, subject/text/html may be omitted
89
+ * and are taken from the template (unless overridden here).
90
+ */
91
+ templateId: string;
92
+ subject?: string;
93
+ text?: string;
94
+ html?: string;
95
+ });
96
+ export {};
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@flink-app/email-plugin",
3
- "version": "2.0.0-alpha.109",
4
- "description": "Flink plugin that provides multi-provider email sending capabilities with support for SendGrid, SMTP, Flowmailer, AWS SES, and Postmark",
3
+ "version": "2.0.0-alpha.111",
4
+ "description": "Flink plugin that provides multi-provider email sending capabilities with support for SendGrid, SMTP, Flowmailer, AWS SES, Postmark, and MailerSend",
5
5
  "author": "johan@frost.se",
6
6
  "publishConfig": {
7
7
  "access": "public"
@@ -21,11 +21,11 @@
21
21
  "devDependencies": {
22
22
  "@types/jasmine": "^3.7.1",
23
23
  "@types/node": "22.13.10",
24
- "@flink-app/flink": "2.0.0-alpha.109"
24
+ "@flink-app/flink": "2.0.0-alpha.111"
25
25
  },
26
26
  "gitHead": "4243e3b3cd6d4e1ca001a61baa8436bf2bbe4113",
27
27
  "peerDependencies": {
28
- "@flink-app/flink": ">=2.0.0-alpha.109"
28
+ "@flink-app/flink": ">=2.0.0-alpha.111"
29
29
  },
30
30
  "scripts": {
31
31
  "test": "jasmine-ts --config=./spec/support/jasmine.json",
package/readme.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Email Plugin
2
2
 
3
- A Flink plugin that provides multi-provider email sending capabilities with support for SendGrid, SMTP, Flowmailer, AWS SES, and Postmark.
3
+ A Flink plugin that provides multi-provider email sending capabilities with support for SendGrid, SMTP, Flowmailer, AWS SES, Postmark, and MailerSend.
4
4
 
5
5
  ## Installation
6
6
 
@@ -12,7 +12,7 @@ npm install @flink-app/email-plugin
12
12
 
13
13
  ## Configuration
14
14
 
15
- The email plugin supports five different email providers through a unified client interface:
15
+ The email plugin supports six different email providers through a unified client interface:
16
16
 
17
17
  ### Option 1: SendGrid
18
18
 
@@ -217,6 +217,94 @@ interface PostmarkAttachment {
217
217
  }
218
218
  ```
219
219
 
220
+ ### Option 6: MailerSend
221
+
222
+ Configure the plugin with [MailerSend](https://www.mailersend.com/):
223
+
224
+ ```typescript
225
+ import { emailPlugin, mailersendClient } from "@flink-app/email-plugin";
226
+
227
+ function start() {
228
+ new FlinkApp<AppContext>({
229
+ name: "My app",
230
+ plugins: [
231
+ emailPlugin({
232
+ client: new mailersendClient({
233
+ apiKey: process.env.MAILERSEND_API_KEY!,
234
+ defaultFrom: "noreply@example.com", // optional
235
+ })
236
+ })
237
+ ],
238
+ }).start();
239
+ }
240
+ ```
241
+
242
+ **MailerSend Client Options:**
243
+
244
+ ```typescript
245
+ interface mailersendClientOptions {
246
+ apiKey: string; // MailerSend API token
247
+ defaultFrom?: MailersendAddress; // Used when email.from is empty
248
+ baseUrl?: string; // Defaults to "https://api.mailersend.com/v1"
249
+ }
250
+ ```
251
+
252
+ **MailerSend Email Type:**
253
+
254
+ ```typescript
255
+ type MailersendAddress = string | { email: string; name?: string };
256
+ // Strings may be plain ("user@example.com") or "Display Name <user@example.com>"
257
+
258
+ type emailMailersend = {
259
+ from: MailersendAddress;
260
+ to: MailersendAddress[];
261
+ cc?: MailersendAddress[];
262
+ bcc?: MailersendAddress[];
263
+ replyTo?: MailersendAddress;
264
+ tags?: string[]; // MailerSend tags
265
+ personalization?: MailersendPersonalization[]; // Per-recipient template variables
266
+ attachments?: MailersendAttachment[];
267
+ sendAt?: number; // Unix timestamp for scheduled delivery
268
+ } & (
269
+ | { subject: string; text: string; html?: string; templateId?: string }
270
+ | { subject: string; html: string; text?: string; templateId?: string }
271
+ | { templateId: string; subject?: string; text?: string; html?: string }
272
+ );
273
+
274
+ interface MailersendAttachment {
275
+ content: string; // Base64 encoded
276
+ filename: string;
277
+ disposition?: "attachment" | "inline"; // Defaults to "attachment"
278
+ id?: string; // Content-ID for inline images
279
+ }
280
+
281
+ interface MailersendPersonalization {
282
+ email: string; // Recipient the data applies to
283
+ data: Record<string, any>; // Template variables
284
+ }
285
+ ```
286
+
287
+ **Templates and rich results:**
288
+
289
+ ```typescript
290
+ import { mailersendClient } from "@flink-app/email-plugin";
291
+ const mailersend = ctx.plugins.emailPlugin.client as mailersendClient;
292
+
293
+ // Send using a MailerSend template with per-recipient variables
294
+ const result = await mailersend.sendEmail({
295
+ from: { email: "noreply@example.com", name: "My App" },
296
+ to: ["user@example.com"],
297
+ templateId: "3z0vklo7x1pg7qrx",
298
+ personalization: [{ email: "user@example.com", data: { name: "Jane" } }],
299
+ });
300
+
301
+ if (result.success) {
302
+ console.log("Sent:", result.messageId);
303
+ } else {
304
+ console.error(`Failed: ${result.error?.status} - ${result.error?.message}`);
305
+ }
306
+ ```
307
+
220
308
  ## TypeScript Setup
221
309
 
222
310
  Add the plugin context to your app's context type (usually in `Ctx.ts`):
@@ -405,7 +493,7 @@ All email clients implement this unified interface:
405
493
 
406
494
  ```typescript
407
495
  interface client {
408
- send(email: email | emailSendgrid | emailFlowmailer | emailSes | emailPostmark): Promise<boolean>;
496
+ send(email: email | emailSendgrid | emailFlowmailer | emailSes | emailPostmark | emailMailersend): Promise<boolean>;
409
497
  }
410
498
  ```
411
499
 
@@ -0,0 +1,191 @@
1
+ import { mailersendClient, toMailersendAddress } from "../src/mailersendClient";
2
+
3
+ function createMockHttp(overrides?: { response?: any; error?: any }) {
4
+ return {
5
+ post: jasmine.createSpy("post").and.callFake(async () => {
6
+ if (overrides?.error) throw overrides.error;
7
+ return (
8
+ overrides?.response ?? {
9
+ status: 202,
10
+ headers: { "x-message-id": "msg-123" },
11
+ data: "",
12
+ }
13
+ );
14
+ }),
15
+ } as any;
16
+ }
17
+
18
+ describe("mailersendClient", () => {
19
+ describe("toMailersendAddress()", () => {
20
+ it("should pass through plain email strings", () => {
21
+ expect(toMailersendAddress("a@example.com")).toEqual({ email: "a@example.com" });
22
+ });
23
+
24
+ it("should parse 'Name <email>' strings", () => {
25
+ expect(toMailersendAddress("Jane Doe <jane@example.com>")).toEqual({
26
+ email: "jane@example.com",
27
+ name: "Jane Doe",
28
+ });
29
+ expect(toMailersendAddress('"Jane Doe" <jane@example.com>')).toEqual({
30
+ email: "jane@example.com",
31
+ name: "Jane Doe",
32
+ });
33
+ });
34
+
35
+ it("should pass through address objects", () => {
36
+ expect(toMailersendAddress({ email: "a@example.com", name: "A" })).toEqual({
37
+ email: "a@example.com",
38
+ name: "A",
39
+ });
40
+ expect(toMailersendAddress({ email: "a@example.com" })).toEqual({ email: "a@example.com" });
41
+ });
42
+ });
43
+
44
+ describe("send()", () => {
45
+ it("should POST a mapped payload to /email and return true", async () => {
46
+ const http = createMockHttp();
47
+ const client = new mailersendClient({ apiKey: "key" }, http);
48
+
49
+ const result = await client.send({
50
+ from: "Sender <sender@example.com>",
51
+ to: ["recipient@example.com", { email: "other@example.com", name: "Other" }],
52
+ cc: ["cc@example.com"],
53
+ bcc: ["bcc@example.com"],
54
+ replyTo: "reply@example.com",
55
+ subject: "Test",
56
+ text: "Hello",
57
+ html: "<p>Hello</p>",
58
+ tags: ["welcome"],
59
+ attachments: [{ content: "YmFzZTY0", filename: "file.txt" }],
60
+ });
61
+
62
+ expect(result).toBe(true);
63
+ expect(http.post).toHaveBeenCalledTimes(1);
64
+ const [url, payload] = http.post.calls.first().args;
65
+ expect(url).toBe("/email");
66
+ expect(payload).toEqual({
67
+ from: { email: "sender@example.com", name: "Sender" },
68
+ to: [{ email: "recipient@example.com" }, { email: "other@example.com", name: "Other" }],
69
+ cc: [{ email: "cc@example.com" }],
70
+ bcc: [{ email: "bcc@example.com" }],
71
+ reply_to: { email: "reply@example.com" },
72
+ subject: "Test",
73
+ text: "Hello",
74
+ html: "<p>Hello</p>",
75
+ tags: ["welcome"],
76
+ attachments: [{ content: "YmFzZTY0", filename: "file.txt", disposition: "attachment" }],
77
+ });
78
+ });
79
+
80
+ it("should send template emails with personalization", async () => {
81
+ const http = createMockHttp();
82
+ const client = new mailersendClient({ apiKey: "key" }, http);
83
+
84
+ await client.send({
85
+ from: "sender@example.com",
86
+ to: ["recipient@example.com"],
87
+ templateId: "tpl-1",
88
+ personalization: [{ email: "recipient@example.com", data: { name: "Jane" } }],
89
+ });
90
+
91
+ const payload = http.post.calls.first().args[1];
92
+ expect(payload.template_id).toBe("tpl-1");
93
+ expect(payload.personalization).toEqual([
94
+ { email: "recipient@example.com", data: { name: "Jane" } },
95
+ ]);
96
+ expect(payload.subject).toBeUndefined();
97
+ expect(payload.text).toBeUndefined();
98
+ expect(payload.html).toBeUndefined();
99
+ });
100
+
101
+ it("should use defaultFrom when email has no from", async () => {
102
+ const http = createMockHttp();
103
+ const client = new mailersendClient(
104
+ { apiKey: "key", defaultFrom: { email: "default@example.com", name: "Default" } },
105
+ http
106
+ );
107
+
108
+ await client.send({
109
+ from: "",
110
+ to: ["recipient@example.com"],
111
+ subject: "Test",
112
+ text: "Hello",
113
+ });
114
+
115
+ expect(http.post.calls.first().args[1].from).toEqual({
116
+ email: "default@example.com",
117
+ name: "Default",
118
+ });
119
+ });
120
+
121
+ it("should return false without calling the API when no from is available", async () => {
122
+ const http = createMockHttp();
123
+ const client = new mailersendClient({ apiKey: "key" }, http);
124
+
125
+ const result = await client.send({
126
+ from: "",
127
+ to: ["recipient@example.com"],
128
+ subject: "Test",
129
+ text: "Hello",
130
+ });
131
+
132
+ expect(result).toBe(false);
133
+ expect(http.post).not.toHaveBeenCalled();
134
+ });
135
+
136
+ it("should return false on API error", async () => {
137
+ const http = createMockHttp({
138
+ error: { response: { status: 422, data: { message: "The from.email must be verified." } } },
139
+ });
140
+ const client = new mailersendClient({ apiKey: "key" }, http);
141
+
142
+ const result = await client.send({
143
+ from: "sender@example.com",
144
+ to: ["recipient@example.com"],
145
+ subject: "Test",
146
+ text: "Hello",
147
+ });
148
+
149
+ expect(result).toBe(false);
150
+ });
151
+ });
152
+
153
+ describe("sendEmail()", () => {
154
+ it("should return messageId from response header on success", async () => {
155
+ const http = createMockHttp({
156
+ response: { status: 202, headers: { "x-message-id": "abc-987" }, data: "" },
157
+ });
158
+ const client = new mailersendClient({ apiKey: "key" }, http);
159
+
160
+ const result = await client.sendEmail({
161
+ from: "sender@example.com",
162
+ to: ["recipient@example.com"],
163
+ subject: "Test",
164
+ text: "Hello",
165
+ });
166
+
167
+ expect(result).toEqual({ success: true, messageId: "abc-987" });
168
+ });
169
+
170
+ it("should return error details on failure", async () => {
171
+ const http = createMockHttp({
172
+ error: {
173
+ response: { status: 422, data: { message: "Validation failed", errors: { to: ["bad"] } } },
174
+ },
175
+ });
176
+ const client = new mailersendClient({ apiKey: "key" }, http);
177
+
178
+ const result = await client.sendEmail({
179
+ from: "sender@example.com",
180
+ to: ["recipient@example.com"],
181
+ subject: "Test",
182
+ text: "Hello",
183
+ });
184
+
185
+ expect(result.success).toBe(false);
186
+ expect(result.error?.status).toBe(422);
187
+ expect(result.error?.message).toBe("Validation failed");
188
+ expect(result.error?.details).toEqual({ message: "Validation failed", errors: { to: ["bad"] } });
189
+ });
190
+ });
191
+ });
package/src/index.ts CHANGED
@@ -5,9 +5,17 @@ export { smtpClient } from "./smtpClient";
5
5
  export { flowMailerClient } from "./flowmailerClient";
6
6
  export { sesClient } from "./sesClient";
7
7
  export { postmarkClient } from "./postmarkClient";
8
+ export { mailersendClient, toMailersendAddress } from "./mailersendClient";
9
+ export type { mailersendClientOptions, MailersendResult } from "./mailersendClient";
8
10
  export type { email } from "./schemas/email";
9
11
  export type { emailSes } from "./schemas/emailSes";
10
12
  export type { emailPostmark, PostmarkAttachment } from "./schemas/emailPostmark";
13
+ export type {
14
+ emailMailersend,
15
+ MailersendAddress,
16
+ MailersendAttachment,
17
+ MailersendPersonalization,
18
+ } from "./schemas/emailMailersend";
11
19
  export * from "./ses-types";
12
20
 
13
21
  export type emailPluginOptions = {
@@ -0,0 +1,170 @@
1
+ import axios, { AxiosInstance } from "axios";
2
+ import { client } from "./schemas/client";
3
+ import {
4
+ emailMailersend as email,
5
+ MailersendAddress,
6
+ } from "./schemas/emailMailersend";
7
+
8
+ export interface mailersendClientOptions {
9
+ /**
10
+ * MailerSend API token
11
+ */
12
+ apiKey: string;
13
+
14
+ /**
15
+ * Default "from" address when not specified per-email
16
+ */
17
+ defaultFrom?: MailersendAddress;
18
+
19
+ /**
20
+ * Override API base url, defaults to "https://api.mailersend.com/v1"
21
+ */
22
+ baseUrl?: string;
23
+ }
24
+
25
+ export interface MailersendResult {
26
+ success: boolean;
27
+ /**
28
+ * Message id returned by MailerSend (from the `x-message-id` response header)
29
+ */
30
+ messageId?: string;
31
+ error?: {
32
+ /**
33
+ * HTTP status code from MailerSend, if the request reached the API
34
+ */
35
+ status?: number;
36
+ message: string;
37
+ /**
38
+ * Raw error body returned by MailerSend, if any
39
+ */
40
+ details?: any;
41
+ };
42
+ }
43
+
44
+ type MailersendAddressPayload = { email: string; name?: string };
45
+
46
+ type MailersendPayload = {
47
+ from: MailersendAddressPayload;
48
+ to: MailersendAddressPayload[];
49
+ cc?: MailersendAddressPayload[];
50
+ bcc?: MailersendAddressPayload[];
51
+ reply_to?: MailersendAddressPayload;
52
+ subject?: string;
53
+ text?: string;
54
+ html?: string;
55
+ template_id?: string;
56
+ personalization?: { email: string; data: Record<string, any> }[];
57
+ tags?: string[];
58
+ attachments?: {
59
+ content: string;
60
+ filename: string;
61
+ disposition?: "attachment" | "inline";
62
+ id?: string;
63
+ }[];
64
+ send_at?: number;
65
+ };
66
+
67
+ /**
68
+ * Parse an address string such as "Jane Doe <jane@example.com>" into
69
+ * MailerSend's `{ email, name }` shape. Plain addresses are passed through.
70
+ */
71
+ export function toMailersendAddress(
72
+ address: MailersendAddress
73
+ ): MailersendAddressPayload {
74
+ if (typeof address !== "string") {
75
+ return address.name ? { email: address.email, name: address.name } : { email: address.email };
76
+ }
77
+ const match = address.trim().match(/^(.*?)\s*<([^<>]+)>$/);
78
+ if (match) {
79
+ const name = match[1].replace(/^"|"$/g, "").trim();
80
+ return name ? { email: match[2].trim(), name } : { email: match[2].trim() };
81
+ }
82
+ return { email: address.trim() };
83
+ }
84
+
85
+ export class mailersendClient implements client {
86
+ private http: AxiosInstance;
87
+ private defaultFrom?: MailersendAddress;
88
+
89
+ constructor(options: mailersendClientOptions, http?: AxiosInstance) {
90
+ this.defaultFrom = options.defaultFrom;
91
+ this.http =
92
+ http ??
93
+ axios.create({
94
+ baseURL: options.baseUrl ?? "https://api.mailersend.com/v1",
95
+ headers: {
96
+ Authorization: `Bearer ${options.apiKey}`,
97
+ "Content-Type": "application/json",
98
+ "X-Requested-With": "XMLHttpRequest",
99
+ },
100
+ });
101
+ }
102
+
103
+ /**
104
+ * Send an email via MailerSend. Implements the base `client` interface.
105
+ * Returns `true` on success, `false` on failure.
106
+ * For richer results (messageId, error details), use `sendEmail()` instead.
107
+ */
108
+ async send(email: email): Promise<boolean> {
109
+ const result = await this.sendEmail(email);
110
+ return result.success;
111
+ }
112
+
113
+ /**
114
+ * Send an email via MailerSend with rich result including messageId and error details.
115
+ */
116
+ async sendEmail(email: email): Promise<MailersendResult> {
117
+ const from = email.from || this.defaultFrom;
118
+ if (!from) {
119
+ return {
120
+ success: false,
121
+ error: {
122
+ message:
123
+ "No 'from' address specified. Set it on the email or use defaultFrom in mailersendClientOptions.",
124
+ },
125
+ };
126
+ }
127
+
128
+ const payload = this.buildPayload(email, from);
129
+
130
+ try {
131
+ const resp = await this.http.post("/email", payload);
132
+ const messageId = resp.headers?.["x-message-id"];
133
+ return { success: true, messageId };
134
+ } catch (ex: any) {
135
+ const status: number | undefined = ex?.response?.status;
136
+ const details = ex?.response?.data;
137
+ const message: string = details?.message || ex?.message || "Unknown error";
138
+ console.log(
139
+ JSON.stringify({ error: "MailerSend send failed", status, message, details })
140
+ );
141
+ return { success: false, error: { status, message, details } };
142
+ }
143
+ }
144
+
145
+ private buildPayload(email: email, from: MailersendAddress): MailersendPayload {
146
+ const payload: MailersendPayload = {
147
+ from: toMailersendAddress(from),
148
+ to: email.to.map(toMailersendAddress),
149
+ };
150
+ if (email.cc?.length) payload.cc = email.cc.map(toMailersendAddress);
151
+ if (email.bcc?.length) payload.bcc = email.bcc.map(toMailersendAddress);
152
+ if (email.replyTo) payload.reply_to = toMailersendAddress(email.replyTo);
153
+ if (email.subject !== undefined) payload.subject = email.subject;
154
+ if (email.text !== undefined) payload.text = email.text;
155
+ if (email.html !== undefined) payload.html = email.html;
156
+ if (email.templateId) payload.template_id = email.templateId;
157
+ if (email.personalization?.length) payload.personalization = email.personalization;
158
+ if (email.tags?.length) payload.tags = email.tags;
159
+ if (email.attachments?.length) {
160
+ payload.attachments = email.attachments.map((a) => ({
161
+ content: a.content,
162
+ filename: a.filename,
163
+ disposition: a.disposition ?? "attachment",
164
+ ...(a.id ? { id: a.id } : {}),
165
+ }));
166
+ }
167
+ if (email.sendAt !== undefined) payload.send_at = email.sendAt;
168
+ return payload;
169
+ }
170
+ }
@@ -1,9 +1,10 @@
1
1
  import { email } from "./email";
2
2
  import { emailFlowmailer } from "./emailFlowmailer";
3
+ import { emailMailersend } from "./emailMailersend";
3
4
  import { emailPostmark } from "./emailPostmark";
4
5
  import { emailSendgrid } from "./emailSendgrid";
5
6
  import { emailSes } from "./emailSes";
6
7
 
7
8
  export interface client {
8
- send(email: email | emailSendgrid | emailFlowmailer | emailSes | emailPostmark): Promise<boolean>;
9
+ send(email: email | emailSendgrid | emailFlowmailer | emailSes | emailPostmark | emailMailersend): Promise<boolean>;
9
10
  }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * An email address, either as a plain string ("user@example.com" or
3
+ * "Display Name <user@example.com>") or as an object with email and optional name.
4
+ */
5
+ export type MailersendAddress = string | { email: string; name?: string };
6
+
7
+ export interface MailersendAttachment {
8
+ /**
9
+ * Base64 encoded content
10
+ */
11
+ content: string;
12
+
13
+ /**
14
+ * Filename shown to recipient
15
+ */
16
+ filename: string;
17
+
18
+ /**
19
+ * "attachment" (default) or "inline". Inline attachments can be referenced
20
+ * from HTML via the `id`, e.g. `<img src="cid:image1">`
21
+ */
22
+ disposition?: "attachment" | "inline";
23
+
24
+ /**
25
+ * Content-ID used to reference inline attachments from the HTML body
26
+ */
27
+ id?: string;
28
+ }
29
+
30
+ export interface MailersendPersonalization {
31
+ /**
32
+ * Recipient email address this personalization data applies to
33
+ */
34
+ email: string;
35
+
36
+ /**
37
+ * Variables available to the template / `{{ var }}` substitutions
38
+ */
39
+ data: Record<string, any>;
40
+ }
41
+
42
+ type emailMailersendBase = {
43
+ /**
44
+ * From address used to send the email
45
+ */
46
+ from: MailersendAddress;
47
+
48
+ /**
49
+ * Email addresses to send to
50
+ */
51
+ to: MailersendAddress[];
52
+
53
+ /**
54
+ * CC addresses
55
+ */
56
+ cc?: MailersendAddress[];
57
+
58
+ /**
59
+ * BCC addresses
60
+ */
61
+ bcc?: MailersendAddress[];
62
+
63
+ /**
64
+ * Reply-to address
65
+ */
66
+ replyTo?: MailersendAddress;
67
+
68
+ /**
69
+ * MailerSend tags attached to the message
70
+ */
71
+ tags?: string[];
72
+
73
+ /**
74
+ * Per-recipient template/substitution variables
75
+ */
76
+ personalization?: MailersendPersonalization[];
77
+
78
+ /**
79
+ * File attachments
80
+ */
81
+ attachments?: MailersendAttachment[];
82
+
83
+ /**
84
+ * Unix timestamp (seconds) to schedule the email for later delivery (max 72h ahead)
85
+ */
86
+ sendAt?: number;
87
+ };
88
+
89
+ export type emailMailersend = emailMailersendBase &
90
+ (
91
+ | { subject: string; text: string; html?: string; templateId?: string }
92
+ | { subject: string; html: string; text?: string; templateId?: string }
93
+ | {
94
+ /**
95
+ * MailerSend template id. When set, subject/text/html may be omitted
96
+ * and are taken from the template (unless overridden here).
97
+ */
98
+ templateId: string;
99
+ subject?: string;
100
+ text?: string;
101
+ html?: string;
102
+ }
103
+ );