@mailtarn/node 0.0.0-stage → 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Limenzy Technologies Pvt Ltd
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,76 @@
1
- # Temporary Holding Version
1
+ # @mailtarn/node
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Mailtarn for Node.js (18+). No dependencies.
4
+
5
+ - **Read captured email in tests:** wait for a message, then use its links and one-time codes.
6
+ - **Send into a Mailtarn inbox over HTTP** instead of SMTP. Mailtarn never delivers anything:
7
+ messages land in your inbox and stay there.
8
+
9
+ ```bash
10
+ npm install @mailtarn/node
11
+ ```
12
+
13
+ Create an API token in Mailtarn (Developer → API tokens) with the permissions you need and keep
14
+ it in a secret, e.g. `MAILTARN_TOKEN`.
15
+
16
+ ## Wait for an email in a test
17
+
18
+ ```ts
19
+ import { Mailtarn } from '@mailtarn/node';
20
+
21
+ const mt = new Mailtarn({ token: process.env.MAILTARN_TOKEN! });
22
+ const inboxId = process.env.MAILTARN_INBOX!;
23
+
24
+ const since = new Date();
25
+ await signUp('priya@example.com'); // your app sends the verification email
26
+
27
+ const email = await mt.waitFor(inboxId, { to: 'priya@example.com', since });
28
+ expect(email.subject).toBe('Verify your email');
29
+ await page.goto(email.links[0]!.url); // or: await page.fill('#code', email.codes[0]!)
30
+ ```
31
+
32
+ `waitFor` returns the first matching message received after `since` (without `since`, the newest
33
+ match, or the next to arrive). It waits up to `timeoutMs` (default 30 000) and throws a
34
+ `MailtarnError` with code `wait_timeout` if nothing arrives. Filters: `to`, `from`, `subject`,
35
+ `q` (anywhere), `hasAttachments`. Needs `messages:read`.
36
+
37
+ Each message has `text`, `html` (as sent; untrusted, render it only in a sandbox), `headers`,
38
+ `attachments`, `envelope` (every recipient, including BCC), `links` and `codes`.
39
+
40
+ ## Send into an inbox
41
+
42
+ ```ts
43
+ await mt.send({
44
+ inbox: inboxId, // not needed with a token for one inbox
45
+ from: { name: 'Acme', address: 'hello@acme.test' },
46
+ to: 'priya@example.com',
47
+ subject: 'Welcome',
48
+ html: '<p>Hello</p>',
49
+ attachments: [{ filename: 'invoice.pdf', content: pdfBytes, contentType: 'application/pdf' }],
50
+ });
51
+ ```
52
+
53
+ Addresses take `a@b.test`, `Name <a@b.test>`, `"Last, First" <a@b.test>` or `{ name, address }`.
54
+ Needs `messages:send`. Returns once the message is stored (`202`); it is processed a moment later.
55
+
56
+ ## Everything else
57
+
58
+ ```ts
59
+ await mt.inboxes.list();
60
+ await mt.messages.list(inboxId, { subject: 'Welcome', limit: 20 });
61
+ await mt.messages.get(id);
62
+ await mt.messages.raw(id); // the .eml bytes
63
+ await mt.messages.attachment(id, 0);
64
+ await mt.messages.delete(id); // messages:delete
65
+ await mt.inboxes.clear(inboxId); // messages:delete
66
+ await mt.usage();
67
+ ```
68
+
69
+ Errors are `MailtarnError` with `status` and a stable `code` (`unauthorized`,
70
+ `insufficient_scope`, `not_found`, `rate_limited`, `wait_timeout`, …).
71
+
72
+ Another Mailtarn server (for example a local one while developing): `new Mailtarn({ token, baseUrl: 'http://localhost:3001' })`.
73
+
74
+ ## Licence
75
+
76
+ MIT. Copyright (c) 2026 Limenzy Technologies Pvt Ltd.
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Mailtarn for Node.js.
3
+ *
4
+ * import { Mailtarn } from '@mailtarn/node';
5
+ * const mt = new Mailtarn({ token: process.env.MAILTARN_TOKEN! });
6
+ *
7
+ * const since = new Date();
8
+ * await signUp('priya@example.com'); // your app sends the email
9
+ * const email = await mt.waitFor(inboxId, { to: 'priya@example.com', since });
10
+ * expect(email.subject).toBe('Verify your email');
11
+ * await page.goto(email.links[0]!.url); // or type email.codes[0]
12
+ *
13
+ * Mailtarn captures email and never delivers it. `send` puts a message into a Mailtarn inbox
14
+ * over HTTP (instead of SMTP); it does not send to the real recipients.
15
+ */
16
+ export interface Address {
17
+ name: string;
18
+ address: string;
19
+ }
20
+ export interface Attachment {
21
+ index: number;
22
+ filename: string | null;
23
+ contentType: string;
24
+ size: number;
25
+ contentId: string | null;
26
+ inline: boolean;
27
+ }
28
+ export interface Message {
29
+ id: string;
30
+ inboxId: string;
31
+ status: 'received' | 'parsed' | 'failed';
32
+ receivedAt: string;
33
+ subject: string | null;
34
+ from: string | null;
35
+ to: Address[];
36
+ cc: Address[];
37
+ messageId: string | null;
38
+ envelope: {
39
+ from: string | null;
40
+ to: string[];
41
+ };
42
+ text: string | null;
43
+ /** The HTML exactly as sent. Untrusted: never render it outside a sandbox. */
44
+ html: string | null;
45
+ headers: Array<{
46
+ name: string;
47
+ value: string;
48
+ }>;
49
+ attachments: Attachment[];
50
+ /** http(s) links, from the HTML (with their text) then the plain text. */
51
+ links: Array<{
52
+ url: string;
53
+ text: string | null;
54
+ }>;
55
+ /** One-time codes near words like "code" or "OTP", most likely first. */
56
+ codes: string[];
57
+ warnings: string[];
58
+ }
59
+ export interface MessageSummary {
60
+ id: string;
61
+ inboxId: string;
62
+ status: Message['status'];
63
+ receivedAt: string;
64
+ subject: string | null;
65
+ from: string | null;
66
+ to: Address[];
67
+ snippet: string | null;
68
+ hasAttachments: boolean;
69
+ }
70
+ export interface Inbox {
71
+ id: string;
72
+ name: string;
73
+ projectId: string;
74
+ smtpUsername: string;
75
+ isCi: boolean;
76
+ createdAt: string;
77
+ }
78
+ export interface Filters {
79
+ /** Substring of a To/Cc/envelope recipient. */
80
+ to?: string;
81
+ from?: string;
82
+ /** Substring of the subject. */
83
+ subject?: string;
84
+ /** Words anywhere (subject, addresses, body). */
85
+ q?: string;
86
+ /** Only messages received after this time. */
87
+ since?: Date | string;
88
+ hasAttachments?: boolean;
89
+ }
90
+ /** `a@b.test`, `Name <a@b.test>`, `"Last, First" <a@b.test>`, or the object form. */
91
+ export type AddressInput = string | {
92
+ name?: string;
93
+ address: string;
94
+ };
95
+ export interface SendInput {
96
+ /** Inbox id. Not needed with a token for one inbox. */
97
+ inbox?: string;
98
+ from: AddressInput;
99
+ to?: AddressInput | AddressInput[];
100
+ cc?: AddressInput | AddressInput[];
101
+ bcc?: AddressInput | AddressInput[];
102
+ replyTo?: AddressInput;
103
+ subject?: string;
104
+ text?: string;
105
+ html?: string;
106
+ /** Custom headers such as X-Env (addressing and MIME headers come from the fields). */
107
+ headers?: Record<string, string>;
108
+ attachments?: Array<{
109
+ filename?: string;
110
+ content: string | Uint8Array;
111
+ contentType?: string;
112
+ contentId?: string;
113
+ inline?: boolean;
114
+ }>;
115
+ }
116
+ export interface MailtarnOptions {
117
+ /** An API token (mtk_…). */
118
+ token: string;
119
+ /** Default https://app.mailtarn.com */
120
+ baseUrl?: string;
121
+ /** For tests and unusual runtimes. Default: the global fetch. */
122
+ fetch?: typeof fetch;
123
+ }
124
+ /** An error response from the API, with its stable code (e.g. "insufficient_scope", "wait_timeout"). */
125
+ export declare class MailtarnError extends Error {
126
+ readonly status: number;
127
+ readonly code: string;
128
+ constructor(status: number, code: string, message: string);
129
+ }
130
+ export declare class Mailtarn {
131
+ private readonly base;
132
+ private readonly token;
133
+ private readonly doFetch;
134
+ constructor(opts: MailtarnOptions);
135
+ /** Puts a message into a Mailtarn inbox (stored, never delivered). Needs messages:send. */
136
+ send(input: SendInput): Promise<{
137
+ id: string;
138
+ inboxId: string;
139
+ status: 'received';
140
+ }>;
141
+ /**
142
+ * Waits for a message matching the filters and returns it in full. With `since`, the first
143
+ * match received after it; without, the newest match (or the next to arrive). `timeoutMs`
144
+ * (default 30 s) may exceed the server's 60 s per request: it keeps asking until then.
145
+ * Throws MailtarnError with code "wait_timeout" if nothing arrives.
146
+ */
147
+ waitFor(inboxId: string, filters?: Filters & {
148
+ timeoutMs?: number;
149
+ }): Promise<Message>;
150
+ readonly inboxes: {
151
+ list: () => Promise<Inbox[]>;
152
+ /** Needs inboxes:manage. The password is returned once. */
153
+ create: (input: {
154
+ name: string;
155
+ projectId?: string;
156
+ isCi?: boolean;
157
+ }) => Promise<{
158
+ inbox: {
159
+ id: string;
160
+ name: string;
161
+ projectId: string;
162
+ smtpUsername: string;
163
+ };
164
+ credential: {
165
+ username: string;
166
+ password: string;
167
+ };
168
+ }>;
169
+ /** Deletes every message in the inbox. Needs messages:delete. */
170
+ clear: (inboxId: string) => Promise<{
171
+ deleted: number;
172
+ }>;
173
+ };
174
+ readonly messages: {
175
+ list: (inboxId: string, filters?: Filters & {
176
+ limit?: number;
177
+ cursor?: string;
178
+ }) => Promise<{
179
+ messages: MessageSummary[];
180
+ nextCursor: string | null;
181
+ }>;
182
+ get: (id: string) => Promise<Message>;
183
+ /** The message as received (.eml bytes). */
184
+ raw: (id: string) => Promise<Uint8Array>;
185
+ attachment: (id: string, index: number) => Promise<Uint8Array>;
186
+ delete: (id: string) => Promise<void>;
187
+ };
188
+ /** This month's usage for what the token can see: its organization, project or inbox (`scope`). */
189
+ usage(): Promise<{
190
+ scope: "org" | "project" | "inbox";
191
+ periodStart: string;
192
+ messages: number;
193
+ bytesIn: number;
194
+ storageBytes: number;
195
+ }>;
196
+ private request;
197
+ private json;
198
+ }
package/dist/index.js ADDED
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Mailtarn for Node.js.
3
+ *
4
+ * import { Mailtarn } from '@mailtarn/node';
5
+ * const mt = new Mailtarn({ token: process.env.MAILTARN_TOKEN! });
6
+ *
7
+ * const since = new Date();
8
+ * await signUp('priya@example.com'); // your app sends the email
9
+ * const email = await mt.waitFor(inboxId, { to: 'priya@example.com', since });
10
+ * expect(email.subject).toBe('Verify your email');
11
+ * await page.goto(email.links[0]!.url); // or type email.codes[0]
12
+ *
13
+ * Mailtarn captures email and never delivers it. `send` puts a message into a Mailtarn inbox
14
+ * over HTTP (instead of SMTP); it does not send to the real recipients.
15
+ */
16
+ /** An error response from the API, with its stable code (e.g. "insufficient_scope", "wait_timeout"). */
17
+ export class MailtarnError extends Error {
18
+ status;
19
+ code;
20
+ constructor(status, code, message) {
21
+ super(message);
22
+ this.status = status;
23
+ this.code = code;
24
+ this.name = 'MailtarnError';
25
+ }
26
+ }
27
+ const WAIT_MAX_S = 60;
28
+ export class Mailtarn {
29
+ base;
30
+ token;
31
+ doFetch;
32
+ constructor(opts) {
33
+ if (!opts?.token)
34
+ throw new Error('Mailtarn: pass { token } (an API token starting with mtk_).');
35
+ this.token = opts.token;
36
+ this.base = (opts.baseUrl ?? 'https://app.mailtarn.com').replace(/\/+$/, '');
37
+ this.doFetch = opts.fetch ?? fetch;
38
+ }
39
+ /** Puts a message into a Mailtarn inbox (stored, never delivered). Needs messages:send. */
40
+ async send(input) {
41
+ const body = {
42
+ ...input,
43
+ attachments: input.attachments?.map((a) => ({
44
+ ...a,
45
+ content: typeof a.content === 'string' ? a.content : base64(a.content),
46
+ })),
47
+ };
48
+ return this.json('POST', '/v1/send', body);
49
+ }
50
+ /**
51
+ * Waits for a message matching the filters and returns it in full. With `since`, the first
52
+ * match received after it; without, the newest match (or the next to arrive). `timeoutMs`
53
+ * (default 30 s) may exceed the server's 60 s per request: it keeps asking until then.
54
+ * Throws MailtarnError with code "wait_timeout" if nothing arrives.
55
+ */
56
+ async waitFor(inboxId, filters = {}) {
57
+ const deadline = Date.now() + (filters.timeoutMs ?? 30_000);
58
+ for (;;) {
59
+ const left = Math.max(0, Math.ceil((deadline - Date.now()) / 1000));
60
+ try {
61
+ const r = await this.json('GET', `/v1/inboxes/${enc(inboxId)}/messages/wait${qs({ ...filters, timeout: Math.min(left, WAIT_MAX_S) })}`);
62
+ return r.message;
63
+ }
64
+ catch (e) {
65
+ if (e instanceof MailtarnError && e.code === 'wait_timeout' && Date.now() < deadline)
66
+ continue;
67
+ throw e;
68
+ }
69
+ }
70
+ }
71
+ inboxes = {
72
+ list: async () => (await this.json('GET', '/v1/inboxes')).inboxes,
73
+ /** Needs inboxes:manage. The password is returned once. */
74
+ create: async (input) => this.json('POST', '/v1/inboxes', input),
75
+ /** Deletes every message in the inbox. Needs messages:delete. */
76
+ clear: async (inboxId) => this.json('DELETE', `/v1/inboxes/${enc(inboxId)}/messages`),
77
+ };
78
+ messages = {
79
+ list: async (inboxId, filters = {}) => this.json('GET', `/v1/inboxes/${enc(inboxId)}/messages${qs(filters)}`),
80
+ get: async (id) => (await this.json('GET', `/v1/messages/${enc(id)}`)).message,
81
+ /** The message as received (.eml bytes). */
82
+ raw: async (id) => new Uint8Array(await (await this.request('GET', `/v1/messages/${enc(id)}/raw`)).arrayBuffer()),
83
+ attachment: async (id, index) => new Uint8Array(await (await this.request('GET', `/v1/messages/${enc(id)}/attachments/${index}`)).arrayBuffer()),
84
+ delete: async (id) => { await this.request('DELETE', `/v1/messages/${enc(id)}`); },
85
+ };
86
+ /** This month's usage for what the token can see: its organization, project or inbox (`scope`). */
87
+ async usage() {
88
+ return this.json('GET', '/v1/usage');
89
+ }
90
+ async request(method, path, body) {
91
+ const res = await this.doFetch(`${this.base}${path}`, {
92
+ method,
93
+ headers: { authorization: `Bearer ${this.token}`, ...(body !== undefined ? { 'content-type': 'application/json' } : {}) },
94
+ body: body !== undefined ? JSON.stringify(body) : undefined,
95
+ });
96
+ if (!res.ok) {
97
+ const e = (await res.json().catch(() => null));
98
+ throw new MailtarnError(res.status, e?.error?.code ?? `http_${res.status}`, e?.error?.message ?? res.statusText);
99
+ }
100
+ return res;
101
+ }
102
+ async json(method, path, body) {
103
+ const res = await this.request(method, path, body);
104
+ return (res.status === 204 ? undefined : await res.json());
105
+ }
106
+ }
107
+ function enc(s) {
108
+ return encodeURIComponent(s);
109
+ }
110
+ function qs(f) {
111
+ const p = new URLSearchParams();
112
+ for (const [k, v] of Object.entries(f)) {
113
+ if (v === undefined || v === null || k === 'timeoutMs')
114
+ continue;
115
+ p.set(k, v instanceof Date ? v.toISOString() : String(v));
116
+ }
117
+ const s = p.toString();
118
+ return s ? `?${s}` : '';
119
+ }
120
+ function base64(b) {
121
+ let s = '';
122
+ for (let i = 0; i < b.length; i += 0x8000)
123
+ s += String.fromCharCode(...b.subarray(i, i + 0x8000));
124
+ return btoa(s);
125
+ }
package/package.json CHANGED
@@ -1,6 +1,41 @@
1
1
  {
2
2
  "name": "@mailtarn/node",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "Mailtarn for Node.js: send into a Mailtarn inbox and read captured email in tests (wait for a message, get its links and codes).",
5
+ "license": "MIT",
6
+ "author": "Limenzy Technologies Pvt Ltd",
7
+ "homepage": "https://mailtarn.com",
8
+ "type": "module",
9
+ "main": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "README.md",
20
+ "LICENSE"
21
+ ],
22
+ "engines": {
23
+ "node": ">=18"
24
+ },
25
+ "sideEffects": false,
26
+ "keywords": [
27
+ "email",
28
+ "testing",
29
+ "smtp",
30
+ "sandbox",
31
+ "e2e",
32
+ "otp",
33
+ "mailtarn"
34
+ ],
35
+ "publishConfig": {
36
+ "access": "public"
37
+ },
38
+ "scripts": {
39
+ "build": "tsc -p tsconfig.build.json"
40
+ }
6
41
  }