From 37907dd2f5094cff6355f5b25a22f2548b26a044 Mon Sep 17 00:00:00 2001 From: zeer Date: Sat, 25 Apr 2026 20:51:08 +0800 Subject: [PATCH] Initial commit for mail-sr --- .dockerignore | 9 + .env.example | 2 + .gitignore | 9 + Dockerfile | 18 + README.md | 335 ++++++++ db.js | 93 +++ docker-compose.yml | 21 + mailService.js | 227 ++++++ package-lock.json | 1816 ++++++++++++++++++++++++++++++++++++++++++++ package.json | 23 + public/app.js | 481 ++++++++++++ public/index.html | 219 ++++++ public/styles.css | 780 +++++++++++++++++++ server.js | 595 +++++++++++++++ 14 files changed, 4628 insertions(+) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 db.js create mode 100644 docker-compose.yml create mode 100644 mailService.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 public/app.js create mode 100644 public/index.html create mode 100644 public/styles.css create mode 100644 server.js diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..32baa3e --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +node_modules +npm-debug.log +.git +.gitignore +.DS_Store +mail.db +mail.db-shm +mail.db-wal +.env diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..077bb54 --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +PORT=3000 +DB_PATH=./mail.db diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f4cf1a2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +node_modules/ +.env +.DS_Store +mail.db +mail.db-shm +mail.db-wal +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2d5c0c4 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +FROM node:20-bookworm-slim + +WORKDIR /app + +COPY package.json package-lock.json ./ +RUN npm ci --omit=dev + +COPY . . + +RUN mkdir -p /data + +ENV NODE_ENV=production +ENV PORT=3000 +ENV DB_PATH=/data/mail.db + +EXPOSE 3000 + +CMD ["npm", "start"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..718e0aa --- /dev/null +++ b/README.md @@ -0,0 +1,335 @@ +# 多邮箱帐号收发系统 + +一个基于 Node.js + Express + SQLite 的多邮箱工作台,支持在网页中统一管理多个邮箱渠道与邮箱帐号,进行收信、查看邮件、发信,以及通过 API 调用邮件能力。 + +## 功能特性 + +- 支持配置多个邮箱渠道,如 QQ 邮箱、163 邮箱和自定义 IMAP/SMTP 渠道 +- 支持导入邮箱帐号与授权码 +- 三栏式 Web 界面:邮箱帐号、邮件列表、邮件详情 +- 支持网页写邮件、发邮件、查看邮件详情 +- 支持拉取多文件夹邮件,不限于 `INBOX` +- 支持按需补拉单封邮件正文,避免全量同步过慢 +- 支持批量导入导出邮箱信息 +- 提供 API 用于发邮件、获取邮件列表、获取最新一封邮件 + +## 技术栈 + +- Node.js +- Express +- SQLite `better-sqlite3` +- SMTP `nodemailer` +- IMAP `imapflow` +- 前端:原生 HTML / CSS / JavaScript + +## 目录结构 + +```text +mail-sr/ +├── public/ +│ ├── index.html +│ ├── styles.css +│ └── app.js +├── db.js +├── mailService.js +├── server.js +├── Dockerfile +├── docker-compose.yml +└── README.md +``` + +## 本地启动 + +### 1. 安装依赖 + +```bash +npm install +``` + +### 2. 配置环境变量 + +```bash +cp .env.example .env +``` + +默认环境变量: + +```env +PORT=3000 +DB_PATH=./mail.db +``` + +### 3. 启动服务 + +```bash +npm start +``` + +访问地址:`http://localhost:3000` + +## Docker 部署 + +项目已经提供 `Dockerfile` 和 `docker-compose.yml`,可以直接用 Docker Compose 部署。 + +### 1. 准备环境变量 + +```bash +cp .env.example .env +``` + +如果你不需要额外环境变量,默认配置即可。 + +### 2. 构建并启动 + +```bash +docker compose up -d --build +``` + +启动后访问:`http://localhost:3000` + +### 3. 停止服务 + +```bash +docker compose down +``` + +### 4. 查看日志 + +```bash +docker compose logs -f +``` + +### 5. 数据持久化 + +Docker Compose 默认使用命名卷 `mail-sr-data` 持久化 SQLite 数据库,容器重建后数据不会丢失。 + +容器内数据库路径: + +```text +/data/mail.db +``` + +## 内置邮箱渠道 + +系统默认会初始化以下渠道: + +- QQ邮箱 + - IMAP: `imap.qq.com:993` + - SMTP: `smtp.qq.com:465` +- 163邮箱 + - IMAP: `imap.163.com:993` + - SMTP: `smtp.163.com:465` + +说明:通常需要在邮箱后台开启 IMAP/SMTP,并使用授权码,而不是邮箱登录密码。 + +## Web 使用说明 + +### 1. 渠道管理 + +在设置弹窗中可以新增渠道,填写: + +- 渠道名称 +- 渠道标识 code +- IMAP 主机、端口、是否 SSL +- SMTP 主机、端口、是否 SSL + +### 2. 单个邮箱帐号导入 + +在设置弹窗中填写: + +- 渠道 +- 邮箱帐号 +- 授权码 +- 发件人名称,可选 + +### 3. 批量导入导出 + +支持以下格式,每行一条: + +```text +渠道名----帐号----授权码 +``` + +例如: + +```text +QQ邮箱----demo@qq.com----abcd1234 +163邮箱----demo@163.com----efgh5678 +``` + +说明: + +- 导入时按“渠道名”匹配已有渠道 +- 如果渠道不存在,会直接报错 +- 如果同一渠道下邮箱已存在,则更新授权码 +- 导出会导出全部帐号,格式同上 + +### 4. 收信 + +点击左侧邮箱帐号后会: + +- 复制邮箱号到剪贴板 +- 立即触发收信 +- 拉取全部可读文件夹中的最近邮件 +- 在中间栏展示邮件列表 +- 在右侧显示邮件详情 + +为了避免 IMAP 全量拉取过慢,系统使用“两阶段策略”: + +- 先同步邮件列表与头信息 +- 打开某封邮件时再按需拉取正文详情 + +### 5. 发信 + +点击左上角“写邮件”按钮,选择当前邮箱帐号后即可发信。 + +## API 文档 + +### 健康检查 + +```bash +curl http://localhost:3000/api/health +``` + +### 查询渠道 + +```bash +curl http://localhost:3000/api/channels +``` + +### 新增渠道 + +```bash +curl -X POST http://localhost:3000/api/channels \ + -H 'Content-Type: application/json' \ + -d '{ + "name": "企业邮箱", + "code": "corp", + "imap_host": "imap.example.com", + "imap_port": 993, + "imap_secure": true, + "smtp_host": "smtp.example.com", + "smtp_port": 465, + "smtp_secure": true + }' +``` + +### 查询帐号列表 + +```bash +curl http://localhost:3000/api/accounts +``` + +### 新增邮箱帐号 + +```bash +curl -X POST http://localhost:3000/api/accounts \ + -H 'Content-Type: application/json' \ + -d '{ + "channel_id": 1, + "email": "your-account@qq.com", + "auth_code": "邮箱授权码", + "display_name": "业务邮箱" + }' +``` + +### 删除邮箱帐号 + +```bash +curl -X DELETE http://localhost:3000/api/accounts/1 +``` + +### 批量导出帐号 + +```bash +curl http://localhost:3000/api/accounts/export +``` + +### 批量导入帐号 + +```bash +curl -X POST http://localhost:3000/api/accounts/import \ + -H 'Content-Type: application/json' \ + -d '{ + "content": "QQ邮箱----demo@qq.com----abcd1234\n163邮箱----demo@163.com----efgh5678" + }' +``` + +### 发送邮件 + +```bash +curl -X POST http://localhost:3000/api/send \ + -H 'Content-Type: application/json' \ + -d '{ + "accountId": 1, + "to": "target@example.com", + "subject": "测试邮件", + "text": "这是一封来自 API 的测试邮件" + }' +``` + +### 获取邮件列表 + +首次建议带 `refresh=true`,从远程 IMAP 同步最新邮件。 + +```bash +curl "http://localhost:3000/api/messages?accountId=1&limit=20&refresh=true" +``` + +### 获取最新一封邮件 + +```bash +curl "http://localhost:3000/api/messages/latest?accountId=1&refresh=true" +``` + +### 获取单封邮件详情 + +```bash +curl "http://localhost:3000/api/accounts/1/messages/INBOX%3A123?refresh=true" +``` + +说明:`uid` 需要使用 URL 编码。 + +## 数据说明 + +- `channels`:邮箱渠道配置 +- `accounts`:邮箱帐号和授权码 +- `mail_cache`:同步后的邮件缓存 + +默认本地数据库文件为: + +```text +./mail.db +``` + +如果通过 Docker 运行,则默认为: + +```text +/data/mail.db +``` + +## 部署建议 + +- 生产环境建议使用反向代理,如 Nginx 或 Traefik +- 建议通过 HTTPS 暴露服务 +- 当前授权码存储在 SQLite 中,生产环境建议改为加密存储 +- 建议增加访问控制与登录认证,避免未授权访问 + +## 当前实现边界 + +- 当前未实现附件上传与下载 +- 当前未实现用户登录、权限和多租户隔离 +- 当前邮件缓存为本地 SQLite,不适合超大规模并发 +- 当前 HTML 邮件正文做了基础清理,但不是完整沙箱渲染 + +## 后续建议 + +如果准备继续往生产方向演进,建议优先补以下能力: + +1. 用户登录与权限管理 +2. 授权码加密存储 +3. 附件收发 +4. 邮件标签、已读未读、分页 +5. 定时自动拉取邮件 +6. 发信模板和批量发信 +7. 更完整的 HTML 邮件安全隔离策略 diff --git a/db.js b/db.js new file mode 100644 index 0000000..5049f7d --- /dev/null +++ b/db.js @@ -0,0 +1,93 @@ +const path = require('path'); +const Database = require('better-sqlite3'); + +const dbPath = process.env.DB_PATH + ? path.resolve(process.env.DB_PATH) + : path.join(__dirname, 'mail.db'); + +const db = new Database(dbPath); + +db.pragma('journal_mode = WAL'); + +db.exec(` + CREATE TABLE IF NOT EXISTS channels ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + code TEXT NOT NULL UNIQUE, + imap_host TEXT NOT NULL, + imap_port INTEGER NOT NULL, + imap_secure INTEGER NOT NULL DEFAULT 1, + smtp_host TEXT NOT NULL, + smtp_port INTEGER NOT NULL, + smtp_secure INTEGER NOT NULL DEFAULT 1, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP + ); + + CREATE TABLE IF NOT EXISTS accounts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + channel_id INTEGER NOT NULL, + email TEXT NOT NULL UNIQUE, + auth_code TEXT NOT NULL, + display_name TEXT, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY(channel_id) REFERENCES channels(id) + ); + + CREATE TABLE IF NOT EXISTS mail_cache ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + account_id INTEGER NOT NULL, + uid TEXT NOT NULL, + folder TEXT NOT NULL DEFAULT 'INBOX', + subject TEXT, + from_name TEXT, + from_address TEXT, + to_addresses TEXT, + sent_at TEXT, + text_content TEXT, + html_content TEXT, + raw_json TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE(account_id, uid), + FOREIGN KEY(account_id) REFERENCES accounts(id) + ); +`); + +const seedChannels = [ + { + name: 'QQ邮箱', + code: 'qq', + imap_host: 'imap.qq.com', + imap_port: 993, + imap_secure: 1, + smtp_host: 'smtp.qq.com', + smtp_port: 465, + smtp_secure: 1, + }, + { + name: '163邮箱', + code: '163', + imap_host: 'imap.163.com', + imap_port: 993, + imap_secure: 1, + smtp_host: 'smtp.163.com', + smtp_port: 465, + smtp_secure: 1, + }, +]; + +const channelExistsStmt = db.prepare('SELECT id FROM channels WHERE code = ?'); +const insertChannelStmt = db.prepare(` + INSERT INTO channels ( + name, code, imap_host, imap_port, imap_secure, smtp_host, smtp_port, smtp_secure + ) VALUES ( + @name, @code, @imap_host, @imap_port, @imap_secure, @smtp_host, @smtp_port, @smtp_secure + ) +`); + +for (const channel of seedChannels) { + if (!channelExistsStmt.get(channel.code)) { + insertChannelStmt.run(channel); + } +} + +module.exports = db; diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..58ec0ee --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,21 @@ +version: '3.9' + +services: + mail-sr: + build: + context: . + dockerfile: Dockerfile + container_name: mail-sr + restart: unless-stopped + ports: + - "3000:3000" + environment: + PORT: 3000 + DB_PATH: /data/mail.db + env_file: + - .env + volumes: + - mail-sr-data:/data + +volumes: + mail-sr-data: diff --git a/mailService.js b/mailService.js new file mode 100644 index 0000000..64545da --- /dev/null +++ b/mailService.js @@ -0,0 +1,227 @@ +const nodemailer = require('nodemailer'); +const { ImapFlow } = require('imapflow'); +const { simpleParser } = require('mailparser'); + +function createSmtpTransport(channel, account) { + return nodemailer.createTransport({ + host: channel.smtp_host, + port: channel.smtp_port, + secure: Boolean(channel.smtp_secure), + auth: { + user: account.email, + pass: account.auth_code, + }, + }); +} + +function createImapClient(channel, account) { + return new ImapFlow({ + host: channel.imap_host, + port: channel.imap_port, + secure: Boolean(channel.imap_secure), + auth: { + user: account.email, + pass: account.auth_code, + }, + }); +} + +async function sendMail({ channel, account, payload }) { + const transporter = createSmtpTransport(channel, account); + const info = await transporter.sendMail({ + from: payload.from || formatFrom(account), + to: payload.to, + cc: payload.cc, + bcc: payload.bcc, + subject: payload.subject, + text: payload.text, + html: payload.html, + }); + return { + messageId: info.messageId, + accepted: info.accepted, + rejected: info.rejected, + response: info.response, + }; +} + +function formatFrom(account) { + if (account.display_name) { + return `${account.display_name} <${account.email}>`; + } + return account.email; +} + +function normalizeAddressList(list = []) { + return list.map((item) => ({ + name: item.name || '', + address: item.address || '', + })); +} + +async function fetchLatestEmails({ channel, account, limit = 20, folder = 'INBOX', includeAllFolders = true }) { + const client = createImapClient(channel, account); + + try { + await client.connect(); + const folders = includeAllFolders ? await listReadableFolders(client) : [folder]; + const candidates = []; + + const perFolderLimit = Math.max(limit * 2, 20); + + for (const folderName of folders) { + try { + const folderCandidates = await fetchFolderCandidates(client, folderName, perFolderLimit); + candidates.push(...folderCandidates); + } catch (_error) { + // Some providers expose special folders that are listed but not fetchable. + continue; + } + } + + const targetMessages = candidates + .sort((a, b) => compareDatesDesc(a.date, b.date)) + .slice(0, limit > 0 ? limit : candidates.length); + + return hydrateMessages(client, targetMessages); + } finally { + await client.logout().catch(() => {}); + } +} + +async function listReadableFolders(client) { + const folders = []; + const listed = await client.list(); + + for (const mailbox of listed) { + if (mailbox.flags?.has('\\Noselect')) { + continue; + } + folders.push(mailbox.path); + } + + if (!folders.length) { + return ['INBOX']; + } + + return folders; +} + +async function fetchFolderCandidates(client, folder, limit) { + const mailbox = await client.mailboxOpen(folder); + const total = mailbox.exists || 0; + if (!total) { + return []; + } + + const messages = []; + const start = Math.max(total - limit + 1, 1); + for await (const message of client.fetch(`${start}:${total}`, { + uid: true, + envelope: true, + })) { + messages.push({ + uid: `${folder}:${message.uid}`, + rawUid: String(message.uid), + folder, + subject: message.envelope?.subject || '', + from: normalizeAddressList(message.envelope?.from || []), + to: normalizeAddressList(message.envelope?.to || []), + date: message.envelope?.date?.toISOString?.() || null, + }); + } + + return messages; +} + +async function hydrateMessages(client, messages) { + const result = []; + + for (const message of messages) { + try { + await client.mailboxOpen(message.folder); + const downloaded = await client.download(message.rawUid, null, { uid: true }); + const source = downloaded?.content ? await streamToString(downloaded.content) : ''; + const simple = await simpleParser(source); + + result.push({ + ...message, + subject: simple.subject || message.subject || '', + from: normalizeAddressList(simple.from?.value || message.from || []), + to: normalizeAddressList(simple.to?.value || message.to || []), + date: simple.date ? simple.date.toISOString() : message.date, + text: simple.text || '', + html: simple.html || '', + }); + } catch (_error) { + result.push({ + ...message, + text: '', + html: '', + }); + } + } + + return result; +} + +async function fetchMessageDetail({ channel, account, compositeUid }) { + const client = createImapClient(channel, account); + try { + await client.connect(); + const [folder, rawUid] = splitCompositeUid(compositeUid); + await client.mailboxOpen(folder); + + const envelope = await client.fetchOne(rawUid, { uid: true, envelope: true }, { uid: true }); + const downloaded = await client.download(rawUid, null, { uid: true }); + const source = downloaded?.content ? await streamToString(downloaded.content) : ''; + const simple = await simpleParser(source); + + return { + uid: compositeUid, + rawUid, + folder, + subject: simple.subject || envelope?.envelope?.subject || '', + from: normalizeAddressList(simple.from?.value || envelope?.envelope?.from || []), + to: normalizeAddressList(simple.to?.value || envelope?.envelope?.to || []), + date: simple.date ? simple.date.toISOString() : envelope?.envelope?.date?.toISOString?.() || null, + text: simple.text || '', + html: simple.html || '', + }; + } finally { + await client.logout().catch(() => {}); + } +} + +function splitCompositeUid(value) { + const separatorIndex = String(value).indexOf(':'); + if (separatorIndex === -1) { + return ['INBOX', String(value)]; + } + + return [ + String(value).slice(0, separatorIndex), + String(value).slice(separatorIndex + 1), + ]; +} + +function streamToString(stream) { + return new Promise((resolve, reject) => { + const chunks = []; + stream.on('data', (chunk) => chunks.push(Buffer.from(chunk))); + stream.on('error', reject); + stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf8'))); + }); +} + +function compareDatesDesc(a, b) { + const left = a ? new Date(a).getTime() : 0; + const right = b ? new Date(b).getTime() : 0; + return right - left; +} + +module.exports = { + sendMail, + fetchLatestEmails, + fetchMessageDetail, +}; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..4e74596 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1816 @@ +{ + "name": "mail-sr", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mail-sr", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "better-sqlite3": "^12.9.0", + "cors": "^2.8.6", + "dotenv": "^17.4.2", + "express": "^5.2.1", + "imapflow": "^1.3.2", + "mailparser": "^3.9.8", + "nodemailer": "^8.0.6" + } + }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "license": "MIT" + }, + "node_modules/@selderee/plugin-htmlparser2": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz", + "integrity": "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==", + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "selderee": "^0.11.0" + }, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, + "node_modules/@zone-eu/mailsplit": { + "version": "5.4.9", + "resolved": "https://registry.npmjs.org/@zone-eu/mailsplit/-/mailsplit-5.4.9.tgz", + "integrity": "sha512-Qq7k6FzA5SmGf5HFPcr17gE7M+O1gttlmWn7tlGUlhGsbbjUaBL/4cEWIwExeCzqu5+kyZJ91mcBZbQ9zEwwYA==", + "license": "(MIT OR EUPL-1.1+)", + "dependencies": { + "libbase64": "1.3.0", + "libmime": "5.3.8", + "libqp": "2.1.1" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/better-sqlite3": { + "version": "12.9.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.9.0.tgz", + "integrity": "sha512-wqUv4Gm3toFpHDQmaKD4QhZm3g1DjUBI0yzS4UBl6lElUmXFYdTQmmEDpAFa5o8FiFiymURypEnfVHzILKaxqQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dotenv": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/encoding-japanese": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/encoding-japanese/-/encoding-japanese-2.2.0.tgz", + "integrity": "sha512-EuJWwlHPZ1LbADuKTClvHtwbaFn4rOD+dRAbWysqEOXRc2Uui0hJInNJrsdH0c+OhJA4nrCBdSkW4DD5YxAo6A==", + "license": "MIT", + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/html-to-text": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-9.0.5.tgz", + "integrity": "sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==", + "license": "MIT", + "dependencies": { + "@selderee/plugin-htmlparser2": "^0.11.0", + "deepmerge": "^4.3.1", + "dom-serializer": "^2.0.0", + "htmlparser2": "^8.0.2", + "selderee": "^0.11.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/imapflow": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/imapflow/-/imapflow-1.3.2.tgz", + "integrity": "sha512-lIhVpjAf+o/1SELeR34vqeoEjAyBOMd6xq2Vdx2E4XIRwDi5sfqfBqqr5YkTwp7G/Q3f5ACpU7/zuGklJeCj9Q==", + "license": "MIT", + "dependencies": { + "@zone-eu/mailsplit": "5.4.9", + "encoding-japanese": "2.2.0", + "iconv-lite": "0.7.2", + "libbase64": "1.3.0", + "libmime": "5.3.8", + "libqp": "2.1.1", + "nodemailer": "8.0.5", + "pino": "10.3.1", + "socks": "2.8.7" + } + }, + "node_modules/imapflow/node_modules/nodemailer": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.5.tgz", + "integrity": "sha512-0PF8Yb1yZuQfQbq+5/pZJrtF6WQcjTd5/S4JOHs9PGFxuTqoB/icwuB44pOdURHJbRKX1PPoJZtY7R4VUoCC8w==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/leac": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/leac/-/leac-0.6.0.tgz", + "integrity": "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==", + "license": "MIT", + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, + "node_modules/libbase64": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/libbase64/-/libbase64-1.3.0.tgz", + "integrity": "sha512-GgOXd0Eo6phYgh0DJtjQ2tO8dc0IVINtZJeARPeiIJqge+HdsWSuaDTe8ztQ7j/cONByDZ3zeB325AHiv5O0dg==", + "license": "MIT" + }, + "node_modules/libmime": { + "version": "5.3.8", + "resolved": "https://registry.npmjs.org/libmime/-/libmime-5.3.8.tgz", + "integrity": "sha512-ZrCY+Q66mPvasAfjsQ/IgahzoBvfE1VdtGRpo1hwRB1oK3wJKxhKA3GOcd2a6j7AH5eMFccxK9fBoCpRZTf8ng==", + "license": "MIT", + "dependencies": { + "encoding-japanese": "2.2.0", + "iconv-lite": "0.7.2", + "libbase64": "1.3.0", + "libqp": "2.1.1" + } + }, + "node_modules/libqp": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/libqp/-/libqp-2.1.1.tgz", + "integrity": "sha512-0Wd+GPz1O134cP62YU2GTOPNA7Qgl09XwCqM5zpBv87ERCXdfDtyKXvV7c9U22yWJh44QZqBocFnXN11K96qow==", + "license": "MIT" + }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/mailparser": { + "version": "3.9.8", + "resolved": "https://registry.npmjs.org/mailparser/-/mailparser-3.9.8.tgz", + "integrity": "sha512-7jSlFGXiianVnhnb6wdutJFloD34488nrHY7r6FNqwXAhZ7YiJDYrKKTxZJ0oSrXcAPHm8YoYnh97xyGtrBQ3w==", + "license": "MIT", + "dependencies": { + "@zone-eu/mailsplit": "5.4.8", + "encoding-japanese": "2.2.0", + "he": "1.2.0", + "html-to-text": "9.0.5", + "iconv-lite": "0.7.2", + "libmime": "5.3.8", + "linkify-it": "5.0.0", + "nodemailer": "8.0.5", + "punycode.js": "2.3.1", + "tlds": "1.261.0" + } + }, + "node_modules/mailparser/node_modules/@zone-eu/mailsplit": { + "version": "5.4.8", + "resolved": "https://registry.npmjs.org/@zone-eu/mailsplit/-/mailsplit-5.4.8.tgz", + "integrity": "sha512-eEyACj4JZ7sjzRvy26QhLgKEMWwQbsw1+QZnlLX+/gihcNH07lVPOcnwf5U6UAL7gkc//J3jVd76o/WS+taUiA==", + "license": "(MIT OR EUPL-1.1+)", + "dependencies": { + "libbase64": "1.3.0", + "libmime": "5.3.7", + "libqp": "2.1.1" + } + }, + "node_modules/mailparser/node_modules/@zone-eu/mailsplit/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mailparser/node_modules/@zone-eu/mailsplit/node_modules/libmime": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/libmime/-/libmime-5.3.7.tgz", + "integrity": "sha512-FlDb3Wtha8P01kTL3P9M+ZDNDWPKPmKHWaU/cG/lg5pfuAwdflVpZE+wm9m7pKmC5ww6s+zTxBKS1p6yl3KpSw==", + "license": "MIT", + "dependencies": { + "encoding-japanese": "2.2.0", + "iconv-lite": "0.6.3", + "libbase64": "1.3.0", + "libqp": "2.1.1" + } + }, + "node_modules/mailparser/node_modules/nodemailer": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.5.tgz", + "integrity": "sha512-0PF8Yb1yZuQfQbq+5/pZJrtF6WQcjTd5/S4JOHs9PGFxuTqoB/icwuB44pOdURHJbRKX1PPoJZtY7R4VUoCC8w==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abi": { + "version": "3.89.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz", + "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/nodemailer": { + "version": "8.0.6", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.6.tgz", + "integrity": "sha512-Nm2XeuDwwy2wi5A+8jPWwQwNzcjNjhWdE3pVLoXEusxJqCnAPAgnBGkSmiLknbnWuOF9qraRpYZjfxqtKZ4tPw==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseley": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/parseley/-/parseley-0.12.1.tgz", + "integrity": "sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==", + "license": "MIT", + "dependencies": { + "leac": "^0.6.0", + "peberminta": "^0.9.0" + }, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/peberminta": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/peberminta/-/peberminta-0.9.0.tgz", + "integrity": "sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==", + "license": "MIT", + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, + "node_modules/pino": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", + "integrity": "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^3.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^4.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz", + "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "license": "MIT" + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/selderee": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/selderee/-/selderee-0.11.0.tgz", + "integrity": "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==", + "license": "MIT", + "dependencies": { + "parseley": "^0.12.0" + }, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/thread-stream": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.0.0.tgz", + "integrity": "sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/tlds": { + "version": "1.261.0", + "resolved": "https://registry.npmjs.org/tlds/-/tlds-1.261.0.tgz", + "integrity": "sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA==", + "license": "MIT", + "bin": { + "tlds": "bin.js" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..efd4626 --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "mail-sr", + "version": "1.0.0", + "description": "Multi mailbox send and receive system", + "main": "server.js", + "scripts": { + "start": "node server.js", + "dev": "node server.js", + "test": "echo \"No automated tests yet\"" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "better-sqlite3": "^12.9.0", + "cors": "^2.8.6", + "dotenv": "^17.4.2", + "express": "^5.2.1", + "imapflow": "^1.3.2", + "mailparser": "^3.9.8", + "nodemailer": "^8.0.6" + } +} diff --git a/public/app.js b/public/app.js new file mode 100644 index 0000000..9ec0471 --- /dev/null +++ b/public/app.js @@ -0,0 +1,481 @@ +const state = { + channels: [], + accounts: [], + selectedAccountId: null, + messages: [], + selectedMessageUid: null, +}; + +const channelTree = document.getElementById('channel-tree'); +const channelForm = document.getElementById('channel-form'); +const accountForm = document.getElementById('account-form'); +const accountSubmitBtn = document.getElementById('account-submit-btn'); +const sendForm = document.getElementById('send-form'); +const sendBtn = document.getElementById('send-btn'); +const refreshMailsBtn = document.getElementById('refresh-mails-btn'); +const loadMailsBtn = document.getElementById('load-mails-btn'); +const selectedAccountTitle = document.getElementById('selected-account-title'); +const selectedAccountMeta = document.getElementById('selected-account-meta'); +const messageList = document.getElementById('message-list'); +const messageDetail = document.getElementById('message-detail'); +const toast = document.getElementById('toast'); +const composeModal = document.getElementById('compose-modal'); +const settingsModal = document.getElementById('settings-modal'); +const composeBtn = document.getElementById('compose-btn'); +const settingsBtn = document.getElementById('settings-btn'); +const closeCompose = document.getElementById('close-compose'); +const closeSettings = document.getElementById('close-settings'); +const batchImportForm = document.getElementById('batch-import-form'); +const batchImportContent = document.getElementById('batch-import-content'); +const batchImportBtn = document.getElementById('batch-import-btn'); +const exportAccountsBtn = document.getElementById('export-accounts-btn'); +const copyExportBtn = document.getElementById('copy-export-btn'); + +boot(); + +async function boot() { + bindEvents(); + await reloadBaseData(); +} + +function bindEvents() { + composeBtn.addEventListener('click', () => openModal(composeModal)); + settingsBtn.addEventListener('click', () => openModal(settingsModal)); + closeCompose.addEventListener('click', () => closeModal(composeModal)); + closeSettings.addEventListener('click', () => closeModal(settingsModal)); + composeModal.querySelector('.modal-backdrop').addEventListener('click', () => closeModal(composeModal)); + settingsModal.querySelector('.modal-backdrop').addEventListener('click', () => closeModal(settingsModal)); + + channelForm.addEventListener('submit', onCreateChannel); + accountForm.addEventListener('submit', onCreateAccount); + batchImportForm.addEventListener('submit', onBatchImport); + sendForm.addEventListener('submit', onSendMail); + refreshMailsBtn.addEventListener('click', () => loadMessages(true)); + loadMailsBtn.addEventListener('click', () => loadMessages(false)); + exportAccountsBtn.addEventListener('click', onExportAccounts); + copyExportBtn.addEventListener('click', onCopyExportAccounts); +} + +function openModal(modal) { + modal.classList.remove('hidden'); +} + +function closeModal(modal) { + modal.classList.add('hidden'); +} + +async function reloadBaseData() { + const [channels, accounts] = await Promise.all([ + request('/api/channels'), + request('/api/accounts'), + ]); + + state.channels = channels; + state.accounts = accounts; + renderAccountOptions(); + renderChannelTree(); +} + +async function onCreateChannel(event) { + event.preventDefault(); + const formData = new FormData(channelForm); + const payload = Object.fromEntries(formData.entries()); + payload.imap_secure = formData.get('imap_secure') === 'on'; + payload.smtp_secure = formData.get('smtp_secure') === 'on'; + + await request('/api/channels', { + method: 'POST', + body: JSON.stringify(payload), + }); + + channelForm.reset(); + channelForm.querySelector('[name="imap_secure"]').checked = true; + channelForm.querySelector('[name="smtp_secure"]').checked = true; + showToast('渠道已创建'); + await reloadBaseData(); +} + +async function onCreateAccount(event) { + event.preventDefault(); + const payload = Object.fromEntries(new FormData(accountForm).entries()); + accountSubmitBtn.disabled = true; + accountSubmitBtn.textContent = '保存中...'; + + try { + await request('/api/accounts', { + method: 'POST', + body: JSON.stringify(payload), + }); + + accountForm.reset(); + showToast('邮箱帐号已导入'); + await reloadBaseData(); + } finally { + accountSubmitBtn.disabled = false; + accountSubmitBtn.textContent = '导入帐号'; + } +} + +async function onSendMail(event) { + event.preventDefault(); + if (!state.selectedAccountId) { + showToast('请先选择邮箱帐号', true); + return; + } + + const payload = Object.fromEntries(new FormData(sendForm).entries()); + sendBtn.disabled = true; + sendBtn.textContent = '发送中...'; + + try { + await request(`/api/accounts/${state.selectedAccountId}/messages/send`, { + method: 'POST', + body: JSON.stringify(payload), + }); + + sendForm.reset(); + closeModal(composeModal); + showToast('邮件发送成功'); + } finally { + sendBtn.disabled = false; + sendBtn.textContent = '发送'; + } +} + +async function onBatchImport(event) { + event.preventDefault(); + const content = batchImportContent.value.trim(); + if (!content) { + showToast('请先粘贴导入内容', true); + return; + } + + batchImportBtn.disabled = true; + batchImportBtn.textContent = '导入中...'; + + try { + const result = await request('/api/accounts/import', { + method: 'POST', + body: JSON.stringify({ content }), + }); + + await reloadBaseData(); + showToast(`导入完成,新增 ${result.created} 条,更新 ${result.updated} 条`); + } finally { + batchImportBtn.disabled = false; + batchImportBtn.textContent = '批量导入'; + } +} + +async function onExportAccounts() { + const result = await request('/api/accounts/export'); + batchImportContent.value = result.content || ''; + showToast('导出内容已填入文本框'); +} + +async function onCopyExportAccounts() { + const content = batchImportContent.value.trim(); + if (!content) { + const result = await request('/api/accounts/export'); + batchImportContent.value = result.content || ''; + } + + await copyText(batchImportContent.value); + showToast('导出内容已复制'); +} + +function renderAccountOptions() { + const select = accountForm.querySelector('select[name="channel_id"]'); + select.innerHTML = '' + state.channels + .map((channel) => ``) + .join(''); +} + +function renderChannelTree() { + if (!state.channels.length) { + channelTree.innerHTML = '
暂无渠道,点击底部设置新增
'; + return; + } + + channelTree.innerHTML = state.channels.map((channel) => { + const accounts = state.accounts.filter((account) => Number(account.channel_id) === Number(channel.id)); + const items = accounts.length + ? accounts.map((account) => ` +
+ + +
+ `).join('') + : '
该渠道下暂无邮箱
'; + + return ` +
+
+ + + + + ${escapeHtml(channel.name)} + + ${accounts.length} +
+
+ ${items} +
+
+ `; + }).join(''); + + channelTree.querySelectorAll('[data-channel-toggle]').forEach((header) => { + header.addEventListener('click', () => { + header.classList.toggle('collapsed'); + }); + }); + + channelTree.querySelectorAll('[data-account-id]').forEach((button) => { + button.addEventListener('click', async () => { + const email = button.dataset.accountEmail || ''; + let copied = false; + try { + await copyText(email); + copied = true; + } catch (_error) { + copied = false; + } + state.selectedAccountId = Number(button.dataset.accountId); + state.selectedMessageUid = null; + syncSelectionState(); + renderChannelTree(); + if (copied) { + showToast(`已复制: ${email}`); + } + await loadMessages(true, true); + }); + }); + + channelTree.querySelectorAll('[data-delete-account-id]').forEach((button) => { + button.addEventListener('click', async (event) => { + event.stopPropagation(); + const accountId = Number(button.dataset.deleteAccountId); + const email = button.dataset.deleteAccountEmail || ''; + if (!window.confirm(`确认删除 ${email} 吗?`)) { + return; + } + + await request(`/api/accounts/${accountId}`, { method: 'DELETE' }); + if (Number(state.selectedAccountId) === accountId) { + state.selectedAccountId = null; + state.selectedMessageUid = null; + state.messages = []; + renderMessages(); + syncSelectionState(); + } + showToast(`已删除: ${email}`); + await reloadBaseData(); + }); + }); +} + +async function loadMessages(refresh, autoFocusLatest = false) { + if (!state.selectedAccountId) { + return; + } + + const query = new URLSearchParams({ + limit: '20', + refresh: refresh ? 'true' : 'false', + }); + + const messages = await request(`/api/accounts/${state.selectedAccountId}/messages?${query.toString()}`); + state.messages = messages; + if (autoFocusLatest || !state.selectedMessageUid) { + state.selectedMessageUid = messages[0]?.uid || null; + } + renderMessages(); + syncSelectionState(); + showToast(refresh ? '已收取最新邮件' : '已加载缓存邮件'); +} + +function syncSelectionState() { + const account = state.accounts.find((item) => Number(item.id) === Number(state.selectedAccountId)); + const enabled = Boolean(account); + refreshMailsBtn.disabled = !enabled; + loadMailsBtn.disabled = !enabled; + sendBtn.disabled = !enabled; + + if (!account) { + selectedAccountTitle.textContent = '选择邮箱帐号'; + selectedAccountMeta.textContent = '左侧选择邮箱后开始操作'; + return; + } + + const channel = state.channels.find((item) => Number(item.id) === Number(account.channel_id)); + selectedAccountTitle.textContent = account.email; + selectedAccountMeta.textContent = `${channel?.name || '未知渠道'} · ID: ${account.id}`; +} + +function renderMessages() { + if (!state.messages.length) { + messageList.innerHTML = ` +
+ + + + +

暂无邮件,点击刷新收取

+
`; + messageDetail.className = 'mail-detail empty'; + messageDetail.innerHTML = ` +
+ + + + +

选择一封邮件查看详情

+
`; + return; + } + + messageList.innerHTML = state.messages.map((message) => ` +
+

${escapeHtml(message.subject || '(无主题)')}

+

${escapeHtml(message.from?.name || message.from?.address || '')}

+
${escapeHtml(message.folder || 'INBOX')} · ${formatDate(message.date)}
+
+ `).join(''); + + messageList.querySelectorAll('[data-message-uid]').forEach((node) => { + node.addEventListener('click', async () => { + state.selectedMessageUid = node.dataset.messageUid; + await loadMessageDetail(state.selectedMessageUid); + renderMessages(); + }); + }); + + const active = state.messages.find((message) => String(message.uid) === String(state.selectedMessageUid)) || state.messages[0]; + if (!active) { + return; + } + + messageDetail.className = 'mail-detail'; + const bodyHtml = normalizeMessageBody(active); + messageDetail.innerHTML = ` +
+

${escapeHtml(active.subject || '(无主题)')}

+
+
+

发件人:${escapeHtml(active.from?.name || '')} ${escapeHtml(active.from?.address || '')}

+

时间:${formatDate(active.date)}

+

收件人:${escapeHtml((active.to || []).map((item) => item.address).join(', '))}

+

文件夹:${escapeHtml(active.folder || 'INBOX')}

+
+
+
${bodyHtml.content}
+ `; +} + +function normalizeMessageBody(message) { + const html = String(message.html || '').trim(); + const text = String(message.text || '').trim(); + + if (html) { + return { isHtml: true, content: sanitizeHtml(html) }; + } + + return { isHtml: false, content: escapeHtml(text || '该邮件暂无可展示正文') }; +} + +function sanitizeHtml(value) { + return String(value) + .replace(/)<[^<]*)*<\/script>/gi, '') + .replace(/on\w+=/gi, 'data-blocked='); +} + +async function loadMessageDetail(uid) { + if (!state.selectedAccountId || !uid) { + return; + } + + try { + const detail = await request(`/api/accounts/${state.selectedAccountId}/messages/${encodeURIComponent(uid)}?refresh=true`); + state.messages = state.messages.map((message) => (String(message.uid) === String(uid) ? detail : message)); + } catch (_error) { + } +} + +async function copyText(value) { + if (!value) return; + if (navigator.clipboard && window.isSecureContext) { + await navigator.clipboard.writeText(value); + return; + } + const input = document.createElement('textarea'); + input.value = value; + input.setAttribute('readonly', 'readonly'); + input.style.position = 'absolute'; + input.style.left = '-9999px'; + document.body.appendChild(input); + input.select(); + document.execCommand('copy'); + document.body.removeChild(input); +} + +async function request(url, options = {}) { + const response = await fetch(url, { + headers: { + 'Content-Type': 'application/json', + ...(options.headers || {}), + }, + ...options, + }); + + const data = await response.json().catch(() => ({})); + if (!response.ok) { + showToast(data.error || '请求失败', true); + throw new Error(data.error || 'Request failed'); + } + return data; +} + +function showToast(message, error = false) { + toast.textContent = message; + toast.className = `toast ${error ? 'error' : ''}`; + setTimeout(() => { + toast.className = 'toast hidden'; + }, 2500); +} + +function escapeHtml(value) { + return String(value || '') + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll("'", '''); +} + +function formatDate(value) { + if (!value) return '未知时间'; + return new Date(value).toLocaleString('zh-CN'); +} diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..8ba61f4 --- /dev/null +++ b/public/index.html @@ -0,0 +1,219 @@ + + + + + + Mail SR - 多邮箱工作台 + + + + + + +
+ + + + +
+
+
+

选择邮箱帐号

+ +
+
+ + +
+
+ +
+
+
+ + + + +

选择邮箱帐号查看邮件

+
+
+
+
+ + +
+
+
+ + + + +

选择一封邮件查看详情

+
+
+
+
+ + + + + + + + + + + diff --git a/public/styles.css b/public/styles.css new file mode 100644 index 0000000..3768609 --- /dev/null +++ b/public/styles.css @@ -0,0 +1,780 @@ +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +:root { + color-scheme: dark; + --bg-base: #0f172a; + --bg-sidebar: #0c1424; + --bg-card: #1e293b; + --bg-hover: #273548; + --bg-active: #1e3a5f; + --border: #334155; + --border-light: #1e293b; + --text-primary: #f1f5f9; + --text-secondary: #94a3b8; + --text-muted: #64748b; + --accent: #3b82f6; + --accent-light: #1e3a5f; + --accent-hover: #60a5fa; + --danger: #ef4444; + --danger-hover: #f87171; + --shadow-sm: 0 1px 3px rgba(0,0,0,0.3); + --shadow-md: 0 4px 12px rgba(0,0,0,0.35); + --radius-sm: 6px; + --radius-md: 10px; + --radius-lg: 14px; + --font: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif; +} + +body { + font-family: var(--font); + background: var(--bg-base); + color: var(--text-primary); + line-height: 1.5; + -webkit-font-smoothing: antialiased; +} + +button, +input, +textarea, +select { + font: inherit; +} + +button { + border: 0; + background: none; + cursor: pointer; + padding: 0; +} + +button:disabled { + opacity: 0.45; + cursor: not-allowed; +} + +input, +textarea, +select { + width: 100%; + border: 1px solid var(--border); + background: var(--bg-card); + color: var(--text-primary); + border-radius: var(--radius-sm); + padding: 9px 12px; + outline: none; + transition: border-color 0.15s, box-shadow 0.15s; + font-size: 14px; +} + +input:focus, +textarea:focus, +select:focus { + border-color: var(--accent); + box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1); +} + +.app-container { + display: grid; + grid-template-columns: 240px 320px 1fr; + height: 100vh; + overflow: hidden; +} + +/* === Sidebar === */ +.sidebar { + background: var(--bg-sidebar); + border-right: 1px solid var(--border); + display: flex; + flex-direction: column; + overflow: hidden; +} + +.sidebar-header { + padding: 20px 16px 16px; + display: flex; + flex-direction: column; + gap: 14px; + border-bottom: 1px solid var(--border-light); +} + +.logo { + display: flex; + align-items: center; + gap: 10px; + font-size: 16px; + font-weight: 700; + color: var(--text-primary); +} + +.logo svg { + color: var(--accent); + flex-shrink: 0; +} + +.compose-btn { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + background: var(--accent); + color: #fff; + border-radius: var(--radius-md); + padding: 10px 14px; + font-size: 14px; + font-weight: 600; + transition: background 0.15s, transform 0.1s; + width: 100%; +} + +.compose-btn:hover { + background: var(--accent-hover); + transform: translateY(-1px); +} + +.compose-btn:active { + transform: translateY(0); +} + +.account-section { + flex: 1; + overflow: hidden; + display: flex; + flex-direction: column; + padding: 12px 0; +} + +.section-header { + padding: 4px 16px 8px; + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--text-muted); +} + +.account-list { + flex: 1; + overflow-y: auto; + padding: 0 8px; +} + +.channel-group { + margin-bottom: 4px; +} + +.channel-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 8px 4px; + font-size: 13px; + font-weight: 600; + color: var(--text-secondary); + cursor: pointer; +} + +.channel-header:hover .chevron, +.channel-header:hover .channel-name { + color: var(--accent); +} + +.channel-name { + display: flex; + align-items: center; + gap: 6px; + color: var(--text-secondary); + transition: color 0.15s; +} + +.chevron { + transition: transform 0.2s, color 0.15s; + color: var(--text-muted); +} + +.channel-header.collapsed .chevron { + transform: rotate(-90deg); +} + +.channel-accounts { + overflow: hidden; + transition: max-height 0.2s ease; +} + +.channel-header.collapsed + .channel-accounts { + display: none; +} + +.account-item { + display: flex; + align-items: center; + gap: 4px; + padding: 2px 0; +} + +.account-copy-btn { + flex: 1; + display: flex; + align-items: center; + gap: 6px; + padding: 8px 10px; + border-radius: var(--radius-sm); + font-size: 13px; + color: var(--text-primary); + transition: background 0.12s, color 0.12s; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + min-width: 0; +} + +.account-copy-btn:hover { + background: var(--bg-hover); + color: var(--accent); +} + +.account-item.active .account-copy-btn { + background: var(--bg-active); + color: var(--accent); + font-weight: 600; +} + +.account-delete-btn { + width: 28px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; + border-radius: var(--radius-sm); + color: var(--text-muted); + opacity: 0; + transition: opacity 0.15s, background 0.15s, color 0.15s; + flex-shrink: 0; +} + +.account-item:hover .account-delete-btn { + opacity: 1; +} + +.account-delete-btn:hover { + background: #fee2e2; + color: var(--danger); +} + +.empty-hint { + padding: 16px; + font-size: 13px; + color: var(--text-muted); + text-align: center; +} + +.sidebar-footer { + padding: 12px 16px; + border-top: 1px solid var(--border-light); + display: flex; + justify-content: flex-end; +} + +.icon-btn { + width: 36px; + height: 36px; + display: flex; + align-items: center; + justify-content: center; + border-radius: var(--radius-sm); + color: var(--text-muted); + transition: background 0.15s, color 0.15s; +} + +.icon-btn:hover { + background: var(--bg-hover); + color: var(--text-primary); +} + +/* === Mail List Section === */ +.mail-list-section { + border-right: 1px solid var(--border); + background: var(--bg-card); + display: flex; + flex-direction: column; + overflow: hidden; +} + +.mail-list-header { + padding: 16px 16px 12px; + border-bottom: 1px solid var(--border-light); + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + flex-shrink: 0; +} + +.header-left { + min-width: 0; +} + +#selected-account-title { + font-size: 15px; + font-weight: 700; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.account-info { + font-size: 12px; + color: var(--text-muted); + display: block; + margin-top: 2px; +} + +.header-actions { + display: flex; + gap: 4px; + flex-shrink: 0; +} + +.action-btn { + width: 34px; + height: 34px; + display: flex; + align-items: center; + justify-content: center; + border-radius: var(--radius-sm); + color: var(--text-secondary); + transition: background 0.15s, color 0.15s; +} + +.action-btn:hover:not(:disabled) { + background: var(--bg-hover); + color: var(--accent); +} + +.mail-list { + flex: 1; + overflow: hidden; + display: flex; + flex-direction: column; +} + +.message-items { + flex: 1; + overflow-y: auto; + padding: 8px; +} + +.empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + gap: 12px; + color: var(--text-muted); + font-size: 14px; +} + +.empty-state svg { + opacity: 0.3; +} + +.message-card { + padding: 12px; + border-radius: var(--radius-md); + cursor: pointer; + transition: background 0.12s; + border: 1px solid transparent; + margin-bottom: 4px; +} + +.message-card:hover { + background: var(--bg-hover); +} + +.message-card.active { + background: var(--accent-light); + border-color: #bfdbfe; +} + +.message-card h3 { + font-size: 13px; + font-weight: 600; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + margin-bottom: 4px; +} + +.message-card p { + font-size: 12px; + color: var(--text-secondary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.message-meta { + font-size: 11px; + color: var(--text-muted); + margin-top: 3px; +} + +/* === Mail Detail Section === */ +.mail-detail-section { + background: var(--bg-base); + overflow: hidden; + display: flex; + flex-direction: column; +} + +.mail-detail { + flex: 1; + overflow-y: auto; + padding: 24px 32px; +} + +.mail-detail.empty { + display: flex; + align-items: center; + justify-content: center; +} + +.mail-detail.empty .empty-state svg { + opacity: 0.2; +} + +.reading-header h3 { + font-size: 18px; + font-weight: 700; + margin-bottom: 16px; + line-height: 1.4; +} + +.reading-meta-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px 16px; + padding: 14px 16px; + background: var(--bg-card); + border-radius: var(--radius-md); + box-shadow: var(--shadow-sm); + margin-bottom: 20px; +} + +.reading-meta-grid p { + font-size: 13px; + color: var(--text-secondary); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.reading-meta-grid strong { + color: var(--text-primary); + font-weight: 600; +} + +.reading-divider { + height: 1px; + background: var(--border-light); + margin: 0 0 20px; +} + +.message-body { + line-height: 1.8; + font-size: 14px; + color: var(--text-primary); + max-width: 720px; +} + +.message-body.text-body { + white-space: pre-wrap; + background: var(--bg-card); + padding: 16px; + border-radius: var(--radius-md); + box-shadow: var(--shadow-sm); +} + +.message-body.html-body { + background: var(--bg-card); + padding: 20px 24px; + border-radius: var(--radius-md); + box-shadow: var(--shadow-sm); +} + +.message-body.html-body img { + max-width: 100%; + height: auto; +} + +.message-body.html-body a { + color: var(--accent); +} + +/* === Modal === */ +.modal { + position: fixed; + inset: 0; + z-index: 1000; + display: flex; + align-items: center; + justify-content: center; +} + +.modal.hidden { + display: none; +} + +.modal-backdrop { + position: absolute; + inset: 0; + background: rgba(0, 0, 0, 0.4); + backdrop-filter: blur(2px); +} + +.modal-content { + position: relative; + background: var(--bg-card); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-md), 0 20px 40px rgba(0,0,0,0.15); + overflow: hidden; + max-height: 90vh; + display: flex; + flex-direction: column; +} + +.compose-modal-content { + width: 620px; +} + +.settings-modal-content { + width: 520px; +} + +.modal-header { + padding: 20px 24px 16px; + border-bottom: 1px solid var(--border-light); + display: flex; + align-items: center; + justify-content: space-between; + flex-shrink: 0; +} + +.modal-header h3 { + font-size: 17px; + font-weight: 700; +} + +.close-btn { + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + border-radius: var(--radius-sm); + color: var(--text-muted); + transition: background 0.15s, color 0.15s; +} + +.close-btn:hover { + background: var(--bg-hover); + color: var(--text-primary); +} + +.compose-form, +.settings-content { + padding: 20px 24px; + overflow-y: auto; +} + +.form-row { + margin-bottom: 14px; +} + +.form-row label { + display: block; + font-size: 13px; + font-weight: 600; + color: var(--text-secondary); + margin-bottom: 6px; +} + +.body-row { + flex: 1; + display: flex; + flex-direction: column; +} + +.body-row textarea { + flex: 1; + min-height: 220px; + resize: vertical; + font-family: inherit; + line-height: 1.6; +} + +.form-actions { + display: flex; + justify-content: flex-end; + padding-top: 8px; +} + +.send-btn { + background: var(--accent); + color: #fff; + border-radius: var(--radius-md); + padding: 10px 24px; + font-size: 14px; + font-weight: 600; + transition: background 0.15s; +} + +.send-btn:hover:not(:disabled) { + background: var(--accent-hover); +} + +.settings-content { + display: flex; + flex-direction: column; + gap: 20px; +} + +.settings-section h4 { + font-size: 14px; + font-weight: 700; + margin-bottom: 12px; + color: var(--text-primary); +} + +.settings-form { + display: flex; + flex-direction: column; + gap: 10px; +} + +.batch-tip { + margin-bottom: 10px; + font-size: 12px; + color: var(--text-muted); +} + +.batch-textarea { + min-height: 150px; + resize: vertical; + line-height: 1.7; +} + +.inline-actions { + display: flex; + gap: 10px; + flex-wrap: wrap; +} + +.form-row.compact { + margin-bottom: 0; +} + +.form-row.split { + display: flex; + align-items: center; + gap: 10px; +} + +.form-row.split input { + flex: 1; +} + +.checkbox { + display: flex; + align-items: center; + gap: 6px; + font-size: 13px; + color: var(--text-secondary); + white-space: nowrap; + flex-shrink: 0; + cursor: pointer; +} + +.checkbox input { + width: auto; +} + +.add-btn { + background: var(--accent); + color: #fff; + border-radius: var(--radius-md); + padding: 10px 16px; + font-size: 13px; + font-weight: 600; + transition: background 0.15s; + margin-top: 4px; +} + +.add-btn:hover:not(:disabled) { + background: var(--accent-hover); +} + +.secondary-btn { + background: transparent; + color: var(--text-secondary); + border: 1px solid var(--border); + border-radius: var(--radius-md); + padding: 10px 16px; + font-size: 13px; + font-weight: 600; + transition: background 0.15s, border-color 0.15s, color 0.15s; +} + +.secondary-btn:hover:not(:disabled) { + background: var(--bg-hover); + border-color: var(--accent); + color: var(--text-primary); +} + +/* === Toast === */ +.toast { + position: fixed; + bottom: 24px; + right: 24px; + padding: 12px 20px; + border-radius: var(--radius-md); + background: #1a1d21; + color: #fff; + font-size: 14px; + font-weight: 500; + box-shadow: var(--shadow-md); + z-index: 2000; + max-width: 360px; +} + +.toast.error { + background: var(--danger); +} + +.toast.hidden { + display: none; +} + +@media (max-width: 1100px) { + .app-container { + grid-template-columns: 220px 280px 1fr; + } +} + +@media (max-width: 900px) { + .app-container { + grid-template-columns: 1fr; + } + + .sidebar { + display: none; + } + + .mail-list-section { + border-right: none; + } +} diff --git a/server.js b/server.js new file mode 100644 index 0000000..aae8a67 --- /dev/null +++ b/server.js @@ -0,0 +1,595 @@ +require('dotenv').config(); + +const path = require('path'); +const express = require('express'); +const cors = require('cors'); + +const db = require('./db'); +const { sendMail, fetchLatestEmails, fetchMessageDetail } = require('./mailService'); + +const app = express(); +const port = Number(process.env.PORT || 3000); + +app.use(cors()); +app.use(express.json({ limit: '2mb' })); +app.use(express.static(path.join(__dirname, 'public'))); + +const getChannelStmt = db.prepare('SELECT * FROM channels WHERE id = ?'); +const getAccountStmt = db.prepare('SELECT * FROM accounts WHERE id = ?'); +const listChannelsStmt = db.prepare(` + SELECT + c.*, + COUNT(a.id) AS account_count + FROM channels c + LEFT JOIN accounts a ON a.channel_id = c.id + GROUP BY c.id + ORDER BY c.name ASC +`); +const listAccountsByChannelStmt = db.prepare(` + SELECT a.*, c.name AS channel_name, c.code AS channel_code + FROM accounts a + JOIN channels c ON c.id = a.channel_id + WHERE a.channel_id = ? + ORDER BY a.email ASC +`); +const listAccountsStmt = db.prepare(` + SELECT a.*, c.name AS channel_name, c.code AS channel_code + FROM accounts a + JOIN channels c ON c.id = a.channel_id + ORDER BY c.name ASC, a.email ASC +`); +const listAccountsWithSecretsStmt = db.prepare(` + SELECT a.email, a.auth_code, c.name AS channel_name + FROM accounts a + JOIN channels c ON c.id = a.channel_id + ORDER BY c.name ASC, a.email ASC +`); +const getChannelByNameStmt = db.prepare('SELECT * FROM channels WHERE name = ?'); +const getAccountByChannelEmailStmt = db.prepare('SELECT * FROM accounts WHERE channel_id = ? AND email = ?'); +const insertChannelStmt = db.prepare(` + INSERT INTO channels ( + name, code, imap_host, imap_port, imap_secure, smtp_host, smtp_port, smtp_secure + ) VALUES ( + @name, @code, @imap_host, @imap_port, @imap_secure, @smtp_host, @smtp_port, @smtp_secure + ) +`); +const insertAccountStmt = db.prepare(` + INSERT INTO accounts (channel_id, email, auth_code, display_name) + VALUES (@channel_id, @email, @auth_code, @display_name) +`); +const updateAccountAuthCodeStmt = db.prepare('UPDATE accounts SET auth_code = ? WHERE id = ?'); +const deleteAccountStmt = db.prepare('DELETE FROM accounts WHERE id = ?'); +const deleteMailCacheByAccountStmt = db.prepare('DELETE FROM mail_cache WHERE account_id = ?'); +const deleteAccountByEmailStmt = db.prepare('DELETE FROM accounts WHERE email = ?'); +const deleteMailCacheByAccountEmailStmt = db.prepare(` + DELETE FROM mail_cache + WHERE account_id IN (SELECT id FROM accounts WHERE email = ?) +`); +const listCacheByAccountStmt = db.prepare(` + SELECT * + FROM mail_cache + WHERE account_id = ? + ORDER BY datetime(COALESCE(sent_at, created_at)) DESC, id DESC + LIMIT ? +`); +const latestCacheByAccountStmt = db.prepare(` + SELECT * + FROM mail_cache + WHERE account_id = ? + ORDER BY datetime(COALESCE(sent_at, created_at)) DESC, id DESC + LIMIT 1 +`); +const getCacheByAccountUidStmt = db.prepare(` + SELECT * + FROM mail_cache + WHERE account_id = ? AND uid = ? + LIMIT 1 +`); +const upsertMailCacheStmt = db.prepare(` + INSERT INTO mail_cache ( + account_id, uid, folder, subject, from_name, from_address, to_addresses, + sent_at, text_content, html_content, raw_json + ) VALUES ( + @account_id, @uid, @folder, @subject, @from_name, @from_address, @to_addresses, + @sent_at, @text_content, @html_content, @raw_json + ) + ON CONFLICT(account_id, uid) DO UPDATE SET + subject = excluded.subject, + from_name = excluded.from_name, + from_address = excluded.from_address, + to_addresses = excluded.to_addresses, + sent_at = excluded.sent_at, + text_content = excluded.text_content, + html_content = excluded.html_content, + raw_json = excluded.raw_json +`); + +app.get('/api/health', (_req, res) => { + res.json({ ok: true }); +}); + +app.get('/api/channels', (_req, res) => { + const channels = listChannelsStmt.all().map((channel) => ({ + ...channel, + imap_secure: Boolean(channel.imap_secure), + smtp_secure: Boolean(channel.smtp_secure), + })); + res.json(channels); +}); + +app.post('/api/channels', (req, res) => { + const payload = sanitizeChannelPayload(req.body); + if (!payload.ok) { + return res.status(400).json({ error: payload.error }); + } + + try { + const result = insertChannelStmt.run(payload.data); + const channel = getChannelStmt.get(result.lastInsertRowid); + res.status(201).json(channel); + } catch (error) { + res.status(400).json({ error: error.message }); + } +}); + +app.get('/api/accounts', (_req, res) => { + res.json(listAccountsStmt.all().map(serializeAccount)); +}); + +app.get('/api/channels/:id/accounts', (req, res) => { + res.json(listAccountsByChannelStmt.all(req.params.id).map(serializeAccount)); +}); + +app.post('/api/accounts', (req, res) => { + const payload = sanitizeAccountPayload(req.body); + if (!payload.ok) { + return res.status(400).json({ error: payload.error }); + } + + try { + const result = insertAccountStmt.run(payload.data); + const account = getAccountStmt.get(result.lastInsertRowid); + res.status(201).json(serializeAccount(account)); + } catch (error) { + res.status(400).json({ error: error.message }); + } +}); + +app.get('/api/accounts/export', (_req, res) => { + const content = listAccountsWithSecretsStmt + .all() + .map((account) => `${account.channel_name}----${account.email}----${account.auth_code}`) + .join('\n'); + + res.json({ content }); +}); + +app.post('/api/accounts/import', (req, res) => { + try { + const payload = sanitizeImportPayload(req.body); + if (!payload.ok) { + return res.status(400).json({ error: payload.error }); + } + + const result = importAccounts(payload.lines); + res.json(result); + } catch (error) { + res.status(resolveStatus(error)).json({ error: error.message }); + } +}); + +app.delete('/api/accounts/:id', (req, res) => { + try { + const account = ensureAccount(req.params.id); + deleteAccountWithCache(account.id); + + if (Number(req.params.id) === 1 && account.email === 'demo@qq.com') { + return res.json({ ok: true, removed: account.email }); + } + + res.json({ ok: true, removed: account.email }); + } catch (error) { + res.status(resolveStatus(error)).json({ error: error.message }); + } +}); + +app.get('/api/accounts/:id/messages', async (req, res) => { + try { + const account = ensureAccount(req.params.id); + const channel = ensureChannel(account.channel_id); + const limit = clampLimit(req.query.limit); + const refresh = String(req.query.refresh || '').toLowerCase() === 'true'; + + if (refresh) { + const messages = await withTimeout( + fetchLatestEmails({ channel, account, limit }), + 45000, + '收信超时,请稍后重试' + ); + persistMessages(account.id, messages); + } + + const cached = listCacheByAccountStmt.all(account.id, limit).map(serializeCachedMail); + res.json(cached); + } catch (error) { + res.status(resolveStatus(error)).json({ error: error.message }); + } +}); + +app.get('/api/accounts/:id/messages/latest', async (req, res) => { + try { + const account = ensureAccount(req.params.id); + const channel = ensureChannel(account.channel_id); + const refresh = String(req.query.refresh || 'true').toLowerCase() !== 'false'; + + if (refresh) { + const messages = await withTimeout( + fetchLatestEmails({ channel, account, limit: 1 }), + 45000, + '收信超时,请稍后重试' + ); + persistMessages(account.id, messages); + } + + const latest = latestCacheByAccountStmt.get(account.id); + if (!latest) { + return res.status(404).json({ error: '该邮箱暂无邮件' }); + } + + res.json(serializeCachedMail(latest)); + } catch (error) { + res.status(resolveStatus(error)).json({ error: error.message }); + } +}); + +app.get('/api/accounts/:id/messages/:uid', async (req, res) => { + try { + const account = ensureAccount(req.params.id); + const channel = ensureChannel(account.channel_id); + const refresh = String(req.query.refresh || 'true').toLowerCase() !== 'false'; + const uid = decodeURIComponent(req.params.uid); + + if (refresh) { + const message = await withTimeout( + fetchMessageDetail({ channel, account, compositeUid: uid }), + 45000, + '加载邮件正文超时,请稍后重试' + ); + if (message) { + persistMessages(account.id, [message]); + } + } + + const cached = getCacheByAccountUidStmt.get(account.id, uid); + if (!cached) { + return res.status(404).json({ error: '邮件不存在' }); + } + + res.json(serializeCachedMail(cached)); + } catch (error) { + res.status(resolveStatus(error)).json({ error: error.message }); + } +}); + +app.post('/api/accounts/:id/messages/send', async (req, res) => { + try { + const result = await sendMailForAccount(req.params.id, req.body); + res.json({ ok: true, ...result }); + } catch (error) { + res.status(resolveStatus(error)).json({ error: error.message }); + } +}); + +app.post('/api/send', async (req, res) => { + const accountId = req.body.accountId; + if (!accountId) { + return res.status(400).json({ error: '缺少 accountId' }); + } + + try { + const result = await sendMailForAccount(accountId, req.body); + res.json({ ok: true, ...result }); + } catch (error) { + res.status(resolveStatus(error)).json({ error: error.message }); + } +}); + +app.get('/api/messages/latest', async (req, res) => { + const accountId = req.query.accountId; + if (!accountId) { + return res.status(400).json({ error: '缺少 accountId' }); + } + + try { + const account = ensureAccount(accountId); + const channel = ensureChannel(account.channel_id); + const refresh = String(req.query.refresh || 'true').toLowerCase() !== 'false'; + + if (refresh) { + const messages = await withTimeout( + fetchLatestEmails({ channel, account, limit: 1 }), + 45000, + '收信超时,请稍后重试' + ); + persistMessages(account.id, messages); + } + + const latest = latestCacheByAccountStmt.get(account.id); + if (!latest) { + return res.status(404).json({ error: '该邮箱暂无邮件' }); + } + + res.json(serializeCachedMail(latest)); + } catch (error) { + res.status(resolveStatus(error)).json({ error: error.message }); + } +}); + +app.get('/api/messages', async (req, res) => { + const accountId = req.query.accountId; + if (!accountId) { + return res.status(400).json({ error: '缺少 accountId' }); + } + + try { + const account = ensureAccount(accountId); + const channel = ensureChannel(account.channel_id); + const limit = clampLimit(req.query.limit); + const refresh = String(req.query.refresh || '').toLowerCase() === 'true'; + + if (refresh) { + const messages = await withTimeout( + fetchLatestEmails({ channel, account, limit }), + 45000, + '收信超时,请稍后重试' + ); + persistMessages(account.id, messages); + } + + res.json(listCacheByAccountStmt.all(account.id, limit).map(serializeCachedMail)); + } catch (error) { + res.status(resolveStatus(error)).json({ error: error.message }); + } +}); + +app.get(/^(?!\/api).*/, (_req, res) => { + res.sendFile(path.join(__dirname, 'public', 'index.html')); +}); + +app.listen(port, () => { + console.log(`mail-sr running at http://localhost:${port}`); +}); + +function serializeAccount(account) { + return { + ...account, + auth_code: undefined, + }; +} + +function sanitizeChannelPayload(body) { + const data = { + name: String(body.name || '').trim(), + code: String(body.code || '').trim().toLowerCase(), + imap_host: String(body.imap_host || '').trim(), + imap_port: Number(body.imap_port), + imap_secure: body.imap_secure === false ? 0 : 1, + smtp_host: String(body.smtp_host || '').trim(), + smtp_port: Number(body.smtp_port), + smtp_secure: body.smtp_secure === false ? 0 : 1, + }; + + if (!data.name || !data.code || !data.imap_host || !data.smtp_host || !data.imap_port || !data.smtp_port) { + return { ok: false, error: '渠道参数不完整' }; + } + + return { ok: true, data }; +} + +function sanitizeAccountPayload(body) { + const data = { + channel_id: Number(body.channel_id), + email: String(body.email || '').trim(), + auth_code: String(body.auth_code || '').trim(), + display_name: String(body.display_name || '').trim() || null, + }; + + if (!data.channel_id || !data.email || !data.auth_code) { + return { ok: false, error: '帐号参数不完整' }; + } + + return { ok: true, data }; +} + +function sanitizeSendPayload(body) { + const data = { + from: body.from ? String(body.from).trim() : undefined, + to: String(body.to || '').trim(), + cc: body.cc ? String(body.cc).trim() : undefined, + bcc: body.bcc ? String(body.bcc).trim() : undefined, + subject: String(body.subject || '').trim(), + text: body.text ? String(body.text) : undefined, + html: body.html ? String(body.html) : undefined, + }; + + if (!data.to || !data.subject || (!data.text && !data.html)) { + return { ok: false, error: '发信参数不完整,需要 to、subject、text/html' }; + } + + return { ok: true, data }; +} + +function sanitizeImportPayload(body) { + const content = String(body.content || '').replace(/\r\n/g, '\n').trim(); + if (!content) { + return { ok: false, error: '导入内容不能为空' }; + } + + const lines = content + .split('\n') + .map((line) => line.trim()) + .filter(Boolean); + + if (!lines.length) { + return { ok: false, error: '导入内容不能为空' }; + } + + return { ok: true, lines }; +} + +function ensureAccount(id) { + const account = getAccountStmt.get(id); + if (!account) { + const error = new Error('邮箱帐号不存在'); + error.status = 404; + throw error; + } + return account; +} + +function ensureChannel(id) { + const channel = getChannelStmt.get(id); + if (!channel) { + const error = new Error('邮箱渠道不存在'); + error.status = 404; + throw error; + } + return channel; +} + +function importAccounts(lines) { + const transaction = db.transaction((rawLines) => { + let created = 0; + let updated = 0; + + rawLines.forEach((line, index) => { + const lineNumber = index + 1; + const parts = line.split('----').map((item) => item.trim()); + if (parts.length !== 3 || parts.some((item) => !item)) { + const error = new Error(`第 ${lineNumber} 行格式错误,应为:渠道名----帐号----授权码`); + error.status = 400; + throw error; + } + + const [channelName, email, authCode] = parts; + const channel = getChannelByNameStmt.get(channelName); + if (!channel) { + const error = new Error(`第 ${lineNumber} 行渠道不存在:${channelName}`); + error.status = 400; + throw error; + } + + const existing = getAccountByChannelEmailStmt.get(channel.id, email); + if (existing) { + updateAccountAuthCodeStmt.run(authCode, existing.id); + updated += 1; + return; + } + + insertAccountStmt.run({ + channel_id: channel.id, + email, + auth_code: authCode, + display_name: null, + }); + created += 1; + }); + + return { + ok: true, + created, + updated, + total: rawLines.length, + }; + }); + + return transaction(lines); +} + +function persistMessages(accountId, messages) { + const transaction = db.transaction((list) => { + for (const message of list) { + const from = message.from[0] || {}; + upsertMailCacheStmt.run({ + account_id: accountId, + uid: message.uid, + folder: message.folder || 'INBOX', + subject: message.subject || '', + from_name: from.name || '', + from_address: from.address || '', + to_addresses: JSON.stringify(message.to || []), + sent_at: message.date || null, + text_content: message.text || '', + html_content: message.html || '', + raw_json: JSON.stringify(message), + }); + } + }); + + transaction(messages); +} + +async function sendMailForAccount(accountId, body) { + const account = ensureAccount(accountId); + const channel = ensureChannel(account.channel_id); + const payload = sanitizeSendPayload(body); + if (!payload.ok) { + const error = new Error(payload.error); + error.status = 400; + throw error; + } + + return sendMail({ channel, account, payload: payload.data }); +} + +function deleteAccountWithCache(accountId) { + const transaction = db.transaction((id) => { + deleteMailCacheByAccountStmt.run(id); + deleteAccountStmt.run(id); + }); + + transaction(accountId); +} + +function serializeCachedMail(row) { + return { + id: row.id, + uid: row.uid, + folder: row.folder, + subject: row.subject, + from: { + name: row.from_name, + address: row.from_address, + }, + to: JSON.parse(row.to_addresses || '[]'), + date: row.sent_at, + text: row.text_content, + html: row.html_content, + raw: JSON.parse(row.raw_json), + }; +} + +function clampLimit(value) { + const number = Number(value || 20); + if (Number.isNaN(number)) { + return 20; + } + return Math.min(Math.max(number, 1), 100); +} + +function resolveStatus(error) { + return error.status || 500; +} + +function withTimeout(promise, timeoutMs, message) { + return Promise.race([ + promise, + new Promise((_, reject) => { + setTimeout(() => { + const error = new Error(message); + error.status = 504; + reject(error); + }, timeoutMs); + }), + ]); +}