@z_06/relay-temp-mail 2.0.1 → 2.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/README.md CHANGED
@@ -4,12 +4,13 @@
4
4
 
5
5
  A modular TypeScript/JavaScript package for managing email aliases and retrieving temporary emails through pluggable providers.
6
6
 
7
- Built on a provider architecture — combine any **alias provider** with any **mail provider** to fit your workflow. Currently ships with Firefox Relay and CloudFlare Temp Mail support, with more providers coming.
7
+ Built on a provider architecture — combine any **alias provider** with any **mail provider** to fit your workflow. Currently ships with Firefox Relay, DuckDuckGo Email Protection, and CloudFlare Temp Mail support.
8
8
 
9
9
  ## Features
10
10
 
11
11
  - **Provider-based architecture** — mix and match alias + mail providers
12
12
  - **Firefox Relay** — create, list, and delete email aliases
13
+ - **DuckDuckGo Email Protection** — create email aliases with local storage
13
14
  - **CloudFlare Temp Mail** — retrieve and parse emails via API
14
15
  - **TypeScript support** — full type definitions for all APIs, including provider interfaces
15
16
  - **ESM + CommonJS support** — works with both module systems
@@ -60,7 +61,7 @@ The library uses two types of providers that can be combined independently:
60
61
 
61
62
  | Provider Type | Interface | Current Implementations |
62
63
  |---|---|---|
63
- | **Alias Provider** | `AliasProvider` | `firefox-relay` |
64
+ | **Alias Provider** | `AliasProvider` | `firefox-relay`, `duckduckgo-email` |
64
65
  | **Mail Provider** | `MailProvider` | `cf-temp-mail` |
65
66
 
66
67
  ### Alias Providers
@@ -87,6 +88,53 @@ Manages email aliases through [Firefox Relay](https://relay.firefox.com).
87
88
  4. Find Cookies for `relay.firefox.com`
88
89
  5. Copy the values for `csrftoken` and `sessionid`
89
90
 
91
+ #### `duckduckgo-email`
92
+
93
+ Manages email aliases through [DuckDuckGo Email Protection](https://duckduckgo.com/email/).
94
+
95
+ Since the DuckDuckGo API does not provide endpoints for listing or deleting aliases, this provider uses a local store. A default in-memory store is included; implement `DuckDuckGoAliasStore` for custom persistence (e.g., file-based, database).
96
+
97
+ **Configuration:**
98
+
99
+ ```typescript
100
+ {
101
+ type: 'duckduckgo-email',
102
+ jwtToken: string; // JWT token from DuckDuckGo Email Protection
103
+ store?: DuckDuckGoAliasStore; // Optional custom store (default: in-memory)
104
+ }
105
+ ```
106
+
107
+ **Getting your JWT token:**
108
+
109
+ 1. Visit [duckduckgo.com/email](https://duckduckgo.com/email/) and register an account
110
+ 2. Open your browser's developer tools (F12)
111
+ 3. Click "Generate New Address" in the DuckDuckGo Email UI
112
+ 4. In the Network tab, find the request to `quack.duckduckgo.com`
113
+ 5. Copy the Bearer token from the `Authorization` header
114
+
115
+ **Custom persistence:**
116
+
117
+ ```typescript
118
+ import type { DuckDuckGoAliasStore, RelayAlias } from '@z_06/relay-temp-mail';
119
+
120
+ class MyFileStore implements DuckDuckGoAliasStore {
121
+ getAll(): RelayAlias[] { /* read from file */ }
122
+ add(alias: RelayAlias): void { /* append to file */ }
123
+ remove(id: number): void { /* remove from file */ }
124
+ }
125
+
126
+ const client = new TempMailClient({
127
+ aliasProvider: {
128
+ type: 'duckduckgo-email',
129
+ jwtToken: 'your-jwt-token',
130
+ store: new MyFileStore(),
131
+ },
132
+ mailProvider: { /* ... */ },
133
+ });
134
+ ```
135
+
136
+ **Duplicate detection:** The DuckDuckGo API may occasionally return a previously-generated address with a 201 response. The provider detects this and throws a `RelayTempMailError` with code `DUPLICATE_ALIAS`.
137
+
90
138
  ### Mail Providers
91
139
 
92
140
  #### `cf-temp-mail`
@@ -255,6 +303,8 @@ import type {
255
303
  MailProvider,
256
304
  TempMailConfig,
257
305
  FirefoxRelayConfig,
306
+ DuckDuckGoEmailConfig,
307
+ DuckDuckGoAliasStore,
258
308
  CFTempMailConfig,
259
309
  RelayAlias,
260
310
  Email,
package/README.zh-CN.md CHANGED
@@ -4,12 +4,13 @@
4
4
 
5
5
  一个模块化的 TypeScript/JavaScript 包,通过可插拔的 Provider 管理邮箱别名和接收临时邮件。
6
6
 
7
- 基于 Provider 架构 — 自由组合**别名提供商**与**邮件提供商**。当前内置 Firefox Relay CloudFlare 临时邮箱支持,更多提供商即将推出。
7
+ 基于 Provider 架构 — 自由组合**别名提供商**与**邮件提供商**。当前内置 Firefox Relay、DuckDuckGo 邮件保护和 CloudFlare 临时邮箱支持。
8
8
 
9
9
  ## 功能特性
10
10
 
11
11
  - **Provider 架构** — 自由组合别名与邮件提供商
12
12
  - **Firefox Relay** — 创建、列出、删除邮箱别名
13
+ - **DuckDuckGo 邮件保护** — 创建邮箱别名,支持本地存储
13
14
  - **CloudFlare 临时邮箱** — 通过 API 获取并解析邮件
14
15
  - **TypeScript 支持** — 所有 API 均有完整类型定义,包括 Provider 接口
15
16
  - **ESM + CommonJS 支持** — 兼容两种模块系统
@@ -60,7 +61,7 @@ const emails = await client.getEmails(alias.fullAddress, { limit: 10 });
60
61
 
61
62
  | Provider 类型 | 接口 | 当前实现 |
62
63
  |---|---|---|
63
- | **别名提供商** | `AliasProvider` | `firefox-relay` |
64
+ | **别名提供商** | `AliasProvider` | `firefox-relay`, `duckduckgo-email` |
64
65
  | **邮件提供商** | `MailProvider` | `cf-temp-mail` |
65
66
 
66
67
  ### 别名提供商
@@ -87,6 +88,53 @@ const emails = await client.getEmails(alias.fullAddress, { limit: 10 });
87
88
  4. 找到 `relay.firefox.com` 的 Cookies
88
89
  5. 复制 `csrftoken` 和 `sessionid` 的值
89
90
 
91
+ #### `duckduckgo-email`
92
+
93
+ 通过 [DuckDuckGo 邮件保护](https://duckduckgo.com/email/) 管理邮箱别名。
94
+
95
+ 由于 DuckDuckGo API 不提供列出或删除别名的接口,该提供商使用本地存储。内置内存存储;可通过实现 `DuckDuckGoAliasStore` 接口自定义持久化(如文件、数据库)。
96
+
97
+ **配置:**
98
+
99
+ ```typescript
100
+ {
101
+ type: 'duckduckgo-email',
102
+ jwtToken: string; // DuckDuckGo 邮件保护的 JWT token
103
+ store?: DuckDuckGoAliasStore; // 可选自定义存储(默认: 内存存储)
104
+ }
105
+ ```
106
+
107
+ **获取 JWT Token:**
108
+
109
+ 1. 访问 [duckduckgo.com/email](https://duckduckgo.com/email/) 并注册账户
110
+ 2. 打开浏览器开发者工具(F12)
111
+ 3. 在 DuckDuckGo 邮件界面点击"生成新地址"
112
+ 4. 在网络请求栏中找到发往 `quack.duckduckgo.com` 的请求
113
+ 5. 从 `Authorization` 请求头中复制 Bearer token
114
+
115
+ **自定义持久化:**
116
+
117
+ ```typescript
118
+ import type { DuckDuckGoAliasStore, RelayAlias } from '@z_06/relay-temp-mail';
119
+
120
+ class MyFileStore implements DuckDuckGoAliasStore {
121
+ getAll(): RelayAlias[] { /* 从文件读取 */ }
122
+ add(alias: RelayAlias): void { /* 追加到文件 */ }
123
+ remove(id: number): void { /* 从文件中删除 */ }
124
+ }
125
+
126
+ const client = new TempMailClient({
127
+ aliasProvider: {
128
+ type: 'duckduckgo-email',
129
+ jwtToken: 'your-jwt-token',
130
+ store: new MyFileStore(),
131
+ },
132
+ mailProvider: { /* ... */ },
133
+ });
134
+ ```
135
+
136
+ **重复检测:** DuckDuckGo API 偶尔会返回之前已生成过的地址(仍返回 201)。该提供商会检测这种情况并抛出 `RelayTempMailError`,错误代码为 `DUPLICATE_ALIAS`。
137
+
90
138
  ### 邮件提供商
91
139
 
92
140
  #### `cf-temp-mail`
@@ -255,6 +303,8 @@ import type {
255
303
  MailProvider,
256
304
  TempMailConfig,
257
305
  FirefoxRelayConfig,
306
+ DuckDuckGoEmailConfig,
307
+ DuckDuckGoAliasStore,
258
308
  CFTempMailConfig,
259
309
  RelayAlias,
260
310
  Email,
package/dist/index.cjs CHANGED
@@ -23,7 +23,9 @@ __export(index_exports, {
23
23
  AuthError: () => AuthError,
24
24
  CFEmailClient: () => CFEmailClient,
25
25
  CFTempMailProvider: () => CFTempMailProvider,
26
+ DuckDuckGoEmailProvider: () => DuckDuckGoEmailProvider,
26
27
  FirefoxRelayProvider: () => FirefoxRelayProvider,
28
+ InMemoryDuckDuckGoAliasStore: () => InMemoryDuckDuckGoAliasStore,
27
29
  NetworkError: () => NetworkError,
28
30
  NotFoundError: () => NotFoundError,
29
31
  ParseError: () => ParseError,
@@ -167,114 +169,6 @@ var DefaultHttpClient = class {
167
169
  }
168
170
  };
169
171
 
170
- // src/parser.ts
171
- var EMAIL_PATTERN = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g;
172
- var MOZMAIL_SUFFIX_PATTERN = /@mozmail\.com$/i;
173
- var ENCODED_WORD_PATTERN = /=\?([^?]+)\?([BbQq])\?([^?]*)\?=/g;
174
- var EmailParser = class {
175
- parseEmail(raw) {
176
- const headers = this.parseHeaders(raw);
177
- const toHeader = headers.get("to") || "";
178
- const fromHeader = headers.get("from") || "";
179
- const messageIdHeader = headers.get("message-id") || "";
180
- const relayAlias = this.extractRelayAlias(raw);
181
- return {
182
- id: 0,
183
- messageId: this.extractMessageId(messageIdHeader),
184
- source: this.extractEmailAddress(fromHeader),
185
- address: this.extractEmailAddress(toHeader),
186
- raw,
187
- metadata: null,
188
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
189
- relayAlias: relayAlias || void 0
190
- };
191
- }
192
- extractRelayAlias(raw) {
193
- try {
194
- const headers = this.parseHeaders(raw);
195
- const fromAddresses = this.extractHeaderEmails(headers.get("from"));
196
- const toAddresses = this.extractHeaderEmails(headers.get("to"));
197
- const allAddresses = [...fromAddresses, ...toAddresses];
198
- const mozmailAddress = allAddresses.find(
199
- (address) => MOZMAIL_SUFFIX_PATTERN.test(address)
200
- );
201
- if (mozmailAddress) return mozmailAddress;
202
- return toAddresses[0] ?? null;
203
- } catch {
204
- return null;
205
- }
206
- }
207
- parseHeaders(raw) {
208
- const headers = /* @__PURE__ */ new Map();
209
- const headerEnd = raw.indexOf("\r\n\r\n");
210
- const headerSection = headerEnd === -1 ? raw : raw.substring(0, headerEnd);
211
- const lines = headerSection.split(/\r?\n/);
212
- let currentHeader = null;
213
- let currentValue = "";
214
- for (const line of lines) {
215
- if (/^\s/.test(line)) {
216
- if (currentHeader) currentValue += " " + line.trim();
217
- } else {
218
- if (currentHeader) headers.set(currentHeader, currentValue);
219
- const colonIndex = line.indexOf(":");
220
- if (colonIndex > 0) {
221
- currentHeader = line.substring(0, colonIndex).toLowerCase().trim();
222
- currentValue = line.substring(colonIndex + 1).trim();
223
- } else {
224
- currentHeader = null;
225
- currentValue = "";
226
- }
227
- }
228
- }
229
- if (currentHeader) headers.set(currentHeader, currentValue);
230
- return headers;
231
- }
232
- decodeHeader(value) {
233
- return value.replace(ENCODED_WORD_PATTERN, (_, charset, encoding, encoded) => {
234
- try {
235
- if (encoding.toUpperCase() === "Q") {
236
- return this.decodeQuotedPrintable(encoded);
237
- } else if (encoding.toUpperCase() === "B") {
238
- return this.decodeBase64(encoded);
239
- }
240
- return encoded;
241
- } catch {
242
- return encoded;
243
- }
244
- });
245
- }
246
- decodeQuotedPrintable(encoded) {
247
- let decoded = encoded.replace(/_/g, " ");
248
- decoded = decoded.replace(
249
- /=([0-9A-Fa-f]{2})/g,
250
- (_, hex) => String.fromCharCode(parseInt(hex, 16))
251
- );
252
- return decoded;
253
- }
254
- decodeBase64(encoded) {
255
- return Buffer.from(encoded, "base64").toString("utf-8");
256
- }
257
- extractEmailAddress(headerValue) {
258
- if (!headerValue) return "";
259
- const decoded = this.decodeHeader(headerValue);
260
- const bracketMatch = decoded.match(/<([^>]+)>/);
261
- if (bracketMatch) return bracketMatch[1].trim().toLowerCase();
262
- const emailMatch = decoded.match(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/);
263
- return emailMatch ? emailMatch[0].toLowerCase() : decoded.trim().toLowerCase();
264
- }
265
- extractHeaderEmails(headerValue) {
266
- if (!headerValue) return [];
267
- const decoded = this.decodeHeader(headerValue);
268
- const matches = decoded.match(EMAIL_PATTERN);
269
- return matches ? matches.map((match) => match.toLowerCase()) : [];
270
- }
271
- extractMessageId(headerValue) {
272
- if (!headerValue) return `<generated-${Date.now()}@relay-temp-mail>`;
273
- const cleaned = headerValue.replace(/[<>]/g, "").trim();
274
- return `<${cleaned}>`;
275
- }
276
- };
277
-
278
172
  // src/http.ts
279
173
  var HttpClient = class {
280
174
  /**
@@ -431,6 +325,187 @@ var HttpClient = class {
431
325
  }
432
326
  };
433
327
 
328
+ // src/duckduckgo-api.ts
329
+ var InMemoryDuckDuckGoAliasStore = class {
330
+ constructor() {
331
+ this.aliases = [];
332
+ }
333
+ getAll() {
334
+ return [...this.aliases];
335
+ }
336
+ add(alias) {
337
+ this.aliases.push(alias);
338
+ }
339
+ remove(id) {
340
+ this.aliases = this.aliases.filter((a) => a.id !== id);
341
+ }
342
+ };
343
+ var DuckDuckGoEmailProvider = class {
344
+ constructor(jwtToken, store, httpClient) {
345
+ this.jwtToken = jwtToken;
346
+ this.store = store ?? new InMemoryDuckDuckGoAliasStore();
347
+ this.httpClient = httpClient ?? new HttpClient("https://quack.duckduckgo.com");
348
+ this.nextId = 1;
349
+ }
350
+ async listAliases() {
351
+ return this.store.getAll();
352
+ }
353
+ async createAlias() {
354
+ const response = await this.httpClient.request(
355
+ "POST",
356
+ "/api/email/addresses",
357
+ {
358
+ headers: {
359
+ Authorization: `Bearer ${this.jwtToken}`
360
+ }
361
+ }
362
+ );
363
+ const fullAddress = `${response.address}@duck.com`;
364
+ const existing = await this.store.getAll();
365
+ if (existing.some((a) => a.fullAddress === fullAddress)) {
366
+ throw new RelayTempMailError(
367
+ `DuckDuckGo returned a duplicate alias: ${fullAddress}`,
368
+ "DUPLICATE_ALIAS",
369
+ 201
370
+ );
371
+ }
372
+ const alias = {
373
+ id: this.nextId++,
374
+ address: response.address,
375
+ fullAddress,
376
+ enabled: true,
377
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
378
+ domain: 3,
379
+ maskType: "random"
380
+ };
381
+ await this.store.add(alias);
382
+ return alias;
383
+ }
384
+ async deleteAlias(id) {
385
+ await this.store.remove(id);
386
+ }
387
+ };
388
+
389
+ // src/parser.ts
390
+ var EMAIL_PATTERN = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g;
391
+ var MOZMAIL_SUFFIX_PATTERN = /@mozmail\.com$/i;
392
+ var DUCK_SUFFIX_PATTERN = /@duck\.com$/i;
393
+ var ENCODED_WORD_PATTERN = /=\?([^?]+)\?([BbQq])\?([^?]*)\?=/g;
394
+ var EmailParser = class {
395
+ parseEmail(raw) {
396
+ const headers = this.parseHeaders(raw);
397
+ const toHeader = headers.get("to") || "";
398
+ const fromHeader = headers.get("from") || "";
399
+ const messageIdHeader = headers.get("message-id") || "";
400
+ const relayAlias = this.extractRelayAlias(raw);
401
+ return {
402
+ id: 0,
403
+ messageId: this.extractMessageId(messageIdHeader),
404
+ source: this.extractEmailAddress(fromHeader),
405
+ address: this.extractEmailAddress(toHeader),
406
+ raw,
407
+ metadata: null,
408
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
409
+ relayAlias: relayAlias || void 0
410
+ };
411
+ }
412
+ extractRelayAlias(raw) {
413
+ try {
414
+ const headers = this.parseHeaders(raw);
415
+ const duckOriginalTo = headers.get("duck-original-to");
416
+ if (duckOriginalTo) {
417
+ const duckAddress2 = this.extractEmailAddress(duckOriginalTo);
418
+ if (duckAddress2 && DUCK_SUFFIX_PATTERN.test(duckAddress2)) {
419
+ return duckAddress2;
420
+ }
421
+ }
422
+ const fromAddresses = this.extractHeaderEmails(headers.get("from"));
423
+ const toAddresses = this.extractHeaderEmails(headers.get("to"));
424
+ const allAddresses = [...fromAddresses, ...toAddresses];
425
+ const mozmailAddress = allAddresses.find(
426
+ (address) => MOZMAIL_SUFFIX_PATTERN.test(address)
427
+ );
428
+ if (mozmailAddress) return mozmailAddress;
429
+ const duckAddress = allAddresses.find(
430
+ (address) => DUCK_SUFFIX_PATTERN.test(address)
431
+ );
432
+ if (duckAddress) return duckAddress;
433
+ return toAddresses[0] ?? null;
434
+ } catch {
435
+ return null;
436
+ }
437
+ }
438
+ parseHeaders(raw) {
439
+ const headers = /* @__PURE__ */ new Map();
440
+ const headerEnd = raw.indexOf("\r\n\r\n");
441
+ const headerSection = headerEnd === -1 ? raw : raw.substring(0, headerEnd);
442
+ const lines = headerSection.split(/\r?\n/);
443
+ let currentHeader = null;
444
+ let currentValue = "";
445
+ for (const line of lines) {
446
+ if (/^\s/.test(line)) {
447
+ if (currentHeader) currentValue += " " + line.trim();
448
+ } else {
449
+ if (currentHeader) headers.set(currentHeader, currentValue);
450
+ const colonIndex = line.indexOf(":");
451
+ if (colonIndex > 0) {
452
+ currentHeader = line.substring(0, colonIndex).toLowerCase().trim();
453
+ currentValue = line.substring(colonIndex + 1).trim();
454
+ } else {
455
+ currentHeader = null;
456
+ currentValue = "";
457
+ }
458
+ }
459
+ }
460
+ if (currentHeader) headers.set(currentHeader, currentValue);
461
+ return headers;
462
+ }
463
+ decodeHeader(value) {
464
+ return value.replace(ENCODED_WORD_PATTERN, (_, charset, encoding, encoded) => {
465
+ try {
466
+ if (encoding.toUpperCase() === "Q") {
467
+ return this.decodeQuotedPrintable(encoded);
468
+ } else if (encoding.toUpperCase() === "B") {
469
+ return this.decodeBase64(encoded);
470
+ }
471
+ return encoded;
472
+ } catch {
473
+ return encoded;
474
+ }
475
+ });
476
+ }
477
+ decodeQuotedPrintable(encoded) {
478
+ let decoded = encoded.replace(/_/g, " ");
479
+ decoded = decoded.replace(
480
+ /=([0-9A-Fa-f]{2})/g,
481
+ (_, hex) => String.fromCharCode(parseInt(hex, 16))
482
+ );
483
+ return decoded;
484
+ }
485
+ decodeBase64(encoded) {
486
+ return Buffer.from(encoded, "base64").toString("utf-8");
487
+ }
488
+ extractEmailAddress(headerValue) {
489
+ if (!headerValue) return "";
490
+ const decoded = this.decodeHeader(headerValue);
491
+ const bracketMatch = decoded.match(/<([^>]+)>/);
492
+ if (bracketMatch) return bracketMatch[1].trim().toLowerCase();
493
+ const emailMatch = decoded.match(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/);
494
+ return emailMatch ? emailMatch[0].toLowerCase() : decoded.trim().toLowerCase();
495
+ }
496
+ extractHeaderEmails(headerValue) {
497
+ if (!headerValue) return [];
498
+ const decoded = this.decodeHeader(headerValue);
499
+ const matches = decoded.match(EMAIL_PATTERN);
500
+ return matches ? matches.map((match) => match.toLowerCase()) : [];
501
+ }
502
+ extractMessageId(headerValue) {
503
+ if (!headerValue) return `<generated-${Date.now()}@relay-temp-mail>`;
504
+ const cleaned = headerValue.replace(/[<>]/g, "").trim();
505
+ return `<${cleaned}>`;
506
+ }
507
+ };
508
+
434
509
  // src/relay-api.ts
435
510
  var FirefoxRelayProvider = class {
436
511
  constructor(csrfToken, sessionId, httpClient) {
@@ -508,6 +583,11 @@ function createAliasProvider(config, httpClient) {
508
583
  aliasConfig.sessionId,
509
584
  httpClient
510
585
  );
586
+ case "duckduckgo-email":
587
+ return new DuckDuckGoEmailProvider(
588
+ aliasConfig.jwtToken,
589
+ aliasConfig.store
590
+ );
511
591
  }
512
592
  }
513
593
  function createMailProvider(config) {
@@ -568,7 +648,9 @@ var RelayClient = TempMailClient;
568
648
  AuthError,
569
649
  CFEmailClient,
570
650
  CFTempMailProvider,
651
+ DuckDuckGoEmailProvider,
571
652
  FirefoxRelayProvider,
653
+ InMemoryDuckDuckGoAliasStore,
572
654
  NetworkError,
573
655
  NotFoundError,
574
656
  ParseError,