Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

- Node.js 20.18+
- 可用代理,默认读取 `config.json.defaultProxyUrl`
- 一个可用邮箱 provider:`cloudflare`、`gmail` 或 `hotmail`
- 一个可用邮箱 provider:`cloudflare`、`gmail`、`hotmail` 或 `mailnest`

## 快速开始

Expand Down Expand Up @@ -124,6 +124,11 @@ Cloudflare:
- `cloudflareApiBaseUrl`:邮件 Worker 地址。
- `cloudflareApiKey`:邮件 Worker 的 `x-api-key`。

MailNest:

- `mailNestApiKey`:Outlook 邮箱提供商迈巢的`api-key`,获取页面:https://mailnest.top/account。
- `mailNestProjectCode`:迈巢提供临时与独占两种 Outlook 邮箱。填写该值,即项目代码,则使用对应项目的临时邮箱,不填则使用独占邮箱。项目代码获取页面:https://mailnest.top/buy-email。Codex 的项目代码默认为`chatgpt001`,可直接使用。

CLIProxyAPI 自动上传:

- `cliproxyApiAutoUploadAuth`:授权成功后是否自动上传 auth 文件。
Expand Down Expand Up @@ -175,6 +180,20 @@ CLIProxyAPI 自动上传:

程序会随机取一个账号生成别名邮箱,刷新 token,并读取收件箱和垃圾箱里的验证码邮件。刷新后的 `refresh_token` 会回写到 `tokens.txt`。

### MailNest

Outlook 邮箱提供商迈巢,提供临时与独占两种 Outlook 邮箱,配置便捷,开箱即用。

```json
{
"provider": "mailnest",
"mailNestApiKey": "",
"mailNestProjectCode": "chatgpt001"
}
```

字段含义已在**配置项**章节中阐述。

## 授权文件

Codex OAuth 授权文件会保存到 `auth/`,文件名格式为:
Expand Down
2 changes: 2 additions & 0 deletions config.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
"cloudflareEmailDomain": "",
"cloudflareApiBaseUrl": "",
"cloudflareApiKey": "",
"mailNestApiKey": "",
"mailNestProjectCode": "chatgpt001",
"cliproxyApiAutoUploadAuth": false,
"cliproxyApiBaseUrl": "http://localhost:8317",
"cliproxyApiManagementKey": ""
Expand Down
18 changes: 16 additions & 2 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {readFileSync} from "node:fs";
import path from "node:path";

export type MailProviderName = "gmail" | "hotmail" | "cloudflare";
export type MailProviderName = "gmail" | "hotmail" | "cloudflare" | "mailnest";

interface AppConfigFile {
provider?: unknown;
Expand All @@ -12,6 +12,8 @@ interface AppConfigFile {
cloudflareEmailDomain?: unknown;
cloudflareApiBaseUrl?: unknown;
cloudflareApiKey?: unknown;
mailNestApiKey?: unknown,
mailNestProjectCode?: unknown,
defaultProxyUrl?: unknown;
cliproxyApiAutoUploadAuth?: unknown;
cliproxyApiBaseUrl?: unknown;
Expand All @@ -27,6 +29,8 @@ export interface AppConfig {
cloudflareEmailDomain: string;
cloudflareApiBaseUrl: string;
cloudflareApiKey: string;
mailNestApiKey: string;
mailNestProjectCode: string;
defaultProxyUrl: string;
cliproxyApiAutoUploadAuth: boolean;
cliproxyApiBaseUrl: string;
Expand All @@ -43,6 +47,8 @@ const DEFAULT_CONFIG: AppConfig = {
cloudflareApiBaseUrl: "",
cloudflareApiKey: "",
defaultProxyUrl: "http://127.0.0.1:10808",
mailNestApiKey: "",
mailNestProjectCode: "",
cliproxyApiAutoUploadAuth: false,
cliproxyApiBaseUrl: "http://localhost:8317",
cliproxyApiManagementKey: "",
Expand All @@ -56,7 +62,7 @@ function normalizeNumber(value: unknown, fallback: number): number {
}

function normalizeProvider(value: unknown): MailProviderName {
if (value === "gmail" || value === "hotmail" || value === "cloudflare") {
if (value === "gmail" || value === "hotmail" || value === "cloudflare" || value === "mailnest") {
return value;
}
return DEFAULT_CONFIG.provider;
Expand Down Expand Up @@ -115,6 +121,14 @@ function loadConfig(): AppConfig {
typeof parsed.cloudflareApiKey === "string"
? parsed.cloudflareApiKey.trim()
: DEFAULT_CONFIG.cloudflareApiKey,
mailNestApiKey:
typeof parsed.mailNestApiKey === "string"
? parsed.mailNestApiKey.trim()
: DEFAULT_CONFIG.mailNestApiKey,
mailNestProjectCode:
typeof parsed.mailNestProjectCode === "string"
? parsed.mailNestProjectCode.trim()
: DEFAULT_CONFIG.mailNestProjectCode,
defaultProxyUrl:
typeof parsed.defaultProxyUrl === "string"
? parsed.defaultProxyUrl.trim()
Expand Down
141 changes: 141 additions & 0 deletions src/mail/mailnest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import {appConfig} from "../config.js";
import {Agent, Dispatcher, ProxyAgent, fetch as undiciFetch, type RequestInit as UndiciRequestInit} from "undici";


interface MailNestMailItem {
email: string;
code_match: string;
}

interface MailNestItems {
code?: string;
data: MailNestMailItem[];
}

const MAILNEST_POLL_ATTEMPTS = 12;
const MAILNEST_POLL_INTERVAL_MS = 5000;

function buildDispatcher(): Dispatcher {
const proxyUrl = String(appConfig.defaultProxyUrl ?? "").trim();
return proxyUrl
? new ProxyAgent({
uri: proxyUrl,
requestTls: {rejectUnauthorized: false},
})
: new Agent({
connect: {rejectUnauthorized: false},
});
}

function ensureApiKeyConfigured(): string {
const apiKey = String(appConfig.mailNestApiKey ?? "").trim();
if (!apiKey) {
throw new Error("MailNestApiKey 未配置,请先在 https://mailnest.top/account 页面获取");
}
return apiKey;
}

function projectCodeApiKeyConfigured(): string {
return String(appConfig.mailNestProjectCode ?? "").trim();
}


async function mailNestFetch(input: string | URL, init = {}) {
return undiciFetch(input, {
...init,
dispatcher: buildDispatcher(),
} satisfies UndiciRequestInit);
}


async function fetchEmail(): Promise<string> {
const apiKey = ensureApiKeyConfigured();
const code = projectCodeApiKeyConfigured();
let response;
if (code.length === 0) {
console.log('未配置项目代码 购买独占邮箱')
response = await mailNestFetch(new URL(`https://mailnest.top/api/v1/email/exclusive/buy`), {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
Accept: "application/json",
},
body: JSON.stringify({
count: 1,
})
});
} else {
console.log('配置项目代码 购买临时邮箱')
response = await mailNestFetch(new URL(`https://mailnest.top/api/v1/email/temporary/buy`), {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
Accept: "application/json",
},
body: JSON.stringify({
project_code: code,
count: 1,
})
});
}
const payload = await response.json() as MailNestItems;
console.log(payload);
if (payload.code != '00000') {
throw new Error(`mailNest 邮箱请求失败: ${response.status} body=${await response.text()}`);
}

if (!Array.isArray(payload?.data)) {
throw new Error(`mailNest 邮箱返回格式异常: ${JSON.stringify(payload)}`);
}
if (payload.data.length == 0) {
throw new Error(`mailNest 没有获取到邮箱: ${JSON.stringify(payload)}`);
}
return payload.data[0].email;
}


async function receive(email: string): Promise<string> {
const apiKey = ensureApiKeyConfigured();
const response = await mailNestFetch(new URL(` https://mailnest.top/api/v1/email/receive`), {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
Accept: "application/json",
},
body: JSON.stringify({
email: email,
})
});
const payload = await response.json() as MailNestItems;
console.log(payload)
if (payload.code != '00000') {
throw new Error(`mailNest 邮箱请求失败: ${response.status} body=${await response.text()}`);
}
if (payload.data.length == 0) {
return ''
}
return payload.data[0].code_match;
}


export function createMailNestProvider() {
return {
async getEmailAddress() {
return fetchEmail()
},
async getEmailVerificationCode(email: string) {
await new Promise((resolve) => setTimeout(resolve, MAILNEST_POLL_INTERVAL_MS));
ensureApiKeyConfigured();
for (let attempt = 1; attempt <= MAILNEST_POLL_ATTEMPTS; attempt += 1) {
const code = await receive(email);
if (code.length > 0) {
return code;
}
if (attempt < MAILNEST_POLL_ATTEMPTS) {
await new Promise((resolve) => setTimeout(resolve, MAILNEST_POLL_INTERVAL_MS));
}
}
throw new Error(`mailNest 邮箱中未找到验证码: targetEmail=${email}`);
},
};
}
3 changes: 3 additions & 0 deletions src/mailbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {appConfig, type MailProviderName} from "./config.js";
import {createCloudflareProvider} from "./mail/cloudflare.js";
import {createGmailProvider} from "./mail/gmail.js";
import {createHotmailProvider} from "./mail/hotmail.js";
import {createMailNestProvider} from "./mail/mailnest.js";

export interface EmailCodeProvider {
getEmailAddress(): Promise<string>;
Expand All @@ -22,6 +23,8 @@ function createProvider(): EmailCodeProvider {
return createHotmailProvider();
case "cloudflare":
return createCloudflareProvider();
case "mailnest":
return createMailNestProvider();
default:
throw new Error(`不支持的邮箱 provider: ${MAILBOX_CONFIG.provider}`);
}
Expand Down