@agdirlabs/nodemailer-transport 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/CHANGELOG.md +16 -0
- package/LICENSE +21 -0
- package/README.md +167 -2
- package/dist/index.cjs +228 -0
- package/dist/index.d.cts +76 -0
- package/dist/index.d.ts +76 -0
- package/dist/index.js +194 -0
- package/package.json +70 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Changelog — @agdirlabs/nodemailer-transport
|
|
2
|
+
|
|
3
|
+
All notable changes to this package are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the package uses [Semantic Versioning](https://semver.org).
|
|
4
|
+
|
|
5
|
+
## [Unreleased]
|
|
6
|
+
|
|
7
|
+
## [0.1.0] — 2026-10-30
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `agdirTransport()` / `AgdirTransport`: a Nodemailer transport that sends through the Agdir Email API with `@agdirlabs/email`.
|
|
12
|
+
- Accepts every Nodemailer address form (strings, `{ name, address }`, arrays, groups) and `html`/`text` as strings, Buffers, streams, files or URLs.
|
|
13
|
+
- Custom headers (object or array form), `inReplyTo`, `references`, `list` and `priority`.
|
|
14
|
+
- Tags, scheduled sending and idempotency keys via message options (`tags`, `scheduledAt`, `idempotencyKey`) or `X-Agdir-*` headers.
|
|
15
|
+
- `sendMail` info with the Agdir email id as `messageId`; API failures reported as `AgdirEmailError` (`code`, `status`, `responseCode`).
|
|
16
|
+
- Clear errors for features the API does not support yet (attachments, `raw`, `alternatives`, `icalEvent`, custom `envelope`).
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Agdir AS
|
|
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,168 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @agdirlabs/nodemailer-transport
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@agdirlabs/nodemailer-transport)
|
|
4
|
+
[](https://github.com/AgdirAS/agdirlabs-sdks/actions/workflows/node.yml)
|
|
5
|
+
|
|
6
|
+
A [Nodemailer](https://nodemailer.com) transport for [Agdir Email](https://agdir.email) — programmatic SMTP for developers, hosted in the EU. Keep your existing `transporter.sendMail()` code and send over the Agdir Email API instead of SMTP. Built by [Agdir Labs](https://agdirlabs.com) at [Agdir](https://agdir.no) on top of the official SDK, [`@agdirlabs/email`](https://www.npmjs.com/package/@agdirlabs/email).
|
|
7
|
+
|
|
8
|
+
- Drop-in: any Nodemailer address form, Buffers and streams for `html`/`text`, custom headers
|
|
9
|
+
- Tags, scheduled sending and idempotency keys
|
|
10
|
+
- Safe automatic retries (rate limits, server and network errors) from the SDK
|
|
11
|
+
- Fully typed, ESM and CommonJS, Nodemailer 6 and newer, Node.js 20+
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install @agdirlabs/nodemailer-transport nodemailer
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quick start
|
|
20
|
+
|
|
21
|
+
Create an API key at [app.agdir.email](https://app.agdir.email) and verify a sending domain, then:
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import nodemailer from 'nodemailer';
|
|
25
|
+
import { agdirTransport } from '@agdirlabs/nodemailer-transport';
|
|
26
|
+
|
|
27
|
+
const transporter = nodemailer.createTransport(agdirTransport({ apiKey: process.env.AGDIR_EMAIL_API_KEY }));
|
|
28
|
+
|
|
29
|
+
const info = await transporter.sendMail({
|
|
30
|
+
from: 'Acme <hello@acme.com>',
|
|
31
|
+
to: 'user@example.com',
|
|
32
|
+
subject: 'Welcome to Acme',
|
|
33
|
+
html: '<p>Thanks for signing up!</p>'
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
info.messageId; // the Agdir Email id, e.g. for agdir.emails.get(info.messageId)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`agdirTransport()` with no arguments reads `AGDIR_EMAIL_API_KEY` (and `AGDIR_EMAIL_BASE_URL`) from the environment.
|
|
40
|
+
|
|
41
|
+
CommonJS works the same way:
|
|
42
|
+
|
|
43
|
+
```js
|
|
44
|
+
const nodemailer = require('nodemailer');
|
|
45
|
+
const { agdirTransport } = require('@agdirlabs/nodemailer-transport');
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Options
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
agdirTransport({
|
|
52
|
+
apiKey: 're_…', // default: AGDIR_EMAIL_API_KEY
|
|
53
|
+
baseUrl: 'https://api.agdir.email', // default: AGDIR_EMAIL_BASE_URL, else this
|
|
54
|
+
maxRetries: 2, // retries for rate limits, server errors and network failures; 0 disables
|
|
55
|
+
timeoutMs: 30_000, // per attempt
|
|
56
|
+
fetch: customFetch // e.g. for a proxy
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Or reuse a client you already have
|
|
60
|
+
import { AgdirEmail } from '@agdirlabs/email';
|
|
61
|
+
const agdir = new AgdirEmail();
|
|
62
|
+
const transporter = nodemailer.createTransport(agdirTransport({ client: agdir }));
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Messages
|
|
66
|
+
|
|
67
|
+
All standard address fields work — `from`, `to`, `cc`, `bcc`, `replyTo` — as strings (`'Ana <ana@example.com>, ben@example.com'`), `{ name, address }` objects, or arrays of either. `html` and `text` can be strings, Buffers, streams, or `{ path }` / `{ href }` (resolved by Nodemailer, subject to its `disableFileAccess` / `disableUrlAccess` settings). At least one of `html` and `text` is required.
|
|
68
|
+
|
|
69
|
+
Custom `headers` (object or `[{ key, value }]` form) are sent with the email; `inReplyTo`, `references`, `list` and `priority` become their usual headers.
|
|
70
|
+
|
|
71
|
+
### Tags, scheduling and idempotency
|
|
72
|
+
|
|
73
|
+
Agdir-specific options can be given as message fields or, if you'd rather not touch your message objects (or their TypeScript types), as `X-Agdir-*` headers. The transport reads these headers and does **not** send them.
|
|
74
|
+
|
|
75
|
+
| | Message field | Header |
|
|
76
|
+
|---|---|---|
|
|
77
|
+
| Tags (echoed back in webhook events) | `tags: [{ name, value }]` or `tags: { name: value }` | `X-Agdir-Tags: category=welcome, plan=pro` |
|
|
78
|
+
| Deliver later | `scheduledAt: Date \| ISO 8601 string` | `X-Agdir-Scheduled-At: 2026-11-01T09:00:00Z` |
|
|
79
|
+
| Idempotency key | `idempotencyKey: 'welcome/42'` | `X-Agdir-Idempotency-Key: welcome/42` |
|
|
80
|
+
|
|
81
|
+
Tag names and values may contain ASCII letters, digits, `_` and `-`. Tags from both places are combined; for the other two the message field wins.
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
// Headers: works with any typing
|
|
85
|
+
await transporter.sendMail({
|
|
86
|
+
from, to, subject, html,
|
|
87
|
+
headers: {
|
|
88
|
+
'X-Agdir-Tags': 'category=welcome',
|
|
89
|
+
'X-Agdir-Scheduled-At': new Date(Date.now() + 3600_000).toISOString(),
|
|
90
|
+
'X-Agdir-Idempotency-Key': `welcome/${user.id}`
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Message fields: build the message as a variable (Nodemailer's types don't declare these fields,
|
|
95
|
+
// so an inline object literal would fail TypeScript's excess-property check)
|
|
96
|
+
import type { AgdirMailOptions } from '@agdirlabs/nodemailer-transport';
|
|
97
|
+
import type { SendMailOptions } from 'nodemailer';
|
|
98
|
+
|
|
99
|
+
const message: SendMailOptions & AgdirMailOptions = {
|
|
100
|
+
from, to, subject, html,
|
|
101
|
+
tags: { category: 'welcome' },
|
|
102
|
+
scheduledAt: new Date(Date.now() + 3600_000),
|
|
103
|
+
idempotencyKey: `welcome/${user.id}`
|
|
104
|
+
};
|
|
105
|
+
await transporter.sendMail(message);
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Scheduled emails can be cancelled with the SDK: `await agdir.emails.cancel(info.messageId)`.
|
|
109
|
+
|
|
110
|
+
### Result
|
|
111
|
+
|
|
112
|
+
`sendMail` resolves (or calls back) with:
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
{
|
|
116
|
+
messageId: '4ef9a3c2-…', // Agdir Email id
|
|
117
|
+
envelope: { from: 'hello@acme.com', to: ['user@example.com'] },
|
|
118
|
+
accepted: ['user@example.com'], // every envelope recipient: the API accepts the email as a whole
|
|
119
|
+
rejected: [],
|
|
120
|
+
pending: [],
|
|
121
|
+
response: 'Accepted 4ef9a3c2-…'
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Delivery happens asynchronously; follow it with `agdir.emails.get(id)` or [webhooks](https://www.npmjs.com/package/@agdirlabs/email#webhooks).
|
|
126
|
+
|
|
127
|
+
### Not supported (yet)
|
|
128
|
+
|
|
129
|
+
The Agdir Email API does not take attachments yet, so a message with `attachments` (or `raw`, `alternatives`, `icalEvent`) fails with an `AgdirEmailError` whose `code` is `unsupported` — link to files from the body instead. A custom SMTP `envelope` is rejected the same way, because the email always goes to its `to`, `cc` and `bcc` recipients. `amp` and `watchHtml` are ignored, as is `verify()` (there is no connection to check).
|
|
130
|
+
|
|
131
|
+
## Errors
|
|
132
|
+
|
|
133
|
+
Failures are reported as an `AgdirEmailError` (re-exported from this package):
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
import { AgdirEmailError } from '@agdirlabs/nodemailer-transport';
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
await transporter.sendMail(message);
|
|
140
|
+
} catch (err) {
|
|
141
|
+
if (err instanceof AgdirEmailError) {
|
|
142
|
+
err.code; // 'validation_error' | 'daily_quota_exceeded' | 'rate_limit_exceeded' | 'network_error' | 'unsupported' | …
|
|
143
|
+
err.status; // HTTP status, 0 if no response (or the message was rejected before sending)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Like Nodemailer's SMTP transports, the error also carries `responseCode` (the HTTP status) when the API answered.
|
|
149
|
+
|
|
150
|
+
## Retries and idempotency
|
|
151
|
+
|
|
152
|
+
Rate limits (`429 rate_limit_exceeded`), server errors and network failures are retried by the SDK up to `maxRetries` times, honouring `retry-after`. Each send gets a generated idempotency key, so a retry can never deliver an email twice; pass your own (`idempotencyKey` / `X-Agdir-Idempotency-Key`) to make sends safe across process restarts too. Quota errors are not retried.
|
|
153
|
+
|
|
154
|
+
## Releasing
|
|
155
|
+
|
|
156
|
+
Push a tag `nodemailer-transport@<version>` matching `package.json` — `.github/workflows/release.yml` publishes it to npm with Trusted Publishing. One-time setup: on npmjs.com, add `AgdirAS/agdirlabs-sdks` with workflow `release.yml` as a trusted publisher for `@agdirlabs/nodemailer-transport` (for a brand-new package, the first version may have to be published by hand before the trusted publisher can be configured).
|
|
157
|
+
|
|
158
|
+
## Contributing
|
|
159
|
+
|
|
160
|
+
Issues and pull requests are welcome — see [CONTRIBUTING.md](../../CONTRIBUTING.md). Changes are listed in [CHANGELOG.md](CHANGELOG.md). The opt-in live test runs against a real API:
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
AGDIR_EMAIL_LIVE_BASE_URL=… AGDIR_EMAIL_LIVE_API_KEY=re_… AGDIR_EMAIL_LIVE_FROM=hi@your-domain.com pnpm test
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## License
|
|
167
|
+
|
|
168
|
+
[MIT](LICENSE) © [Agdir AS](https://agdir.no)
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
AgdirEmail: () => import_email2.AgdirEmail,
|
|
24
|
+
AgdirEmailError: () => import_email2.AgdirEmailError,
|
|
25
|
+
AgdirTransport: () => AgdirTransport,
|
|
26
|
+
IDEMPOTENCY_KEY_HEADER: () => IDEMPOTENCY_KEY_HEADER,
|
|
27
|
+
SCHEDULED_AT_HEADER: () => SCHEDULED_AT_HEADER,
|
|
28
|
+
TAGS_HEADER: () => TAGS_HEADER,
|
|
29
|
+
VERSION: () => VERSION,
|
|
30
|
+
agdirTransport: () => agdirTransport
|
|
31
|
+
});
|
|
32
|
+
module.exports = __toCommonJS(index_exports);
|
|
33
|
+
|
|
34
|
+
// src/transport.ts
|
|
35
|
+
var import_email = require("@agdirlabs/email");
|
|
36
|
+
|
|
37
|
+
// src/version.ts
|
|
38
|
+
var VERSION = "0.1.0";
|
|
39
|
+
|
|
40
|
+
// src/transport.ts
|
|
41
|
+
var TAGS_HEADER = "x-agdir-tags";
|
|
42
|
+
var SCHEDULED_AT_HEADER = "x-agdir-scheduled-at";
|
|
43
|
+
var IDEMPOTENCY_KEY_HEADER = "x-agdir-idempotency-key";
|
|
44
|
+
var CONTROL_HEADERS = /* @__PURE__ */ new Set([TAGS_HEADER, SCHEDULED_AT_HEADER, IDEMPOTENCY_KEY_HEADER]);
|
|
45
|
+
var AgdirTransport = class {
|
|
46
|
+
name = "AgdirEmail";
|
|
47
|
+
version = VERSION;
|
|
48
|
+
client;
|
|
49
|
+
constructor(options = {}) {
|
|
50
|
+
this.client = options.client ?? new import_email.AgdirEmail(options);
|
|
51
|
+
}
|
|
52
|
+
send(mail, callback) {
|
|
53
|
+
this.sendAsync(mail).then(
|
|
54
|
+
(info) => callback(null, info),
|
|
55
|
+
(err) => callback(toNodemailerError(err))
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
async sendAsync(mail) {
|
|
59
|
+
rejectUnsupported(mail.data);
|
|
60
|
+
const envelope = mail.message.getEnvelope();
|
|
61
|
+
const data = await new Promise(
|
|
62
|
+
(resolve, reject) => mail.normalize((err, normalized) => err ? reject(err) : resolve(normalized))
|
|
63
|
+
);
|
|
64
|
+
const { input, idempotencyKey } = toSendInput(data);
|
|
65
|
+
const { id } = await this.client.emails.send(input, idempotencyKey ? { idempotencyKey } : {});
|
|
66
|
+
return {
|
|
67
|
+
messageId: id,
|
|
68
|
+
envelope,
|
|
69
|
+
accepted: [...envelope.to],
|
|
70
|
+
rejected: [],
|
|
71
|
+
pending: [],
|
|
72
|
+
response: `Accepted ${id}`
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
function agdirTransport(options = {}) {
|
|
77
|
+
return new AgdirTransport(options);
|
|
78
|
+
}
|
|
79
|
+
function unsupported(message) {
|
|
80
|
+
return new import_email.AgdirEmailError(message, "unsupported", 0);
|
|
81
|
+
}
|
|
82
|
+
function invalid(message) {
|
|
83
|
+
return new import_email.AgdirEmailError(message, "validation_error", 0);
|
|
84
|
+
}
|
|
85
|
+
function rejectUnsupported(data) {
|
|
86
|
+
if (Array.isArray(data.attachments) ? data.attachments.length > 0 : data.attachments) {
|
|
87
|
+
throw unsupported("Attachments are not supported by the Agdir Email API yet. Link to the file from the email body instead.");
|
|
88
|
+
}
|
|
89
|
+
if (data.raw) throw unsupported("Raw MIME messages (`raw`) are not supported by the Agdir Email API.");
|
|
90
|
+
if (Array.isArray(data.alternatives) ? data.alternatives.length > 0 : data.alternatives) {
|
|
91
|
+
throw unsupported("`alternatives` are not supported by the Agdir Email API; use `html` and `text`.");
|
|
92
|
+
}
|
|
93
|
+
if (data.icalEvent) throw unsupported("Calendar events (`icalEvent`) are not supported by the Agdir Email API yet.");
|
|
94
|
+
if (data.envelope) {
|
|
95
|
+
throw unsupported("A custom SMTP `envelope` is not supported: the email is sent to its to, cc and bcc recipients.");
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function toSendInput(data) {
|
|
99
|
+
const headers = {};
|
|
100
|
+
const control = {};
|
|
101
|
+
for (const [key, value] of headerEntries(data.headers)) {
|
|
102
|
+
const lower = key.toLowerCase();
|
|
103
|
+
if (CONTROL_HEADERS.has(lower)) control[lower] = value;
|
|
104
|
+
else headers[key] = key in headers ? `${headers[key]}, ${value}` : value;
|
|
105
|
+
}
|
|
106
|
+
for (const [key, value] of Object.entries(data.normalizedHeaders ?? {})) {
|
|
107
|
+
if ((key === "in-reply-to" || key === "references" || key.startsWith("list-")) && typeof value === "string" && value) {
|
|
108
|
+
const existing = Object.keys(headers).find((k) => k.toLowerCase() === key);
|
|
109
|
+
headers[existing ?? canonicalHeaderName(key)] = value;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
Object.assign(headers, priorityHeaders(data.priority));
|
|
113
|
+
const tags = [...toTags(data.tags), ...control[TAGS_HEADER] ? parseTagsHeader(control[TAGS_HEADER]) : []];
|
|
114
|
+
const scheduledAt = data.scheduledAt ?? control[SCHEDULED_AT_HEADER];
|
|
115
|
+
const idempotencyKey = data.idempotencyKey ?? control[IDEMPOTENCY_KEY_HEADER];
|
|
116
|
+
const input = {
|
|
117
|
+
from: formatAddresses(data.from)[0],
|
|
118
|
+
to: formatAddresses(data.to),
|
|
119
|
+
subject: data.subject
|
|
120
|
+
};
|
|
121
|
+
const html = content(data.html);
|
|
122
|
+
const text = content(data.text);
|
|
123
|
+
if (html !== void 0) input.html = html;
|
|
124
|
+
if (text !== void 0) input.text = text;
|
|
125
|
+
const cc = formatAddresses(data.cc);
|
|
126
|
+
const bcc = formatAddresses(data.bcc);
|
|
127
|
+
const replyTo = formatAddresses(data.replyTo);
|
|
128
|
+
if (cc.length) input.cc = cc;
|
|
129
|
+
if (bcc.length) input.bcc = bcc;
|
|
130
|
+
if (replyTo.length) input.replyTo = replyTo;
|
|
131
|
+
if (Object.keys(headers).length) input.headers = headers;
|
|
132
|
+
if (tags.length) input.tags = tags;
|
|
133
|
+
if (scheduledAt !== void 0 && scheduledAt !== "") input.scheduledAt = scheduledAt;
|
|
134
|
+
return { input, idempotencyKey: idempotencyKey || void 0 };
|
|
135
|
+
}
|
|
136
|
+
function headerEntries(headers) {
|
|
137
|
+
const entries = [];
|
|
138
|
+
if (Array.isArray(headers)) {
|
|
139
|
+
for (const h of headers) if (h && typeof h.key === "string") entries.push([h.key, h.value]);
|
|
140
|
+
} else if (headers && typeof headers === "object") {
|
|
141
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
142
|
+
for (const v of Array.isArray(value) ? value : [value]) entries.push([key, v]);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const out = [];
|
|
146
|
+
for (const [key, raw] of entries) {
|
|
147
|
+
const value = raw && typeof raw === "object" && "value" in raw ? raw.value : raw;
|
|
148
|
+
if (value === void 0 || value === null || value === false || value === "") continue;
|
|
149
|
+
out.push([key, String(value)]);
|
|
150
|
+
}
|
|
151
|
+
return out;
|
|
152
|
+
}
|
|
153
|
+
function canonicalHeaderName(key) {
|
|
154
|
+
if (key === "in-reply-to") return "In-Reply-To";
|
|
155
|
+
return key.replace(/(^|-)([a-z])/g, (_, dash, c) => dash + c.toUpperCase());
|
|
156
|
+
}
|
|
157
|
+
function priorityHeaders(priority) {
|
|
158
|
+
switch (String(priority ?? "").toLowerCase()) {
|
|
159
|
+
case "high":
|
|
160
|
+
return { "X-Priority": "1 (Highest)", "X-MSMail-Priority": "High", Importance: "High" };
|
|
161
|
+
case "low":
|
|
162
|
+
return { "X-Priority": "5 (Lowest)", "X-MSMail-Priority": "Low", Importance: "Low" };
|
|
163
|
+
default:
|
|
164
|
+
return {};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function toTags(tags) {
|
|
168
|
+
if (!tags) return [];
|
|
169
|
+
if (Array.isArray(tags)) return tags.map((t) => ({ name: String(t.name), value: String(t.value) }));
|
|
170
|
+
if (typeof tags === "object") return Object.entries(tags).map(([name, value]) => ({ name, value: String(value) }));
|
|
171
|
+
throw invalid("`tags` must be an array of { name, value } or an object of name \u2192 value.");
|
|
172
|
+
}
|
|
173
|
+
function parseTagsHeader(value) {
|
|
174
|
+
return value.split(/[,;]/).map((part) => part.trim()).filter(Boolean).map((part) => {
|
|
175
|
+
const eq = part.indexOf("=");
|
|
176
|
+
if (eq <= 0) throw invalid(`Invalid X-Agdir-Tags entry "${part}": expected name=value.`);
|
|
177
|
+
return { name: part.slice(0, eq).trim(), value: part.slice(eq + 1).trim() };
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
function content(value) {
|
|
181
|
+
if (value === void 0 || value === null || value === false) return void 0;
|
|
182
|
+
if (typeof value === "string") return value;
|
|
183
|
+
if (Buffer.isBuffer(value)) return value.toString();
|
|
184
|
+
if (typeof value === "object" && "content" in value) return content(value.content);
|
|
185
|
+
return String(value);
|
|
186
|
+
}
|
|
187
|
+
function formatAddresses(value) {
|
|
188
|
+
if (!value) return [];
|
|
189
|
+
const out = [];
|
|
190
|
+
for (const entry of Array.isArray(value) ? value : [value]) {
|
|
191
|
+
if (!entry) continue;
|
|
192
|
+
if (typeof entry === "string") {
|
|
193
|
+
out.push(entry);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
const parsed = entry;
|
|
197
|
+
if (parsed.group) {
|
|
198
|
+
out.push(...formatAddresses(parsed.group));
|
|
199
|
+
} else if (parsed.address) {
|
|
200
|
+
out.push(parsed.name ? `${quoteName(parsed.name)} <${parsed.address}>` : parsed.address);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return out;
|
|
204
|
+
}
|
|
205
|
+
function quoteName(name) {
|
|
206
|
+
return /^[\w !#$%&'*+\-/=?^`{|}~]*$/.test(name) ? name : `"${name.replace(/["\\]/g, (c) => `\\${c}`)}"`;
|
|
207
|
+
}
|
|
208
|
+
function toNodemailerError(err) {
|
|
209
|
+
if (err instanceof import_email.AgdirEmailError) {
|
|
210
|
+
if (err.status) Object.assign(err, { responseCode: err.status });
|
|
211
|
+
return err;
|
|
212
|
+
}
|
|
213
|
+
return err instanceof Error ? err : new Error(String(err));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// src/index.ts
|
|
217
|
+
var import_email2 = require("@agdirlabs/email");
|
|
218
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
219
|
+
0 && (module.exports = {
|
|
220
|
+
AgdirEmail,
|
|
221
|
+
AgdirEmailError,
|
|
222
|
+
AgdirTransport,
|
|
223
|
+
IDEMPOTENCY_KEY_HEADER,
|
|
224
|
+
SCHEDULED_AT_HEADER,
|
|
225
|
+
TAGS_HEADER,
|
|
226
|
+
VERSION,
|
|
227
|
+
agdirTransport
|
|
228
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { SendEmailInput, Tag, AgdirEmailOptions } from '@agdirlabs/email';
|
|
2
|
+
export { AgdirEmail, AgdirEmailError } from '@agdirlabs/email';
|
|
3
|
+
|
|
4
|
+
/** The part of `AgdirEmail` the transport uses (handy for tests and wrappers). */
|
|
5
|
+
interface AgdirEmailSender {
|
|
6
|
+
emails: {
|
|
7
|
+
send(input: SendEmailInput, options?: {
|
|
8
|
+
idempotencyKey?: string;
|
|
9
|
+
}): Promise<{
|
|
10
|
+
id: string;
|
|
11
|
+
}>;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
type AgdirTransportOptions = (AgdirEmailOptions & {
|
|
15
|
+
client?: undefined;
|
|
16
|
+
}) | {
|
|
17
|
+
/** An existing client; `apiKey`, `baseUrl` etc. are then ignored. */
|
|
18
|
+
client: AgdirEmailSender;
|
|
19
|
+
};
|
|
20
|
+
/** Extra message fields understood by the transport (alongside the standard Nodemailer ones). */
|
|
21
|
+
interface AgdirMailOptions {
|
|
22
|
+
/** Tags echoed back in webhook events: `[{ name, value }]` or `{ name: value }`. ASCII letters, digits, `_`, `-`. */
|
|
23
|
+
tags?: Tag[] | Record<string, string>;
|
|
24
|
+
/** Deliver later: a `Date` or an ISO 8601 string with offset. */
|
|
25
|
+
scheduledAt?: Date | string;
|
|
26
|
+
/** Makes the send safe to repeat: the same key and message return the original email id. */
|
|
27
|
+
idempotencyKey?: string;
|
|
28
|
+
}
|
|
29
|
+
/** Passed to the `sendMail` callback / resolved by its promise. */
|
|
30
|
+
interface AgdirSentMessageInfo {
|
|
31
|
+
/** The Agdir Email id; use it with `agdir.emails.get()` / `cancel()`. */
|
|
32
|
+
messageId: string;
|
|
33
|
+
envelope: {
|
|
34
|
+
from: string | false;
|
|
35
|
+
to: string[];
|
|
36
|
+
};
|
|
37
|
+
accepted: string[];
|
|
38
|
+
rejected: string[];
|
|
39
|
+
pending: string[];
|
|
40
|
+
response: string;
|
|
41
|
+
}
|
|
42
|
+
/** Control headers: read by the transport and never sent with the email. */
|
|
43
|
+
declare const TAGS_HEADER = "x-agdir-tags";
|
|
44
|
+
declare const SCHEDULED_AT_HEADER = "x-agdir-scheduled-at";
|
|
45
|
+
declare const IDEMPOTENCY_KEY_HEADER = "x-agdir-idempotency-key";
|
|
46
|
+
interface MailMessageLike {
|
|
47
|
+
data: Record<string, any>;
|
|
48
|
+
message: {
|
|
49
|
+
getEnvelope(): {
|
|
50
|
+
from: string | false;
|
|
51
|
+
to: string[];
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
normalize(callback: (err: Error | null | undefined, data?: Record<string, any>) => void): void;
|
|
55
|
+
}
|
|
56
|
+
/** A Nodemailer transport; pass it to `nodemailer.createTransport()`. */
|
|
57
|
+
declare class AgdirTransport {
|
|
58
|
+
readonly name = "AgdirEmail";
|
|
59
|
+
readonly version = "0.1.0";
|
|
60
|
+
readonly client: AgdirEmailSender;
|
|
61
|
+
constructor(options?: AgdirTransportOptions);
|
|
62
|
+
send(mail: MailMessageLike, callback: (err: Error | null, info?: AgdirSentMessageInfo) => void): void;
|
|
63
|
+
private sendAsync;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Creates a Nodemailer transport that sends through the Agdir Email API.
|
|
67
|
+
*
|
|
68
|
+
* ```ts
|
|
69
|
+
* const transporter = nodemailer.createTransport(agdirTransport({ apiKey: 're_…' }));
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
declare function agdirTransport(options?: AgdirTransportOptions): AgdirTransport;
|
|
73
|
+
|
|
74
|
+
declare const VERSION = "0.1.0";
|
|
75
|
+
|
|
76
|
+
export { type AgdirEmailSender, type AgdirMailOptions, type AgdirSentMessageInfo, AgdirTransport, type AgdirTransportOptions, IDEMPOTENCY_KEY_HEADER, SCHEDULED_AT_HEADER, TAGS_HEADER, VERSION, agdirTransport };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { SendEmailInput, Tag, AgdirEmailOptions } from '@agdirlabs/email';
|
|
2
|
+
export { AgdirEmail, AgdirEmailError } from '@agdirlabs/email';
|
|
3
|
+
|
|
4
|
+
/** The part of `AgdirEmail` the transport uses (handy for tests and wrappers). */
|
|
5
|
+
interface AgdirEmailSender {
|
|
6
|
+
emails: {
|
|
7
|
+
send(input: SendEmailInput, options?: {
|
|
8
|
+
idempotencyKey?: string;
|
|
9
|
+
}): Promise<{
|
|
10
|
+
id: string;
|
|
11
|
+
}>;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
type AgdirTransportOptions = (AgdirEmailOptions & {
|
|
15
|
+
client?: undefined;
|
|
16
|
+
}) | {
|
|
17
|
+
/** An existing client; `apiKey`, `baseUrl` etc. are then ignored. */
|
|
18
|
+
client: AgdirEmailSender;
|
|
19
|
+
};
|
|
20
|
+
/** Extra message fields understood by the transport (alongside the standard Nodemailer ones). */
|
|
21
|
+
interface AgdirMailOptions {
|
|
22
|
+
/** Tags echoed back in webhook events: `[{ name, value }]` or `{ name: value }`. ASCII letters, digits, `_`, `-`. */
|
|
23
|
+
tags?: Tag[] | Record<string, string>;
|
|
24
|
+
/** Deliver later: a `Date` or an ISO 8601 string with offset. */
|
|
25
|
+
scheduledAt?: Date | string;
|
|
26
|
+
/** Makes the send safe to repeat: the same key and message return the original email id. */
|
|
27
|
+
idempotencyKey?: string;
|
|
28
|
+
}
|
|
29
|
+
/** Passed to the `sendMail` callback / resolved by its promise. */
|
|
30
|
+
interface AgdirSentMessageInfo {
|
|
31
|
+
/** The Agdir Email id; use it with `agdir.emails.get()` / `cancel()`. */
|
|
32
|
+
messageId: string;
|
|
33
|
+
envelope: {
|
|
34
|
+
from: string | false;
|
|
35
|
+
to: string[];
|
|
36
|
+
};
|
|
37
|
+
accepted: string[];
|
|
38
|
+
rejected: string[];
|
|
39
|
+
pending: string[];
|
|
40
|
+
response: string;
|
|
41
|
+
}
|
|
42
|
+
/** Control headers: read by the transport and never sent with the email. */
|
|
43
|
+
declare const TAGS_HEADER = "x-agdir-tags";
|
|
44
|
+
declare const SCHEDULED_AT_HEADER = "x-agdir-scheduled-at";
|
|
45
|
+
declare const IDEMPOTENCY_KEY_HEADER = "x-agdir-idempotency-key";
|
|
46
|
+
interface MailMessageLike {
|
|
47
|
+
data: Record<string, any>;
|
|
48
|
+
message: {
|
|
49
|
+
getEnvelope(): {
|
|
50
|
+
from: string | false;
|
|
51
|
+
to: string[];
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
normalize(callback: (err: Error | null | undefined, data?: Record<string, any>) => void): void;
|
|
55
|
+
}
|
|
56
|
+
/** A Nodemailer transport; pass it to `nodemailer.createTransport()`. */
|
|
57
|
+
declare class AgdirTransport {
|
|
58
|
+
readonly name = "AgdirEmail";
|
|
59
|
+
readonly version = "0.1.0";
|
|
60
|
+
readonly client: AgdirEmailSender;
|
|
61
|
+
constructor(options?: AgdirTransportOptions);
|
|
62
|
+
send(mail: MailMessageLike, callback: (err: Error | null, info?: AgdirSentMessageInfo) => void): void;
|
|
63
|
+
private sendAsync;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Creates a Nodemailer transport that sends through the Agdir Email API.
|
|
67
|
+
*
|
|
68
|
+
* ```ts
|
|
69
|
+
* const transporter = nodemailer.createTransport(agdirTransport({ apiKey: 're_…' }));
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
declare function agdirTransport(options?: AgdirTransportOptions): AgdirTransport;
|
|
73
|
+
|
|
74
|
+
declare const VERSION = "0.1.0";
|
|
75
|
+
|
|
76
|
+
export { type AgdirEmailSender, type AgdirMailOptions, type AgdirSentMessageInfo, AgdirTransport, type AgdirTransportOptions, IDEMPOTENCY_KEY_HEADER, SCHEDULED_AT_HEADER, TAGS_HEADER, VERSION, agdirTransport };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// src/transport.ts
|
|
2
|
+
import { AgdirEmail, AgdirEmailError } from "@agdirlabs/email";
|
|
3
|
+
|
|
4
|
+
// src/version.ts
|
|
5
|
+
var VERSION = "0.1.0";
|
|
6
|
+
|
|
7
|
+
// src/transport.ts
|
|
8
|
+
var TAGS_HEADER = "x-agdir-tags";
|
|
9
|
+
var SCHEDULED_AT_HEADER = "x-agdir-scheduled-at";
|
|
10
|
+
var IDEMPOTENCY_KEY_HEADER = "x-agdir-idempotency-key";
|
|
11
|
+
var CONTROL_HEADERS = /* @__PURE__ */ new Set([TAGS_HEADER, SCHEDULED_AT_HEADER, IDEMPOTENCY_KEY_HEADER]);
|
|
12
|
+
var AgdirTransport = class {
|
|
13
|
+
name = "AgdirEmail";
|
|
14
|
+
version = VERSION;
|
|
15
|
+
client;
|
|
16
|
+
constructor(options = {}) {
|
|
17
|
+
this.client = options.client ?? new AgdirEmail(options);
|
|
18
|
+
}
|
|
19
|
+
send(mail, callback) {
|
|
20
|
+
this.sendAsync(mail).then(
|
|
21
|
+
(info) => callback(null, info),
|
|
22
|
+
(err) => callback(toNodemailerError(err))
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
async sendAsync(mail) {
|
|
26
|
+
rejectUnsupported(mail.data);
|
|
27
|
+
const envelope = mail.message.getEnvelope();
|
|
28
|
+
const data = await new Promise(
|
|
29
|
+
(resolve, reject) => mail.normalize((err, normalized) => err ? reject(err) : resolve(normalized))
|
|
30
|
+
);
|
|
31
|
+
const { input, idempotencyKey } = toSendInput(data);
|
|
32
|
+
const { id } = await this.client.emails.send(input, idempotencyKey ? { idempotencyKey } : {});
|
|
33
|
+
return {
|
|
34
|
+
messageId: id,
|
|
35
|
+
envelope,
|
|
36
|
+
accepted: [...envelope.to],
|
|
37
|
+
rejected: [],
|
|
38
|
+
pending: [],
|
|
39
|
+
response: `Accepted ${id}`
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
function agdirTransport(options = {}) {
|
|
44
|
+
return new AgdirTransport(options);
|
|
45
|
+
}
|
|
46
|
+
function unsupported(message) {
|
|
47
|
+
return new AgdirEmailError(message, "unsupported", 0);
|
|
48
|
+
}
|
|
49
|
+
function invalid(message) {
|
|
50
|
+
return new AgdirEmailError(message, "validation_error", 0);
|
|
51
|
+
}
|
|
52
|
+
function rejectUnsupported(data) {
|
|
53
|
+
if (Array.isArray(data.attachments) ? data.attachments.length > 0 : data.attachments) {
|
|
54
|
+
throw unsupported("Attachments are not supported by the Agdir Email API yet. Link to the file from the email body instead.");
|
|
55
|
+
}
|
|
56
|
+
if (data.raw) throw unsupported("Raw MIME messages (`raw`) are not supported by the Agdir Email API.");
|
|
57
|
+
if (Array.isArray(data.alternatives) ? data.alternatives.length > 0 : data.alternatives) {
|
|
58
|
+
throw unsupported("`alternatives` are not supported by the Agdir Email API; use `html` and `text`.");
|
|
59
|
+
}
|
|
60
|
+
if (data.icalEvent) throw unsupported("Calendar events (`icalEvent`) are not supported by the Agdir Email API yet.");
|
|
61
|
+
if (data.envelope) {
|
|
62
|
+
throw unsupported("A custom SMTP `envelope` is not supported: the email is sent to its to, cc and bcc recipients.");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function toSendInput(data) {
|
|
66
|
+
const headers = {};
|
|
67
|
+
const control = {};
|
|
68
|
+
for (const [key, value] of headerEntries(data.headers)) {
|
|
69
|
+
const lower = key.toLowerCase();
|
|
70
|
+
if (CONTROL_HEADERS.has(lower)) control[lower] = value;
|
|
71
|
+
else headers[key] = key in headers ? `${headers[key]}, ${value}` : value;
|
|
72
|
+
}
|
|
73
|
+
for (const [key, value] of Object.entries(data.normalizedHeaders ?? {})) {
|
|
74
|
+
if ((key === "in-reply-to" || key === "references" || key.startsWith("list-")) && typeof value === "string" && value) {
|
|
75
|
+
const existing = Object.keys(headers).find((k) => k.toLowerCase() === key);
|
|
76
|
+
headers[existing ?? canonicalHeaderName(key)] = value;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
Object.assign(headers, priorityHeaders(data.priority));
|
|
80
|
+
const tags = [...toTags(data.tags), ...control[TAGS_HEADER] ? parseTagsHeader(control[TAGS_HEADER]) : []];
|
|
81
|
+
const scheduledAt = data.scheduledAt ?? control[SCHEDULED_AT_HEADER];
|
|
82
|
+
const idempotencyKey = data.idempotencyKey ?? control[IDEMPOTENCY_KEY_HEADER];
|
|
83
|
+
const input = {
|
|
84
|
+
from: formatAddresses(data.from)[0],
|
|
85
|
+
to: formatAddresses(data.to),
|
|
86
|
+
subject: data.subject
|
|
87
|
+
};
|
|
88
|
+
const html = content(data.html);
|
|
89
|
+
const text = content(data.text);
|
|
90
|
+
if (html !== void 0) input.html = html;
|
|
91
|
+
if (text !== void 0) input.text = text;
|
|
92
|
+
const cc = formatAddresses(data.cc);
|
|
93
|
+
const bcc = formatAddresses(data.bcc);
|
|
94
|
+
const replyTo = formatAddresses(data.replyTo);
|
|
95
|
+
if (cc.length) input.cc = cc;
|
|
96
|
+
if (bcc.length) input.bcc = bcc;
|
|
97
|
+
if (replyTo.length) input.replyTo = replyTo;
|
|
98
|
+
if (Object.keys(headers).length) input.headers = headers;
|
|
99
|
+
if (tags.length) input.tags = tags;
|
|
100
|
+
if (scheduledAt !== void 0 && scheduledAt !== "") input.scheduledAt = scheduledAt;
|
|
101
|
+
return { input, idempotencyKey: idempotencyKey || void 0 };
|
|
102
|
+
}
|
|
103
|
+
function headerEntries(headers) {
|
|
104
|
+
const entries = [];
|
|
105
|
+
if (Array.isArray(headers)) {
|
|
106
|
+
for (const h of headers) if (h && typeof h.key === "string") entries.push([h.key, h.value]);
|
|
107
|
+
} else if (headers && typeof headers === "object") {
|
|
108
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
109
|
+
for (const v of Array.isArray(value) ? value : [value]) entries.push([key, v]);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const out = [];
|
|
113
|
+
for (const [key, raw] of entries) {
|
|
114
|
+
const value = raw && typeof raw === "object" && "value" in raw ? raw.value : raw;
|
|
115
|
+
if (value === void 0 || value === null || value === false || value === "") continue;
|
|
116
|
+
out.push([key, String(value)]);
|
|
117
|
+
}
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
function canonicalHeaderName(key) {
|
|
121
|
+
if (key === "in-reply-to") return "In-Reply-To";
|
|
122
|
+
return key.replace(/(^|-)([a-z])/g, (_, dash, c) => dash + c.toUpperCase());
|
|
123
|
+
}
|
|
124
|
+
function priorityHeaders(priority) {
|
|
125
|
+
switch (String(priority ?? "").toLowerCase()) {
|
|
126
|
+
case "high":
|
|
127
|
+
return { "X-Priority": "1 (Highest)", "X-MSMail-Priority": "High", Importance: "High" };
|
|
128
|
+
case "low":
|
|
129
|
+
return { "X-Priority": "5 (Lowest)", "X-MSMail-Priority": "Low", Importance: "Low" };
|
|
130
|
+
default:
|
|
131
|
+
return {};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function toTags(tags) {
|
|
135
|
+
if (!tags) return [];
|
|
136
|
+
if (Array.isArray(tags)) return tags.map((t) => ({ name: String(t.name), value: String(t.value) }));
|
|
137
|
+
if (typeof tags === "object") return Object.entries(tags).map(([name, value]) => ({ name, value: String(value) }));
|
|
138
|
+
throw invalid("`tags` must be an array of { name, value } or an object of name \u2192 value.");
|
|
139
|
+
}
|
|
140
|
+
function parseTagsHeader(value) {
|
|
141
|
+
return value.split(/[,;]/).map((part) => part.trim()).filter(Boolean).map((part) => {
|
|
142
|
+
const eq = part.indexOf("=");
|
|
143
|
+
if (eq <= 0) throw invalid(`Invalid X-Agdir-Tags entry "${part}": expected name=value.`);
|
|
144
|
+
return { name: part.slice(0, eq).trim(), value: part.slice(eq + 1).trim() };
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
function content(value) {
|
|
148
|
+
if (value === void 0 || value === null || value === false) return void 0;
|
|
149
|
+
if (typeof value === "string") return value;
|
|
150
|
+
if (Buffer.isBuffer(value)) return value.toString();
|
|
151
|
+
if (typeof value === "object" && "content" in value) return content(value.content);
|
|
152
|
+
return String(value);
|
|
153
|
+
}
|
|
154
|
+
function formatAddresses(value) {
|
|
155
|
+
if (!value) return [];
|
|
156
|
+
const out = [];
|
|
157
|
+
for (const entry of Array.isArray(value) ? value : [value]) {
|
|
158
|
+
if (!entry) continue;
|
|
159
|
+
if (typeof entry === "string") {
|
|
160
|
+
out.push(entry);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
const parsed = entry;
|
|
164
|
+
if (parsed.group) {
|
|
165
|
+
out.push(...formatAddresses(parsed.group));
|
|
166
|
+
} else if (parsed.address) {
|
|
167
|
+
out.push(parsed.name ? `${quoteName(parsed.name)} <${parsed.address}>` : parsed.address);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return out;
|
|
171
|
+
}
|
|
172
|
+
function quoteName(name) {
|
|
173
|
+
return /^[\w !#$%&'*+\-/=?^`{|}~]*$/.test(name) ? name : `"${name.replace(/["\\]/g, (c) => `\\${c}`)}"`;
|
|
174
|
+
}
|
|
175
|
+
function toNodemailerError(err) {
|
|
176
|
+
if (err instanceof AgdirEmailError) {
|
|
177
|
+
if (err.status) Object.assign(err, { responseCode: err.status });
|
|
178
|
+
return err;
|
|
179
|
+
}
|
|
180
|
+
return err instanceof Error ? err : new Error(String(err));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// src/index.ts
|
|
184
|
+
import { AgdirEmail as AgdirEmail2, AgdirEmailError as AgdirEmailError2 } from "@agdirlabs/email";
|
|
185
|
+
export {
|
|
186
|
+
AgdirEmail2 as AgdirEmail,
|
|
187
|
+
AgdirEmailError2 as AgdirEmailError,
|
|
188
|
+
AgdirTransport,
|
|
189
|
+
IDEMPOTENCY_KEY_HEADER,
|
|
190
|
+
SCHEDULED_AT_HEADER,
|
|
191
|
+
TAGS_HEADER,
|
|
192
|
+
VERSION,
|
|
193
|
+
agdirTransport
|
|
194
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,72 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agdirlabs/nodemailer-transport",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Nodemailer transport for Agdir Email — send with Nodemailer through the Agdir Email API.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Agdir AS",
|
|
7
|
+
"homepage": "https://agdir.email",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/AgdirAS/agdirlabs-sdks.git",
|
|
11
|
+
"directory": "packages/nodemailer-transport"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"email",
|
|
15
|
+
"smtp",
|
|
16
|
+
"transactional-email",
|
|
17
|
+
"email-api",
|
|
18
|
+
"nodemailer",
|
|
19
|
+
"nodemailer-transport",
|
|
20
|
+
"agdir"
|
|
21
|
+
],
|
|
22
|
+
"type": "module",
|
|
23
|
+
"main": "./dist/index.cjs",
|
|
24
|
+
"module": "./dist/index.js",
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"import": {
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"default": "./dist/index.js"
|
|
31
|
+
},
|
|
32
|
+
"require": {
|
|
33
|
+
"types": "./dist/index.d.cts",
|
|
34
|
+
"default": "./dist/index.cjs"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"files": [
|
|
39
|
+
"dist",
|
|
40
|
+
"CHANGELOG.md"
|
|
41
|
+
],
|
|
42
|
+
"sideEffects": false,
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=20.3"
|
|
45
|
+
},
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build": "tsup src/index.ts --format esm,cjs --dts --clean --target es2022",
|
|
51
|
+
"test": "vitest run",
|
|
52
|
+
"typecheck": "tsc --noEmit",
|
|
53
|
+
"prepublishOnly": "pnpm build"
|
|
54
|
+
},
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"@agdirlabs/email": "^0.1.1"
|
|
57
|
+
},
|
|
58
|
+
"peerDependencies": {
|
|
59
|
+
"nodemailer": ">=6"
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"@types/node": "^22.0.0",
|
|
63
|
+
"@types/nodemailer": "^8.0.2",
|
|
64
|
+
"nodemailer": "^10.3.0",
|
|
65
|
+
"tsup": "^8.5.0",
|
|
66
|
+
"typescript": "^5.9.0",
|
|
67
|
+
"vitest": "^4.0.0"
|
|
68
|
+
},
|
|
69
|
+
"bugs": {
|
|
70
|
+
"url": "https://github.com/AgdirAS/agdirlabs-sdks/issues"
|
|
71
|
+
}
|
|
72
|
+
}
|