@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 +21 -0
- package/README.md +539 -0
- package/dist/chunk-TXGT6IDZ.mjs +54 -0
- package/dist/index.d.mts +62 -0
- package/dist/index.d.ts +62 -0
- package/dist/index.js +54 -0
- package/dist/index.mjs +1 -0
- package/dist/mailer-CNOSp-w6.d.mts +186 -0
- package/dist/mailer-CNOSp-w6.d.ts +186 -0
- package/dist/queue.d.mts +93 -0
- package/dist/queue.d.ts +93 -0
- package/dist/queue.js +54 -0
- package/dist/queue.mjs +1 -0
- package/package.json +60 -0
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
|
+
[](https://badge.fury.io/js/@ribassu%2Fworker-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
|
+
- �️ 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};
|