@luca-emmert/worker-mailer 1.3.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 +21 -0
- package/README.md +386 -0
- package/dist/index.d.mts +246 -0
- package/dist/index.d.ts +246 -0
- package/dist/index.js +49 -0
- package/dist/index.mjs +49 -0
- package/package.json +40 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) zou-yu
|
|
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
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
# Worker Mailer
|
|
2
|
+
|
|
3
|
+
[English](./README.md) | [įŽäŊ䏿](./README_zh-CN.md)
|
|
4
|
+
|
|
5
|
+
[](https://badge.fury.io/js/worker-mailer)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
Worker Mailer is an SMTP client that runs on Cloudflare Workers. It leverages [Cloudflare TCP Sockets](https://developers.cloudflare.com/workers/runtime-apis/tcp-sockets/) and doesn't rely on any other dependencies.
|
|
9
|
+
|
|
10
|
+
## Features
|
|
11
|
+
|
|
12
|
+
- đ Completely built on the Cloudflare Workers runtime with no other dependencies
|
|
13
|
+
- đ Full TypeScript type support
|
|
14
|
+
- đ§ Supports sending plain text and HTML emails with attachments
|
|
15
|
+
- đ Supports multiple SMTP authentication methods: `plain`, `login`, `CRAM-MD5` and `XOAUTH2`
|
|
16
|
+
- ⥠Uses SMTP `PIPELINING` so a message with many recipients costs two round trips instead of one per recipient
|
|
17
|
+
- đ
DSN support
|
|
18
|
+
|
|
19
|
+
## Table of Contents
|
|
20
|
+
|
|
21
|
+
- [Installation](#installation)
|
|
22
|
+
- [Quick Start](#quick-start)
|
|
23
|
+
- [API Reference](#api-reference)
|
|
24
|
+
- [Limitations](#limitations)
|
|
25
|
+
- [Contributing](#contributing)
|
|
26
|
+
- [License](#license)
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
```shell
|
|
31
|
+
npm i worker-mailer
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Quick Start
|
|
35
|
+
|
|
36
|
+
1. Configure your `wrangler.toml`:
|
|
37
|
+
|
|
38
|
+
```toml
|
|
39
|
+
compatibility_flags = ["nodejs_compat"]
|
|
40
|
+
# or compatibility_flags = ["nodejs_compat_v2"]
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
2. Use in your code:
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
import { WorkerMailer } from 'worker-mailer'
|
|
47
|
+
|
|
48
|
+
// Connect to SMTP server
|
|
49
|
+
const mailer = await WorkerMailer.connect({
|
|
50
|
+
credentials: {
|
|
51
|
+
username: 'bob@acme.com',
|
|
52
|
+
password: 'password',
|
|
53
|
+
},
|
|
54
|
+
authType: 'plain',
|
|
55
|
+
host: 'smtp.acme.com',
|
|
56
|
+
port: 587,
|
|
57
|
+
secure: true,
|
|
58
|
+
// The name announced in EHLO. Receiving servers score it â set it to a domain
|
|
59
|
+
// you control, otherwise the connection announces itself as `[127.0.0.1]`.
|
|
60
|
+
ehloName: 'acme.com',
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
// Send email
|
|
64
|
+
await mailer.send({
|
|
65
|
+
from: { name: 'Bob', email: 'bob@acme.com' },
|
|
66
|
+
to: { name: 'Alice', email: 'alice@acme.com' },
|
|
67
|
+
subject: 'Hello from Worker Mailer',
|
|
68
|
+
text: 'This is a plain text message',
|
|
69
|
+
html: '<h1>Hello</h1><p>This is an HTML message</p>',
|
|
70
|
+
})
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
3. Using with modern JavaScript frameworks (Next.js, Nuxt, SvelteKit, etc.)
|
|
74
|
+
|
|
75
|
+
When working with frameworks that use Node.js as their development runtime, you'll need to handle the fact that Cloudflare Workers-specific APIs (like `cloudflare:sockets`) aren't available during local development.
|
|
76
|
+
|
|
77
|
+
The recommended approach is to use conditional dynamic imports. Here's an example for Nuxt.js:
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
export default defineEventHandler(async event => {
|
|
81
|
+
// Check if running in development environment
|
|
82
|
+
if (import.meta.dev) {
|
|
83
|
+
// Development: Use nodemailer (or any Node.js compatible email library)
|
|
84
|
+
const nodemailer = await import('nodemailer')
|
|
85
|
+
const transporter = nodemailer.default.createTransport()
|
|
86
|
+
return await transporter.sendMail()
|
|
87
|
+
} else {
|
|
88
|
+
// Production: Use worker-mailer in Cloudflare Workers environment
|
|
89
|
+
const { WorkerMailer } = await import('worker-mailer')
|
|
90
|
+
const mailer = await WorkerMailer.connect()
|
|
91
|
+
return await mailer.send()
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
This pattern ensures your application works seamlessly in both development and production environments.
|
|
97
|
+
|
|
98
|
+
## API Reference
|
|
99
|
+
|
|
100
|
+
### WorkerMailer.connect(options)
|
|
101
|
+
|
|
102
|
+
Creates a new SMTP connection.
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
type WorkerMailerOptions = {
|
|
106
|
+
host: string // SMTP server hostname
|
|
107
|
+
port: number // SMTP server port (usually 587 or 465)
|
|
108
|
+
secure?: boolean // Use TLS (default: false)
|
|
109
|
+
startTls?: boolean // Upgrade to TLS if SMTP server supports (default: true)
|
|
110
|
+
requireTls?: boolean // Fail instead of sending unencrypted (default: false)
|
|
111
|
+
ehloName?: string // Name announced in EHLO (default: '[127.0.0.1]')
|
|
112
|
+
credentials?: {
|
|
113
|
+
// SMTP authentication credentials
|
|
114
|
+
username: string
|
|
115
|
+
password?: string // Required for plain, login and cram-md5
|
|
116
|
+
accessToken?: string // Required for xoauth2
|
|
117
|
+
}
|
|
118
|
+
authType?:
|
|
119
|
+
| 'plain'
|
|
120
|
+
| 'login'
|
|
121
|
+
| 'cram-md5'
|
|
122
|
+
| 'xoauth2'
|
|
123
|
+
| Array<'plain' | 'login' | 'cram-md5' | 'xoauth2'>
|
|
124
|
+
logLevel?: LogLevel // Logging level (default: LogLevel.INFO)
|
|
125
|
+
socketTimeoutMs?: number // Socket timeout in milliseconds (default: 60000)
|
|
126
|
+
responseTimeoutMs?: number // Server response timeout in milliseconds (default: 30000)
|
|
127
|
+
pipelining?: boolean // Batch MAIL/RCPT when the server supports it (default: true)
|
|
128
|
+
chunking?: boolean // Transfer with BDAT when the server supports it (default: false)
|
|
129
|
+
allowPartialRecipients?: boolean // Deliver even if some recipients are rejected (default: false)
|
|
130
|
+
dsn?: {
|
|
131
|
+
RET?: {
|
|
132
|
+
HEADERS?: boolean
|
|
133
|
+
FULL?: boolean
|
|
134
|
+
}
|
|
135
|
+
NOTIFY?: {
|
|
136
|
+
DELAY?: boolean
|
|
137
|
+
FAILURE?: boolean
|
|
138
|
+
SUCCESS?: boolean
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
#### `ehloName`
|
|
145
|
+
|
|
146
|
+
Every SMTP session opens by announcing a name. Receiving servers feed that name
|
|
147
|
+
into their spam scoring, and some reject a name that is not a fully qualified
|
|
148
|
+
domain. Set it to a domain you control:
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
await WorkerMailer.connect({ host, port, ehloName: 'mail.acme.com' })
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
A bare IP address is wrapped in the address literal syntax RFC 5321 requires, so
|
|
155
|
+
`'203.0.113.7'` is announced as `[203.0.113.7]`.
|
|
156
|
+
|
|
157
|
+
#### `requireTls`
|
|
158
|
+
|
|
159
|
+
By default the client upgrades to TLS when the server offers `STARTTLS` and
|
|
160
|
+
otherwise authenticates over a plaintext connection. Set `requireTls: true` to
|
|
161
|
+
fail the connection instead.
|
|
162
|
+
|
|
163
|
+
#### `pipelining` and `chunking`
|
|
164
|
+
|
|
165
|
+
`pipelining` sends `MAIL FROM` and every `RCPT TO` as a single batch when the
|
|
166
|
+
server advertises `PIPELINING` (RFC 2920), which is what virtually every server
|
|
167
|
+
does. A message to 20 recipients then costs two round trips instead of 22. Turn
|
|
168
|
+
it off only to debug against a server that mishandles it.
|
|
169
|
+
|
|
170
|
+
`chunking` transfers the message with `BDAT` (RFC 3030) instead of `DATA` when
|
|
171
|
+
the server advertises `CHUNKING`. Because the message is length delimited it
|
|
172
|
+
needs no dot-stuffing pass, which is worthwhile for large attachments. It is off
|
|
173
|
+
by default because far less mail traffic goes through `BDAT` than through
|
|
174
|
+
`DATA`.
|
|
175
|
+
|
|
176
|
+
### mailer.send(options)
|
|
177
|
+
|
|
178
|
+
Sends an email.
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
type EmailOptions = {
|
|
182
|
+
from:
|
|
183
|
+
| string
|
|
184
|
+
| {
|
|
185
|
+
// Sender's email
|
|
186
|
+
name?: string
|
|
187
|
+
email: string
|
|
188
|
+
}
|
|
189
|
+
to:
|
|
190
|
+
| string
|
|
191
|
+
| string[]
|
|
192
|
+
| {
|
|
193
|
+
// Recipients (TO)
|
|
194
|
+
name?: string
|
|
195
|
+
email: string
|
|
196
|
+
}
|
|
197
|
+
| Array<{ name?: string; email: string }>
|
|
198
|
+
reply?:
|
|
199
|
+
| string
|
|
200
|
+
| {
|
|
201
|
+
// Reply-To address
|
|
202
|
+
name?: string
|
|
203
|
+
email: string
|
|
204
|
+
}
|
|
205
|
+
cc?:
|
|
206
|
+
| string
|
|
207
|
+
| string[]
|
|
208
|
+
| {
|
|
209
|
+
// Carbon Copy recipients
|
|
210
|
+
name?: string
|
|
211
|
+
email: string
|
|
212
|
+
}
|
|
213
|
+
| Array<{ name?: string; email: string }>
|
|
214
|
+
bcc?:
|
|
215
|
+
| string
|
|
216
|
+
| string[]
|
|
217
|
+
| {
|
|
218
|
+
// Blind Carbon Copy recipients
|
|
219
|
+
name?: string
|
|
220
|
+
email: string
|
|
221
|
+
}
|
|
222
|
+
| Array<{ name?: string; email: string }>
|
|
223
|
+
subject: string // Email subject
|
|
224
|
+
text?: string // Plain text content
|
|
225
|
+
html?: string // HTML content
|
|
226
|
+
headers?: Record<string, string> // Custom email headers
|
|
227
|
+
attachments?: {
|
|
228
|
+
filename: string
|
|
229
|
+
// Base64 string, or raw bytes which are base64-encoded for you
|
|
230
|
+
content: string | ArrayBuffer | Uint8Array
|
|
231
|
+
mimeType?: string // Inferred from the filename if not set
|
|
232
|
+
}[]
|
|
233
|
+
dsnOverride?: { // overrides dsn defined in WorkerMailer, if not set, it will take the WorkerMailer-Option.
|
|
234
|
+
envelopeId?: string | undefined
|
|
235
|
+
RET?: {
|
|
236
|
+
HEADERS?: boolean
|
|
237
|
+
FULL?: boolean
|
|
238
|
+
}
|
|
239
|
+
NOTIFY?: {
|
|
240
|
+
DELAY?: boolean
|
|
241
|
+
FAILURE?: boolean
|
|
242
|
+
SUCCESS?: boolean
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
#### Return value
|
|
249
|
+
|
|
250
|
+
`send()` resolves with the outcome of the transaction:
|
|
251
|
+
|
|
252
|
+
```typescript
|
|
253
|
+
type SendResult = {
|
|
254
|
+
accepted: User[] // Recipients the server accepted
|
|
255
|
+
rejected: { user: User; response: string }[] // Recipients it rejected, with the response
|
|
256
|
+
response: string // The server's final response to the message
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
By default a single rejected recipient fails the whole message and `send()`
|
|
261
|
+
rejects, so `rejected` is only ever populated when `allowPartialRecipients` is
|
|
262
|
+
set:
|
|
263
|
+
|
|
264
|
+
```typescript
|
|
265
|
+
const mailer = await WorkerMailer.connect({
|
|
266
|
+
host,
|
|
267
|
+
port,
|
|
268
|
+
allowPartialRecipients: true,
|
|
269
|
+
})
|
|
270
|
+
const { accepted, rejected } = await mailer.send({ from, to, subject, text })
|
|
271
|
+
if (rejected.length) {
|
|
272
|
+
console.warn(
|
|
273
|
+
'Not delivered to',
|
|
274
|
+
rejected.map(r => r.user.email),
|
|
275
|
+
)
|
|
276
|
+
}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
#### Bcc
|
|
280
|
+
|
|
281
|
+
`bcc` recipients are sent in the SMTP envelope only and never written into the
|
|
282
|
+
message, so the other recipients cannot see them. If you deliberately want a
|
|
283
|
+
`Bcc` header in the message, set it yourself through `headers`.
|
|
284
|
+
|
|
285
|
+
### Static Method: WorkerMailer.send()
|
|
286
|
+
|
|
287
|
+
Send a one-off email without maintaining the connection.
|
|
288
|
+
|
|
289
|
+
```typescript
|
|
290
|
+
await WorkerMailer.send(
|
|
291
|
+
{
|
|
292
|
+
// WorkerMailerOptions
|
|
293
|
+
host: 'smtp.acme.com',
|
|
294
|
+
port: 587,
|
|
295
|
+
credentials: {
|
|
296
|
+
username: 'user',
|
|
297
|
+
password: 'pass',
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
// EmailOptions
|
|
302
|
+
from: 'sender@acme.com',
|
|
303
|
+
to: 'recipient@acme.com',
|
|
304
|
+
subject: 'Test',
|
|
305
|
+
text: 'Hello',
|
|
306
|
+
attachments: [
|
|
307
|
+
{
|
|
308
|
+
filename: 'test.txt',
|
|
309
|
+
content: 'SGVsbG8gV29ybGQ=', // base64-encoded string for "Hello World"
|
|
310
|
+
type: 'text/plain',
|
|
311
|
+
},
|
|
312
|
+
],
|
|
313
|
+
},
|
|
314
|
+
)
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
## Limitations
|
|
318
|
+
|
|
319
|
+
- **Port Restrictions:** Cloudflare Workers cannot make outbound connections on port 25. You won't be able to send emails via port 25, but common ports like 587 and 465 are supported.
|
|
320
|
+
- **Connection Limits:** Each Worker instance has a limit on the number of concurrent TCP connections. Make sure to properly close connections when done.
|
|
321
|
+
|
|
322
|
+
## Contributing
|
|
323
|
+
|
|
324
|
+
### Development Workflow
|
|
325
|
+
|
|
326
|
+
> For major changes, please open an issue first to discuss what you would like to change.
|
|
327
|
+
|
|
328
|
+
1. Fork and clone the repository
|
|
329
|
+
2. Install dependencies:
|
|
330
|
+
```bash
|
|
331
|
+
pnpm install
|
|
332
|
+
```
|
|
333
|
+
3. Create a new branch for your feature from `develop`:
|
|
334
|
+
```bash
|
|
335
|
+
git checkout -b feat/your-feature-name
|
|
336
|
+
```
|
|
337
|
+
4. Make your changes and make sure all tests pass
|
|
338
|
+
5. Update README.md & changelog `pnpm changeset` if needed
|
|
339
|
+
6. Push your changes to your fork and create a pull request from your branch to `develop`
|
|
340
|
+
|
|
341
|
+
### Testing
|
|
342
|
+
|
|
343
|
+
1. Unit Tests:
|
|
344
|
+
```bash
|
|
345
|
+
npm test
|
|
346
|
+
```
|
|
347
|
+
2. Integration Tests:
|
|
348
|
+
```bash
|
|
349
|
+
pnpm dlx wrangler dev ./test/worker.ts
|
|
350
|
+
```
|
|
351
|
+
Then, send a POST request to `http://127.0.0.1:8787` with the following JSON body:
|
|
352
|
+
```json
|
|
353
|
+
{
|
|
354
|
+
"config": {
|
|
355
|
+
"credentials": {
|
|
356
|
+
"username": "xxx@xx.com",
|
|
357
|
+
"password": "xxxx"
|
|
358
|
+
},
|
|
359
|
+
"authType": "plain",
|
|
360
|
+
"host": "smtp.acme.com",
|
|
361
|
+
"port": 587,
|
|
362
|
+
"secure": false,
|
|
363
|
+
"startTls": true
|
|
364
|
+
},
|
|
365
|
+
"email": {
|
|
366
|
+
"from": "xxx@xx.com",
|
|
367
|
+
"to": "yyy@yy.com",
|
|
368
|
+
"subject": "Test Email",
|
|
369
|
+
"text": "Hello World"
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
### Reporting Issues
|
|
375
|
+
|
|
376
|
+
When reporting issues, please include:
|
|
377
|
+
|
|
378
|
+
- Version of worker-mailer you're using
|
|
379
|
+
- A clear description of the problem
|
|
380
|
+
- Steps to reproduce the issue
|
|
381
|
+
- Expected vs actual behavior
|
|
382
|
+
- Any relevant code snippets or error messages
|
|
383
|
+
|
|
384
|
+
## License
|
|
385
|
+
|
|
386
|
+
This project is licensed under the MIT License.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
declare function encodeHeader(text: string): string;
|
|
2
|
+
type User = {
|
|
3
|
+
name?: string;
|
|
4
|
+
email: string;
|
|
5
|
+
};
|
|
6
|
+
declare function formatAddress(user: User): string;
|
|
7
|
+
type AttachmentContent = string | ArrayBuffer | Uint8Array;
|
|
8
|
+
type Attachment = {
|
|
9
|
+
filename: string;
|
|
10
|
+
/** Base64 string, or raw bytes which are base64-encoded for you. */
|
|
11
|
+
content: AttachmentContent;
|
|
12
|
+
mimeType?: string;
|
|
13
|
+
};
|
|
14
|
+
type DsnOptions = {
|
|
15
|
+
envelopeId?: string;
|
|
16
|
+
RET?: {
|
|
17
|
+
HEADERS?: boolean;
|
|
18
|
+
FULL?: boolean;
|
|
19
|
+
};
|
|
20
|
+
NOTIFY?: {
|
|
21
|
+
DELAY?: boolean;
|
|
22
|
+
FAILURE?: boolean;
|
|
23
|
+
SUCCESS?: boolean;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
type EmailOptions = {
|
|
27
|
+
from: string | User;
|
|
28
|
+
to: string | string[] | User | User[];
|
|
29
|
+
reply?: string | User;
|
|
30
|
+
cc?: string | string[] | User | User[];
|
|
31
|
+
bcc?: string | string[] | User | User[];
|
|
32
|
+
subject: string;
|
|
33
|
+
text?: string;
|
|
34
|
+
html?: string;
|
|
35
|
+
headers?: Record<string, string>;
|
|
36
|
+
attachments?: Attachment[];
|
|
37
|
+
dsnOverride?: DsnOptions;
|
|
38
|
+
};
|
|
39
|
+
type SendResult = {
|
|
40
|
+
/** Recipients the server accepted. */
|
|
41
|
+
accepted: User[];
|
|
42
|
+
/** Recipients the server rejected, with the response that rejected them. */
|
|
43
|
+
rejected: {
|
|
44
|
+
user: User;
|
|
45
|
+
response: string;
|
|
46
|
+
}[];
|
|
47
|
+
/** The server's final response to the message. */
|
|
48
|
+
response: string;
|
|
49
|
+
};
|
|
50
|
+
type SerializeOptions = {
|
|
51
|
+
/** Set when the server advertised 8BITMIME. */
|
|
52
|
+
allow8bit?: boolean;
|
|
53
|
+
};
|
|
54
|
+
declare class Email {
|
|
55
|
+
readonly from: User;
|
|
56
|
+
readonly to: User[];
|
|
57
|
+
readonly reply?: User;
|
|
58
|
+
readonly cc?: User[];
|
|
59
|
+
readonly bcc?: User[];
|
|
60
|
+
readonly subject: string;
|
|
61
|
+
readonly text?: string;
|
|
62
|
+
readonly html?: string;
|
|
63
|
+
readonly dsnOverride?: DsnOptions;
|
|
64
|
+
readonly attachments?: Attachment[];
|
|
65
|
+
readonly headers: Record<string, string>;
|
|
66
|
+
/** Populated once the server has accepted (or rejected) the message. */
|
|
67
|
+
result?: SendResult;
|
|
68
|
+
setSent: () => void;
|
|
69
|
+
setSentError: (e: unknown) => void;
|
|
70
|
+
sent: Promise<void>;
|
|
71
|
+
constructor(options: EmailOptions);
|
|
72
|
+
private static toUsers;
|
|
73
|
+
/** Every envelope recipient, in the order they are sent to the server. */
|
|
74
|
+
get recipients(): User[];
|
|
75
|
+
/**
|
|
76
|
+
* Rough upper bound on the wire size, used to reject a message locally when
|
|
77
|
+
* the server advertised a smaller SIZE limit.
|
|
78
|
+
*/
|
|
79
|
+
estimateSize(): number;
|
|
80
|
+
/**
|
|
81
|
+
* Yields the message piece by piece. Every chunk starts on a line boundary so
|
|
82
|
+
* the caller can dot-stuff and write them one at a time instead of building
|
|
83
|
+
* the whole message â attachments included â as a single string.
|
|
84
|
+
*/
|
|
85
|
+
chunks(options?: SerializeOptions): Generator<string>;
|
|
86
|
+
/** The complete, dot-stuffed message including the SMTP terminator. */
|
|
87
|
+
getEmailData(options?: SerializeOptions): string;
|
|
88
|
+
private buildContentPart;
|
|
89
|
+
private renderTextPart;
|
|
90
|
+
private renderAttachment;
|
|
91
|
+
private messageHeaders;
|
|
92
|
+
private hasHeader;
|
|
93
|
+
private resolveHeader;
|
|
94
|
+
private resolveAddressHeader;
|
|
95
|
+
private resolveSubject;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
declare enum LogLevel {
|
|
99
|
+
DEBUG = 0,
|
|
100
|
+
INFO = 1,
|
|
101
|
+
WARN = 2,
|
|
102
|
+
ERROR = 3,
|
|
103
|
+
NONE = 4
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
type AuthType = 'plain' | 'login' | 'cram-md5' | 'xoauth2';
|
|
107
|
+
type Credentials = {
|
|
108
|
+
username: string;
|
|
109
|
+
password?: string;
|
|
110
|
+
/** OAuth 2.0 bearer token. Required for `xoauth2`, ignored otherwise. */
|
|
111
|
+
accessToken?: string;
|
|
112
|
+
};
|
|
113
|
+
type WorkerMailerOptions = {
|
|
114
|
+
host: string;
|
|
115
|
+
port: number;
|
|
116
|
+
secure?: boolean;
|
|
117
|
+
startTls?: boolean;
|
|
118
|
+
/** Refuse to authenticate or send over an unencrypted connection. */
|
|
119
|
+
requireTls?: boolean;
|
|
120
|
+
/**
|
|
121
|
+
* The name announced in EHLO. Should be a domain you control â receiving
|
|
122
|
+
* servers score it, and some reject a HELO name that is not a FQDN.
|
|
123
|
+
* A bare IP address is wrapped in the address literal syntax RFC 5321 wants.
|
|
124
|
+
*/
|
|
125
|
+
ehloName?: string;
|
|
126
|
+
credentials?: Credentials;
|
|
127
|
+
authType?: AuthType | AuthType[];
|
|
128
|
+
logLevel?: LogLevel;
|
|
129
|
+
dsn?: DsnOptions | undefined;
|
|
130
|
+
socketTimeoutMs?: number;
|
|
131
|
+
responseTimeoutMs?: number;
|
|
132
|
+
/**
|
|
133
|
+
* Send MAIL FROM and RCPT TO as one batch when the server advertises
|
|
134
|
+
* PIPELINING (RFC 2920). Defaults to true.
|
|
135
|
+
*/
|
|
136
|
+
pipelining?: boolean;
|
|
137
|
+
/**
|
|
138
|
+
* Transfer the message with BDAT when the server advertises CHUNKING
|
|
139
|
+
* (RFC 3030), which avoids the dot-stuffing pass over the whole message.
|
|
140
|
+
* Defaults to false, because far fewer servers see BDAT traffic than DATA.
|
|
141
|
+
*/
|
|
142
|
+
chunking?: boolean;
|
|
143
|
+
/**
|
|
144
|
+
* Deliver to the recipients the server accepted even if it rejected others.
|
|
145
|
+
* Defaults to false, so a rejected recipient still fails the whole message.
|
|
146
|
+
*/
|
|
147
|
+
allowPartialRecipients?: boolean;
|
|
148
|
+
};
|
|
149
|
+
declare class WorkerMailer {
|
|
150
|
+
private socket;
|
|
151
|
+
private readonly host;
|
|
152
|
+
private readonly port;
|
|
153
|
+
private readonly secure;
|
|
154
|
+
private readonly startTls;
|
|
155
|
+
private readonly requireTls;
|
|
156
|
+
private readonly ehloName;
|
|
157
|
+
private readonly authType;
|
|
158
|
+
private readonly credentials?;
|
|
159
|
+
private readonly socketTimeoutMs;
|
|
160
|
+
private readonly responseTimeoutMs;
|
|
161
|
+
private readonly pipelining;
|
|
162
|
+
private readonly chunking;
|
|
163
|
+
private readonly allowPartialRecipients;
|
|
164
|
+
private reader;
|
|
165
|
+
private writer;
|
|
166
|
+
private decoder;
|
|
167
|
+
/** Bytes received but not yet consumed as a complete response. */
|
|
168
|
+
private buffer;
|
|
169
|
+
private streamClosed;
|
|
170
|
+
private readonly logger;
|
|
171
|
+
private readonly dsn;
|
|
172
|
+
private active;
|
|
173
|
+
private tlsActive;
|
|
174
|
+
private emailSending;
|
|
175
|
+
private emailToBeSent;
|
|
176
|
+
/** SMTP server capabilities **/
|
|
177
|
+
private supportsDSN;
|
|
178
|
+
private allowAuth;
|
|
179
|
+
private authTypeSupported;
|
|
180
|
+
private supportsStartTls;
|
|
181
|
+
private supportsPipelining;
|
|
182
|
+
private supportsChunking;
|
|
183
|
+
private supports8BitMime;
|
|
184
|
+
private maxMessageSize;
|
|
185
|
+
private constructor();
|
|
186
|
+
/**
|
|
187
|
+
* RFC 5321 §4.1.3 wants either a fully qualified domain or a bracketed
|
|
188
|
+
* address literal. A bare `127.0.0.1` is neither, and strict servers say so.
|
|
189
|
+
*/
|
|
190
|
+
private static resolveEhloName;
|
|
191
|
+
static connect(options: WorkerMailerOptions): Promise<WorkerMailer>;
|
|
192
|
+
send(options: EmailOptions): Promise<SendResult>;
|
|
193
|
+
static send(options: WorkerMailerOptions, email: EmailOptions): Promise<SendResult>;
|
|
194
|
+
private readTimeout;
|
|
195
|
+
/**
|
|
196
|
+
* Reads exactly one SMTP response. Anything the server sent beyond it stays
|
|
197
|
+
* buffered â with pipelining several responses routinely arrive in a single
|
|
198
|
+
* TCP segment, and dropping the remainder desynchronizes the connection.
|
|
199
|
+
*/
|
|
200
|
+
private readResponse;
|
|
201
|
+
/** Removes one complete response from the buffer, or null if none is ready. */
|
|
202
|
+
private takeResponse;
|
|
203
|
+
private writeLine;
|
|
204
|
+
private write;
|
|
205
|
+
private writeBytes;
|
|
206
|
+
private initializeSmtpSession;
|
|
207
|
+
private start;
|
|
208
|
+
close(error?: Error): Promise<void>;
|
|
209
|
+
private waitForSocketConnected;
|
|
210
|
+
private greet;
|
|
211
|
+
private ehlo;
|
|
212
|
+
private helo;
|
|
213
|
+
private tls;
|
|
214
|
+
private parseCapabilities;
|
|
215
|
+
private defaultAuthOrder;
|
|
216
|
+
private selectAuthType;
|
|
217
|
+
private auth;
|
|
218
|
+
private requirePassword;
|
|
219
|
+
private authWithPlain;
|
|
220
|
+
private authWithLogin;
|
|
221
|
+
private authWithCramMD5;
|
|
222
|
+
private authWithXOAuth2;
|
|
223
|
+
/**
|
|
224
|
+
* Runs one mail transaction. MAIL FROM and every RCPT TO go out as a single
|
|
225
|
+
* batch when the server supports pipelining, turning N+2 round trips into 2.
|
|
226
|
+
*/
|
|
227
|
+
private transaction;
|
|
228
|
+
private mailFromCommand;
|
|
229
|
+
private rcptToCommand;
|
|
230
|
+
private checkSize;
|
|
231
|
+
private serializeOptions;
|
|
232
|
+
/** Ends a data phase we entered but do not want to complete. */
|
|
233
|
+
private abortData;
|
|
234
|
+
private sendWithData;
|
|
235
|
+
/**
|
|
236
|
+
* RFC 3030 BDAT. The message is length-delimited, so it needs no dot-stuffing
|
|
237
|
+
* and no terminator scan. Commands are pipelined; responses are tiny and read
|
|
238
|
+
* once everything is on the wire.
|
|
239
|
+
*/
|
|
240
|
+
private sendWithChunking;
|
|
241
|
+
private rset;
|
|
242
|
+
private notificationBuilder;
|
|
243
|
+
private retBuilder;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export { type Attachment, type AttachmentContent, type AuthType, type Credentials, type DsnOptions, Email, type EmailOptions, LogLevel, type SendResult, type SerializeOptions, type User, WorkerMailer, type WorkerMailerOptions, encodeHeader, formatAddress };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
declare function encodeHeader(text: string): string;
|
|
2
|
+
type User = {
|
|
3
|
+
name?: string;
|
|
4
|
+
email: string;
|
|
5
|
+
};
|
|
6
|
+
declare function formatAddress(user: User): string;
|
|
7
|
+
type AttachmentContent = string | ArrayBuffer | Uint8Array;
|
|
8
|
+
type Attachment = {
|
|
9
|
+
filename: string;
|
|
10
|
+
/** Base64 string, or raw bytes which are base64-encoded for you. */
|
|
11
|
+
content: AttachmentContent;
|
|
12
|
+
mimeType?: string;
|
|
13
|
+
};
|
|
14
|
+
type DsnOptions = {
|
|
15
|
+
envelopeId?: string;
|
|
16
|
+
RET?: {
|
|
17
|
+
HEADERS?: boolean;
|
|
18
|
+
FULL?: boolean;
|
|
19
|
+
};
|
|
20
|
+
NOTIFY?: {
|
|
21
|
+
DELAY?: boolean;
|
|
22
|
+
FAILURE?: boolean;
|
|
23
|
+
SUCCESS?: boolean;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
type EmailOptions = {
|
|
27
|
+
from: string | User;
|
|
28
|
+
to: string | string[] | User | User[];
|
|
29
|
+
reply?: string | User;
|
|
30
|
+
cc?: string | string[] | User | User[];
|
|
31
|
+
bcc?: string | string[] | User | User[];
|
|
32
|
+
subject: string;
|
|
33
|
+
text?: string;
|
|
34
|
+
html?: string;
|
|
35
|
+
headers?: Record<string, string>;
|
|
36
|
+
attachments?: Attachment[];
|
|
37
|
+
dsnOverride?: DsnOptions;
|
|
38
|
+
};
|
|
39
|
+
type SendResult = {
|
|
40
|
+
/** Recipients the server accepted. */
|
|
41
|
+
accepted: User[];
|
|
42
|
+
/** Recipients the server rejected, with the response that rejected them. */
|
|
43
|
+
rejected: {
|
|
44
|
+
user: User;
|
|
45
|
+
response: string;
|
|
46
|
+
}[];
|
|
47
|
+
/** The server's final response to the message. */
|
|
48
|
+
response: string;
|
|
49
|
+
};
|
|
50
|
+
type SerializeOptions = {
|
|
51
|
+
/** Set when the server advertised 8BITMIME. */
|
|
52
|
+
allow8bit?: boolean;
|
|
53
|
+
};
|
|
54
|
+
declare class Email {
|
|
55
|
+
readonly from: User;
|
|
56
|
+
readonly to: User[];
|
|
57
|
+
readonly reply?: User;
|
|
58
|
+
readonly cc?: User[];
|
|
59
|
+
readonly bcc?: User[];
|
|
60
|
+
readonly subject: string;
|
|
61
|
+
readonly text?: string;
|
|
62
|
+
readonly html?: string;
|
|
63
|
+
readonly dsnOverride?: DsnOptions;
|
|
64
|
+
readonly attachments?: Attachment[];
|
|
65
|
+
readonly headers: Record<string, string>;
|
|
66
|
+
/** Populated once the server has accepted (or rejected) the message. */
|
|
67
|
+
result?: SendResult;
|
|
68
|
+
setSent: () => void;
|
|
69
|
+
setSentError: (e: unknown) => void;
|
|
70
|
+
sent: Promise<void>;
|
|
71
|
+
constructor(options: EmailOptions);
|
|
72
|
+
private static toUsers;
|
|
73
|
+
/** Every envelope recipient, in the order they are sent to the server. */
|
|
74
|
+
get recipients(): User[];
|
|
75
|
+
/**
|
|
76
|
+
* Rough upper bound on the wire size, used to reject a message locally when
|
|
77
|
+
* the server advertised a smaller SIZE limit.
|
|
78
|
+
*/
|
|
79
|
+
estimateSize(): number;
|
|
80
|
+
/**
|
|
81
|
+
* Yields the message piece by piece. Every chunk starts on a line boundary so
|
|
82
|
+
* the caller can dot-stuff and write them one at a time instead of building
|
|
83
|
+
* the whole message â attachments included â as a single string.
|
|
84
|
+
*/
|
|
85
|
+
chunks(options?: SerializeOptions): Generator<string>;
|
|
86
|
+
/** The complete, dot-stuffed message including the SMTP terminator. */
|
|
87
|
+
getEmailData(options?: SerializeOptions): string;
|
|
88
|
+
private buildContentPart;
|
|
89
|
+
private renderTextPart;
|
|
90
|
+
private renderAttachment;
|
|
91
|
+
private messageHeaders;
|
|
92
|
+
private hasHeader;
|
|
93
|
+
private resolveHeader;
|
|
94
|
+
private resolveAddressHeader;
|
|
95
|
+
private resolveSubject;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
declare enum LogLevel {
|
|
99
|
+
DEBUG = 0,
|
|
100
|
+
INFO = 1,
|
|
101
|
+
WARN = 2,
|
|
102
|
+
ERROR = 3,
|
|
103
|
+
NONE = 4
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
type AuthType = 'plain' | 'login' | 'cram-md5' | 'xoauth2';
|
|
107
|
+
type Credentials = {
|
|
108
|
+
username: string;
|
|
109
|
+
password?: string;
|
|
110
|
+
/** OAuth 2.0 bearer token. Required for `xoauth2`, ignored otherwise. */
|
|
111
|
+
accessToken?: string;
|
|
112
|
+
};
|
|
113
|
+
type WorkerMailerOptions = {
|
|
114
|
+
host: string;
|
|
115
|
+
port: number;
|
|
116
|
+
secure?: boolean;
|
|
117
|
+
startTls?: boolean;
|
|
118
|
+
/** Refuse to authenticate or send over an unencrypted connection. */
|
|
119
|
+
requireTls?: boolean;
|
|
120
|
+
/**
|
|
121
|
+
* The name announced in EHLO. Should be a domain you control â receiving
|
|
122
|
+
* servers score it, and some reject a HELO name that is not a FQDN.
|
|
123
|
+
* A bare IP address is wrapped in the address literal syntax RFC 5321 wants.
|
|
124
|
+
*/
|
|
125
|
+
ehloName?: string;
|
|
126
|
+
credentials?: Credentials;
|
|
127
|
+
authType?: AuthType | AuthType[];
|
|
128
|
+
logLevel?: LogLevel;
|
|
129
|
+
dsn?: DsnOptions | undefined;
|
|
130
|
+
socketTimeoutMs?: number;
|
|
131
|
+
responseTimeoutMs?: number;
|
|
132
|
+
/**
|
|
133
|
+
* Send MAIL FROM and RCPT TO as one batch when the server advertises
|
|
134
|
+
* PIPELINING (RFC 2920). Defaults to true.
|
|
135
|
+
*/
|
|
136
|
+
pipelining?: boolean;
|
|
137
|
+
/**
|
|
138
|
+
* Transfer the message with BDAT when the server advertises CHUNKING
|
|
139
|
+
* (RFC 3030), which avoids the dot-stuffing pass over the whole message.
|
|
140
|
+
* Defaults to false, because far fewer servers see BDAT traffic than DATA.
|
|
141
|
+
*/
|
|
142
|
+
chunking?: boolean;
|
|
143
|
+
/**
|
|
144
|
+
* Deliver to the recipients the server accepted even if it rejected others.
|
|
145
|
+
* Defaults to false, so a rejected recipient still fails the whole message.
|
|
146
|
+
*/
|
|
147
|
+
allowPartialRecipients?: boolean;
|
|
148
|
+
};
|
|
149
|
+
declare class WorkerMailer {
|
|
150
|
+
private socket;
|
|
151
|
+
private readonly host;
|
|
152
|
+
private readonly port;
|
|
153
|
+
private readonly secure;
|
|
154
|
+
private readonly startTls;
|
|
155
|
+
private readonly requireTls;
|
|
156
|
+
private readonly ehloName;
|
|
157
|
+
private readonly authType;
|
|
158
|
+
private readonly credentials?;
|
|
159
|
+
private readonly socketTimeoutMs;
|
|
160
|
+
private readonly responseTimeoutMs;
|
|
161
|
+
private readonly pipelining;
|
|
162
|
+
private readonly chunking;
|
|
163
|
+
private readonly allowPartialRecipients;
|
|
164
|
+
private reader;
|
|
165
|
+
private writer;
|
|
166
|
+
private decoder;
|
|
167
|
+
/** Bytes received but not yet consumed as a complete response. */
|
|
168
|
+
private buffer;
|
|
169
|
+
private streamClosed;
|
|
170
|
+
private readonly logger;
|
|
171
|
+
private readonly dsn;
|
|
172
|
+
private active;
|
|
173
|
+
private tlsActive;
|
|
174
|
+
private emailSending;
|
|
175
|
+
private emailToBeSent;
|
|
176
|
+
/** SMTP server capabilities **/
|
|
177
|
+
private supportsDSN;
|
|
178
|
+
private allowAuth;
|
|
179
|
+
private authTypeSupported;
|
|
180
|
+
private supportsStartTls;
|
|
181
|
+
private supportsPipelining;
|
|
182
|
+
private supportsChunking;
|
|
183
|
+
private supports8BitMime;
|
|
184
|
+
private maxMessageSize;
|
|
185
|
+
private constructor();
|
|
186
|
+
/**
|
|
187
|
+
* RFC 5321 §4.1.3 wants either a fully qualified domain or a bracketed
|
|
188
|
+
* address literal. A bare `127.0.0.1` is neither, and strict servers say so.
|
|
189
|
+
*/
|
|
190
|
+
private static resolveEhloName;
|
|
191
|
+
static connect(options: WorkerMailerOptions): Promise<WorkerMailer>;
|
|
192
|
+
send(options: EmailOptions): Promise<SendResult>;
|
|
193
|
+
static send(options: WorkerMailerOptions, email: EmailOptions): Promise<SendResult>;
|
|
194
|
+
private readTimeout;
|
|
195
|
+
/**
|
|
196
|
+
* Reads exactly one SMTP response. Anything the server sent beyond it stays
|
|
197
|
+
* buffered â with pipelining several responses routinely arrive in a single
|
|
198
|
+
* TCP segment, and dropping the remainder desynchronizes the connection.
|
|
199
|
+
*/
|
|
200
|
+
private readResponse;
|
|
201
|
+
/** Removes one complete response from the buffer, or null if none is ready. */
|
|
202
|
+
private takeResponse;
|
|
203
|
+
private writeLine;
|
|
204
|
+
private write;
|
|
205
|
+
private writeBytes;
|
|
206
|
+
private initializeSmtpSession;
|
|
207
|
+
private start;
|
|
208
|
+
close(error?: Error): Promise<void>;
|
|
209
|
+
private waitForSocketConnected;
|
|
210
|
+
private greet;
|
|
211
|
+
private ehlo;
|
|
212
|
+
private helo;
|
|
213
|
+
private tls;
|
|
214
|
+
private parseCapabilities;
|
|
215
|
+
private defaultAuthOrder;
|
|
216
|
+
private selectAuthType;
|
|
217
|
+
private auth;
|
|
218
|
+
private requirePassword;
|
|
219
|
+
private authWithPlain;
|
|
220
|
+
private authWithLogin;
|
|
221
|
+
private authWithCramMD5;
|
|
222
|
+
private authWithXOAuth2;
|
|
223
|
+
/**
|
|
224
|
+
* Runs one mail transaction. MAIL FROM and every RCPT TO go out as a single
|
|
225
|
+
* batch when the server supports pipelining, turning N+2 round trips into 2.
|
|
226
|
+
*/
|
|
227
|
+
private transaction;
|
|
228
|
+
private mailFromCommand;
|
|
229
|
+
private rcptToCommand;
|
|
230
|
+
private checkSize;
|
|
231
|
+
private serializeOptions;
|
|
232
|
+
/** Ends a data phase we entered but do not want to complete. */
|
|
233
|
+
private abortData;
|
|
234
|
+
private sendWithData;
|
|
235
|
+
/**
|
|
236
|
+
* RFC 3030 BDAT. The message is length-delimited, so it needs no dot-stuffing
|
|
237
|
+
* and no terminator scan. Commands are pipelined; responses are tiny and read
|
|
238
|
+
* once everything is on the wire.
|
|
239
|
+
*/
|
|
240
|
+
private sendWithChunking;
|
|
241
|
+
private rset;
|
|
242
|
+
private notificationBuilder;
|
|
243
|
+
private retBuilder;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export { type Attachment, type AttachmentContent, type AuthType, type Credentials, type DsnOptions, Email, type EmailOptions, LogLevel, type SendResult, type SerializeOptions, type User, WorkerMailer, type WorkerMailerOptions, encodeHeader, formatAddress };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";var A=Object.defineProperty;var q=Object.getOwnPropertyDescriptor;var _=Object.getOwnPropertyNames;var Y=Object.prototype.hasOwnProperty;var G=(s,e)=>{for(var t in e)A(s,t,{get:e[t],enumerable:!0})},Q=(s,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let n of _(e))!Y.call(s,n)&&n!==t&&A(s,n,{get:()=>e[n],enumerable:!(r=q(e,n))||r.enumerable});return s};var X=s=>Q(A({},"__esModule",{value:!0}),s);var ae={};G(ae,{Email:()=>w,LogLevel:()=>U,WorkerMailer:()=>C,encodeHeader:()=>E,formatAddress:()=>B});module.exports=X(ae);var T=class{values=[];resolvers=[];enqueue(e){this.resolvers.length||this.addWrapper(),this.resolvers.shift()(e)}async dequeue(){return this.values.length||this.addWrapper(),this.values.shift()}get length(){return this.values.length}clear(){this.values=[],this.resolvers=[]}addWrapper(){this.values.push(new Promise(e=>{this.resolvers.push(e)}))}};async function x(s,e,t){let r;try{return await Promise.race([s,new Promise((n,i)=>{r=setTimeout(()=>i(t),e)})])}finally{clearTimeout(r)}}var V=new TextEncoder;function h(s){return V.encode(s)}var le=new TextDecoder("utf-8");function R(s){return s.replace(/\r\n|\r|\n/g,`\r
|
|
2
|
+
`)}var Z=["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],J=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];function D(s=new Date){let e=t=>String(t).padStart(2,"0");return`${Z[s.getUTCDay()]}, ${e(s.getUTCDate())} ${J[s.getUTCMonth()]} ${s.getUTCFullYear()} ${e(s.getUTCHours())}:${e(s.getUTCMinutes())}:${e(s.getUTCSeconds())} +0000`}function O(s){return s.replace(/\r\n[ \t]+/g," ").replace(/[\r\n]+/g," ")}function d(s,e,t=78){let r=O(e),n=`${s}: `;if(n.length+r.length<=t)return n+r;let i=[],a=n,o=!0;for(let l of r.split(" ")){if(l===""){a+=" ";continue}o?(a+=l,o=!1):a.length+1+l.length>t?(i.push(a),a=" "+l):a+=" "+l}return i.push(a),i.join(`\r
|
|
3
|
+
`)}var K=/[A-Za-z0-9!#$&+\-.^_`|~]/;function $(s,e){let t=O(e);if(!/[^\x20-\x7E]/.test(t))return`${s}="${t.replace(/([\\"])/g,"\\$1")}"`;let r="";for(let n of h(t)){let i=String.fromCharCode(n);r+=K.test(i)?i:`%${n.toString(16).toUpperCase().padStart(2,"0")}`}return`${s}*=UTF-8''${r}`}function p(s){let e="";for(let r=0;r<s.length;r+=32768)e+=String.fromCharCode(...s.subarray(r,r+32768));return btoa(e)}function M(s){let e=atob(s.replace(/\s+/g,"")),t=new Uint8Array(e.length);for(let r=0;r<e.length;r++)t[r]=e.charCodeAt(r);return t}function v(s,e=76){let t=s.replace(/\s+/g,"");if(t.length<=e)return t;let r=[];for(let n=0;n<t.length;n+=e)r.push(t.slice(n,n+e));return r.join(`\r
|
|
4
|
+
`)}function S(){let s=!0;return e=>{if(!e)return e;let t=e.replace(/\r\n\./g,`\r
|
|
5
|
+
..`);return s&&t.startsWith(".")&&(t=`.${t}`),s=t.endsWith(`\r
|
|
6
|
+
`),t}}function W(s){return s>126||s===61||s<32&&s!==9&&s!==10&&s!==13}function L(s){let e=0;for(let t of s)W(t)&&e++;return s.length+e*2}function P(s,e=76){let t=h(s),r=e-3,n=[],i="",a=0,o=()=>{let l=i.charCodeAt(i.length-1);l===32?i=i.slice(0,-1)+"=20":l===9&&(i=i.slice(0,-1)+"=09"),n.push(i,`=\r
|
|
7
|
+
`),i=""};for(;a<t.length;){let l=t[a],c;if(l===10){n.push(i,`\r
|
|
8
|
+
`),i="",a++;continue}else if(l===13){if(a+1<t.length&&t[a+1]===10){n.push(i,`\r
|
|
9
|
+
`),i="",a+=2;continue}c="=0D"}if(c===void 0){let m=l===32||l===9,f=a+1>=t.length||t[a+1]===10||t[a+1]===13;c=W(l)||m&&f?`=${l.toString(16).toUpperCase().padStart(2,"0")}`:String.fromCharCode(l)}i.length+c.length>r&&o(),i+=c,a++}return n.push(i),n.join("")}var I="=?UTF-8?Q?",H="?=",ee=75-I.length-H.length;function E(s){if(!/[^\x20-\x7E]/.test(s))return s;let e=[],t="";for(let r of s){let n="";for(let i of h(r))i>=33&&i<=126&&i!==63&&i!==61&&i!==95?n+=String.fromCharCode(i):i===32?n+="_":n+=`=${i.toString(16).toUpperCase().padStart(2,"0")}`;t.length+n.length>ee&&(e.push(t),t=""),t+=n}return t&&e.push(t),e.map(r=>`${I}${r}${H}`).join(" ")}function B(s){if(!s.name)return s.email;let e=E(s.name);return e===s.name?`"${s.name.replace(/([\\"])/g,"\\$1")}" <${s.email}>`:`${e} <${s.email}>`}function F(s,e){let t=R(s),r=/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/.test(t),n=/[ \t]\r\n|[ \t]$/.test(t),i=0;for(let l of t.split(`\r
|
|
10
|
+
`))l.length>i&&(i=l.length);if(!r&&!n){if(i<=990&&!/[^\x00-\x7F]/.test(t))return{encoding:"7bit",render:()=>t};if(e&&i<=240)return{encoding:"8bit",render:()=>t}}let a=h(t),o=Math.ceil(a.length/3)*4;return L(a)<=o?{encoding:"quoted-printable",render:()=>P(t)}:{encoding:"base64",render:()=>v(p(a))}}function te(s){return s instanceof Uint8Array?s.length:s.byteLength}var w=class s{from;to;reply;cc;bcc;subject;text;html;dsnOverride;attachments;headers;result;setSent;setSentError;sent=new Promise((e,t)=>{this.setSent=e,this.setSentError=t});constructor(e){if(!e.text&&!e.html)throw new Error("At least one of text or html must be provided");typeof e.from=="string"?this.from={email:e.from}:this.from=e.from,typeof e.reply=="string"?this.reply={email:e.reply}:this.reply=e.reply,this.to=s.toUsers(e.to),this.cc=s.toUsers(e.cc),this.bcc=s.toUsers(e.bcc),this.subject=e.subject,this.text=e.text,this.html=e.html,this.attachments=e.attachments,this.dsnOverride=e.dsnOverride,this.headers=e.headers||{}}static toUsers(e){if(e)return typeof e=="string"?[{email:e}]:Array.isArray(e)?e.map(t=>typeof t=="string"?{email:t}:t):[e]}get recipients(){return[...this.to,...this.cc||[],...this.bcc||[]]}estimateSize(){let e=2048;this.text&&(e+=Math.ceil(h(this.text).length*1.4)),this.html&&(e+=Math.ceil(h(this.html).length*1.4));for(let t of this.attachments||[])e+=typeof t.content=="string"?t.content.length:Math.ceil(te(t.content)/3)*4,e+=512;return e}*chunks(e={}){let t=!!e.allow8bit,r=this.messageHeaders(),n=this.buildContentPart(t);if(!this.attachments?.length){yield`${[...r,...n.headers].join(`\r
|
|
11
|
+
`)}\r
|
|
12
|
+
\r
|
|
13
|
+
`,yield*n.body();return}let i=N("mixed");yield`${[...r,d("Content-Type",`multipart/mixed; boundary="${i}"`)].join(`\r
|
|
14
|
+
`)}\r
|
|
15
|
+
\r
|
|
16
|
+
`,yield`--${i}\r
|
|
17
|
+
${n.headers.join(`\r
|
|
18
|
+
`)}\r
|
|
19
|
+
\r
|
|
20
|
+
`,yield*n.body();for(let a of this.attachments)yield*this.renderAttachment(i,a);yield`--${i}--\r
|
|
21
|
+
`}getEmailData(e={}){let t=S(),r="";for(let n of this.chunks(e))r+=t(n);return r.endsWith(`\r
|
|
22
|
+
`)||(r+=`\r
|
|
23
|
+
`),`${r}.\r
|
|
24
|
+
`}buildContentPart(e){let t=this.text,r=this.html;if(t!==void 0&&r!==void 0){let a=N("alternative"),o=this.renderTextPart.bind(this);return{headers:[d("Content-Type",`multipart/alternative; boundary="${a}"`)],*body(){yield*o(a,"text/plain",t,e),yield*o(a,"text/html",r,e),yield`--${a}--\r
|
|
25
|
+
`}}}let n=t!==void 0?"text/plain":"text/html",i=F(t??r,e);return{headers:[d("Content-Type",`${n}; charset="UTF-8"`),`Content-Transfer-Encoding: ${i.encoding}`],*body(){yield`${i.render()}\r
|
|
26
|
+
`}}}*renderTextPart(e,t,r,n){let i=F(r,n);yield`--${e}\r
|
|
27
|
+
`+d("Content-Type",`${t}; charset="UTF-8"`)+`\r
|
|
28
|
+
Content-Transfer-Encoding: ${i.encoding}\r
|
|
29
|
+
\r
|
|
30
|
+
`,yield`${i.render()}\r
|
|
31
|
+
`}*renderAttachment(e,t){let r=t.mimeType||ie(t.filename),n=[d("Content-Type",`${r}; ${$("name",t.filename)}`),d("Content-Description",E(t.filename)),d("Content-Disposition",`attachment; ${$("filename",t.filename)}`),"Content-Transfer-Encoding: base64"];if(yield`--${e}\r
|
|
32
|
+
${n.join(`\r
|
|
33
|
+
`)}\r
|
|
34
|
+
\r
|
|
35
|
+
`,typeof t.content=="string"){yield`${v(t.content)}\r
|
|
36
|
+
`;return}let i=t.content instanceof Uint8Array?t.content:new Uint8Array(t.content),a=57*96;for(let o=0;o<i.length;o+=a)yield`${v(p(i.subarray(o,o+a)))}\r
|
|
37
|
+
`}messageHeaders(){this.resolveHeader();let e=["MIME-Version: 1.0"];for(let[t,r]of Object.entries(this.headers))e.push(d(t,r));return e}hasHeader(e){let t=e.toLowerCase();return Object.keys(this.headers).some(r=>r.toLowerCase()===t)}resolveHeader(){this.resolveAddressHeader("From",[this.from]),this.resolveAddressHeader("To",this.to),this.resolveAddressHeader("Reply-To",this.reply?[this.reply]:void 0),this.resolveAddressHeader("Cc",this.cc),this.resolveSubject(),this.hasHeader("Date")||(this.headers.Date=D()),this.hasHeader("Message-ID")||(this.headers["Message-ID"]=`<${crypto.randomUUID()}@${this.from.email.split("@").pop()}>`)}resolveAddressHeader(e,t){!t?.length||this.hasHeader(e)||(this.headers[e]=t.map(B).join(", "))}resolveSubject(){this.hasHeader("Subject")||!this.subject||(this.headers.Subject=E(this.subject))}};function N(s){let e=new Uint8Array(16);crypto.getRandomValues(e);let t=Array.from(e).map(r=>r.toString(16).padStart(2,"0")).join("");return`${s}_${t}`}var re={txt:"text/plain",html:"text/html",csv:"text/csv",pdf:"application/pdf",png:"image/png",jpg:"image/jpeg",jpeg:"image/jpeg",gif:"image/gif",zip:"application/zip"};function ie(s){let e=s.split(".").pop()?.toLowerCase();return re[e||"txt"]||"application/octet-stream"}var j=require("cloudflare:sockets");var U=(i=>(i[i.DEBUG=0]="DEBUG",i[i.INFO=1]="INFO",i[i.WARN=2]="WARN",i[i.ERROR=3]="ERROR",i[i.NONE=4]="NONE",i))(U||{}),y=class{constructor(e=1,t){this.level=e;this.prefix=t}prefix;debug(e,...t){this.level<=0&&console.debug(this.prefix+e,...t)}info(e,...t){this.level<=1&&console.info(this.prefix+e,...t)}warn(e,...t){this.level<=2&&console.warn(this.prefix+e,...t)}error(e,...t){this.level<=3&&console.error(this.prefix+e,...t)}};var se=262144,ne=32768,C=class s{socket;host;port;secure;startTls;requireTls;ehloName;authType;credentials;socketTimeoutMs;responseTimeoutMs;pipelining;chunking;allowPartialRecipients;reader;writer;decoder=new TextDecoder("utf-8");buffer="";streamClosed=!1;logger;dsn;active=!1;tlsActive=!1;emailSending=null;emailToBeSent=new T;supportsDSN=!1;allowAuth=!1;authTypeSupported=[];supportsStartTls=!1;supportsPipelining=!1;supportsChunking=!1;supports8BitMime=!1;maxMessageSize=0;constructor(e){this.port=e.port,this.host=e.host,this.secure=!!e.secure,Array.isArray(e.authType)?this.authType=e.authType:typeof e.authType=="string"?this.authType=[e.authType]:this.authType=[],this.startTls=e.startTls===void 0?!0:e.startTls,this.requireTls=!!e.requireTls,this.credentials=e.credentials,this.dsn=e.dsn||{},this.pipelining=e.pipelining!==!1,this.chunking=!!e.chunking,this.allowPartialRecipients=!!e.allowPartialRecipients,this.socketTimeoutMs=e.socketTimeoutMs||6e4,this.responseTimeoutMs=e.responseTimeoutMs||3e4,this.tlsActive=this.secure,this.socket=(0,j.connect)({hostname:this.host,port:this.port},{secureTransport:this.secure?"on":this.startTls?"starttls":"off",allowHalfOpen:!1}),this.reader=this.socket.readable.getReader(),this.writer=this.socket.writable.getWriter(),this.logger=new y(e.logLevel,`[WorkerMailer:${this.host}:${this.port}]`),this.ehloName=s.resolveEhloName(e.ehloName),e.ehloName||this.logger.warn("No ehloName set, announcing [127.0.0.1]. Set ehloName to a domain you control \u2014 receiving servers use it for spam scoring.")}static resolveEhloName(e){let t=e?.trim();return t?/^\[.+\]$/.test(t)?t:/^\d{1,3}(\.\d{1,3}){3}$/.test(t)?`[${t}]`:t.includes(":")?`[IPv6:${t}]`:t:"[127.0.0.1]"}static async connect(e){let t=new s(e);return await t.initializeSmtpSession(),t.start().catch(console.error),t}async send(e){if(!this.active)throw new Error("WorkerMailer is not connected");let t=new w(e);return this.emailToBeSent.enqueue(t),await t.sent,t.result}static async send(e,t){let r=await s.connect(e);try{return await r.send(t)}finally{await r.close()}}async readTimeout(){return x(this.readResponse(),this.responseTimeoutMs,new Error("Timeout while waiting for smtp server response"))}async readResponse(){for(;;){let e=this.takeResponse();if(e!==null)return e;if(this.streamClosed)throw new Error("SMTP server closed the connection");let{value:t,done:r}=await this.reader.read();if(r){this.streamClosed=!0;continue}if(!t)continue;let n=this.decoder.decode(t,{stream:!0});this.logger.debug(`SMTP server response:
|
|
38
|
+
`+n),this.buffer+=n}}takeResponse(){let e=0;for(;;){let t=this.buffer.indexOf(`
|
|
39
|
+
`,e);if(t===-1)return null;if(!/^\d{3}-/.test(this.buffer.slice(e,t))){let r=this.buffer.slice(0,t+1);return this.buffer=this.buffer.slice(t+1),r}e=t+1}}async writeLine(e){await this.write(`${e}\r
|
|
40
|
+
`)}async write(e,t=!0){this.logger.debug(t?`Write to socket:
|
|
41
|
+
`+e:`Write to socket: ${e.length} characters`),await this.writer.write(h(e))}async writeBytes(e){this.logger.debug(`Write to socket: ${e.byteLength} bytes`),await this.writer.write(e)}async initializeSmtpSession(){if(await this.waitForSocketConnected(),await this.greet(),await this.ehlo(),this.startTls&&!this.secure&&this.supportsStartTls&&(await this.tls(),await this.ehlo()),this.requireTls&&!this.tlsActive)throw new Error("requireTls is set but the connection is not encrypted (server does not support STARTTLS)");await this.auth(),this.active=!0}async start(){for(;this.active;){this.emailSending=await this.emailToBeSent.dequeue();try{this.emailSending.result=await this.transaction(this.emailSending),this.emailSending.setSent()}catch(e){if(this.logger.error("Failed to send email: "+e.message),!this.active)return;this.emailSending.setSentError(e);try{await this.rset()}catch(t){await this.close(t)}}this.emailSending=null}}async close(e){for(this.active=!1,this.logger.info("WorkerMailer is closed",e?.message||""),this.emailSending?.setSentError?.(e||new Error("WorkerMailer is shutting down"));this.emailToBeSent.length;)(await this.emailToBeSent.dequeue()).setSentError(e||new Error("WorkerMailer is shutting down"));try{await this.writeLine("QUIT"),await this.readTimeout(),this.socket.close().catch(()=>this.logger.error("Failed to close socket"))}catch{}}async waitForSocketConnected(){this.logger.info("Connecting to SMTP server"),await x(this.socket.opened,this.socketTimeoutMs,new Error("Socket timeout!")),this.logger.info("SMTP server connected")}async greet(){let e=await this.readTimeout();if(!e.startsWith("220"))throw new Error("Failed to connect to SMTP server: "+e)}async ehlo(){await this.writeLine(`EHLO ${this.ehloName}`);let e=await this.readTimeout();if(e.startsWith("421"))throw new Error(`Failed to EHLO. ${e}`);if(!e.startsWith("2")){await this.helo();return}this.parseCapabilities(e)}async helo(){await this.writeLine(`HELO ${this.ehloName}`);let e=await this.readTimeout();if(!e.startsWith("2"))throw new Error(`Failed to HELO. ${e}`)}async tls(){await this.writeLine("STARTTLS");let e=await this.readTimeout();if(!e.startsWith("220"))throw new Error("Failed to start TLS: "+e);this.reader.releaseLock(),this.writer.releaseLock(),this.socket=this.socket.startTls(),this.reader=this.socket.readable.getReader(),this.writer=this.socket.writable.getWriter(),this.decoder=new TextDecoder("utf-8"),this.buffer="",this.tlsActive=!0}parseCapabilities(e){this.allowAuth=!1,this.authTypeSupported=[],this.supportsStartTls=!1,this.supportsDSN=!1,this.supportsPipelining=!1,this.supportsChunking=!1,this.supports8BitMime=!1,this.maxMessageSize=0;let t=new Set;for(let r of e.split(/\r?\n/)){let n=r.replace(/^\d{3}[- ]?/,"").trim();if(!n)continue;let[i,...a]=n.split(/[ =]+/);switch(i.toUpperCase()){case"AUTH":this.allowAuth=!0;for(let o of a)switch(o.toUpperCase()){case"PLAIN":t.add("plain");break;case"LOGIN":t.add("login");break;case"CRAM-MD5":t.add("cram-md5");break;case"XOAUTH2":t.add("xoauth2");break}break;case"STARTTLS":this.supportsStartTls=!0;break;case"DSN":this.supportsDSN=!0;break;case"PIPELINING":this.supportsPipelining=!0;break;case"CHUNKING":this.supportsChunking=!0;break;case"8BITMIME":this.supports8BitMime=!0;break;case"SIZE":this.maxMessageSize=Number(a[0])||0;break}}this.authTypeSupported=[...t]}defaultAuthOrder(){return this.tlsActive?["plain","login","cram-md5"]:["cram-md5","plain","login"]}selectAuthType(){return(this.authType.length?this.authType:this.defaultAuthOrder()).find(t=>this.authTypeSupported.includes(t))}async auth(){if(this.allowAuth){if(!this.credentials)throw new Error("smtp server requires authentication, but no credentials found");switch(this.tlsActive||this.logger.warn("Authenticating over an unencrypted connection, credentials are sent in the clear"),this.selectAuthType()){case"plain":return this.authWithPlain();case"login":return this.authWithLogin();case"cram-md5":return this.authWithCramMD5();case"xoauth2":return this.authWithXOAuth2();default:throw new Error("No supported auth method found.")}}}requirePassword(){let e=this.credentials?.password;if(e===void 0)throw new Error("credentials.password is required for this auth method");return e}async authWithPlain(){let e=this.requirePassword(),t=p(h(`\0${this.credentials.username}\0${e}`));await this.writeLine(`AUTH PLAIN ${t}`);let r=await this.readTimeout();if(!r.startsWith("2"))throw new Error(`Failed to plain authentication: ${r}`)}async authWithLogin(){let e=this.requirePassword();await this.writeLine("AUTH LOGIN");let t=await this.readTimeout();if(!t.startsWith("3"))throw new Error("Invalid login: "+t);await this.writeLine(p(h(this.credentials.username)));let r=await this.readTimeout();if(!r.startsWith("3"))throw new Error("Failed to login authentication: "+r);await this.writeLine(p(h(e)));let n=await this.readTimeout();if(!n.startsWith("2"))throw new Error("Failed to login authentication: "+n)}async authWithCramMD5(){let e=this.requirePassword();await this.writeLine("AUTH CRAM-MD5");let t=await this.readTimeout(),r=t.match(/^334\s+(.+)$/m)?.pop()?.trim();if(!r)throw new Error("Invalid CRAM-MD5 challenge: "+t);let n=await crypto.subtle.importKey("raw",h(e),{name:"HMAC",hash:"MD5"},!1,["sign"]),i=await crypto.subtle.sign("HMAC",n,M(r)),a=Array.from(new Uint8Array(i)).map(l=>l.toString(16).padStart(2,"0")).join("");await this.writeLine(p(h(`${this.credentials.username} ${a}`)));let o=await this.readTimeout();if(!o.startsWith("2"))throw new Error(`Failed to cram-md5 authentication: ${o}`)}async authWithXOAuth2(){let e=this.credentials?.accessToken;if(!e)throw new Error("credentials.accessToken is required for xoauth2");let t=p(h(`user=${this.credentials.username}auth=Bearer ${e}`));await this.writeLine(`AUTH XOAUTH2 ${t}`);let r=await this.readTimeout();if(!r.startsWith("2"))throw r.startsWith("3")?(await this.writeLine(""),new Error(`Failed to xoauth2 authentication: ${await this.readTimeout()}`)):new Error(`Failed to xoauth2 authentication: ${r}`)}async transaction(e){let t=e.recipients;if(!t.length)throw new Error("Email has no recipients");this.checkSize(e);let r=this.chunking&&this.supportsChunking,n=this.mailFromCommand(e),i=t.map(u=>this.rcptToCommand(e,u)),a=!r&&(this.allowPartialRecipients||t.length===1),o,l=[],c=null;if(this.pipelining&&this.supportsPipelining){let u=[n,...i];a&&u.push("DATA"),await this.write(u.map(g=>`${g}\r
|
|
42
|
+
`).join("")),o=await this.readTimeout();for(let g of i)l.push(await this.readTimeout());a&&(c=await this.readTimeout())}else if(await this.writeLine(n),o=await this.readTimeout(),o.startsWith("2"))for(let u of i)await this.writeLine(u),l.push(await this.readTimeout());if(!o.startsWith("2"))throw new Error(`Invalid ${n} ${o}`);let m=[],f=[];if(t.forEach((u,g)=>{let k=l[g];k?.startsWith("2")?m.push(u):f.push({user:u,response:(k||"").trim()})}),f.length&&(!m.length||!this.allowPartialRecipients)){c?.startsWith("3")&&await this.abortData();let{user:u,response:g}=f[0];throw new Error(`Invalid RCPT TO: <${u.email}> ${g}`)}f.length&&this.logger.warn(`Server rejected ${f.length} of ${t.length} recipients`);let z=r?await this.sendWithChunking(e):await this.sendWithData(e,c);return{accepted:m,rejected:f,response:z.trim()}}mailFromCommand(e){let t=`MAIL FROM: <${e.from.email}>`;if(this.supportsDSN){let r=this.retBuilder(e);r&&(t+=` ${r}`),e.dsnOverride?.envelopeId&&(t+=` ENVID=${e.dsnOverride.envelopeId}`)}return t}rcptToCommand(e,t){let r=`RCPT TO: <${t.email}>`;return this.supportsDSN&&(r+=this.notificationBuilder(e)),r}checkSize(e){if(!this.maxMessageSize)return;let t=e.estimateSize();if(t>this.maxMessageSize)throw new Error(`Message is roughly ${t} bytes but the server accepts at most ${this.maxMessageSize} bytes`)}serializeOptions(){return{allow8bit:this.supports8BitMime}}async abortData(){await this.write(`\r
|
|
43
|
+
.\r
|
|
44
|
+
`),await this.readTimeout()}async sendWithData(e,t){let r=t;if(r===null&&(await this.writeLine("DATA"),r=await this.readTimeout()),!r.startsWith("3"))throw new Error(`Failed to send DATA: ${r}`);let n=S(),i="",a=!0;for(let l of e.chunks(this.serializeOptions())){let c=n(l);a=c.endsWith(`\r
|
|
45
|
+
`),i+=c,i.length>=ne&&(await this.write(i,!1),i="")}i+=a?`.\r
|
|
46
|
+
`:`\r
|
|
47
|
+
.\r
|
|
48
|
+
`,await this.write(i,!1);let o=await this.readTimeout();if(!o.startsWith("2"))throw new Error("Failed send email body: "+o);return o}async sendWithChunking(e){let t="",r=0,n=async a=>{let o=h(t);t="",await this.write(`BDAT ${o.byteLength}${a?" LAST":""}\r
|
|
49
|
+
`),await this.writeBytes(o),r++};for(let a of e.chunks(this.serializeOptions()))t+=a,t.length>=se&&await n(!1);await n(!0);let i="";for(let a=0;a<r;a++)if(i=await this.readTimeout(),!i.startsWith("2"))throw new Error("Failed send email body: "+i);return i}async rset(){await this.writeLine("RSET");let e=await this.readTimeout();if(!e.startsWith("2"))throw new Error(`Failed to reset: ${e}`)}notificationBuilder(e){let t=e.dsnOverride?.NOTIFY,r=t?void 0:this.dsn?.NOTIFY,n=[];return(t?.SUCCESS||r?.SUCCESS)&&n.push("SUCCESS"),(t?.FAILURE||r?.FAILURE)&&n.push("FAILURE"),(t?.DELAY||r?.DELAY)&&n.push("DELAY"),n.length>0?` NOTIFY=${n.join(",")}`:" NOTIFY=NEVER"}retBuilder(e){let t=e.dsnOverride?.RET,r=t?void 0:this.dsn?.RET,n=[];return(t?.HEADERS||r?.HEADERS)&&n.push("HDRS"),(t?.FULL||r?.FULL)&&n.push("FULL"),n.length>0?`RET=${n.join(",")}`:""}};0&&(module.exports={Email,LogLevel,WorkerMailer,encodeHeader,formatAddress});
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
var y=class{values=[];resolvers=[];enqueue(e){this.resolvers.length||this.addWrapper(),this.resolvers.shift()(e)}async dequeue(){return this.values.length||this.addWrapper(),this.values.shift()}get length(){return this.values.length}clear(){this.values=[],this.resolvers=[]}addWrapper(){this.values.push(new Promise(e=>{this.resolvers.push(e)}))}};async function A(s,e,t){let r;try{return await Promise.race([s,new Promise((n,i)=>{r=setTimeout(()=>i(t),e)})])}finally{clearTimeout(r)}}var j=new TextEncoder;function h(s){return j.encode(s)}var ee=new TextDecoder("utf-8");function C(s){return s.replace(/\r\n|\r|\n/g,`\r
|
|
2
|
+
`)}var z=["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],q=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];function k(s=new Date){let e=t=>String(t).padStart(2,"0");return`${z[s.getUTCDay()]}, ${e(s.getUTCDate())} ${q[s.getUTCMonth()]} ${s.getUTCFullYear()} ${e(s.getUTCHours())}:${e(s.getUTCMinutes())}:${e(s.getUTCSeconds())} +0000`}function R(s){return s.replace(/\r\n[ \t]+/g," ").replace(/[\r\n]+/g," ")}function d(s,e,t=78){let r=R(e),n=`${s}: `;if(n.length+r.length<=t)return n+r;let i=[],a=n,o=!0;for(let l of r.split(" ")){if(l===""){a+=" ";continue}o?(a+=l,o=!1):a.length+1+l.length>t?(i.push(a),a=" "+l):a+=" "+l}return i.push(a),i.join(`\r
|
|
3
|
+
`)}var _=/[A-Za-z0-9!#$&+\-.^_`|~]/;function x(s,e){let t=R(e);if(!/[^\x20-\x7E]/.test(t))return`${s}="${t.replace(/([\\"])/g,"\\$1")}"`;let r="";for(let n of h(t)){let i=String.fromCharCode(n);r+=_.test(i)?i:`%${n.toString(16).toUpperCase().padStart(2,"0")}`}return`${s}*=UTF-8''${r}`}function p(s){let e="";for(let r=0;r<s.length;r+=32768)e+=String.fromCharCode(...s.subarray(r,r+32768));return btoa(e)}function D(s){let e=atob(s.replace(/\s+/g,"")),t=new Uint8Array(e.length);for(let r=0;r<e.length;r++)t[r]=e.charCodeAt(r);return t}function v(s,e=76){let t=s.replace(/\s+/g,"");if(t.length<=e)return t;let r=[];for(let n=0;n<t.length;n+=e)r.push(t.slice(n,n+e));return r.join(`\r
|
|
4
|
+
`)}function S(){let s=!0;return e=>{if(!e)return e;let t=e.replace(/\r\n\./g,`\r
|
|
5
|
+
..`);return s&&t.startsWith(".")&&(t=`.${t}`),s=t.endsWith(`\r
|
|
6
|
+
`),t}}function O(s){return s>126||s===61||s<32&&s!==9&&s!==10&&s!==13}function M(s){let e=0;for(let t of s)O(t)&&e++;return s.length+e*2}function W(s,e=76){let t=h(s),r=e-3,n=[],i="",a=0,o=()=>{let l=i.charCodeAt(i.length-1);l===32?i=i.slice(0,-1)+"=20":l===9&&(i=i.slice(0,-1)+"=09"),n.push(i,`=\r
|
|
7
|
+
`),i=""};for(;a<t.length;){let l=t[a],c;if(l===10){n.push(i,`\r
|
|
8
|
+
`),i="",a++;continue}else if(l===13){if(a+1<t.length&&t[a+1]===10){n.push(i,`\r
|
|
9
|
+
`),i="",a+=2;continue}c="=0D"}if(c===void 0){let m=l===32||l===9,f=a+1>=t.length||t[a+1]===10||t[a+1]===13;c=O(l)||m&&f?`=${l.toString(16).toUpperCase().padStart(2,"0")}`:String.fromCharCode(l)}i.length+c.length>r&&o(),i+=c,a++}return n.push(i),n.join("")}var F="=?UTF-8?Q?",N="?=",Y=75-F.length-N.length;function $(s){if(!/[^\x20-\x7E]/.test(s))return s;let e=[],t="";for(let r of s){let n="";for(let i of h(r))i>=33&&i<=126&&i!==63&&i!==61&&i!==95?n+=String.fromCharCode(i):i===32?n+="_":n+=`=${i.toString(16).toUpperCase().padStart(2,"0")}`;t.length+n.length>Y&&(e.push(t),t=""),t+=n}return t&&e.push(t),e.map(r=>`${F}${r}${N}`).join(" ")}function G(s){if(!s.name)return s.email;let e=$(s.name);return e===s.name?`"${s.name.replace(/([\\"])/g,"\\$1")}" <${s.email}>`:`${e} <${s.email}>`}function L(s,e){let t=C(s),r=/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/.test(t),n=/[ \t]\r\n|[ \t]$/.test(t),i=0;for(let l of t.split(`\r
|
|
10
|
+
`))l.length>i&&(i=l.length);if(!r&&!n){if(i<=990&&!/[^\x00-\x7F]/.test(t))return{encoding:"7bit",render:()=>t};if(e&&i<=240)return{encoding:"8bit",render:()=>t}}let a=h(t),o=Math.ceil(a.length/3)*4;return M(a)<=o?{encoding:"quoted-printable",render:()=>W(t)}:{encoding:"base64",render:()=>v(p(a))}}function Q(s){return s instanceof Uint8Array?s.length:s.byteLength}var E=class s{from;to;reply;cc;bcc;subject;text;html;dsnOverride;attachments;headers;result;setSent;setSentError;sent=new Promise((e,t)=>{this.setSent=e,this.setSentError=t});constructor(e){if(!e.text&&!e.html)throw new Error("At least one of text or html must be provided");typeof e.from=="string"?this.from={email:e.from}:this.from=e.from,typeof e.reply=="string"?this.reply={email:e.reply}:this.reply=e.reply,this.to=s.toUsers(e.to),this.cc=s.toUsers(e.cc),this.bcc=s.toUsers(e.bcc),this.subject=e.subject,this.text=e.text,this.html=e.html,this.attachments=e.attachments,this.dsnOverride=e.dsnOverride,this.headers=e.headers||{}}static toUsers(e){if(e)return typeof e=="string"?[{email:e}]:Array.isArray(e)?e.map(t=>typeof t=="string"?{email:t}:t):[e]}get recipients(){return[...this.to,...this.cc||[],...this.bcc||[]]}estimateSize(){let e=2048;this.text&&(e+=Math.ceil(h(this.text).length*1.4)),this.html&&(e+=Math.ceil(h(this.html).length*1.4));for(let t of this.attachments||[])e+=typeof t.content=="string"?t.content.length:Math.ceil(Q(t.content)/3)*4,e+=512;return e}*chunks(e={}){let t=!!e.allow8bit,r=this.messageHeaders(),n=this.buildContentPart(t);if(!this.attachments?.length){yield`${[...r,...n.headers].join(`\r
|
|
11
|
+
`)}\r
|
|
12
|
+
\r
|
|
13
|
+
`,yield*n.body();return}let i=P("mixed");yield`${[...r,d("Content-Type",`multipart/mixed; boundary="${i}"`)].join(`\r
|
|
14
|
+
`)}\r
|
|
15
|
+
\r
|
|
16
|
+
`,yield`--${i}\r
|
|
17
|
+
${n.headers.join(`\r
|
|
18
|
+
`)}\r
|
|
19
|
+
\r
|
|
20
|
+
`,yield*n.body();for(let a of this.attachments)yield*this.renderAttachment(i,a);yield`--${i}--\r
|
|
21
|
+
`}getEmailData(e={}){let t=S(),r="";for(let n of this.chunks(e))r+=t(n);return r.endsWith(`\r
|
|
22
|
+
`)||(r+=`\r
|
|
23
|
+
`),`${r}.\r
|
|
24
|
+
`}buildContentPart(e){let t=this.text,r=this.html;if(t!==void 0&&r!==void 0){let a=P("alternative"),o=this.renderTextPart.bind(this);return{headers:[d("Content-Type",`multipart/alternative; boundary="${a}"`)],*body(){yield*o(a,"text/plain",t,e),yield*o(a,"text/html",r,e),yield`--${a}--\r
|
|
25
|
+
`}}}let n=t!==void 0?"text/plain":"text/html",i=L(t??r,e);return{headers:[d("Content-Type",`${n}; charset="UTF-8"`),`Content-Transfer-Encoding: ${i.encoding}`],*body(){yield`${i.render()}\r
|
|
26
|
+
`}}}*renderTextPart(e,t,r,n){let i=L(r,n);yield`--${e}\r
|
|
27
|
+
`+d("Content-Type",`${t}; charset="UTF-8"`)+`\r
|
|
28
|
+
Content-Transfer-Encoding: ${i.encoding}\r
|
|
29
|
+
\r
|
|
30
|
+
`,yield`${i.render()}\r
|
|
31
|
+
`}*renderAttachment(e,t){let r=t.mimeType||V(t.filename),n=[d("Content-Type",`${r}; ${x("name",t.filename)}`),d("Content-Description",$(t.filename)),d("Content-Disposition",`attachment; ${x("filename",t.filename)}`),"Content-Transfer-Encoding: base64"];if(yield`--${e}\r
|
|
32
|
+
${n.join(`\r
|
|
33
|
+
`)}\r
|
|
34
|
+
\r
|
|
35
|
+
`,typeof t.content=="string"){yield`${v(t.content)}\r
|
|
36
|
+
`;return}let i=t.content instanceof Uint8Array?t.content:new Uint8Array(t.content),a=57*96;for(let o=0;o<i.length;o+=a)yield`${v(p(i.subarray(o,o+a)))}\r
|
|
37
|
+
`}messageHeaders(){this.resolveHeader();let e=["MIME-Version: 1.0"];for(let[t,r]of Object.entries(this.headers))e.push(d(t,r));return e}hasHeader(e){let t=e.toLowerCase();return Object.keys(this.headers).some(r=>r.toLowerCase()===t)}resolveHeader(){this.resolveAddressHeader("From",[this.from]),this.resolveAddressHeader("To",this.to),this.resolveAddressHeader("Reply-To",this.reply?[this.reply]:void 0),this.resolveAddressHeader("Cc",this.cc),this.resolveSubject(),this.hasHeader("Date")||(this.headers.Date=k()),this.hasHeader("Message-ID")||(this.headers["Message-ID"]=`<${crypto.randomUUID()}@${this.from.email.split("@").pop()}>`)}resolveAddressHeader(e,t){!t?.length||this.hasHeader(e)||(this.headers[e]=t.map(G).join(", "))}resolveSubject(){this.hasHeader("Subject")||!this.subject||(this.headers.Subject=$(this.subject))}};function P(s){let e=new Uint8Array(16);crypto.getRandomValues(e);let t=Array.from(e).map(r=>r.toString(16).padStart(2,"0")).join("");return`${s}_${t}`}var X={txt:"text/plain",html:"text/html",csv:"text/csv",pdf:"application/pdf",png:"image/png",jpg:"image/jpeg",jpeg:"image/jpeg",gif:"image/gif",zip:"application/zip"};function V(s){let e=s.split(".").pop()?.toLowerCase();return X[e||"txt"]||"application/octet-stream"}import{connect as Z}from"cloudflare:sockets";var I=(i=>(i[i.DEBUG=0]="DEBUG",i[i.INFO=1]="INFO",i[i.WARN=2]="WARN",i[i.ERROR=3]="ERROR",i[i.NONE=4]="NONE",i))(I||{}),w=class{constructor(e=1,t){this.level=e;this.prefix=t}prefix;debug(e,...t){this.level<=0&&console.debug(this.prefix+e,...t)}info(e,...t){this.level<=1&&console.info(this.prefix+e,...t)}warn(e,...t){this.level<=2&&console.warn(this.prefix+e,...t)}error(e,...t){this.level<=3&&console.error(this.prefix+e,...t)}};var J=262144,K=32768,H=class s{socket;host;port;secure;startTls;requireTls;ehloName;authType;credentials;socketTimeoutMs;responseTimeoutMs;pipelining;chunking;allowPartialRecipients;reader;writer;decoder=new TextDecoder("utf-8");buffer="";streamClosed=!1;logger;dsn;active=!1;tlsActive=!1;emailSending=null;emailToBeSent=new y;supportsDSN=!1;allowAuth=!1;authTypeSupported=[];supportsStartTls=!1;supportsPipelining=!1;supportsChunking=!1;supports8BitMime=!1;maxMessageSize=0;constructor(e){this.port=e.port,this.host=e.host,this.secure=!!e.secure,Array.isArray(e.authType)?this.authType=e.authType:typeof e.authType=="string"?this.authType=[e.authType]:this.authType=[],this.startTls=e.startTls===void 0?!0:e.startTls,this.requireTls=!!e.requireTls,this.credentials=e.credentials,this.dsn=e.dsn||{},this.pipelining=e.pipelining!==!1,this.chunking=!!e.chunking,this.allowPartialRecipients=!!e.allowPartialRecipients,this.socketTimeoutMs=e.socketTimeoutMs||6e4,this.responseTimeoutMs=e.responseTimeoutMs||3e4,this.tlsActive=this.secure,this.socket=Z({hostname:this.host,port:this.port},{secureTransport:this.secure?"on":this.startTls?"starttls":"off",allowHalfOpen:!1}),this.reader=this.socket.readable.getReader(),this.writer=this.socket.writable.getWriter(),this.logger=new w(e.logLevel,`[WorkerMailer:${this.host}:${this.port}]`),this.ehloName=s.resolveEhloName(e.ehloName),e.ehloName||this.logger.warn("No ehloName set, announcing [127.0.0.1]. Set ehloName to a domain you control \u2014 receiving servers use it for spam scoring.")}static resolveEhloName(e){let t=e?.trim();return t?/^\[.+\]$/.test(t)?t:/^\d{1,3}(\.\d{1,3}){3}$/.test(t)?`[${t}]`:t.includes(":")?`[IPv6:${t}]`:t:"[127.0.0.1]"}static async connect(e){let t=new s(e);return await t.initializeSmtpSession(),t.start().catch(console.error),t}async send(e){if(!this.active)throw new Error("WorkerMailer is not connected");let t=new E(e);return this.emailToBeSent.enqueue(t),await t.sent,t.result}static async send(e,t){let r=await s.connect(e);try{return await r.send(t)}finally{await r.close()}}async readTimeout(){return A(this.readResponse(),this.responseTimeoutMs,new Error("Timeout while waiting for smtp server response"))}async readResponse(){for(;;){let e=this.takeResponse();if(e!==null)return e;if(this.streamClosed)throw new Error("SMTP server closed the connection");let{value:t,done:r}=await this.reader.read();if(r){this.streamClosed=!0;continue}if(!t)continue;let n=this.decoder.decode(t,{stream:!0});this.logger.debug(`SMTP server response:
|
|
38
|
+
`+n),this.buffer+=n}}takeResponse(){let e=0;for(;;){let t=this.buffer.indexOf(`
|
|
39
|
+
`,e);if(t===-1)return null;if(!/^\d{3}-/.test(this.buffer.slice(e,t))){let r=this.buffer.slice(0,t+1);return this.buffer=this.buffer.slice(t+1),r}e=t+1}}async writeLine(e){await this.write(`${e}\r
|
|
40
|
+
`)}async write(e,t=!0){this.logger.debug(t?`Write to socket:
|
|
41
|
+
`+e:`Write to socket: ${e.length} characters`),await this.writer.write(h(e))}async writeBytes(e){this.logger.debug(`Write to socket: ${e.byteLength} bytes`),await this.writer.write(e)}async initializeSmtpSession(){if(await this.waitForSocketConnected(),await this.greet(),await this.ehlo(),this.startTls&&!this.secure&&this.supportsStartTls&&(await this.tls(),await this.ehlo()),this.requireTls&&!this.tlsActive)throw new Error("requireTls is set but the connection is not encrypted (server does not support STARTTLS)");await this.auth(),this.active=!0}async start(){for(;this.active;){this.emailSending=await this.emailToBeSent.dequeue();try{this.emailSending.result=await this.transaction(this.emailSending),this.emailSending.setSent()}catch(e){if(this.logger.error("Failed to send email: "+e.message),!this.active)return;this.emailSending.setSentError(e);try{await this.rset()}catch(t){await this.close(t)}}this.emailSending=null}}async close(e){for(this.active=!1,this.logger.info("WorkerMailer is closed",e?.message||""),this.emailSending?.setSentError?.(e||new Error("WorkerMailer is shutting down"));this.emailToBeSent.length;)(await this.emailToBeSent.dequeue()).setSentError(e||new Error("WorkerMailer is shutting down"));try{await this.writeLine("QUIT"),await this.readTimeout(),this.socket.close().catch(()=>this.logger.error("Failed to close socket"))}catch{}}async waitForSocketConnected(){this.logger.info("Connecting to SMTP server"),await A(this.socket.opened,this.socketTimeoutMs,new Error("Socket timeout!")),this.logger.info("SMTP server connected")}async greet(){let e=await this.readTimeout();if(!e.startsWith("220"))throw new Error("Failed to connect to SMTP server: "+e)}async ehlo(){await this.writeLine(`EHLO ${this.ehloName}`);let e=await this.readTimeout();if(e.startsWith("421"))throw new Error(`Failed to EHLO. ${e}`);if(!e.startsWith("2")){await this.helo();return}this.parseCapabilities(e)}async helo(){await this.writeLine(`HELO ${this.ehloName}`);let e=await this.readTimeout();if(!e.startsWith("2"))throw new Error(`Failed to HELO. ${e}`)}async tls(){await this.writeLine("STARTTLS");let e=await this.readTimeout();if(!e.startsWith("220"))throw new Error("Failed to start TLS: "+e);this.reader.releaseLock(),this.writer.releaseLock(),this.socket=this.socket.startTls(),this.reader=this.socket.readable.getReader(),this.writer=this.socket.writable.getWriter(),this.decoder=new TextDecoder("utf-8"),this.buffer="",this.tlsActive=!0}parseCapabilities(e){this.allowAuth=!1,this.authTypeSupported=[],this.supportsStartTls=!1,this.supportsDSN=!1,this.supportsPipelining=!1,this.supportsChunking=!1,this.supports8BitMime=!1,this.maxMessageSize=0;let t=new Set;for(let r of e.split(/\r?\n/)){let n=r.replace(/^\d{3}[- ]?/,"").trim();if(!n)continue;let[i,...a]=n.split(/[ =]+/);switch(i.toUpperCase()){case"AUTH":this.allowAuth=!0;for(let o of a)switch(o.toUpperCase()){case"PLAIN":t.add("plain");break;case"LOGIN":t.add("login");break;case"CRAM-MD5":t.add("cram-md5");break;case"XOAUTH2":t.add("xoauth2");break}break;case"STARTTLS":this.supportsStartTls=!0;break;case"DSN":this.supportsDSN=!0;break;case"PIPELINING":this.supportsPipelining=!0;break;case"CHUNKING":this.supportsChunking=!0;break;case"8BITMIME":this.supports8BitMime=!0;break;case"SIZE":this.maxMessageSize=Number(a[0])||0;break}}this.authTypeSupported=[...t]}defaultAuthOrder(){return this.tlsActive?["plain","login","cram-md5"]:["cram-md5","plain","login"]}selectAuthType(){return(this.authType.length?this.authType:this.defaultAuthOrder()).find(t=>this.authTypeSupported.includes(t))}async auth(){if(this.allowAuth){if(!this.credentials)throw new Error("smtp server requires authentication, but no credentials found");switch(this.tlsActive||this.logger.warn("Authenticating over an unencrypted connection, credentials are sent in the clear"),this.selectAuthType()){case"plain":return this.authWithPlain();case"login":return this.authWithLogin();case"cram-md5":return this.authWithCramMD5();case"xoauth2":return this.authWithXOAuth2();default:throw new Error("No supported auth method found.")}}}requirePassword(){let e=this.credentials?.password;if(e===void 0)throw new Error("credentials.password is required for this auth method");return e}async authWithPlain(){let e=this.requirePassword(),t=p(h(`\0${this.credentials.username}\0${e}`));await this.writeLine(`AUTH PLAIN ${t}`);let r=await this.readTimeout();if(!r.startsWith("2"))throw new Error(`Failed to plain authentication: ${r}`)}async authWithLogin(){let e=this.requirePassword();await this.writeLine("AUTH LOGIN");let t=await this.readTimeout();if(!t.startsWith("3"))throw new Error("Invalid login: "+t);await this.writeLine(p(h(this.credentials.username)));let r=await this.readTimeout();if(!r.startsWith("3"))throw new Error("Failed to login authentication: "+r);await this.writeLine(p(h(e)));let n=await this.readTimeout();if(!n.startsWith("2"))throw new Error("Failed to login authentication: "+n)}async authWithCramMD5(){let e=this.requirePassword();await this.writeLine("AUTH CRAM-MD5");let t=await this.readTimeout(),r=t.match(/^334\s+(.+)$/m)?.pop()?.trim();if(!r)throw new Error("Invalid CRAM-MD5 challenge: "+t);let n=await crypto.subtle.importKey("raw",h(e),{name:"HMAC",hash:"MD5"},!1,["sign"]),i=await crypto.subtle.sign("HMAC",n,D(r)),a=Array.from(new Uint8Array(i)).map(l=>l.toString(16).padStart(2,"0")).join("");await this.writeLine(p(h(`${this.credentials.username} ${a}`)));let o=await this.readTimeout();if(!o.startsWith("2"))throw new Error(`Failed to cram-md5 authentication: ${o}`)}async authWithXOAuth2(){let e=this.credentials?.accessToken;if(!e)throw new Error("credentials.accessToken is required for xoauth2");let t=p(h(`user=${this.credentials.username}auth=Bearer ${e}`));await this.writeLine(`AUTH XOAUTH2 ${t}`);let r=await this.readTimeout();if(!r.startsWith("2"))throw r.startsWith("3")?(await this.writeLine(""),new Error(`Failed to xoauth2 authentication: ${await this.readTimeout()}`)):new Error(`Failed to xoauth2 authentication: ${r}`)}async transaction(e){let t=e.recipients;if(!t.length)throw new Error("Email has no recipients");this.checkSize(e);let r=this.chunking&&this.supportsChunking,n=this.mailFromCommand(e),i=t.map(u=>this.rcptToCommand(e,u)),a=!r&&(this.allowPartialRecipients||t.length===1),o,l=[],c=null;if(this.pipelining&&this.supportsPipelining){let u=[n,...i];a&&u.push("DATA"),await this.write(u.map(g=>`${g}\r
|
|
42
|
+
`).join("")),o=await this.readTimeout();for(let g of i)l.push(await this.readTimeout());a&&(c=await this.readTimeout())}else if(await this.writeLine(n),o=await this.readTimeout(),o.startsWith("2"))for(let u of i)await this.writeLine(u),l.push(await this.readTimeout());if(!o.startsWith("2"))throw new Error(`Invalid ${n} ${o}`);let m=[],f=[];if(t.forEach((u,g)=>{let U=l[g];U?.startsWith("2")?m.push(u):f.push({user:u,response:(U||"").trim()})}),f.length&&(!m.length||!this.allowPartialRecipients)){c?.startsWith("3")&&await this.abortData();let{user:u,response:g}=f[0];throw new Error(`Invalid RCPT TO: <${u.email}> ${g}`)}f.length&&this.logger.warn(`Server rejected ${f.length} of ${t.length} recipients`);let B=r?await this.sendWithChunking(e):await this.sendWithData(e,c);return{accepted:m,rejected:f,response:B.trim()}}mailFromCommand(e){let t=`MAIL FROM: <${e.from.email}>`;if(this.supportsDSN){let r=this.retBuilder(e);r&&(t+=` ${r}`),e.dsnOverride?.envelopeId&&(t+=` ENVID=${e.dsnOverride.envelopeId}`)}return t}rcptToCommand(e,t){let r=`RCPT TO: <${t.email}>`;return this.supportsDSN&&(r+=this.notificationBuilder(e)),r}checkSize(e){if(!this.maxMessageSize)return;let t=e.estimateSize();if(t>this.maxMessageSize)throw new Error(`Message is roughly ${t} bytes but the server accepts at most ${this.maxMessageSize} bytes`)}serializeOptions(){return{allow8bit:this.supports8BitMime}}async abortData(){await this.write(`\r
|
|
43
|
+
.\r
|
|
44
|
+
`),await this.readTimeout()}async sendWithData(e,t){let r=t;if(r===null&&(await this.writeLine("DATA"),r=await this.readTimeout()),!r.startsWith("3"))throw new Error(`Failed to send DATA: ${r}`);let n=S(),i="",a=!0;for(let l of e.chunks(this.serializeOptions())){let c=n(l);a=c.endsWith(`\r
|
|
45
|
+
`),i+=c,i.length>=K&&(await this.write(i,!1),i="")}i+=a?`.\r
|
|
46
|
+
`:`\r
|
|
47
|
+
.\r
|
|
48
|
+
`,await this.write(i,!1);let o=await this.readTimeout();if(!o.startsWith("2"))throw new Error("Failed send email body: "+o);return o}async sendWithChunking(e){let t="",r=0,n=async a=>{let o=h(t);t="",await this.write(`BDAT ${o.byteLength}${a?" LAST":""}\r
|
|
49
|
+
`),await this.writeBytes(o),r++};for(let a of e.chunks(this.serializeOptions()))t+=a,t.length>=J&&await n(!1);await n(!0);let i="";for(let a=0;a<r;a++)if(i=await this.readTimeout(),!i.startsWith("2"))throw new Error("Failed send email body: "+i);return i}async rset(){await this.writeLine("RSET");let e=await this.readTimeout();if(!e.startsWith("2"))throw new Error(`Failed to reset: ${e}`)}notificationBuilder(e){let t=e.dsnOverride?.NOTIFY,r=t?void 0:this.dsn?.NOTIFY,n=[];return(t?.SUCCESS||r?.SUCCESS)&&n.push("SUCCESS"),(t?.FAILURE||r?.FAILURE)&&n.push("FAILURE"),(t?.DELAY||r?.DELAY)&&n.push("DELAY"),n.length>0?` NOTIFY=${n.join(",")}`:" NOTIFY=NEVER"}retBuilder(e){let t=e.dsnOverride?.RET,r=t?void 0:this.dsn?.RET,n=[];return(t?.HEADERS||r?.HEADERS)&&n.push("HDRS"),(t?.FULL||r?.FULL)&&n.push("FULL"),n.length>0?`RET=${n.join(",")}`:""}};export{E as Email,I as LogLevel,H as WorkerMailer,$ as encodeHeader,G as formatAddress};
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@luca-emmert/worker-mailer",
|
|
3
|
+
"version": "1.3.0",
|
|
4
|
+
"main": "./dist/index.js",
|
|
5
|
+
"module": "./dist/index.mjs",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"cloudflare",
|
|
9
|
+
"workers",
|
|
10
|
+
"cloudflare-workers",
|
|
11
|
+
"email",
|
|
12
|
+
"smtp"
|
|
13
|
+
],
|
|
14
|
+
"author": "zou-yu, lucaemt",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "https://github.com/zou-yu/worker-mailer.git"
|
|
18
|
+
},
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"homepage": "https://github.com/zou-yu/worker-mailer",
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@changesets/cli": "^2.27.9",
|
|
23
|
+
"@cloudflare/vitest-pool-workers": "^0.4.5",
|
|
24
|
+
"@cloudflare/workers-types": "^4.20240722.0",
|
|
25
|
+
"@types/libqp": "^1.1.3",
|
|
26
|
+
"@types/node": "^20.14.12",
|
|
27
|
+
"letterparser": "^0.1.8",
|
|
28
|
+
"libqp": "^2.1.1",
|
|
29
|
+
"prettier": "^3.3.3",
|
|
30
|
+
"tsup": "^8.2.3",
|
|
31
|
+
"typescript": "^5.5.2",
|
|
32
|
+
"vitest": "1.5.0",
|
|
33
|
+
"wrangler": "^3.60.3"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsup",
|
|
37
|
+
"test": "vitest",
|
|
38
|
+
"format": "prettier '**/*.{json,ts,js,cjs,mjs,md}' --write --ignore-path .gitignore"
|
|
39
|
+
}
|
|
40
|
+
}
|