@workermailer/smtp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 André Ribas
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,539 @@
1
+ # Worker Mailer
2
+
3
+ [English](./README.md) | [Português](./README_pt-BR.md)
4
+
5
+ [![npm version](https://badge.fury.io/js/@ribassu%2Fworker-mailer.svg)](https://badge.fury.io/js/@ribassu%2Fworker-mailer)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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
+ - �️ Inline image attachments with Content-ID (CID) support
16
+ - 🔒 Supports multiple SMTP authentication methods: `plain`, `login`, and `CRAM-MD5`
17
+ - ✅ Email address validation (RFC 5322 compliant)
18
+ - 🎯 Custom error classes for better error handling
19
+ - 🪝 Lifecycle hooks for monitoring email operations
20
+ - 📅 DSN support
21
+ - 📬 Optional Cloudflare Queues integration for async email processing
22
+
23
+ ## Table of Contents
24
+
25
+ - [Installation](#installation)
26
+ - [Quick Start](#quick-start)
27
+ - [API Reference](#api-reference)
28
+ - [Inline Images (CID)](#inline-images-cid)
29
+ - [Lifecycle Hooks](#lifecycle-hooks)
30
+ - [Error Handling](#error-handling)
31
+ - [Cloudflare Queues Integration](#cloudflare-queues-integration)
32
+ - [Limitations](#limitations)
33
+ - [Contributing](#contributing)
34
+ - [License](#license)
35
+
36
+ ## Installation
37
+
38
+ ```shell
39
+ npm i @ribassu/worker-mailer
40
+ ```
41
+
42
+ ## Quick Start
43
+
44
+ 1. Configure your `wrangler.toml`:
45
+
46
+ ```toml
47
+ compatibility_flags = ["nodejs_compat"]
48
+ # or compatibility_flags = ["nodejs_compat_v2"]
49
+ ```
50
+
51
+ 2. Use in your code:
52
+
53
+ ```typescript
54
+ import { WorkerMailer } from '@ribassu/worker-mailer'
55
+
56
+ // Connect to SMTP server
57
+ const mailer = await WorkerMailer.connect({
58
+ credentials: {
59
+ username: 'bob@acme.com',
60
+ password: 'password',
61
+ },
62
+ authType: 'plain',
63
+ host: 'smtp.acme.com',
64
+ port: 587,
65
+ secure: true,
66
+ })
67
+
68
+ // Send email
69
+ await mailer.send({
70
+ from: { name: 'Bob', email: 'bob@acme.com' },
71
+ to: { name: 'Alice', email: 'alice@acme.com' },
72
+ subject: 'Hello from Worker Mailer',
73
+ text: 'This is a plain text message',
74
+ html: '<h1>Hello</h1><p>This is an HTML message</p>',
75
+ })
76
+ ```
77
+
78
+ 3. Using with modern JavaScript frameworks (Next.js, Nuxt, SvelteKit, etc.)
79
+
80
+ 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.
81
+
82
+ The recommended approach is to use conditional dynamic imports. Here's an example for Nuxt.js:
83
+
84
+ ```typescript
85
+ export default defineEventHandler(async event => {
86
+ // Check if running in development environment
87
+ if (import.meta.dev) {
88
+ // Development: Use nodemailer (or any Node.js compatible email library)
89
+ const nodemailer = await import('nodemailer')
90
+ const transporter = nodemailer.default.createTransport()
91
+ return await transporter.sendMail()
92
+ } else {
93
+ // Production: Use worker-mailer in Cloudflare Workers environment
94
+ const { WorkerMailer } = await import('@ribassu/worker-mailer')
95
+ const mailer = await WorkerMailer.connect()
96
+ return await mailer.send()
97
+ }
98
+ })
99
+ ```
100
+
101
+ This pattern ensures your application works seamlessly in both development and production environments.
102
+
103
+ ## API Reference
104
+
105
+ ### WorkerMailer.connect(options)
106
+
107
+ Creates a new SMTP connection.
108
+
109
+ ```typescript
110
+ type WorkerMailerOptions = {
111
+ host: string // SMTP server hostname
112
+ port: number // SMTP server port (usually 587 or 465)
113
+ secure?: boolean // Use TLS (default: false)
114
+ startTls?: boolean // Upgrade to TLS if SMTP server supports (default: true)
115
+ credentials?: {
116
+ // SMTP authentication credentials
117
+ username: string
118
+ password: string
119
+ }
120
+ authType?:
121
+ | 'plain'
122
+ | 'login'
123
+ | 'cram-md5'
124
+ | Array<'plain' | 'login' | 'cram-md5'>
125
+ logLevel?: LogLevel // Logging level (default: LogLevel.INFO)
126
+ socketTimeoutMs?: number // Socket timeout in milliseconds
127
+ responseTimeoutMs?: number // Server response timeout in milliseconds
128
+ hooks?: WorkerMailerHooks // Lifecycle hooks for monitoring
129
+ dsn?: {
130
+ RET?: {
131
+ HEADERS?: boolean
132
+ FULL?: boolean
133
+ }
134
+ NOTIFY?: {
135
+ DELAY?: boolean
136
+ FAILURE?: boolean
137
+ SUCCESS?: boolean
138
+ }
139
+ }
140
+ }
141
+ ```
142
+
143
+ ### mailer.send(options)
144
+
145
+ Sends an email.
146
+
147
+ ```typescript
148
+ type EmailOptions = {
149
+ from:
150
+ | string
151
+ | {
152
+ // Sender's email
153
+ name?: string
154
+ email: string
155
+ }
156
+ to:
157
+ | string
158
+ | string[]
159
+ | {
160
+ // Recipients (TO)
161
+ name?: string
162
+ email: string
163
+ }
164
+ | Array<{ name?: string; email: string }>
165
+ reply?:
166
+ | string
167
+ | {
168
+ // Reply-To address
169
+ name?: string
170
+ email: string
171
+ }
172
+ cc?:
173
+ | string
174
+ | string[]
175
+ | {
176
+ // Carbon Copy recipients
177
+ name?: string
178
+ email: string
179
+ }
180
+ | Array<{ name?: string; email: string }>
181
+ bcc?:
182
+ | string
183
+ | string[]
184
+ | {
185
+ // Blind Carbon Copy recipients
186
+ name?: string
187
+ email: string
188
+ }
189
+ | Array<{ name?: string; email: string }>
190
+ subject: string // Email subject
191
+ text?: string // Plain text content
192
+ html?: string // HTML content
193
+ headers?: Record<string, string> // Custom email headers
194
+ attachments?: Attachment[] // Attachments
195
+ dsnOverride?: {
196
+ // Overrides dsn defined in WorkerMailer
197
+ envelopeId?: string | undefined
198
+ RET?: {
199
+ HEADERS?: boolean
200
+ FULL?: boolean
201
+ }
202
+ NOTIFY?: {
203
+ DELAY?: boolean
204
+ FAILURE?: boolean
205
+ SUCCESS?: boolean
206
+ }
207
+ }
208
+ }
209
+
210
+ type Attachment = {
211
+ filename: string
212
+ content: string // Base64-encoded content
213
+ mimeType?: string // MIME type (auto-detected if not set)
214
+ cid?: string // Content-ID for inline images
215
+ inline?: boolean // If true, attachment will be inline
216
+ }
217
+ ```
218
+
219
+ ### Static Method: WorkerMailer.send()
220
+
221
+ Send a one-off email without maintaining the connection.
222
+
223
+ ```typescript
224
+ await WorkerMailer.send(
225
+ {
226
+ // WorkerMailerOptions
227
+ host: 'smtp.acme.com',
228
+ port: 587,
229
+ credentials: {
230
+ username: 'user',
231
+ password: 'pass',
232
+ },
233
+ },
234
+ {
235
+ // EmailOptions
236
+ from: 'sender@acme.com',
237
+ to: 'recipient@acme.com',
238
+ subject: 'Test',
239
+ text: 'Hello',
240
+ attachments: [
241
+ {
242
+ filename: 'test.txt',
243
+ content: 'SGVsbG8gV29ybGQ=', // base64-encoded string for "Hello World"
244
+ mimeType: 'text/plain',
245
+ },
246
+ ],
247
+ },
248
+ )
249
+ ```
250
+
251
+ ## Inline Images (CID)
252
+
253
+ You can embed images directly in HTML emails using Content-ID (CID):
254
+
255
+ ```typescript
256
+ import { WorkerMailer } from '@ribassu/worker-mailer'
257
+
258
+ const mailer = await WorkerMailer.connect({
259
+ host: 'smtp.acme.com',
260
+ port: 587,
261
+ credentials: { username: 'user', password: 'pass' },
262
+ })
263
+
264
+ await mailer.send({
265
+ from: 'sender@acme.com',
266
+ to: 'recipient@acme.com',
267
+ subject: 'Email with embedded image',
268
+ html: `
269
+ <h1>Hello!</h1>
270
+ <p>Here's our logo:</p>
271
+ <img src="cid:company-logo" alt="Company Logo">
272
+ `,
273
+ attachments: [
274
+ {
275
+ filename: 'logo.png',
276
+ content: logoBase64, // Base64-encoded image
277
+ mimeType: 'image/png',
278
+ cid: 'company-logo', // Referenced in HTML as cid:company-logo
279
+ inline: true,
280
+ },
281
+ ],
282
+ })
283
+ ```
284
+
285
+ ## Lifecycle Hooks
286
+
287
+ Monitor email operations with lifecycle hooks:
288
+
289
+ ```typescript
290
+ import { WorkerMailer } from '@ribassu/worker-mailer'
291
+
292
+ const mailer = await WorkerMailer.connect({
293
+ host: 'smtp.acme.com',
294
+ port: 587,
295
+ credentials: { username: 'user', password: 'pass' },
296
+ hooks: {
297
+ onConnect: () => {
298
+ console.log('Connected to SMTP server')
299
+ },
300
+ onSent: (email, response) => {
301
+ console.log(`Email sent to ${email.to}:`, response)
302
+ },
303
+ onError: (email, error) => {
304
+ console.error(`Failed to send email:`, error)
305
+ // Send to error tracking service, etc.
306
+ },
307
+ onClose: error => {
308
+ if (error) {
309
+ console.error('Connection closed with error:', error)
310
+ } else {
311
+ console.log('Connection closed')
312
+ }
313
+ },
314
+ },
315
+ })
316
+ ```
317
+
318
+ ## Error Handling
319
+
320
+ Worker Mailer provides custom error classes for better error handling:
321
+
322
+ ```typescript
323
+ import {
324
+ WorkerMailer,
325
+ InvalidEmailError,
326
+ SmtpAuthError,
327
+ SmtpConnectionError,
328
+ SmtpRecipientError,
329
+ SmtpTimeoutError,
330
+ InvalidContentError,
331
+ } from '@ribassu/worker-mailer'
332
+
333
+ try {
334
+ const mailer = await WorkerMailer.connect({
335
+ host: 'smtp.acme.com',
336
+ port: 587,
337
+ credentials: { username: 'user', password: 'wrong-password' },
338
+ })
339
+
340
+ await mailer.send({
341
+ from: 'invalid-email', // This will throw InvalidEmailError
342
+ to: 'recipient@acme.com',
343
+ subject: 'Test',
344
+ text: 'Hello',
345
+ })
346
+ } catch (error) {
347
+ if (error instanceof InvalidEmailError) {
348
+ console.error('Invalid emails:', error.invalidEmails)
349
+ } else if (error instanceof SmtpAuthError) {
350
+ console.error('Authentication failed')
351
+ } else if (error instanceof SmtpConnectionError) {
352
+ console.error('Could not connect to SMTP server')
353
+ } else if (error instanceof SmtpRecipientError) {
354
+ console.error('Recipient rejected:', error.recipient)
355
+ } else if (error instanceof SmtpTimeoutError) {
356
+ console.error('Operation timed out')
357
+ } else if (error instanceof InvalidContentError) {
358
+ console.error('Invalid email content (missing text or html)')
359
+ }
360
+ }
361
+ ```
362
+
363
+ ## Cloudflare Queues Integration
364
+
365
+ For high-volume email sending, you can use Cloudflare Queues for async processing:
366
+
367
+ ### Setup
368
+
369
+ 1. Add a Queue binding in `wrangler.toml`:
370
+
371
+ ```toml
372
+ [[queues.producers]]
373
+ queue = "email-queue"
374
+ binding = "EMAIL_QUEUE"
375
+
376
+ [[queues.consumers]]
377
+ queue = "email-queue"
378
+ max_batch_size = 10
379
+ max_retries = 3
380
+ ```
381
+
382
+ 2. Create your worker with queue handler:
383
+
384
+ ```typescript
385
+ import { WorkerMailer } from '@ribassu/worker-mailer'
386
+ import {
387
+ createQueueHandler,
388
+ enqueueEmail,
389
+ type QueueEmailMessage,
390
+ } from '@ribassu/worker-mailer/queue'
391
+
392
+ interface Env {
393
+ EMAIL_QUEUE: Queue<QueueEmailMessage>
394
+ }
395
+
396
+ export default {
397
+ // Handle HTTP requests - enqueue emails
398
+ async fetch(request: Request, env: Env): Promise<Response> {
399
+ await enqueueEmail(env.EMAIL_QUEUE, {
400
+ mailerOptions: {
401
+ host: 'smtp.acme.com',
402
+ port: 587,
403
+ credentials: { username: 'user', password: 'pass' },
404
+ authType: 'plain',
405
+ },
406
+ emailOptions: {
407
+ from: 'sender@acme.com',
408
+ to: 'recipient@acme.com',
409
+ subject: 'Hello from Queue',
410
+ text: 'This email was sent via Cloudflare Queues!',
411
+ },
412
+ })
413
+
414
+ return new Response('Email queued successfully')
415
+ },
416
+
417
+ // Process queued emails
418
+ async queue(batch: MessageBatch<QueueEmailMessage>, env: Env): Promise<void> {
419
+ const handler = createQueueHandler({
420
+ onSuccess: result => console.log('Email sent:', result.emailOptions.to),
421
+ onError: result => console.error('Failed:', result.error),
422
+ })
423
+
424
+ await handler(batch)
425
+ },
426
+ }
427
+ ```
428
+
429
+ ### Queue Helper Functions
430
+
431
+ ```typescript
432
+ import {
433
+ enqueueEmail,
434
+ enqueueEmails,
435
+ type QueueEmailMessage,
436
+ } from '@ribassu/worker-mailer/queue'
437
+
438
+ // Enqueue a single email
439
+ await enqueueEmail(env.EMAIL_QUEUE, {
440
+ mailerOptions: { host: 'smtp.acme.com', port: 587 /* ... */ },
441
+ emailOptions: {
442
+ from: 'a@b.com',
443
+ to: 'c@d.com',
444
+ subject: 'Hi',
445
+ text: 'Hello',
446
+ },
447
+ })
448
+
449
+ // Enqueue multiple emails at once
450
+ await enqueueEmails(env.EMAIL_QUEUE, [
451
+ {
452
+ mailerOptions: {
453
+ /* ... */
454
+ },
455
+ emailOptions: {
456
+ /* ... */
457
+ },
458
+ },
459
+ {
460
+ mailerOptions: {
461
+ /* ... */
462
+ },
463
+ emailOptions: {
464
+ /* ... */
465
+ },
466
+ },
467
+ ])
468
+ ```
469
+
470
+ ## Limitations
471
+
472
+ - **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.
473
+ - **Connection Limits:** Each Worker instance has a limit on the number of concurrent TCP connections. Make sure to properly close connections when done.
474
+
475
+ ## Contributing
476
+
477
+ ### Development Workflow
478
+
479
+ > For major changes, please open an issue first to discuss what you would like to change.
480
+
481
+ 1. Fork and clone the repository
482
+ 2. Install dependencies:
483
+ ```bash
484
+ bun install
485
+ ```
486
+ 3. Create a new branch for your feature from `develop`:
487
+ ```bash
488
+ git checkout -b feat/your-feature-name
489
+ ```
490
+ 4. Make your changes and make sure all tests pass
491
+ 5. Update README.md & changelog `bun changeset` if needed
492
+ 6. Push your changes to your fork and create a pull request from your branch to `develop`
493
+
494
+ ### Testing
495
+
496
+ 1. Unit Tests:
497
+ ```bash
498
+ bun test
499
+ ```
500
+ 2. Integration Tests:
501
+ ```bash
502
+ bunx wrangler dev ./test/worker.ts
503
+ ```
504
+ Then, send a POST request to `http://127.0.0.1:8787` with the following JSON body:
505
+ ```json
506
+ {
507
+ "config": {
508
+ "credentials": {
509
+ "username": "xxx@xx.com",
510
+ "password": "xxxx"
511
+ },
512
+ "authType": "plain",
513
+ "host": "smtp.acme.com",
514
+ "port": 587,
515
+ "secure": false,
516
+ "startTls": true
517
+ },
518
+ "email": {
519
+ "from": "xxx@xx.com",
520
+ "to": "yyy@yy.com",
521
+ "subject": "Test Email",
522
+ "text": "Hello World"
523
+ }
524
+ }
525
+ ```
526
+
527
+ ### Reporting Issues
528
+
529
+ When reporting issues, please include:
530
+
531
+ - Version of worker-mailer you're using
532
+ - A clear description of the problem
533
+ - Steps to reproduce the issue
534
+ - Expected vs actual behavior
535
+ - Any relevant code snippets or error messages
536
+
537
+ ## License
538
+
539
+ This project is licensed under the MIT License.
@@ -0,0 +1,54 @@
1
+ var S=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)}))}};function p(s){if(!s||typeof s!="string"||!/^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/.test(s))return!1;let[t,i]=s.split("@");if(t.length>64||i.length>255||!i.includes("."))return!1;let a=i.split(".").pop();return!(!a||a.length<2)}function W(s){return s.filter(e=>!p(e))}async function L(s,e,t){return Promise.race([s,new Promise((i,a)=>setTimeout(()=>a(t),e))])}var F=new TextEncoder;function u(s){return F.encode(s)}var D=new TextDecoder("utf-8");function U(s){return D.decode(s)}function R(s,e=76){let t=u(s),i="",a=0,n=0;for(;n<t.length;){let l=t[n],h;if(l===10){i+=`\r
2
+ `,a=0,n++;continue}else if(l===13)if(n+1<t.length&&t[n+1]===10){i+=`\r
3
+ `,a=0,n+=2;continue}else h="=0D";if(h===void 0){let m=l===32||l===9,f=n+1>=t.length||t[n+1]===10||t[n+1]===13;l<32&&!m||l>126||l===61||m&&f?h=`=${l.toString(16).toUpperCase().padStart(2,"0")}`:h=String.fromCharCode(l)}a+h.length>e-3&&(i+=`=\r
4
+ `,a=0),i+=h,a+=h.length,n++}return i}var c=class extends Error{code;constructor(e,t){super(e),this.name="WorkerMailerError",this.code=t}},A=class extends c{invalidEmails;constructor(e,t=[]){super(e,"INVALID_EMAIL"),this.name="InvalidEmailError",this.invalidEmails=t}},d=class extends c{constructor(e){super(e,"AUTH_FAILED"),this.name="SmtpAuthError"}},C=class extends c{constructor(e){super(e,"CONNECTION_FAILED"),this.name="SmtpConnectionError"}},O=class extends c{recipient;constructor(e,t){super(e,"RECIPIENT_REJECTED"),this.name="SmtpRecipientError",this.recipient=t}},v=class extends c{constructor(e){super(e,"TIMEOUT"),this.name="SmtpTimeoutError"}},x=class extends c{constructor(e){super(e,"INVALID_CONTENT"),this.name="InvalidContentError"}};function g(s){if(!/[^\x00-\x7F]/.test(s))return s;let e=u(s),t="";for(let i of e)i>=33&&i<=126&&i!==63&&i!==61&&i!==95?t+=String.fromCharCode(i):i===32?t+="_":t+=`=${i.toString(16).toUpperCase().padStart(2,"0")}`;return`=?UTF-8?Q?${t}?=`}var $=class s{from;to;reply;cc;bcc;subject;text;html;dsnOverride;attachments;headers;setSent;setSentError;sent=new Promise((e,t)=>{this.setSent=e,this.setSentError=t});constructor(e){if(!e.text&&!e.html)throw new x("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.validateEmails(),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]}validateEmails(){let e=[];p(this.from.email)||e.push(this.from.email);for(let t of this.to)p(t.email)||e.push(t.email);if(this.reply&&!p(this.reply.email)&&e.push(this.reply.email),this.cc)for(let t of this.cc)p(t.email)||e.push(t.email);if(this.bcc)for(let t of this.bcc)p(t.email)||e.push(t.email);if(e.length>0)throw new A(`Invalid email address(es): ${e.join(", ")}`,e)}getEmailData(){this.resolveHeader();let e=["MIME-Version: 1.0"];for(let[o,T]of Object.entries(this.headers))e.push(`${o}: ${T}`);let t=this.generateSafeBoundary("mixed_"),i=this.generateSafeBoundary("related_"),a=this.generateSafeBoundary("alternative_"),n=this.attachments?.filter(o=>o.cid)||[],l=this.attachments?.filter(o=>!o.cid)||[],h=n.length>0,m=l.length>0;e.push(`Content-Type: multipart/mixed; boundary="${t}"`);let r=`${e.join(`\r
5
+ `)}\r
6
+ \r
7
+ `;if(r+=`--${t}\r
8
+ `,h&&(r+=`Content-Type: multipart/related; boundary="${i}"\r
9
+ \r
10
+ `,r+=`--${i}\r
11
+ `),r+=`Content-Type: multipart/alternative; boundary="${a}"\r
12
+ \r
13
+ `,this.text){r+=`--${a}\r
14
+ `,r+=`Content-Type: text/plain; charset="UTF-8"\r
15
+ `,r+=`Content-Transfer-Encoding: quoted-printable\r
16
+ \r
17
+ `;let o=R(this.text);r+=`${o}\r
18
+ \r
19
+ `}if(this.html){r+=`--${a}\r
20
+ `,r+=`Content-Type: text/html; charset="UTF-8"\r
21
+ `,r+=`Content-Transfer-Encoding: quoted-printable\r
22
+ \r
23
+ `;let o=R(this.html);r+=`${o}\r
24
+ \r
25
+ `}if(r+=`--${a}--\r
26
+ `,h){for(let o of n){let T=o.mimeType||this.getMimeType(o.filename);r+=`--${i}\r
27
+ `,r+=`Content-Type: ${T}; name="${o.filename}"\r
28
+ `,r+=`Content-Transfer-Encoding: base64\r
29
+ `,r+=`Content-ID: <${o.cid}>\r
30
+ `,r+=`Content-Disposition: inline; filename="${o.filename}"\r
31
+ \r
32
+ `;let y=o.content.match(/.{1,72}/g);y?r+=`${y.join(`\r
33
+ `)}`:r+=`${o.content}`,r+=`\r
34
+ \r
35
+ `}r+=`--${i}--\r
36
+ `}if(m)for(let o of l){let T=o.mimeType||this.getMimeType(o.filename);r+=`--${t}\r
37
+ `,r+=`Content-Type: ${T}; name="${o.filename}"\r
38
+ `,r+=`Content-Description: ${o.filename}\r
39
+ `,r+=`Content-Disposition: attachment; filename="${o.filename}";\r
40
+ `,r+=` creation-date="${new Date().toUTCString()}";\r
41
+ `,r+=`Content-Transfer-Encoding: base64\r
42
+ \r
43
+ `;let y=o.content.match(/.{1,72}/g);y?r+=`${y.join(`\r
44
+ `)}`:r+=`${o.content}`,r+=`\r
45
+ \r
46
+ `}return r+=`--${t}--\r
47
+ `,`${this.applyDotStuffing(r)}\r
48
+ .\r
49
+ `}applyDotStuffing(e){let t=e.replace(/\r\n\./g,`\r
50
+ ..`);return t.startsWith(".")&&(t=`.${t}`),t}generateSafeBoundary(e){let t=new Uint8Array(28);crypto.getRandomValues(t);let i=Array.from(t).map(n=>n.toString(16).padStart(2,"0")).join(""),a=e+i;return a=a.replace(/[<>@,;:\\/[\]?=" ]/g,"_"),a}getMimeType(e){let t=e.split(".").pop()?.toLowerCase();return{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"}[t||"txt"]||"application/octet-stream"}resolveHeader(){this.resolveFrom(),this.resolveTo(),this.resolveReply(),this.resolveCC(),this.resolveBCC(),this.resolveSubject(),this.headers.Date=this.headers.Date??new Date().toUTCString(),this.headers["Message-ID"]=this.headers["Message-ID"]??`<${crypto.randomUUID()}@${this.from.email.split("@").pop()}>`}resolveFrom(){if(this.headers.From)return;let e=this.from.email;this.from.name&&(e=`"${g(this.from.name)}" <${e}>`),this.headers.From=e}resolveTo(){if(this.headers.To)return;let e=this.to.map(t=>t.name?`"${g(t.name)}" <${t.email}>`:t.email);this.headers.To=e.join(", ")}resolveSubject(){this.headers.Subject||this.subject&&(this.headers.Subject=g(this.subject))}resolveReply(){if(!this.headers["Reply-To"]&&this.reply){let e=this.reply.email;this.reply.name&&(e=`"${g(this.reply.name)}" <${e}>`),this.headers["Reply-To"]=e}}resolveCC(){if(!this.headers.CC&&this.cc){let e=this.cc.map(t=>t.name?`"${g(t.name)}" <${t.email}>`:t.email);this.headers.CC=e.join(", ")}}resolveBCC(){if(!this.headers.BCC&&this.bcc){let e=this.bcc.map(t=>t.name?`"${g(t.name)}" <${t.email}>`:t.email);this.headers.BCC=e.join(", ")}}};var k=(n=>(n[n.DEBUG=0]="DEBUG",n[n.INFO=1]="INFO",n[n.WARN=2]="WARN",n[n.ERROR=3]="ERROR",n[n.NONE=4]="NONE",n))(k||{}),w=class{constructor(e=1,t){this.level=e;this.prefix=t}level;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)}};import{connect as M}from"cloudflare:sockets";var I=class s{socket;host;port;secure;startTls;authType;credentials;socketTimeoutMs;responseTimeoutMs;reader;writer;logger;dsn;sendNotificationsTo;hooks;active=!1;emailSending=null;emailSendingOptions=null;emailToBeSent=new S;supportsDSN=!1;allowAuth=!1;authTypeSupported=[];supportsStartTls=!1;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.credentials=e.credentials,this.dsn=e.dsn||{},this.hooks=e.hooks||{},this.socketTimeoutMs=e.socketTimeoutMs||6e4,this.responseTimeoutMs=e.responseTimeoutMs||3e4,this.socket=M({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}]`)}static async connect(e){let t=new s(e);return await t.initializeSmtpSession(),t.start().catch(console.error),t}send(e){let t=new $(e);return this.emailToBeSent.enqueue({email:t,options:e}),t.sent}static async send(e,t){let i=await s.connect(e);await i.send(t),await i.close()}async readTimeout(){return L(this.read(),this.responseTimeoutMs,new v("Timeout while waiting for smtp server response"))}async read(){let e="";for(;;){let{value:t}=await this.reader.read();if(!t)continue;let i=U(t).toString();if(this.logger.debug(`SMTP server response:
51
+ `+i),e=e+i,!e.endsWith(`
52
+ `))continue;let a=e.split(/\r?\n/),n=a[a.length-2];if(!/^\d+-/.test(n))return e}}async writeLine(e){await this.write(`${e}\r
53
+ `)}async write(e){this.logger.debug(`Write to socket:
54
+ `+e),await this.writer.write(u(e))}async initializeSmtpSession(){await this.waitForSocketConnected(),await this.greet(),await this.ehlo(),this.startTls&&!this.secure&&this.supportsStartTls&&(await this.tls(),await this.ehlo()),await this.auth(),this.active=!0,await this.hooks.onConnect?.()}async start(){for(;this.active;){let{email:e,options:t}=await this.emailToBeSent.dequeue();this.emailSending=e,this.emailSendingOptions=t;try{await this.mail(),await this.rcpt(),await this.data();let i=await this.body();this.emailSending.setSent(),await this.hooks.onSent?.(t,i)}catch(i){if(this.logger.error("Failed to send email: "+i.message),!this.active)return;this.emailSending.setSentError(i),await this.hooks.onError?.(t,i);try{await this.rset()}catch(a){await this.close(a)}}this.emailSending=null,this.emailSendingOptions=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;){let{email:t}=await this.emailToBeSent.dequeue();t.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{}await this.hooks.onClose?.(e)}async waitForSocketConnected(){this.logger.info("Connecting to SMTP server"),await L(this.socket.opened,this.socketTimeoutMs,new v("Socket timeout!")),this.logger.info("SMTP server connected")}async greet(){let e=await this.readTimeout();if(!e.startsWith("220"))throw new C("Failed to connect to SMTP server: "+e)}async ehlo(){await this.writeLine("EHLO 127.0.0.1");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 127.0.0.1");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()}parseCapabilities(e){/[ -]AUTH\b/i.test(e)&&(this.allowAuth=!0),/[ -]AUTH(?:(\s+|=)[^\n]*\s+|\s+|=)PLAIN/i.test(e)&&this.authTypeSupported.push("plain"),/[ -]AUTH(?:(\s+|=)[^\n]*\s+|\s+|=)LOGIN/i.test(e)&&this.authTypeSupported.push("login"),/[ -]AUTH(?:(\s+|=)[^\n]*\s+|\s+|=)CRAM-MD5/i.test(e)&&this.authTypeSupported.push("cram-md5"),/[ -]STARTTLS\b/i.test(e)&&(this.supportsStartTls=!0),/[ -]DSN\b/i.test(e)&&(this.supportsDSN=!0)}async auth(){if(this.allowAuth){if(!this.credentials)throw new d("smtp server requires authentication, but no credentials found");if(this.authTypeSupported.includes("plain")&&this.authType.includes("plain"))await this.authWithPlain();else if(this.authTypeSupported.includes("login")&&this.authType.includes("login"))await this.authWithLogin();else if(this.authTypeSupported.includes("cram-md5")&&this.authType.includes("cram-md5"))await this.authWithCramMD5();else throw new d("No supported auth method found.")}}async authWithPlain(){let e=btoa(`\0${this.credentials.username}\0${this.credentials.password}`);await this.writeLine(`AUTH PLAIN ${e}`);let t=await this.readTimeout();if(!t.startsWith("2"))throw new d(`Failed to plain authentication: ${t}`)}async authWithLogin(){await this.writeLine("AUTH LOGIN");let e=await this.readTimeout();if(!e.startsWith("3"))throw new d("Invalid login: "+e);let t=btoa(this.credentials.username);await this.writeLine(t);let i=await this.readTimeout();if(!i.startsWith("3"))throw new d("Failed to login authentication: "+i);let a=btoa(this.credentials.password);await this.writeLine(a);let n=await this.readTimeout();if(!n.startsWith("2"))throw new d("Failed to login authentication: "+n)}async authWithCramMD5(){await this.writeLine("AUTH CRAM-MD5");let e=await this.readTimeout(),t=e.match(/^334\s+(.+)$/)?.pop();if(!t)throw new d("Invalid CRAM-MD5 challenge: "+e);let i=atob(t),a=u(this.credentials.password),n=await crypto.subtle.importKey("raw",a,{name:"HMAC",hash:"MD5"},!1,["sign"]),l=u(i),h=await crypto.subtle.sign("HMAC",n,l),m=Array.from(new Uint8Array(h)).map(r=>r.toString(16).padStart(2,"0")).join("");await this.writeLine(btoa(`${this.credentials.username} ${m}`));let f=await this.readTimeout();if(!f.startsWith("2"))throw new d("Failed to cram-md5 authentication: "+f)}async mail(){let e=`MAIL FROM: <${this.emailSending.from.email}>`;this.supportsDSN&&(e+=` ${this.retBuilder()}`,this.emailSending?.dsnOverride?.envelopeId&&(e+=` ENVID=${this.emailSending?.dsnOverride?.envelopeId}`)),await this.writeLine(e);let t=await this.readTimeout();if(!t.startsWith("2"))throw new Error(`Invalid ${e} ${t}`)}async rcpt(){let e=[...this.emailSending.to,...this.emailSending.cc||[],...this.emailSending.bcc||[]];for(let t of e){let i=`RCPT TO: <${t.email}>`;this.supportsDSN&&(i+=this.notificationBuilder()),await this.writeLine(i);let a=await this.readTimeout();if(!a.startsWith("2"))throw new O(`Invalid ${i} ${a}`,t.email)}}async data(){await this.writeLine("DATA");let e=await this.readTimeout();if(!e.startsWith("3"))throw new Error(`Failed to send DATA: ${e}`)}async body(){await this.write(this.emailSending.getEmailData());let e=await this.readTimeout();if(!e.startsWith("2"))throw new Error("Failed send email body: "+e);return e}async rset(){await this.writeLine("RSET");let e=await this.readTimeout();if(!e.startsWith("2"))throw new Error(`Failed to reset: ${e}`)}notificationBuilder(){let e=[];return(this.emailSending?.dsnOverride?.NOTIFY&&this.emailSending?.dsnOverride?.NOTIFY?.SUCCESS||!this.emailSending?.dsnOverride?.NOTIFY&&this.dsn?.NOTIFY?.SUCCESS)&&e.push("SUCCESS"),(this.emailSending?.dsnOverride?.NOTIFY&&this.emailSending?.dsnOverride?.NOTIFY?.FAILURE||!this.emailSending?.dsnOverride?.NOTIFY&&this.dsn?.NOTIFY?.FAILURE)&&e.push("FAILURE"),(this.emailSending?.dsnOverride?.NOTIFY&&this.emailSending?.dsnOverride?.NOTIFY?.DELAY||!this.emailSending?.dsnOverride?.NOTIFY&&this.dsn?.NOTIFY?.DELAY)&&e.push("DELAY"),e.length>0?` NOTIFY=${e.join(",")}`:" NOTIFY=NEVER"}retBuilder(){let e=[];return(this.emailSending?.dsnOverride?.RET&&this.emailSending?.dsnOverride?.RET?.HEADERS||!this.emailSending?.dsnOverride?.RET&&this.dsn?.RET?.HEADERS)&&e.push("HDRS"),(this.emailSending?.dsnOverride?.RET&&this.emailSending?.dsnOverride?.RET?.FULL||!this.emailSending?.dsnOverride?.RET&&this.dsn?.RET?.FULL)&&e.push("FULL"),e.length>0?`RET=${e.join(",")}`:""}};export{p as a,W as b,c,A as d,d as e,C as f,O as g,v as h,x as i,g as j,$ as k,k as l,I as m};