@flink-app/email-plugin 2.0.0-alpha.110 → 2.0.0-alpha.112
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 +23 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +4 -1
- package/dist/mailersendClient.d.ts +61 -0
- package/dist/mailersendClient.js +179 -0
- package/dist/schemas/client.d.ts +2 -1
- package/dist/schemas/emailMailersend.d.ts +96 -0
- package/dist/schemas/emailMailersend.js +2 -0
- package/package.json +4 -4
- package/readme.md +91 -3
- package/spec/mailersendClient.spec.ts +191 -0
- package/src/index.ts +8 -0
- package/src/mailersendClient.ts +170 -0
- package/src/schemas/client.ts +2 -1
- package/src/schemas/emailMailersend.ts +103 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# @flink-app/email-plugin
|
|
2
2
|
|
|
3
|
+
## 2.0.0-alpha.112
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [2e21643]
|
|
8
|
+
- @flink-app/flink@2.0.0-alpha.112
|
|
9
|
+
|
|
10
|
+
## 2.0.0-alpha.111
|
|
11
|
+
|
|
12
|
+
### Minor Changes
|
|
13
|
+
|
|
14
|
+
- 00246e8: feat(email-plugin): add MailerSend provider
|
|
15
|
+
|
|
16
|
+
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 }) })`.
|
|
17
|
+
|
|
18
|
+
- New `emailMailersend` type: addresses as plain strings, `"Name <email>"` strings or `{ email, name }` objects; supports `templateId`, `personalization`, `tags`, inline/regular base64 attachments and `sendAt`
|
|
19
|
+
- `sendEmail()` returns rich results (`messageId`, HTTP status and error details)
|
|
20
|
+
- Optional `defaultFrom` and `baseUrl` client options
|
|
21
|
+
|
|
22
|
+
### Patch Changes
|
|
23
|
+
|
|
24
|
+
- @flink-app/flink@2.0.0-alpha.111
|
|
25
|
+
|
|
3
26
|
## 2.0.0-alpha.110
|
|
4
27
|
|
|
5
28
|
### 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;
|
package/dist/schemas/client.d.ts
CHANGED
|
@@ -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 {};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flink-app/email-plugin",
|
|
3
|
-
"version": "2.0.0-alpha.
|
|
4
|
-
"description": "Flink plugin that provides multi-provider email sending capabilities with support for SendGrid, SMTP, Flowmailer, AWS SES, and
|
|
3
|
+
"version": "2.0.0-alpha.112",
|
|
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.
|
|
24
|
+
"@flink-app/flink": "2.0.0-alpha.112"
|
|
25
25
|
},
|
|
26
26
|
"gitHead": "4243e3b3cd6d4e1ca001a61baa8436bf2bbe4113",
|
|
27
27
|
"peerDependencies": {
|
|
28
|
-
"@flink-app/flink": ">=2.0.0-alpha.
|
|
28
|
+
"@flink-app/flink": ">=2.0.0-alpha.112"
|
|
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
|
|
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
|
|
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
|
+
}
|
package/src/schemas/client.ts
CHANGED
|
@@ -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
|
+
);
|