mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-08-28 11:37:42 +08:00
Compare commits
64
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4911b3ec05 | ||
|
|
e5c3c7bf71 | ||
|
|
2299c56040 | ||
|
|
624fc9bb96 | ||
|
|
12152fc893 | ||
|
|
a09ede8944 | ||
|
|
f9281818e9 | ||
|
|
5553c6484a | ||
|
|
d04c1a865d | ||
|
|
116ddc7324 | ||
|
|
2dcbad40ad | ||
|
|
95badf5aed | ||
|
|
b3666c0900 | ||
|
|
e499211197 | ||
|
|
342fe22e4f | ||
|
|
8c883b269f | ||
|
|
4c1e593d07 | ||
|
|
7eaa3b3b8e | ||
|
|
0581632b59 | ||
|
|
4ce22ef249 | ||
|
|
99b332345b | ||
|
|
565bb839db | ||
|
|
dbd1f8706d | ||
|
|
3f1d800e90 | ||
|
|
70b30c2494 | ||
|
|
1a1dd720c8 | ||
|
|
2d501d82cf | ||
|
|
7c57592742 | ||
|
|
41105ed803 | ||
|
|
c924b71a5c | ||
|
|
4f0b44de2e | ||
|
|
f6fcbe793c | ||
|
|
05a8ebb590 | ||
|
|
b7718100c5 | ||
|
|
bf786947e3 | ||
|
|
cfb31807f1 | ||
|
|
308fbe2f9a | ||
|
|
44b29aa646 | ||
|
|
2221342560 | ||
|
|
add0124cfd | ||
|
|
8324b133fb | ||
|
|
74c8e8f7e4 | ||
|
|
2df4de22d9 | ||
|
|
437db7c050 | ||
|
|
dd294037ab | ||
|
|
a9e2c89246 | ||
|
|
72bbfe8fd6 | ||
|
|
796a5e4ac5 | ||
|
|
347be5c762 | ||
|
|
0b17953097 | ||
|
|
ec3e9f0b1f | ||
|
|
fb6e77fded | ||
|
|
c703082391 | ||
|
|
4e01b4e26f | ||
|
|
7d68cbdffa | ||
|
|
6d43c107d8 | ||
|
|
46fbb4c099 | ||
|
|
c9698d03f1 | ||
|
|
5f955ccca6 | ||
|
|
7e7f824f88 | ||
|
|
eb62c37e02 | ||
|
|
1e50bb0933 | ||
|
|
063b6be2b1 | ||
|
|
e94ff52add |
Symlink
+1
@@ -0,0 +1 @@
|
||||
../.claude/skills
|
||||
+1
@@ -0,0 +1 @@
|
||||
../../skills/cf-temp-mail-agent-mail
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: cf-temp-mail-upgrade-dependencies
|
||||
description: Upgrade npm dependencies across all sub-packages of the project. Use when the user asks to upgrade/update dependencies, bump deps, refresh lockfiles, or update wrangler. Runs pnpm upgrades on frontend/, worker/, pages/, and vitepress-docs/.
|
||||
description: Upgrade npm dependencies across all sub-packages of the project. Use when the user asks to upgrade/update dependencies, bump deps, refresh lockfiles, or update wrangler. Runs pnpm upgrades on frontend/, worker/, pages/, and vitepress-docs/, plus npm upgrades on e2e/.
|
||||
---
|
||||
|
||||
# Upgrade Dependencies
|
||||
@@ -23,18 +23,21 @@ The script runs the following in order:
|
||||
| `worker/` | `pnpm up` + `pnpm add -D wrangler@latest` |
|
||||
| `pages/` | `pnpm up` + `pnpm add -D wrangler@latest` |
|
||||
| `vitepress-docs/` | `pnpm up --latest` + `pnpm add -D wrangler@latest` |
|
||||
| `e2e/` | `npx --yes npm-check-updates@23.0.0 --upgrade` + `npm install` + Playwright image validation |
|
||||
|
||||
Note: `vitepress-docs/` uses `--latest` (crosses semver ranges); other packages upgrade within ranges only.
|
||||
Note: `vitepress-docs/` and `e2e/` upgrade to the latest versions and may cross semver ranges; other packages upgrade within ranges only. `e2e/` uses npm because it has a `package-lock.json`.
|
||||
|
||||
## Post-upgrade checklist
|
||||
|
||||
1. Inspect `git diff` on `package.json` / `pnpm-lock.yaml` files for reasonable changes.
|
||||
1. Inspect `git diff` on `package.json`, `pnpm-lock.yaml`, and `package-lock.json` files for reasonable changes.
|
||||
2. Verify builds in each sub-package:
|
||||
- `cd frontend && pnpm build`
|
||||
- `cd worker && pnpm build && pnpm lint`
|
||||
- `cd vitepress-docs && pnpm build`
|
||||
3. If wrangler had a major version bump, check `worker/wrangler.toml` for any required syntax changes.
|
||||
4. Commit with Conventional Commits format, e.g. `chore: upgrade dependencies`.
|
||||
- `cd e2e && npm test`
|
||||
3. If Playwright changed, keep `e2e/Dockerfile.e2e` on the matching Playwright image version.
|
||||
4. If wrangler had a major version bump, check `worker/wrangler.toml` for any required syntax changes.
|
||||
5. Commit with Conventional Commits format, e.g. `chore: upgrade dependencies`.
|
||||
|
||||
## Do NOT
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ Upgrade the version number of the cloudflare_temp_email project.
|
||||
4. Update the `VERSION` constant in `worker/src/constants.ts`.
|
||||
5. Insert a new version placeholder at the top of `CHANGELOG.md`.
|
||||
6. Insert a new version placeholder at the top of `CHANGELOG_EN.md`.
|
||||
7. Rename the previous version heading in both changelogs from `## v{OLD_VERSION}(main)` to `## v{OLD_VERSION}` so only the new active development version keeps `(main)`.
|
||||
|
||||
## CHANGELOG format
|
||||
|
||||
@@ -44,7 +45,14 @@ In `CHANGELOG.md`, insert before the existing `## v{OLD_VERSION}(main)` line (i.
|
||||
|
||||
```
|
||||
|
||||
`CHANGELOG_EN.md` uses the same format.
|
||||
After inserting the new placeholder, update the old heading:
|
||||
|
||||
```diff
|
||||
-## v{OLD_VERSION}(main)
|
||||
+## v{OLD_VERSION}
|
||||
```
|
||||
|
||||
`CHANGELOG_EN.md` uses the same format and must receive the same old-heading update.
|
||||
|
||||
## Commit message format
|
||||
|
||||
@@ -53,4 +61,5 @@ feat: upgrade version to v{VERSION}
|
||||
|
||||
- Update version number to {VERSION} in all package.json files
|
||||
- Add v{VERSION} placeholder in CHANGELOG.md
|
||||
- Move (main) marker from v{OLD_VERSION} to v{VERSION}
|
||||
```
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: 24
|
||||
|
||||
- uses: pnpm/action-setup@v5
|
||||
name: Install pnpm
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: 24
|
||||
|
||||
- uses: pnpm/action-setup@v5
|
||||
name: Install pnpm
|
||||
@@ -40,11 +40,17 @@ jobs:
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
WORKFLOW_RUN_HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }}
|
||||
run: |
|
||||
cd vitepress-docs/
|
||||
wget https://github.com/dreamhunter2333/cloudflare_temp_email/releases/latest/download/frontend.zip -O docs/public/ui_install/frontend.zip
|
||||
pnpm install --no-frozen-lockfile
|
||||
TAG_NAME=$(gh release view --json tagName --jq '.tagName')
|
||||
if TAG_NAME=$(gh release view --json tagName --jq '.tagName' 2>/dev/null); then
|
||||
echo "Using release tag $TAG_NAME"
|
||||
else
|
||||
TAG_NAME="${WORKFLOW_RUN_HEAD_BRANCH:-${GITHUB_REF_NAME:-main}}"
|
||||
echo "No GitHub release found for this repo; fallback TAG_NAME=$TAG_NAME"
|
||||
fi
|
||||
echo "Deploying docs for tag $TAG_NAME"
|
||||
export TAG_NAME
|
||||
pnpm run deploy
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: 24
|
||||
|
||||
- uses: pnpm/action-setup@v5
|
||||
name: Install pnpm
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: 24
|
||||
|
||||
- uses: pnpm/action-setup@v5
|
||||
name: Install pnpm
|
||||
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: 24
|
||||
|
||||
- uses: pnpm/action-setup@v5
|
||||
name: Install pnpm
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
PR_REVIEWER.REQUIRE_TESTS_REVIEW: "false"
|
||||
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
|
||||
OPENAI_API_BASE: ${{ secrets.OPENAI_API_BASE }}
|
||||
CONFIG.MODEL: "gpt-4o"
|
||||
CONFIG.MODEL_TURBO: "gpt-4o"
|
||||
CONFIG.MODEL: "gpt-5.4-nano"
|
||||
CONFIG.MODEL_TURBO: "gpt-5.4-nano"
|
||||
OPENAI.API_BASE: ${{ secrets.OPENAI_API_BASE }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: 24
|
||||
|
||||
- uses: pnpm/action-setup@v5
|
||||
name: Install pnpm
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: 24
|
||||
|
||||
- uses: pnpm/action-setup@v5
|
||||
name: Install pnpm
|
||||
@@ -73,7 +73,7 @@ jobs:
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: 24
|
||||
|
||||
- uses: pnpm/action-setup@v5
|
||||
name: Install pnpm
|
||||
|
||||
@@ -142,3 +142,6 @@ pnpm-lock.yaml
|
||||
e2e/test-results/
|
||||
e2e/playwright-report/
|
||||
e2e/.e2e-pids
|
||||
|
||||
# SMTP/IMAP proxy persisted IMAP flags (local dev / bare-metal runs)
|
||||
smtp_proxy_server/data/
|
||||
|
||||
+96
-4
@@ -6,15 +6,107 @@
|
||||
<a href="CHANGELOG_EN.md">English</a>
|
||||
</p>
|
||||
|
||||
## v1.8.0(main)
|
||||
## v1.11.0(main)
|
||||
|
||||
### Features
|
||||
|
||||
- feat: |Frontend Next| 新增 `frontend-next` Vite React + shadcn 命令生成组件的非 admin 邮件客户端,按参考稿 B 风格实现创建/恢复地址、收件箱、写信、地址管理、用户账号集成、设置与亮色/暗色切换
|
||||
|
||||
- docs: |前端/文档| 统一邮箱地址、用户账号与 Admin 权限的表述,并明确发信权限和额度按邮箱地址独立申请与管理
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
### Improvements
|
||||
|
||||
## v1.7.0(main)
|
||||
- fix: |Worker| 地址活跃时间保活增加 1 天写入窗口,用户设置和邮箱访问不再重复更新近期活跃地址,降低 D1 写入量(issue #1103)
|
||||
|
||||
- feat: |用户系统| 用户绑定地址列表改用服务端分页,并仅在第一页查询总数;用户邮件列表改用 JOIN、删除改用 `EXISTS` 在数据库侧校验地址归属,避免为大用户加载全部绑定地址(issue #1103)
|
||||
|
||||
- feat: |Worker| 邮件、发件箱及按创建/活跃时间清理地址时改为分批处理,默认每次最多 3000 条并支持通过 `CLEANUP_BATCH_SIZE` 调整(上限 5000),减少单次扫描和删除量(issue #1103)
|
||||
|
||||
### Testing
|
||||
|
||||
- fix: |E2E| 新增近期地址活跃时间不会被用户设置接口重复写入的回归测试
|
||||
- fix: |E2E| 新增清理批次上限、后续批次继续执行、保留未过期数据及地址关联数据清理测试
|
||||
|
||||
## v1.10.0
|
||||
|
||||
### Features
|
||||
|
||||
- feat: |Admin| 新增 `GET /admin/mails/:id` 接口,支持管理员按邮件 ID 跨邮箱读取单封邮件,并兼容 gzip 压缩存储(issue #1096)
|
||||
- feat: |Frontend| 邮箱新增「邮箱全宽列表视图」开关(在外观设置中控制),开启后默认全宽列表展示邮件标题与正文预览,点击单封邮件再展开为双栏,再次点击同一封邮件可回到列表视图;多选模式下点击邮件会同步切换勾选状态与右侧预览,并禁用同邮件点击收回列表,展开时双栏左侧列表宽度仍遵循「邮箱双栏视图左侧列表宽度占比」配置;默认关闭,保留原有双栏行为
|
||||
- feat: |Frontend| 邮箱全宽列表视图新增「正文预览行数」配置(在外观设置中控制),可设置邮件正文预览的最大行数,默认 2 行,0 表示关闭预览
|
||||
- feat: |Frontend| 外观设置新增「自动加载邮件正文中的外部图片」开关,关闭后邮件预览(含全屏视图)会先经 DOMPurify 消毒,并以白名单策略处理所有可能发起请求的位置:仅保留可证明为本地的引用(`cid:`、`data:image/`、`blob:` 与站内相对路径),其余一律阻断;`base`、`meta`、`script`、`link`、`iframe`、`object`、`embed`、`noscript` 等会自行取用资源或改变解析基准的元素在此模式下移除,`<style>` 保留但其中 `url()`、`image-set()`、`@import` 的远端引用会被替换。正文上方显示已阻断资源数量的提示条,可一键按封加载;默认保持开启,行为与此前一致(issue #1073)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- fix: |Frontend| 关闭邮件外部图片自动加载时保留 `<a>` 与 `<area>` 的外部导航链接,并阻断通过 CSS 转义函数名或 at-rule 绕过远程资源过滤的情况
|
||||
- fix: |Frontend| 使用共享 DOMPurify 净化逻辑处理关于页面与启动通知中的 HTML 公告,避免 `ANNOUNCEMENT` 中的可执行标签或事件属性造成 XSS
|
||||
- fix: |Worker| 按邮件认证规范修复垃圾邮件检测:SPF、DKIM、DMARC 的 `none` 及 SPF/DKIM `neutral` 按认证方法不存在处理,并忽略未注册结果和不支持的方法版本;`JUNK_MAIL_FORCE_PASS_LIST` 仍要求明确返回受支持的 `pass`
|
||||
- fix: |Admin| 管理后台删除邮箱地址时,先删除该地址的邮件、发件记录、自动回复等关联数据,最后再删除地址本身;此前地址行先被删除导致按地址名匹配的子查询查不到数据,邮件等记录被遗留在数据库中
|
||||
- fix: |AI 提取| 强化提示词,要求 AI 保持邮件原始链接域名,避免小模型改写验证链接域名导致错误跳转(issue #1072)
|
||||
- fix: |AI 提取| HTML-only 邮件在发送给 Workers AI 前会先压缩为可读文本,避免样式模板过长导致验证码位于 4000 字截断之后而无法识别
|
||||
- fix: |Frontend| 移动端 Header 增加页头内边距,避免标题、菜单按钮与屏幕边缘过近
|
||||
- fix: |IMAP 代理| 修复 IMAP `STORE` 无法真正标记邮件已读的问题:邮件不再硬编码为 `\Seen`,且 `SimpleMailbox` 的 flags 变更现持久化到本地 SQLite(新增 `imap_flag_db_path` 配置),使已读/未读状态可在客户端断线重连(如 Thunderbird 轮询)后保留,而非每次新建连接即丢失(issue #1074)
|
||||
- fix: |IMAP 代理| 修复 `SEARCH UNSEEN` 返回全部邮件的问题:`SimpleMailbox.search()` 现按持久化的 flags 计算 `SEEN`/`UNSEEN`/`FLAGGED`/`DELETED`/`ANSWERED`/`DRAFT` 及其否定形式,多个条件按 AND 组合;无法识别的检索条件仍沿用原有行为返回全部邮件
|
||||
- fix: |IMAP 代理| 修复取信不会自动标记已读的问题:`BODY[...]`、`RFC822`、`RFC822.TEXT` 取信现按 RFC 3501 自动置 `\Seen`,而 `BODY.PEEK[...]`、`RFC822.HEADER` 及仅取元数据(如 `FLAGS`)不会
|
||||
|
||||
### Testing
|
||||
|
||||
- test: |Worker| 新增 junk_mail_policy 回归测试(issue #1084):覆盖 SPF/DKIM/DMARC 的 `none`/`neutral` 按认证方法不存在处理、明确 `fail` 仍被拒收,以及 `JUNK_MAIL_FORCE_PASS_LIST` 仅接受明确 `pass`
|
||||
|
||||
### Improvements
|
||||
|
||||
- docs: |README| 新增完整日文 README,并在中文和英文 README 中添加日文导航链接
|
||||
- feat: |Frontend| 「邮箱双栏视图左侧列表宽度占比」最小值由 0.25 放宽至 0,左侧列表可完全折叠使正文近乎全屏,刻度增加 0 点;收件箱与发件箱的双栏拆分同步生效,并优化外观设置文案以明确该比例控制左侧邮件列表宽度
|
||||
|
||||
## v1.9.0
|
||||
|
||||
### Features
|
||||
|
||||
- feat: |AI 识别| 未配置 Workers AI 绑定时,自动回退到内置正则提取验证码(支持中英日韩,并排除年份与 `YYYYMMDD` 日期误判),让无 Workers AI 的自部署用户也能在 Telegram 推送与 Webhook 中拿到验证码
|
||||
- feat: |Telegram| Telegram 新邮件推送与 `/mails` 历史邮件查看支持展示 AI 提取结果,包含验证码、验证链接、服务链接、订阅链接等关键信息
|
||||
- feat: |Webhook| 邮件 Webhook 模板支持填充 AI 提取结果占位符,包括 `aiExtractType`、`aiExtractResult`、`aiExtractResultText`
|
||||
- feat: |Frontend| 新增 `DISABLE_SHOW_GITHUB_FOR_USER` 配置,可仅对普通用户隐藏 Header 的 GitHub/版本入口,admin 仍可见(issue #1041)
|
||||
- feat: |Frontend| 将邮箱地址凭证弹窗升级为“地址凭证与连接方式”,复用普通用户与 admin 创建邮箱结果弹窗;支持通过 `ENABLE_AGENT_EMAIL_INFO` 展示 AI Agent 接入信息,并通过 `SMTP_IMAP_PROXY_CONFIG` 展示 SMTP/IMAP 客户端连接信息
|
||||
- docs: |随机子域名| 在前端“启用随机子域名”提示与 `subdomain` / `worker-vars` 文档(中英)中明确说明:要让 `name@<随机>.abc.com` 真正收到邮件,必须在基础域名 DNS 中为 `*` 子域添加通配 MX 记录,Email Routing 子域不继承父域配置(issue #1035)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- fix: |Admin| 管理员重置邮箱地址密码时改为前端 SHA-256 后提交,后端只接受并存储哈希值,避免该接口继续接收明文密码
|
||||
- fix: |Address| 管理员邮箱地址列表与用户绑定地址列表不再返回已存储的地址密码哈希值,避免列表接口暴露敏感字段
|
||||
- fix: |Address| 统一规范化配置域名、收件地址域名与前缀的空白和大小写,覆盖 `DOMAINS`、`DEFAULT_DOMAINS`、`USER_ROLES.domains`、随机子域名、转发规则、SMTP 与 `SEND_MAIL` 域名匹配,保留转发规则空域名 catch-all 行为,并明确空 `DEFAULT_DOMAINS` / 角色域名回退到 `DOMAINS` 的行为,避免大小写配置或入站收件域名导致创建、收件、转发或发信失败(issue #926)
|
||||
- fix: |AI 提取| 将 AI 邮件识别默认 Workers AI 模型切换为支持 JSON Mode 且未弃用的 `@cf/meta/llama-3.1-8b-instruct-fast`,并在文档中补充 `@cf/zai-org/glm-4.7-flash` 结构化输出兼容性提示(issue #1029)
|
||||
- fix: |CI| 将 GitHub Actions 与 e2e Docker 镜像统一升级到 Node.js 24,适配 Wrangler 4.90.0 的运行时要求
|
||||
- fix: |Frontend| 修复 iOS Safari 点击输入框时因移动端表单控件字号过小导致页面自动放大的问题
|
||||
|
||||
### Improvements
|
||||
|
||||
## v1.8.0
|
||||
|
||||
### Features
|
||||
|
||||
- feat: |Frontend| 前端新增 6 国语言支持(`zh` / `en` / `es` / `pt-BR` / `ja` / `de`),默认语言保持为 `zh`;无 locale 前缀路由(如 `/`、`/user`)默认使用中文渲染,同时会记录浏览器语言作为语言偏好。用户手动切换后会持久化语言偏好,并保持当前页面路径、查询参数与 canonical locale URL 一致
|
||||
- feat: |API| 新增服务端解析邮件接口 `/api/parsed_mails` 与 `/api/parsed_mail/:id`,直接返回 `sender` / `subject` / `text` / `html` / `attachments` 元信息(复用 `commonParseMail`),AI agent 侧不再需要引入 MIME 解析器
|
||||
- feat: |Skill| 新增仓库内置只读 skill `cf-temp-mail-agent-mail`(`skills/cf-temp-mail-agent-mail/`),让 OpenClaw / Codex / Cursor 等 AI agent 凭用户提供的 Address JWT + API 地址读取邮箱、轮询验证码,绕开创建邮箱时的 Turnstile 人机验证;可通过 `npx degit dreamhunter2333/cloudflare_temp_email/skills/cf-temp-mail-agent-mail` 安装
|
||||
- docs: |文档| 新增"AI Agent 使用邮箱"文档(`guide/feature/agent-email`),说明 `parsed_mail` API 用法,并在 parsed API 不可用时给出对齐前端的 `mail-parser-wasm` + `postal-mime` 本地解析回退方案
|
||||
- docs: |文档| 在 `quick-start` / `worker-vars` / `email-routing` 三个入口文档(中英文)显式补充"域名是部署前提条件"提示,强调需先在 Cloudflare 启用 Email Routing 并下发邮件 DNS 记录、Worker 部署后再绑定 Catch-all,子域名需单独启用,避免用户在没有可用域名时直接开始部署却收不到邮件(issue #1004)
|
||||
- docs: |部署排障| 优化近期 issue 暴露的 UI 部署与升级排障文档:补充 `nodejs_compat`、D1 绑定名必须为 `DB`、`/open_api/settings` 校验、后端 API 地址填写、Cloudflare 安全挑战导致 `Network Error`、D1 容量上限与 Cron Trigger 自动清理、GitHub OAuth 公开邮箱、admin 管理口令与用户账号区别、随机二级域名 API 需传 `enableRandomSubdomain` 等说明;同时将帮助/FAQ 菜单移动到核心配置之后,提升可见性
|
||||
- docs: |文档| 补充重新创建旧邮箱提示地址已存在时的处理方式,并完善 GitHub Actions 自动更新配合 Page Functions 转发后端请求的 workflow 说明(issues #947 #654)
|
||||
- docs: |OAuth2| 补充 GitHub 私密邮箱登录配置,说明可使用 `https://api.github.com/user/emails`、JSONPath 邮箱字段和 `user:email` scope 获取主邮箱(issue #655)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- fix: |Frontend| 收窄地址管理相关弹窗宽度,并让地址表格在弹窗内部横向滚动,避免多地址场景撑宽弹窗
|
||||
- fix: |Frontend| 修复 `/open_api/settings` 未返回 `domains` 数组时前端设置初始化直接调用 `map()` 报 `undefined` 错误的问题,统一按空数组兜底处理
|
||||
- fix: |Frontend| 修复前端在 `jwt` / `auth` / `adminAuth` 等 localStorage 凭据为空字符串、字面量 `"undefined"` 或包含换行/控制符时,请求构造的 `Authorization` 等头部抛出 `Invalid character in header content` 导致前端所有接口报错的问题(issue #1000)。新增 `safeHeaderValue` / `safeBearerHeader` 工具,对全部认证头做 RFC 7230 校验,不安全的值直接跳过该头部,让 worker 走标准 401 而不是请求级崩溃
|
||||
- fix: |Frontend| 修复多语言菜单在移动端顶部显示语言与版本按钮导致 Header 横向拥挤或溢出的问题,移动端仅保留菜单按钮并将语言/版本入口放入抽屉
|
||||
|
||||
### Improvements
|
||||
|
||||
- refactor: |Worker| 拆分 `mails_api/index.ts` 与 `admin_api/index.ts`,入口只负责挂路由,业务拆到各自的 `*_api.ts` 文件(`mails_crud.ts` / `new_address.ts` / `parsed_mail_api.ts` / `address_api.ts` / `address_sender_api.ts` / `sendbox_api.ts` / `statistics_api.ts` / `account_settings_api.ts`),保持路径与行为不变
|
||||
|
||||
## v1.7.0
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
@@ -32,7 +124,7 @@
|
||||
|
||||
### Improvements
|
||||
|
||||
## v1.6.0(main)
|
||||
## v1.6.0
|
||||
|
||||
### Features
|
||||
|
||||
@@ -56,7 +148,7 @@
|
||||
- docs: |发送邮件| 增强 SMTP_CONFIG 字段说明和多域名示例,新增发信余额机制说明
|
||||
- docs: |Email Routing| 补充子域名需单独启用 Email Routing 的说明,避免仅在一级域名开启导致子域收不到邮件(#969)
|
||||
|
||||
## v1.5.0(main)
|
||||
## v1.5.0
|
||||
|
||||
### Features
|
||||
|
||||
|
||||
+96
-4
@@ -6,15 +6,107 @@
|
||||
<a href="CHANGELOG_EN.md">English</a>
|
||||
</p>
|
||||
|
||||
## v1.8.0(main)
|
||||
## v1.11.0(main)
|
||||
|
||||
### Features
|
||||
|
||||
- feat: |Frontend Next| Add a `frontend-next` Vite React non-admin mail client using shadcn CLI-generated components in the reference B style, covering address create/restore, inbox, compose, address management, user account integration, settings, and light/dark theme support
|
||||
|
||||
- docs: |Frontend/Docs| Clarify mailbox address, user account, and Admin permission terminology, including address-specific send access and balances
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
### Improvements
|
||||
|
||||
## v1.7.0(main)
|
||||
- fix: |Worker| Throttle address-activity touches to one write per day so user settings and mailbox access do not repeatedly update recently active addresses, reducing D1 writes (issue #1103)
|
||||
|
||||
- feat: |User| Add server-side pagination for bound addresses, with totals queried only on the first page; validate user-mail list ownership with a JOIN and delete ownership with `EXISTS` instead of loading every bound address for large users (issue #1103)
|
||||
|
||||
- feat: |Worker| Process mail, sent-mail, and creation/activity-based address cleanup in batches of 3000 by default, configurable through `CLEANUP_BATCH_SIZE` up to 5000, reducing per-run scans and deletes (issue #1103)
|
||||
|
||||
### Testing
|
||||
|
||||
- fix: |E2E| Add regression coverage ensuring user settings do not rewrite recent address activity timestamps
|
||||
- fix: |E2E| Cover cleanup batch limits, continuation on later runs, preservation of recent data, and address-related data cleanup
|
||||
|
||||
## v1.10.0
|
||||
|
||||
### Features
|
||||
|
||||
- feat: |Admin| Add `GET /admin/mails/:id` for administrators to fetch a single mail by ID across mailboxes, including gzip-compressed storage support (issue #1096)
|
||||
- feat: |Frontend| Add a "Full-width mailbox list view" toggle in Appearance settings. When enabled, the mailbox shows a full-width list of subjects and body previews by default; clicking a mail expands it into the two-pane split view, clicking the same mail again returns to the list view; in multi-select mode, clicking a mail updates both its checked state and the right-side preview while disabling same-mail collapse, and the split width still follows the "Left list width in two-column mailbox view" setting. Defaults to off, preserving the original two-pane behavior
|
||||
- feat: |Frontend| Add "Body Preview Lines" in Appearance settings for the full-width mailbox list view, allowing runtime control over the body-preview clamp. It defaults to 2 lines, and 0 disables previews
|
||||
- feat: |Frontend| Add an "Automatically load external images in mail body" toggle in Appearance settings. When disabled, the mail body (including fullscreen view) is run through DOMPurify and an allowlist policy: only references that can be *proven* local are kept (`cid:`, `data:image/`, `blob:` and same-origin relative paths), everything else is blocked. Elements that fetch on their own or change how relative URLs resolve — `base`, `meta`, `script`, `link`, `iframe`, `object`, `embed`, `noscript` — are removed in this mode, while `<style>` is kept with remote `url()`, `image-set()` and `@import` references substituted. A banner above the body reports how many resources were blocked and loads them for that mail on demand; defaults to on, preserving the previous behavior (issue #1073)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- fix: |Frontend| Preserve external navigation links on `<a>` and `<area>` elements when automatic remote-image loading is disabled, and block remote CSS resources hidden behind escaped function or at-rule names
|
||||
- fix: |Frontend| Sanitize HTML announcements in both the About page and startup notification through a shared DOMPurify helper, preventing executable tags or event attributes in `ANNOUNCEMENT` from causing XSS
|
||||
- fix: |Worker| Align junk-mail checking with authentication standards: treat SPF, DKIM, and DMARC `none` plus SPF/DKIM `neutral` as absent, and ignore unregistered results and unsupported method versions; `JUNK_MAIL_FORCE_PASS_LIST` still requires an explicit supported `pass`
|
||||
- fix: |Admin| When deleting an address from the admin panel, delete its mails, sender records, sendbox and auto-reply entries before removing the address row itself; previously the address row was deleted first, so the name-based subqueries matched nothing and the mails were left orphaned in the database
|
||||
- fix: |AI Extract| Strengthen the prompt to keep original link domains from the email, preventing small models from rewriting verification-link domains (issue #1072)
|
||||
- fix: |AI Extract| Convert HTML-only mail bodies into compact readable text before sending them to Workers AI, preventing long templates from pushing verification codes past the 4000-character truncation window
|
||||
- fix: |Frontend| Add mobile Header page padding so the title and menu button no longer sit too close to the screen edge
|
||||
- fix: |IMAP Proxy| Fix IMAP `STORE` not actually marking mail as read: messages are no longer hardcoded to `\Seen`, and `SimpleMailbox` flag changes are now persisted to a local SQLite file (new `imap_flag_db_path` setting) so the read/unread state survives a client disconnect and reconnect (e.g. Thunderbird polling) instead of resetting on every new connection (issue #1074)
|
||||
- fix: |IMAP Proxy| Fix `SEARCH UNSEEN` returning every message: `SimpleMailbox.search()` now evaluates `SEEN`/`UNSEEN`/`FLAGGED`/`DELETED`/`ANSWERED`/`DRAFT` and their negations against the persisted flags, combining multiple keys with AND; search keys it cannot evaluate keep the previous behaviour of matching everything
|
||||
- fix: |IMAP Proxy| Fix fetches never marking mail as read: `BODY[...]`, `RFC822` and `RFC822.TEXT` fetches now set `\Seen` per RFC 3501, while `BODY.PEEK[...]`, `RFC822.HEADER` and metadata-only fetches (e.g. `FLAGS`) do not
|
||||
|
||||
### Testing
|
||||
|
||||
- test: |Worker| Add junk_mail_policy regression tests (issue #1084): `none`/`neutral` results for SPF/DKIM/DMARC are treated as the method being absent, explicit `fail` results are still rejected, and `JUNK_MAIL_FORCE_PASS_LIST` only accepts an explicit `pass`
|
||||
|
||||
### Improvements
|
||||
|
||||
- docs: |README| Add a complete Japanese README and Japanese navigation links to the Chinese and English READMEs
|
||||
- feat: |Frontend| Lower the "Left list width in two-column mailbox view" minimum from 0.25 to 0 so the left list pane can fully collapse for a near-fullscreen content view, with a 0 mark added; applies to both the inbox and send-box two-pane splits, and clarifies the Appearance setting label so it is clear the value controls the left mail list width
|
||||
|
||||
## v1.9.0
|
||||
|
||||
### Features
|
||||
|
||||
- feat: |AI Extract| Fall back to a built-in regex verification-code extractor (English / Chinese / Japanese / Korean, with year and `YYYYMMDD` date rejection) when no Workers AI binding is configured, so self-hosted deployments without Workers AI still surface codes in Telegram pushes and webhooks
|
||||
- feat: |Telegram| Show AI extraction results in Telegram new-mail notifications and `/mails` history views, including verification codes, auth links, service links, and subscription links
|
||||
- feat: |Webhook| Support AI extraction placeholders in mail webhook templates, including `aiExtractType`, `aiExtractResult`, and `aiExtractResultText`
|
||||
- feat: |Frontend| Add `DISABLE_SHOW_GITHUB_FOR_USER` to hide the Header GitHub/version entry from normal users while keeping it visible to admin users (issue #1041)
|
||||
- feat: |Frontend| Upgrade the address credential dialog to "Address Credentials & Connection Methods" and reuse it for both normal users and admin-created addresses; support showing AI Agent access via `ENABLE_AGENT_EMAIL_INFO` and SMTP/IMAP client settings via `SMTP_IMAP_PROXY_CONFIG`
|
||||
- docs: |Random Subdomain| Clarify in the "Use Random Subdomain" frontend tip and the `subdomain` / `worker-vars` docs (zh & en) that receiving mail on `name@<random>.abc.com` requires a wildcard `*` MX record under the base domain in DNS, because Cloudflare Email Routing does not inherit the apex configuration onto subdomains (issue #1035)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- fix: |Admin| Hash address passwords in the frontend before admin reset requests, and make the backend accept and store only the hash instead of plaintext
|
||||
- fix: |Address| Stop returning stored address password hashes from the admin address list and user bound-address list APIs to avoid exposing sensitive fields
|
||||
- fix: |Address| Normalize whitespace and casing for configured domains, inbound recipient domains, and prefixes across `DOMAINS`, `DEFAULT_DOMAINS`, `USER_ROLES.domains`, random subdomains, forwarding rules, SMTP, and `SEND_MAIL` domain matching, preserve blank-domain catch-all forwarding rules, and clarify that empty `DEFAULT_DOMAINS` / role domains fall back to `DOMAINS`, to avoid create, receive, forward, or send failures caused by mixed-case configuration or inbound recipient domains (issue #926)
|
||||
- fix: |AI Extract| Switch the default Workers AI model for AI email recognition to the JSON Mode-compatible, non-deprecated `@cf/meta/llama-3.1-8b-instruct-fast`, and document structured-output compatibility guidance for `@cf/zai-org/glm-4.7-flash` (issue #1029)
|
||||
- fix: |CI| Upgrade GitHub Actions and e2e Docker images to Node.js 24 to satisfy Wrangler 4.90.0 runtime requirements
|
||||
- fix: |Frontend| Prevent iOS Safari from auto-zooming the page when focusing mobile form controls with small font sizes
|
||||
|
||||
### Improvements
|
||||
|
||||
## v1.8.0
|
||||
|
||||
### Features
|
||||
|
||||
- feat: |Frontend| Add six-language frontend support (`zh` / `en` / `es` / `pt-BR` / `ja` / `de`), keep `zh` as the default locale; locale-unprefixed routes (for example `/` and `/user`) render in Chinese by default while still recording browser language as the stored preference. Explicit locale switches are persisted, and the current route, query string, and canonical locale URL stay in sync during switching
|
||||
- feat: |API| Add server-side parsed-mail endpoints `/api/parsed_mails` and `/api/parsed_mail/:id` that return `sender` / `subject` / `text` / `html` / `attachments` metadata directly (reuses `commonParseMail`), so AI agents no longer need a client-side MIME parser
|
||||
- feat: |Skill| Bundle a read-only skill `cf-temp-mail-agent-mail` (`skills/cf-temp-mail-agent-mail/`) so AI agents like OpenClaw / Codex / Cursor can consume a mailbox with a user-supplied Address JWT + API base URL — list mails, poll verification codes, etc. — sidestepping the Turnstile challenge required to create a mailbox. Install via `npx degit dreamhunter2333/cloudflare_temp_email/skills/cf-temp-mail-agent-mail`
|
||||
- docs: |Docs| Add "AI Agent Mailbox Usage" doc (`guide/feature/agent-email`) covering the `parsed_mail` API and a local-parse fallback using `mail-parser-wasm` + `postal-mime` (mirrors the frontend) when parsed endpoints are unavailable
|
||||
- docs: |Docs| Make "a domain is a hard prerequisite" explicit at the top of `quick-start`, `worker-vars`, and `email-routing` (zh + en), spelling out that Cloudflare Email Routing must be enabled with email DNS records provisioned before deployment, the Catch-all rule must be bound after the Worker is deployed, and subdomains do not inherit the parent domain's Email Routing — so users no longer start deploying without a usable domain and end up unable to receive mail (issue #1004)
|
||||
- docs: |Deployment troubleshooting| Improve docs for recent UI-deployment and upgrade issues: document `nodejs_compat`, the required uppercase `DB` D1 binding, `/open_api/settings` verification, backend API URL entry, Cloudflare security challenges causing `Network Error`, D1 size limits and Cron Trigger cleanup, GitHub OAuth public email requirements, the difference between admin passwords and user accounts, and the `enableRandomSubdomain` API flag; move the Help/FAQ menu directly after Core Configuration so it is easier to find
|
||||
- docs: |Docs| Document how to handle the "address already exists" case when recreating an old mailbox, and clarify the GitHub Actions workflow for automatic updates with Page Functions forwarding backend requests (issues #947 #654)
|
||||
- docs: |OAuth2| Document GitHub private-email login configuration using `https://api.github.com/user/emails`, a JSONPath email field, and the `user:email` scope to read the primary email (issue #655)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- fix: |Frontend| Narrow address-management modal widths and keep address tables horizontally scrollable inside the modal to prevent multi-address lists from stretching the dialog
|
||||
- fix: |Frontend| Fix the frontend settings bootstrap throwing an `undefined` error when `/open_api/settings` does not return a `domains` array by normalizing the field to an empty array before mapping it
|
||||
- fix: |Frontend| Fix every API call crashing client-side with `Invalid character in header content ["Authorization"]` when stale localStorage credentials (`jwt` / `auth` / `adminAuth` / `userJwt` / `access_token`) are empty, the literal string `"undefined"`, or contain a stray newline or other control character (issue #1000). Adds `safeHeaderValue` / `safeBearerHeader` helpers that validate every auth header against RFC 7230 and omit the header entirely when unsafe, so the worker returns a clean 401 instead of the request being rejected by axios/undici
|
||||
- fix: |Frontend| Fix the multilingual header on mobile by keeping only the menu button in the top bar and moving language/version actions into the drawer to avoid horizontal crowding or overflow
|
||||
|
||||
### Improvements
|
||||
|
||||
- refactor: |Worker| Split `mails_api/index.ts` and `admin_api/index.ts` so the index files only wire routes. Business logic moved into dedicated `*_api.ts` files (`mails_crud.ts` / `new_address.ts` / `parsed_mail_api.ts` / `address_api.ts` / `address_sender_api.ts` / `sendbox_api.ts` / `statistics_api.ts` / `account_settings_api.ts`). Paths and behavior unchanged
|
||||
|
||||
## v1.7.0
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
@@ -32,7 +124,7 @@
|
||||
|
||||
### Improvements
|
||||
|
||||
## v1.6.0(main)
|
||||
## v1.6.0
|
||||
|
||||
### Features
|
||||
|
||||
@@ -56,7 +148,7 @@
|
||||
- docs: |Email Sending| Enhance SMTP_CONFIG field reference and multi-domain examples, add send balance mechanism documentation
|
||||
- docs: |Email Routing| Note that subdomains require Email Routing to be enabled separately; enabling it only on the apex domain does not cover subdomains (#969)
|
||||
|
||||
## v1.5.0(main)
|
||||
## v1.5.0
|
||||
|
||||
### Features
|
||||
|
||||
|
||||
@@ -30,7 +30,8 @@
|
||||
|
||||
<p align="center">
|
||||
<a href="README.md">中文文档</a> |
|
||||
<a href="README_EN.md">English Document</a>
|
||||
<a href="README_EN.md">English Document</a> |
|
||||
<a href="README_JA.md">日本語ドキュメント</a>
|
||||
</p>
|
||||
|
||||
> 本项目仅供学习和个人用途,请勿将其用于任何违法行为,否则后果自负。
|
||||
@@ -40,7 +41,9 @@
|
||||
- **完全免费** - 基于 Cloudflare 免费服务构建,零成本运行
|
||||
- **高性能** - Rust WASM 邮件解析,响应速度极快
|
||||
- **现代化界面** - 响应式设计,支持多语言,操作简便
|
||||
- **地址密码** - 支持为邮箱地址设置独立密码,增强安全性 (通过 `ENABLE_ADDRESS_PASSWORD` 启用)
|
||||
- **地址密码** - 支持为邮箱地址设置独立密码,增强安全性
|
||||
- **Agent 友好** - 内置邮箱 [`skill`](skills/cf-temp-mail-agent-mail/SKILL.md),方便 AI agent 使用邮箱
|
||||
- **移动端管理** - 社区客户端 [CloudMail](https://github.com/Lur1N77777/CloudMail),支持 Android 管理后台和邮箱管理
|
||||
|
||||
## 部署文档 - 快速开始
|
||||
|
||||
@@ -72,9 +75,9 @@
|
||||
<summary>Star History(点击收缩/展开)</summary>
|
||||
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=dreamhunter2333/cloudflare_temp_email&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=dreamhunter2333/cloudflare_temp_email&type=Date" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=dreamhunter2333/cloudflare_temp_email&type=Date" />
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://star-history.dera.page/svg?repos=dreamhunter2333/cloudflare_temp_email&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://star-history.dera.page/svg?repos=dreamhunter2333/cloudflare_temp_email&type=Date" />
|
||||
<img alt="Star History Chart" src="https://star-history.dera.page/svg?repos=dreamhunter2333/cloudflare_temp_email&type=Date" />
|
||||
</picture>
|
||||
|
||||
</details>
|
||||
@@ -150,6 +153,8 @@
|
||||
- [x] Webhook 支持,消息推送集成
|
||||
- [x] 支持 `CF Turnstile` 人机验证
|
||||
- [x] 限流配置,防止滥用
|
||||
- [x] **Agent 友好**:内置 [`cf-temp-mail-agent-mail`](skills/cf-temp-mail-agent-mail/SKILL.md) skill,AI agent 可直接消费邮箱,详见 [文档](vitepress-docs/docs/zh/guide/feature/agent-email.md)
|
||||
- [x] 社区移动端管理客户端:[CloudMail](https://github.com/Lur1N77777/CloudMail) 基于 Expo / React Native,面向本项目兼容 API,提供 Android 管理员后台、地址管理、收件/发件/未知邮件、验证码快捷复制、OLED 黑主题和本地分组。
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
+10
-5
@@ -30,7 +30,8 @@
|
||||
|
||||
<p align="center">
|
||||
<a href="README.md">中文文档</a> |
|
||||
<a href="README_EN.md">English Document</a>
|
||||
<a href="README_EN.md">English Document</a> |
|
||||
<a href="README_JA.md">日本語ドキュメント</a>
|
||||
</p>
|
||||
|
||||
> This project is for learning and personal use only. Please do not use it for any illegal activities, or you will be responsible for the consequences.
|
||||
@@ -40,7 +41,9 @@
|
||||
- **Completely Free** - Built on Cloudflare's free services with zero cost
|
||||
- **High Performance** - Rust WASM email parsing for extremely fast response
|
||||
- **Modern UI** - Responsive design with multi-language support and easy operation
|
||||
- **Address Password** - Support setting individual passwords for email addresses to enhance security (enabled via `ENABLE_ADDRESS_PASSWORD`)
|
||||
- **Address Password** - Support setting individual passwords for email addresses to enhance security
|
||||
- **Agent-friendly** - Built-in mailbox [`skill`](skills/cf-temp-mail-agent-mail/SKILL.md) for AI agents
|
||||
- **Mobile admin** - Community client [CloudMail](https://github.com/Lur1N77777/CloudMail) for Android admin and mailbox management
|
||||
|
||||
## Deployment Documentation - Quick Start
|
||||
|
||||
@@ -72,9 +75,9 @@ Try it now → [https://mail.awsl.uk/](https://mail.awsl.uk/)
|
||||
<summary>Star History (Click to expand/collapse)</summary>
|
||||
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=dreamhunter2333/cloudflare_temp_email&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=dreamhunter2333/cloudflare_temp_email&type=Date" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=dreamhunter2333/cloudflare_temp_email&type=Date" />
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://star-history.dera.page/svg?repos=dreamhunter2333/cloudflare_temp_email&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://star-history.dera.page/svg?repos=dreamhunter2333/cloudflare_temp_email&type=Date" />
|
||||
<img alt="Star History Chart" src="https://star-history.dera.page/svg?repos=dreamhunter2333/cloudflare_temp_email&type=Date" />
|
||||
</picture>
|
||||
|
||||
</details>
|
||||
@@ -150,6 +153,8 @@ Try it now → [https://mail.awsl.uk/](https://mail.awsl.uk/)
|
||||
- [x] Webhook support and message push integration
|
||||
- [x] Support `CF Turnstile` CAPTCHA verification
|
||||
- [x] Rate limiting configuration to prevent abuse
|
||||
- [x] **Agent-friendly**: bundled [`cf-temp-mail-agent-mail`](skills/cf-temp-mail-agent-mail/SKILL.md) skill lets AI agents consume a mailbox directly, see [docs](vitepress-docs/docs/en/guide/feature/agent-email.md)
|
||||
- [x] Community mobile admin client: [CloudMail](https://github.com/Lur1N77777/CloudMail) is built with Expo / React Native for this project's compatible API, providing an Android admin console, address management, inbox/sent/unknown mail, quick verification-code copy, OLED black theme, and local grouping.
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
+202
@@ -0,0 +1,202 @@
|
||||
<!-- markdownlint-disable-file MD033 MD045 -->
|
||||
# Cloudflare 一時メール - 無料で構築できる一時メールサービス
|
||||
|
||||
<p align="center">
|
||||
<a href="https://temp-mail-docs.awsl.uk" target="_blank">
|
||||
<img alt="docs" src="https://img.shields.io/badge/docs-grey?logo=vitepress">
|
||||
</a>
|
||||
<a href="https://github.com/dreamhunter2333/cloudflare_temp_email/releases/latest" target="_blank">
|
||||
<img src="https://img.shields.io/github/v/release/dreamhunter2333/cloudflare_temp_email">
|
||||
</a>
|
||||
<a href="https://github.com/dreamhunter2333/cloudflare_temp_email/blob/main/LICENSE" target="_blank">
|
||||
<img alt="MIT License" src="https://img.shields.io/github/license/dreamhunter2333/cloudflare_temp_email">
|
||||
</a>
|
||||
<a href="https://github.com/dreamhunter2333/cloudflare_temp_email/graphs/contributors" target="_blank">
|
||||
<img alt="GitHub contributors" src="https://img.shields.io/github/contributors/dreamhunter2333/cloudflare_temp_email">
|
||||
</a>
|
||||
<a href="">
|
||||
<img alt="GitHub top language" src="https://img.shields.io/github/languages/top/dreamhunter2333/cloudflare_temp_email">
|
||||
</a>
|
||||
<a href="">
|
||||
<img src="https://img.shields.io/github/last-commit/dreamhunter2333/cloudflare_temp_email">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://hellogithub.com/repository/2ccc64bb1ba346b480625f584aa19eb1" target="_blank">
|
||||
<img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=2ccc64bb1ba346b480625f584aa19eb1&claim_uid=FxNypXK7UQ9OECT" alt="Featured|HelloGitHub" height="30"/>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="README.md">中文文档</a> |
|
||||
<a href="README_EN.md">English Document</a> |
|
||||
<a href="README_JA.md">日本語ドキュメント</a>
|
||||
</p>
|
||||
|
||||
> 本プロジェクトは学習および個人利用のみを目的としています。違法行為には使用しないでください。使用した場合、その結果については利用者自身が責任を負うものとします。
|
||||
|
||||
**機能の充実した一時メールサービスです!**
|
||||
|
||||
- **完全無料** - Cloudflare の無料サービス上に構築され、運用コストはかかりません
|
||||
- **高性能** - Rust WASM によるメール解析で極めて高速に応答します
|
||||
- **モダンな UI** - 多言語対応のレスポンシブデザインで、簡単に操作できます
|
||||
- **アドレスパスワード** - メールアドレスごとに個別のパスワードを設定し、セキュリティを強化できます
|
||||
- **Agent フレンドリー** - AI agent がメールボックスを利用できる組み込みの [`skill`](skills/cf-temp-mail-agent-mail/SKILL.md) を提供します
|
||||
- **モバイル管理** - Android の管理画面とメールボックス管理に対応したコミュニティクライアント [CloudMail](https://github.com/Lur1N77777/CloudMail) を利用できます
|
||||
|
||||
## デプロイドキュメント - クイックスタート
|
||||
|
||||
[ドキュメント](https://temp-mail-docs.awsl.uk) | [GitHub Actions デプロイガイド](https://temp-mail-docs.awsl.uk/en/guide/actions/github-action.html)
|
||||
|
||||
<a href="https://temp-mail-docs.awsl.uk/en/guide/actions/github-action.html">
|
||||
<img src="https://deploy.workers.cloudflare.com/button" alt="Deploy to Cloudflare Workers" height="32">
|
||||
</a>
|
||||
|
||||
## 変更履歴
|
||||
|
||||
最新の更新内容については [CHANGELOG](CHANGELOG.md) を参照してください。
|
||||
|
||||
## ライブデモ
|
||||
|
||||
今すぐ試す → [https://mail.awsl.uk/](https://mail.awsl.uk/)
|
||||
|
||||
<details>
|
||||
<summary>サービス稼働状況(クリックして展開/折りたたみ)</summary>
|
||||
|
||||
| | |
|
||||
| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| [Backend](https://temp-email-api.awsl.uk/) | [](https://github.com/dreamhunter2333/cloudflare_temp_email/actions/workflows/backend_deploy.yaml)       |
|
||||
| [Frontend](https://mail.awsl.uk/) | [](https://github.com/dreamhunter2333/cloudflare_temp_email/actions/workflows/frontend_deploy.yaml)       |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Star History(クリックして展開/折りたたみ)</summary>
|
||||
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://star-history.dera.page/svg?repos=dreamhunter2333/cloudflare_temp_email&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://star-history.dera.page/svg?repos=dreamhunter2333/cloudflare_temp_email&type=Date" />
|
||||
<img alt="Star History Chart" src="https://star-history.dera.page/svg?repos=dreamhunter2333/cloudflare_temp_email&type=Date" />
|
||||
</picture>
|
||||
|
||||
</details>
|
||||
|
||||
<details open>
|
||||
<summary>目次(クリックして展開/折りたたみ)</summary>
|
||||
|
||||
- [Cloudflare 一時メール - 無料で構築できる一時メールサービス](#cloudflare-一時メール---無料で構築できる一時メールサービス)
|
||||
- [デプロイドキュメント - クイックスタート](#デプロイドキュメント---クイックスタート)
|
||||
- [変更履歴](#変更履歴)
|
||||
- [ライブデモ](#ライブデモ)
|
||||
- [主な機能](#主な機能)
|
||||
- [メール処理](#メール処理)
|
||||
- [ユーザー管理](#ユーザー管理)
|
||||
- [管理機能](#管理機能)
|
||||
- [多言語対応とインターフェース](#多言語対応とインターフェース)
|
||||
- [連携と拡張](#連携と拡張)
|
||||
- [技術アーキテクチャ](#技術アーキテクチャ)
|
||||
- [システム構成](#システム構成)
|
||||
- [技術スタック](#技術スタック)
|
||||
- [主要コンポーネント](#主要コンポーネント)
|
||||
- [コミュニティに参加](#コミュニティに参加)
|
||||
|
||||
</details>
|
||||
|
||||
## 主な機能
|
||||
|
||||
<details open>
|
||||
<summary>主な機能の詳細(クリックして展開/折りたたみ)</summary>
|
||||
|
||||
### メール処理
|
||||
|
||||
- [x] `rust wasm` でメールを高速に解析します。ほぼすべてのメールを解析でき、Node.js の解析モジュールで失敗するメールも rust wasm なら正常に解析できます
|
||||
- [x] **AI メール認識** - Cloudflare Workers AI を使用して、メール内の確認コード、認証リンク、サービスリンクなどの重要情報を自動抽出します
|
||||
- [x] 指定したベースドメインに対して、ランダムな第 2 レベルサブドメインのメールボックスを任意で作成できます
|
||||
- [x] `DKIM` 検証付きのメール送信に対応します
|
||||
- [x] `SMTP` や `Resend` など、複数の送信方法に対応します
|
||||
- [x] 添付ファイルの表示機能を備え、添付画像も表示できます
|
||||
- [x] S3 での添付ファイルの保存と削除に対応します
|
||||
- [x] スパム検出とブラックリスト/ホワイトリストの設定に対応します
|
||||
- [x] グローバル転送先アドレスを指定できるメール転送機能を備えています
|
||||
|
||||
### ユーザー管理
|
||||
|
||||
- [x] `credentials` を使用して、以前利用したメールボックスへ再ログインできます
|
||||
- [x] 完全なユーザー登録・ログイン機能を備えています。メールアドレスを紐付けると、そのアドレスのメール JWT 資格情報を自動取得し、複数のメールボックスを切り替えられます
|
||||
- [x] `OAuth2` によるサードパーティログイン(Github、Authentik など)に対応します
|
||||
- [x] `Passkey` によるパスワードレスログインに対応します
|
||||
- [x] 複数ロールのドメインおよびプレフィックスを設定できるユーザーロール管理に対応します
|
||||
- [x] アドレスやキーワードで絞り込めるユーザー受信箱を提供します
|
||||
|
||||
### 管理機能
|
||||
|
||||
- [x] 完全な admin コンソールを備えています
|
||||
- [x] `admin` バックエンドからプレフィックスのないメールボックスを作成できます
|
||||
- [x] admin ユーザー管理画面でユーザーのアドレスを確認できます
|
||||
- [x] 複数のクリーンアップ戦略を選べる定期クリーンアップ機能を備えています
|
||||
- [x] カスタム名のメールボックスを取得でき、`admin` でブラックリストを設定できます
|
||||
- [x] アクセスパスワードを追加し、プライベートサイトとして利用できます
|
||||
|
||||
### 多言語対応とインターフェース
|
||||
|
||||
- [x] フロントエンドとバックエンドの両方が多言語に対応しています
|
||||
- [x] レスポンシブレイアウトのモダンな UI デザインを採用しています
|
||||
- [x] Google Ads の連携に対応します
|
||||
- [x] shadow DOM を使用してスタイルの干渉を防ぎます
|
||||
- [x] URL の JWT パラメーターによる自動ログインに対応します
|
||||
|
||||
### 連携と拡張
|
||||
|
||||
- [x] 完全な `Telegram Bot`、`Telegram` プッシュ通知、Telegram Bot ミニアプリに対応します
|
||||
- [x] `SMTP proxy server` を追加し、`SMTP` によるメール送信と `IMAP` によるメール閲覧に対応します
|
||||
- [x] Webhook とメッセージプッシュ連携に対応します
|
||||
- [x] `CF Turnstile` CAPTCHA 検証に対応します
|
||||
- [x] 悪用を防ぐためのレート制限を設定できます
|
||||
- [x] **Agent フレンドリー**:組み込みの [`cf-temp-mail-agent-mail`](skills/cf-temp-mail-agent-mail/SKILL.md) skill により、AI agent がメールボックスを直接利用できます。詳しくは[ドキュメント](vitepress-docs/docs/en/guide/feature/agent-email.md)を参照してください
|
||||
- [x] コミュニティのモバイル管理クライアント:[CloudMail](https://github.com/Lur1N77777/CloudMail) は本プロジェクト互換の API 向けに Expo / React Native で構築されており、Android 管理コンソール、アドレス管理、受信済み/送信済み/不明メール、確認コードのクイックコピー、OLED ブラックテーマ、ローカルグループ分けを提供します
|
||||
|
||||
</details>
|
||||
|
||||
## 技術アーキテクチャ
|
||||
|
||||
<details>
|
||||
<summary>技術アーキテクチャの詳細(クリックして展開/折りたたみ)</summary>
|
||||
|
||||
### システム構成
|
||||
|
||||
- **データベース**:メインデータベースとして Cloudflare D1 を使用
|
||||
- **フロントエンドのデプロイ**:Cloudflare Pages を使用してフロントエンドをデプロイ
|
||||
- **バックエンドのデプロイ**:Cloudflare Workers を使用してバックエンドをデプロイ
|
||||
- **メールルーティング**:Cloudflare Email Routing を使用
|
||||
|
||||
### 技術スタック
|
||||
|
||||
- **フロントエンド**:Vue 3 + Vite + TypeScript
|
||||
- **バックエンド**:TypeScript + Cloudflare Workers
|
||||
- **メール解析**:Rust WASM(mail-parser-wasm)
|
||||
- **データベース**:Cloudflare D1(SQLite)
|
||||
- **ストレージ**:Cloudflare KV + R2(任意で S3)
|
||||
- **プロキシサービス**:Python SMTP/IMAP Proxy Server
|
||||
|
||||
### 主要コンポーネント
|
||||
|
||||
- **Worker**:中核となるバックエンドサービス
|
||||
- **Frontend**:Vue 3 ユーザーインターフェース
|
||||
- **Mail Parser WASM**:Rust メール解析モジュール
|
||||
- **SMTP Proxy Server**:Python メールプロキシサービス
|
||||
- **Pages Functions**:Cloudflare Pages ミドルウェア
|
||||
- **Documentation**:VitePress ドキュメントサイト
|
||||
|
||||
</details>
|
||||
|
||||
### 重要な注意事項
|
||||
|
||||
- Resend でドメインレコードを追加する際、DNS プロバイダーが第 3 レベルドメイン a.b.com をホストしている場合は、Resend が生成したデフォルト名から第 2 レベルドメインのプレフィックス b を削除してください。削除しないと a.b.b.com が追加され、検証に失敗します。レコードを追加した後、次のコマンドで確認できます。
|
||||
```bash
|
||||
nslookup -qt="mx" a.b.com 1.1.1.1
|
||||
```
|
||||
|
||||
## コミュニティに参加
|
||||
|
||||
- [Telegram](https://t.me/cloudflare_temp_email)
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
# Keep this version in sync with @playwright/test in package.json
|
||||
FROM mcr.microsoft.com/playwright:v1.58.2-noble
|
||||
FROM mcr.microsoft.com/playwright:v1.62.1-noble
|
||||
|
||||
RUN apt-get update && apt-get install -y curl netcat-openbsd && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:20-slim
|
||||
FROM node:24-slim
|
||||
|
||||
RUN apt-get update && apt-get install -y openssl && rm -rf /var/lib/apt/lists/*
|
||||
RUN corepack enable && corepack prepare pnpm@10.10.0 --activate
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:20-slim
|
||||
FROM node:24-slim
|
||||
|
||||
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
|
||||
RUN corepack enable && corepack prepare pnpm@10.10.0 --activate
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { APIRequestContext } from '@playwright/test';
|
||||
import type { APIRequestContext } from '@playwright/test';
|
||||
import { createHash } from 'crypto';
|
||||
import WebSocket from 'ws';
|
||||
|
||||
export const WORKER_URL = process.env.WORKER_URL!;
|
||||
@@ -10,6 +11,13 @@ export const FRONTEND_URL = process.env.FRONTEND_URL!;
|
||||
export const MAILPIT_API = process.env.MAILPIT_API!;
|
||||
export const TEST_DOMAIN = 'test.example.com';
|
||||
|
||||
/**
|
||||
* SHA-256 hash matching the frontend hashPassword utility.
|
||||
*/
|
||||
export function hashPassword(password: string): string {
|
||||
return createHash('sha256').update(password).digest('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new email address via the worker API.
|
||||
* Appends a timestamp suffix to avoid UNIQUE constraint collisions
|
||||
|
||||
@@ -5,9 +5,13 @@ compatibility_flags = [ "nodejs_compat" ]
|
||||
keep_vars = true
|
||||
|
||||
[vars]
|
||||
PREFIX = "tmp"
|
||||
DEFAULT_DOMAINS = ["test.example.com"]
|
||||
DOMAINS = ["test.example.com"]
|
||||
PREFIX = "TMP"
|
||||
DEFAULT_DOMAINS = []
|
||||
DOMAINS = ["TEST.EXAMPLE.COM"]
|
||||
USER_ROLES = [
|
||||
{ domains = ["TEST.EXAMPLE.COM"], role = "case-role", prefix = "ROLE" },
|
||||
{ domains = [], role = "empty-role", prefix = "EMPTY" },
|
||||
]
|
||||
JWT_SECRET = "e2e-test-secret-key"
|
||||
BLACK_LIST = ""
|
||||
ENABLE_USER_CREATE_EMAIL = true
|
||||
@@ -19,8 +23,9 @@ DISABLE_ADMIN_PASSWORD_CHECK = true
|
||||
ADMIN_PASSWORDS = '["e2e-admin-pass"]'
|
||||
ENABLE_WEBHOOK = true
|
||||
E2E_TEST_MODE = true
|
||||
CLEANUP_BATCH_SIZE = 10
|
||||
SMTP_CONFIG = """
|
||||
{"test.example.com":{"host":"mailpit","port":1025,"secure":false}}
|
||||
{"TEST.EXAMPLE.COM":{"host":"mailpit","port":1025,"secure":false}}
|
||||
"""
|
||||
|
||||
[[kv_namespaces]]
|
||||
|
||||
@@ -9,10 +9,10 @@ send_email = [
|
||||
]
|
||||
|
||||
[vars]
|
||||
PREFIX = "tmp"
|
||||
DEFAULT_DOMAINS = ["test.example.com"]
|
||||
DOMAINS = ["test.example.com"]
|
||||
SEND_MAIL_DOMAINS = ["test.example.com"]
|
||||
PREFIX = "TMP"
|
||||
DEFAULT_DOMAINS = ["TEST.EXAMPLE.COM"]
|
||||
DOMAINS = ["TEST.EXAMPLE.COM"]
|
||||
SEND_MAIL_DOMAINS = ["TEST.EXAMPLE.COM"]
|
||||
JWT_SECRET = "e2e-test-secret-key"
|
||||
BLACK_LIST = ""
|
||||
ENABLE_USER_CREATE_EMAIL = true
|
||||
@@ -25,7 +25,7 @@ ADMIN_PASSWORDS = '["e2e-admin-pass"]'
|
||||
ENABLE_WEBHOOK = true
|
||||
E2E_TEST_MODE = true
|
||||
SMTP_CONFIG = """
|
||||
{"test.example.com":{"host":"mailpit","port":1025,"secure":false}}
|
||||
{"TEST.EXAMPLE.COM":{"host":"mailpit","port":1025,"secure":false}}
|
||||
"""
|
||||
|
||||
[[kv_namespaces]]
|
||||
|
||||
Generated
+74
-93
@@ -6,14 +6,14 @@
|
||||
"": {
|
||||
"name": "cloudflare-temp-email-e2e",
|
||||
"dependencies": {
|
||||
"imapflow": "^1.3.1",
|
||||
"nodemailer": "^8.0.5"
|
||||
"imapflow": "^1.6.5",
|
||||
"nodemailer": "^9.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "1.58.2",
|
||||
"@types/nodemailer": "^7.0.11",
|
||||
"@types/ws": "^8.5.0",
|
||||
"ws": "^8.18.0"
|
||||
"@playwright/test": "1.62.1",
|
||||
"@types/nodemailer": "^8.0.1",
|
||||
"@types/ws": "^8.18.1",
|
||||
"ws": "^8.21.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@pinojs/redact": {
|
||||
@@ -23,35 +23,35 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.58.2",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz",
|
||||
"integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==",
|
||||
"version": "1.62.1",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.62.1.tgz",
|
||||
"integrity": "sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright": "1.58.2"
|
||||
"playwright": "1.62.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "25.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.3.tgz",
|
||||
"integrity": "sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ==",
|
||||
"version": "26.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.2.tgz",
|
||||
"integrity": "sha512-Vu4a5UFA9rIIFJ7rB/Vaafh9lrCQszopTCx6KjFboXTGQbPNasehVR5TEiithSDGyd1DEiUByggTZsg8jukeIg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~7.18.0"
|
||||
"undici-types": "~8.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/nodemailer": {
|
||||
"version": "7.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-7.0.11.tgz",
|
||||
"integrity": "sha512-E+U4RzR2dKrx+u3N4DlsmLaDC6mMZOM/TPROxA0UAPiTgI0y4CEFBmZE+coGWTjakDriRsXG368lNk1u9Q0a2g==",
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-8.0.1.tgz",
|
||||
"integrity": "sha512-PxpaInm8V1JQDd4j0ds5HfvWQk8JupS1C0Picb96QJsrrRDjBH+DlK7L4ZdNSqNULhiZRQHc40nLVShaGxXAMw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -69,13 +69,13 @@
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"version": "5.4.14",
|
||||
"resolved": "https://registry.npmjs.org/@zone-eu/mailsplit/-/mailsplit-5.4.14.tgz",
|
||||
"integrity": "sha512-rz0FQOhN3Vq1XrSeSSa9+dPcaFbBxmQPjiZm6zS9oxdVHV7rOWIAYX3yP2YAUf0qBncY8CI+NogzPCmMVrMXcw==",
|
||||
"license": "(MIT OR EUPL-1.1+)",
|
||||
"dependencies": {
|
||||
"libbase64": "1.3.0",
|
||||
"libmime": "5.3.7",
|
||||
"libmime": "5.4.1",
|
||||
"libqp": "2.1.1"
|
||||
}
|
||||
},
|
||||
@@ -113,9 +113,9 @@
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"version": "0.7.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz",
|
||||
"integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
@@ -129,38 +129,25 @@
|
||||
}
|
||||
},
|
||||
"node_modules/imapflow": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/imapflow/-/imapflow-1.3.1.tgz",
|
||||
"integrity": "sha512-DKwpMDR1EWXpV5T7adqQAccN7n684AX3poEZ5F3YoPlm2MyGeKavpRgNr3qptdEQaK+x5SlZ9jigT+cMs4geBA==",
|
||||
"version": "1.6.5",
|
||||
"resolved": "https://registry.npmjs.org/imapflow/-/imapflow-1.6.5.tgz",
|
||||
"integrity": "sha512-5XgLcfl6blDju2SP7TgTv4WIlTt7zxXYcfjznJFmfFrogHR2XzHGlS9s2wdqxv9ZJ5CPaRnig7KaK7qNoJPmlg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@zone-eu/mailsplit": "5.4.8",
|
||||
"@zone-eu/mailsplit": "5.4.14",
|
||||
"encoding-japanese": "2.2.0",
|
||||
"iconv-lite": "0.7.2",
|
||||
"iconv-lite": "0.7.3",
|
||||
"libbase64": "1.3.0",
|
||||
"libmime": "5.3.8",
|
||||
"libmime": "5.4.1",
|
||||
"libqp": "2.1.1",
|
||||
"nodemailer": "8.0.5",
|
||||
"pino": "10.3.1",
|
||||
"socks": "2.8.7"
|
||||
}
|
||||
},
|
||||
"node_modules/imapflow/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"
|
||||
"socks": "2.8.9"
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"version": "10.3.1",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.3.1.tgz",
|
||||
"integrity": "sha512-1e9d3kb97NHJTIJDZW9rKqW2h6+dFa50Dy0fpPSMQp2ADje5gvKsXmdiK6dwY5t76TaTt5+P5N1Y/LoToIxP6g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
@@ -173,29 +160,17 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"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==",
|
||||
"version": "5.4.1",
|
||||
"resolved": "https://registry.npmjs.org/libmime/-/libmime-5.4.1.tgz",
|
||||
"integrity": "sha512-0wHGhsofo9IdQPenr3BBHXuxcwMq4atFUTsZ9Ogc1OvI5h4rUdDIrBQEN9JHjCXfDMrE59LUMJWsTD82wTYk8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"encoding-japanese": "2.2.0",
|
||||
"iconv-lite": "0.6.3",
|
||||
"iconv-lite": "0.7.3",
|
||||
"libbase64": "1.3.0",
|
||||
"libqp": "2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/libmime/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/libqp": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/libqp/-/libqp-2.1.1.tgz",
|
||||
@@ -203,9 +178,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"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==",
|
||||
"version": "9.0.4",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-9.0.4.tgz",
|
||||
"integrity": "sha512-LmJNRVRtfSCULxcZpy0Cpg4WWenlUZ9+zbmTO+S7v9wD6XreYLjXRFtDjtV/4F0HT5p1GyZfA0Ux/myxHb18CQ==",
|
||||
"license": "MIT-0",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
@@ -258,41 +233,41 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.58.2",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz",
|
||||
"integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==",
|
||||
"version": "1.62.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.1.tgz",
|
||||
"integrity": "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.58.2"
|
||||
"playwright-core": "1.62.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=20"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.58.2",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz",
|
||||
"integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==",
|
||||
"version": "1.62.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz",
|
||||
"integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.1.0.tgz",
|
||||
"integrity": "sha512-jQSaVHsPgtyw60e1rQ/A+/ArPEj/S8pS/vFnyGa/gYFXrKk/6RuDkoqVDQ5NI5MmS01698ltlAk0NoDBNLujRw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -346,12 +321,12 @@
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"version": "2.8.9",
|
||||
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz",
|
||||
"integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ip-address": "^10.0.1",
|
||||
"ip-address": "^10.1.1",
|
||||
"smart-buffer": "^4.2.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -378,28 +353,34 @@
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz",
|
||||
"integrity": "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"real-require": "^0.2.0"
|
||||
"real-require": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/thread-stream/node_modules/real-require": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/real-require/-/real-require-1.0.0.tgz",
|
||||
"integrity": "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.18.2",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
|
||||
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz",
|
||||
"integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.19.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
|
||||
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
|
||||
"version": "8.21.2",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.2.tgz",
|
||||
"integrity": "sha512-54dMVAo4WIe6SKy3vBgN+9bJZqqQ8IMRevAkOLQALhi49qkkQDQfWdAZ8KQlXiEabw88ARXXdUrlvtbKQX+aKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
||||
+6
-6
@@ -7,13 +7,13 @@
|
||||
"test:down": "docker compose down -v"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "1.58.2",
|
||||
"@types/nodemailer": "^7.0.11",
|
||||
"@types/ws": "^8.5.0",
|
||||
"ws": "^8.18.0"
|
||||
"@playwright/test": "1.62.1",
|
||||
"@types/nodemailer": "^8.0.1",
|
||||
"@types/ws": "^8.18.1",
|
||||
"ws": "^8.21.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"imapflow": "^1.3.1",
|
||||
"nodemailer": "^8.0.5"
|
||||
"imapflow": "^1.6.5",
|
||||
"nodemailer": "^9.0.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,6 +140,34 @@ test.describe('Mail Gzip Storage', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('gzip-compressed mail is readable through admin detail API', async ({ request }) => {
|
||||
const { jwt, address } = await createGzipAddress(request, 'gzip-admin-detail');
|
||||
try {
|
||||
await receiveGzipMail(request, address, {
|
||||
subject: 'Gzip Admin Detail Test',
|
||||
text: 'admin compressed content',
|
||||
});
|
||||
|
||||
const listRes = await request.get(`${WORKER_GZIP_URL}/api/mails?limit=10&offset=0`, {
|
||||
headers: { Authorization: `Bearer ${jwt}` },
|
||||
});
|
||||
const { results } = await listRes.json();
|
||||
expect(results.length).toBeGreaterThanOrEqual(1);
|
||||
const mailId = results[0].id;
|
||||
|
||||
const detailRes = await request.get(`${WORKER_GZIP_URL}/admin/mails/${mailId}`, {
|
||||
headers: { 'x-admin-auth': 'e2e-admin-pass' },
|
||||
});
|
||||
expect(detailRes.ok()).toBe(true);
|
||||
const mail = await detailRes.json();
|
||||
expect(mail.raw).toContain('Gzip Admin Detail Test');
|
||||
expect(mail.raw).toContain('admin compressed content');
|
||||
expect(mail.raw_blob).toBeUndefined();
|
||||
} finally {
|
||||
await deleteGzipAddress(request, jwt);
|
||||
}
|
||||
});
|
||||
|
||||
test('mixed: plaintext seed + gzip receive both readable in same list', async ({ request }) => {
|
||||
const { jwt, address } = await createGzipAddress(request, 'gzip-mixed');
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import {
|
||||
WORKER_URL,
|
||||
createTestAddress,
|
||||
deleteAddress,
|
||||
hashPassword,
|
||||
} from '../../fixtures/test-helpers';
|
||||
|
||||
const waitForNextTimestamp = () => new Promise((resolve) => setTimeout(resolve, 1_100));
|
||||
|
||||
test.describe('Address activity throttling', () => {
|
||||
test('does not rewrite recently active addresses from user settings', async ({ request }) => {
|
||||
const email = `activity-throttle-${Date.now()}@test.example.com`;
|
||||
const password = hashPassword('test-password-123');
|
||||
const address = await createTestAddress(request, 'activity-throttle');
|
||||
let userId: number | undefined;
|
||||
|
||||
try {
|
||||
const settingsRes = await request.post(`${WORKER_URL}/admin/user_settings`, {
|
||||
data: { enable: true, enableMailVerify: false },
|
||||
});
|
||||
expect(settingsRes.ok()).toBe(true);
|
||||
|
||||
const registerRes = await request.post(`${WORKER_URL}/user_api/register`, {
|
||||
data: { email, password },
|
||||
});
|
||||
expect(registerRes.ok()).toBe(true);
|
||||
|
||||
const loginRes = await request.post(`${WORKER_URL}/user_api/login`, {
|
||||
data: { email, password },
|
||||
});
|
||||
expect(loginRes.ok()).toBe(true);
|
||||
const { jwt: userJwt } = await loginRes.json();
|
||||
const payload = JSON.parse(Buffer.from(userJwt.split('.')[1], 'base64url').toString('utf8'));
|
||||
userId = payload.user_id;
|
||||
|
||||
const bindRes = await request.post(`${WORKER_URL}/user_api/bind_address`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${address.jwt}`,
|
||||
'x-user-token': userJwt,
|
||||
},
|
||||
});
|
||||
expect(bindRes.ok()).toBe(true);
|
||||
|
||||
const beforeRes = await request.get(`${WORKER_URL}/user_api/bind_address`, {
|
||||
headers: { 'x-user-token': userJwt },
|
||||
});
|
||||
expect(beforeRes.ok()).toBe(true);
|
||||
const before = await beforeRes.json();
|
||||
const initialUpdatedAt = before.results.find(
|
||||
(row: { name: string }) => row.name === address.address,
|
||||
)?.updated_at;
|
||||
expect(initialUpdatedAt).toBeTruthy();
|
||||
|
||||
await waitForNextTimestamp();
|
||||
const userSettingsRes = await request.get(`${WORKER_URL}/user_api/settings`, {
|
||||
headers: { 'x-user-token': userJwt },
|
||||
});
|
||||
expect(userSettingsRes.ok()).toBe(true);
|
||||
await waitForNextTimestamp();
|
||||
|
||||
const afterRes = await request.get(`${WORKER_URL}/user_api/bind_address`, {
|
||||
headers: { 'x-user-token': userJwt },
|
||||
});
|
||||
expect(afterRes.ok()).toBe(true);
|
||||
const after = await afterRes.json();
|
||||
const updatedAt = after.results.find(
|
||||
(row: { name: string }) => row.name === address.address,
|
||||
)?.updated_at;
|
||||
expect(updatedAt).toBe(initialUpdatedAt);
|
||||
} finally {
|
||||
await deleteAddress(request, address.jwt);
|
||||
if (userId) {
|
||||
const deleteUserRes = await request.delete(`${WORKER_URL}/admin/users/${userId}`);
|
||||
expect(deleteUserRes.ok()).toBe(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,15 +1,16 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { WORKER_URL, createTestAddress, deleteAddress } from '../../fixtures/test-helpers';
|
||||
import { WORKER_URL, createTestAddress, deleteAddress, hashPassword } from '../../fixtures/test-helpers';
|
||||
|
||||
test.describe('Address Password Login', () => {
|
||||
test('set password then login with it', async ({ request }) => {
|
||||
const { jwt, address } = await createTestAddress(request, 'pwd-login');
|
||||
const passwordHash = hashPassword('test-password-123');
|
||||
|
||||
try {
|
||||
// Set a password on the address
|
||||
const changePwdRes = await request.post(`${WORKER_URL}/api/address_change_password`, {
|
||||
headers: { Authorization: `Bearer ${jwt}` },
|
||||
data: { new_password: 'test-password-123' },
|
||||
data: { new_password: passwordHash },
|
||||
});
|
||||
expect(changePwdRes.ok()).toBe(true);
|
||||
const changePwdBody = await changePwdRes.json();
|
||||
@@ -17,7 +18,7 @@ test.describe('Address Password Login', () => {
|
||||
|
||||
// Login with the correct password
|
||||
const loginRes = await request.post(`${WORKER_URL}/api/address_login`, {
|
||||
data: { email: address, password: 'test-password-123' },
|
||||
data: { email: address, password: passwordHash },
|
||||
});
|
||||
expect(loginRes.ok()).toBe(true);
|
||||
const loginBody = await loginRes.json();
|
||||
@@ -36,12 +37,13 @@ test.describe('Address Password Login', () => {
|
||||
|
||||
test('login with wrong password returns 401', async ({ request }) => {
|
||||
const { jwt, address } = await createTestAddress(request, 'pwd-wrong');
|
||||
const passwordHash = hashPassword('correct-password');
|
||||
|
||||
try {
|
||||
// Set a password
|
||||
const changePwdRes = await request.post(`${WORKER_URL}/api/address_change_password`, {
|
||||
headers: { Authorization: `Bearer ${jwt}` },
|
||||
data: { new_password: 'correct-password' },
|
||||
data: { new_password: passwordHash },
|
||||
});
|
||||
expect(changePwdRes.ok()).toBe(true);
|
||||
const changePwdBody = await changePwdRes.json();
|
||||
@@ -49,11 +51,117 @@ test.describe('Address Password Login', () => {
|
||||
|
||||
// Login with wrong password
|
||||
const loginRes = await request.post(`${WORKER_URL}/api/address_login`, {
|
||||
data: { email: address, password: 'wrong-password' },
|
||||
data: { email: address, password: hashPassword('wrong-password') },
|
||||
});
|
||||
expect(loginRes.status()).toBe(401);
|
||||
} finally {
|
||||
await deleteAddress(request, jwt);
|
||||
}
|
||||
});
|
||||
|
||||
test('admin reset stores frontend-hashed address password', async ({ request }) => {
|
||||
const { jwt, address, address_id } = await createTestAddress(request, 'pwd-admin-reset');
|
||||
const plainPassword = `admin-reset-${Date.now()}`;
|
||||
const passwordHash = hashPassword(plainPassword);
|
||||
|
||||
try {
|
||||
const resetRes = await request.post(`${WORKER_URL}/admin/address/${address_id}/reset_password`, {
|
||||
data: { password: passwordHash },
|
||||
});
|
||||
expect(resetRes.ok()).toBe(true);
|
||||
await expect(resetRes.json()).resolves.toMatchObject({ success: true });
|
||||
|
||||
const plaintextLoginRes = await request.post(`${WORKER_URL}/api/address_login`, {
|
||||
data: { email: address, password: plainPassword },
|
||||
});
|
||||
expect(plaintextLoginRes.status()).toBe(401);
|
||||
|
||||
const loginRes = await request.post(`${WORKER_URL}/api/address_login`, {
|
||||
data: { email: address, password: passwordHash },
|
||||
});
|
||||
expect(loginRes.ok()).toBe(true);
|
||||
const loginBody = await loginRes.json();
|
||||
expect(loginBody.jwt).toBeTruthy();
|
||||
expect(loginBody.address).toBe(address);
|
||||
} finally {
|
||||
await deleteAddress(request, jwt);
|
||||
}
|
||||
});
|
||||
|
||||
test('admin address list does not expose stored password hash', async ({ request }) => {
|
||||
const { jwt, address } = await createTestAddress(request, 'pwd-list-hidden');
|
||||
const passwordHash = hashPassword('list-hidden-password');
|
||||
|
||||
try {
|
||||
const changePwdRes = await request.post(`${WORKER_URL}/api/address_change_password`, {
|
||||
headers: { Authorization: `Bearer ${jwt}` },
|
||||
data: { new_password: passwordHash },
|
||||
});
|
||||
expect(changePwdRes.ok()).toBe(true);
|
||||
|
||||
const listRes = await request.get(
|
||||
`${WORKER_URL}/admin/address?limit=10&offset=0&query=${encodeURIComponent(address)}`
|
||||
);
|
||||
expect(listRes.ok()).toBe(true);
|
||||
const listBody = await listRes.json();
|
||||
const listedAddress = listBody.results.find((row: { name: string }) => row.name === address);
|
||||
expect(listedAddress).toBeTruthy();
|
||||
expect(listedAddress).not.toHaveProperty('password');
|
||||
} finally {
|
||||
await deleteAddress(request, jwt);
|
||||
}
|
||||
});
|
||||
|
||||
test('user bind address list does not expose stored password hash', async ({ request }) => {
|
||||
const userEmail = `pwd-bind-hidden-${Date.now()}@test.example.com`;
|
||||
const userPasswordHash = hashPassword('bind-hidden-user-password');
|
||||
const { jwt, address } = await createTestAddress(request, 'pwd-bind-hidden');
|
||||
const addressPasswordHash = hashPassword('bind-hidden-address-password');
|
||||
|
||||
try {
|
||||
const enableRes = await request.post(`${WORKER_URL}/admin/user_settings`, {
|
||||
data: {
|
||||
enable: true,
|
||||
enableMailVerify: false,
|
||||
},
|
||||
});
|
||||
expect(enableRes.ok()).toBe(true);
|
||||
|
||||
const registerRes = await request.post(`${WORKER_URL}/user_api/register`, {
|
||||
data: { email: userEmail, password: userPasswordHash },
|
||||
});
|
||||
expect(registerRes.ok()).toBe(true);
|
||||
|
||||
const loginRes = await request.post(`${WORKER_URL}/user_api/login`, {
|
||||
data: { email: userEmail, password: userPasswordHash },
|
||||
});
|
||||
expect(loginRes.ok()).toBe(true);
|
||||
const { jwt: userJwt } = await loginRes.json();
|
||||
|
||||
const changePwdRes = await request.post(`${WORKER_URL}/api/address_change_password`, {
|
||||
headers: { Authorization: `Bearer ${jwt}` },
|
||||
data: { new_password: addressPasswordHash },
|
||||
});
|
||||
expect(changePwdRes.ok()).toBe(true);
|
||||
|
||||
const bindRes = await request.post(`${WORKER_URL}/user_api/bind_address`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
'x-user-token': userJwt,
|
||||
},
|
||||
});
|
||||
expect(bindRes.ok()).toBe(true);
|
||||
|
||||
const listRes = await request.get(`${WORKER_URL}/user_api/bind_address`, {
|
||||
headers: { 'x-user-token': userJwt },
|
||||
});
|
||||
expect(listRes.ok()).toBe(true);
|
||||
const listBody = await listRes.json();
|
||||
const listedAddress = listBody.results.find((row: { name: string }) => row.name === address);
|
||||
expect(listedAddress).toBeTruthy();
|
||||
expect(listedAddress).not.toHaveProperty('password');
|
||||
} finally {
|
||||
await deleteAddress(request, jwt);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,4 +16,116 @@ test.describe('Admin New Address', () => {
|
||||
expect(body.address_id).toBeGreaterThan(0);
|
||||
expect(typeof body.address_id).toBe('number');
|
||||
});
|
||||
|
||||
test('normalizes uppercase configured prefix and domain', async ({ request }) => {
|
||||
const uniqueName = `admincase${Date.now()}`;
|
||||
const res = await request.post(`${WORKER_URL}/admin/new_address`, {
|
||||
data: { name: uniqueName, domain: TEST_DOMAIN.toUpperCase(), enablePrefix: true },
|
||||
});
|
||||
|
||||
expect(res.ok()).toBe(true);
|
||||
const body = await res.json();
|
||||
|
||||
expect(body.address).toBe(`tmp${uniqueName}@${TEST_DOMAIN}`);
|
||||
expect(body.jwt).toBeTruthy();
|
||||
expect(body.address_id).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('falls back to domains when default domains is empty', async ({ request }) => {
|
||||
const uniqueName = `fallback${Date.now().toString(36)}`;
|
||||
const res = await request.post(`${WORKER_URL}/api/new_address`, {
|
||||
data: { name: uniqueName },
|
||||
});
|
||||
|
||||
expect(res.ok()).toBe(true);
|
||||
const body = await res.json();
|
||||
|
||||
expect(body.address).toBe(`tmp${uniqueName}@${TEST_DOMAIN}`);
|
||||
expect(body.jwt).toBeTruthy();
|
||||
expect(body.address_id).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('normalizes user role domains and prefix', async ({ request }) => {
|
||||
const suffix = `${Date.now()}${Math.random().toString(36).slice(2, 8)}`;
|
||||
const email = `role-case-${suffix}@${TEST_DOMAIN}`;
|
||||
const password = `role-pass-${suffix}`;
|
||||
const name = `rolecase${suffix}`;
|
||||
|
||||
const createUserRes = await request.post(`${WORKER_URL}/admin/users`, {
|
||||
data: { email, password },
|
||||
});
|
||||
expect(createUserRes.ok()).toBe(true);
|
||||
|
||||
const usersRes = await request.get(`${WORKER_URL}/admin/users`, {
|
||||
params: { limit: '20', offset: '0', query: email },
|
||||
});
|
||||
expect(usersRes.ok()).toBe(true);
|
||||
const usersBody = await usersRes.json();
|
||||
const user = usersBody.results.find((row: { user_email: string }) => row.user_email === email);
|
||||
expect(user).toBeTruthy();
|
||||
|
||||
const updateRoleRes = await request.post(`${WORKER_URL}/admin/user_roles`, {
|
||||
data: { user_id: user.id, role_text: 'case-role' },
|
||||
});
|
||||
expect(updateRoleRes.ok()).toBe(true);
|
||||
|
||||
const loginRes = await request.post(`${WORKER_URL}/user_api/login`, {
|
||||
data: { email, password },
|
||||
});
|
||||
expect(loginRes.ok()).toBe(true);
|
||||
const { jwt: userJwt } = await loginRes.json();
|
||||
|
||||
const createAddressRes = await request.post(`${WORKER_URL}/api/new_address`, {
|
||||
headers: { 'x-user-token': userJwt },
|
||||
data: { name },
|
||||
});
|
||||
expect(createAddressRes.ok()).toBe(true);
|
||||
const addressBody = await createAddressRes.json();
|
||||
|
||||
expect(addressBody.address).toBe(`role${name}@${TEST_DOMAIN}`);
|
||||
expect(addressBody.jwt).toBeTruthy();
|
||||
expect(addressBody.address_id).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('falls back to default domains when user role domains is empty', async ({ request }) => {
|
||||
const suffix = `${Date.now()}${Math.random().toString(36).slice(2, 8)}`;
|
||||
const email = `empty-role-${suffix}@${TEST_DOMAIN}`;
|
||||
const password = `empty-role-pass-${suffix}`;
|
||||
const name = `emptyrole${suffix}`;
|
||||
|
||||
const createUserRes = await request.post(`${WORKER_URL}/admin/users`, {
|
||||
data: { email, password },
|
||||
});
|
||||
expect(createUserRes.ok()).toBe(true);
|
||||
|
||||
const usersRes = await request.get(`${WORKER_URL}/admin/users`, {
|
||||
params: { limit: '20', offset: '0', query: email },
|
||||
});
|
||||
expect(usersRes.ok()).toBe(true);
|
||||
const usersBody = await usersRes.json();
|
||||
const user = usersBody.results.find((row: { user_email: string }) => row.user_email === email);
|
||||
expect(user).toBeTruthy();
|
||||
|
||||
const updateRoleRes = await request.post(`${WORKER_URL}/admin/user_roles`, {
|
||||
data: { user_id: user.id, role_text: 'empty-role' },
|
||||
});
|
||||
expect(updateRoleRes.ok()).toBe(true);
|
||||
|
||||
const loginRes = await request.post(`${WORKER_URL}/user_api/login`, {
|
||||
data: { email, password },
|
||||
});
|
||||
expect(loginRes.ok()).toBe(true);
|
||||
const { jwt: userJwt } = await loginRes.json();
|
||||
|
||||
const createAddressRes = await request.post(`${WORKER_URL}/api/new_address`, {
|
||||
headers: { 'x-user-token': userJwt },
|
||||
data: { name },
|
||||
});
|
||||
expect(createAddressRes.ok()).toBe(true);
|
||||
const addressBody = await createAddressRes.json();
|
||||
|
||||
expect(addressBody.address).toBe(`empty${name}@${TEST_DOMAIN}`);
|
||||
expect(addressBody.jwt).toBeTruthy();
|
||||
expect(addressBody.address_id).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import { test, expect, type APIRequestContext } from '@playwright/test';
|
||||
import { WORKER_URL, createTestAddress, deleteAddress } from '../../fixtures/test-helpers';
|
||||
|
||||
const listMails = async (request: APIRequestContext, address: string) => {
|
||||
const response = await request.get(`${WORKER_URL}/admin/mails`, {
|
||||
params: { address, limit: '100', offset: '0' },
|
||||
});
|
||||
expect(response.ok()).toBe(true);
|
||||
return response.json();
|
||||
};
|
||||
|
||||
test.describe('Bounded cleanup', () => {
|
||||
test('cleans at most one batch and continues on the next run', async ({ request }) => {
|
||||
const address = `cleanup-batch-${Date.now()}@test.example.com`;
|
||||
const seedResponses = await Promise.all(Array.from({ length: 11 }, (_, index) =>
|
||||
request.post(`${WORKER_URL}/admin/test/seed_mail`, {
|
||||
data: {
|
||||
address,
|
||||
raw: 'old cleanup mail',
|
||||
message_id: `<cleanup-old-${Date.now()}-${index}@test>`,
|
||||
},
|
||||
})
|
||||
));
|
||||
expect(seedResponses.every((response) => response.ok())).toBe(true);
|
||||
await new Promise((resolve) => setTimeout(resolve, 1100));
|
||||
|
||||
const firstCleanup = await request.post(`${WORKER_URL}/admin/cleanup`, {
|
||||
data: { cleanType: 'mails', cleanDays: 0 },
|
||||
});
|
||||
expect(firstCleanup.ok()).toBe(true);
|
||||
|
||||
const afterFirstCleanup = await listMails(request, address);
|
||||
expect(afterFirstCleanup.count).toBe(1);
|
||||
|
||||
const secondCleanup = await request.post(`${WORKER_URL}/admin/cleanup`, {
|
||||
data: { cleanType: 'mails', cleanDays: 0 },
|
||||
});
|
||||
expect(secondCleanup.ok()).toBe(true);
|
||||
|
||||
const afterSecondCleanup = await listMails(request, address);
|
||||
expect(afterSecondCleanup.count).toBe(0);
|
||||
|
||||
const recentMailResponse = await request.post(`${WORKER_URL}/admin/test/seed_mail`, {
|
||||
data: {
|
||||
address,
|
||||
raw: 'recent cleanup mail',
|
||||
message_id: `<cleanup-recent-${Date.now()}@test>`,
|
||||
},
|
||||
});
|
||||
expect(recentMailResponse.ok()).toBe(true);
|
||||
|
||||
const recentCleanup = await request.post(`${WORKER_URL}/admin/cleanup`, {
|
||||
data: { cleanType: 'mails', cleanDays: 1 },
|
||||
});
|
||||
expect(recentCleanup.ok()).toBe(true);
|
||||
|
||||
const recentMails = await listMails(request, address);
|
||||
expect(recentMails.count).toBe(1);
|
||||
expect(recentMails.results[0].raw).toBe('recent cleanup mail');
|
||||
await request.delete(`${WORKER_URL}/admin/mails/${recentMails.results[0].id}`);
|
||||
});
|
||||
|
||||
test('deletes one address batch and its related data', async ({ request }) => {
|
||||
const oldAddress = await createTestAddress(request, 'cleanup-old');
|
||||
|
||||
const seedResponse = await request.post(`${WORKER_URL}/admin/test/seed_mail`, {
|
||||
data: {
|
||||
address: oldAddress.address,
|
||||
raw: 'address cleanup mail',
|
||||
message_id: `<cleanup-address-${Date.now()}@test>`,
|
||||
},
|
||||
});
|
||||
expect(seedResponse.ok()).toBe(true);
|
||||
await new Promise((resolve) => setTimeout(resolve, 1100));
|
||||
|
||||
const cleanupResponse = await request.post(`${WORKER_URL}/admin/cleanup`, {
|
||||
data: { cleanType: 'addressCreated', cleanDays: 0 },
|
||||
});
|
||||
expect(cleanupResponse.ok()).toBe(true);
|
||||
|
||||
const oldAddressResponse = await request.get(`${WORKER_URL}/admin/address`, {
|
||||
params: { query: oldAddress.address, limit: '20', offset: '0' },
|
||||
});
|
||||
expect(oldAddressResponse.ok()).toBe(true);
|
||||
expect((await oldAddressResponse.json()).count).toBe(0);
|
||||
expect((await listMails(request, oldAddress.address)).count).toBe(0);
|
||||
|
||||
const recentAddress = await createTestAddress(request, 'cleanup-recent');
|
||||
try {
|
||||
const recentCleanup = await request.post(`${WORKER_URL}/admin/cleanup`, {
|
||||
data: { cleanType: 'addressCreated', cleanDays: 1 },
|
||||
});
|
||||
expect(recentCleanup.ok()).toBe(true);
|
||||
|
||||
const recentAddressResponse = await request.get(`${WORKER_URL}/admin/address`, {
|
||||
params: { query: recentAddress.address, limit: '20', offset: '0' },
|
||||
});
|
||||
expect(recentAddressResponse.ok()).toBe(true);
|
||||
expect((await recentAddressResponse.json()).count).toBe(1);
|
||||
} finally {
|
||||
await deleteAddress(request, recentAddress.jwt);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,175 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import type { APIRequestContext } from '@playwright/test';
|
||||
import {
|
||||
WORKER_URL,
|
||||
TEST_DOMAIN,
|
||||
createTestAddress,
|
||||
deleteAddress,
|
||||
} from '../../fixtures/test-helpers';
|
||||
|
||||
const ADMIN_PASSWORD = 'e2e-admin-pass';
|
||||
const ADMIN_HEADERS = { 'x-admin-auth': ADMIN_PASSWORD };
|
||||
|
||||
const DEFAULT_ACCOUNT_SETTINGS = {
|
||||
blockList: [],
|
||||
sendBlockList: [],
|
||||
verifiedAddressList: [],
|
||||
fromBlockList: [],
|
||||
noLimitSendAddressList: [],
|
||||
emailRuleSettings: {},
|
||||
addressCreationSettings: {},
|
||||
};
|
||||
|
||||
async function resetAccountSettings(request: APIRequestContext) {
|
||||
const res = await request.post(`${WORKER_URL}/admin/account_settings`, {
|
||||
headers: ADMIN_HEADERS,
|
||||
data: DEFAULT_ACCOUNT_SETTINGS,
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
}
|
||||
|
||||
test.describe('Email forward domain normalization', () => {
|
||||
test.afterEach(async ({ request }) => {
|
||||
await resetAccountSettings(request);
|
||||
});
|
||||
|
||||
test('normalizes uppercase forwarding rule and recipient domains', async ({ request }) => {
|
||||
const { jwt, address } = await createTestAddress(request, 'forward-case');
|
||||
const forwardAddress = 'forward-target@test.example.com';
|
||||
|
||||
try {
|
||||
const saveRes = await request.post(`${WORKER_URL}/admin/account_settings`, {
|
||||
headers: ADMIN_HEADERS,
|
||||
data: {
|
||||
...DEFAULT_ACCOUNT_SETTINGS,
|
||||
emailRuleSettings: {
|
||||
emailForwardingList: [{
|
||||
domains: [TEST_DOMAIN.toUpperCase()],
|
||||
forward: forwardAddress,
|
||||
}],
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(saveRes.ok()).toBe(true);
|
||||
|
||||
const to = address.replace(`@${TEST_DOMAIN}`, `@${TEST_DOMAIN.toUpperCase()}`);
|
||||
const subject = `forward-case-${Date.now()}`;
|
||||
const raw = [
|
||||
`From: sender@test.example.com`,
|
||||
`To: ${to}`,
|
||||
`Subject: ${subject}`,
|
||||
`Message-ID: <${subject}@test>`,
|
||||
`MIME-Version: 1.0`,
|
||||
`Content-Type: text/plain; charset=utf-8`,
|
||||
``,
|
||||
`Forward domain normalization test`,
|
||||
].join('\r\n');
|
||||
|
||||
const res = await request.post(`${WORKER_URL}/admin/test/receive_mail`, {
|
||||
data: { from: 'sender@test.example.com', to, raw },
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
const body = await res.json();
|
||||
expect(body.success).toBe(true);
|
||||
expect(body.forwardedTo).toEqual([forwardAddress]);
|
||||
|
||||
const mailsRes = await request.get(`${WORKER_URL}/api/mails?limit=10&offset=0`, {
|
||||
headers: { Authorization: `Bearer ${jwt}` },
|
||||
});
|
||||
expect(mailsRes.ok()).toBe(true);
|
||||
const mailsBody = await mailsRes.json();
|
||||
expect(mailsBody.results.some((mail: { address: string; raw: string }) => {
|
||||
return mail.address === address && mail.raw.includes(subject);
|
||||
})).toBe(true);
|
||||
} finally {
|
||||
await deleteAddress(request, jwt);
|
||||
}
|
||||
});
|
||||
|
||||
test('does not forward when only the domain suffix string matches', async ({ request }) => {
|
||||
const { jwt, address } = await createTestAddress(request, 'forward-boundary');
|
||||
const forwardAddress = 'forward-boundary-target@test.example.com';
|
||||
|
||||
try {
|
||||
const saveRes = await request.post(`${WORKER_URL}/admin/account_settings`, {
|
||||
headers: ADMIN_HEADERS,
|
||||
data: {
|
||||
...DEFAULT_ACCOUNT_SETTINGS,
|
||||
emailRuleSettings: {
|
||||
emailForwardingList: [{
|
||||
domains: [TEST_DOMAIN],
|
||||
forward: forwardAddress,
|
||||
}],
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(saveRes.ok()).toBe(true);
|
||||
|
||||
const to = address.replace(`@${TEST_DOMAIN}`, `@evil${TEST_DOMAIN}`);
|
||||
const subject = `forward-boundary-${Date.now()}`;
|
||||
const raw = [
|
||||
`From: sender@test.example.com`,
|
||||
`To: ${to}`,
|
||||
`Subject: ${subject}`,
|
||||
`Message-ID: <${subject}@test>`,
|
||||
`MIME-Version: 1.0`,
|
||||
`Content-Type: text/plain; charset=utf-8`,
|
||||
``,
|
||||
`Forward domain boundary test`,
|
||||
].join('\r\n');
|
||||
|
||||
const res = await request.post(`${WORKER_URL}/admin/test/receive_mail`, {
|
||||
data: { from: 'sender@test.example.com', to, raw },
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
const body = await res.json();
|
||||
expect(body.success).toBe(true);
|
||||
expect(body.forwardedTo).toEqual([]);
|
||||
} finally {
|
||||
await deleteAddress(request, jwt);
|
||||
}
|
||||
});
|
||||
|
||||
test('keeps blank forwarding rule domain as catch-all', async ({ request }) => {
|
||||
const { jwt, address } = await createTestAddress(request, 'forward-catch-all');
|
||||
const forwardAddress = 'forward-catch-all-target@test.example.com';
|
||||
|
||||
try {
|
||||
const saveRes = await request.post(`${WORKER_URL}/admin/account_settings`, {
|
||||
headers: ADMIN_HEADERS,
|
||||
data: {
|
||||
...DEFAULT_ACCOUNT_SETTINGS,
|
||||
emailRuleSettings: {
|
||||
emailForwardingList: [{
|
||||
domains: ['', 'not-the-domain.example.com'],
|
||||
forward: forwardAddress,
|
||||
}],
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(saveRes.ok()).toBe(true);
|
||||
|
||||
const subject = `forward-catch-all-${Date.now()}`;
|
||||
const raw = [
|
||||
`From: sender@test.example.com`,
|
||||
`To: ${address}`,
|
||||
`Subject: ${subject}`,
|
||||
`Message-ID: <${subject}@test>`,
|
||||
`MIME-Version: 1.0`,
|
||||
`Content-Type: text/plain; charset=utf-8`,
|
||||
``,
|
||||
`Forward catch-all domain test`,
|
||||
].join('\r\n');
|
||||
|
||||
const res = await request.post(`${WORKER_URL}/admin/test/receive_mail`, {
|
||||
data: { from: 'sender@test.example.com', to: address, raw },
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
const body = await res.json();
|
||||
expect(body.success).toBe(true);
|
||||
expect(body.forwardedTo).toEqual([forwardAddress]);
|
||||
} finally {
|
||||
await deleteAddress(request, jwt);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -15,6 +15,7 @@ test.describe('Health & Settings', () => {
|
||||
const settings = await res.json();
|
||||
expect(settings.domains).toContain('test.example.com');
|
||||
expect(settings.defaultDomains).toContain('test.example.com');
|
||||
expect(settings.prefix).toBe('tmp');
|
||||
expect(settings.enableSendMail).toBe(true);
|
||||
expect(settings.enableUserCreateEmail).toBe(true);
|
||||
expect(settings.enableUserDeleteEmail).toBe(true);
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { WORKER_URL, createTestAddress, deleteAddress } from '../../fixtures/test-helpers';
|
||||
import * as crypto from 'crypto';
|
||||
|
||||
/**
|
||||
* SHA-256 hash matching frontend hashPassword utility.
|
||||
*/
|
||||
function hashPassword(password: string): string {
|
||||
return crypto.createHash('sha256').update(password).digest('hex');
|
||||
}
|
||||
import { WORKER_URL, createTestAddress, deleteAddress, hashPassword } from '../../fixtures/test-helpers';
|
||||
|
||||
test.describe('Turnstile Login Endpoints (ENABLE_GLOBAL_TURNSTILE_CHECK disabled)', () => {
|
||||
|
||||
@@ -110,14 +102,14 @@ test.describe('Turnstile Login Endpoints (ENABLE_GLOBAL_TURNSTILE_CHECK disabled
|
||||
// Set a password
|
||||
await request.post(`${WORKER_URL}/api/address_change_password`, {
|
||||
headers: { Authorization: `Bearer ${jwt}` },
|
||||
data: { new_password: 'addr-pass-123' },
|
||||
data: { new_password: hashPassword('addr-pass-123') },
|
||||
});
|
||||
|
||||
// Login with cf_token field present but empty
|
||||
const loginRes = await request.post(`${WORKER_URL}/api/address_login`, {
|
||||
data: {
|
||||
email: address,
|
||||
password: 'addr-pass-123',
|
||||
password: hashPassword('addr-pass-123'),
|
||||
cf_token: ''
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { WORKER_URL, createTestAddress, seedTestMail, deleteAddress } from '../../fixtures/test-helpers';
|
||||
|
||||
const ADMIN_HEADERS = { 'x-admin-auth': 'e2e-admin-pass' };
|
||||
|
||||
test.describe('Mail Detail', () => {
|
||||
test('fetch a single mail by ID', async ({ request }) => {
|
||||
const { jwt, address } = await createTestAddress(request, 'detail-get');
|
||||
@@ -53,3 +55,46 @@ test.describe('Mail Detail', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Admin Mail Detail', () => {
|
||||
test('fetch a single mail by ID without a mailbox JWT', async ({ request }) => {
|
||||
const { jwt, address } = await createTestAddress(request, 'admin-detail-get');
|
||||
|
||||
try {
|
||||
await seedTestMail(request, address, {
|
||||
subject: 'Admin Detail Test',
|
||||
from: 'admin-detail@test.example.com',
|
||||
text: 'Hello admin detail',
|
||||
});
|
||||
|
||||
const listRes = await request.get(`${WORKER_URL}/api/mails?limit=10&offset=0`, {
|
||||
headers: { Authorization: `Bearer ${jwt}` },
|
||||
});
|
||||
expect(listRes.ok()).toBe(true);
|
||||
const { results } = await listRes.json();
|
||||
expect(results).toHaveLength(1);
|
||||
const mailId = results[0].id;
|
||||
|
||||
const detailRes = await request.get(`${WORKER_URL}/admin/mails/${mailId}`, {
|
||||
headers: ADMIN_HEADERS,
|
||||
});
|
||||
expect(detailRes.ok()).toBe(true);
|
||||
const mail = await detailRes.json();
|
||||
expect(mail.id).toBe(mailId);
|
||||
expect(mail.address).toBe(address);
|
||||
expect(mail.source).toBe('admin-detail@test.example.com');
|
||||
expect(mail.raw).toContain('Admin Detail Test');
|
||||
expect(mail.raw_blob).toBeUndefined();
|
||||
} finally {
|
||||
await deleteAddress(request, jwt);
|
||||
}
|
||||
});
|
||||
|
||||
test('fetch non-existent mail returns null', async ({ request }) => {
|
||||
const res = await request.get(`${WORKER_URL}/admin/mails/99999999`, {
|
||||
headers: ADMIN_HEADERS,
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
expect(await res.json()).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { test, expect, APIRequestContext } from '@playwright/test';
|
||||
import { test, expect } from '@playwright/test';
|
||||
import type { APIRequestContext } from '@playwright/test';
|
||||
import {
|
||||
WORKER_URL,
|
||||
WORKER_URL_SEND_MAIL_DOMAIN,
|
||||
@@ -425,11 +426,11 @@ test.describe('Send Mail Limit', () => {
|
||||
expect(res.status()).toBe(400);
|
||||
});
|
||||
|
||||
test('/admin/send_mail_by_binding returns 200 when domain is allowed', async ({ request }) => {
|
||||
test('/admin/send_mail_by_binding normalizes uppercase allowed domain', async ({ request }) => {
|
||||
const res = await request.post(`${WORKER_URL_SEND_MAIL_DOMAIN}/admin/send_mail_by_binding`, {
|
||||
headers: ADMIN_HEADERS,
|
||||
data: {
|
||||
from: 'admin@test.example.com',
|
||||
from: 'admin@TEST.EXAMPLE.COM',
|
||||
to: ['recipient@test.example.com'],
|
||||
subject: `send-mail-domain-ok-${Date.now()}`,
|
||||
text: 'body',
|
||||
@@ -439,6 +440,20 @@ test.describe('Send Mail Limit', () => {
|
||||
expect(await res.json()).toEqual({ status: 'ok' });
|
||||
});
|
||||
|
||||
test('/admin/send_mail_by_binding normalizes object-form from domain', async ({ request }) => {
|
||||
const res = await request.post(`${WORKER_URL_SEND_MAIL_DOMAIN}/admin/send_mail_by_binding`, {
|
||||
headers: ADMIN_HEADERS,
|
||||
data: {
|
||||
from: { email: 'admin@TEST.EXAMPLE.COM', name: 'Admin' },
|
||||
to: ['recipient@test.example.com'],
|
||||
subject: `send-mail-domain-object-ok-${Date.now()}`,
|
||||
text: 'body',
|
||||
},
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
expect(await res.json()).toEqual({ status: 'ok' });
|
||||
});
|
||||
|
||||
test('/admin/send_mail_by_binding returns 400 when domain is not allowed', async ({ request }) => {
|
||||
const res = await request.post(`${WORKER_URL_SEND_MAIL_DOMAIN}/admin/send_mail_by_binding`, {
|
||||
headers: ADMIN_HEADERS,
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { WORKER_URL, createTestAddress, deleteAddress } from '../../fixtures/test-helpers';
|
||||
|
||||
test.describe('Telegram AI extraction rendering', () => {
|
||||
test('realtime mail stores AI extraction metadata for Telegram rendering', async ({ request }) => {
|
||||
const { jwt, address } = await createTestAddress(request, 'tg-ai');
|
||||
|
||||
try {
|
||||
const subject = `Telegram AI realtime ${Date.now()}`;
|
||||
const raw = [
|
||||
'From: sender@test.example.com',
|
||||
`To: ${address}`,
|
||||
`Subject: ${subject}`,
|
||||
`Message-ID: <telegram-ai-${Date.now()}@test>`,
|
||||
'MIME-Version: 1.0',
|
||||
'Content-Type: text/plain; charset=utf-8',
|
||||
'',
|
||||
'Telegram AI extraction realtime body',
|
||||
].join('\r\n');
|
||||
|
||||
const receiveRes = await request.post(`${WORKER_URL}/admin/test/receive_mail`, {
|
||||
data: {
|
||||
from: 'sender@test.example.com',
|
||||
to: address,
|
||||
raw,
|
||||
ai_extract_result: {
|
||||
type: 'auth_code',
|
||||
result: '123456',
|
||||
result_text: '',
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(receiveRes.ok()).toBe(true);
|
||||
const receiveBody = await receiveRes.json();
|
||||
expect(receiveBody.success).toBe(true);
|
||||
|
||||
const mailsRes = await request.get(`${WORKER_URL}/api/mails?limit=10&offset=0`, {
|
||||
headers: { Authorization: `Bearer ${jwt}` },
|
||||
});
|
||||
expect(mailsRes.ok()).toBe(true);
|
||||
const { results } = await mailsRes.json();
|
||||
expect(results).toHaveLength(1);
|
||||
|
||||
const metadata = JSON.parse(results[0].metadata);
|
||||
expect(metadata.ai_extract).toEqual({
|
||||
type: 'auth_code',
|
||||
result: '123456',
|
||||
result_text: '',
|
||||
});
|
||||
expect(metadata.extracted_at).toBeTruthy();
|
||||
} finally {
|
||||
await deleteAddress(request, jwt);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,170 @@
|
||||
import { test, expect, type APIRequestContext } from '@playwright/test';
|
||||
import {
|
||||
WORKER_URL,
|
||||
createTestAddress,
|
||||
deleteAddress,
|
||||
hashPassword,
|
||||
seedTestMail,
|
||||
} from '../../fixtures/test-helpers';
|
||||
|
||||
async function createUser(request: APIRequestContext) {
|
||||
const email = `address-page-${Date.now()}@test.example.com`;
|
||||
const password = hashPassword('test-password-123');
|
||||
|
||||
const registerRes = await request.post(`${WORKER_URL}/user_api/register`, {
|
||||
data: { email, password },
|
||||
});
|
||||
expect(registerRes.ok()).toBe(true);
|
||||
|
||||
const loginRes = await request.post(`${WORKER_URL}/user_api/login`, {
|
||||
data: { email, password },
|
||||
});
|
||||
expect(loginRes.ok()).toBe(true);
|
||||
const { jwt } = await loginRes.json();
|
||||
const payload = JSON.parse(Buffer.from(jwt.split('.')[1], 'base64url').toString('utf8'));
|
||||
return { jwt, userId: payload.user_id as number };
|
||||
}
|
||||
|
||||
test.describe('User address pagination', () => {
|
||||
test('paginates addresses and enforces mail ownership', async ({ request }) => {
|
||||
const addresses: Awaited<ReturnType<typeof createTestAddress>>[] = [];
|
||||
let outsider: Awaited<ReturnType<typeof createTestAddress>> | undefined;
|
||||
let originalUserSettings: Record<string, unknown> | undefined;
|
||||
let userId: number | undefined;
|
||||
|
||||
try {
|
||||
const settingsRes = await request.get(`${WORKER_URL}/admin/user_settings`);
|
||||
expect(settingsRes.ok()).toBe(true);
|
||||
originalUserSettings = await settingsRes.json();
|
||||
const enableUserRes = await request.post(`${WORKER_URL}/admin/user_settings`, {
|
||||
data: {
|
||||
...originalUserSettings,
|
||||
enable: true,
|
||||
enableMailVerify: false,
|
||||
maxAddressCount: 0,
|
||||
},
|
||||
});
|
||||
expect(enableUserRes.ok()).toBe(true);
|
||||
|
||||
const user = await createUser(request);
|
||||
const userJwt = user.jwt;
|
||||
userId = user.userId;
|
||||
addresses.push(...await Promise.all([
|
||||
createTestAddress(request, 'user-page-a'),
|
||||
createTestAddress(request, 'user-page-b'),
|
||||
createTestAddress(request, 'user-page-c'),
|
||||
]));
|
||||
outsider = await createTestAddress(request, 'user-page-outsider');
|
||||
|
||||
for (const item of addresses) {
|
||||
const bindRes = await request.post(`${WORKER_URL}/user_api/bind_address`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${item.jwt}`,
|
||||
'x-user-token': userJwt,
|
||||
},
|
||||
});
|
||||
expect(bindRes.ok()).toBe(true);
|
||||
}
|
||||
|
||||
const defaultPageRes = await request.get(`${WORKER_URL}/user_api/bind_address`, {
|
||||
headers: { 'x-user-token': userJwt },
|
||||
});
|
||||
expect(defaultPageRes.ok()).toBe(true);
|
||||
const defaultPage = await defaultPageRes.json();
|
||||
expect(defaultPage.count).toBe(3);
|
||||
expect(defaultPage.results).toHaveLength(3);
|
||||
|
||||
const firstPageRes = await request.get(
|
||||
`${WORKER_URL}/user_api/bind_address?limit=2&offset=0`,
|
||||
{ headers: { 'x-user-token': userJwt } },
|
||||
);
|
||||
expect(firstPageRes.ok()).toBe(true);
|
||||
const firstPage = await firstPageRes.json();
|
||||
expect(firstPage.count).toBe(3);
|
||||
expect(firstPage.results).toHaveLength(2);
|
||||
expect(firstPage.results[0].mail_count).toBe(0);
|
||||
expect(firstPage.results[0].send_count).toBe(0);
|
||||
expect(firstPage.results[0]).toHaveProperty('source_meta');
|
||||
expect(firstPage.results[0]).not.toHaveProperty('password');
|
||||
|
||||
const secondPageRes = await request.get(
|
||||
`${WORKER_URL}/user_api/bind_address?limit=2&offset=2`,
|
||||
{ headers: { 'x-user-token': userJwt } },
|
||||
);
|
||||
expect(secondPageRes.ok()).toBe(true);
|
||||
const secondPage = await secondPageRes.json();
|
||||
expect(secondPage.count).toBe(0);
|
||||
expect(secondPage.results).toHaveLength(1);
|
||||
|
||||
const invalidLimitRes = await request.get(
|
||||
`${WORKER_URL}/user_api/bind_address?limit=101&offset=0`,
|
||||
{ headers: { 'x-user-token': userJwt } },
|
||||
);
|
||||
expect(invalidLimitRes.status()).toBe(400);
|
||||
|
||||
const invalidOffsetRes = await request.get(
|
||||
`${WORKER_URL}/user_api/bind_address?limit=20&offset=-1`,
|
||||
{ headers: { 'x-user-token': userJwt } },
|
||||
);
|
||||
expect(invalidOffsetRes.status()).toBe(400);
|
||||
|
||||
await seedTestMail(request, addresses[0].address, { subject: 'Bound mail' });
|
||||
await seedTestMail(request, outsider.address, { subject: 'Outsider mail' });
|
||||
|
||||
const userMailsRes = await request.get(`${WORKER_URL}/user_api/mails?limit=20&offset=0`, {
|
||||
headers: { 'x-user-token': userJwt },
|
||||
});
|
||||
expect(userMailsRes.ok()).toBe(true);
|
||||
const userMails = await userMailsRes.json();
|
||||
expect(userMails.count).toBe(1);
|
||||
expect(userMails.results[0].address).toBe(addresses[0].address);
|
||||
|
||||
const filteredOutsiderMailsRes = await request.get(
|
||||
`${WORKER_URL}/user_api/mails?limit=20&offset=0&address=${encodeURIComponent(outsider.address)}`,
|
||||
{ headers: { 'x-user-token': userJwt } },
|
||||
);
|
||||
expect(filteredOutsiderMailsRes.ok()).toBe(true);
|
||||
const filteredOutsiderMails = await filteredOutsiderMailsRes.json();
|
||||
expect(filteredOutsiderMails.count).toBe(0);
|
||||
expect(filteredOutsiderMails.results).toHaveLength(0);
|
||||
|
||||
const outsiderMailsRes = await request.get(`${WORKER_URL}/api/mails?limit=20&offset=0`, {
|
||||
headers: { Authorization: `Bearer ${outsider.jwt}` },
|
||||
});
|
||||
expect(outsiderMailsRes.ok()).toBe(true);
|
||||
const outsiderMails = await outsiderMailsRes.json();
|
||||
expect(outsiderMails.results).toHaveLength(1);
|
||||
|
||||
const forbiddenDeleteRes = await request.delete(
|
||||
`${WORKER_URL}/user_api/mails/${outsiderMails.results[0].id}`,
|
||||
{ headers: { 'x-user-token': userJwt } },
|
||||
);
|
||||
expect(forbiddenDeleteRes.ok()).toBe(true);
|
||||
|
||||
const outsiderAfterRes = await request.get(`${WORKER_URL}/api/mails?limit=20&offset=0`, {
|
||||
headers: { Authorization: `Bearer ${outsider.jwt}` },
|
||||
});
|
||||
expect(outsiderAfterRes.ok()).toBe(true);
|
||||
const outsiderAfter = await outsiderAfterRes.json();
|
||||
expect(outsiderAfter.results).toHaveLength(1);
|
||||
} finally {
|
||||
try {
|
||||
await Promise.allSettled(
|
||||
[...addresses, outsider].filter((item) => item !== undefined)
|
||||
.map((item) => deleteAddress(request, item.jwt)),
|
||||
);
|
||||
if (userId !== undefined) {
|
||||
const deleteUserRes = await request.delete(`${WORKER_URL}/admin/users/${userId}`);
|
||||
expect(deleteUserRes.ok()).toBe(true);
|
||||
}
|
||||
} finally {
|
||||
if (originalUserSettings) {
|
||||
const restoreSettingsRes = await request.post(`${WORKER_URL}/admin/user_settings`, {
|
||||
data: originalUserSettings,
|
||||
});
|
||||
expect(restoreSettingsRes.ok()).toBe(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,118 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import type { APIRequestContext } from '@playwright/test';
|
||||
import http from 'node:http';
|
||||
import { WORKER_URL } from '../../fixtures/test-helpers';
|
||||
|
||||
async function resetUserSettings(request: APIRequestContext) {
|
||||
const res = await request.post(`${WORKER_URL}/admin/user_settings`, {
|
||||
data: {
|
||||
enable: true,
|
||||
enableMailVerify: false,
|
||||
enableMailAllowList: false,
|
||||
mailAllowList: [],
|
||||
},
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
}
|
||||
|
||||
async function resetOauth2Settings(request: APIRequestContext) {
|
||||
const res = await request.post(`${WORKER_URL}/admin/user_oauth2_settings`, {
|
||||
data: [],
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
}
|
||||
|
||||
async function startOauthServer(email: string): Promise<{ server: http.Server; baseUrl: string }> {
|
||||
const server = http.createServer((req, res) => {
|
||||
if (req.url === '/token') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ access_token: 'token', token_type: 'Bearer' }));
|
||||
return;
|
||||
}
|
||||
if (req.url === '/userinfo') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ email }));
|
||||
return;
|
||||
}
|
||||
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
||||
res.end('not found');
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => server.listen(0, '0.0.0.0', resolve));
|
||||
const addr = server.address();
|
||||
if (!addr || typeof addr === 'string') throw new Error('Failed to resolve OAuth test server port');
|
||||
const hostname = process.env.CI ? 'e2e-runner' : 'localhost';
|
||||
return { server, baseUrl: `http://${hostname}:${addr.port}` };
|
||||
}
|
||||
|
||||
test.describe('User domain normalization', () => {
|
||||
test.afterEach(async ({ request }) => {
|
||||
await resetOauth2Settings(request);
|
||||
await resetUserSettings(request);
|
||||
});
|
||||
|
||||
test('normalizes uppercase verify mail sender domain in admin settings', async ({ request }) => {
|
||||
const res = await request.post(`${WORKER_URL}/admin/user_settings`, {
|
||||
data: {
|
||||
enable: true,
|
||||
enableMailVerify: true,
|
||||
verifyMailSender: 'verify@TEST.EXAMPLE.COM',
|
||||
},
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
});
|
||||
|
||||
test('normalizes uppercase user registration allow-list domains', async ({ request }) => {
|
||||
const saveRes = await request.post(`${WORKER_URL}/admin/user_settings`, {
|
||||
data: {
|
||||
enable: true,
|
||||
enableMailVerify: false,
|
||||
enableMailAllowList: true,
|
||||
mailAllowList: ['TEST.EXAMPLE.COM'],
|
||||
},
|
||||
});
|
||||
expect(saveRes.ok()).toBe(true);
|
||||
|
||||
const registerRes = await request.post(`${WORKER_URL}/user_api/register`, {
|
||||
data: {
|
||||
email: `allow-list-${Date.now()}@TEST.EXAMPLE.COM`,
|
||||
password: 'allow-list-password',
|
||||
},
|
||||
});
|
||||
expect(registerRes.ok()).toBe(true);
|
||||
});
|
||||
|
||||
test('normalizes uppercase OAuth2 allow-list domains', async ({ request }) => {
|
||||
const email = `oauth-allow-${Date.now()}@TEST.EXAMPLE.COM`;
|
||||
const { server, baseUrl } = await startOauthServer(email);
|
||||
|
||||
try {
|
||||
const saveRes = await request.post(`${WORKER_URL}/admin/user_oauth2_settings`, {
|
||||
data: [{
|
||||
name: 'case-oauth',
|
||||
clientID: 'case-client',
|
||||
clientSecret: 'case-secret',
|
||||
authorizationURL: `${baseUrl}/authorize`,
|
||||
accessTokenURL: `${baseUrl}/token`,
|
||||
accessTokenFormat: 'json',
|
||||
userInfoURL: `${baseUrl}/userinfo`,
|
||||
redirectURL: `${baseUrl}/callback`,
|
||||
userEmailKey: 'email',
|
||||
scope: 'openid email',
|
||||
enableMailAllowList: true,
|
||||
mailAllowList: ['TEST.EXAMPLE.COM'],
|
||||
}],
|
||||
});
|
||||
expect(saveRes.ok()).toBe(true);
|
||||
|
||||
const callbackRes = await request.post(`${WORKER_URL}/user_api/oauth2/callback`, {
|
||||
data: { clientID: 'case-client', code: 'case-code' },
|
||||
});
|
||||
expect(callbackRes.ok()).toBe(true);
|
||||
const body = await callbackRes.json();
|
||||
expect(body.jwt).toBeTruthy();
|
||||
} finally {
|
||||
server.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -72,6 +72,9 @@ test.describe('Webhook — triggered on incoming mail', () => {
|
||||
from: '${from}',
|
||||
to: '${to}',
|
||||
subject: '${subject}',
|
||||
aiExtractType: '${aiExtractType}',
|
||||
aiExtractResult: '${aiExtractResult}',
|
||||
aiExtractResultText: '${aiExtractResultText}',
|
||||
}),
|
||||
},
|
||||
});
|
||||
@@ -93,7 +96,16 @@ test.describe('Webhook — triggered on incoming mail', () => {
|
||||
].join('\r\n');
|
||||
|
||||
const res = await request.post(`${WORKER_URL}/admin/test/receive_mail`, {
|
||||
data: { from, to: address, raw },
|
||||
data: {
|
||||
from,
|
||||
to: address,
|
||||
raw,
|
||||
ai_extract_result: {
|
||||
type: 'auth_code',
|
||||
result: '654321',
|
||||
result_text: 'Login verification code',
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
|
||||
@@ -106,6 +118,9 @@ test.describe('Webhook — triggered on incoming mail', () => {
|
||||
expect(payload.from).toContain('webhook-sender@test.example.com');
|
||||
expect(payload.to).toBe(address);
|
||||
expect(payload.subject).toBe(subject);
|
||||
expect(payload.aiExtractType).toBe('auth_code');
|
||||
expect(payload.aiExtractResult).toBe('654321');
|
||||
expect(payload.aiExtractResultText).toBe('Login verification code');
|
||||
} finally {
|
||||
server.close();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
import { FRONTEND_URL } from '../../fixtures/test-helpers';
|
||||
|
||||
const installLocaleInitScript = async (page: Page, locales: string[], preferredLocale: string | null = null) => {
|
||||
await page.addInitScript(
|
||||
({ locales: initialLocales, preferredLocale: initialPreferredLocale }: { locales: string[]; preferredLocale: string | null }) => {
|
||||
const localeInitStorageKey = '__localeInitDone';
|
||||
if (!window.sessionStorage.getItem(localeInitStorageKey)) {
|
||||
window.localStorage.removeItem('preferredLocale');
|
||||
if (initialPreferredLocale) {
|
||||
window.localStorage.setItem('preferredLocale', initialPreferredLocale);
|
||||
}
|
||||
window.sessionStorage.setItem(localeInitStorageKey, '1');
|
||||
}
|
||||
|
||||
Object.defineProperty(window.navigator, 'language', {
|
||||
configurable: true,
|
||||
get: () => initialLocales[0],
|
||||
});
|
||||
Object.defineProperty(window.navigator, 'languages', {
|
||||
configurable: true,
|
||||
get: () => initialLocales,
|
||||
});
|
||||
},
|
||||
{ locales, preferredLocale },
|
||||
);
|
||||
};
|
||||
|
||||
const selectLanguage = async (page: Page, selectTrigger: Locator, optionLabel: string) => {
|
||||
await selectTrigger.click();
|
||||
const option = page.locator('.n-dropdown-option, .n-dropdown-option-body').filter({ hasText: optionLabel }).first();
|
||||
await expect(option).toBeVisible();
|
||||
await option.click();
|
||||
};
|
||||
|
||||
test.describe('Locale switching', () => {
|
||||
test('keeps default route in Chinese while persisting browser language preference', async ({ page }) => {
|
||||
await installLocaleInitScript(page, ['es-ES', 'en-US']);
|
||||
|
||||
await page.goto(`${FRONTEND_URL}/`);
|
||||
|
||||
await expect(page).toHaveURL(`${FRONTEND_URL}/`);
|
||||
await expect.poll(() => page.evaluate(() => window.localStorage.getItem('preferredLocale'))).toBe('es');
|
||||
await expect.poll(() => page.evaluate(() => document.documentElement.lang)).toBe('zh');
|
||||
});
|
||||
|
||||
test('mobile drawer switch updates locale route and persisted preference', async ({ page }) => {
|
||||
await page.setViewportSize({ width: 375, height: 844 });
|
||||
await installLocaleInitScript(page, ['zh-CN']);
|
||||
|
||||
await page.goto(`${FRONTEND_URL}/`);
|
||||
|
||||
await page.getByRole('button', { name: /菜单|Menu/i }).click();
|
||||
|
||||
const drawerLocaleDropdown = page.locator('.n-drawer').getByRole('button', { name: /中文|English|Español|Português|日本語|Deutsch/ }).first();
|
||||
await selectLanguage(page, drawerLocaleDropdown, 'Deutsch');
|
||||
|
||||
await expect(page).toHaveURL(`${FRONTEND_URL}/de/`);
|
||||
await expect.poll(() => page.evaluate(() => window.localStorage.getItem('preferredLocale'))).toBe('de');
|
||||
await expect.poll(() => page.evaluate(() => document.documentElement.lang)).toBe('de');
|
||||
});
|
||||
});
|
||||
@@ -62,8 +62,8 @@ test.describe('Passkey Browser Flow', () => {
|
||||
// Wait for user settings to load (shows user email)
|
||||
await expect(page.getByText(TEST_USER_EMAIL)).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
// === Step 2: Click "User Settings" tab ===
|
||||
await page.getByText('User Settings').click();
|
||||
// === Step 2: Click "User Account Settings" tab ===
|
||||
await page.getByText('User Account Settings').click();
|
||||
|
||||
// === Step 3: Create a passkey ===
|
||||
await page.getByRole('button', { name: 'Create Passkey' }).click();
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
import { expect, request as apiRequest, test } from '@playwright/test';
|
||||
import type { APIRequestContext } from '@playwright/test';
|
||||
|
||||
import {
|
||||
FRONTEND_URL,
|
||||
WORKER_URL,
|
||||
createTestAddress,
|
||||
deleteAddress,
|
||||
hashPassword,
|
||||
} from '../../fixtures/test-helpers';
|
||||
|
||||
async function saveUserSettings(request: APIRequestContext, settings: Record<string, unknown>) {
|
||||
const response = await request.post(`${WORKER_URL}/admin/user_settings`, { data: settings });
|
||||
expect(response.ok()).toBe(true);
|
||||
}
|
||||
|
||||
async function createUser(request: APIRequestContext) {
|
||||
const email = `address-browser-${Date.now()}@test.example.com`;
|
||||
const password = hashPassword('test-password-123');
|
||||
const registerResponse = await request.post(`${WORKER_URL}/user_api/register`, {
|
||||
data: { email, password },
|
||||
});
|
||||
expect(registerResponse.ok()).toBe(true);
|
||||
|
||||
const loginResponse = await request.post(`${WORKER_URL}/user_api/login`, {
|
||||
data: { email, password },
|
||||
});
|
||||
expect(loginResponse.ok()).toBe(true);
|
||||
const { jwt } = await loginResponse.json();
|
||||
const payload = JSON.parse(Buffer.from(jwt.split('.')[1], 'base64url').toString('utf8'));
|
||||
return { email, jwt, userId: payload.user_id as number };
|
||||
}
|
||||
|
||||
async function bindAddress(request: APIRequestContext, userJwt: string, addressJwt: string) {
|
||||
const response = await request.post(`${WORKER_URL}/user_api/bind_address`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${addressJwt}`,
|
||||
'x-user-token': userJwt,
|
||||
},
|
||||
});
|
||||
expect(response.ok()).toBe(true);
|
||||
}
|
||||
|
||||
test.describe('User address pagination browser flow', () => {
|
||||
test('paginates addresses and filters mail', async ({ page }) => {
|
||||
test.setTimeout(120_000);
|
||||
|
||||
const request = await apiRequest.newContext();
|
||||
const createdAddresses: Awaited<ReturnType<typeof createTestAddress>>[] = [];
|
||||
let originalUserSettings: Record<string, unknown> | undefined;
|
||||
let userId: number | undefined;
|
||||
|
||||
try {
|
||||
const settingsResponse = await request.get(`${WORKER_URL}/admin/user_settings`);
|
||||
expect(settingsResponse.ok()).toBe(true);
|
||||
originalUserSettings = await settingsResponse.json();
|
||||
await saveUserSettings(request, {
|
||||
...originalUserSettings,
|
||||
enable: true,
|
||||
enableMailVerify: false,
|
||||
maxAddressCount: 0,
|
||||
});
|
||||
|
||||
const user = await createUser(request);
|
||||
userId = user.userId;
|
||||
|
||||
for (let index = 0; index < 21; index += 1) {
|
||||
const address = await createTestAddress(request, `browser-page-${index}-`);
|
||||
createdAddresses.push(address);
|
||||
await bindAddress(request, user.jwt, address.jwt);
|
||||
}
|
||||
const defaultAddressPageResponse = await request.get(
|
||||
`${WORKER_URL}/user_api/bind_address`,
|
||||
{ headers: { 'x-user-token': user.jwt } },
|
||||
);
|
||||
expect(defaultAddressPageResponse.ok()).toBe(true);
|
||||
const defaultAddressPage = await defaultAddressPageResponse.json();
|
||||
expect(defaultAddressPage.count).toBe(21);
|
||||
expect(defaultAddressPage.results).toHaveLength(20);
|
||||
|
||||
await page.goto(`${FRONTEND_URL}/en/`);
|
||||
await page.evaluate((userJwt) => {
|
||||
localStorage.setItem('userJwt', userJwt);
|
||||
}, user.jwt);
|
||||
await page.goto(`${FRONTEND_URL}/en/user`);
|
||||
|
||||
await expect(page.getByText(user.email)).toBeVisible({ timeout: 15_000 });
|
||||
const pagination = page.locator('.n-pagination').first();
|
||||
const addressRows = page.locator('.n-data-table-tbody .n-data-table-tr');
|
||||
await expect(pagination).toContainText(/Total:\s*21/);
|
||||
await expect(addressRows).toHaveCount(20);
|
||||
|
||||
await pagination.locator('.n-pagination-item').filter({ hasText: /^2$/ }).click();
|
||||
await expect(addressRows).toHaveCount(1);
|
||||
|
||||
const selectedAddress = createdAddresses[20];
|
||||
|
||||
const initialMailboxAddressesResponse = page.waitForResponse((response) => {
|
||||
const url = new URL(response.url());
|
||||
return url.pathname === '/user_api/bind_address'
|
||||
&& url.searchParams.get('limit') === '100';
|
||||
});
|
||||
await page.getByText('Mail Box', { exact: true }).click();
|
||||
const initialMailboxResponse = await initialMailboxAddressesResponse;
|
||||
expect(initialMailboxResponse.ok()).toBe(true);
|
||||
const mailboxAddressSelect = page.locator('.n-input-group .n-select').first();
|
||||
await mailboxAddressSelect.click();
|
||||
|
||||
const mailboxOptions = page.locator('.n-base-select-menu:visible');
|
||||
await expect(mailboxOptions).toContainText(selectedAddress.address);
|
||||
const filteredMailResponse = page.waitForResponse((response) => {
|
||||
const url = new URL(response.url());
|
||||
return url.pathname === '/user_api/mails'
|
||||
&& url.searchParams.get('address') === selectedAddress.address;
|
||||
});
|
||||
await mailboxOptions.getByText(selectedAddress.address, { exact: true }).click();
|
||||
expect((await filteredMailResponse).ok()).toBe(true);
|
||||
} finally {
|
||||
try {
|
||||
try {
|
||||
await Promise.allSettled(createdAddresses.map((address) => deleteAddress(request, address.jwt)));
|
||||
if (userId !== undefined) {
|
||||
await request.delete(`${WORKER_URL}/admin/users/${userId}`);
|
||||
}
|
||||
} finally {
|
||||
if (originalUserSettings) {
|
||||
await saveUserSettings(request, originalUserSettings);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
await request.dispose();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -132,8 +132,10 @@ test.describe('IMAP Proxy', () => {
|
||||
try {
|
||||
const results = await client.search({ all: true });
|
||||
expect(results.length).toBeGreaterThan(0);
|
||||
const msg = await client.fetchOne(String(results[0]), { uid: true, flags: true }, { uid: true });
|
||||
expect(msg.uid).toBe(results[0]);
|
||||
const seqMsg = await client.fetchOne(String(results[0]), { uid: true, flags: true });
|
||||
expect(seqMsg.uid).toBeGreaterThan(0);
|
||||
const uidMsg = await client.fetchOne(String(seqMsg.uid), { uid: true, flags: true }, { uid: true });
|
||||
expect(uidMsg.uid).toBe(seqMsg.uid);
|
||||
} finally {
|
||||
lock.release();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
node_modules
|
||||
dist
|
||||
.DS_Store
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/index.css",
|
||||
"baseColor": "zinc",
|
||||
"cssVariables": true
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Temp Email Next</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "cloudflare-temp-email-next",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "^4.1.17",
|
||||
"@vitejs/plugin-react": "^5.1.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"dompurify": "^3.3.0",
|
||||
"lucide-react": "^0.561.0",
|
||||
"next-themes": "^0.4.6",
|
||||
"radix-ui": "^1.6.0",
|
||||
"react": "^19.2.3",
|
||||
"react-dom": "^19.2.3",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tailwindcss": "^4.1.17",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.3.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^26.0.1",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/react-dom": "^19.2.3"
|
||||
},
|
||||
"packageManager": "pnpm@10.10.0"
|
||||
}
|
||||
Generated
+3210
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,399 @@
|
||||
const API_BASE = import.meta.env.VITE_API_BASE || ""
|
||||
|
||||
export type DomainOption = {
|
||||
label: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export type OpenSettings = {
|
||||
fetched: boolean
|
||||
title: string
|
||||
prefix: string
|
||||
addressRegex: string
|
||||
minAddressLen: number
|
||||
maxAddressLen: number
|
||||
needAuth: boolean
|
||||
enableUserCreateEmail: boolean
|
||||
disableAnonymousUserCreateEmail: boolean
|
||||
disableCustomAddressName: boolean
|
||||
enableUserDeleteEmail: boolean
|
||||
enableSendMail: boolean
|
||||
enableAddressPassword: boolean
|
||||
defaultDomains: string[]
|
||||
randomSubdomainDomains: string[]
|
||||
domains: DomainOption[]
|
||||
cfTurnstileSiteKey: string
|
||||
enableGlobalTurnstileCheck: boolean
|
||||
}
|
||||
|
||||
export type AddressSettings = {
|
||||
fetched: boolean
|
||||
address: string
|
||||
send_balance: number
|
||||
auto_reply?: unknown
|
||||
}
|
||||
|
||||
export type UserOpenSettings = {
|
||||
fetched: boolean
|
||||
enable: boolean
|
||||
enableMailVerify: boolean
|
||||
oauth2ClientIDs: { clientID: string; name: string; icon?: string }[]
|
||||
}
|
||||
|
||||
export type UserSettings = {
|
||||
fetched: boolean
|
||||
user_email: string
|
||||
user_id: number
|
||||
is_admin: boolean
|
||||
access_token: string | null
|
||||
new_user_token: string | null
|
||||
user_role: { domains?: string[] | null; role: string; prefix?: string | null } | null
|
||||
}
|
||||
|
||||
export type BoundAddress = {
|
||||
id: number | string
|
||||
name?: string
|
||||
address?: string
|
||||
mail_count?: number
|
||||
send_count?: number
|
||||
}
|
||||
|
||||
export type MailItem = {
|
||||
id: number | string
|
||||
source?: string
|
||||
address?: string
|
||||
subject?: string
|
||||
sender?: string
|
||||
html?: string
|
||||
message?: string
|
||||
text?: string
|
||||
raw?: string
|
||||
created_at?: string
|
||||
metadata?: unknown
|
||||
}
|
||||
|
||||
export type MailListResponse = {
|
||||
results: MailItem[]
|
||||
count: number
|
||||
}
|
||||
|
||||
export type SendMailPayload = {
|
||||
from_name: string
|
||||
to_name: string
|
||||
to_mail: string
|
||||
subject: string
|
||||
is_html: boolean
|
||||
content: string
|
||||
}
|
||||
|
||||
export const defaultOpenSettings: OpenSettings = {
|
||||
fetched: false,
|
||||
title: "",
|
||||
prefix: "",
|
||||
addressRegex: "",
|
||||
minAddressLen: 1,
|
||||
maxAddressLen: 30,
|
||||
needAuth: false,
|
||||
enableUserCreateEmail: false,
|
||||
disableAnonymousUserCreateEmail: false,
|
||||
disableCustomAddressName: false,
|
||||
enableUserDeleteEmail: false,
|
||||
enableSendMail: false,
|
||||
enableAddressPassword: false,
|
||||
defaultDomains: [],
|
||||
randomSubdomainDomains: [],
|
||||
domains: [],
|
||||
cfTurnstileSiteKey: "",
|
||||
enableGlobalTurnstileCheck: false,
|
||||
}
|
||||
|
||||
export const defaultAddressSettings: AddressSettings = {
|
||||
fetched: false,
|
||||
address: "",
|
||||
send_balance: 0,
|
||||
}
|
||||
|
||||
export const defaultUserOpenSettings: UserOpenSettings = {
|
||||
fetched: false,
|
||||
enable: false,
|
||||
enableMailVerify: false,
|
||||
oauth2ClientIDs: [],
|
||||
}
|
||||
|
||||
export const defaultUserSettings: UserSettings = {
|
||||
fetched: false,
|
||||
user_email: "",
|
||||
user_id: 0,
|
||||
is_admin: false,
|
||||
access_token: null,
|
||||
new_user_token: null,
|
||||
user_role: null,
|
||||
}
|
||||
|
||||
const hasControlChar = (value: string) => {
|
||||
for (let index = 0; index < value.length; index += 1) {
|
||||
const code = value.charCodeAt(index)
|
||||
if (code < 32 || code === 127) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const safeHeaderValue = (value: string | null | undefined) => {
|
||||
if (!value) return undefined
|
||||
const trimmed = value.trim()
|
||||
if (!trimmed || trimmed === "undefined" || trimmed === "null") return undefined
|
||||
if (hasControlChar(trimmed)) return undefined
|
||||
return trimmed
|
||||
}
|
||||
|
||||
const safeBearerHeader = (jwt: string | null | undefined) => {
|
||||
const safe = safeHeaderValue(jwt)
|
||||
return safe ? `Bearer ${safe}` : undefined
|
||||
}
|
||||
|
||||
const normalizeOpenSettings = (payload: Record<string, unknown>): OpenSettings => {
|
||||
const domains = Array.isArray(payload.domains) ? (payload.domains as string[]) : []
|
||||
const domainLabels = Array.isArray(payload.domainLabels) ? (payload.domainLabels as string[]) : []
|
||||
return {
|
||||
...defaultOpenSettings,
|
||||
fetched: true,
|
||||
title: String(payload.title || ""),
|
||||
prefix: String(payload.prefix || ""),
|
||||
addressRegex: String(payload.addressRegex || ""),
|
||||
minAddressLen: Number(payload.minAddressLen ?? 1),
|
||||
maxAddressLen: Number(payload.maxAddressLen ?? 30),
|
||||
needAuth: Boolean(payload.needAuth),
|
||||
enableUserCreateEmail: Boolean(payload.enableUserCreateEmail),
|
||||
disableAnonymousUserCreateEmail: Boolean(payload.disableAnonymousUserCreateEmail),
|
||||
disableCustomAddressName: Boolean(payload.disableCustomAddressName),
|
||||
enableUserDeleteEmail: Boolean(payload.enableUserDeleteEmail),
|
||||
enableSendMail: Boolean(payload.enableSendMail),
|
||||
enableAddressPassword: Boolean(payload.enableAddressPassword),
|
||||
defaultDomains: Array.isArray(payload.defaultDomains) ? (payload.defaultDomains as string[]) : [],
|
||||
randomSubdomainDomains: Array.isArray(payload.randomSubdomainDomains)
|
||||
? (payload.randomSubdomainDomains as string[])
|
||||
: [],
|
||||
domains: domains.map((domain, index) => ({
|
||||
label: domainLabels[index] || domain,
|
||||
value: domain,
|
||||
})),
|
||||
cfTurnstileSiteKey: String(payload.cfTurnstileSiteKey || ""),
|
||||
enableGlobalTurnstileCheck: Boolean(payload.enableGlobalTurnstileCheck),
|
||||
}
|
||||
}
|
||||
|
||||
export async function hashPassword(password: string) {
|
||||
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(password))
|
||||
return Array.from(new Uint8Array(digest))
|
||||
.map((byte) => byte.toString(16).padStart(2, "0"))
|
||||
.join("")
|
||||
}
|
||||
|
||||
export function parseJwtAddress(jwt: string) {
|
||||
try {
|
||||
const payload = JSON.parse(decodeURIComponent(atob(jwt.split(".")[1].replace(/-/g, "+").replace(/_/g, "/"))))
|
||||
return typeof payload.address === "string" ? payload.address : ""
|
||||
} catch {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
export function formatDate(value?: string) {
|
||||
if (!value) return ""
|
||||
const date = new Date(`${value} UTC`)
|
||||
if (Number.isNaN(date.getTime())) return value
|
||||
return date.toLocaleString()
|
||||
}
|
||||
|
||||
export class ApiClient {
|
||||
constructor(
|
||||
private readonly getJwt: () => string,
|
||||
private readonly getCustomAuth: () => string,
|
||||
private readonly getUserJwt: () => string,
|
||||
) {}
|
||||
|
||||
async request<T>(path: string, options: RequestInit = {}) {
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
"x-lang": "en",
|
||||
}
|
||||
const customAuth = safeHeaderValue(this.getCustomAuth())
|
||||
const userJwt = safeHeaderValue(this.getUserJwt())
|
||||
const authorization = safeBearerHeader(this.getJwt())
|
||||
if (customAuth) headers["x-custom-auth"] = customAuth
|
||||
if (userJwt) headers["x-user-token"] = userJwt
|
||||
if (authorization) headers.Authorization = authorization
|
||||
|
||||
const response = await fetch(`${API_BASE}${path}`, {
|
||||
...options,
|
||||
headers: {
|
||||
...headers,
|
||||
...(options.headers as Record<string, string> | undefined),
|
||||
},
|
||||
})
|
||||
const text = await response.text()
|
||||
const data = text ? tryParseJson(text) : null
|
||||
if (response.status >= 300) {
|
||||
throw new Error(typeof data === "string" ? data : text || `[${response.status}] request failed`)
|
||||
}
|
||||
return data as T
|
||||
}
|
||||
|
||||
async getOpenSettings() {
|
||||
return normalizeOpenSettings(await this.request<Record<string, unknown>>("/open_api/settings"))
|
||||
}
|
||||
|
||||
async getSettings() {
|
||||
if (!safeHeaderValue(this.getJwt())) return { ...defaultAddressSettings, fetched: true }
|
||||
const payload = await this.request<Record<string, unknown>>("/api/settings")
|
||||
return {
|
||||
fetched: true,
|
||||
address: String(payload.address || ""),
|
||||
send_balance: Number(payload.send_balance || 0),
|
||||
auto_reply: payload.auto_reply,
|
||||
} satisfies AddressSettings
|
||||
}
|
||||
|
||||
async getUserOpenSettings() {
|
||||
return {
|
||||
...defaultUserOpenSettings,
|
||||
...(await this.request<Partial<UserOpenSettings>>("/user_api/open_settings")),
|
||||
fetched: true,
|
||||
} satisfies UserOpenSettings
|
||||
}
|
||||
|
||||
async getUserSettings() {
|
||||
if (!safeHeaderValue(this.getUserJwt())) return { ...defaultUserSettings, fetched: true }
|
||||
return {
|
||||
...defaultUserSettings,
|
||||
...(await this.request<Partial<UserSettings>>("/user_api/settings")),
|
||||
fetched: true,
|
||||
} satisfies UserSettings
|
||||
}
|
||||
|
||||
async userLogin(email: string, password: string, cfToken: string) {
|
||||
const response = await this.request<{ jwt: string }>("/user_api/login", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
email,
|
||||
password: await hashPassword(password),
|
||||
cf_token: cfToken,
|
||||
}),
|
||||
})
|
||||
return response.jwt
|
||||
}
|
||||
|
||||
async sendUserVerifyCode(email: string, cfToken: string) {
|
||||
return this.request<{ expirationTtl?: number }>("/user_api/verify_code", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ email, cf_token: cfToken }),
|
||||
})
|
||||
}
|
||||
|
||||
async userRegister(email: string, password: string, code: string, cfToken: string) {
|
||||
await this.request("/user_api/register", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
email,
|
||||
password: await hashPassword(password),
|
||||
code,
|
||||
cf_token: cfToken,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
async listBoundAddresses() {
|
||||
const response = await this.request<{ results: BoundAddress[] }>("/user_api/bind_address")
|
||||
return response.results || []
|
||||
}
|
||||
|
||||
async bindCurrentAddress() {
|
||||
await this.request("/user_api/bind_address", { method: "POST" })
|
||||
}
|
||||
|
||||
async getBoundAddressJwt(addressId: string | number) {
|
||||
const response = await this.request<{ jwt: string }>(`/user_api/bind_address_jwt/${addressId}`)
|
||||
return response.jwt
|
||||
}
|
||||
|
||||
async unbindAddress(addressId: string | number) {
|
||||
await this.request("/user_api/unbind_address", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ address_id: addressId }),
|
||||
})
|
||||
}
|
||||
|
||||
async createAddress(name: string, domain: string, cfToken: string, enableRandomSubdomain: boolean) {
|
||||
return this.request<{ jwt: string; password?: string }>("/api/new_address", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
name,
|
||||
domain,
|
||||
cf_token: cfToken,
|
||||
enableRandomSubdomain,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
async credentialLogin(credential: string, cfToken: string) {
|
||||
await this.request("/open_api/credential_login", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ credential, cf_token: cfToken }),
|
||||
})
|
||||
return credential
|
||||
}
|
||||
|
||||
async passwordLogin(email: string, password: string, cfToken: string) {
|
||||
const response = await this.request<{ jwt: string }>("/api/address_login", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
email,
|
||||
password: await hashPassword(password),
|
||||
cf_token: cfToken,
|
||||
}),
|
||||
})
|
||||
return response.jwt
|
||||
}
|
||||
|
||||
async listMails(limit: number, offset: number) {
|
||||
return this.request<MailListResponse>(`/api/parsed_mails?limit=${limit}&offset=${offset}`)
|
||||
}
|
||||
|
||||
async deleteMail(id: string | number) {
|
||||
await this.request(`/api/mails/${id}`, { method: "DELETE" })
|
||||
}
|
||||
|
||||
async sendMail(payload: SendMailPayload) {
|
||||
await this.request("/api/send_mail", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
}
|
||||
|
||||
async requestSendAccess() {
|
||||
await this.request("/api/request_send_mail_access", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({}),
|
||||
})
|
||||
}
|
||||
|
||||
async clearInbox() {
|
||||
await this.request("/api/clear_inbox", { method: "DELETE" })
|
||||
}
|
||||
|
||||
async clearSentItems() {
|
||||
await this.request("/api/clear_sent_items", { method: "DELETE" })
|
||||
}
|
||||
|
||||
async deleteAddress() {
|
||||
await this.request("/api/delete_address", { method: "DELETE" })
|
||||
}
|
||||
}
|
||||
|
||||
function tryParseJson(value: string) {
|
||||
try {
|
||||
return JSON.parse(value)
|
||||
} catch {
|
||||
return value
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { useEffect, useId, useRef, useState } from "react"
|
||||
import { Button } from "./ui/button"
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
turnstile?: {
|
||||
render: (
|
||||
selector: string,
|
||||
options: {
|
||||
sitekey: string
|
||||
theme: "light" | "dark"
|
||||
callback: (token: string) => void
|
||||
},
|
||||
) => string
|
||||
remove: (id: string) => void
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type TurnstileWidgetProps = {
|
||||
siteKey: string
|
||||
theme: "light" | "dark"
|
||||
onToken: (token: string) => void
|
||||
}
|
||||
|
||||
let scriptPromise: Promise<void> | null = null
|
||||
|
||||
function loadTurnstileScript() {
|
||||
if (window.turnstile) return Promise.resolve()
|
||||
if (scriptPromise) return scriptPromise
|
||||
scriptPromise = new Promise((resolve, reject) => {
|
||||
const existing = document.querySelector<HTMLScriptElement>("script[data-turnstile]")
|
||||
if (existing) {
|
||||
existing.addEventListener("load", () => resolve(), { once: true })
|
||||
existing.addEventListener("error", () => reject(new Error("Turnstile script failed")), { once: true })
|
||||
return
|
||||
}
|
||||
const script = document.createElement("script")
|
||||
script.src = "https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit"
|
||||
script.async = true
|
||||
script.defer = true
|
||||
script.dataset.turnstile = "true"
|
||||
script.onload = () => resolve()
|
||||
script.onerror = () => reject(new Error("Turnstile script failed"))
|
||||
document.head.appendChild(script)
|
||||
})
|
||||
return scriptPromise
|
||||
}
|
||||
|
||||
export function TurnstileWidget({ siteKey, theme, onToken }: TurnstileWidgetProps) {
|
||||
const id = `turnstile-${useId().replace(/:/g, "")}`
|
||||
const widgetId = useRef("")
|
||||
const [failed, setFailed] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!siteKey) return undefined
|
||||
let mounted = true
|
||||
|
||||
const renderWidget = async () => {
|
||||
setFailed(false)
|
||||
onToken("")
|
||||
try {
|
||||
await loadTurnstileScript()
|
||||
if (!mounted || !window.turnstile) return
|
||||
if (widgetId.current) window.turnstile.remove(widgetId.current)
|
||||
widgetId.current = window.turnstile.render(`#${id}`, {
|
||||
sitekey: siteKey,
|
||||
theme,
|
||||
callback: onToken,
|
||||
})
|
||||
} catch {
|
||||
if (mounted) setFailed(true)
|
||||
}
|
||||
}
|
||||
|
||||
renderWidget()
|
||||
return () => {
|
||||
mounted = false
|
||||
if (widgetId.current && window.turnstile) {
|
||||
window.turnstile.remove(widgetId.current)
|
||||
widgetId.current = ""
|
||||
}
|
||||
}
|
||||
}, [id, onToken, siteKey, theme])
|
||||
|
||||
if (!siteKey) return null
|
||||
|
||||
return (
|
||||
<div className="turnstile-box">
|
||||
<div id={id} />
|
||||
{failed && (
|
||||
<Button variant="outline" size="sm" onClick={() => window.location.reload()}>
|
||||
Reload challenge
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const alertVariants = cva(
|
||||
"relative grid w-full grid-cols-[0_1fr] items-start gap-y-0.5 rounded-lg border px-4 py-3 text-sm has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-card text-card-foreground",
|
||||
destructive:
|
||||
"bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 [&>svg]:text-current",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Alert({
|
||||
className,
|
||||
variant,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert"
|
||||
role="alert"
|
||||
className={cn(alertVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-title"
|
||||
className={cn(
|
||||
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-description"
|
||||
className={cn(
|
||||
"col-start-2 grid justify-items-start gap-1 text-sm text-muted-foreground [&_p]:leading-relaxed",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Alert, AlertTitle, AlertDescription }
|
||||
@@ -0,0 +1,48 @@
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { Slot } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
||||
destructive:
|
||||
"bg-destructive text-white focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40 [a&]:hover:bg-destructive/90",
|
||||
outline:
|
||||
"border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||
ghost: "[a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 [a&]:hover:underline",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Badge({
|
||||
className,
|
||||
variant = "default",
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"span"> &
|
||||
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot.Root : "span"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="badge"
|
||||
data-variant={variant}
|
||||
className={cn(badgeVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
@@ -0,0 +1,64 @@
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { Slot } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40",
|
||||
outline:
|
||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost:
|
||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
|
||||
sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
icon: "size-9",
|
||||
"icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
|
||||
"icon-sm": "size-8",
|
||||
"icon-lg": "size-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot.Root : "button"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
data-variant={variant}
|
||||
data-size={size}
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Button, buttonVariants }
|
||||
@@ -0,0 +1,92 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card"
|
||||
className={cn(
|
||||
"flex flex-col gap-6 rounded-xl border bg-card py-6 text-card-foreground shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-title"
|
||||
className={cn("leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-description"
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-action"
|
||||
className={cn(
|
||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-content"
|
||||
className={cn("px-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardAction,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import * as React from "react"
|
||||
import { CheckIcon } from "lucide-react"
|
||||
import { Checkbox as CheckboxPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Checkbox({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
||||
return (
|
||||
<CheckboxPrimitive.Root
|
||||
data-slot="checkbox"
|
||||
className={cn(
|
||||
"peer size-4 shrink-0 rounded-[4px] border border-input shadow-xs transition-shadow outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:bg-input/30 dark:aria-invalid:ring-destructive/40 dark:data-[state=checked]:bg-primary",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
data-slot="checkbox-indicator"
|
||||
className="grid place-content-center text-current transition-none"
|
||||
>
|
||||
<CheckIcon className="size-3.5" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export { Checkbox }
|
||||
@@ -0,0 +1,158 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { XIcon } from "lucide-react"
|
||||
import { Dialog as DialogPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
|
||||
function Dialog({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
||||
}
|
||||
|
||||
function DialogTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
||||
}
|
||||
|
||||
function DialogPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
||||
}
|
||||
|
||||
function DialogClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
||||
}
|
||||
|
||||
function DialogOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
||||
return (
|
||||
<DialogPrimitive.Overlay
|
||||
data-slot="dialog-overlay"
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogContent({
|
||||
className,
|
||||
children,
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||
showCloseButton?: boolean
|
||||
}) {
|
||||
return (
|
||||
<DialogPortal data-slot="dialog-portal">
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
"fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border bg-background p-6 shadow-lg duration-200 outline-none data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 sm:max-w-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close
|
||||
data-slot="dialog-close"
|
||||
className="absolute top-4 right-4 rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
||||
>
|
||||
<XIcon />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-header"
|
||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogFooter({
|
||||
className,
|
||||
showCloseButton = false,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
showCloseButton?: boolean
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-footer"
|
||||
className={cn(
|
||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close asChild>
|
||||
<Button variant="outline">Close</Button>
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||
return (
|
||||
<DialogPrimitive.Title
|
||||
data-slot="dialog-title"
|
||||
className={cn("text-lg leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
||||
return (
|
||||
<DialogPrimitive.Description
|
||||
data-slot="dialog-description"
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
import * as React from "react"
|
||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function DropdownMenu({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
||||
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
|
||||
}
|
||||
|
||||
function DropdownMenuPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Trigger
|
||||
data-slot="dropdown-menu-trigger"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuContent({
|
||||
className,
|
||||
sideOffset = 4,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
data-slot="dropdown-menu-content"
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuItem({
|
||||
className,
|
||||
inset,
|
||||
variant = "default",
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean
|
||||
variant?: "default" | "destructive"
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Item
|
||||
data-slot="dropdown-menu-item"
|
||||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:text-destructive!",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuCheckboxItem({
|
||||
className,
|
||||
children,
|
||||
checked,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
data-slot="dropdown-menu-checkbox-item"
|
||||
className={cn(
|
||||
"relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuRadioGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.RadioGroup
|
||||
data-slot="dropdown-menu-radio-group"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuRadioItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
data-slot="dropdown-menu-radio-item"
|
||||
className={cn(
|
||||
"relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CircleIcon className="size-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuLabel({
|
||||
className,
|
||||
inset,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Label
|
||||
data-slot="dropdown-menu-label"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
data-slot="dropdown-menu-separator"
|
||||
className={cn("-mx-1 my-1 h-px bg-border", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuShortcut({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="dropdown-menu-shortcut"
|
||||
className={cn(
|
||||
"ml-auto text-xs tracking-widest text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuSub({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
||||
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
|
||||
}
|
||||
|
||||
function DropdownMenuSubTrigger({
|
||||
className,
|
||||
inset,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
data-slot="dropdown-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[inset]:pl-8 data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-auto size-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuSubContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
data-slot="dropdown-menu-sub-content"
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuSubContent,
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
"h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30",
|
||||
"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
|
||||
"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Input }
|
||||
@@ -0,0 +1,22 @@
|
||||
import * as React from "react"
|
||||
import { Label as LabelPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Label({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||
return (
|
||||
<LabelPrimitive.Root
|
||||
data-slot="label"
|
||||
className={cn(
|
||||
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Label }
|
||||
@@ -0,0 +1,56 @@
|
||||
import * as React from "react"
|
||||
import { ScrollArea as ScrollAreaPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function ScrollArea({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
|
||||
return (
|
||||
<ScrollAreaPrimitive.Root
|
||||
data-slot="scroll-area"
|
||||
className={cn("relative", className)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport
|
||||
data-slot="scroll-area-viewport"
|
||||
className="size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1"
|
||||
>
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
function ScrollBar({
|
||||
className,
|
||||
orientation = "vertical",
|
||||
...props
|
||||
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
|
||||
return (
|
||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||
data-slot="scroll-area-scrollbar"
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"flex touch-none p-px transition-colors select-none",
|
||||
orientation === "vertical" &&
|
||||
"h-full w-2.5 border-l border-l-transparent",
|
||||
orientation === "horizontal" &&
|
||||
"h-2.5 flex-col border-t border-t-transparent",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb
|
||||
data-slot="scroll-area-thumb"
|
||||
className="relative flex-1 rounded-full bg-border"
|
||||
/>
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
)
|
||||
}
|
||||
|
||||
export { ScrollArea, ScrollBar }
|
||||
@@ -0,0 +1,190 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
|
||||
import { Select as SelectPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Select({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
||||
return <SelectPrimitive.Root data-slot="select" {...props} />
|
||||
}
|
||||
|
||||
function SelectGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
||||
return <SelectPrimitive.Group data-slot="select-group" {...props} />
|
||||
}
|
||||
|
||||
function SelectValue({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
||||
return <SelectPrimitive.Value data-slot="select-value" {...props} />
|
||||
}
|
||||
|
||||
function SelectTrigger({
|
||||
className,
|
||||
size = "default",
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
||||
size?: "sm" | "default"
|
||||
}) {
|
||||
return (
|
||||
<SelectPrimitive.Trigger
|
||||
data-slot="select-trigger"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"flex w-fit items-center justify-between gap-2 rounded-md border border-input bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[placeholder]:text-muted-foreground data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDownIcon className="size-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectContent({
|
||||
className,
|
||||
children,
|
||||
position = "item-aligned",
|
||||
align = "center",
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
||||
return (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
data-slot="select-content"
|
||||
className={cn(
|
||||
"relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover text-popover-foreground shadow-md data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className
|
||||
)}
|
||||
position={position}
|
||||
align={align}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectLabel({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
||||
return (
|
||||
<SelectPrimitive.Label
|
||||
data-slot="select-label"
|
||||
className={cn("px-2 py-1.5 text-xs text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
||||
return (
|
||||
<SelectPrimitive.Item
|
||||
data-slot="select-item"
|
||||
className={cn(
|
||||
"relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span
|
||||
data-slot="select-item-indicator"
|
||||
className="absolute right-2 flex size-3.5 items-center justify-center"
|
||||
>
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
||||
return (
|
||||
<SelectPrimitive.Separator
|
||||
data-slot="select-separator"
|
||||
className={cn("pointer-events-none -mx-1 my-1 h-px bg-border", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectScrollUpButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
data-slot="select-scroll-up-button"
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUpIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectScrollDownButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
data-slot="select-scroll-down-button"
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDownIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectScrollDownButton,
|
||||
SelectScrollUpButton,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import * as React from "react"
|
||||
import { Separator as SeparatorPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Separator({
|
||||
className,
|
||||
orientation = "horizontal",
|
||||
decorative = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
||||
return (
|
||||
<SeparatorPrimitive.Root
|
||||
data-slot="separator"
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"shrink-0 bg-border data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Separator }
|
||||
@@ -0,0 +1,143 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { XIcon } from "lucide-react"
|
||||
import { Dialog as SheetPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
|
||||
return <SheetPrimitive.Root data-slot="sheet" {...props} />
|
||||
}
|
||||
|
||||
function SheetTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
|
||||
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
|
||||
}
|
||||
|
||||
function SheetClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
|
||||
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
|
||||
}
|
||||
|
||||
function SheetPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
|
||||
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
|
||||
}
|
||||
|
||||
function SheetOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
|
||||
return (
|
||||
<SheetPrimitive.Overlay
|
||||
data-slot="sheet-overlay"
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SheetContent({
|
||||
className,
|
||||
children,
|
||||
side = "right",
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
|
||||
side?: "top" | "right" | "bottom" | "left"
|
||||
showCloseButton?: boolean
|
||||
}) {
|
||||
return (
|
||||
<SheetPortal>
|
||||
<SheetOverlay />
|
||||
<SheetPrimitive.Content
|
||||
data-slot="sheet-content"
|
||||
className={cn(
|
||||
"fixed z-50 flex flex-col gap-4 bg-background shadow-lg transition ease-in-out data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:animate-in data-[state=open]:duration-500",
|
||||
side === "right" &&
|
||||
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
|
||||
side === "left" &&
|
||||
"inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
|
||||
side === "top" &&
|
||||
"inset-x-0 top-0 h-auto border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
|
||||
side === "bottom" &&
|
||||
"inset-x-0 bottom-0 h-auto border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<SheetPrimitive.Close className="absolute top-4 right-4 rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none data-[state=open]:bg-secondary">
|
||||
<XIcon className="size-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
)}
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
)
|
||||
}
|
||||
|
||||
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sheet-header"
|
||||
className={cn("flex flex-col gap-1.5 p-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sheet-footer"
|
||||
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SheetTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Title>) {
|
||||
return (
|
||||
<SheetPrimitive.Title
|
||||
data-slot="sheet-title"
|
||||
className={cn("font-semibold text-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SheetDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Description>) {
|
||||
return (
|
||||
<SheetPrimitive.Description
|
||||
data-slot="sheet-description"
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Sheet,
|
||||
SheetTrigger,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetFooter,
|
||||
SheetTitle,
|
||||
SheetDescription,
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import {
|
||||
CircleCheckIcon,
|
||||
InfoIcon,
|
||||
Loader2Icon,
|
||||
OctagonXIcon,
|
||||
TriangleAlertIcon,
|
||||
} from "lucide-react"
|
||||
import { useTheme } from "next-themes"
|
||||
import { Toaster as Sonner, type ToasterProps } from "sonner"
|
||||
|
||||
const Toaster = ({ ...props }: ToasterProps) => {
|
||||
const { theme = "system" } = useTheme()
|
||||
|
||||
return (
|
||||
<Sonner
|
||||
theme={theme as ToasterProps["theme"]}
|
||||
className="toaster group"
|
||||
icons={{
|
||||
success: <CircleCheckIcon className="size-4" />,
|
||||
info: <InfoIcon className="size-4" />,
|
||||
warning: <TriangleAlertIcon className="size-4" />,
|
||||
error: <OctagonXIcon className="size-4" />,
|
||||
loading: <Loader2Icon className="size-4 animate-spin" />,
|
||||
}}
|
||||
style={
|
||||
{
|
||||
"--normal-bg": "var(--popover)",
|
||||
"--normal-text": "var(--popover-foreground)",
|
||||
"--normal-border": "var(--border)",
|
||||
"--border-radius": "var(--radius)",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Toaster }
|
||||
@@ -0,0 +1,35 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Switch as SwitchPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Switch({
|
||||
className,
|
||||
size = "default",
|
||||
...props
|
||||
}: React.ComponentProps<typeof SwitchPrimitive.Root> & {
|
||||
size?: "sm" | "default"
|
||||
}) {
|
||||
return (
|
||||
<SwitchPrimitive.Root
|
||||
data-slot="switch"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"peer group/switch inline-flex shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-[1.15rem] data-[size=default]:w-8 data-[size=sm]:h-3.5 data-[size=sm]:w-6 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input dark:data-[state=unchecked]:bg-input/80",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SwitchPrimitive.Thumb
|
||||
data-slot="switch-thumb"
|
||||
className={cn(
|
||||
"pointer-events-none block rounded-full bg-background ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0 dark:data-[state=checked]:bg-primary-foreground dark:data-[state=unchecked]:bg-foreground"
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export { Switch }
|
||||
@@ -0,0 +1,91 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { Tabs as TabsPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Tabs({
|
||||
className,
|
||||
orientation = "horizontal",
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
|
||||
return (
|
||||
<TabsPrimitive.Root
|
||||
data-slot="tabs"
|
||||
data-orientation={orientation}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"group/tabs flex gap-2 data-[orientation=horizontal]:flex-col",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const tabsListVariants = cva(
|
||||
"group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-[orientation=horizontal]/tabs:h-9 group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col data-[variant=line]:rounded-none",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-muted",
|
||||
line: "gap-1 bg-transparent",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function TabsList({
|
||||
className,
|
||||
variant = "default",
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.List> &
|
||||
VariantProps<typeof tabsListVariants>) {
|
||||
return (
|
||||
<TabsPrimitive.List
|
||||
data-slot="tabs-list"
|
||||
data-variant={variant}
|
||||
className={cn(tabsListVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TabsTrigger({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
|
||||
return (
|
||||
<TabsPrimitive.Trigger
|
||||
data-slot="tabs-trigger"
|
||||
className={cn(
|
||||
"relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none dark:text-muted-foreground dark:hover:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent",
|
||||
"data-[state=active]:bg-background data-[state=active]:text-foreground dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 dark:data-[state=active]:text-foreground",
|
||||
"after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TabsContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
||||
return (
|
||||
<TabsPrimitive.Content
|
||||
data-slot="tabs-content"
|
||||
className={cn("flex-1 outline-none", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants }
|
||||
@@ -0,0 +1,18 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||
return (
|
||||
<textarea
|
||||
data-slot="textarea"
|
||||
className={cn(
|
||||
"flex field-sizing-content min-h-16 w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:aria-invalid:ring-destructive/40",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Textarea }
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { StrictMode } from "react"
|
||||
import { createRoot } from "react-dom/client"
|
||||
import App from "./App"
|
||||
import "./index.css"
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2023",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "Bundler",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { defineConfig } from "vite"
|
||||
import react from "@vitejs/plugin-react"
|
||||
import tailwindcss from "@tailwindcss/vite"
|
||||
import path from "node:path"
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react(), tailwindcss()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
"/api": "http://127.0.0.1:8787",
|
||||
"/open_api": "http://127.0.0.1:8787",
|
||||
"/user_api": "http://127.0.0.1:8787",
|
||||
"/telegram": "http://127.0.0.1:8787",
|
||||
},
|
||||
},
|
||||
})
|
||||
+17
-17
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cloudflare_temp_email",
|
||||
"version": "1.8.0",
|
||||
"version": "1.11.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -18,42 +18,42 @@
|
||||
"deploy:preview": "npm run build && wrangler pages deploy ./dist --branch preview",
|
||||
"deploy": "npm run build && wrangler pages deploy ./dist --branch production",
|
||||
"deploy:actions": "npm run build && wrangler pages deploy ./dist",
|
||||
"test": "vitest run",
|
||||
"test": "vitest run --passWithNoTests",
|
||||
"test:watch": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fingerprintjs/fingerprintjs": "^5.2.0",
|
||||
"@simplewebauthn/browser": "13.2.2",
|
||||
"@unhead/vue": "^2.1.13",
|
||||
"@vueuse/core": "^14.2.1",
|
||||
"@simplewebauthn/browser": "^13.3.0",
|
||||
"@unhead/vue": "^2.1.17",
|
||||
"@vueuse/core": "^14.4.0",
|
||||
"@wangeditor/editor": "^5.1.23",
|
||||
"@wangeditor/editor-for-vue": "^5.1.12",
|
||||
"axios": "^1.15.1",
|
||||
"dompurify": "^3.4.0",
|
||||
"axios": "^1.19.0",
|
||||
"dompurify": "^3.4.13",
|
||||
"jszip": "^3.10.1",
|
||||
"mail-parser-wasm": "^0.2.2",
|
||||
"naive-ui": "^2.44.1",
|
||||
"postal-mime": "^2.7.4",
|
||||
"postal-mime": "^2.7.5",
|
||||
"vooks": "^0.2.12",
|
||||
"vue": "^3.5.32",
|
||||
"vue": "^3.5.41",
|
||||
"vue-clipboard3": "^2.0.0",
|
||||
"vue-i18n": "^11.3.2",
|
||||
"vue-i18n": "^11.4.8",
|
||||
"vue-router": "^4.6.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vicons/fa": "^0.13.0",
|
||||
"@vicons/material": "^0.13.0",
|
||||
"@vitejs/plugin-vue": "^6.0.6",
|
||||
"@vitejs/plugin-vue": "^6.0.8",
|
||||
"jsdom": "^28.1.0",
|
||||
"unplugin-auto-import": "^20.3.0",
|
||||
"unplugin-vue-components": "^30.0.0",
|
||||
"vite": "^7.3.2",
|
||||
"vite-plugin-pwa": "^1.2.0",
|
||||
"vite": "^7.3.6",
|
||||
"vite-plugin-pwa": "^1.3.0",
|
||||
"vite-plugin-wasm": "^3.6.0",
|
||||
"vitest": "^3.2.4",
|
||||
"workbox-build": "^7.4.0",
|
||||
"workbox-window": "^7.4.0",
|
||||
"wrangler": "^4.83.0"
|
||||
"vitest": "^4.1.10",
|
||||
"workbox-build": "^7.4.1",
|
||||
"workbox-window": "^7.4.1",
|
||||
"wrangler": "^4.119.0"
|
||||
},
|
||||
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39"
|
||||
}
|
||||
|
||||
Generated
+1960
-2266
File diff suppressed because it is too large
Load Diff
+24
-5
@@ -1,6 +1,8 @@
|
||||
<script setup>
|
||||
import { darkTheme, NGlobalStyle, zhCN } from 'naive-ui'
|
||||
import { computed, onMounted } from 'vue'
|
||||
import {
|
||||
darkTheme,
|
||||
} from 'naive-ui'
|
||||
import { computed, onMounted, watchEffect } from 'vue'
|
||||
import { useScript } from '@unhead/vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useGlobalState } from './store'
|
||||
@@ -8,20 +10,27 @@ import { useIsMobile } from './utils/composables'
|
||||
import Header from './views/Header.vue';
|
||||
import Footer from './views/Footer.vue';
|
||||
import { api } from './api'
|
||||
import { getNaiveLocaleConfig } from './i18n/naive-locale'
|
||||
import { DEFAULT_LOCALE, isSupportedLocale } from './i18n/utils'
|
||||
|
||||
const {
|
||||
isDark, loading, useSideMargin, telegramApp, isTelegram
|
||||
} = useGlobalState()
|
||||
const adClient = import.meta.env.VITE_GOOGLE_AD_CLIENT;
|
||||
const adSlot = import.meta.env.VITE_GOOGLE_AD_SLOT;
|
||||
const { locale } = useI18n({});
|
||||
const { locale } = useI18n({ useScope: 'global' });
|
||||
const theme = computed(() => isDark.value ? darkTheme : null)
|
||||
const localeConfig = computed(() => locale.value == 'zh' ? zhCN : null)
|
||||
const localeConfig = computed(() => getNaiveLocaleConfig(isSupportedLocale(locale.value) ? locale.value : DEFAULT_LOCALE))
|
||||
const isMobile = useIsMobile()
|
||||
const showSideMargin = computed(() => !isMobile.value && useSideMargin.value);
|
||||
const showAd = computed(() => !isMobile.value && adClient && adSlot);
|
||||
const gridMaxCols = computed(() => showAd.value ? 8 : 12);
|
||||
|
||||
watchEffect(() => {
|
||||
if (typeof document === 'undefined') return
|
||||
document.documentElement.lang = isSupportedLocale(locale.value) ? locale.value : DEFAULT_LOCALE
|
||||
})
|
||||
|
||||
// Load Google Ad script at top level (not inside onMounted)
|
||||
if (showAd.value) {
|
||||
useScript({
|
||||
@@ -77,7 +86,7 @@ onMounted(async () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-config-provider :locale="localeConfig" :theme="theme">
|
||||
<n-config-provider :locale="localeConfig.locale" :date-locale="localeConfig.dateLocale" :theme="theme">
|
||||
<n-global-style />
|
||||
<n-spin description="loading..." :show="loading">
|
||||
<n-notification-provider container-style="margin-top: 60px;">
|
||||
@@ -120,6 +129,16 @@ onMounted(async () => {
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
@media (hover: none) and (pointer: coarse) and (max-width: 1024px) {
|
||||
:where(input, textarea, select, [contenteditable="true"]) {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
:where(.n-input, .n-input-number, .n-base-selection, .n-input-group-label) {
|
||||
--n-font-size: 16px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
|
||||
+29
-13
@@ -4,6 +4,8 @@ import axios from 'axios'
|
||||
|
||||
import i18n from '../i18n'
|
||||
import { getFingerprint } from '../utils/fingerprint'
|
||||
import { safeBearerHeader, safeHeaderValue } from '../utils/headers'
|
||||
import { sanitizeHtml } from '../utils/sanitize-html'
|
||||
|
||||
const API_BASE = import.meta.env.VITE_API_BASE || "";
|
||||
const {
|
||||
@@ -24,19 +26,29 @@ const apiFetch = async (path, options = {}) => {
|
||||
// Get browser fingerprint for request tracking
|
||||
const fingerprint = await getFingerprint();
|
||||
|
||||
// Skip auth headers whose value is empty / "undefined" / contains
|
||||
// control chars (otherwise axios throws "Invalid character in header
|
||||
// content" before the request is sent — see issue #1000).
|
||||
const headers = {
|
||||
'x-lang': i18n.global.locale.value,
|
||||
'x-fingerprint': fingerprint,
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
const userTokenHeader = safeHeaderValue(options.userJwt || userJwt.value);
|
||||
if (userTokenHeader) headers['x-user-token'] = userTokenHeader;
|
||||
const userAccessHeader = safeHeaderValue(userSettings.value.access_token);
|
||||
if (userAccessHeader) headers['x-user-access-token'] = userAccessHeader;
|
||||
const customAuthHeader = safeHeaderValue(auth.value);
|
||||
if (customAuthHeader) headers['x-custom-auth'] = customAuthHeader;
|
||||
const adminAuthHeader = safeHeaderValue(adminAuth.value);
|
||||
if (adminAuthHeader) headers['x-admin-auth'] = adminAuthHeader;
|
||||
const authorizationHeader = safeBearerHeader(jwt.value);
|
||||
if (authorizationHeader) headers['Authorization'] = authorizationHeader;
|
||||
|
||||
const response = await instance.request(path, {
|
||||
method: options.method || 'GET',
|
||||
data: options.body || null,
|
||||
headers: {
|
||||
'x-lang': i18n.global.locale.value,
|
||||
'x-user-token': options.userJwt || userJwt.value,
|
||||
'x-user-access-token': userSettings.value.access_token,
|
||||
'x-custom-auth': auth.value,
|
||||
'x-admin-auth': adminAuth.value,
|
||||
'x-fingerprint': fingerprint,
|
||||
'Authorization': `Bearer ${jwt.value}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
headers,
|
||||
});
|
||||
if (response.status === 401 && path.startsWith("/admin")) {
|
||||
showAdminAuth.value = true;
|
||||
@@ -62,8 +74,9 @@ const apiFetch = async (path, options = {}) => {
|
||||
const getOpenSettings = async (message, notification) => {
|
||||
try {
|
||||
const res = await api.fetch("/open_api/settings");
|
||||
const domains = Array.isArray(res["domains"]) ? res["domains"] : [];
|
||||
const domainLabels = res["domainLabels"] || [];
|
||||
if (res["domains"]?.length < 1) {
|
||||
if (domains.length < 1) {
|
||||
message.error("No domains found, please check your worker settings");
|
||||
}
|
||||
Object.assign(openSettings.value, {
|
||||
@@ -75,7 +88,7 @@ const getOpenSettings = async (message, notification) => {
|
||||
needAuth: res["needAuth"] || false,
|
||||
defaultDomains: res["defaultDomains"] || [],
|
||||
randomSubdomainDomains: res["randomSubdomainDomains"] || [],
|
||||
domains: res["domains"].map((domain, index) => {
|
||||
domains: domains.map((domain, index) => {
|
||||
return {
|
||||
label: domainLabels.length > index ? domainLabels[index] : domain,
|
||||
value: domain
|
||||
@@ -92,7 +105,10 @@ const getOpenSettings = async (message, notification) => {
|
||||
cfTurnstileSiteKey: res["cfTurnstileSiteKey"] || "",
|
||||
enableWebhook: res["enableWebhook"] || false,
|
||||
isS3Enabled: res["isS3Enabled"] || false,
|
||||
showGithubForUser: res["showGithubForUser"] ?? openSettings.value.showGithubForUser,
|
||||
enableAddressPassword: res["enableAddressPassword"] || false,
|
||||
enableAgentEmailInfo: res["enableAgentEmailInfo"] || false,
|
||||
smtpImapProxyConfig: res["smtpImapProxyConfig"] || openSettings.value.smtpImapProxyConfig,
|
||||
statusUrl: res["statusUrl"] || "",
|
||||
enableGlobalTurnstileCheck: res["enableGlobalTurnstileCheck"] || false,
|
||||
});
|
||||
@@ -108,7 +124,7 @@ const getOpenSettings = async (message, notification) => {
|
||||
notification.info({
|
||||
content: () => {
|
||||
return h("div", {
|
||||
innerHTML: announcement.value
|
||||
innerHTML: sanitizeHtml(announcement.value)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,322 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useScopedI18n } from '@/i18n/app'
|
||||
|
||||
import { useGlobalState } from '../store'
|
||||
|
||||
const props = defineProps({
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
address: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
jwt: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
addressPassword: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:show'])
|
||||
|
||||
const { openSettings, auth } = useGlobalState()
|
||||
const { locale, t } = useScopedI18n('components.AddressCredentialModal')
|
||||
const message = useMessage()
|
||||
|
||||
const modalShow = computed({
|
||||
get: () => props.show,
|
||||
set: (value) => emit('update:show', value),
|
||||
})
|
||||
|
||||
const configuredApiBaseUrl = import.meta.env.VITE_API_BASE || ''
|
||||
const frontendBaseUrl = computed(() => window.location.origin)
|
||||
const apiBaseUrl = computed(() => (configuredApiBaseUrl || frontendBaseUrl.value).replace(/\/$/, ''))
|
||||
const docLocale = computed(() => locale.value === 'zh' ? 'zh' : 'en')
|
||||
const agentDocUrl = computed(() => `https://temp-mail-docs.awsl.uk/${docLocale.value}/guide/feature/agent-email.html`)
|
||||
const smtpImapDocUrl = computed(() => `https://temp-mail-docs.awsl.uk/${docLocale.value}/guide/feature/config-smtp-proxy.html`)
|
||||
const agentSkillUrl = 'https://github.com/dreamhunter2333/cloudflare_temp_email/blob/main/skills/cf-temp-mail-agent-mail/SKILL.md'
|
||||
const autoLoginUrl = computed(() => `${frontendBaseUrl.value}/?jwt=${encodeURIComponent(props.jwt)}`)
|
||||
const showAgent = computed(() => !!openSettings.value.enableAgentEmailInfo)
|
||||
const smtpImapConfig = computed(() => openSettings.value.smtpImapProxyConfig || {})
|
||||
const smtpConfig = computed(() => smtpImapConfig.value.smtp || {})
|
||||
const imapConfig = computed(() => smtpImapConfig.value.imap || {})
|
||||
const showSmtpImap = computed(() => !!smtpConfig.value.host || !!imapConfig.value.host)
|
||||
const securityLabel = computed(() =>
|
||||
smtpConfig.value.starttls || imapConfig.value.starttls ? t('starttls') : t('plainOrProxyTls')
|
||||
)
|
||||
const agentConfigJson = computed(() => JSON.stringify({
|
||||
base: apiBaseUrl.value,
|
||||
jwt: props.jwt,
|
||||
site_password: auth.value || '',
|
||||
}, null, 2))
|
||||
const agentText = computed(() => [
|
||||
`${t('currentAddress')}: ${props.address || '-'}`,
|
||||
`${t('apiBase')}: ${apiBaseUrl.value}`,
|
||||
`${t('agentSkill')}: ${agentSkillUrl}`,
|
||||
`${t('agentConfig')}:`,
|
||||
agentConfigJson.value,
|
||||
].join('\n'))
|
||||
const smtpImapText = computed(() => [
|
||||
`${t('smtpHost')}: ${smtpConfig.value.host || '-'}`,
|
||||
`${t('smtpPort')}: ${smtpConfig.value.port || 8025}`,
|
||||
`${t('imapHost')}: ${imapConfig.value.host || '-'}`,
|
||||
`${t('imapPort')}: ${imapConfig.value.port || 11143}`,
|
||||
`${t('security')}: ${securityLabel.value}`,
|
||||
`${t('username')}: ${props.address || '-'}`,
|
||||
`${t('password')}: ${props.jwt}`,
|
||||
].join('\n'))
|
||||
|
||||
const copyText = async (text) => {
|
||||
if (!text) return
|
||||
try {
|
||||
if (navigator.clipboard?.writeText) {
|
||||
await navigator.clipboard.writeText(text)
|
||||
message.success(t('copySuccess'))
|
||||
return
|
||||
}
|
||||
|
||||
const textarea = document.createElement('textarea')
|
||||
try {
|
||||
textarea.value = text
|
||||
textarea.setAttribute('readonly', '')
|
||||
textarea.style.position = 'fixed'
|
||||
textarea.style.opacity = '0'
|
||||
document.body.appendChild(textarea)
|
||||
textarea.select()
|
||||
if (document.execCommand('copy')) {
|
||||
message.success(t('copySuccess'))
|
||||
return
|
||||
}
|
||||
message.error(t('copyFailed'))
|
||||
} finally {
|
||||
textarea.parentNode?.removeChild(textarea)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
message.error(t('copyFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-modal v-model:show="modalShow" preset="card" :title="t('title')"
|
||||
style="width: min(760px, calc(100vw - 32px));">
|
||||
<n-alert type="info" :show-icon="false" :bordered="false">
|
||||
{{ t('tip') }}
|
||||
</n-alert>
|
||||
<section class="credential-panel">
|
||||
<h3 class="credential-title">{{ t('addressCredential') }}</h3>
|
||||
<div class="credential-section">
|
||||
<div class="credential-field" v-if="address">
|
||||
<span class="credential-label">{{ t('currentAddress') }}</span>
|
||||
<div class="credential-copy-row">
|
||||
<code class="credential-code">{{ address }}</code>
|
||||
<n-button size="tiny" tertiary type="primary" @click="copyText(address)">
|
||||
{{ t('copySection') }}
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="credential-field">
|
||||
<span class="credential-label">{{ t('addressCredentialLabel') }}</span>
|
||||
<div class="credential-copy-row">
|
||||
<code class="credential-code">{{ jwt }}</code>
|
||||
<n-button size="tiny" tertiary type="primary" @click="copyText(jwt)">
|
||||
{{ t('copySection') }}
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="credential-field" v-if="addressPassword">
|
||||
<span class="credential-label">{{ t('addressPassword') }}</span>
|
||||
<code class="credential-code">{{ addressPassword }}</code>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<n-collapse accordion class="credential-collapse">
|
||||
<n-collapse-item v-if="showAgent" name="agent" :title="t('agentAccess')">
|
||||
<template #header-extra>
|
||||
<n-button size="tiny" tertiary type="primary" @click.stop="copyText(agentText)">
|
||||
{{ t('copySection') }}
|
||||
</n-button>
|
||||
</template>
|
||||
<div class="credential-section">
|
||||
<p class="credential-tip">{{ t('agentAccessTip') }}</p>
|
||||
<div class="credential-field">
|
||||
<span class="credential-label">{{ t('apiBase') }}</span>
|
||||
<code class="credential-code">{{ apiBaseUrl }}</code>
|
||||
</div>
|
||||
<div class="credential-field">
|
||||
<span class="credential-label">{{ t('agentSkill') }}</span>
|
||||
<code class="credential-code">
|
||||
<a :href="agentSkillUrl" target="_blank" rel="noopener noreferrer">{{ agentSkillUrl }}</a>
|
||||
</code>
|
||||
</div>
|
||||
<div class="credential-field">
|
||||
<span class="credential-label">{{ t('agentConfig') }}</span>
|
||||
<pre class="credential-code credential-code-block">{{ agentConfigJson }}</pre>
|
||||
</div>
|
||||
<div class="credential-actions">
|
||||
<n-button tag="a" :href="agentDocUrl" target="_blank" rel="noopener noreferrer" text type="primary">
|
||||
{{ t('docs') }}
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</n-collapse-item>
|
||||
|
||||
<n-collapse-item v-if="showSmtpImap" name="smtp-imap" :title="t('smtpImapAccess')">
|
||||
<template #header-extra>
|
||||
<n-button size="tiny" tertiary type="primary" @click.stop="copyText(smtpImapText)">
|
||||
{{ t('copySection') }}
|
||||
</n-button>
|
||||
</template>
|
||||
<div class="credential-section">
|
||||
<p class="credential-tip">{{ t('smtpImapTip') }}</p>
|
||||
<div class="credential-grid">
|
||||
<div class="credential-field">
|
||||
<span class="credential-label">{{ t('smtpHost') }}</span>
|
||||
<code class="credential-code">{{ smtpConfig.host || '-' }}</code>
|
||||
</div>
|
||||
<div class="credential-field">
|
||||
<span class="credential-label">{{ t('smtpPort') }}</span>
|
||||
<code class="credential-code">{{ smtpConfig.port || 8025 }}</code>
|
||||
</div>
|
||||
<div class="credential-field">
|
||||
<span class="credential-label">{{ t('imapHost') }}</span>
|
||||
<code class="credential-code">{{ imapConfig.host || '-' }}</code>
|
||||
</div>
|
||||
<div class="credential-field">
|
||||
<span class="credential-label">{{ t('imapPort') }}</span>
|
||||
<code class="credential-code">{{ imapConfig.port || 11143 }}</code>
|
||||
</div>
|
||||
</div>
|
||||
<div class="credential-field">
|
||||
<span class="credential-label">{{ t('security') }}</span>
|
||||
<code class="credential-code">{{ securityLabel }}</code>
|
||||
</div>
|
||||
<div class="credential-field">
|
||||
<span class="credential-label">{{ t('username') }}</span>
|
||||
<code class="credential-code">{{ address }}</code>
|
||||
</div>
|
||||
<div class="credential-field">
|
||||
<span class="credential-label">{{ t('password') }}</span>
|
||||
<code class="credential-code">{{ jwt }}</code>
|
||||
</div>
|
||||
<div class="credential-actions">
|
||||
<n-button tag="a" :href="smtpImapDocUrl" target="_blank" rel="noopener noreferrer" text type="primary">
|
||||
{{ t('docs') }}
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</n-collapse-item>
|
||||
|
||||
<n-collapse-item name="share-link" :title="t('autoLoginLink')">
|
||||
<template #header-extra>
|
||||
<n-button size="tiny" tertiary type="primary" @click.stop="copyText(autoLoginUrl)">
|
||||
{{ t('copySection') }}
|
||||
</n-button>
|
||||
</template>
|
||||
<div class="credential-section">
|
||||
<div class="credential-field">
|
||||
<code class="credential-code">{{ autoLoginUrl }}</code>
|
||||
</div>
|
||||
</div>
|
||||
</n-collapse-item>
|
||||
</n-collapse>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.credential-collapse {
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.credential-panel {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.credential-title {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.credential-section {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.credential-tip {
|
||||
margin: 0;
|
||||
color: var(--n-text-color-2);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.credential-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.credential-field {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.credential-label {
|
||||
color: var(--n-text-color-2);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.credential-code {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
border-radius: 6px;
|
||||
padding: 6px 8px;
|
||||
background: var(--n-color-embedded);
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.credential-copy-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: start;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.credential-code-block {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.credential-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.credential-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.credential-copy-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup>
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
import { useLocalStorage } from '@vueuse/core'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useScopedI18n } from '@/i18n/app'
|
||||
import { useMessage } from 'naive-ui'
|
||||
import useClipboard from 'vue-clipboard3'
|
||||
import { Copy } from '@vicons/fa'
|
||||
@@ -27,24 +27,7 @@ const {
|
||||
jwt, settings, userJwt, isTelegram, openSettings, telegramApp
|
||||
} = useGlobalState()
|
||||
|
||||
const { t } = useI18n({
|
||||
messages: {
|
||||
en: {
|
||||
userAddresses: 'User Addresses',
|
||||
localAddresses: 'Local Addresses',
|
||||
address: 'Address',
|
||||
copy: 'Copy',
|
||||
copied: 'Copied',
|
||||
},
|
||||
zh: {
|
||||
userAddresses: '用户地址',
|
||||
localAddresses: '本地地址',
|
||||
address: '地址',
|
||||
copy: '复制',
|
||||
copied: '已复制',
|
||||
}
|
||||
}
|
||||
});
|
||||
const { t } = useScopedI18n('components.AddressSelect')
|
||||
|
||||
const addressOptions = ref([])
|
||||
const addressValue = ref(null)
|
||||
@@ -114,7 +97,7 @@ const buildLocalOptions = (excludeAddresses = new Set()) => {
|
||||
const buildUserOptions = async () => {
|
||||
const children = [];
|
||||
try {
|
||||
const { results } = await api.fetch(`/user_api/bind_address`);
|
||||
const { results } = await api.fetch(`/user_api/bind_address?limit=100&offset=0`);
|
||||
for (const row of results || []) {
|
||||
const address = row.address || row.name;
|
||||
if (!address) continue;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useScopedI18n } from '@/i18n/app';
|
||||
import { ContentCopyOutlined, LinkRound, CodeRound } from '@vicons/material';
|
||||
import { useMessage } from 'naive-ui';
|
||||
import { useGlobalState } from '../store';
|
||||
@@ -32,30 +32,7 @@ const tagThemeOverrides = computed(() => {
|
||||
return {}
|
||||
});
|
||||
|
||||
const { t } = useI18n({
|
||||
messages: {
|
||||
en: {
|
||||
authCode: 'Verification Code',
|
||||
authLink: 'Authentication Link',
|
||||
serviceLink: 'Service Link',
|
||||
subscriptionLink: 'Subscription Link',
|
||||
otherLink: 'Other Link',
|
||||
copySuccess: 'Copied successfully',
|
||||
copyFailed: 'Copy failed',
|
||||
open: 'Open',
|
||||
},
|
||||
zh: {
|
||||
authCode: '验证码',
|
||||
authLink: '认证链接',
|
||||
serviceLink: '服务链接',
|
||||
subscriptionLink: '订阅链接',
|
||||
otherLink: '其他链接',
|
||||
copySuccess: '复制成功',
|
||||
copyFailed: '复制失败',
|
||||
open: '打开',
|
||||
}
|
||||
}
|
||||
});
|
||||
const { t } = useScopedI18n('components.AiExtractInfo')
|
||||
|
||||
const props = defineProps({
|
||||
metadata: {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup>
|
||||
import { watch, onMounted, ref, onBeforeUnmount, computed } from "vue";
|
||||
import { useMessage } from 'naive-ui'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useScopedI18n } from '@/i18n/app'
|
||||
import { useGlobalState } from '../store'
|
||||
import { CloudDownloadRound, ArrowBackIosNewFilled, ArrowForwardIosFilled, InboxRound } from '@vicons/material'
|
||||
import { useIsMobile } from '../utils/composables'
|
||||
@@ -60,7 +60,7 @@ const props = defineProps({
|
||||
const localFilterKeyword = ref('')
|
||||
|
||||
const {
|
||||
isDark, mailboxSplitSize, indexTab, loading, useUTCDate,
|
||||
isDark, mailboxSplitSize, mailListView, mailListPreviewLineClamp, indexTab, loading, useUTCDate,
|
||||
autoRefresh, configAutoRefreshInterval, sendMailModel
|
||||
} = useGlobalState()
|
||||
const autoRefreshInterval = ref(configAutoRefreshInterval.value)
|
||||
@@ -71,6 +71,12 @@ const count = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(20)
|
||||
|
||||
const mailListPreviewLineClampValue = computed(() => {
|
||||
const value = Number(mailListPreviewLineClamp.value)
|
||||
if (!Number.isFinite(value)) return 0
|
||||
return Math.min(5, Math.max(0, Math.round(value)))
|
||||
})
|
||||
|
||||
// Computed property for filtered data (only filter current page)
|
||||
const data = computed(() => {
|
||||
if (!localFilterKeyword.value || localFilterKeyword.value.trim() === '') {
|
||||
@@ -138,60 +144,7 @@ const showMultiActionDelete = ref(false)
|
||||
const multiActionDownloadZip = ref({})
|
||||
const multiActionDeleteProgress = ref({ percentage: 0, tip: '0/0' })
|
||||
|
||||
const { t } = useI18n({
|
||||
messages: {
|
||||
en: {
|
||||
success: 'Success',
|
||||
autoRefresh: 'Auto Refresh',
|
||||
refreshAfter: 'Refresh After {msg} Seconds',
|
||||
refresh: 'Refresh',
|
||||
attachments: 'Show Attachments',
|
||||
downloadMail: 'Download Mail',
|
||||
pleaseSelectMail: "Please select mail",
|
||||
emptyInbox: "Your inbox is empty",
|
||||
delete: 'Delete',
|
||||
deleteMailTip: 'Are you sure you want to delete mail?',
|
||||
reply: 'Reply',
|
||||
forwardMail: 'Forward',
|
||||
showTextMail: 'Show Text Mail',
|
||||
showHtmlMail: 'Show Html Mail',
|
||||
saveToS3: 'Save to S3',
|
||||
multiAction: 'Multi Action',
|
||||
cancelMultiAction: 'Cancel Multi Action',
|
||||
selectAll: 'Select All of This Page',
|
||||
unselectAll: 'Unselect All',
|
||||
prevMail: 'Previous',
|
||||
nextMail: 'Next',
|
||||
keywordQueryTip: 'Filter current page',
|
||||
query: 'Query',
|
||||
},
|
||||
zh: {
|
||||
success: '成功',
|
||||
autoRefresh: '自动刷新',
|
||||
refreshAfter: '{msg}秒后刷新',
|
||||
refresh: '刷新',
|
||||
downloadMail: '下载邮件',
|
||||
attachments: '查看附件',
|
||||
pleaseSelectMail: "请选择邮件",
|
||||
emptyInbox: "收件箱为空",
|
||||
delete: '删除',
|
||||
deleteMailTip: '确定要删除邮件吗?',
|
||||
reply: '回复',
|
||||
forwardMail: '转发',
|
||||
showTextMail: '显示纯文本邮件',
|
||||
showHtmlMail: '显示HTML邮件',
|
||||
saveToS3: '保存到S3',
|
||||
multiAction: '多选',
|
||||
cancelMultiAction: '取消多选',
|
||||
selectAll: '全选本页',
|
||||
unselectAll: '取消全选',
|
||||
prevMail: '上一封',
|
||||
nextMail: '下一封',
|
||||
keywordQueryTip: '过滤当前页',
|
||||
query: '查询',
|
||||
}
|
||||
}
|
||||
});
|
||||
const { t } = useScopedI18n('components.MailBox')
|
||||
|
||||
const setupAutoRefresh = async (autoRefresh) => {
|
||||
// auto refresh every configAutoRefreshInterval seconds
|
||||
@@ -236,7 +189,7 @@ const refresh = async () => {
|
||||
count.value = totalCount;
|
||||
}
|
||||
curMail.value = null;
|
||||
if (!isMobile.value && data.value.length > 0) {
|
||||
if (!isMobile.value && !mailListView.value && data.value.length > 0) {
|
||||
curMail.value = data.value[0];
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -255,6 +208,11 @@ const backFirstPageAndRefresh = async () => {
|
||||
const clickRow = async (row) => {
|
||||
if (multiActionMode.value) {
|
||||
row.checked = !row.checked;
|
||||
curMail.value = row;
|
||||
return;
|
||||
}
|
||||
if (mailListView.value && curMail.value?.id === row.id) {
|
||||
curMail.value = null;
|
||||
return;
|
||||
}
|
||||
curMail.value = row;
|
||||
@@ -428,8 +386,13 @@ onBeforeUnmount(() => {
|
||||
clearable />
|
||||
</n-space>
|
||||
</div>
|
||||
<n-split class="left" direction="horizontal" :max="0.75" :min="0.25" :default-size="mailboxSplitSize"
|
||||
:on-update:size="onSpiltSizeChange">
|
||||
<n-split class="left" direction="horizontal" :max="0.75" :min="0" :resize-trigger-size="8"
|
||||
:default-size="mailboxSplitSize" :on-update:size="onSpiltSizeChange" v-if="!mailListView || curMail">
|
||||
<template #resize-trigger>
|
||||
<div class="split-handle">
|
||||
<div class="split-handle__grip" />
|
||||
</div>
|
||||
</template>
|
||||
<template #1>
|
||||
<div style="overflow: auto; min-height: 60vh; max-height: 100vh;">
|
||||
<n-list hoverable clickable>
|
||||
@@ -465,15 +428,25 @@ onBeforeUnmount(() => {
|
||||
</template>
|
||||
<template #2>
|
||||
<div v-if="curMail" style="margin: 8px;">
|
||||
<n-flex justify="space-between">
|
||||
<n-button @click="prevMail" :disabled="!canGoPrevMail" text size="small">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<ArrowBackIosNewFilled />
|
||||
</n-icon>
|
||||
</template>
|
||||
{{ t('prevMail') }}
|
||||
</n-button>
|
||||
<n-flex justify="space-between" align="center">
|
||||
<n-space :wrap="false" align="center">
|
||||
<n-button v-if="mailListView" @click="curMail = null" text size="small">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<ArrowBackIosNewFilled />
|
||||
</n-icon>
|
||||
</template>
|
||||
{{ t('backToList') }}
|
||||
</n-button>
|
||||
<n-button @click="prevMail" :disabled="!canGoPrevMail" text size="small">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<ArrowBackIosNewFilled />
|
||||
</n-icon>
|
||||
</template>
|
||||
{{ t('prevMail') }}
|
||||
</n-button>
|
||||
</n-space>
|
||||
<n-button @click="nextMail" :disabled="!canGoNextMail" text size="small" icon-placement="right">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
@@ -499,6 +472,48 @@ onBeforeUnmount(() => {
|
||||
</n-card>
|
||||
</template>
|
||||
</n-split>
|
||||
<div v-else class="mail-list-scroll">
|
||||
<n-list hoverable clickable>
|
||||
<n-list-item v-for="row in data" v-bind:key="row.id" @click="() => clickRow(row)"
|
||||
:class="mailItemClass(row)">
|
||||
<template #prefix v-if="multiActionMode">
|
||||
<n-checkbox v-model:checked="row.checked" />
|
||||
</template>
|
||||
<n-thing class="mail-list-thing">
|
||||
<template #header>
|
||||
<n-ellipsis class="mail-list-title">
|
||||
{{ row.subject }}
|
||||
</n-ellipsis>
|
||||
</template>
|
||||
<template #description>
|
||||
<div class="mail-list-meta">
|
||||
<n-tag type="info">
|
||||
ID: {{ row.id }}
|
||||
</n-tag>
|
||||
<n-tag type="info">
|
||||
{{ utcToLocalDate(row.created_at, useUTCDate) }}
|
||||
</n-tag>
|
||||
<n-tag type="info">
|
||||
<n-ellipsis class="mail-list-meta-text">
|
||||
{{ showEMailTo ? "FROM: " + row.source : row.source }}
|
||||
</n-ellipsis>
|
||||
</n-tag>
|
||||
<n-tag v-if="showEMailTo" type="info">
|
||||
<n-ellipsis class="mail-list-meta-text">
|
||||
TO: {{ row.address }}
|
||||
</n-ellipsis>
|
||||
</n-tag>
|
||||
<AiExtractInfo :metadata="row.metadata" compact />
|
||||
</div>
|
||||
</template>
|
||||
<n-ellipsis v-if="row.text && mailListPreviewLineClampValue > 0"
|
||||
:line-clamp="mailListPreviewLineClampValue" class="mail-list-preview" :tooltip="false">
|
||||
{{ row.text }}
|
||||
</n-ellipsis>
|
||||
</n-thing>
|
||||
</n-list-item>
|
||||
</n-list>
|
||||
</div>
|
||||
</div>
|
||||
<div class="left" v-else>
|
||||
<n-space justify="space-around" align="center" :wrap="false" style="display: flex; align-items: center;">
|
||||
@@ -608,8 +623,80 @@ onBeforeUnmount(() => {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.mail-list-scroll {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
min-height: 60vh;
|
||||
max-height: 100vh;
|
||||
}
|
||||
|
||||
.mail-list-thing,
|
||||
.mail-list-title,
|
||||
.mail-list-preview {
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.mail-list-thing,
|
||||
.mail-list-preview {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mail-list-thing :deep(.n-thing-main),
|
||||
.mail-list-thing :deep(.n-thing-header),
|
||||
.mail-list-thing :deep(.n-thing-header__title),
|
||||
.mail-list-thing :deep(.n-thing-main__description),
|
||||
.mail-list-thing :deep(.n-thing-main__content) {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.mail-list-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.mail-list-meta :deep(.n-tag) {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.mail-list-meta-text {
|
||||
max-width: min(240px, 100%);
|
||||
}
|
||||
|
||||
.mail-list-preview {
|
||||
display: -webkit-box;
|
||||
overflow-wrap: anywhere;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.mail-list-scroll :deep(.n-list-item__main) {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
pre {
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.split-handle {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.split-handle__grip {
|
||||
width: 4px;
|
||||
height: 32px;
|
||||
border-radius: 2px;
|
||||
background-color: var(--n-resize-trigger-color);
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.split-handle:hover .split-handle__grip {
|
||||
background-color: var(--n-resize-trigger-color-hover);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,45 +1,17 @@
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { CloudDownloadRound, ReplyFilled, ForwardFilled, FullscreenRound } from '@vicons/material'
|
||||
import { ref, computed, watch } from "vue";
|
||||
import { useScopedI18n } from '@/i18n/app'
|
||||
import { CloudDownloadRound, ReplyFilled, ForwardFilled, FullscreenRound, ImageRound } from '@vicons/material'
|
||||
import ShadowHtmlComponent from "./ShadowHtmlComponent.vue";
|
||||
import AiExtractInfo from "./AiExtractInfo.vue";
|
||||
import { getDownloadEmlUrl } from '../utils/email-parser';
|
||||
import { blockRemoteContent } from '../utils/remote-content-policy';
|
||||
import { utcToLocalDate } from '../utils';
|
||||
import { useGlobalState } from '../store';
|
||||
|
||||
const { preferShowTextMail, useIframeShowMail, useUTCDate, isDark } = useGlobalState();
|
||||
const { preferShowTextMail, useIframeShowMail, useUTCDate, isDark, autoLoadRemoteImages } = useGlobalState();
|
||||
|
||||
const { t } = useI18n({
|
||||
messages: {
|
||||
en: {
|
||||
delete: 'Delete',
|
||||
deleteMailTip: 'Are you sure you want to delete mail?',
|
||||
attachments: 'View Attachments',
|
||||
downloadMail: 'Download Mail',
|
||||
reply: 'Reply',
|
||||
forward: 'Forward',
|
||||
showTextMail: 'Show Text Mail',
|
||||
showHtmlMail: 'Show HTML Mail',
|
||||
saveToS3: 'Save to S3',
|
||||
size: 'Size',
|
||||
fullscreen: 'Fullscreen',
|
||||
},
|
||||
zh: {
|
||||
delete: '删除',
|
||||
deleteMailTip: '确定要删除邮件吗?',
|
||||
attachments: '查看附件',
|
||||
downloadMail: '下载邮件',
|
||||
reply: '回复',
|
||||
forward: '转发',
|
||||
showTextMail: '显示纯文本邮件',
|
||||
showHtmlMail: '显示HTML邮件',
|
||||
saveToS3: '保存到S3',
|
||||
size: '大小',
|
||||
fullscreen: '全屏',
|
||||
}
|
||||
}
|
||||
});
|
||||
const { t } = useScopedI18n('components.MailContentRenderer')
|
||||
|
||||
const props = defineProps({
|
||||
mail: {
|
||||
@@ -87,6 +59,26 @@ const curAttachments = ref([]);
|
||||
const attachmentLoding = ref(false);
|
||||
const showFullscreen = ref(false);
|
||||
|
||||
// Per-mail consent, deliberately independent of the global setting: it only
|
||||
// ever turns true when the user clicks "load images" for this specific mail,
|
||||
// and resets when a different mail is shown.
|
||||
const showRemoteImages = ref(false);
|
||||
watch(() => props.mail.id, () => {
|
||||
showRemoteImages.value = false;
|
||||
});
|
||||
|
||||
const processedMail = computed(() => {
|
||||
if (autoLoadRemoteImages.value || showRemoteImages.value) {
|
||||
return { message: props.mail.message, blocked: 0 };
|
||||
}
|
||||
const { html, blocked } = blockRemoteContent(props.mail.message);
|
||||
return { message: html, blocked };
|
||||
});
|
||||
|
||||
const handleLoadRemoteImages = () => {
|
||||
showRemoteImages.value = true;
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
props.onDelete();
|
||||
};
|
||||
@@ -178,17 +170,32 @@ const handleSaveToS3 = async (filename, blob) => {
|
||||
</template>
|
||||
{{ t('fullscreen') }}
|
||||
</n-button>
|
||||
|
||||
</n-space>
|
||||
|
||||
<!-- 外部资源阻断提示 -->
|
||||
<n-alert v-if="processedMail.blocked" type="warning" :show-icon="false" :bordered="false"
|
||||
class="remote-images-banner">
|
||||
<n-space align="center" justify="space-between">
|
||||
<span>{{ t('remoteImagesBlocked', { count: processedMail.blocked }) }}</span>
|
||||
<n-button size="tiny" tertiary type="warning" @click="handleLoadRemoteImages">
|
||||
<template #icon>
|
||||
<n-icon :component="ImageRound" />
|
||||
</template>
|
||||
{{ t('loadRemoteImages') }}
|
||||
</n-button>
|
||||
</n-space>
|
||||
</n-alert>
|
||||
|
||||
<!-- AI 提取信息 -->
|
||||
<AiExtractInfo :metadata="mail.metadata" />
|
||||
|
||||
<!-- 邮件内容 -->
|
||||
<div class="mail-content" :class="{ 'dark-mode': isDark }">
|
||||
<pre v-if="showTextMail" class="mail-text">{{ mail.text }}</pre>
|
||||
<iframe v-else-if="useIframeShowMail" :srcdoc="mail.message" class="mail-iframe">
|
||||
<iframe v-else-if="useIframeShowMail" :srcdoc="processedMail.message" class="mail-iframe">
|
||||
</iframe>
|
||||
<ShadowHtmlComponent v-else :key="mail.id" :htmlContent="mail.message" :isDark="isDark" class="mail-html" />
|
||||
<ShadowHtmlComponent v-else :key="mail.id" :htmlContent="processedMail.message" :isDark="isDark" class="mail-html" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -197,9 +204,9 @@ const handleSaveToS3 = async (filename, blob) => {
|
||||
<n-drawer-content :title="mail.subject" closable>
|
||||
<div class="fullscreen-mail-content" :class="{ 'dark-mode': isDark }">
|
||||
<pre v-if="showTextMail" class="mail-text">{{ mail.text }}</pre>
|
||||
<iframe v-else-if="useIframeShowMail" :srcdoc="mail.message" class="mail-iframe">
|
||||
<iframe v-else-if="useIframeShowMail" :srcdoc="processedMail.message" class="mail-iframe">
|
||||
</iframe>
|
||||
<ShadowHtmlComponent v-else :key="mail.id" :htmlContent="mail.message" :isDark="isDark" class="mail-html" />
|
||||
<ShadowHtmlComponent v-else :key="mail.id" :htmlContent="processedMail.message" :isDark="isDark" class="mail-html" />
|
||||
</div>
|
||||
</n-drawer-content>
|
||||
</n-drawer>
|
||||
@@ -244,6 +251,11 @@ const handleSaveToS3 = async (filename, blob) => {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* Let the banner's inner space fill the alert so the button sits on the right. */
|
||||
.remote-images-banner :deep(.n-space) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mail-content {
|
||||
margin-top: 10px;
|
||||
flex: 1;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup>
|
||||
import { watch, onMounted, ref, computed } from "vue";
|
||||
import { useMessage } from 'naive-ui'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useScopedI18n } from '@/i18n/app'
|
||||
import { useGlobalState } from '../store'
|
||||
import { useIsMobile } from '../utils/composables'
|
||||
import { utcToLocalDate } from '../utils';
|
||||
@@ -46,36 +46,7 @@ const multiActionMode = ref(false)
|
||||
const showMultiActionDelete = ref(false)
|
||||
const multiActionDeleteProgress = ref({ percentage: 0, tip: '0/0' })
|
||||
|
||||
const { t } = useI18n({
|
||||
messages: {
|
||||
en: {
|
||||
success: 'Success',
|
||||
refresh: 'Refresh',
|
||||
showCode: 'Change View Original Code',
|
||||
pleaseSelectMail: "Please select a mail to view.",
|
||||
emptySent: "No sent emails",
|
||||
delete: 'Delete',
|
||||
deleteMailTip: 'Are you sure you want to delete mail?',
|
||||
multiAction: 'Multi Action',
|
||||
cancelMultiAction: 'Cancel Multi Action',
|
||||
selectAll: 'Select All of This Page',
|
||||
unselectAll: 'Unselect All',
|
||||
},
|
||||
zh: {
|
||||
success: '成功',
|
||||
refresh: '刷新',
|
||||
showCode: '切换查看元数据',
|
||||
pleaseSelectMail: "请选择一封邮件查看。",
|
||||
emptySent: "发件箱为空",
|
||||
delete: '删除',
|
||||
deleteMailTip: '确定要删除邮件吗?',
|
||||
multiAction: '多选',
|
||||
cancelMultiAction: '取消多选',
|
||||
selectAll: '全选本页',
|
||||
unselectAll: '取消全选',
|
||||
}
|
||||
}
|
||||
});
|
||||
const { t } = useScopedI18n('components.SendBox')
|
||||
|
||||
watch([page, pageSize], async ([page, pageSize], [oldPage, oldPageSize]) => {
|
||||
if (page !== oldPage || pageSize !== oldPageSize) {
|
||||
@@ -239,8 +210,13 @@ onMounted(async () => {
|
||||
</n-button>
|
||||
</n-space>
|
||||
</div>
|
||||
<n-split direction="horizontal" :max="0.75" :min="0.25" :default-size="mailboxSplitSize"
|
||||
:on-update:size="onSpiltSizeChange">
|
||||
<n-split direction="horizontal" :max="0.75" :min="0" :resize-trigger-size="8"
|
||||
:default-size="mailboxSplitSize" :on-update:size="onSpiltSizeChange">
|
||||
<template #resize-trigger>
|
||||
<div class="split-handle">
|
||||
<div class="split-handle__grip" />
|
||||
</div>
|
||||
</template>
|
||||
<template #1>
|
||||
<div style="overflow: auto; min-height: 60vh; max-height: 100vh;">
|
||||
<n-list hoverable clickable>
|
||||
@@ -408,4 +384,23 @@ pre {
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.split-handle {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.split-handle__grip {
|
||||
width: 4px;
|
||||
height: 32px;
|
||||
border-radius: 2px;
|
||||
background-color: var(--n-resize-trigger-color);
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.split-handle:hover .split-handle__grip {
|
||||
background-color: var(--n-resize-trigger-color-hover);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,29 +1,32 @@
|
||||
<script setup>
|
||||
import { ref, watch, onMounted } from "vue";
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { ref, watch } from "vue";
|
||||
import { useScopedI18n } from '@/i18n/app'
|
||||
import { useGlobalState } from '../store'
|
||||
import { getTurnstileLocale } from '../i18n/locale-registry'
|
||||
import { DEFAULT_LOCALE, isSupportedLocale } from '../i18n/utils'
|
||||
const { openSettings, isDark } = useGlobalState()
|
||||
|
||||
const cfToken = defineModel('value')
|
||||
|
||||
const { locale, t } = useI18n({
|
||||
messages: {
|
||||
en: {
|
||||
refresh: 'Refresh'
|
||||
},
|
||||
zh: {
|
||||
refresh: '刷新'
|
||||
}
|
||||
}
|
||||
});
|
||||
const { locale, t } = useScopedI18n('components.Turnstile')
|
||||
|
||||
const containerId = `cf-turnstile-${Math.random().toString(36).slice(2, 9)}`
|
||||
const cfTurnstileId = ref("")
|
||||
const turnstileLoading = ref(false)
|
||||
let turnstileRenderQueue = Promise.resolve()
|
||||
|
||||
const refresh = () => checkCfTurnstile(true)
|
||||
const refresh = () => rerenderTurnstile()
|
||||
defineExpose({ refresh })
|
||||
|
||||
const rerenderTurnstile = () => {
|
||||
cfToken.value = "";
|
||||
turnstileRenderQueue = turnstileRenderQueue
|
||||
.catch(() => { })
|
||||
.then(() => checkCfTurnstile(true))
|
||||
turnstileRenderQueue.catch(() => { })
|
||||
return turnstileRenderQueue
|
||||
}
|
||||
|
||||
const checkCfTurnstile = async (remove) => {
|
||||
if (!openSettings.value.cfTurnstileSiteKey) return;
|
||||
turnstileLoading.value = true;
|
||||
@@ -41,11 +44,15 @@ const checkCfTurnstile = async (remove) => {
|
||||
if (remove && cfTurnstileId.value) {
|
||||
window.turnstile.remove(cfTurnstileId.value);
|
||||
}
|
||||
// Cloudflare documents sitekey/theme/language as render-time options and
|
||||
// exposes remove()/render() for widget lifecycle updates, so recreate the
|
||||
// widget when any of those inputs change:
|
||||
// https://developers.cloudflare.com/turnstile/get-started/client-side-rendering/
|
||||
cfTurnstileId.value = window.turnstile.render(
|
||||
`#${containerId}`,
|
||||
{
|
||||
sitekey: openSettings.value.cfTurnstileSiteKey,
|
||||
language: locale.value == 'zh' ? 'zh-CN' : 'en-US',
|
||||
language: getTurnstileLocale(isSupportedLocale(locale.value) ? locale.value : DEFAULT_LOCALE),
|
||||
theme: isDark.value ? 'dark' : 'light',
|
||||
callback: function (token) {
|
||||
cfToken.value = token;
|
||||
@@ -57,14 +64,7 @@ const checkCfTurnstile = async (remove) => {
|
||||
}
|
||||
}
|
||||
|
||||
watch(isDark, async (isDark) => {
|
||||
checkCfTurnstile(true)
|
||||
}, { immediate: true })
|
||||
|
||||
onMounted(() => {
|
||||
cfToken.value = "";
|
||||
checkCfTurnstile(true);
|
||||
})
|
||||
watch([isDark, locale, () => openSettings.value.cfTurnstileSiteKey], rerenderTurnstile, { immediate: true })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -73,7 +73,7 @@ onMounted(() => {
|
||||
<n-form-item-row>
|
||||
<n-flex vertical>
|
||||
<div :id="containerId"></div>
|
||||
<n-button text @click="checkCfTurnstile(true)">
|
||||
<n-button text @click="rerenderTurnstile">
|
||||
{{ t('refresh') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, h } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useScopedI18n } from '@/i18n/app'
|
||||
import type { DropdownOption } from 'naive-ui'
|
||||
|
||||
const props = defineProps({
|
||||
@@ -24,30 +24,7 @@ const props = defineProps({
|
||||
// @ts-ignore
|
||||
const message = useMessage()
|
||||
|
||||
const { t } = useI18n({
|
||||
messages: {
|
||||
en: {
|
||||
successTip: 'Success',
|
||||
test: 'Test',
|
||||
save: 'Save',
|
||||
notEnabled: 'Webhook is not enabled for you',
|
||||
urlMissing: 'URL is required',
|
||||
enable: 'Enable',
|
||||
presets: 'Presets',
|
||||
fillInDemoTip: 'Please modify the URL and other settings to your own',
|
||||
},
|
||||
zh: {
|
||||
successTip: '成功',
|
||||
test: '测试',
|
||||
save: '保存',
|
||||
notEnabled: 'Webhook 未开启,请联系管理员开启',
|
||||
urlMissing: 'URL 不能为空',
|
||||
enable: '启用',
|
||||
presets: '示例模板',
|
||||
fillInDemoTip: '请修改URL和其他设置为您自己的配置',
|
||||
}
|
||||
}
|
||||
});
|
||||
const { t } = useScopedI18n('components.WebhookComponent')
|
||||
|
||||
class WebhookSettings {
|
||||
enabled: boolean = false
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false, // you must set `false`, to use Composition API
|
||||
locale: 'zh', // set locale
|
||||
fallbackLocale: 'en', // set fallback locale
|
||||
'en': {
|
||||
messages: {}
|
||||
},
|
||||
'zh': {
|
||||
messages: {}
|
||||
}
|
||||
})
|
||||
|
||||
export default i18n;
|
||||
@@ -0,0 +1,12 @@
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const withNamespace = (namespace: string, key: string) => `${namespace}.${key}`
|
||||
|
||||
export const useScopedI18n = (namespace: string) => {
|
||||
const composer = useI18n({ useScope: 'global' })
|
||||
|
||||
return {
|
||||
...composer,
|
||||
t: ((key: string, ...args: unknown[]) => composer.t(withNamespace(namespace, key), ...(args as []))) as typeof composer.t,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import {
|
||||
FALLBACK_LOCALE,
|
||||
getInitialLocale,
|
||||
} from './utils'
|
||||
import { I18N_MESSAGES } from './messages'
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false, // you must set `false`, to use Composition API
|
||||
locale: getInitialLocale(),
|
||||
fallbackLocale: FALLBACK_LOCALE,
|
||||
messages: I18N_MESSAGES,
|
||||
missingWarn: false,
|
||||
fallbackWarn: false,
|
||||
})
|
||||
|
||||
export default i18n;
|
||||
@@ -0,0 +1,107 @@
|
||||
import {
|
||||
dateDeDE,
|
||||
dateEnUS,
|
||||
dateEsAR,
|
||||
dateJaJP,
|
||||
datePtBR,
|
||||
dateZhCN,
|
||||
deDE,
|
||||
enUS,
|
||||
esAR,
|
||||
jaJP,
|
||||
ptBR,
|
||||
zhCN,
|
||||
} from 'naive-ui'
|
||||
|
||||
import type { NDateLocale, NLocale } from 'naive-ui'
|
||||
|
||||
type NaiveLocaleConfig = {
|
||||
locale: NLocale
|
||||
dateLocale: NDateLocale
|
||||
}
|
||||
|
||||
type LocaleRegistryEntry = {
|
||||
locale: string
|
||||
label: string
|
||||
browserMatches: string[]
|
||||
naive: NaiveLocaleConfig
|
||||
turnstileLocale: string
|
||||
}
|
||||
|
||||
export const LOCALE_REGISTRY = [
|
||||
{
|
||||
locale: 'zh',
|
||||
label: '中文',
|
||||
browserMatches: ['zh'],
|
||||
naive: { locale: zhCN, dateLocale: dateZhCN },
|
||||
turnstileLocale: 'zh-CN',
|
||||
},
|
||||
{
|
||||
locale: 'en',
|
||||
label: 'English',
|
||||
browserMatches: ['en'],
|
||||
naive: { locale: enUS, dateLocale: dateEnUS },
|
||||
turnstileLocale: 'en',
|
||||
},
|
||||
{
|
||||
locale: 'es',
|
||||
label: 'Español',
|
||||
browserMatches: ['es'],
|
||||
naive: { locale: esAR, dateLocale: dateEsAR },
|
||||
turnstileLocale: 'es',
|
||||
},
|
||||
{
|
||||
locale: 'pt-BR',
|
||||
label: 'Português (Brasil)',
|
||||
browserMatches: ['pt'],
|
||||
naive: { locale: ptBR, dateLocale: datePtBR },
|
||||
turnstileLocale: 'pt-BR',
|
||||
},
|
||||
{
|
||||
locale: 'ja',
|
||||
label: '日本語',
|
||||
browserMatches: ['ja'],
|
||||
naive: { locale: jaJP, dateLocale: dateJaJP },
|
||||
turnstileLocale: 'ja',
|
||||
},
|
||||
{
|
||||
locale: 'de',
|
||||
label: 'Deutsch',
|
||||
browserMatches: ['de'],
|
||||
naive: { locale: deDE, dateLocale: dateDeDE },
|
||||
turnstileLocale: 'de',
|
||||
},
|
||||
] as const satisfies readonly LocaleRegistryEntry[]
|
||||
|
||||
export type SupportedLocale = (typeof LOCALE_REGISTRY)[number]['locale']
|
||||
|
||||
export const SUPPORTED_LOCALES = LOCALE_REGISTRY.map(({ locale }) => locale) as SupportedLocale[]
|
||||
|
||||
const localeRegistryMap = Object.fromEntries(
|
||||
LOCALE_REGISTRY.map((entry) => [entry.locale, entry]),
|
||||
) as Record<SupportedLocale, (typeof LOCALE_REGISTRY)[number]>
|
||||
|
||||
export const getLocaleRegistryEntry = (locale: SupportedLocale) => {
|
||||
return localeRegistryMap[locale]
|
||||
}
|
||||
|
||||
export const getLocaleLabel = (locale: SupportedLocale) => {
|
||||
return getLocaleRegistryEntry(locale).label
|
||||
}
|
||||
|
||||
export const getLocaleOptions = () => {
|
||||
return LOCALE_REGISTRY.map(({ locale, label }) => ({
|
||||
label,
|
||||
value: locale,
|
||||
key: locale,
|
||||
}))
|
||||
}
|
||||
|
||||
export const getNaiveLocaleConfig = (locale: SupportedLocale) => {
|
||||
return getLocaleRegistryEntry(locale).naive
|
||||
}
|
||||
|
||||
export const getTurnstileLocale = (locale: SupportedLocale) => {
|
||||
return getLocaleRegistryEntry(locale).turnstileLocale
|
||||
}
|
||||
|
||||
@@ -0,0 +1,621 @@
|
||||
export const deMessages = {
|
||||
"views.index.SimpleIndex.mailCount": "{current} / {total} E-Mails",
|
||||
"views.admin.Statistics.activeAddressCount30days": "Aktive Adressanzahl in 30 Tagen",
|
||||
"views.admin.Statistics.activeAddressCount7days": "Aktive Adressanzahl in 7 Tagen",
|
||||
"views.Index.about": "Über",
|
||||
"views.Admin.about": "Über",
|
||||
"views.Header.accessHeader": "Zugangspasswort",
|
||||
"views.Admin.account": "E-Mail-Adressen",
|
||||
"views.Index.accountSettings": "E-Mail-Adress-Einstellungen",
|
||||
"views.Admin.account_settings": "E-Mail-Adress-Einstellungen",
|
||||
"views.index.SimpleIndex.accountSettings": "E-Mail-Adress-Einstellungen",
|
||||
"views.index.Attachment.action": "Aktion",
|
||||
"views.admin.SenderAccess.action": "Aktion",
|
||||
"views.user.UserSettings.actions": "Aktionen",
|
||||
"views.user.AddressManagement.actions": "Aktionen",
|
||||
"views.index.TelegramAddress.actions": "Aktionen",
|
||||
"views.admin.Account.actions": "Aktionen",
|
||||
"views.index.LocalAddress.actions": "Aktionen",
|
||||
"views.admin.UserManagement.actions": "Aktionen",
|
||||
"views.admin.AccountSettings.actions": "Aktionen",
|
||||
"views.admin.AccountSettings.add": "Hinzufügen",
|
||||
"views.admin.Maintenance.customSqlTip": "Füge benutzerdefinierte DELETE-SQL-Anweisungen für die geplante Bereinigung hinzu. Pro Eintrag ist nur eine DELETE-Anweisung erlaubt.",
|
||||
"views.admin.Maintenance.addCustomSql": "Benutzerdefinierte SQL hinzufügen",
|
||||
"views.admin.UserOauth2Settings.addOauth2": "Oauth2 hinzufügen",
|
||||
"components.AddressSelect.address": "Adresse",
|
||||
"views.user.AddressManagement.address": "Adresse",
|
||||
"views.index.TelegramAddress.address": "Adresse",
|
||||
"views.admin.SenderAccess.address": "Adresse",
|
||||
"views.index.LocalAddress.address": "Adresse",
|
||||
"views.admin.CreateAccount.address": "Adresse",
|
||||
"views.admin.AiExtractSettings.allowList": "Adressfreigabeliste (Adresse eingeben und Enter drücken; Wildcards werden unterstützt)",
|
||||
"views.admin.AccountSettings.send_address_block_list": "Blockierbegriffe für ausgehende E-Mails",
|
||||
"views.admin.AccountSettings.address_block_list": "Blockierbegriffe für Benutzer (Administrator kann überspringen)",
|
||||
"views.index.SimpleIndex.addressCopied": "Adresse erfolgreich kopiert",
|
||||
"views.admin.Statistics.addressCount": "Adressanzahl",
|
||||
"views.admin.UserManagement.address_count": "Adressanzahl",
|
||||
"views.User.address_management": "Adressverwaltung",
|
||||
"views.admin.UserManagement.userAddressManagement": "Adressverwaltung",
|
||||
"views.index.AddressBar.addressPassword": "Adresspasswort",
|
||||
"views.admin.CreateAccount.addressPassword": "Adresspasswort",
|
||||
"views.Admin.adminAccount": "Administrator",
|
||||
"views.Admin.accessHeader": "Admin-Passwort",
|
||||
"views.Admin.loginViaPassword": "Administrator mit Passwort anmelden",
|
||||
"views.admin.AiExtractSettings.title": "KI-E-Mail-Extraktionseinstellungen",
|
||||
"views.Admin.aiExtractSettings": "KI-Extraktionseinstellungen",
|
||||
"views.admin.AccountSettings.match_all": "Alle",
|
||||
"views.admin.AccountSettings.create_address_subdomain_match": "Suffix-Abgleich für Subdomains beim Erstellen von Adressen erlauben",
|
||||
"views.admin.UserSettings.enableUserRegister": "Benutzerregistrierung erlauben",
|
||||
"views.admin.AccountSettings.match_any": "Beliebig",
|
||||
"views.Index.appearance": "Darstellung",
|
||||
"views.Admin.appearance": "Darstellung",
|
||||
"views.admin.IpBlacklistSettings.tip_scope": "Gilt für: Adresse erstellen, E-Mail senden, externe Sende-API, Benutzerregistrierung und Code-Prüfung",
|
||||
"views.index.AccountSettings.clearInboxConfirm": "Möchtest du wirklich alle E-Mails im Posteingang löschen?",
|
||||
"views.index.AccountSettings.clearSentItemsConfirm": "Möchtest du wirklich alle E-Mails in den gesendeten Elementen löschen?",
|
||||
"views.admin.Account.multiClearInboxTip": "Möchtest du wirklich den Posteingang der ausgewählten Adressen leeren?",
|
||||
"views.admin.Account.clearInboxTip": "Möchtest du wirklich den Posteingang dieser E-Mail-Adresse leeren?",
|
||||
"views.admin.Account.multiClearSentItemsTip": "Möchtest du wirklich die gesendeten Elemente der ausgewählten Adressen leeren?",
|
||||
"views.admin.Account.clearSentItemsTip": "Möchtest du wirklich die gesendeten Elemente dieser E-Mail-Adresse leeren?",
|
||||
"views.admin.Account.multiDeleteTip": "Möchtest du wirklich die ausgewählten Adressen löschen?",
|
||||
"views.index.Attachment.deleteConfirm": "Möchtest du wirklich diesen Anhang löschen?",
|
||||
"views.admin.Account.deleteTip": "Möchtest du wirklich diese E-Mail löschen?",
|
||||
"views.admin.SenderAccess.deleteTip": "Möchtest du dies wirklich löschen?",
|
||||
"views.index.AccountSettings.deleteAccountConfirm": "Möchtest du diese E-Mail-Adresse und alle zugehörigen E-Mails wirklich löschen?",
|
||||
"views.index.AccountSettings.logoutConfirm": "Möchtest du dich wirklich abmelden?",
|
||||
"components.MailBox.deleteMailTip": "Möchtest du die E-Mail wirklich löschen?",
|
||||
"components.MailContentRenderer.deleteMailTip": "Möchtest du die E-Mail wirklich löschen?",
|
||||
"components.SendBox.deleteMailTip": "Möchtest du die E-Mail wirklich löschen?",
|
||||
"views.admin.AccountSettings.delete_rule_confirm": "Möchtest du diese Regel wirklich löschen?",
|
||||
"views.admin.UserManagement.deleteUserTip": "Möchtest du dieses Benutzerkonto wirklich löschen?",
|
||||
"views.Admin.logoutConfirmContent": "Möchtest du dich wirklich aus dem Admin-Bereich abmelden?",
|
||||
"views.user.UserSettings.logoutConfirm": "Möchtest du dich wirklich abmelden?",
|
||||
"views.admin.IpBlacklistSettings.asn_blacklist": "ASN-Organisationssperrliste",
|
||||
"views.admin.IpBlacklistSettings.tip_asn": "ASN-Organisation: Nach ISP/Provider blockieren. Groß-/Kleinschreibung-unabhängiger Textabgleich oder Regex.",
|
||||
"components.AiExtractInfo.authLink": "Authentifizierungslink",
|
||||
"views.admin.Maintenance.autoCleanup": "Automatische Bereinigung",
|
||||
"components.MailBox.autoRefresh": "Automatische Aktualisierung",
|
||||
"components.MailBox.backToList": "Zurück zur Liste",
|
||||
"views.common.Appearance.autoRefreshInterval": "Automatisches Aktualisierungsintervall (s)",
|
||||
"views.Index.auto_reply": "Automatische Antwort",
|
||||
"views.index.AutoReply.autoReply": "Automatische Antwort",
|
||||
"views.common.Login.autoGeneratedName": "Automatisch erzeugter Name",
|
||||
"views.admin.SenderAccess.balance": "Guthaben",
|
||||
"views.admin.Maintenance.basicCleanup": "Grundbereinigung",
|
||||
"views.user.AddressManagement.unbindAddressTip": "Wechsle vor dem Trennen zu dieser E-Mail-Adresse und speichere die Zugangsdaten.",
|
||||
"views.index.TelegramAddress.bind": "Verknüpfen",
|
||||
"views.index.TelegramAddress.bindAddressSuccess": "Adresse erfolgreich verknüpft",
|
||||
"views.index.LocalAddress.bindAddressSuccess": "Adresse erfolgreich verknüpft",
|
||||
"views.User.bind_address": "Mailadresse verknüpfen",
|
||||
"views.admin.IpBlacklistSettings.enable_tip": "IPs blockieren, die Sperrlistenmustern entsprechen und auf limitierte APIs zugreifen",
|
||||
"views.admin.AccountSettings.fromBlockList": "Blockierbegriffe für eingehende E-Mails",
|
||||
"views.admin.AccountSettings.block_receive_unknow_address_email": "Empfang von E-Mails an unbekannte Adressen blockieren",
|
||||
"views.common.Appearance.bottom": "unten",
|
||||
"views.admin.IpBlacklistSettings.fingerprint_blacklist": "Browser-Fingerprint-Sperrliste",
|
||||
"views.admin.IpBlacklistSettings.tip_fingerprint": "Browser-Fingerprint: Sperrt per Fingerprint. Unterstützt exakte Treffer oder Regex.",
|
||||
"views.admin.AccountSettings.cancel": "Abbrechen",
|
||||
"components.MailBox.cancelMultiAction": "Mehrfachaktion abbrechen",
|
||||
"components.SendBox.cancelMultiAction": "Mehrfachaktion abbrechen",
|
||||
"views.user.AddressManagement.changeMailAddress": "Adresse wechseln",
|
||||
"views.index.TelegramAddress.changeMailAddress": "Mailadresse ändern",
|
||||
"views.index.LocalAddress.changeMailAddress": "Mailadresse ändern",
|
||||
"views.index.AccountSettings.changePassword": "Passwort ändern",
|
||||
"views.admin.UserManagement.changeRole": "Ändern Rolle",
|
||||
"components.SendBox.showCode": "Ansicht des Originalcodes umschalten",
|
||||
"views.admin.Telegram.status": "Status prüfen",
|
||||
"views.admin.AccountSettings.create_address_subdomain_match_follow_env_note": "Wenn du „Umgebungsvariable folgen“ auswählst, wird die Admin-Überschreibung entfernt und der Zustand auf „nicht gesetzt“ zurückgesetzt. Das tatsächliche Ergebnis wird weiterhin durch die Worker-Umgebung und die Prioritätsregeln bestimmt.",
|
||||
"views.admin.Maintenance.cleanupNow": "Jetzt bereinigen",
|
||||
"views.admin.Maintenance.cleanupSuccess": "Bereinigung erfolgreich",
|
||||
"views.admin.Maintenance.addressCreateLabel": "Adressen löschen, die vor mehr als n Tagen erstellt wurden",
|
||||
"views.admin.Maintenance.emptyAddressLabel": "Leere Adressen löschen, die älter als n Tage sind",
|
||||
"views.admin.Maintenance.inactiveAddressLabel": "Inaktive Adressen löschen, die älter als n Tage sind",
|
||||
"views.admin.Maintenance.mailBoxLabel": "Posteingänge löschen, die älter als n Tage sind",
|
||||
"views.admin.Maintenance.sendBoxLabel": "Postausgänge löschen, die älter als n Tage sind",
|
||||
"views.admin.Maintenance.unboundAddressLabel": "Seit n Tagen nicht verknüpfte E-Mail-Adressen löschen",
|
||||
"views.admin.Maintenance.mailUnknowLabel": "E-Mails mit unbekanntem Empfänger löschen, die älter als n Tage sind",
|
||||
"views.index.AccountSettings.clearInbox": "Posteingang leeren",
|
||||
"views.admin.Account.clearInbox": "Posteingang leeren",
|
||||
"views.index.AccountSettings.clearSentItems": "Gesendete Elemente leeren",
|
||||
"views.admin.Account.clearSentItems": "Gesendete Elemente leeren",
|
||||
"views.Header.title": "Cloudflare Temporäre E-Mail",
|
||||
"views.admin.DatabaseManager.code_db_version": "Vom Code benötigte DB-Version",
|
||||
"views.user.UserOauth2Callback.codeNotFound": "Code nicht gefunden",
|
||||
"views.admin.AccountSettings.config": "Konfiguration",
|
||||
"views.admin.RoleAddressConfig.roleConfigDesc": "Lege die maximale Anzahl von Adressen pro Benutzerrolle fest. Rollenbasierte Limits haben Vorrang vor globalen Einstellungen. Verwende 0 für unbegrenzt.",
|
||||
"views.Admin.confirm": "Bestätigen",
|
||||
"views.Admin.logoutConfirmTitle": "Abmeldung bestätigen",
|
||||
"views.index.AccountSettings.confirmPassword": "Passwort bestätigen",
|
||||
"views.index.SendMail.content": "Inhalt",
|
||||
"views.admin.SendMail.content": "Inhalt",
|
||||
"views.index.SendMail.contentEmpty": "Der Inhalt ist leer",
|
||||
"views.admin.SendMail.contentEmpty": "Der Inhalt ist leer",
|
||||
"components.AddressSelect.copied": "Kopiert",
|
||||
"components.AiExtractInfo.copySuccess": "Erfolgreich kopiert",
|
||||
"components.AddressSelect.copy": "Kopieren",
|
||||
"views.index.SimpleIndex.copyAddress": "Kopieren",
|
||||
"components.AiExtractInfo.copyFailed": "Kopieren fehlgeschlagen",
|
||||
"views.Footer.copyright": "Urheberrecht",
|
||||
"views.Admin.account_create": "E-Mail-Adresse erstellen",
|
||||
"views.admin.CreateAccount.creatNewEmail": "Neue E-Mail erstellen",
|
||||
"views.common.Login.getNewEmail": "Neue E-Mail erstellen",
|
||||
"views.user.AddressManagement.create_or_bind": "Erstellen oder verknüpfen",
|
||||
"views.index.LocalAddress.create_or_bind": "Erstellen oder verknüpfen",
|
||||
"views.user.UserSettings.createPasskey": "Passkey erstellen",
|
||||
"views.admin.UserManagement.createUser": "Benutzerkonto erstellen",
|
||||
"views.user.UserSettings.created_at": "Erstellt am",
|
||||
"views.admin.Account.created_at": "Erstellt am",
|
||||
"views.admin.SenderAccess.created_at": "Erstellt am",
|
||||
"views.admin.UserManagement.created_at": "Erstellt am",
|
||||
"views.common.Login.credentialLogin": "Mit Zugangsdaten anmelden",
|
||||
"views.admin.DatabaseManager.current_db_version": "Aktuelle DB-Version",
|
||||
"views.user.UserBar.currentUser": "Aktuelles Benutzerkonto",
|
||||
"views.admin.UserManagement.roleDonotExist": "Die aktuelle Rolle existiert nicht",
|
||||
"views.admin.Maintenance.customSqlCleanup": "Benutzerdefinierte SQL-Bereinigung",
|
||||
"views.admin.AccountSettings.send_mail_daily_limit": "Tageslimit",
|
||||
"views.admin.AccountSettings.send_mail_daily_limit_invalid": "Das Tageslimit muss eine ganze Zahl größer oder gleich -1 sein",
|
||||
"views.admin.IpBlacklistSettings.tip_daily_limit": "Tageslimit: Begrenzt die maximale Anzahl von Anfragen pro IP-Adresse und Tag (1-1000000).",
|
||||
"views.admin.IpBlacklistSettings.daily_request_limit": "Tägliches Anfrage-Limit",
|
||||
"views.Header.dark": "Dunkel",
|
||||
"views.Admin.database": "Datenbank",
|
||||
"views.admin.DatabaseManager.need_initialization_tip": "Die Datenbank muss initialisiert werden. Bitte zuerst initialisieren.",
|
||||
"views.admin.DatabaseManager.initializationSuccess": "Datenbank erfolgreich initialisiert",
|
||||
"views.admin.DatabaseManager.migrationSuccess": "Datenbank erfolgreich migriert",
|
||||
"views.admin.DatabaseManager.need_migration_tip": "Die Datenbank muss migriert werden. Bitte zuerst migrieren.",
|
||||
"components.MailBox.delete": "Löschen",
|
||||
"components.MailContentRenderer.delete": "Löschen",
|
||||
"components.SendBox.delete": "Löschen",
|
||||
"views.index.Attachment.delete": "Löschen",
|
||||
"views.admin.Account.delete": "Löschen",
|
||||
"views.admin.UserOauth2Settings.delete": "Löschen",
|
||||
"views.admin.SenderAccess.delete": "Löschen",
|
||||
"views.admin.UserManagement.delete": "Löschen",
|
||||
"views.admin.AccountSettings.delete_rule": "Löschen",
|
||||
"views.admin.Maintenance.deleteCustomSql": "Löschen",
|
||||
"views.index.AccountSettings.deleteAccount": "E-Mail-Adresse löschen",
|
||||
"views.admin.Account.deleteAccount": "E-Mail-Adresse löschen",
|
||||
"views.user.UserSettings.deletePasskey": "Passkey löschen",
|
||||
"views.admin.AccountSettings.delete_success": "Erfolgreich gelöscht",
|
||||
"views.admin.UserManagement.deleteUser": "Benutzerkonto löschen",
|
||||
"views.index.Attachment.deleteSuccess": "Erfolgreich gelöscht",
|
||||
"views.admin.SenderAccess.disable": "Deaktivieren",
|
||||
"views.Admin.loginViaDisabledCheck": "Passwortprüfung deaktiviert",
|
||||
"views.common.Appearance.preferShowTextMail": "Text-Mail standardmäßig anzeigen",
|
||||
"views.admin.AccountSettings.domain_list": "Domain-Liste (optional)",
|
||||
"views.admin.AccountSettings.source_patterns_tip": "Die Domain-Liste filtert nach Empfängeradresse, die Quell-Regex nach Absenderadresse. Für die Weiterleitung müssen beide Bedingungen erfüllt sein (UND-Logik). Leer lassen, um den jeweiligen Filter zu überspringen.",
|
||||
"views.admin.UserManagement.domains": "Domains",
|
||||
"views.index.Attachment.download": "Herunterladen",
|
||||
"components.MailBox.downloadMail": "Herunterladen Mail",
|
||||
"components.MailContentRenderer.downloadMail": "Herunterladen Mail",
|
||||
"views.admin.Maintenance.sqlNamePlaceholder": "z. B. Alte Logs bereinigen",
|
||||
"views.admin.Maintenance.sqlPlaceholder": "z. B. DELETE FROM raw_mails WHERE source GLOB '*{'@'}example.com' AND created_at < datetime('now', '-3 day')",
|
||||
"views.admin.AccountSettings.source_patterns_placeholder": "z. B. gmail.com",
|
||||
"views.admin.AccountSettings.forwarding_rule_warning": "Jede Regel wird unabhängig ausgeführt. Die Weiterleitungsadresse muss verifiziert sein.",
|
||||
"views.index.SendMail.edit": "Bearbeiten",
|
||||
"views.admin.SendMail.edit": "Bearbeiten",
|
||||
"views.user.UserLogin.email": "E-Mail",
|
||||
"views.admin.UserManagement.email": "E-Mail",
|
||||
"views.common.Login.email": "E-Mail",
|
||||
"views.common.Login.credential": "E-Mail-Adresszugangsdaten",
|
||||
"views.common.Login.emailPasswordRequired": "E-Mail und Passwort sind erforderlich",
|
||||
"views.admin.UserSettings.emailCheckRegex": "Regex zur E-Mail-Prüfung (z. B. ^[^.]+{'@'}.+$, um Punkte vor {'@'} zu verbieten)",
|
||||
"views.admin.AccountSettings.email_forwarding_config": "E-Mail-Weiterleitungskonfiguration",
|
||||
"views.admin.UserOauth2Settings.userEmailFormat": "E-Mail-Regex-Muster",
|
||||
"views.Admin.mails": "E-Mails",
|
||||
"views.index.AutoReply.sourcePrefixPlaceholder": "Leer=alle, Präfixabgleich oder /regex/",
|
||||
"components.WebhookComponent.enable": "Aktivieren",
|
||||
"views.admin.UserOauth2Settings.enable": "Aktivieren",
|
||||
"views.admin.SenderAccess.enable": "Aktivieren",
|
||||
"views.admin.Telegram.enable": "Aktivieren",
|
||||
"views.admin.UserSettings.enable": "Aktivieren",
|
||||
"views.admin.AiExtractSettings.enableAllowList": "Aktivieren Adressfreigabeliste",
|
||||
"views.admin.Webhook.enableAllowList": "Freigabeliste aktivieren (Webhook-Zugriff auf bestimmte E-Mail-Adressen beschränken)",
|
||||
"views.index.AutoReply.enableAutoReply": "Automatische Antwort aktivieren",
|
||||
"views.admin.Maintenance.cronTip": "Um die Cron-Bereinigung zu aktivieren, konfiguriere [crons] im Worker. Siehe Dokumentation; 0 Tage bedeutet alles löschen.",
|
||||
"views.admin.IpBlacklistSettings.enable_daily_limit": "Aktivieren Tägliches Anfrage-Limit",
|
||||
"views.admin.UserSettings.enableEmailCheckRegex": "E-Mail-Prüfregex aktivieren",
|
||||
"views.admin.UserOauth2Settings.enableEmailFormat": "E-Mail-Format aktivieren",
|
||||
"views.admin.Telegram.enableGlobalMailPush": "Globalen Mail-Push aktivieren (Telegram-Chat-ID manuell eingeben)",
|
||||
"views.admin.IpBlacklistSettings.enable_ip_blacklist": "IP-Sperrliste aktivieren",
|
||||
"views.admin.IpBlacklistSettings.enable_ip_whitelist": "IP-Freigabeliste aktivieren (streng)",
|
||||
"views.admin.UserOauth2Settings.enableMailAllowList": "Aktivieren E-Mail-Adressfreigabeliste(Manually enterable)",
|
||||
"views.admin.UserSettings.enableMailAllowList": "Aktivieren E-Mail-Adressfreigabeliste(Manually enterable)",
|
||||
"views.admin.UserSettings.enableMailVerify": "Mail-Prüfung aktivieren (die Absenderadresse muss im System vorhanden sein, Guthaben haben und normal senden können)",
|
||||
"views.admin.Telegram.enableTelegramAllowList": "Aktivieren Telegram-Freigabeliste(Manually input Chat ID)",
|
||||
"views.admin.IpBlacklistSettings.asn_blacklist_placeholder": "ASN-Organisation eingeben (z. B. Google, Amazon)",
|
||||
"views.admin.IpBlacklistSettings.fingerprint_blacklist_placeholder": "Fingerprint-ID eingeben (z. B. a1b2c3d4e5f6g7h8)",
|
||||
"views.admin.IpBlacklistSettings.daily_request_limit_placeholder": "Limit eingeben (z. B. 1000)",
|
||||
"views.admin.IpBlacklistSettings.ip_blacklist_placeholder": "Muster eingeben (z. B. 192.168.1 oder ^10\\.0\\.0\\.5$)",
|
||||
"views.common.Login.bindUserAddressError": "Fehler beim Verknüpfen der E-Mail-Adresse mit dem Benutzer",
|
||||
"views.admin.IpBlacklistSettings.ip_whitelist_placeholder": "Exakte IP (z. B. 1.2.3.4) oder verankerte Regex (z. B. ^192\\.168\\.1\\.\\d+$)",
|
||||
"views.index.SimpleIndex.exitSimpleIndex": "Einfachen Modus verlassen",
|
||||
"components.MailBox.keywordQueryTip": "Aktuelle Seite filtern",
|
||||
"views.admin.AccountSettings.create_address_subdomain_match_follow_env": "Umgebungsvariable folgen",
|
||||
"views.admin.AccountSettings.create_address_subdomain_match_force_disable": "Erzwingt deaktivieren",
|
||||
"views.admin.AccountSettings.create_address_subdomain_match_force_enable": "Erzwingt aktivieren",
|
||||
"views.user.UserLogin.forgotPassword": "Passwort vergessen",
|
||||
"components.MailBox.forwardMail": "Weiterleiten",
|
||||
"components.MailContentRenderer.forward": "Weiterleiten",
|
||||
"views.admin.AccountSettings.forward_address": "Weiterleitungsadresse",
|
||||
"views.admin.AccountSettings.forward_address_required": "Eine Weiterleitungsadresse ist erforderlich",
|
||||
"views.admin.AccountSettings.forward_placeholder": "forward@example.com",
|
||||
"components.MailContentRenderer.fullscreen": "Vollbild",
|
||||
"views.common.Login.generateName": "Zufälligen Namen erzeugen",
|
||||
"views.admin.Telegram.globalMailPushList": "Globale Mail-Push-Chat-ID-Liste",
|
||||
"views.common.Appearance.globalTabplacement": "Globale Tab-Position",
|
||||
"views.common.Login.help": "Hilfe",
|
||||
"views.Header.home": "Startseite",
|
||||
"views.index.SendMail.html": "HTML",
|
||||
"views.admin.SendMail.html": "HTML",
|
||||
"views.admin.UserOauth2Settings.icon": "Icon (SVG, bitte nur aus vertrauenswürdiger Quelle)",
|
||||
"views.admin.CreateAccount.enablePrefix": "Wenn Präfix aktiviert ist",
|
||||
"views.common.AdminContact.adminContact": "Wenn du Hilfe brauchst, kontaktiere bitte den Administrator ({msg})",
|
||||
"views.admin.Telegram.init": "Initialisieren",
|
||||
"views.admin.DatabaseManager.init": "Datenbank initialisieren",
|
||||
"views.admin.AccountSettings.regex_invalid": "Ungültiges Regex-Muster",
|
||||
"views.Admin.ipBlacklistSettings": "IP-Sperrliste",
|
||||
"views.admin.IpBlacklistSettings.ip_blacklist": "Muster der IP-Sperrliste",
|
||||
"views.admin.IpBlacklistSettings.title": "Einstellungen der IP-Sperrliste",
|
||||
"views.admin.IpBlacklistSettings.tip_ip": "IP-Sperrliste: Unterstützt Textabgleich (z. B. \"192.168.1\") oder Regex (z. B. \"^10\\.0\\.0\\.5$\").",
|
||||
"views.admin.IpBlacklistSettings.whitelist_empty_warning": "Die IP-Freigabeliste ist aktiviert, aber leer. Der Server ignoriert dies, um eine Aussperrung zu vermeiden. Bitte vor dem Aktivieren mindestens einen Eintrag hinzufügen.",
|
||||
"views.admin.IpBlacklistSettings.ip_whitelist": "IP-Freigabeliste Muster",
|
||||
"views.admin.IpBlacklistSettings.tip_whitelist": "IP-Freigabeliste: Strenger Freigabemodus. Einfache Einträge müssen EXAKT zur IP passen (keine Teilzeichenfolge). Verwende verankerte Regex (^...$) für Bereiche. Freigegebene IPs überspringen die Sperrlistenprüfung.",
|
||||
"views.admin.SenderAccess.is_enabled": "Aktiviert",
|
||||
"views.admin.Account.itemCount": "itemCount",
|
||||
"views.admin.SenderAccess.itemCount": "itemCount",
|
||||
"views.admin.UserManagement.itemCount": "itemCount",
|
||||
"views.user.UserMailBox.addressQueryTip": "Leer lassen, um alle Adressen abzufragen",
|
||||
"views.admin.Account.addressQueryTip": "Leer lassen, um alle Adressen abzufragen",
|
||||
"views.admin.Mails.addressQueryTip": "Leer lassen, um alle Adressen abzufragen",
|
||||
"views.common.Appearance.left": "links",
|
||||
"views.common.Login.getNewEmailTip2": "Wenn es leer bleibt, wird eine zufällige E-Mail-Adresse erzeugt.",
|
||||
"views.Header.light": "Hell",
|
||||
"views.admin.IpBlacklistSettings.enable_daily_limit_tip": "Anzahl der API-Anfragen pro IP-Adresse und Tag begrenzen",
|
||||
"components.AddressSelect.localAddresses": "Lokale Adressen",
|
||||
"views.common.Login.bindUserInfo": "Angemeldete Benutzer: Anmeldung ohne verknüpfte E-Mail oder das Erstellen einer neuen Adresse verknüpft sie mit dem aktuellen Benutzer",
|
||||
"views.user.UserOauth2Callback.logging": "Anmeldung läuft...",
|
||||
"views.user.UserLogin.login": "Anmelden",
|
||||
"views.common.Login.login": "Anmelden",
|
||||
"views.common.Login.loginAndBind": "Anmelden und verknüpfen",
|
||||
"views.Admin.loginMethod": "Anmeldemethode",
|
||||
"views.user.UserBar.fetchUserSettingsError": "Das Anmeldepasswort ist ungültig oder das Konto existiert nicht. Es könnte auch ein Netzwerkproblem vorliegen. Bitte später erneut versuchen.",
|
||||
"views.user.UserLogin.loginWith": "Mit {provider} anmelden",
|
||||
"views.user.UserLogin.loginWithPasskey": "Mit Passkey anmelden",
|
||||
"views.user.UserSettings.logout": "Abmelden",
|
||||
"views.user.BindAddress.logout": "Abmelden",
|
||||
"views.Admin.logout": "Abmelden",
|
||||
"views.index.AccountSettings.logout": "Abmelden",
|
||||
"views.Admin.logoutSuccess": "Erfolgreich abgemeldet",
|
||||
"views.admin.UserOauth2Settings.mailAllowList": "E-Mail-Adressfreigabeliste",
|
||||
"views.admin.UserSettings.mailAllowList": "E-Mail-Adressfreigabeliste",
|
||||
"views.index.SimpleIndex.addressCredential": "E-Mail-Adresszugangsdaten",
|
||||
"views.index.AddressBar.addressCredential": "E-Mail-Adresszugangsdaten",
|
||||
"views.admin.Account.addressCredential": "E-Mail-Adresszugangsdaten",
|
||||
"views.admin.CreateAccount.addressCredential": "E-Mail-Adresszugangsdaten",
|
||||
"views.index.AddressBar.fetchAddressError": "Die E-Mail-Zugangsdaten sind ungültig oder das Konto existiert nicht. Es kann auch ein Netzwerkproblem sein. Bitte später erneut versuchen.",
|
||||
"views.Index.mailbox": "Posteingang",
|
||||
"views.User.user_mail_box_tab": "Posteingang",
|
||||
"views.user.AddressManagement.mail_count": "E-Mail-Anzahl",
|
||||
"views.admin.Account.mail_count": "E-Mail-Anzahl",
|
||||
"views.admin.Statistics.mailCount": "E-Mail-Anzahl",
|
||||
"views.admin.UserAddressManagement.mail_count": "E-Mail-Anzahl",
|
||||
"views.index.SimpleIndex.deleteSuccess": "E-Mail erfolgreich gelöscht",
|
||||
"views.user.UserLogin.cannotForgotPassword": "E-Mail-Verifizierung oder Registrierung ist deaktiviert; das Passwort kann nicht zurückgesetzt werden. Bitte den Administrator kontaktieren.",
|
||||
"views.Admin.mailWebhook": "Mail-Webhook",
|
||||
"views.common.Appearance.mailboxSplitSize": "Breite der linken Liste in der zweispaltigen Postfachansicht",
|
||||
"views.common.Appearance.mailListView": "Postfach-Listenansicht in voller Breite",
|
||||
"views.common.Appearance.mailListPreviewLineClamp": "Zeilen der Textvorschau",
|
||||
"views.common.Appearance.off": "Aus",
|
||||
"views.index.SimpleIndex.refreshSuccess": "E-Mails erfolgreich aktualisiert",
|
||||
"views.Admin.unknow": "E-Mails mit unbekanntem Empfänger",
|
||||
"views.Admin.maintenance": "Wartung",
|
||||
"views.index.AddressBar.addressManage": "Verwalten",
|
||||
"views.admin.AccountSettings.source_match_mode": "Abgleichsmodus",
|
||||
"views.admin.RoleAddressConfig.maxAddressCount": "Maximale Adressanzahl (0 = unbegrenzt)",
|
||||
"views.admin.UserSettings.maxAddressCount": "Maximale Anzahl verknüpfbarer E-Mail-Adressen (0 = unbegrenzt)",
|
||||
"views.Header.menu": "Menü",
|
||||
"views.admin.DatabaseManager.migration": "Datenbank migrieren",
|
||||
"views.admin.SenderAccess.modify": "Ändern",
|
||||
"views.admin.AccountSettings.send_mail_monthly_limit": "Monatslimit",
|
||||
"views.admin.AccountSettings.send_mail_monthly_limit_invalid": "Das Monatslimit muss eine ganze Zahl größer oder gleich -1 sein",
|
||||
"components.MailBox.multiAction": "Mehrfachaktion",
|
||||
"components.SendBox.multiAction": "Mehrfachaktion",
|
||||
"views.admin.Account.multiClearInbox": "Mehrere Posteingänge leeren",
|
||||
"views.admin.Account.multiClearSentItems": "Mehrere gesendete Elemente leeren",
|
||||
"views.admin.Account.multiDelete": "Mehrfach löschen",
|
||||
"views.user.AddressManagement.name": "Name",
|
||||
"views.index.AutoReply.name": "Name",
|
||||
"views.admin.Account.name": "Name",
|
||||
"views.admin.UserOauth2Settings.name": "Name",
|
||||
"views.admin.UserAddressManagement.name": "Name",
|
||||
"views.admin.Maintenance.sqlName": "Name",
|
||||
"views.index.AccountSettings.newPassword": "Neues Passwort",
|
||||
"views.admin.Account.newPassword": "Neues Passwort",
|
||||
"components.MailBox.nextMail": "Weiter",
|
||||
"views.index.SimpleIndex.nextPage": "Weiter",
|
||||
"views.admin.AccountSettings.noLimitSendAddressList": "Adressliste ohne Guthabenlimit",
|
||||
"views.index.SimpleIndex.noMails": "Keine E-Mails gefunden",
|
||||
"views.admin.RoleAddressConfig.noRolesAvailable": "In der Systemkonfiguration sind keine Rollen verfügbar",
|
||||
"views.index.SendMail.requestAccessTip": "Sendezugang und Guthaben gehören zur aktuellen E-Mail-Adresse, nicht zum Benutzerkonto. Für diese Adresse Zugriff anfordern oder den Administrator kontaktieren.",
|
||||
"components.SendBox.emptySent": "Keine gesendeten E-Mails",
|
||||
"views.admin.RoleAddressConfig.notConfigured": "Nicht konfiguriert (globale Einstellungen verwenden)",
|
||||
"views.Admin.userOauth2Settings": "OAuth2-Einstellungen",
|
||||
"views.admin.UserOauth2Settings.oauth2Type": "OAuth2-Typ",
|
||||
"views.Header.ok": "OK",
|
||||
"views.Admin.ok": "OK",
|
||||
"views.index.AddressBar.ok": "OK",
|
||||
"views.admin.SenderAccess.ok": "OK",
|
||||
"views.common.Login.ok": "OK",
|
||||
"views.admin.AccountSettings.create_address_subdomain_match_tip": "Wirkt sich nur auf die Domainvalidierung von /api/new_address und /admin/new_address aus. Beispiel: Wenn aktiviert, kann foo.example.com der Basisdomain example.com entsprechen.",
|
||||
"components.AiExtractInfo.open": "Öffnen",
|
||||
"views.index.AddressBar.linkWithAddressCredential": "Automatischen E-Mail-Anmeldelink öffnen",
|
||||
"views.admin.CreateAccount.linkWithAddressCredential": "Automatischen E-Mail-Anmeldelink öffnen",
|
||||
"views.index.SendMail.options": "Optionen",
|
||||
"views.admin.SendMail.options": "Optionen",
|
||||
"components.AiExtractInfo.otherLink": "Weiterer Link",
|
||||
"views.user.UserSettings.passkeyCreated": "Passkey erfolgreich erstellt",
|
||||
"views.user.UserSettings.passkey_name": "Passkey-Name",
|
||||
"views.user.UserLogin.password": "Passwort",
|
||||
"views.admin.UserManagement.password": "Passwort",
|
||||
"views.common.Login.password": "Passwort",
|
||||
"views.index.AccountSettings.passwordChanged": "Passwort erfolgreich geändert",
|
||||
"views.common.Login.passwordLogin": "Passwortanmeldung",
|
||||
"views.admin.Account.passwordResetSuccess": "Passwort erfolgreich zurückgesetzt",
|
||||
"views.index.AccountSettings.passwordMismatch": "Passwörter stimmen nicht überein",
|
||||
"views.index.SendMail.successSend": "Bitte den Postausgang prüfen. Falls es fehlschlägt, Guthaben prüfen oder später erneut versuchen.",
|
||||
"views.admin.SendMail.successSend": "Bitte prüfe deinen Postausgang. Falls es fehlgeschlagen ist, versuche es später erneut.",
|
||||
"views.user.UserLogin.pleaseCompleteTurnstile": "Bitte Turnstile abschließen",
|
||||
"views.index.SimpleIndex.addressCredentialTip": "Bitte kopiere die E-Mail-Adresszugangsdaten. Du kannst sie zum Anmelden verwenden.",
|
||||
"views.index.AddressBar.addressCredentialTip": "Bitte kopiere die E-Mail-Adresszugangsdaten. Du kannst sie verwenden, um dich bei deinem E-Mail-Konto anzumelden.",
|
||||
"views.admin.Account.addressCredentialTip": "Bitte kopiere die E-Mail-Adresszugangsdaten. Du kannst sie verwenden, um dich bei deinem E-Mail-Konto anzumelden.",
|
||||
"views.admin.CreateAccount.addressCredentialTip": "Bitte kopiere die E-Mail-Adresszugangsdaten. Du kannst sie verwenden, um dich bei deinem E-Mail-Konto anzumelden.",
|
||||
"views.Admin.accessTip": "Bitte das Admin-Passwort eingeben",
|
||||
"views.Header.accessTip": "Bitte das korrekte Zugangspasswort eingeben",
|
||||
"views.admin.AccountSettings.address_block_list_placeholder": "Bitte die zu blockierenden Schlüsselwörter eingeben",
|
||||
"views.user.UserSettings.renamePasskeyNamePlaceholder": "Bitte den neuen Passkey-Namen eingeben",
|
||||
"views.user.UserSettings.passkeyNamePlaceholder": "Bitte den Passkey-Namen eingeben oder leer lassen, um einen zufälligen zu erzeugen",
|
||||
"views.admin.CreateAccount.fillInAllFields": "Bitte alle Felder ausfüllen",
|
||||
"views.admin.SendBox.queryTip": "Bitte die abzufragende Adresse eingeben; leer lassen für alle",
|
||||
"views.user.UserLogin.pleaseInputCode": "Bitte den Code eingeben",
|
||||
"views.admin.UserManagement.pleaseInput": "Bitte alle erforderlichen Informationen eingeben",
|
||||
"views.user.UserLogin.pleaseInputEmail": "Bitte die E-Mail eingeben",
|
||||
"views.user.UserLogin.pleaseInput": "Bitte E-Mail und Passwort eingeben",
|
||||
"views.admin.Maintenance.tip": "Bitte die Tage eingeben",
|
||||
"views.common.Login.getNewEmailTip1": "Bitte die gewünschte E-Mail-Adresse eingeben. Erlaubt sind nur:",
|
||||
"views.common.Login.credentialInput": "Bitte die Zugangsdaten der Mailadresse eingeben",
|
||||
"views.admin.SenderAccess.modalTip": "Bitte das Sendeguthaben eingeben",
|
||||
"views.user.UserLogin.pleaseLogin": "Bitte anmelden",
|
||||
"views.common.Login.pleaseGetNewEmail": "Bitte anmelden oder auf „Neue E-Mail erstellen“ klicken, um eine neue Adresse zu erhalten.",
|
||||
"components.WebhookComponent.fillInDemoTip": "Bitte URL und weitere Einstellungen an deine Umgebung anpassen",
|
||||
"components.SendBox.pleaseSelectMail": "Bitte eine E-Mail zum Anzeigen auswählen.",
|
||||
"views.admin.Account.pleaseSelectAddress": "Bitte eine Adresse auswählen",
|
||||
"components.MailBox.pleaseSelectMail": "Bitte eine E-Mail auswählen",
|
||||
"views.admin.UserManagement.prefix": "Präfix",
|
||||
"components.WebhookComponent.presets": "Voreinstellungen",
|
||||
"views.index.SendMail.preview": "Vorschau",
|
||||
"views.admin.SendMail.preview": "Vorschau",
|
||||
"views.admin.UserOauth2Settings.iconPreview": "Vorschau",
|
||||
"components.MailBox.prevMail": "Zurück",
|
||||
"views.index.SimpleIndex.prevPage": "Zurück",
|
||||
"components.MailBox.query": "Abfragen",
|
||||
"views.user.UserMailBox.query": "Abfragen",
|
||||
"views.Index.query": "Abfragen",
|
||||
"views.admin.SendBox.query": "Abfragen",
|
||||
"views.admin.Account.query": "Abfragen",
|
||||
"views.admin.SenderAccess.query": "Abfragen",
|
||||
"views.admin.UserManagement.query": "Abfragen",
|
||||
"views.admin.Mails.query": "Abfragen",
|
||||
"views.Admin.qucickSetup": "Schnelleinrichtung",
|
||||
"views.index.SendMail.toMailEmpty": "Die Empfängeradresse ist leer",
|
||||
"views.admin.SendMail.toMailEmpty": "Die Empfängeradresse ist leer",
|
||||
"views.index.SendMail.toName": "Name und Adresse des Empfängers; Namen leer lassen, um die E-Mail-Adresse zu verwenden",
|
||||
"views.admin.SendMail.toName": "Name und Adresse des Empfängers; Namen leer lassen, um die E-Mail-Adresse zu verwenden",
|
||||
"components.MailBox.refresh": "Aktualisieren",
|
||||
"components.Turnstile.refresh": "Aktualisieren",
|
||||
"components.SendBox.refresh": "Aktualisieren",
|
||||
"views.index.SimpleIndex.refreshMails": "Aktualisieren",
|
||||
"components.MailBox.refreshAfter": "Aktualisieren in {msg} Sekunden",
|
||||
"views.index.SimpleIndex.refreshAfter": "Aktualisieren in {msg} Sekunden",
|
||||
"views.admin.AccountSettings.regex_too_long": "Regex-Muster zu lang (max. 200 Zeichen)",
|
||||
"views.user.UserLogin.register": "Registrieren",
|
||||
"views.user.UserSettings.renamePasskey": "Passkey umbenennen",
|
||||
"views.admin.UserOauth2Settings.userEmailReplace": "Ersetzungsvorlage",
|
||||
"components.MailBox.reply": "Antworten",
|
||||
"components.MailContentRenderer.reply": "Antworten",
|
||||
"views.index.SendMail.requestAccess": "Zugriff für diese Adresse anfordern",
|
||||
"views.user.UserLogin.resetPassword": "Zurücksetzen Passwort",
|
||||
"views.admin.Account.resetPassword": "Zurücksetzen Passwort",
|
||||
"views.admin.UserManagement.resetPassword": "Zurücksetzen Passwort",
|
||||
"views.index.SendMail.rich text": "Rich Text",
|
||||
"views.admin.SendMail.rich text": "Rich Text",
|
||||
"views.common.Appearance.right": "rechts",
|
||||
"views.admin.UserManagement.role": "Rolle",
|
||||
"views.admin.RoleAddressConfig.role": "Rolle",
|
||||
"views.Admin.roleAddressConfig": "Rollenbasierte Adresskonfiguration",
|
||||
"views.admin.AccountSettings.rule_index": "Regel",
|
||||
"views.Index.s3Attachment": "S3-Anhang",
|
||||
"components.WebhookComponent.save": "Speichern",
|
||||
"views.index.AutoReply.save": "Speichern",
|
||||
"views.admin.AiExtractSettings.save": "Speichern",
|
||||
"views.admin.UserOauth2Settings.save": "Speichern",
|
||||
"views.admin.IpBlacklistSettings.save": "Speichern",
|
||||
"views.admin.Telegram.save": "Speichern",
|
||||
"views.admin.UserSettings.save": "Speichern",
|
||||
"views.admin.RoleAddressConfig.save": "Speichern",
|
||||
"views.admin.AccountSettings.save": "Speichern",
|
||||
"views.admin.Maintenance.save": "Speichern",
|
||||
"views.admin.Webhook.save": "Speichern",
|
||||
"views.admin.Maintenance.saveSuccess": "Erfolgreich gespeichert",
|
||||
"views.admin.UserOauth2Settings.successTip": "Erfolgreich gespeichert",
|
||||
"views.admin.IpBlacklistSettings.successTip": "Erfolgreich gespeichert",
|
||||
"views.admin.UserSettings.successTip": "Erfolgreich gespeichert",
|
||||
"views.admin.AccountSettings.successTip": "Erfolgreich gespeichert",
|
||||
"components.MailBox.saveToS3": "In S3 speichern",
|
||||
"components.MailContentRenderer.saveToS3": "In S3 speichern",
|
||||
"views.Index.saveToS3Success": "In S3 gespeichert",
|
||||
"components.MailBox.selectAll": "Alles auf dieser Seite auswählen",
|
||||
"components.SendBox.selectAll": "Alles auf dieser Seite auswählen",
|
||||
"views.admin.Account.selectAll": "Alles auf dieser Seite auswählen",
|
||||
"views.admin.AccountSettings.select_domain": "Domain auswählen",
|
||||
"views.admin.Account.selectedItems": "Ausgewählt",
|
||||
"views.index.SendMail.send": "Senden",
|
||||
"views.admin.SendMail.send": "Senden",
|
||||
"views.Index.sendbox": "Postausgang",
|
||||
"views.Admin.sendBox": "Postausgang",
|
||||
"views.user.AddressManagement.send_count": "Sendeanzahl",
|
||||
"views.admin.Account.send_count": "Sendeanzahl",
|
||||
"views.admin.UserAddressManagement.send_count": "Sendeanzahl",
|
||||
"views.Index.sendmail": "E-Mail senden",
|
||||
"views.Admin.sendMail": "E-Mail senden",
|
||||
"views.index.SendMail.send_balance": "Verbleibendes Sendeguthaben",
|
||||
"views.admin.Statistics.sendMailCount": "Anzahl gesendeter E-Mails",
|
||||
"views.admin.AccountSettings.send_mail_limit": "Sende-Limit",
|
||||
"views.user.UserLogin.sendVerificationCode": "Bestätigungscode senden",
|
||||
"views.Admin.senderAccess": "Sender-Zugriffssteuerung",
|
||||
"views.admin.SendMail.fromMailEmpty": "Die Absenderadresse ist leer",
|
||||
"views.index.AutoReply.sourcePrefix": "Absenderfilter",
|
||||
"components.AiExtractInfo.serviceLink": "Servicelink",
|
||||
"views.index.AutoReply.settings": "Einstellungen",
|
||||
"views.index.AccountSettings.showAddressCredential": "Adresszugangsdaten anzeigen",
|
||||
"components.MailBox.attachments": "Anhänge anzeigen",
|
||||
"components.MailBox.showHtmlMail": "HTML-Mail anzeigen",
|
||||
"components.MailContentRenderer.showHtmlMail": "HTML-Mail anzeigen",
|
||||
"views.admin.Account.showCredential": "E-Mail-Adresszugangsdaten anzeigen",
|
||||
"views.user.UserSettings.showPasskeyList": "Passkey-Liste anzeigen",
|
||||
"components.MailBox.showTextMail": "Textmail anzeigen",
|
||||
"components.MailContentRenderer.showTextMail": "Textmail anzeigen",
|
||||
"views.Index.enterSimpleMode": "Einfacher Modus",
|
||||
"components.MailContentRenderer.size": "Größe",
|
||||
"views.admin.Account.source_meta": "Quelle",
|
||||
"views.admin.AccountSettings.source_patterns": "Regex für Quelladresse (optional)",
|
||||
"views.admin.Maintenance.sqlStatement": "SQL-Anweisung (nur DELETE)",
|
||||
"views.user.UserOauth2Callback.stateNotMatch": "State stimmt nicht überein",
|
||||
"views.Admin.statistics": "Statistiken",
|
||||
"views.Header.status": "Status",
|
||||
"views.admin.IpBlacklistSettings.enable_whitelist_tip": "Strenger Modus: Nur IPs aus der Freigabeliste dürfen auf limitierte APIs zugreifen. Alle anderen IPs werden abgewiesen.",
|
||||
"views.index.AutoReply.subject": "Betreff",
|
||||
"views.index.SendMail.subject": "Betreff",
|
||||
"views.admin.SendMail.subject": "Betreff",
|
||||
"views.index.SendMail.subjectEmpty": "Der Betreff ist leer",
|
||||
"views.admin.SendMail.subjectEmpty": "Der Betreff ist leer",
|
||||
"components.AiExtractInfo.subscriptionLink": "Abonnement-Link",
|
||||
"views.user.AddressManagement.success": "Erfolg",
|
||||
"views.index.TelegramAddress.success": "Erfolg",
|
||||
"views.index.LocalAddress.success": "Erfolg",
|
||||
"views.admin.UserAddressManagement.success": "Erfolg",
|
||||
"components.WebhookComponent.successTip": "Erfolg",
|
||||
"components.MailBox.success": "Erfolg",
|
||||
"components.SendBox.success": "Erfolg",
|
||||
"views.index.AccountSettings.success": "Erfolg",
|
||||
"views.index.AutoReply.success": "Erfolg",
|
||||
"views.admin.AiExtractSettings.successTip": "Erfolg",
|
||||
"views.admin.Account.success": "Erfolg",
|
||||
"views.admin.SenderAccess.success": "Erfolg",
|
||||
"views.admin.UserManagement.success": "Erfolg",
|
||||
"views.admin.Telegram.successTip": "Erfolg",
|
||||
"views.admin.RoleAddressConfig.successTip": "Erfolg",
|
||||
"views.admin.Webhook.successTip": "Erfolg",
|
||||
"views.admin.CreateAccount.successTip": "Erfolgreich erstellt",
|
||||
"views.admin.Telegram.globalMailPushListTip": "Unterstützt chat_id von Privat-/Gruppen-/Kanal-Chats. Sende deinem Bot eine Nachricht und rufe dann diesen Link auf, um die chat_id zu sehen: https://api.telegram.org/bot<Ersetze durch dein BOT TOKEN>/getUpdates",
|
||||
"views.user.AddressManagement.targetUserEmail": "Zielbenutzer-E-Mail",
|
||||
"views.admin.Telegram.telegramAllowList": "Telegram-Freigabeliste (Chat-ID manuell eingeben)",
|
||||
"views.Admin.telegram": "Telegram-Bot",
|
||||
"views.admin.Telegram.miniAppUrl": "Telegram Mini App URL",
|
||||
"components.WebhookComponent.test": "Test",
|
||||
"views.index.SendMail.text": "Text",
|
||||
"views.admin.SendMail.text": "Text",
|
||||
"views.user.UserSettings.passordTip": "Der Server erhält nur den Hash des Passworts und niemals das Klartext-Passwort. Daher kann er dein Passwort weder sehen noch wiederherstellen. Wenn der Administrator die E-Mail-Verifizierung aktiviert, kannst du es im Inkognito-Modus zurücksetzen.",
|
||||
"views.index.LocalAddress.tip": "Diese Adressen werden in deinem Browser gespeichert und können verloren gehen, wenn du den Browser-Cache leerst.",
|
||||
"views.admin.UserOauth2Settings.tip": "Drittanbieter-Login verwendet automatisch die E-Mail-Adresse des Benutzers zur Registrierung eines Kontos (dieselbe E-Mail gilt als dasselbe Konto). Das Konto entspricht dem regulär registrierten Konto, und das Passwort kann auch über „Passwort vergessen“ gesetzt werden.",
|
||||
"views.admin.AccountSettings.send_mail_limit_tip": "Dies gilt für alle Sendekanäle. Verwende -1 für unbegrenzt und 0, um das Senden zu blockieren.",
|
||||
"views.admin.AccountSettings.create_address_subdomain_match_note": "Dies unterscheidet sich von RANDOM_SUBDOMAIN_DOMAINS: Dieser Schalter erlaubt API-Aufrufern, benutzerdefinierte Subdomains direkt anzugeben, während die zufällige Subdomain nur bei der Erstellung automatisch erzeugt wird.",
|
||||
"views.index.SendMail.tooLarge": "Datei zu groß; bitte eine Datei unter 1 MB hochladen.",
|
||||
"views.admin.SendMail.tooLarge": "Datei zu groß; bitte eine Datei unter 1 MB hochladen.",
|
||||
"views.common.Appearance.top": "oben",
|
||||
"views.user.AddressManagement.transferAddress": "Adresse übertragen",
|
||||
"views.user.AddressManagement.transferAddressTip": "Wenn du diese Adresse an einen anderen Benutzer überträgst, wird sie aus deinem Konto entfernt und in dessen Konto verschoben. Möchtest du die Adresse wirklich übertragen?",
|
||||
"views.common.Appearance.useSideMargin": "Seitliche Abstände links und rechts auf der Seite aktivieren",
|
||||
"views.admin.AiExtractSettings.manualInputPrompt": "Eingeben und mit Enter hinzufügen",
|
||||
"views.admin.UserOauth2Settings.manualInputPrompt": "Eingeben und mit Enter hinzufügen",
|
||||
"views.admin.Telegram.manualInputPrompt": "Eingeben und mit Enter hinzufügen",
|
||||
"views.admin.UserSettings.manualInputPrompt": "Eingeben und mit Enter hinzufügen",
|
||||
"views.admin.AccountSettings.manualInputPrompt": "Eingeben und mit Enter hinzufügen",
|
||||
"views.admin.Webhook.manualInputPrompt": "Eingeben und mit Enter hinzufügen",
|
||||
"views.admin.IpBlacklistSettings.manualInputPrompt": "Muster eingeben und Enter drücken, um es hinzuzufügen",
|
||||
"views.user.AddressManagement.unbindAddress": "Adressverknüpfung lösen",
|
||||
"views.index.TelegramAddress.unbindMailAddress": "Mailadressverknüpfung lösen",
|
||||
"views.index.LocalAddress.unbindMailAddress": "Mailadresszugangsdaten lösen",
|
||||
"components.MailBox.unselectAll": "Auswahl aufheben",
|
||||
"components.SendBox.unselectAll": "Auswahl aufheben",
|
||||
"views.admin.Account.unselectAll": "Auswahl aufheben",
|
||||
"views.admin.Account.updated_at": "Aktualisiert am",
|
||||
"views.user.UserSettings.updated_at": "Aktualisiert am",
|
||||
"components.WebhookComponent.urlMissing": "URL ist erforderlich",
|
||||
"views.common.Appearance.useIframeShowMail": "HTML-Mail per iframe anzeigen",
|
||||
"views.admin.CreateAccount.enableRandomSubdomain": "Zufällige Subdomain verwenden",
|
||||
"views.common.Login.enableRandomSubdomain": "Zufällige Subdomain verwenden",
|
||||
"views.admin.UserOauth2Settings.userEmailFormatTip": "Regex verwenden, um die E-Mail umzuwandeln. Beispiel: ^(.+)@old\\.com$ mit $1@new.com",
|
||||
"views.common.Appearance.useSimpleIndex": "Einfachen Index verwenden",
|
||||
"views.common.Appearance.useUTCDate": "UTC-Datum verwenden",
|
||||
"views.Header.user": "Benutzer",
|
||||
"views.Admin.user": "Benutzerkonten",
|
||||
"components.AddressSelect.userAddresses": "Mit Benutzerkonto verknüpfte Adressen",
|
||||
"views.Admin.loginViaUserAdmin": "Benutzer-Admin-Berechtigung",
|
||||
"views.admin.Statistics.userCount": "Benutzeranzahl",
|
||||
"views.admin.UserManagement.user_email": "E-Mail des Benutzerkontos",
|
||||
"views.index.AddressBar.userLogin": "Benutzerkonto-Anmeldung",
|
||||
"views.Admin.user_management": "Benutzerkonten verwalten",
|
||||
"views.Admin.user_settings": "Benutzerkonto-Einstellungen",
|
||||
"views.User.user_settings": "Benutzerkonto-Einstellungen",
|
||||
"components.AiExtractInfo.authCode": "Bestätigungscode",
|
||||
"views.user.UserLogin.verifyCode": "Bestätigungscode",
|
||||
"views.user.UserLogin.verifyCodeSent": "Bestätigungscode gesendet, läuft ab in {timeout} Sekunden",
|
||||
"views.admin.AccountSettings.verified_address_list": "Liste verifizierter Adressen (kann E-Mails über die interne cf-API senden)",
|
||||
"views.admin.UserSettings.verifyMailSender": "Mail-Absender verifizieren",
|
||||
"components.MailContentRenderer.attachments": "Anhänge anzeigen",
|
||||
"views.admin.Account.viewMails": "E-Mails anzeigen",
|
||||
"views.admin.Account.viewSendBox": "Postausgang anzeigen",
|
||||
"views.user.UserLogin.waitforVerifyCode": "Warten {timeout} Sekunden",
|
||||
"views.admin.Webhook.webhookAllowList": "Webhook-Freigabeliste (erlaubte Mailadresse eingeben und Enter drücken)",
|
||||
"views.admin.Webhook.notEnabled": "Webhook ist nicht aktiviert",
|
||||
"components.WebhookComponent.notEnabled": "Webhook ist für dich nicht aktiviert",
|
||||
"views.Index.webhookSettings": "Webhook-Einstellungen",
|
||||
"views.Admin.webhookSettings": "Webhook-Einstellungen",
|
||||
"views.admin.AiExtractSettings.disabledTip": "Wenn deaktiviert, verarbeitet die KI-Extraktion alle E-Mail-Adressen",
|
||||
"views.admin.AiExtractSettings.enableAllowListTip": "Wenn aktiviert, verarbeitet die KI-Extraktion nur E-Mails an Adressen auf der Freigabeliste",
|
||||
"views.admin.CreateAccount.randomSubdomainTip": "Wenn aktiviert, verwendet die erstellte Adresse eine zufällige Subdomain. Nur für den Empfang empfohlen. Erfordert einen Wildcard-MX-DNS-Eintrag auf der Basisdomain — siehe die Dokumentation zu zufälligen Subdomains.",
|
||||
"views.common.Login.randomSubdomainTip": "Wenn aktiviert, verwendet die erstellte Adresse eine zufällige Subdomain. Nur für den Empfang empfohlen. Erfordert einen Wildcard-MX-DNS-Eintrag auf der Basisdomain — siehe die Dokumentation zu zufälligen Subdomains.",
|
||||
"views.admin.AiExtractSettings.allowListTip": "Der Platzhalter * passt auf beliebige Zeichen; z. B. passt *{'@'}example.com auf alle Adressen der Domain example.com",
|
||||
"views.Admin.workerconfig": "Worker-Konfiguration",
|
||||
"views.admin.AccountSettings.create_address_subdomain_match_env_locked": "Die Worker-Umgebungsvariable ENABLE_CREATE_ADDRESS_SUBDOMAIN_MATCH ist derzeit false. Der gespeicherte Admin-Schalter kann geändert werden, wird aber erst wirksam, wenn die Umgebungsvariable aktiviert oder entfernt wird.",
|
||||
"views.common.Login.getNewEmailTip3": "Du kannst eine Domain aus der Dropdown-Liste auswählen.",
|
||||
"views.admin.AccountSettings.tip": "Die folgenden Mehrfachauswahlwerte können manuell eingegeben und mit Enter hinzugefügt werden",
|
||||
"components.MailBox.emptyInbox": "Dein Posteingang ist leer",
|
||||
"views.index.SendMail.fromName": "Dein Name und deine Adresse; Namen leer lassen, um die E-Mail-Adresse zu verwenden",
|
||||
"views.admin.SendMail.fromName": "Dein Name und deine Adresse; Namen leer lassen, um die E-Mail-Adresse zu verwenden",
|
||||
"components.AddressCredentialModal.addressCredential": "Adresszugangsdaten",
|
||||
"components.AddressCredentialModal.addressCredentialLabel": "Address JWT",
|
||||
"components.AddressCredentialModal.addressPassword": "Adresspasswort",
|
||||
"components.AddressCredentialModal.agentAccess": "AI Agent",
|
||||
"components.AddressCredentialModal.agentAccessTip": "Verwende dieses Postfach in einem AI Agent mit dem Address JWT und den parsed-mail APIs.",
|
||||
"components.AddressCredentialModal.agentConfig": "Agent-Konfiguration",
|
||||
"components.AddressCredentialModal.agentSkill": "Agent skill",
|
||||
"components.AddressCredentialModal.apiBase": "API-Basisadresse",
|
||||
"components.AddressCredentialModal.autoLoginLink": "Auto-Login-Link",
|
||||
"components.AddressCredentialModal.copyFailed": "Kopieren fehlgeschlagen",
|
||||
"components.AddressCredentialModal.copySection": "Kopieren",
|
||||
"components.AddressCredentialModal.copySuccess": "Kopiert",
|
||||
"components.AddressCredentialModal.currentAddress": "Aktuelle Adresse",
|
||||
"components.AddressCredentialModal.docs": "Dokumentation",
|
||||
"components.AddressCredentialModal.imapHost": "IMAP-Host",
|
||||
"components.AddressCredentialModal.imapPort": "IMAP-Port",
|
||||
"components.AddressCredentialModal.password": "Passwort",
|
||||
"components.AddressCredentialModal.plainOrProxyTls": "Klartext oder Proxy-TLS",
|
||||
"components.AddressCredentialModal.security": "Sicherheit",
|
||||
"components.AddressCredentialModal.smtpHost": "SMTP-Host",
|
||||
"components.AddressCredentialModal.smtpImapAccess": "SMTP / IMAP",
|
||||
"components.AddressCredentialModal.smtpImapTip": "Verwende diese Werte in Mail-Clients, nachdem der Administrator den SMTP/IMAP-Proxy konfiguriert hat. Als Passwort kannst du den hier angezeigten Address JWT oder ein vorhandenes Adresspasswort verwenden.",
|
||||
"components.AddressCredentialModal.smtpPort": "SMTP-Port",
|
||||
"components.AddressCredentialModal.starttls": "STARTTLS",
|
||||
"components.AddressCredentialModal.tip": "Verwende diese Zugangsdaten nur mit Clients und Agents, denen du vertraust.",
|
||||
"components.AddressCredentialModal.title": "Adresszugangsdaten & Verbindungsmethoden",
|
||||
"components.AddressCredentialModal.username": "Benutzername"
|
||||
}
|
||||
@@ -0,0 +1,621 @@
|
||||
export const esMessages = {
|
||||
"views.index.SimpleIndex.mailCount": "{current} / {total} correos",
|
||||
"views.admin.Statistics.activeAddressCount30days": "Cantidad de direcciones activas en 30 días",
|
||||
"views.admin.Statistics.activeAddressCount7days": "Cantidad de direcciones activas en 7 días",
|
||||
"views.Index.about": "Acerca de",
|
||||
"views.Admin.about": "Acerca de",
|
||||
"views.Header.accessHeader": "Contraseña de acceso",
|
||||
"views.Admin.account": "Direcciones de correo",
|
||||
"views.Index.accountSettings": "Configuración de la dirección",
|
||||
"views.Admin.account_settings": "Configuración de direcciones",
|
||||
"views.index.SimpleIndex.accountSettings": "Configuración de la dirección",
|
||||
"views.index.Attachment.action": "Acción",
|
||||
"views.admin.SenderAccess.action": "Acción",
|
||||
"views.user.UserSettings.actions": "Acciones",
|
||||
"views.user.AddressManagement.actions": "Acciones",
|
||||
"views.index.TelegramAddress.actions": "Acciones",
|
||||
"views.admin.Account.actions": "Acciones",
|
||||
"views.index.LocalAddress.actions": "Acciones",
|
||||
"views.admin.UserManagement.actions": "Acciones",
|
||||
"views.admin.AccountSettings.actions": "Acciones",
|
||||
"views.admin.AccountSettings.add": "Añadir",
|
||||
"views.admin.Maintenance.customSqlTip": "Añade sentencias SQL DELETE personalizadas para la limpieza programada. Solo se permite una sentencia DELETE por entrada.",
|
||||
"views.admin.Maintenance.addCustomSql": "Añadir SQL personalizada",
|
||||
"views.admin.UserOauth2Settings.addOauth2": "Añadir OAuth2",
|
||||
"components.AddressSelect.address": "Dirección",
|
||||
"views.user.AddressManagement.address": "Dirección",
|
||||
"views.index.TelegramAddress.address": "Dirección",
|
||||
"views.admin.SenderAccess.address": "Dirección",
|
||||
"views.index.LocalAddress.address": "Dirección",
|
||||
"views.admin.CreateAccount.address": "Dirección",
|
||||
"views.admin.AiExtractSettings.allowList": "Lista blanca de direcciones (introduce una dirección y pulsa Enter; admite comodines)",
|
||||
"views.admin.AccountSettings.send_address_block_list": "Palabras clave bloqueadas para enviar correo",
|
||||
"views.admin.AccountSettings.address_block_list": "Palabras clave bloqueadas para usuarios (el administrador puede omitirlo)",
|
||||
"views.index.SimpleIndex.addressCopied": "Dirección copiada correctamente",
|
||||
"views.admin.Statistics.addressCount": "Cantidad de direcciones",
|
||||
"views.admin.UserManagement.address_count": "Cantidad de direcciones",
|
||||
"views.User.address_management": "Gestión de direcciones",
|
||||
"views.admin.UserManagement.userAddressManagement": "Gestión de direcciones",
|
||||
"views.index.AddressBar.addressPassword": "Contraseña de la dirección",
|
||||
"views.admin.CreateAccount.addressPassword": "Contraseña de la dirección",
|
||||
"views.Admin.adminAccount": "Administrador",
|
||||
"views.Admin.accessHeader": "Contraseña de administrador",
|
||||
"views.Admin.loginViaPassword": "Inicio de sesión de administrador con contraseña",
|
||||
"views.admin.AiExtractSettings.title": "Configuración de extracción de correo con IA",
|
||||
"views.Admin.aiExtractSettings": "Configuración de extracción con IA",
|
||||
"views.admin.AccountSettings.match_all": "Todos",
|
||||
"views.admin.AccountSettings.create_address_subdomain_match": "Permitir coincidencia por sufijo de subdominio al crear direcciones",
|
||||
"views.admin.UserSettings.enableUserRegister": "Permitir registro de usuarios",
|
||||
"views.admin.AccountSettings.match_any": "Cualquiera",
|
||||
"views.Index.appearance": "Apariencia",
|
||||
"views.Admin.appearance": "Apariencia",
|
||||
"views.admin.IpBlacklistSettings.tip_scope": "Se aplica a: crear dirección, enviar correo, API externa de envío, registro de usuario y verificación de código",
|
||||
"views.index.AccountSettings.clearInboxConfirm": "¿Seguro que quieres vaciar todos los correos de tu bandeja de entrada?",
|
||||
"views.index.AccountSettings.clearSentItemsConfirm": "¿Seguro que quieres vaciar todos los correos enviados?",
|
||||
"views.admin.Account.multiClearInboxTip": "¿Seguro que quieres vaciar la bandeja de entrada de las direcciones seleccionadas?",
|
||||
"views.admin.Account.clearInboxTip": "¿Seguro que quieres vaciar la bandeja de entrada de este correo?",
|
||||
"views.admin.Account.multiClearSentItemsTip": "¿Seguro que quieres vaciar los enviados de las direcciones seleccionadas?",
|
||||
"views.admin.Account.clearSentItemsTip": "¿Seguro que quieres vaciar los enviados de este correo?",
|
||||
"views.admin.Account.multiDeleteTip": "¿Seguro que quieres eliminar las direcciones seleccionadas?",
|
||||
"views.index.Attachment.deleteConfirm": "¿Seguro que quieres eliminar este adjunto?",
|
||||
"views.admin.Account.deleteTip": "¿Seguro que quieres eliminar este correo?",
|
||||
"views.admin.SenderAccess.deleteTip": "¿Seguro que quieres eliminar esto?",
|
||||
"views.index.AccountSettings.deleteAccountConfirm": "¿Seguro que quieres eliminar esta dirección y todos sus correos?",
|
||||
"views.index.AccountSettings.logoutConfirm": "¿Seguro que quieres cerrar sesión?",
|
||||
"components.MailBox.deleteMailTip": "¿Seguro que quieres eliminar el correo?",
|
||||
"components.MailContentRenderer.deleteMailTip": "¿Seguro que quieres eliminar el correo?",
|
||||
"components.SendBox.deleteMailTip": "¿Seguro que quieres eliminar el correo?",
|
||||
"views.admin.AccountSettings.delete_rule_confirm": "¿Seguro que quieres eliminar esta regla?",
|
||||
"views.admin.UserManagement.deleteUserTip": "¿Seguro que quieres eliminar esta cuenta de usuario?",
|
||||
"views.Admin.logoutConfirmContent": "¿Seguro que quieres salir del panel de administración?",
|
||||
"views.user.UserSettings.logoutConfirm": "¿Seguro que quieres cerrar sesión?",
|
||||
"views.admin.IpBlacklistSettings.asn_blacklist": "Lista negra de organizaciones ASN",
|
||||
"views.admin.IpBlacklistSettings.tip_asn": "Organización ASN: bloquear por ISP/proveedor. Coincidencia de texto sin distinción de mayúsculas o regex.",
|
||||
"components.AiExtractInfo.authLink": "Enlace de autenticación",
|
||||
"views.admin.Maintenance.autoCleanup": "Limpieza automática",
|
||||
"components.MailBox.autoRefresh": "Actualización automática",
|
||||
"components.MailBox.backToList": "Volver a la lista",
|
||||
"views.common.Appearance.autoRefreshInterval": "Intervalo de actualización automática (s)",
|
||||
"views.Index.auto_reply": "Respuesta automática",
|
||||
"views.index.AutoReply.autoReply": "Respuesta automática",
|
||||
"views.common.Login.autoGeneratedName": "Nombre generado automáticamente",
|
||||
"views.admin.SenderAccess.balance": "Saldo",
|
||||
"views.admin.Maintenance.basicCleanup": "Limpieza básica",
|
||||
"views.user.AddressManagement.unbindAddressTip": "Antes de desvincular, cambia a esta dirección y guarda la credencial del correo.",
|
||||
"views.index.TelegramAddress.bind": "Vincular",
|
||||
"views.index.TelegramAddress.bindAddressSuccess": "Dirección vinculada correctamente",
|
||||
"views.index.LocalAddress.bindAddressSuccess": "Dirección vinculada correctamente",
|
||||
"views.User.bind_address": "Vincular dirección de correo",
|
||||
"views.admin.IpBlacklistSettings.enable_tip": "Bloquear IP que coincidan con la lista negra para las API limitadas",
|
||||
"views.admin.AccountSettings.fromBlockList": "Palabras bloqueadas para recibir correo",
|
||||
"views.admin.AccountSettings.block_receive_unknow_address_email": "Bloquear correos para direcciones desconocidas",
|
||||
"views.common.Appearance.bottom": "abajo",
|
||||
"views.admin.IpBlacklistSettings.fingerprint_blacklist": "Lista negra de huella del navegador",
|
||||
"views.admin.IpBlacklistSettings.tip_fingerprint": "Huella del navegador: bloquea por huella. Admite coincidencia exacta o regex.",
|
||||
"views.admin.AccountSettings.cancel": "Cancelar",
|
||||
"components.MailBox.cancelMultiAction": "Cancelar selección múltiple",
|
||||
"components.SendBox.cancelMultiAction": "Cancelar selección múltiple",
|
||||
"views.user.AddressManagement.changeMailAddress": "Cambiar dirección",
|
||||
"views.index.TelegramAddress.changeMailAddress": "Cambiar dirección de correo",
|
||||
"views.index.LocalAddress.changeMailAddress": "Cambiar dirección de correo",
|
||||
"views.index.AccountSettings.changePassword": "Cambiar contraseña",
|
||||
"views.admin.UserManagement.changeRole": "Cambiar Rol",
|
||||
"components.SendBox.showCode": "Cambiar vista del código original",
|
||||
"views.admin.Telegram.status": "Ver estado",
|
||||
"views.admin.AccountSettings.create_address_subdomain_match_follow_env_note": "Elegir \"Seguir variable de entorno\" elimina la anulación del administrador y vuelve al estado sin definir. El resultado efectivo sigue dependiendo de las variables del Worker y de las reglas de prioridad.",
|
||||
"views.admin.Maintenance.cleanupNow": "Limpiar ahora",
|
||||
"views.admin.Maintenance.cleanupSuccess": "Limpieza completada",
|
||||
"views.admin.Maintenance.addressCreateLabel": "Limpiar las direcciones creadas hace más de n días",
|
||||
"views.admin.Maintenance.emptyAddressLabel": "Limpiar las direcciones vacías de hace más de n días",
|
||||
"views.admin.Maintenance.inactiveAddressLabel": "Limpiar las direcciones inactivas de hace más de n días",
|
||||
"views.admin.Maintenance.mailBoxLabel": "Limpiar la bandeja de entrada de hace más de n días",
|
||||
"views.admin.Maintenance.sendBoxLabel": "Limpiar la bandeja de salida de hace más de n días",
|
||||
"views.admin.Maintenance.unboundAddressLabel": "Limpiar direcciones desvinculadas desde hace n días",
|
||||
"views.admin.Maintenance.mailUnknowLabel": "Limpiar los correos con destinatario desconocido de hace más de n días",
|
||||
"views.index.AccountSettings.clearInbox": "Vaciar bandeja de entrada",
|
||||
"views.admin.Account.clearInbox": "Vaciar bandeja de entrada",
|
||||
"views.index.AccountSettings.clearSentItems": "Vaciar enviados",
|
||||
"views.admin.Account.clearSentItems": "Vaciar enviados",
|
||||
"views.Header.title": "Cloudflare Correo Temporal",
|
||||
"views.admin.DatabaseManager.code_db_version": "Versión de BD requerida por el código",
|
||||
"views.user.UserOauth2Callback.codeNotFound": "código no encontrado",
|
||||
"views.admin.AccountSettings.config": "Configuración",
|
||||
"views.admin.RoleAddressConfig.roleConfigDesc": "Configura el número máximo de direcciones para cada rol. Los límites por rol tienen prioridad sobre la configuración global. Usa 0 para ilimitado.",
|
||||
"views.Admin.confirm": "Confirmar",
|
||||
"views.Admin.logoutConfirmTitle": "Confirmar cierre de sesión",
|
||||
"views.index.AccountSettings.confirmPassword": "Confirmar contraseña",
|
||||
"views.index.SendMail.content": "Contenido",
|
||||
"views.admin.SendMail.content": "Contenido",
|
||||
"views.index.SendMail.contentEmpty": "El contenido está vacío",
|
||||
"views.admin.SendMail.contentEmpty": "El contenido está vacío",
|
||||
"components.AddressSelect.copied": "Copiado",
|
||||
"components.AiExtractInfo.copySuccess": "Copiado correctamente",
|
||||
"components.AddressSelect.copy": "Copiar",
|
||||
"views.index.SimpleIndex.copyAddress": "Copiar",
|
||||
"components.AiExtractInfo.copyFailed": "Error al copiar",
|
||||
"views.Footer.copyright": "Derechos de autor",
|
||||
"views.Admin.account_create": "Crear dirección de correo",
|
||||
"views.admin.CreateAccount.creatNewEmail": "Crear nuevo correo",
|
||||
"views.common.Login.getNewEmail": "Crear nuevo correo",
|
||||
"views.user.AddressManagement.create_or_bind": "Crear o vincular",
|
||||
"views.index.LocalAddress.create_or_bind": "Crear o vincular",
|
||||
"views.user.UserSettings.createPasskey": "Crear passkey",
|
||||
"views.admin.UserManagement.createUser": "Crear cuenta de usuario",
|
||||
"views.user.UserSettings.created_at": "Creado el",
|
||||
"views.admin.Account.created_at": "Creado el",
|
||||
"views.admin.SenderAccess.created_at": "Creado el",
|
||||
"views.admin.UserManagement.created_at": "Creado el",
|
||||
"views.common.Login.credentialLogin": "Inicio de sesión con credencial",
|
||||
"views.admin.DatabaseManager.current_db_version": "Versión actual de la BD",
|
||||
"views.user.UserBar.currentUser": "Cuenta de usuario actual",
|
||||
"views.admin.UserManagement.roleDonotExist": "El rol actual no existe",
|
||||
"views.admin.Maintenance.customSqlCleanup": "Limpieza SQL personalizada",
|
||||
"views.admin.AccountSettings.send_mail_daily_limit": "Límite diario",
|
||||
"views.admin.AccountSettings.send_mail_daily_limit_invalid": "El límite diario debe ser un entero mayor o igual que -1",
|
||||
"views.admin.IpBlacklistSettings.tip_daily_limit": "Límite diario: restringe el número máximo de solicitudes por IP al día (1-1000000).",
|
||||
"views.admin.IpBlacklistSettings.daily_request_limit": "Límite diario de solicitudes",
|
||||
"views.Header.dark": "Oscuro",
|
||||
"views.Admin.database": "Base de datos",
|
||||
"views.admin.DatabaseManager.need_initialization_tip": "Es necesario inicializar la base de datos. Inicialízala primero.",
|
||||
"views.admin.DatabaseManager.initializationSuccess": "Base de datos inicializada correctamente",
|
||||
"views.admin.DatabaseManager.migrationSuccess": "Base de datos migrada correctamente",
|
||||
"views.admin.DatabaseManager.need_migration_tip": "Es necesario migrar la base de datos. Realiza la migración primero.",
|
||||
"components.MailBox.delete": "Eliminar",
|
||||
"components.MailContentRenderer.delete": "Eliminar",
|
||||
"components.SendBox.delete": "Eliminar",
|
||||
"views.index.Attachment.delete": "Eliminar",
|
||||
"views.admin.Account.delete": "Eliminar",
|
||||
"views.admin.UserOauth2Settings.delete": "Eliminar",
|
||||
"views.admin.SenderAccess.delete": "Eliminar",
|
||||
"views.admin.UserManagement.delete": "Eliminar",
|
||||
"views.admin.AccountSettings.delete_rule": "Eliminar",
|
||||
"views.admin.Maintenance.deleteCustomSql": "Eliminar",
|
||||
"views.index.AccountSettings.deleteAccount": "Eliminar dirección de correo",
|
||||
"views.admin.Account.deleteAccount": "Eliminar dirección de correo",
|
||||
"views.user.UserSettings.deletePasskey": "Eliminar passkey",
|
||||
"views.admin.AccountSettings.delete_success": "Eliminado correctamente",
|
||||
"views.admin.UserManagement.deleteUser": "Eliminar cuenta de usuario",
|
||||
"views.index.Attachment.deleteSuccess": "Eliminado correctamente",
|
||||
"views.admin.SenderAccess.disable": "Deshabilitar",
|
||||
"views.Admin.loginViaDisabledCheck": "Comprobación de contraseña deshabilitada",
|
||||
"views.common.Appearance.preferShowTextMail": "Mostrar correo en texto por defecto",
|
||||
"views.admin.AccountSettings.domain_list": "Lista de dominios (opcional)",
|
||||
"views.admin.AccountSettings.source_patterns_tip": "La lista de dominios filtra por destinatario y la regex de origen por remitente. Ambas condiciones deben cumplirse para reenviar (lógica AND). Deja alguna vacía para omitirla.",
|
||||
"views.admin.UserManagement.domains": "Dominios",
|
||||
"views.index.Attachment.download": "Descargar",
|
||||
"components.MailBox.downloadMail": "Descargar Correo",
|
||||
"components.MailContentRenderer.downloadMail": "Descargar Correo",
|
||||
"views.admin.Maintenance.sqlNamePlaceholder": "p. ej., limpiar registros antiguos",
|
||||
"views.admin.Maintenance.sqlPlaceholder": "p. ej., DELETE FROM raw_mails WHERE source GLOB '*{'@'}example.com' AND created_at < datetime('now', '-3 day')",
|
||||
"views.admin.AccountSettings.source_patterns_placeholder": "p. ej., gmail.com",
|
||||
"views.admin.AccountSettings.forwarding_rule_warning": "Cada regla se ejecuta de forma independiente. La dirección de reenvío debe estar verificada.",
|
||||
"views.index.SendMail.edit": "Editar",
|
||||
"views.admin.SendMail.edit": "Editar",
|
||||
"views.user.UserLogin.email": "Correo electrónico",
|
||||
"views.admin.UserManagement.email": "Correo electrónico",
|
||||
"views.common.Login.email": "Correo electrónico",
|
||||
"views.common.Login.credential": "Credencial de dirección de correo",
|
||||
"views.common.Login.emailPasswordRequired": "El correo y la contraseña son obligatorios",
|
||||
"views.admin.UserSettings.emailCheckRegex": "Regex de verificación de correo (p. ej., ^[^.]+{'@'}.+$ para impedir puntos antes de {'@'})",
|
||||
"views.admin.AccountSettings.email_forwarding_config": "Configuración de reenvío de correo",
|
||||
"views.admin.UserOauth2Settings.userEmailFormat": "Patrón regex del correo",
|
||||
"views.Admin.mails": "Correos",
|
||||
"views.index.AutoReply.sourcePrefixPlaceholder": "Vacío=todos, prefijo o /regex/",
|
||||
"components.WebhookComponent.enable": "Habilitar",
|
||||
"views.admin.UserOauth2Settings.enable": "Habilitar",
|
||||
"views.admin.SenderAccess.enable": "Habilitar",
|
||||
"views.admin.Telegram.enable": "Habilitar",
|
||||
"views.admin.UserSettings.enable": "Habilitar",
|
||||
"views.admin.AiExtractSettings.enableAllowList": "Habilitar Lista blanca de direcciones",
|
||||
"views.admin.Webhook.enableAllowList": "Habilitar lista de permitidos (restringe el webhook a direcciones específicas)",
|
||||
"views.index.AutoReply.enableAutoReply": "Habilitar respuesta automática",
|
||||
"views.admin.Maintenance.cronTip": "Para activar la limpieza por cron, configura [crons] en el worker. Consulta la documentación; 0 días significa limpiar todo.",
|
||||
"views.admin.IpBlacklistSettings.enable_daily_limit": "Habilitar Límite diario de solicitudes",
|
||||
"views.admin.UserSettings.enableEmailCheckRegex": "Habilitar regex de verificación de correo",
|
||||
"views.admin.UserOauth2Settings.enableEmailFormat": "Habilitar formato de correo",
|
||||
"views.admin.Telegram.enableGlobalMailPush": "Habilitar envío global de correos (introduce manualmente el Chat ID de Telegram)",
|
||||
"views.admin.IpBlacklistSettings.enable_ip_blacklist": "Habilitar Lista negra de IP",
|
||||
"views.admin.IpBlacklistSettings.enable_ip_whitelist": "Habilitar Lista blanca de IP (estricto)",
|
||||
"views.admin.UserOauth2Settings.enableMailAllowList": "Habilitar lista blanca de direcciones de correo (editable manualmente)",
|
||||
"views.admin.UserSettings.enableMailAllowList": "Habilitar lista blanca de direcciones de correo (editable manualmente)",
|
||||
"views.admin.UserSettings.enableMailVerify": "Habilitar verificación de correo (la dirección remitente debe existir en el sistema, tener saldo y poder enviar normalmente)",
|
||||
"views.admin.Telegram.enableTelegramAllowList": "Habilitar lista blanca de Telegram (introduce manualmente el Chat ID)",
|
||||
"views.admin.IpBlacklistSettings.asn_blacklist_placeholder": "Introduce organización ASN (p. ej., Google, Amazon)",
|
||||
"views.admin.IpBlacklistSettings.fingerprint_blacklist_placeholder": "Introduce el ID de huella (p. ej., a1b2c3d4e5f6g7h8)",
|
||||
"views.admin.IpBlacklistSettings.daily_request_limit_placeholder": "Introduce el límite (p. ej., 1000)",
|
||||
"views.admin.IpBlacklistSettings.ip_blacklist_placeholder": "Introduce el patrón (p. ej., 192.168.1 o ^10\\.0\\.0\\.5$)",
|
||||
"views.common.Login.bindUserAddressError": "Error al vincular la dirección al usuario",
|
||||
"views.admin.IpBlacklistSettings.ip_whitelist_placeholder": "IP exacta (p. ej., 1.2.3.4) o regex anclada (p. ej., ^192\\.168\\.1\\.\\d+$)",
|
||||
"views.index.SimpleIndex.exitSimpleIndex": "Salir del modo simple",
|
||||
"components.MailBox.keywordQueryTip": "Filtrar página actual",
|
||||
"views.admin.AccountSettings.create_address_subdomain_match_follow_env": "Seguir variable de entorno",
|
||||
"views.admin.AccountSettings.create_address_subdomain_match_force_disable": "Forzar deshabilitación",
|
||||
"views.admin.AccountSettings.create_address_subdomain_match_force_enable": "Forzar habilitación",
|
||||
"views.user.UserLogin.forgotPassword": "Olvidé mi contraseña",
|
||||
"components.MailBox.forwardMail": "Reenviar",
|
||||
"components.MailContentRenderer.forward": "Reenviar",
|
||||
"views.admin.AccountSettings.forward_address": "Dirección de reenvío",
|
||||
"views.admin.AccountSettings.forward_address_required": "La dirección de reenvío es obligatoria",
|
||||
"views.admin.AccountSettings.forward_placeholder": "forward@example.com",
|
||||
"components.MailContentRenderer.fullscreen": "Pantalla completa",
|
||||
"views.common.Login.generateName": "Generar nombre aleatorio",
|
||||
"views.admin.Telegram.globalMailPushList": "Lista global de chat ID para envío de correos",
|
||||
"views.common.Appearance.globalTabplacement": "Posición global de pestañas",
|
||||
"views.common.Login.help": "Ayuda",
|
||||
"views.Header.home": "Inicio",
|
||||
"views.index.SendMail.html": "HTML",
|
||||
"views.admin.SendMail.html": "HTML",
|
||||
"views.admin.UserOauth2Settings.icon": "Icono (SVG, asegúrate de que la fuente sea fiable)",
|
||||
"views.admin.CreateAccount.enablePrefix": "Si se habilita el prefijo",
|
||||
"views.common.AdminContact.adminContact": "Si necesitas ayuda, contacta con el administrador ({msg})",
|
||||
"views.admin.Telegram.init": "Inicializar",
|
||||
"views.admin.DatabaseManager.init": "Inicializar base de datos",
|
||||
"views.admin.AccountSettings.regex_invalid": "Patrón regex no válido",
|
||||
"views.Admin.ipBlacklistSettings": "Lista negra de IP",
|
||||
"views.admin.IpBlacklistSettings.ip_blacklist": "Patrones de lista negra de IP",
|
||||
"views.admin.IpBlacklistSettings.title": "Configuración de lista negra de IP",
|
||||
"views.admin.IpBlacklistSettings.tip_ip": "Lista negra de IP: admite coincidencia por texto (p. ej., \"192.168.1\") o por regex (p. ej., \"^10\\.0\\.0\\.5$\").",
|
||||
"views.admin.IpBlacklistSettings.whitelist_empty_warning": "La lista blanca de IP está activada, pero vacía. El servidor la ignorará para evitar bloquear el acceso. Añade al menos una entrada antes de activarla.",
|
||||
"views.admin.IpBlacklistSettings.ip_whitelist": "Lista blanca de IP Patrones",
|
||||
"views.admin.IpBlacklistSettings.tip_whitelist": "Lista blanca de IP: modo estricto. Las entradas simples deben coincidir exactamente con la IP (sin subcadenas). Usa regex ancladas (^...$) para rangos. Las IP permitidas omiten la comprobación de la lista negra.",
|
||||
"views.admin.SenderAccess.is_enabled": "Habilitado",
|
||||
"views.admin.Account.itemCount": "itemCount",
|
||||
"views.admin.SenderAccess.itemCount": "itemCount",
|
||||
"views.admin.UserManagement.itemCount": "itemCount",
|
||||
"views.user.UserMailBox.addressQueryTip": "Déjalo vacío para consultar todas las direcciones",
|
||||
"views.admin.Account.addressQueryTip": "Déjalo vacío para consultar todas las direcciones",
|
||||
"views.admin.Mails.addressQueryTip": "Déjalo vacío para consultar todas las direcciones",
|
||||
"views.common.Appearance.left": "izquierda",
|
||||
"views.common.Login.getNewEmailTip2": "Si lo dejas vacío, se generará una dirección aleatoria.",
|
||||
"views.Header.light": "Claro",
|
||||
"views.admin.IpBlacklistSettings.enable_daily_limit_tip": "Limitar el número de solicitudes API por IP y día",
|
||||
"components.AddressSelect.localAddresses": "Direcciones locales",
|
||||
"views.common.Login.bindUserInfo": "Si ya has iniciado sesión, al entrar sin vincular correo o crear uno nuevo se vinculará al usuario actual",
|
||||
"views.user.UserOauth2Callback.logging": "Iniciando sesión...",
|
||||
"views.user.UserLogin.login": "Iniciar sesión",
|
||||
"views.common.Login.login": "Iniciar sesión",
|
||||
"views.common.Login.loginAndBind": "Iniciar sesión y vincular",
|
||||
"views.Admin.loginMethod": "Método de inicio de sesión",
|
||||
"views.user.UserBar.fetchUserSettingsError": "La contraseña de inicio de sesión es incorrecta o la cuenta no existe; también puede tratarse de un problema de red. Inténtalo de nuevo más tarde.",
|
||||
"views.user.UserLogin.loginWith": "Iniciar sesión con {provider}",
|
||||
"views.user.UserLogin.loginWithPasskey": "Iniciar sesión con passkey",
|
||||
"views.user.UserSettings.logout": "Cerrar sesión",
|
||||
"views.user.BindAddress.logout": "Cerrar sesión",
|
||||
"views.Admin.logout": "Cerrar sesión",
|
||||
"views.index.AccountSettings.logout": "Cerrar sesión",
|
||||
"views.Admin.logoutSuccess": "Sesión cerrada correctamente",
|
||||
"views.admin.UserOauth2Settings.mailAllowList": "Lista blanca de direcciones de correo",
|
||||
"views.admin.UserSettings.mailAllowList": "Lista blanca de direcciones de correo",
|
||||
"views.index.SimpleIndex.addressCredential": "Credencial de dirección de correo",
|
||||
"views.index.AddressBar.addressCredential": "Credencial de dirección de correo",
|
||||
"views.admin.Account.addressCredential": "Credencial de dirección de correo",
|
||||
"views.admin.CreateAccount.addressCredential": "Credencial de dirección de correo",
|
||||
"views.index.AddressBar.fetchAddressError": "La credencial del correo no es válida o la cuenta no existe; también puede ser un problema de red. Inténtalo más tarde.",
|
||||
"views.Index.mailbox": "Bandeja de entrada",
|
||||
"views.User.user_mail_box_tab": "Bandeja de entrada",
|
||||
"views.user.AddressManagement.mail_count": "Cantidad de correos",
|
||||
"views.admin.Account.mail_count": "Cantidad de correos",
|
||||
"views.admin.Statistics.mailCount": "Cantidad de correos",
|
||||
"views.admin.UserAddressManagement.mail_count": "Cantidad de correos",
|
||||
"views.index.SimpleIndex.deleteSuccess": "Correo eliminado correctamente",
|
||||
"views.user.UserLogin.cannotForgotPassword": "La verificación por correo o el registro está desactivado; no se puede restablecer la contraseña. Contacta con el administrador.",
|
||||
"views.Admin.mailWebhook": "Webhook de correo",
|
||||
"views.common.Appearance.mailboxSplitSize": "Ancho de la lista izquierda en la vista de buzón de dos columnas",
|
||||
"views.common.Appearance.mailListView": "Vista de lista del buzón de ancho completo",
|
||||
"views.common.Appearance.mailListPreviewLineClamp": "Líneas de vista previa del cuerpo",
|
||||
"views.common.Appearance.off": "Desactivado",
|
||||
"views.index.SimpleIndex.refreshSuccess": "Correos actualizados correctamente",
|
||||
"views.Admin.unknow": "Correos con destinatario desconocido",
|
||||
"views.Admin.maintenance": "Mantenimiento",
|
||||
"views.index.AddressBar.addressManage": "Gestionar",
|
||||
"views.admin.AccountSettings.source_match_mode": "Modo de coincidencia",
|
||||
"views.admin.RoleAddressConfig.maxAddressCount": "Cantidad máxima de direcciones (0 = ilimitado)",
|
||||
"views.admin.UserSettings.maxAddressCount": "Número máximo de direcciones que se pueden vincular (0 = ilimitado)",
|
||||
"views.Header.menu": "Menú",
|
||||
"views.admin.DatabaseManager.migration": "Migrar base de datos",
|
||||
"views.admin.SenderAccess.modify": "Modificar",
|
||||
"views.admin.AccountSettings.send_mail_monthly_limit": "Límite mensual",
|
||||
"views.admin.AccountSettings.send_mail_monthly_limit_invalid": "El límite mensual debe ser un entero mayor o igual que -1",
|
||||
"components.MailBox.multiAction": "Acción múltiple",
|
||||
"components.SendBox.multiAction": "Acción múltiple",
|
||||
"views.admin.Account.multiClearInbox": "Vaciar varias bandejas",
|
||||
"views.admin.Account.multiClearSentItems": "Vaciar varios enviados",
|
||||
"views.admin.Account.multiDelete": "Eliminar en lote",
|
||||
"views.user.AddressManagement.name": "Nombre",
|
||||
"views.index.AutoReply.name": "Nombre",
|
||||
"views.admin.Account.name": "Nombre",
|
||||
"views.admin.UserOauth2Settings.name": "Nombre",
|
||||
"views.admin.UserAddressManagement.name": "Nombre",
|
||||
"views.admin.Maintenance.sqlName": "Nombre",
|
||||
"views.index.AccountSettings.newPassword": "Nueva contraseña",
|
||||
"views.admin.Account.newPassword": "Nueva contraseña",
|
||||
"components.MailBox.nextMail": "Siguiente",
|
||||
"views.index.SimpleIndex.nextPage": "Siguiente",
|
||||
"views.admin.AccountSettings.noLimitSendAddressList": "Lista de direcciones sin límite de saldo",
|
||||
"views.index.SimpleIndex.noMails": "No se encontraron correos",
|
||||
"views.admin.RoleAddressConfig.noRolesAvailable": "No hay roles disponibles en la configuración del sistema",
|
||||
"views.index.SendMail.requestAccessTip": "El acceso y el saldo de envío pertenecen a la dirección actual, no a la cuenta de usuario. Solicita acceso para esta dirección o contacta con el administrador.",
|
||||
"components.SendBox.emptySent": "No hay correos enviados",
|
||||
"views.admin.RoleAddressConfig.notConfigured": "No configurado (usar configuración global)",
|
||||
"views.Admin.userOauth2Settings": "Configuración de OAuth2",
|
||||
"views.admin.UserOauth2Settings.oauth2Type": "Tipo de OAuth2",
|
||||
"views.Header.ok": "Aceptar",
|
||||
"views.Admin.ok": "Aceptar",
|
||||
"views.index.AddressBar.ok": "Aceptar",
|
||||
"views.admin.SenderAccess.ok": "Aceptar",
|
||||
"views.common.Login.ok": "Aceptar",
|
||||
"views.admin.AccountSettings.create_address_subdomain_match_tip": "Solo afecta la validación de dominio en /api/new_address y /admin/new_address. Ejemplo: activado, foo.example.com puede coincidir con example.com.",
|
||||
"components.AiExtractInfo.open": "Abrir",
|
||||
"views.index.AddressBar.linkWithAddressCredential": "Abrir enlace de inicio de sesión automático por correo",
|
||||
"views.admin.CreateAccount.linkWithAddressCredential": "Abrir enlace de inicio de sesión automático por correo",
|
||||
"views.index.SendMail.options": "Opciones",
|
||||
"views.admin.SendMail.options": "Opciones",
|
||||
"components.AiExtractInfo.otherLink": "Otro enlace",
|
||||
"views.user.UserSettings.passkeyCreated": "Passkey creada correctamente",
|
||||
"views.user.UserSettings.passkey_name": "Nombre de la passkey",
|
||||
"views.user.UserLogin.password": "Contraseña",
|
||||
"views.admin.UserManagement.password": "Contraseña",
|
||||
"views.common.Login.password": "Contraseña",
|
||||
"views.index.AccountSettings.passwordChanged": "Contraseña cambiada correctamente",
|
||||
"views.common.Login.passwordLogin": "Inicio de sesión con contraseña",
|
||||
"views.admin.Account.passwordResetSuccess": "Contraseña restablecida correctamente",
|
||||
"views.index.AccountSettings.passwordMismatch": "Las contraseñas no coinciden",
|
||||
"views.index.SendMail.successSend": "Revisa la bandeja de salida. Si falla, comprueba tu saldo o inténtalo más tarde.",
|
||||
"views.admin.SendMail.successSend": "Revisa la bandeja de salida. Si falla, inténtalo de nuevo más tarde.",
|
||||
"views.user.UserLogin.pleaseCompleteTurnstile": "Completa el turnstile",
|
||||
"views.index.SimpleIndex.addressCredentialTip": "Copia la credencial de la dirección de correo; podrás usarla para iniciar sesión.",
|
||||
"views.index.AddressBar.addressCredentialTip": "Copia la credencial de la dirección de correo; podrás usarla para iniciar sesión en tu cuenta de correo.",
|
||||
"views.admin.Account.addressCredentialTip": "Copia la credencial de la dirección de correo; podrás usarla para iniciar sesión en tu cuenta de correo.",
|
||||
"views.admin.CreateAccount.addressCredentialTip": "Copia la credencial de la dirección de correo; podrás usarla para iniciar sesión en tu cuenta de correo.",
|
||||
"views.Admin.accessTip": "Introduce la contraseña de administrador",
|
||||
"views.Header.accessTip": "Introduce la contraseña de acceso correcta",
|
||||
"views.admin.AccountSettings.address_block_list_placeholder": "Introduce las palabras clave que quieres bloquear",
|
||||
"views.user.UserSettings.renamePasskeyNamePlaceholder": "Introduce el nuevo nombre de la passkey",
|
||||
"views.user.UserSettings.passkeyNamePlaceholder": "Introduce el nombre de la passkey o déjalo vacío para generarlo aleatoriamente",
|
||||
"views.admin.CreateAccount.fillInAllFields": "Rellena todos los campos",
|
||||
"views.admin.SendBox.queryTip": "Introduce la dirección a consultar; vacío para todas",
|
||||
"views.user.UserLogin.pleaseInputCode": "Introduce el código",
|
||||
"views.admin.UserManagement.pleaseInput": "Introduce la información completa",
|
||||
"views.user.UserLogin.pleaseInputEmail": "Introduce el correo",
|
||||
"views.user.UserLogin.pleaseInput": "Introduce el correo y la contraseña",
|
||||
"views.admin.Maintenance.tip": "Introduce los días",
|
||||
"views.common.Login.getNewEmailTip1": "Introduce el correo que quieres usar. Solo se permite: ",
|
||||
"views.common.Login.credentialInput": "Introduce la credencial de la dirección de correo",
|
||||
"views.admin.SenderAccess.modalTip": "Introduce el saldo de envío",
|
||||
"views.user.UserLogin.pleaseLogin": "Inicia sesión",
|
||||
"views.common.Login.pleaseGetNewEmail": "Inicia sesión o pulsa \"Crear nuevo correo\" para obtener una nueva dirección.",
|
||||
"components.WebhookComponent.fillInDemoTip": "Modifica la URL y el resto de ajustes por los tuyos",
|
||||
"components.SendBox.pleaseSelectMail": "Selecciona un correo para verlo.",
|
||||
"views.admin.Account.pleaseSelectAddress": "Selecciona una dirección",
|
||||
"components.MailBox.pleaseSelectMail": "Selecciona un correo",
|
||||
"views.admin.UserManagement.prefix": "Prefijo",
|
||||
"components.WebhookComponent.presets": "Plantillas",
|
||||
"views.index.SendMail.preview": "Vista previa",
|
||||
"views.admin.SendMail.preview": "Vista previa",
|
||||
"views.admin.UserOauth2Settings.iconPreview": "Vista previa",
|
||||
"components.MailBox.prevMail": "Anterior",
|
||||
"views.index.SimpleIndex.prevPage": "Anterior",
|
||||
"components.MailBox.query": "Consultar",
|
||||
"views.user.UserMailBox.query": "Consultar",
|
||||
"views.Index.query": "Consultar",
|
||||
"views.admin.SendBox.query": "Consultar",
|
||||
"views.admin.Account.query": "Consultar",
|
||||
"views.admin.SenderAccess.query": "Consultar",
|
||||
"views.admin.UserManagement.query": "Consultar",
|
||||
"views.admin.Mails.query": "Consultar",
|
||||
"views.Admin.qucickSetup": "Configuración rápida",
|
||||
"views.index.SendMail.toMailEmpty": "La dirección del destinatario está vacía",
|
||||
"views.admin.SendMail.toMailEmpty": "La dirección del destinatario está vacía",
|
||||
"views.index.SendMail.toName": "Nombre y dirección del destinatario; deja el nombre vacío para usar el correo",
|
||||
"views.admin.SendMail.toName": "Nombre y dirección del destinatario; deja el nombre vacío para usar el correo",
|
||||
"components.MailBox.refresh": "Actualizar",
|
||||
"components.Turnstile.refresh": "Actualizar",
|
||||
"components.SendBox.refresh": "Actualizar",
|
||||
"views.index.SimpleIndex.refreshMails": "Actualizar",
|
||||
"components.MailBox.refreshAfter": "Actualizar en {msg} segundos",
|
||||
"views.index.SimpleIndex.refreshAfter": "Actualizar en {msg} segundos",
|
||||
"views.admin.AccountSettings.regex_too_long": "El patrón regex es demasiado largo (máx. 200 caracteres)",
|
||||
"views.user.UserLogin.register": "Registrarse",
|
||||
"views.user.UserSettings.renamePasskey": "Renombrar passkey",
|
||||
"views.admin.UserOauth2Settings.userEmailReplace": "Plantilla de reemplazo",
|
||||
"components.MailBox.reply": "Responder",
|
||||
"components.MailContentRenderer.reply": "Responder",
|
||||
"views.index.SendMail.requestAccess": "Solicitar acceso para esta dirección",
|
||||
"views.user.UserLogin.resetPassword": "Restablecer Contraseña",
|
||||
"views.admin.Account.resetPassword": "Restablecer Contraseña",
|
||||
"views.admin.UserManagement.resetPassword": "Restablecer Contraseña",
|
||||
"views.index.SendMail.rich text": "Texto enriquecido",
|
||||
"views.admin.SendMail.rich text": "Texto enriquecido",
|
||||
"views.common.Appearance.right": "derecha",
|
||||
"views.admin.UserManagement.role": "Rol",
|
||||
"views.admin.RoleAddressConfig.role": "Rol",
|
||||
"views.Admin.roleAddressConfig": "Configuración de direcciones por rol",
|
||||
"views.admin.AccountSettings.rule_index": "Regla",
|
||||
"views.Index.s3Attachment": "Adjunto S3",
|
||||
"components.WebhookComponent.save": "Guardar",
|
||||
"views.index.AutoReply.save": "Guardar",
|
||||
"views.admin.AiExtractSettings.save": "Guardar",
|
||||
"views.admin.UserOauth2Settings.save": "Guardar",
|
||||
"views.admin.IpBlacklistSettings.save": "Guardar",
|
||||
"views.admin.Telegram.save": "Guardar",
|
||||
"views.admin.UserSettings.save": "Guardar",
|
||||
"views.admin.RoleAddressConfig.save": "Guardar",
|
||||
"views.admin.AccountSettings.save": "Guardar",
|
||||
"views.admin.Maintenance.save": "Guardar",
|
||||
"views.admin.Webhook.save": "Guardar",
|
||||
"views.admin.Maintenance.saveSuccess": "Guardado correctamente",
|
||||
"views.admin.UserOauth2Settings.successTip": "Guardado correcto",
|
||||
"views.admin.IpBlacklistSettings.successTip": "Guardado correcto",
|
||||
"views.admin.UserSettings.successTip": "Guardado correcto",
|
||||
"views.admin.AccountSettings.successTip": "Guardado correcto",
|
||||
"components.MailBox.saveToS3": "Guardar en S3",
|
||||
"components.MailContentRenderer.saveToS3": "Guardar en S3",
|
||||
"views.Index.saveToS3Success": "guardado en S3 correctamente",
|
||||
"components.MailBox.selectAll": "Seleccionar toda esta página",
|
||||
"components.SendBox.selectAll": "Seleccionar toda esta página",
|
||||
"views.admin.Account.selectAll": "Seleccionar toda esta página",
|
||||
"views.admin.AccountSettings.select_domain": "Seleccionar dominio",
|
||||
"views.admin.Account.selectedItems": "Seleccionado",
|
||||
"views.index.SendMail.send": "Enviar",
|
||||
"views.admin.SendMail.send": "Enviar",
|
||||
"views.Index.sendbox": "Bandeja de salida",
|
||||
"views.Admin.sendBox": "Bandeja de salida",
|
||||
"views.user.AddressManagement.send_count": "Cantidad enviada",
|
||||
"views.admin.Account.send_count": "Cantidad enviada",
|
||||
"views.admin.UserAddressManagement.send_count": "Cantidad enviada",
|
||||
"views.Index.sendmail": "Enviar correo",
|
||||
"views.Admin.sendMail": "Enviar correo",
|
||||
"views.index.SendMail.send_balance": "Saldo restante de envío",
|
||||
"views.admin.Statistics.sendMailCount": "Cantidad de correos enviados",
|
||||
"views.admin.AccountSettings.send_mail_limit": "Límite de envío",
|
||||
"views.user.UserLogin.sendVerificationCode": "Enviar código de verificación",
|
||||
"views.Admin.senderAccess": "Control de permisos de envío",
|
||||
"views.admin.SendMail.fromMailEmpty": "La dirección del remitente está vacía",
|
||||
"views.index.AutoReply.sourcePrefix": "Filtro de remitente",
|
||||
"components.AiExtractInfo.serviceLink": "Enlace del servicio",
|
||||
"views.index.AutoReply.settings": "Configuración",
|
||||
"views.index.AccountSettings.showAddressCredential": "Mostrar credencial de la dirección",
|
||||
"components.MailBox.attachments": "Mostrar adjuntos",
|
||||
"components.MailBox.showHtmlMail": "Mostrar correo HTML",
|
||||
"components.MailContentRenderer.showHtmlMail": "Mostrar correo HTML",
|
||||
"views.admin.Account.showCredential": "Mostrar credencial de la dirección de correo",
|
||||
"views.user.UserSettings.showPasskeyList": "Mostrar lista de passkeys",
|
||||
"components.MailBox.showTextMail": "Mostrar correo en texto",
|
||||
"components.MailContentRenderer.showTextMail": "Mostrar correo en texto",
|
||||
"views.Index.enterSimpleMode": "Modo simple",
|
||||
"components.MailContentRenderer.size": "Tamaño",
|
||||
"views.admin.Account.source_meta": "Origen",
|
||||
"views.admin.AccountSettings.source_patterns": "Regex de dirección de origen (opcional)",
|
||||
"views.admin.Maintenance.sqlStatement": "Sentencia SQL (solo DELETE)",
|
||||
"views.user.UserOauth2Callback.stateNotMatch": "el estado no coincide",
|
||||
"views.Admin.statistics": "Estadísticas",
|
||||
"views.Header.status": "Estado",
|
||||
"views.admin.IpBlacklistSettings.enable_whitelist_tip": "Modo estricto: solo las IP que coincidan con la lista blanca podrán acceder a las API con límite. Todas las demás serán rechazadas.",
|
||||
"views.index.AutoReply.subject": "Asunto",
|
||||
"views.index.SendMail.subject": "Asunto",
|
||||
"views.admin.SendMail.subject": "Asunto",
|
||||
"views.index.SendMail.subjectEmpty": "El asunto está vacío",
|
||||
"views.admin.SendMail.subjectEmpty": "El asunto está vacío",
|
||||
"components.AiExtractInfo.subscriptionLink": "Enlace de suscripción",
|
||||
"views.user.AddressManagement.success": "éxito",
|
||||
"views.index.TelegramAddress.success": "éxito",
|
||||
"views.index.LocalAddress.success": "éxito",
|
||||
"views.admin.UserAddressManagement.success": "éxito",
|
||||
"components.WebhookComponent.successTip": "Correcto",
|
||||
"components.MailBox.success": "Correcto",
|
||||
"components.SendBox.success": "Correcto",
|
||||
"views.index.AccountSettings.success": "Correcto",
|
||||
"views.index.AutoReply.success": "Correcto",
|
||||
"views.admin.AiExtractSettings.successTip": "Correcto",
|
||||
"views.admin.Account.success": "Correcto",
|
||||
"views.admin.SenderAccess.success": "Correcto",
|
||||
"views.admin.UserManagement.success": "Correcto",
|
||||
"views.admin.Telegram.successTip": "Correcto",
|
||||
"views.admin.RoleAddressConfig.successTip": "Correcto",
|
||||
"views.admin.Webhook.successTip": "Correcto",
|
||||
"views.admin.CreateAccount.successTip": "Creado correctamente",
|
||||
"views.admin.Telegram.globalMailPushListTip": "Admite chat_id de chat privado/grupo/canal. Envía un mensaje al bot y visita este enlace para ver el chat_id: https://api.telegram.org/bot<Reemplaza con tu BOT TOKEN>/getUpdates",
|
||||
"views.user.AddressManagement.targetUserEmail": "Correo del usuario objetivo",
|
||||
"views.admin.Telegram.telegramAllowList": "Lista blanca de Telegram (introduce manualmente el Chat ID)",
|
||||
"views.Admin.telegram": "Bot de Telegram",
|
||||
"views.admin.Telegram.miniAppUrl": "URL de la Mini App de Telegram",
|
||||
"components.WebhookComponent.test": "Prueba",
|
||||
"views.index.SendMail.text": "Texto",
|
||||
"views.admin.SendMail.text": "Texto",
|
||||
"views.user.UserSettings.passordTip": "El servidor solo recibe el hash de la contraseña, no la contraseña en texto claro, por lo que no puede verla ni recuperarla. Si el administrador activa la verificación por correo, puedes restablecerla en modo incógnito.",
|
||||
"views.index.LocalAddress.tip": "Estas direcciones se guardan en tu navegador y podrían perderse si borras la caché.",
|
||||
"views.admin.UserOauth2Settings.tip": "El inicio de sesión de terceros usará automáticamente el correo del usuario para registrar una cuenta (el mismo correo se considera la misma cuenta). También puedes establecer la contraseña con “olvidé mi contraseña”.",
|
||||
"views.admin.AccountSettings.send_mail_limit_tip": "Se aplica a todos los canales de envío. Usa -1 para ilimitado y 0 para bloquear el envío.",
|
||||
"views.admin.AccountSettings.create_address_subdomain_match_note": "Esto es diferente de RANDOM_SUBDOMAIN_DOMAINS: este interruptor permite indicar subdominios personalizados; el subdominio aleatorio solo genera uno al crear.",
|
||||
"views.index.SendMail.tooLarge": "Archivo demasiado grande; sube uno de menos de 1 MB.",
|
||||
"views.admin.SendMail.tooLarge": "Archivo demasiado grande; sube uno de menos de 1 MB.",
|
||||
"views.common.Appearance.top": "arriba",
|
||||
"views.user.AddressManagement.transferAddress": "Transferir dirección",
|
||||
"views.user.AddressManagement.transferAddressTip": "Transferir esta dirección a otro usuario la quitará de tu cuenta y la moverá a la suya. ¿Seguro que quieres transferirla?",
|
||||
"views.common.Appearance.useSideMargin": "Activar márgenes laterales a la izquierda y a la derecha de la página",
|
||||
"views.admin.AiExtractSettings.manualInputPrompt": "Escribe y pulsa Enter para añadir",
|
||||
"views.admin.UserOauth2Settings.manualInputPrompt": "Escribe y pulsa Enter para añadir",
|
||||
"views.admin.Telegram.manualInputPrompt": "Escribe y pulsa Enter para añadir",
|
||||
"views.admin.UserSettings.manualInputPrompt": "Escribe y pulsa Enter para añadir",
|
||||
"views.admin.AccountSettings.manualInputPrompt": "Escribe y pulsa Enter para añadir",
|
||||
"views.admin.Webhook.manualInputPrompt": "Escribe y pulsa Enter para añadir",
|
||||
"views.admin.IpBlacklistSettings.manualInputPrompt": "Escribe el patrón y pulsa Enter para añadir",
|
||||
"views.user.AddressManagement.unbindAddress": "Desvincular dirección",
|
||||
"views.index.TelegramAddress.unbindMailAddress": "Desvincular dirección de correo",
|
||||
"views.index.LocalAddress.unbindMailAddress": "Desvincular credencial de dirección de correo",
|
||||
"components.MailBox.unselectAll": "Deseleccionar todo",
|
||||
"components.SendBox.unselectAll": "Deseleccionar todo",
|
||||
"views.admin.Account.unselectAll": "Deseleccionar todo",
|
||||
"views.admin.Account.updated_at": "Actualizado el",
|
||||
"views.user.UserSettings.updated_at": "Actualizado el",
|
||||
"components.WebhookComponent.urlMissing": "La URL es obligatoria",
|
||||
"views.common.Appearance.useIframeShowMail": "Usar iframe para mostrar correo HTML",
|
||||
"views.admin.CreateAccount.enableRandomSubdomain": "Usar subdominio aleatorio",
|
||||
"views.common.Login.enableRandomSubdomain": "Usar subdominio aleatorio",
|
||||
"views.admin.UserOauth2Settings.userEmailFormatTip": "Usa regex para transformar el correo. Ejemplo: ^(.+)@old\\.com$ con $1@new.com",
|
||||
"views.common.Appearance.useSimpleIndex": "Usar índice simple",
|
||||
"views.common.Appearance.useUTCDate": "Usar fecha UTC",
|
||||
"views.Header.user": "Usuario",
|
||||
"views.Admin.user": "Cuentas de usuario",
|
||||
"components.AddressSelect.userAddresses": "Direcciones vinculadas a la cuenta",
|
||||
"views.Admin.loginViaUserAdmin": "Permiso de administrador del usuario",
|
||||
"views.admin.Statistics.userCount": "Cantidad de usuarios",
|
||||
"views.admin.UserManagement.user_email": "Correo de la cuenta de usuario",
|
||||
"views.index.AddressBar.userLogin": "Inicio de sesión de la cuenta",
|
||||
"views.Admin.user_management": "Gestión de cuentas de usuario",
|
||||
"views.Admin.user_settings": "Configuración de cuentas de usuario",
|
||||
"views.User.user_settings": "Configuración de la cuenta de usuario",
|
||||
"components.AiExtractInfo.authCode": "Código de verificación",
|
||||
"views.user.UserLogin.verifyCode": "Código de verificación",
|
||||
"views.user.UserLogin.verifyCodeSent": "Código de verificación enviado, expira en {timeout} segundos",
|
||||
"views.admin.AccountSettings.verified_address_list": "Lista de direcciones verificadas (puede enviar correo mediante la API interna de cf)",
|
||||
"views.admin.UserSettings.verifyMailSender": "Verificar remitente de correo",
|
||||
"components.MailContentRenderer.attachments": "Ver adjuntos",
|
||||
"views.admin.Account.viewMails": "Ver correos",
|
||||
"views.admin.Account.viewSendBox": "Ver bandeja de salida",
|
||||
"views.user.UserLogin.waitforVerifyCode": "Espera {timeout} segundos",
|
||||
"views.admin.Webhook.webhookAllowList": "Lista de permitidos de webhook (introduce el correo autorizado y pulsa Enter)",
|
||||
"views.admin.Webhook.notEnabled": "El webhook no está habilitado",
|
||||
"components.WebhookComponent.notEnabled": "El webhook no está habilitado para ti",
|
||||
"views.Index.webhookSettings": "Configuración de webhook",
|
||||
"views.Admin.webhookSettings": "Configuración de webhook",
|
||||
"views.admin.AiExtractSettings.disabledTip": "Si está desactivado, la extracción IA procesará todas las direcciones",
|
||||
"views.admin.AiExtractSettings.enableAllowListTip": "Si está activado, la extracción IA solo procesará correos enviados a direcciones permitidas",
|
||||
"views.admin.CreateAccount.randomSubdomainTip": "Si está activado, la dirección creada usará un subdominio aleatorio. Recomendado solo para recibir. Requiere un registro MX comodín en el DNS del dominio base — consulta la documentación de subdominios aleatorios.",
|
||||
"views.common.Login.randomSubdomainTip": "Si está activado, la dirección creada usará un subdominio aleatorio. Recomendado solo para recibir. Requiere un registro MX comodín en el DNS del dominio base — consulta la documentación de subdominios aleatorios.",
|
||||
"views.admin.AiExtractSettings.allowListTip": "El comodín * coincide con cualquier carácter; p. ej., *{'@'}example.com coincide con todas las direcciones del dominio example.com",
|
||||
"views.Admin.workerconfig": "Configuración del Worker",
|
||||
"views.admin.AccountSettings.create_address_subdomain_match_env_locked": "La variable ENABLE_CREATE_ADDRESS_SUBDOMAIN_MATCH está en false. Puedes guardar el interruptor, pero no tendrá efecto hasta habilitar o quitar la variable.",
|
||||
"views.common.Login.getNewEmailTip3": "Puedes elegir un dominio en la lista desplegable.",
|
||||
"views.admin.AccountSettings.tip": "Puedes introducir manualmente los siguientes valores y pulsar Enter para añadirlos",
|
||||
"components.MailBox.emptyInbox": "Tu bandeja de entrada está vacía",
|
||||
"views.index.SendMail.fromName": "Tu nombre y dirección; deja el nombre vacío para usar el correo",
|
||||
"views.admin.SendMail.fromName": "Tu nombre y dirección; deja el nombre vacío para usar el correo",
|
||||
"components.AddressCredentialModal.addressCredential": "Credencial de dirección",
|
||||
"components.AddressCredentialModal.addressCredentialLabel": "Address JWT",
|
||||
"components.AddressCredentialModal.addressPassword": "Contraseña de la dirección",
|
||||
"components.AddressCredentialModal.agentAccess": "AI Agent",
|
||||
"components.AddressCredentialModal.agentAccessTip": "Usa este buzón desde un AI Agent con el Address JWT y las APIs parsed-mail.",
|
||||
"components.AddressCredentialModal.agentConfig": "Configuración del agente",
|
||||
"components.AddressCredentialModal.agentSkill": "Agent skill",
|
||||
"components.AddressCredentialModal.apiBase": "Base de la API",
|
||||
"components.AddressCredentialModal.autoLoginLink": "Enlace de inicio automático",
|
||||
"components.AddressCredentialModal.copyFailed": "Error al copiar",
|
||||
"components.AddressCredentialModal.copySection": "Copiar",
|
||||
"components.AddressCredentialModal.copySuccess": "Copiado",
|
||||
"components.AddressCredentialModal.currentAddress": "Dirección actual",
|
||||
"components.AddressCredentialModal.docs": "Documentación",
|
||||
"components.AddressCredentialModal.imapHost": "Host IMAP",
|
||||
"components.AddressCredentialModal.imapPort": "Puerto IMAP",
|
||||
"components.AddressCredentialModal.password": "Contraseña",
|
||||
"components.AddressCredentialModal.plainOrProxyTls": "Texto plano o TLS del proxy",
|
||||
"components.AddressCredentialModal.security": "Seguridad",
|
||||
"components.AddressCredentialModal.smtpHost": "Host SMTP",
|
||||
"components.AddressCredentialModal.smtpImapAccess": "SMTP / IMAP",
|
||||
"components.AddressCredentialModal.smtpImapTip": "Usa estos valores en clientes de correo después de que el administrador configure el proxy SMTP/IMAP. Como contraseña puedes usar el Address JWT mostrado aquí o la contraseña de la dirección si la tienes.",
|
||||
"components.AddressCredentialModal.smtpPort": "Puerto SMTP",
|
||||
"components.AddressCredentialModal.starttls": "STARTTLS",
|
||||
"components.AddressCredentialModal.tip": "Usa estas credenciales solo con clientes y agentes de confianza.",
|
||||
"components.AddressCredentialModal.title": "Credenciales de dirección y métodos de conexión",
|
||||
"components.AddressCredentialModal.username": "Usuario"
|
||||
}
|
||||
@@ -0,0 +1,621 @@
|
||||
export const jaMessages = {
|
||||
"views.index.SimpleIndex.mailCount": "{current} / {total} 件のメール",
|
||||
"views.admin.Statistics.activeAddressCount30days": "30日間のアクティブなアドレス数",
|
||||
"views.admin.Statistics.activeAddressCount7days": "7日間のアクティブなアドレス数",
|
||||
"views.Index.about": "概要",
|
||||
"views.Admin.about": "概要",
|
||||
"views.Header.accessHeader": "アクセス用パスワード",
|
||||
"views.Admin.account": "メールアドレス",
|
||||
"views.Index.accountSettings": "メールアドレス設定",
|
||||
"views.Admin.account_settings": "メールアドレス設定",
|
||||
"views.index.SimpleIndex.accountSettings": "メールアドレス設定",
|
||||
"views.index.Attachment.action": "操作",
|
||||
"views.admin.SenderAccess.action": "操作",
|
||||
"views.user.UserSettings.actions": "操作",
|
||||
"views.user.AddressManagement.actions": "操作",
|
||||
"views.index.TelegramAddress.actions": "操作",
|
||||
"views.admin.Account.actions": "操作",
|
||||
"views.index.LocalAddress.actions": "操作",
|
||||
"views.admin.UserManagement.actions": "操作",
|
||||
"views.admin.AccountSettings.actions": "操作",
|
||||
"views.admin.AccountSettings.add": "追加",
|
||||
"views.admin.Maintenance.customSqlTip": "定期クリーンアップ用にカスタム DELETE SQL を追加します。各項目には 1 つの DELETE 文のみ使用できます。",
|
||||
"views.admin.Maintenance.addCustomSql": "カスタム SQL を追加",
|
||||
"views.admin.UserOauth2Settings.addOauth2": "Oauth2 を追加",
|
||||
"components.AddressSelect.address": "アドレス",
|
||||
"views.user.AddressManagement.address": "アドレス",
|
||||
"views.index.TelegramAddress.address": "アドレス",
|
||||
"views.admin.SenderAccess.address": "アドレス",
|
||||
"views.index.LocalAddress.address": "アドレス",
|
||||
"views.admin.CreateAccount.address": "アドレス",
|
||||
"views.admin.AiExtractSettings.allowList": "アドレス許可リスト (アドレスを入力して Enter。ワイルドカード対応)",
|
||||
"views.admin.AccountSettings.send_address_block_list": "送信メール用のブロックキーワード",
|
||||
"views.admin.AccountSettings.address_block_list": "ユーザー向けブロックキーワード (管理者はスキップ可能)",
|
||||
"views.index.SimpleIndex.addressCopied": "アドレスをコピーしました",
|
||||
"views.admin.Statistics.addressCount": "アドレス数",
|
||||
"views.admin.UserManagement.address_count": "アドレス数",
|
||||
"views.User.address_management": "アドレス管理",
|
||||
"views.admin.UserManagement.userAddressManagement": "アドレス管理",
|
||||
"views.index.AddressBar.addressPassword": "アドレスパスワード",
|
||||
"views.admin.CreateAccount.addressPassword": "アドレスパスワード",
|
||||
"views.Admin.adminAccount": "管理者",
|
||||
"views.Admin.accessHeader": "管理者パスワード",
|
||||
"views.Admin.loginViaPassword": "管理者パスワードでログイン",
|
||||
"views.admin.AiExtractSettings.title": "AI メール抽出設定",
|
||||
"views.Admin.aiExtractSettings": "AI抽出設定",
|
||||
"views.admin.AccountSettings.match_all": "すべて",
|
||||
"views.admin.AccountSettings.create_address_subdomain_match": "アドレス作成時にサブドメイン接尾辞一致を許可",
|
||||
"views.admin.UserSettings.enableUserRegister": "ユーザー登録を許可",
|
||||
"views.admin.AccountSettings.match_any": "いずれか",
|
||||
"views.Index.appearance": "表示",
|
||||
"views.Admin.appearance": "表示",
|
||||
"views.admin.IpBlacklistSettings.tip_scope": "対象: アドレス作成、メール送信、外部送信 API、ユーザー登録、コード検証",
|
||||
"views.index.AccountSettings.clearInboxConfirm": "受信箱のすべてのメールを削除してもよろしいですか?",
|
||||
"views.index.AccountSettings.clearSentItemsConfirm": "送信済みメールをすべて削除してもよろしいですか?",
|
||||
"views.admin.Account.multiClearInboxTip": "選択したアドレスの受信箱を削除してもよろしいですか?",
|
||||
"views.admin.Account.clearInboxTip": "このメールアドレスの受信箱を削除してもよろしいですか?",
|
||||
"views.admin.Account.multiClearSentItemsTip": "選択したアドレスの送信済みを削除してもよろしいですか?",
|
||||
"views.admin.Account.clearSentItemsTip": "このメールアドレスの送信済みを削除してもよろしいですか?",
|
||||
"views.admin.Account.multiDeleteTip": "選択したアドレスを削除してもよろしいですか?",
|
||||
"views.index.Attachment.deleteConfirm": "この添付ファイルを削除してもよろしいですか?",
|
||||
"views.admin.Account.deleteTip": "このメールを削除してもよろしいですか?",
|
||||
"views.admin.SenderAccess.deleteTip": "これを削除してもよろしいですか?",
|
||||
"views.index.AccountSettings.deleteAccountConfirm": "このメールアドレスと関連メールをすべて削除してもよろしいですか?",
|
||||
"views.index.AccountSettings.logoutConfirm": "ログアウトしてもよろしいですか?",
|
||||
"components.MailBox.deleteMailTip": "メールを削除してもよろしいですか?",
|
||||
"components.MailContentRenderer.deleteMailTip": "メールを削除してもよろしいですか?",
|
||||
"components.SendBox.deleteMailTip": "メールを削除してもよろしいですか?",
|
||||
"views.admin.AccountSettings.delete_rule_confirm": "このルールを削除してもよろしいですか?",
|
||||
"views.admin.UserManagement.deleteUserTip": "このユーザーアカウントを削除してもよろしいですか?",
|
||||
"views.Admin.logoutConfirmContent": "管理画面からログアウトしてもよろしいですか?",
|
||||
"views.user.UserSettings.logoutConfirm": "ログアウトしてもよろしいですか?",
|
||||
"views.admin.IpBlacklistSettings.asn_blacklist": "ASN組織ブラックリスト",
|
||||
"views.admin.IpBlacklistSettings.tip_asn": "ASN 組織: ISP/プロバイダ単位でブロックします。大文字小文字を区別しないテキスト一致または正規表現に対応します。",
|
||||
"components.AiExtractInfo.authLink": "認証リンク",
|
||||
"views.admin.Maintenance.autoCleanup": "自動クリーンアップ",
|
||||
"components.MailBox.autoRefresh": "自動更新",
|
||||
"components.MailBox.backToList": "リストに戻る",
|
||||
"views.common.Appearance.autoRefreshInterval": "自動更新間隔 (秒)",
|
||||
"views.Index.auto_reply": "自動返信",
|
||||
"views.index.AutoReply.autoReply": "自動返信",
|
||||
"views.common.Login.autoGeneratedName": "自動生成名",
|
||||
"views.admin.SenderAccess.balance": "残高",
|
||||
"views.admin.Maintenance.basicCleanup": "基本クリーンアップ",
|
||||
"views.user.AddressManagement.unbindAddressTip": "解除前にこのアドレスへ切り替え、メール資格情報を保存してください。",
|
||||
"views.index.TelegramAddress.bind": "紐付け",
|
||||
"views.index.TelegramAddress.bindAddressSuccess": "アドレスの紐付けに成功しました",
|
||||
"views.index.LocalAddress.bindAddressSuccess": "アドレスの紐付けに成功しました",
|
||||
"views.User.bind_address": "メールアドレスを紐付け",
|
||||
"views.admin.IpBlacklistSettings.enable_tip": "ブラックリストに一致するIPによるレート制限APIへのアクセスをブロック",
|
||||
"views.admin.AccountSettings.fromBlockList": "受信メールのブロックキーワード",
|
||||
"views.admin.AccountSettings.block_receive_unknow_address_email": "未知アドレス宛メールの受信をブロック",
|
||||
"views.common.Appearance.bottom": "下",
|
||||
"views.admin.IpBlacklistSettings.fingerprint_blacklist": "ブラウザ指紋ブラックリスト",
|
||||
"views.admin.IpBlacklistSettings.tip_fingerprint": "ブラウザ指紋: 指紋でブロックします。完全一致または正規表現に対応します。",
|
||||
"views.admin.AccountSettings.cancel": "キャンセル",
|
||||
"components.MailBox.cancelMultiAction": "複数選択をキャンセル",
|
||||
"components.SendBox.cancelMultiAction": "複数選択をキャンセル",
|
||||
"views.user.AddressManagement.changeMailAddress": "アドレスを変更",
|
||||
"views.index.TelegramAddress.changeMailAddress": "メールアドレスを変更",
|
||||
"views.index.LocalAddress.changeMailAddress": "メールアドレスを変更",
|
||||
"views.index.AccountSettings.changePassword": "パスワードを変更",
|
||||
"views.admin.UserManagement.changeRole": "ロールを変更",
|
||||
"components.SendBox.showCode": "元のコード表示を切り替え",
|
||||
"views.admin.Telegram.status": "ステータスを確認",
|
||||
"views.admin.AccountSettings.create_address_subdomain_match_follow_env_note": "「環境変数に従う」を選ぶと、管理者による上書きが解除され、未設定状態に戻ります。実際の動作は引き続き Worker の環境変数と優先順位ルールで決まります。",
|
||||
"views.admin.Maintenance.cleanupNow": "今すぐクリーンアップ",
|
||||
"views.admin.Maintenance.cleanupSuccess": "クリーンアップ成功",
|
||||
"views.admin.Maintenance.addressCreateLabel": "n 日より前に作成されたアドレスを削除",
|
||||
"views.admin.Maintenance.emptyAddressLabel": "n 日より前の空アドレスを削除",
|
||||
"views.admin.Maintenance.inactiveAddressLabel": "n 日より前の非アクティブなアドレスを削除",
|
||||
"views.admin.Maintenance.mailBoxLabel": "n 日より前の受信箱を削除",
|
||||
"views.admin.Maintenance.sendBoxLabel": "n 日より前の送信箱を削除",
|
||||
"views.admin.Maintenance.unboundAddressLabel": "紐付け解除から n 日経過したメールアドレスを削除",
|
||||
"views.admin.Maintenance.mailUnknowLabel": "n 日より前の受信者不明メールを削除",
|
||||
"views.index.AccountSettings.clearInbox": "受信箱を削除",
|
||||
"views.admin.Account.clearInbox": "受信箱を削除",
|
||||
"views.index.AccountSettings.clearSentItems": "送信済みを削除",
|
||||
"views.admin.Account.clearSentItems": "送信済みを削除",
|
||||
"views.Header.title": "Cloudflare 一時メール",
|
||||
"views.admin.DatabaseManager.code_db_version": "コードが必要とするDBバージョン",
|
||||
"views.user.UserOauth2Callback.codeNotFound": "コードが見つかりません",
|
||||
"views.admin.AccountSettings.config": "設定",
|
||||
"views.admin.RoleAddressConfig.roleConfigDesc": "各ユーザーロールごとの最大アドレス数を設定します。ロール別制限は全体設定より優先されます。0 は無制限です。",
|
||||
"views.Admin.confirm": "確認",
|
||||
"views.Admin.logoutConfirmTitle": "ログアウトを確認",
|
||||
"views.index.AccountSettings.confirmPassword": "パスワードを確認",
|
||||
"views.index.SendMail.content": "内容",
|
||||
"views.admin.SendMail.content": "内容",
|
||||
"views.index.SendMail.contentEmpty": "内容が空です",
|
||||
"views.admin.SendMail.contentEmpty": "内容が空です",
|
||||
"components.AddressSelect.copied": "コピーしました",
|
||||
"components.AiExtractInfo.copySuccess": "正常にコピーしました",
|
||||
"components.AddressSelect.copy": "コピー",
|
||||
"views.index.SimpleIndex.copyAddress": "コピー",
|
||||
"components.AiExtractInfo.copyFailed": "コピーに失敗しました",
|
||||
"views.Footer.copyright": "著作権",
|
||||
"views.Admin.account_create": "メールアドレスを作成",
|
||||
"views.admin.CreateAccount.creatNewEmail": "新しいメールを作成",
|
||||
"views.common.Login.getNewEmail": "新しいメールを作成",
|
||||
"views.user.AddressManagement.create_or_bind": "作成または紐付け",
|
||||
"views.index.LocalAddress.create_or_bind": "作成または紐付け",
|
||||
"views.user.UserSettings.createPasskey": "パスキーを作成",
|
||||
"views.admin.UserManagement.createUser": "ユーザーアカウントを作成",
|
||||
"views.user.UserSettings.created_at": "作成日時",
|
||||
"views.admin.Account.created_at": "作成日時",
|
||||
"views.admin.SenderAccess.created_at": "作成日時",
|
||||
"views.admin.UserManagement.created_at": "作成日時",
|
||||
"views.common.Login.credentialLogin": "資格情報でログイン",
|
||||
"views.admin.DatabaseManager.current_db_version": "現在のDBバージョン",
|
||||
"views.user.UserBar.currentUser": "現在のユーザーアカウント",
|
||||
"views.admin.UserManagement.roleDonotExist": "現在のロールは存在しません",
|
||||
"views.admin.Maintenance.customSqlCleanup": "カスタムSQLクリーンアップ",
|
||||
"views.admin.AccountSettings.send_mail_daily_limit": "日次上限",
|
||||
"views.admin.AccountSettings.send_mail_daily_limit_invalid": "日次上限は -1 以上の整数である必要があります",
|
||||
"views.admin.IpBlacklistSettings.tip_daily_limit": "日次上限: IP ごとの 1 日あたり最大リクエスト数を制限します (1-1000000)。",
|
||||
"views.admin.IpBlacklistSettings.daily_request_limit": "1日のリクエスト上限",
|
||||
"views.Header.dark": "ダーク",
|
||||
"views.Admin.database": "データベース",
|
||||
"views.admin.DatabaseManager.need_initialization_tip": "データベースの初期化が必要です。先に初期化してください。",
|
||||
"views.admin.DatabaseManager.initializationSuccess": "データベースを初期化しました",
|
||||
"views.admin.DatabaseManager.migrationSuccess": "データベースを移行しました",
|
||||
"views.admin.DatabaseManager.need_migration_tip": "データベースの移行が必要です。先に移行してください。",
|
||||
"components.MailBox.delete": "削除",
|
||||
"components.MailContentRenderer.delete": "削除",
|
||||
"components.SendBox.delete": "削除",
|
||||
"views.index.Attachment.delete": "削除",
|
||||
"views.admin.Account.delete": "削除",
|
||||
"views.admin.UserOauth2Settings.delete": "削除",
|
||||
"views.admin.SenderAccess.delete": "削除",
|
||||
"views.admin.UserManagement.delete": "削除",
|
||||
"views.admin.AccountSettings.delete_rule": "削除",
|
||||
"views.admin.Maintenance.deleteCustomSql": "削除",
|
||||
"views.index.AccountSettings.deleteAccount": "メールアドレスを削除",
|
||||
"views.admin.Account.deleteAccount": "メールアドレスを削除",
|
||||
"views.user.UserSettings.deletePasskey": "Passkeyを削除",
|
||||
"views.admin.AccountSettings.delete_success": "削除しました",
|
||||
"views.admin.UserManagement.deleteUser": "ユーザーアカウントを削除",
|
||||
"views.index.Attachment.deleteSuccess": "正常に削除しました",
|
||||
"views.admin.SenderAccess.disable": "無効化",
|
||||
"views.Admin.loginViaDisabledCheck": "パスワードチェックを無効化",
|
||||
"views.common.Appearance.preferShowTextMail": "既定でテキストメールを表示",
|
||||
"views.admin.AccountSettings.domain_list": "ドメイン一覧(任意)",
|
||||
"views.admin.AccountSettings.source_patterns_tip": "ドメイン一覧は受信先アドレスで、送信元正規表現は送信者アドレスでフィルタします。転送には両方の条件を満たす必要があります(AND)。どちらかを空欄にするとその条件は無視されます。",
|
||||
"views.admin.UserManagement.domains": "ドメイン",
|
||||
"views.index.Attachment.download": "ダウンロード",
|
||||
"components.MailBox.downloadMail": "メールをダウンロード",
|
||||
"components.MailContentRenderer.downloadMail": "メールをダウンロード",
|
||||
"views.admin.Maintenance.sqlNamePlaceholder": "例: 古いログを削除",
|
||||
"views.admin.Maintenance.sqlPlaceholder": "例: DELETE FROM raw_mails WHERE source GLOB '*{'@'}example.com' AND created_at < datetime('now', '-3 day')",
|
||||
"views.admin.AccountSettings.source_patterns_placeholder": "例: gmail.com",
|
||||
"views.admin.AccountSettings.forwarding_rule_warning": "各ルールは個別に実行されます。転送先アドレスは検証済みである必要があります。",
|
||||
"views.index.SendMail.edit": "編集",
|
||||
"views.admin.SendMail.edit": "編集",
|
||||
"views.user.UserLogin.email": "メール",
|
||||
"views.admin.UserManagement.email": "メール",
|
||||
"views.common.Login.email": "メール",
|
||||
"views.common.Login.credential": "メールアドレス資格情報",
|
||||
"views.common.Login.emailPasswordRequired": "メールアドレスとパスワードは必須です",
|
||||
"views.admin.UserSettings.emailCheckRegex": "メール検査用正規表現 (例: ^[^.]+{'@'}.+$ で {'@'} の前のドットを禁止)",
|
||||
"views.admin.AccountSettings.email_forwarding_config": "メール転送設定",
|
||||
"views.admin.UserOauth2Settings.userEmailFormat": "メール正規表現パターン",
|
||||
"views.Admin.mails": "メール",
|
||||
"views.index.AutoReply.sourcePrefixPlaceholder": "空欄=すべて、前方一致、または /regex/",
|
||||
"components.WebhookComponent.enable": "有効化",
|
||||
"views.admin.UserOauth2Settings.enable": "有効化",
|
||||
"views.admin.SenderAccess.enable": "有効化",
|
||||
"views.admin.Telegram.enable": "有効化",
|
||||
"views.admin.UserSettings.enable": "有効化",
|
||||
"views.admin.AiExtractSettings.enableAllowList": "アドレス許可リストを有効化",
|
||||
"views.admin.Webhook.enableAllowList": "許可リストを有効化(Webhook を特定のメールアドレスに制限)",
|
||||
"views.index.AutoReply.enableAutoReply": "自動返信を有効化",
|
||||
"views.admin.Maintenance.cronTip": "cron クリーンアップを有効にするには worker の [crons] を設定してください。詳細はドキュメントを参照してください。0 日はすべて削除を意味します。",
|
||||
"views.admin.IpBlacklistSettings.enable_daily_limit": "1日のリクエスト上限を有効化",
|
||||
"views.admin.UserSettings.enableEmailCheckRegex": "メール検査用正規表現を有効化",
|
||||
"views.admin.UserOauth2Settings.enableEmailFormat": "メール形式チェックを有効化",
|
||||
"views.admin.Telegram.enableGlobalMailPush": "グローバルメール通知を有効化 (Telegram の Chat ID を手動入力)",
|
||||
"views.admin.IpBlacklistSettings.enable_ip_blacklist": "IPブラックリストを有効化",
|
||||
"views.admin.IpBlacklistSettings.enable_ip_whitelist": "IPホワイトリストを有効化(厳格)",
|
||||
"views.admin.UserOauth2Settings.enableMailAllowList": "メールアドレス許可リストを有効化 (手動入力可)",
|
||||
"views.admin.UserSettings.enableMailAllowList": "メールアドレス許可リストを有効化 (手動入力可)",
|
||||
"views.admin.UserSettings.enableMailVerify": "メール検証を有効化 (送信元アドレスはシステム内に存在し、残高があり、正常に送信できる必要があります)",
|
||||
"views.admin.Telegram.enableTelegramAllowList": "Telegram 許可リストを有効化 (Chat ID を手動入力)",
|
||||
"views.admin.IpBlacklistSettings.asn_blacklist_placeholder": "ASN組織を入力してください(例: Google, Amazon)",
|
||||
"views.admin.IpBlacklistSettings.fingerprint_blacklist_placeholder": "フィンガープリントIDを入力してください(例: a1b2c3d4e5f6g7h8)",
|
||||
"views.admin.IpBlacklistSettings.daily_request_limit_placeholder": "上限を入力してください(例: 1000)",
|
||||
"views.admin.IpBlacklistSettings.ip_blacklist_placeholder": "パターンを入力してください(例: 192.168.1 または ^10\\.0\\.0\\.5$)",
|
||||
"views.common.Login.bindUserAddressError": "メールアドレスをユーザーに紐付ける際にエラーが発生しました",
|
||||
"views.admin.IpBlacklistSettings.ip_whitelist_placeholder": "完全一致IP(例: 1.2.3.4)またはアンカー付き正規表現(例: ^192\\.168\\.1\\.\\d+$)",
|
||||
"views.index.SimpleIndex.exitSimpleIndex": "シンプルモードを終了",
|
||||
"components.MailBox.keywordQueryTip": "現在のページを絞り込む",
|
||||
"views.admin.AccountSettings.create_address_subdomain_match_follow_env": "環境変数に従う",
|
||||
"views.admin.AccountSettings.create_address_subdomain_match_force_disable": "強制的に無効化",
|
||||
"views.admin.AccountSettings.create_address_subdomain_match_force_enable": "強制的に有効化",
|
||||
"views.user.UserLogin.forgotPassword": "パスワードを忘れた",
|
||||
"components.MailBox.forwardMail": "転送",
|
||||
"components.MailContentRenderer.forward": "転送",
|
||||
"views.admin.AccountSettings.forward_address": "転送先アドレス",
|
||||
"views.admin.AccountSettings.forward_address_required": "転送先アドレスは必須です",
|
||||
"views.admin.AccountSettings.forward_placeholder": "forward@example.com",
|
||||
"components.MailContentRenderer.fullscreen": "全画面",
|
||||
"views.common.Login.generateName": "ランダム名を生成",
|
||||
"views.admin.Telegram.globalMailPushList": "グローバルメール通知 Chat ID 一覧",
|
||||
"views.common.Appearance.globalTabplacement": "全体タブ位置",
|
||||
"views.common.Login.help": "ヘルプ",
|
||||
"views.Header.home": "ホーム",
|
||||
"views.index.SendMail.html": "HTML",
|
||||
"views.admin.SendMail.html": "HTML",
|
||||
"views.admin.UserOauth2Settings.icon": "アイコン (SVG、信頼できるソースのみ使用してください)",
|
||||
"views.admin.CreateAccount.enablePrefix": "プレフィックスを有効化する場合",
|
||||
"views.common.AdminContact.adminContact": "支援が必要な場合は管理者 ({msg}) に連絡してください",
|
||||
"views.admin.Telegram.init": "初期化",
|
||||
"views.admin.DatabaseManager.init": "データベースを初期化",
|
||||
"views.admin.AccountSettings.regex_invalid": "無効な正規表現です",
|
||||
"views.Admin.ipBlacklistSettings": "IPブラックリスト",
|
||||
"views.admin.IpBlacklistSettings.ip_blacklist": "IPブラックリスト パターン",
|
||||
"views.admin.IpBlacklistSettings.title": "IPブラックリスト 設定",
|
||||
"views.admin.IpBlacklistSettings.tip_ip": "IP ブラックリスト: テキスト一致 (例: \"192.168.1\") または正規表現 (例: \"^10\\.0\\.0\\.5$\") に対応します。",
|
||||
"views.admin.IpBlacklistSettings.whitelist_empty_warning": "IP ホワイトリストは有効ですが一覧が空です。ロックアウト防止のためサーバーはこの設定を無視します。有効化する前に少なくとも 1 件追加してください。",
|
||||
"views.admin.IpBlacklistSettings.ip_whitelist": "IPホワイトリスト パターン",
|
||||
"views.admin.IpBlacklistSettings.tip_whitelist": "IP ホワイトリスト: 厳格な許可リストです。通常の項目は完全一致の IP である必要があります (部分一致不可)。範囲指定にはアンカー付き正規表現 (^...$) を使用してください。許可された IP はブラックリスト判定をスキップします。",
|
||||
"views.admin.SenderAccess.is_enabled": "有効",
|
||||
"views.admin.Account.itemCount": "itemCount",
|
||||
"views.admin.SenderAccess.itemCount": "itemCount",
|
||||
"views.admin.UserManagement.itemCount": "itemCount",
|
||||
"views.user.UserMailBox.addressQueryTip": "空欄で全アドレスを検索します",
|
||||
"views.admin.Account.addressQueryTip": "空欄で全アドレスを検索します",
|
||||
"views.admin.Mails.addressQueryTip": "空欄で全アドレスを検索します",
|
||||
"views.common.Appearance.left": "左",
|
||||
"views.common.Login.getNewEmailTip2": "空欄のままにするとランダムなメールアドレスが生成されます。",
|
||||
"views.Header.light": "ライト",
|
||||
"views.admin.IpBlacklistSettings.enable_daily_limit_tip": "IP ごとの 1 日あたり API リクエスト数を制限",
|
||||
"components.AddressSelect.localAddresses": "ローカルアドレス",
|
||||
"views.common.Login.bindUserInfo": "ログイン済みユーザーは、未紐付けのままログインまたは新規作成すると現在のユーザーに紐付きます",
|
||||
"views.user.UserOauth2Callback.logging": "ログイン中...",
|
||||
"views.user.UserLogin.login": "ログイン",
|
||||
"views.common.Login.login": "ログイン",
|
||||
"views.common.Login.loginAndBind": "ログインして紐付け",
|
||||
"views.Admin.loginMethod": "ログイン方法",
|
||||
"views.user.UserBar.fetchUserSettingsError": "ログイン用パスワードが無効か、アカウントが存在しません。ネットワーク接続の問題の可能性もあるため、後でもう一度お試しください。",
|
||||
"views.user.UserLogin.loginWith": "{provider}でログイン",
|
||||
"views.user.UserLogin.loginWithPasskey": "パスキーでログイン",
|
||||
"views.user.UserSettings.logout": "ログアウト",
|
||||
"views.user.BindAddress.logout": "ログアウト",
|
||||
"views.Admin.logout": "ログアウト",
|
||||
"views.index.AccountSettings.logout": "ログアウト",
|
||||
"views.Admin.logoutSuccess": "ログアウトしました",
|
||||
"views.admin.UserOauth2Settings.mailAllowList": "メールアドレス許可リスト",
|
||||
"views.admin.UserSettings.mailAllowList": "メールアドレス許可リスト",
|
||||
"views.index.SimpleIndex.addressCredential": "メールアドレス資格情報",
|
||||
"views.index.AddressBar.addressCredential": "メールアドレス資格情報",
|
||||
"views.admin.Account.addressCredential": "メールアドレス資格情報",
|
||||
"views.admin.CreateAccount.addressCredential": "メールアドレス資格情報",
|
||||
"views.index.AddressBar.fetchAddressError": "メール資格情報が無効かアカウントが存在しません。ネットワークの問題の可能性もあります。後でもう一度お試しください。",
|
||||
"views.Index.mailbox": "受信箱",
|
||||
"views.User.user_mail_box_tab": "受信箱",
|
||||
"views.user.AddressManagement.mail_count": "メール数",
|
||||
"views.admin.Account.mail_count": "メール数",
|
||||
"views.admin.Statistics.mailCount": "メール数",
|
||||
"views.admin.UserAddressManagement.mail_count": "メール数",
|
||||
"views.index.SimpleIndex.deleteSuccess": "メールを削除しました",
|
||||
"views.user.UserLogin.cannotForgotPassword": "メール検証または登録が無効のため、パスワードを再設定できません。管理者へ連絡してください。",
|
||||
"views.Admin.mailWebhook": "メールWebhook",
|
||||
"views.common.Appearance.mailboxSplitSize": "メールボックス2カラム表示の左側リスト幅",
|
||||
"views.common.Appearance.mailListView": "メールボックス全幅リスト表示",
|
||||
"views.common.Appearance.mailListPreviewLineClamp": "本文プレビュー行数",
|
||||
"views.common.Appearance.off": "オフ",
|
||||
"views.index.SimpleIndex.refreshSuccess": "メールを更新しました",
|
||||
"views.Admin.unknow": "受信者不明のメール",
|
||||
"views.Admin.maintenance": "メンテナンス",
|
||||
"views.index.AddressBar.addressManage": "管理",
|
||||
"views.admin.AccountSettings.source_match_mode": "一致モード",
|
||||
"views.admin.RoleAddressConfig.maxAddressCount": "最大アドレス数 (0 = 無制限)",
|
||||
"views.admin.UserSettings.maxAddressCount": "紐付け可能なメールアドレスの最大数(0 = 無制限)",
|
||||
"views.Header.menu": "メニュー",
|
||||
"views.admin.DatabaseManager.migration": "データベースを移行",
|
||||
"views.admin.SenderAccess.modify": "変更",
|
||||
"views.admin.AccountSettings.send_mail_monthly_limit": "月次上限",
|
||||
"views.admin.AccountSettings.send_mail_monthly_limit_invalid": "月次上限は -1 以上の整数である必要があります",
|
||||
"components.MailBox.multiAction": "複数操作",
|
||||
"components.SendBox.multiAction": "複数操作",
|
||||
"views.admin.Account.multiClearInbox": "受信箱を一括削除",
|
||||
"views.admin.Account.multiClearSentItems": "送信済みを一括削除",
|
||||
"views.admin.Account.multiDelete": "一括削除",
|
||||
"views.user.AddressManagement.name": "名前",
|
||||
"views.index.AutoReply.name": "名前",
|
||||
"views.admin.Account.name": "名前",
|
||||
"views.admin.UserOauth2Settings.name": "名前",
|
||||
"views.admin.UserAddressManagement.name": "名前",
|
||||
"views.admin.Maintenance.sqlName": "名前",
|
||||
"views.index.AccountSettings.newPassword": "新しいパスワード",
|
||||
"views.admin.Account.newPassword": "新しいパスワード",
|
||||
"components.MailBox.nextMail": "次へ",
|
||||
"views.index.SimpleIndex.nextPage": "次へ",
|
||||
"views.admin.AccountSettings.noLimitSendAddressList": "残高無制限の送信アドレス一覧",
|
||||
"views.index.SimpleIndex.noMails": "メールが見つかりません",
|
||||
"views.admin.RoleAddressConfig.noRolesAvailable": "システム設定に利用可能なロールがありません",
|
||||
"views.index.SendMail.requestAccessTip": "送信権限と残高はユーザーアカウントではなく現在のメールアドレスに属します。このアドレスの権限を申請するか、管理者に連絡してください。",
|
||||
"components.SendBox.emptySent": "送信済みメールはありません",
|
||||
"views.admin.RoleAddressConfig.notConfigured": "未設定 (全体設定を使用)",
|
||||
"views.Admin.userOauth2Settings": "OAuth2設定",
|
||||
"views.admin.UserOauth2Settings.oauth2Type": "OAuth2タイプ",
|
||||
"views.Header.ok": "OK",
|
||||
"views.Admin.ok": "OK",
|
||||
"views.index.AddressBar.ok": "OK",
|
||||
"views.admin.SenderAccess.ok": "OK",
|
||||
"views.common.Login.ok": "OK",
|
||||
"views.admin.AccountSettings.create_address_subdomain_match_tip": "/api/new_address と /admin/new_address のドメイン検証にのみ影響します。例: 有効時は foo.example.com が example.com に一致できます。",
|
||||
"components.AiExtractInfo.open": "開く",
|
||||
"views.index.AddressBar.linkWithAddressCredential": "メールの自動ログインリンクを開く",
|
||||
"views.admin.CreateAccount.linkWithAddressCredential": "メールの自動ログインリンクを開く",
|
||||
"views.index.SendMail.options": "オプション",
|
||||
"views.admin.SendMail.options": "オプション",
|
||||
"components.AiExtractInfo.otherLink": "その他のリンク",
|
||||
"views.user.UserSettings.passkeyCreated": "Passkey を作成しました",
|
||||
"views.user.UserSettings.passkey_name": "Passkey 名",
|
||||
"views.user.UserLogin.password": "パスワード",
|
||||
"views.admin.UserManagement.password": "パスワード",
|
||||
"views.common.Login.password": "パスワード",
|
||||
"views.index.AccountSettings.passwordChanged": "パスワードを変更しました",
|
||||
"views.common.Login.passwordLogin": "パスワードログイン",
|
||||
"views.admin.Account.passwordResetSuccess": "パスワードをリセットしました",
|
||||
"views.index.AccountSettings.passwordMismatch": "パスワードが一致しません",
|
||||
"views.index.SendMail.successSend": "送信箱を確認してください。失敗した場合は残高を確認するか、後でもう一度お試しください。",
|
||||
"views.admin.SendMail.successSend": "送信箱を確認してください。失敗した場合は、しばらくしてから再試行してください。",
|
||||
"views.user.UserLogin.pleaseCompleteTurnstile": "Turnstile を完了してください",
|
||||
"views.index.SimpleIndex.addressCredentialTip": "メールアドレス資格情報をコピーしてください。この情報でログインできます。",
|
||||
"views.index.AddressBar.addressCredentialTip": "メールアドレス資格情報をコピーしてください。この情報でメールアカウントにログインできます。",
|
||||
"views.admin.Account.addressCredentialTip": "メールアドレス資格情報をコピーしてください。この情報でメールアカウントにログインできます。",
|
||||
"views.admin.CreateAccount.addressCredentialTip": "メールアドレス資格情報をコピーしてください。この情報でメールアカウントにログインできます。",
|
||||
"views.Admin.accessTip": "管理者パスワードを入力してください",
|
||||
"views.Header.accessTip": "正しいアクセス用パスワードを入力してください",
|
||||
"views.admin.AccountSettings.address_block_list_placeholder": "ブロックしたいキーワードを入力してください",
|
||||
"views.user.UserSettings.renamePasskeyNamePlaceholder": "新しい Passkey 名を入力してください",
|
||||
"views.user.UserSettings.passkeyNamePlaceholder": "Passkey 名を入力するか、空欄でランダム生成してください",
|
||||
"views.admin.CreateAccount.fillInAllFields": "すべての項目を入力してください",
|
||||
"views.admin.SendBox.queryTip": "検索するアドレスを入力してください。空欄で全件検索します",
|
||||
"views.user.UserLogin.pleaseInputCode": "コードを入力してください",
|
||||
"views.admin.UserManagement.pleaseInput": "必要な情報をすべて入力してください",
|
||||
"views.user.UserLogin.pleaseInputEmail": "メールを入力してください",
|
||||
"views.user.UserLogin.pleaseInput": "メールアドレスとパスワードを入力してください",
|
||||
"views.admin.Maintenance.tip": "日数を入力してください",
|
||||
"views.common.Login.getNewEmailTip1": "使用したいメールアドレスを入力してください。使用可能: ",
|
||||
"views.common.Login.credentialInput": "メールアドレス資格情報を入力してください",
|
||||
"views.admin.SenderAccess.modalTip": "送信残高を入力してください",
|
||||
"views.user.UserLogin.pleaseLogin": "ログインしてください",
|
||||
"views.common.Login.pleaseGetNewEmail": "ログインするか、「新しいメールを作成」をクリックして新しいアドレスを取得してください。",
|
||||
"components.WebhookComponent.fillInDemoTip": "URL とその他設定を自分のものに変更してください",
|
||||
"components.SendBox.pleaseSelectMail": "表示するメールを選択してください。",
|
||||
"views.admin.Account.pleaseSelectAddress": "アドレスを選択してください",
|
||||
"components.MailBox.pleaseSelectMail": "メールを選択してください",
|
||||
"views.admin.UserManagement.prefix": "プレフィックス",
|
||||
"components.WebhookComponent.presets": "プリセット",
|
||||
"views.index.SendMail.preview": "プレビュー",
|
||||
"views.admin.SendMail.preview": "プレビュー",
|
||||
"views.admin.UserOauth2Settings.iconPreview": "プレビュー",
|
||||
"components.MailBox.prevMail": "前へ",
|
||||
"views.index.SimpleIndex.prevPage": "前へ",
|
||||
"components.MailBox.query": "検索",
|
||||
"views.user.UserMailBox.query": "検索",
|
||||
"views.Index.query": "検索",
|
||||
"views.admin.SendBox.query": "検索",
|
||||
"views.admin.Account.query": "検索",
|
||||
"views.admin.SenderAccess.query": "検索",
|
||||
"views.admin.UserManagement.query": "検索",
|
||||
"views.admin.Mails.query": "検索",
|
||||
"views.Admin.qucickSetup": "クイック設定",
|
||||
"views.index.SendMail.toMailEmpty": "受信者アドレスが空です",
|
||||
"views.admin.SendMail.toMailEmpty": "受信者アドレスが空です",
|
||||
"views.index.SendMail.toName": "受信者名とアドレス。名前を空欄にするとメールアドレスを使用します",
|
||||
"views.admin.SendMail.toName": "受信者名とアドレス。名前を空欄にするとメールアドレスを使用します",
|
||||
"components.MailBox.refresh": "更新",
|
||||
"components.Turnstile.refresh": "更新",
|
||||
"components.SendBox.refresh": "更新",
|
||||
"views.index.SimpleIndex.refreshMails": "更新",
|
||||
"components.MailBox.refreshAfter": "あと {msg} 秒で更新",
|
||||
"views.index.SimpleIndex.refreshAfter": "あと {msg} 秒で更新",
|
||||
"views.admin.AccountSettings.regex_too_long": "正規表現が長すぎます(最大 200 文字)",
|
||||
"views.user.UserLogin.register": "登録",
|
||||
"views.user.UserSettings.renamePasskey": "Passkeyを名前変更",
|
||||
"views.admin.UserOauth2Settings.userEmailReplace": "置換テンプレート",
|
||||
"components.MailBox.reply": "返信",
|
||||
"components.MailContentRenderer.reply": "返信",
|
||||
"views.index.SendMail.requestAccess": "このアドレスの送信権限を申請",
|
||||
"views.user.UserLogin.resetPassword": "パスワードをリセット",
|
||||
"views.admin.Account.resetPassword": "パスワードをリセット",
|
||||
"views.admin.UserManagement.resetPassword": "パスワードをリセット",
|
||||
"views.index.SendMail.rich text": "リッチテキスト",
|
||||
"views.admin.SendMail.rich text": "リッチテキスト",
|
||||
"views.common.Appearance.right": "右",
|
||||
"views.admin.UserManagement.role": "ロール",
|
||||
"views.admin.RoleAddressConfig.role": "ロール",
|
||||
"views.Admin.roleAddressConfig": "ロール別アドレス設定",
|
||||
"views.admin.AccountSettings.rule_index": "ルール",
|
||||
"views.Index.s3Attachment": "S3添付ファイル",
|
||||
"components.WebhookComponent.save": "保存",
|
||||
"views.index.AutoReply.save": "保存",
|
||||
"views.admin.AiExtractSettings.save": "保存",
|
||||
"views.admin.UserOauth2Settings.save": "保存",
|
||||
"views.admin.IpBlacklistSettings.save": "保存",
|
||||
"views.admin.Telegram.save": "保存",
|
||||
"views.admin.UserSettings.save": "保存",
|
||||
"views.admin.RoleAddressConfig.save": "保存",
|
||||
"views.admin.AccountSettings.save": "保存",
|
||||
"views.admin.Maintenance.save": "保存",
|
||||
"views.admin.Webhook.save": "保存",
|
||||
"views.admin.Maintenance.saveSuccess": "保存しました",
|
||||
"views.admin.UserOauth2Settings.successTip": "保存しました",
|
||||
"views.admin.IpBlacklistSettings.successTip": "保存しました",
|
||||
"views.admin.UserSettings.successTip": "保存しました",
|
||||
"views.admin.AccountSettings.successTip": "保存しました",
|
||||
"components.MailBox.saveToS3": "S3 に保存",
|
||||
"components.MailContentRenderer.saveToS3": "S3 に保存",
|
||||
"views.Index.saveToS3Success": "S3 への保存に成功しました",
|
||||
"components.MailBox.selectAll": "このページをすべて選択",
|
||||
"components.SendBox.selectAll": "このページをすべて選択",
|
||||
"views.admin.Account.selectAll": "このページをすべて選択",
|
||||
"views.admin.AccountSettings.select_domain": "ドメインを選択",
|
||||
"views.admin.Account.selectedItems": "選択済み",
|
||||
"views.index.SendMail.send": "送信",
|
||||
"views.admin.SendMail.send": "送信",
|
||||
"views.Index.sendbox": "送信箱",
|
||||
"views.Admin.sendBox": "送信箱",
|
||||
"views.user.AddressManagement.send_count": "送信数",
|
||||
"views.admin.Account.send_count": "送信数",
|
||||
"views.admin.UserAddressManagement.send_count": "送信数",
|
||||
"views.Index.sendmail": "メール送信",
|
||||
"views.Admin.sendMail": "メール送信",
|
||||
"views.index.SendMail.send_balance": "残り送信枠",
|
||||
"views.admin.Statistics.sendMailCount": "送信メール数",
|
||||
"views.admin.AccountSettings.send_mail_limit": "送信上限",
|
||||
"views.user.UserLogin.sendVerificationCode": "認証コードを送信",
|
||||
"views.Admin.senderAccess": "送信権限管理",
|
||||
"views.admin.SendMail.fromMailEmpty": "送信元アドレスが空です",
|
||||
"views.index.AutoReply.sourcePrefix": "送信者フィルター",
|
||||
"components.AiExtractInfo.serviceLink": "サービスリンク",
|
||||
"views.index.AutoReply.settings": "設定",
|
||||
"views.index.AccountSettings.showAddressCredential": "アドレス資格情報を表示",
|
||||
"components.MailBox.attachments": "添付ファイルを表示",
|
||||
"components.MailBox.showHtmlMail": "HTMLメールを表示",
|
||||
"components.MailContentRenderer.showHtmlMail": "HTMLメールを表示",
|
||||
"views.admin.Account.showCredential": "メールアドレス資格情報を表示",
|
||||
"views.user.UserSettings.showPasskeyList": "Passkey 一覧を表示",
|
||||
"components.MailBox.showTextMail": "テキストメールを表示",
|
||||
"components.MailContentRenderer.showTextMail": "テキストメールを表示",
|
||||
"views.Index.enterSimpleMode": "シンプルモード",
|
||||
"components.MailContentRenderer.size": "サイズ",
|
||||
"views.admin.Account.source_meta": "送信元",
|
||||
"views.admin.AccountSettings.source_patterns": "送信元アドレス正規表現 (任意)",
|
||||
"views.admin.Maintenance.sqlStatement": "SQL文 (DELETEのみ)",
|
||||
"views.user.UserOauth2Callback.stateNotMatch": "state が一致しません",
|
||||
"views.Admin.statistics": "統計",
|
||||
"views.Header.status": "ステータス",
|
||||
"views.admin.IpBlacklistSettings.enable_whitelist_tip": "厳格モード: ホワイトリストに一致する IP のみがレート制限対象 API にアクセスできます。それ以外の IP はすべて拒否されます。",
|
||||
"views.index.AutoReply.subject": "件名",
|
||||
"views.index.SendMail.subject": "件名",
|
||||
"views.admin.SendMail.subject": "件名",
|
||||
"views.index.SendMail.subjectEmpty": "件名が空です",
|
||||
"views.admin.SendMail.subjectEmpty": "件名が空です",
|
||||
"components.AiExtractInfo.subscriptionLink": "購読リンク",
|
||||
"views.user.AddressManagement.success": "成功",
|
||||
"views.index.TelegramAddress.success": "成功",
|
||||
"views.index.LocalAddress.success": "成功",
|
||||
"views.admin.UserAddressManagement.success": "成功",
|
||||
"components.WebhookComponent.successTip": "成功",
|
||||
"components.MailBox.success": "成功",
|
||||
"components.SendBox.success": "成功",
|
||||
"views.index.AccountSettings.success": "成功",
|
||||
"views.index.AutoReply.success": "成功",
|
||||
"views.admin.AiExtractSettings.successTip": "成功",
|
||||
"views.admin.Account.success": "成功",
|
||||
"views.admin.SenderAccess.success": "成功",
|
||||
"views.admin.UserManagement.success": "成功",
|
||||
"views.admin.Telegram.successTip": "成功",
|
||||
"views.admin.RoleAddressConfig.successTip": "成功",
|
||||
"views.admin.Webhook.successTip": "成功",
|
||||
"views.admin.CreateAccount.successTip": "作成に成功",
|
||||
"views.admin.Telegram.globalMailPushListTip": "個人/グループ/チャンネルの chat_id をサポートします。ボットへメッセージ送信後、このリンクで chat_id を確認できます: https://api.telegram.org/bot<あなたの BOT TOKEN に置換>/getUpdates",
|
||||
"views.user.AddressManagement.targetUserEmail": "対象ユーザーのメール",
|
||||
"views.admin.Telegram.telegramAllowList": "Telegram 許可リスト (Chat ID を手動入力)",
|
||||
"views.Admin.telegram": "Telegramボット",
|
||||
"views.admin.Telegram.miniAppUrl": "Telegram Mini App URL",
|
||||
"components.WebhookComponent.test": "テスト",
|
||||
"views.index.SendMail.text": "テキスト",
|
||||
"views.admin.SendMail.text": "テキスト",
|
||||
"views.user.UserSettings.passordTip": "サーバーはパスワードのハッシュ値のみ受け取り、平文は受け取りません。そのためパスワードを閲覧・復元できません。管理者がメール検証を有効にしている場合はシークレットモードで再設定できます。",
|
||||
"views.index.LocalAddress.tip": "これらのアドレスはブラウザに保存されており、キャッシュを消すと失われる可能性があります。",
|
||||
"views.admin.UserOauth2Settings.tip": "サードパーティログインではユーザーのメールアドレスで自動的にアカウント登録されます(同じメールは同一アカウントとして扱われます)。「パスワードを忘れた」からパスワード設定も可能です。",
|
||||
"views.admin.AccountSettings.send_mail_limit_tip": "すべての送信チャネルに適用されます。-1 は無制限、0 は送信禁止です。",
|
||||
"views.admin.AccountSettings.create_address_subdomain_match_note": "これは RANDOM_SUBDOMAIN_DOMAINS と異なります。この設定では API 呼び出し側が独自サブドメインを指定でき、ランダムサブドメインは作成時に自動生成されるだけです。",
|
||||
"views.index.SendMail.tooLarge": "ファイルが大きすぎます。1MB 未満のファイルをアップロードしてください。",
|
||||
"views.admin.SendMail.tooLarge": "ファイルが大きすぎます。1MB 未満のファイルをアップロードしてください。",
|
||||
"views.common.Appearance.top": "上",
|
||||
"views.user.AddressManagement.transferAddress": "アドレスを移行",
|
||||
"views.user.AddressManagement.transferAddressTip": "別のユーザーへアドレスを移行すると、そのアドレスはあなたのアカウントから削除され、相手のアカウントへ移されます。本当に移行しますか?",
|
||||
"views.common.Appearance.useSideMargin": "ページ左右の余白を有効にする",
|
||||
"views.admin.AiExtractSettings.manualInputPrompt": "入力して Enter で追加",
|
||||
"views.admin.UserOauth2Settings.manualInputPrompt": "入力して Enter で追加",
|
||||
"views.admin.Telegram.manualInputPrompt": "入力して Enter で追加",
|
||||
"views.admin.UserSettings.manualInputPrompt": "入力して Enter で追加",
|
||||
"views.admin.AccountSettings.manualInputPrompt": "入力して Enter で追加",
|
||||
"views.admin.Webhook.manualInputPrompt": "入力して Enter で追加",
|
||||
"views.admin.IpBlacklistSettings.manualInputPrompt": "パターンを入力して Enter で追加",
|
||||
"views.user.AddressManagement.unbindAddress": "アドレスの紐付けを解除",
|
||||
"views.index.TelegramAddress.unbindMailAddress": "メールアドレスの紐付けを解除",
|
||||
"views.index.LocalAddress.unbindMailAddress": "メールアドレス資格情報の紐付けを解除",
|
||||
"components.MailBox.unselectAll": "すべての選択を解除",
|
||||
"components.SendBox.unselectAll": "すべての選択を解除",
|
||||
"views.admin.Account.unselectAll": "すべての選択を解除",
|
||||
"views.admin.Account.updated_at": "更新日時",
|
||||
"views.user.UserSettings.updated_at": "更新日時",
|
||||
"components.WebhookComponent.urlMissing": "URL は必須です",
|
||||
"views.common.Appearance.useIframeShowMail": "iframe で HTML メールを表示",
|
||||
"views.admin.CreateAccount.enableRandomSubdomain": "ランダムサブドメインを使用",
|
||||
"views.common.Login.enableRandomSubdomain": "ランダムサブドメインを使用",
|
||||
"views.admin.UserOauth2Settings.userEmailFormatTip": "正規表現でメールを変換します。例: ^(.+)@old\\.com$ と $1@new.com",
|
||||
"views.common.Appearance.useSimpleIndex": "シンプルインデックスを使う",
|
||||
"views.common.Appearance.useUTCDate": "UTC 日時を使う",
|
||||
"views.Header.user": "ユーザー",
|
||||
"views.Admin.user": "ユーザーアカウント",
|
||||
"components.AddressSelect.userAddresses": "ユーザーアカウントに紐付くアドレス",
|
||||
"views.Admin.loginViaUserAdmin": "ユーザー管理者権限",
|
||||
"views.admin.Statistics.userCount": "ユーザー数",
|
||||
"views.admin.UserManagement.user_email": "ユーザーアカウントのメール",
|
||||
"views.index.AddressBar.userLogin": "ユーザーアカウントログイン",
|
||||
"views.Admin.user_management": "ユーザーアカウント管理",
|
||||
"views.Admin.user_settings": "ユーザーアカウント設定",
|
||||
"views.User.user_settings": "ユーザーアカウント設定",
|
||||
"components.AiExtractInfo.authCode": "認証コード",
|
||||
"views.user.UserLogin.verifyCode": "認証コード",
|
||||
"views.user.UserLogin.verifyCodeSent": "認証コードを送信しました, 有効期限 {timeout} 秒",
|
||||
"views.admin.AccountSettings.verified_address_list": "検証済みアドレス一覧 (cf internal api でメール送信可能)",
|
||||
"views.admin.UserSettings.verifyMailSender": "メール送信元を検証",
|
||||
"components.MailContentRenderer.attachments": "添付ファイルを表示",
|
||||
"views.admin.Account.viewMails": "メールを表示",
|
||||
"views.admin.Account.viewSendBox": "送信箱を表示",
|
||||
"views.user.UserLogin.waitforVerifyCode": "待機 {timeout} 秒",
|
||||
"views.admin.Webhook.webhookAllowList": "Webhook 許可リスト (利用を許可するメールアドレスを入力して Enter)",
|
||||
"views.admin.Webhook.notEnabled": "Webhook は有効ではありません",
|
||||
"components.WebhookComponent.notEnabled": "あなたには Webhook が有効ではありません",
|
||||
"views.Index.webhookSettings": "Webhook設定",
|
||||
"views.Admin.webhookSettings": "Webhook設定",
|
||||
"views.admin.AiExtractSettings.disabledTip": "無効時は AI 抽出がすべてのメールアドレスを処理します",
|
||||
"views.admin.AiExtractSettings.enableAllowListTip": "有効時は AI 抽出は許可リストのアドレス宛メールのみ処理します",
|
||||
"views.admin.CreateAccount.randomSubdomainTip": "有効時は作成されるアドレスがランダムなサブドメインを使用します。受信専用として推奨されます。ベースドメインの DNS にワイルドカード MX レコードの設定が必要です — ランダムサブドメインのドキュメントを参照してください。",
|
||||
"views.common.Login.randomSubdomainTip": "有効時は作成されるアドレスがランダムなサブドメインを使用します。受信専用として推奨されます。ベースドメインの DNS にワイルドカード MX レコードの設定が必要です — ランダムサブドメインのドキュメントを参照してください。",
|
||||
"views.admin.AiExtractSettings.allowListTip": "ワイルドカード * は任意の文字に一致します。例: *{'@'}example.com は example.com ドメイン配下のすべてのアドレスに一致します",
|
||||
"views.Admin.workerconfig": "Worker設定",
|
||||
"views.admin.AccountSettings.create_address_subdomain_match_env_locked": "Worker 環境変数 ENABLE_CREATE_ADDRESS_SUBDOMAIN_MATCH は現在 false です。管理画面のスイッチは保存できますが、env を有効化または削除するまで反映されません。",
|
||||
"views.common.Login.getNewEmailTip3": "ドロップダウン一覧からドメインを選択できます。",
|
||||
"views.admin.AccountSettings.tip": "以下の複数選択項目は手動入力して Enter で追加できます",
|
||||
"components.MailBox.emptyInbox": "受信箱は空です",
|
||||
"views.index.SendMail.fromName": "あなたの名前とアドレス。名前を空欄にするとメールアドレスを使用します",
|
||||
"views.admin.SendMail.fromName": "あなたの名前とアドレス。名前を空欄にするとメールアドレスを使用します",
|
||||
"components.AddressCredentialModal.addressCredential": "アドレス認証情報",
|
||||
"components.AddressCredentialModal.addressCredentialLabel": "Address JWT",
|
||||
"components.AddressCredentialModal.addressPassword": "アドレスパスワード",
|
||||
"components.AddressCredentialModal.agentAccess": "AI Agent",
|
||||
"components.AddressCredentialModal.agentAccessTip": "AI Agent から Address JWT と parsed-mail API を使ってこのメールボックスを利用できます。",
|
||||
"components.AddressCredentialModal.agentConfig": "Agent 設定",
|
||||
"components.AddressCredentialModal.agentSkill": "Agent skill",
|
||||
"components.AddressCredentialModal.apiBase": "API ベース",
|
||||
"components.AddressCredentialModal.autoLoginLink": "自動ログインリンク",
|
||||
"components.AddressCredentialModal.copyFailed": "コピーに失敗しました",
|
||||
"components.AddressCredentialModal.copySection": "コピー",
|
||||
"components.AddressCredentialModal.copySuccess": "コピーしました",
|
||||
"components.AddressCredentialModal.currentAddress": "現在のアドレス",
|
||||
"components.AddressCredentialModal.docs": "ドキュメント",
|
||||
"components.AddressCredentialModal.imapHost": "IMAP ホスト",
|
||||
"components.AddressCredentialModal.imapPort": "IMAP ポート",
|
||||
"components.AddressCredentialModal.password": "パスワード",
|
||||
"components.AddressCredentialModal.plainOrProxyTls": "平文またはプロキシ側 TLS",
|
||||
"components.AddressCredentialModal.security": "セキュリティ",
|
||||
"components.AddressCredentialModal.smtpHost": "SMTP ホスト",
|
||||
"components.AddressCredentialModal.smtpImapAccess": "SMTP / IMAP",
|
||||
"components.AddressCredentialModal.smtpImapTip": "管理者が SMTP/IMAP プロキシを設定した後、メールクライアントでこれらの値を使用できます。パスワードにはここに表示される Address JWT、または手元にあるアドレスパスワードを使用できます。",
|
||||
"components.AddressCredentialModal.smtpPort": "SMTP ポート",
|
||||
"components.AddressCredentialModal.starttls": "STARTTLS",
|
||||
"components.AddressCredentialModal.tip": "これらの認証情報は信頼できるクライアントと Agent でのみ使用してください。",
|
||||
"components.AddressCredentialModal.title": "アドレス認証情報と接続方法",
|
||||
"components.AddressCredentialModal.username": "ユーザー名"
|
||||
}
|
||||
@@ -0,0 +1,621 @@
|
||||
export const ptBRMessages = {
|
||||
"views.index.SimpleIndex.mailCount": "{current} / {total} e-mails",
|
||||
"views.admin.Statistics.activeAddressCount30days": "Quantidade de endereços ativos em 30 dias",
|
||||
"views.admin.Statistics.activeAddressCount7days": "Quantidade de endereços ativos em 7 dias",
|
||||
"views.Index.about": "Sobre",
|
||||
"views.Admin.about": "Sobre",
|
||||
"views.Header.accessHeader": "Senha de acesso",
|
||||
"views.Admin.account": "Endereços de e-mail",
|
||||
"views.Index.accountSettings": "Configurações do endereço",
|
||||
"views.Admin.account_settings": "Configurações de endereços",
|
||||
"views.index.SimpleIndex.accountSettings": "Configurações do endereço",
|
||||
"views.index.Attachment.action": "Ação",
|
||||
"views.admin.SenderAccess.action": "Ação",
|
||||
"views.user.UserSettings.actions": "Ações",
|
||||
"views.user.AddressManagement.actions": "Ações",
|
||||
"views.index.TelegramAddress.actions": "Ações",
|
||||
"views.admin.Account.actions": "Ações",
|
||||
"views.index.LocalAddress.actions": "Ações",
|
||||
"views.admin.UserManagement.actions": "Ações",
|
||||
"views.admin.AccountSettings.actions": "Ações",
|
||||
"views.admin.AccountSettings.add": "Adicionar",
|
||||
"views.admin.Maintenance.customSqlTip": "Adicione instruções SQL DELETE personalizadas para a limpeza agendada. Apenas uma instrução DELETE é permitida por item.",
|
||||
"views.admin.Maintenance.addCustomSql": "Adicionar SQL personalizada",
|
||||
"views.admin.UserOauth2Settings.addOauth2": "Adicionar OAuth2",
|
||||
"components.AddressSelect.address": "Endereço",
|
||||
"views.user.AddressManagement.address": "Endereço",
|
||||
"views.index.TelegramAddress.address": "Endereço",
|
||||
"views.admin.SenderAccess.address": "Endereço",
|
||||
"views.index.LocalAddress.address": "Endereço",
|
||||
"views.admin.CreateAccount.address": "Endereço",
|
||||
"views.admin.AiExtractSettings.allowList": "Lista branca de endereços (digite o endereço e pressione Enter; suporta curingas)",
|
||||
"views.admin.AccountSettings.send_address_block_list": "Palavras-chave bloqueadas para envio de e-mail",
|
||||
"views.admin.AccountSettings.address_block_list": "Palavras-chave bloqueadas para usuários (o administrador pode ignorar)",
|
||||
"views.index.SimpleIndex.addressCopied": "Endereço copiado com sucesso",
|
||||
"views.admin.Statistics.addressCount": "Quantidade de endereços",
|
||||
"views.admin.UserManagement.address_count": "Quantidade de endereços",
|
||||
"views.User.address_management": "Gerenciamento de endereços",
|
||||
"views.admin.UserManagement.userAddressManagement": "Gerenciamento de endereços",
|
||||
"views.index.AddressBar.addressPassword": "Senha do endereço",
|
||||
"views.admin.CreateAccount.addressPassword": "Senha do endereço",
|
||||
"views.Admin.adminAccount": "Administrador",
|
||||
"views.Admin.accessHeader": "Senha de administrador",
|
||||
"views.Admin.loginViaPassword": "Entrar como administrador com senha",
|
||||
"views.admin.AiExtractSettings.title": "Configurações de extração de e-mail por IA",
|
||||
"views.Admin.aiExtractSettings": "Configurações de extração por IA",
|
||||
"views.admin.AccountSettings.match_all": "Todos",
|
||||
"views.admin.AccountSettings.create_address_subdomain_match": "Permitir correspondência por sufixo de subdomínio ao criar endereços",
|
||||
"views.admin.UserSettings.enableUserRegister": "Permitir cadastro de usuários",
|
||||
"views.admin.AccountSettings.match_any": "Qualquer",
|
||||
"views.Index.appearance": "Aparência",
|
||||
"views.Admin.appearance": "Aparência",
|
||||
"views.admin.IpBlacklistSettings.tip_scope": "Aplica-se a: criar endereço, enviar e-mail, API externa de envio, cadastro de usuário e verificação de código",
|
||||
"views.index.AccountSettings.clearInboxConfirm": "Tem certeza de que deseja limpar todos os e-mails da sua caixa de entrada?",
|
||||
"views.index.AccountSettings.clearSentItemsConfirm": "Tem certeza de que deseja limpar todos os e-mails enviados?",
|
||||
"views.admin.Account.multiClearInboxTip": "Tem certeza de que deseja limpar a caixa de entrada dos endereços selecionados?",
|
||||
"views.admin.Account.clearInboxTip": "Tem certeza de que deseja limpar a caixa de entrada deste e-mail?",
|
||||
"views.admin.Account.multiClearSentItemsTip": "Tem certeza de que deseja limpar os itens enviados dos endereços selecionados?",
|
||||
"views.admin.Account.clearSentItemsTip": "Tem certeza de que deseja limpar os itens enviados deste e-mail?",
|
||||
"views.admin.Account.multiDeleteTip": "Tem certeza de que deseja excluir os endereços selecionados?",
|
||||
"views.index.Attachment.deleteConfirm": "Tem certeza de que deseja excluir este anexo?",
|
||||
"views.admin.Account.deleteTip": "Tem certeza de que deseja excluir este e-mail?",
|
||||
"views.admin.SenderAccess.deleteTip": "Tem certeza de que deseja excluir isto?",
|
||||
"views.index.AccountSettings.deleteAccountConfirm": "Tem certeza de que deseja excluir este endereço e todos os e-mails dele?",
|
||||
"views.index.AccountSettings.logoutConfirm": "Tem certeza de que deseja sair?",
|
||||
"components.MailBox.deleteMailTip": "Tem certeza de que deseja excluir o e-mail?",
|
||||
"components.MailContentRenderer.deleteMailTip": "Tem certeza de que deseja excluir o e-mail?",
|
||||
"components.SendBox.deleteMailTip": "Tem certeza de que deseja excluir o e-mail?",
|
||||
"views.admin.AccountSettings.delete_rule_confirm": "Tem certeza de que deseja excluir esta regra?",
|
||||
"views.admin.UserManagement.deleteUserTip": "Tem certeza de que deseja excluir esta conta de usuário?",
|
||||
"views.Admin.logoutConfirmContent": "Tem certeza de que deseja sair do painel de administração?",
|
||||
"views.user.UserSettings.logoutConfirm": "Tem certeza de que deseja sair?",
|
||||
"views.admin.IpBlacklistSettings.asn_blacklist": "Lista negra de organizações ASN",
|
||||
"views.admin.IpBlacklistSettings.tip_asn": "Organização ASN: bloquear por ISP/provedor. Correspondência de texto sem diferenciar maiúsculas ou regex.",
|
||||
"components.AiExtractInfo.authLink": "Link de autenticação",
|
||||
"views.admin.Maintenance.autoCleanup": "Limpeza automática",
|
||||
"components.MailBox.autoRefresh": "Atualização automática",
|
||||
"components.MailBox.backToList": "Voltar para a lista",
|
||||
"views.common.Appearance.autoRefreshInterval": "Intervalo de atualização automática (s)",
|
||||
"views.Index.auto_reply": "Resposta automática",
|
||||
"views.index.AutoReply.autoReply": "Resposta automática",
|
||||
"views.common.Login.autoGeneratedName": "Nome gerado automaticamente",
|
||||
"views.admin.SenderAccess.balance": "Saldo",
|
||||
"views.admin.Maintenance.basicCleanup": "Limpeza básica",
|
||||
"views.user.AddressManagement.unbindAddressTip": "Antes de desvincular, mude para este endereço e salve a credencial do e-mail.",
|
||||
"views.index.TelegramAddress.bind": "Vincular",
|
||||
"views.index.TelegramAddress.bindAddressSuccess": "Endereço vinculado com sucesso",
|
||||
"views.index.LocalAddress.bindAddressSuccess": "Endereço vinculado com sucesso",
|
||||
"views.User.bind_address": "Vincular endereço de e-mail",
|
||||
"views.admin.IpBlacklistSettings.enable_tip": "Bloquear IPs que correspondam à lista negra nas APIs com limite",
|
||||
"views.admin.AccountSettings.fromBlockList": "Palavras bloqueadas para receber e-mail",
|
||||
"views.admin.AccountSettings.block_receive_unknow_address_email": "Bloquear recebimento de e-mails para endereços desconhecidos",
|
||||
"views.common.Appearance.bottom": "base",
|
||||
"views.admin.IpBlacklistSettings.fingerprint_blacklist": "Lista negra de impressão digital do navegador",
|
||||
"views.admin.IpBlacklistSettings.tip_fingerprint": "Impressão digital do navegador: bloqueia por impressão. Suporta correspondência exata ou regex.",
|
||||
"views.admin.AccountSettings.cancel": "Cancelar",
|
||||
"components.MailBox.cancelMultiAction": "Cancelar múltipla seleção",
|
||||
"components.SendBox.cancelMultiAction": "Cancelar múltipla seleção",
|
||||
"views.user.AddressManagement.changeMailAddress": "Alterar endereço",
|
||||
"views.index.TelegramAddress.changeMailAddress": "Alterar endereço de e-mail",
|
||||
"views.index.LocalAddress.changeMailAddress": "Alterar endereço de e-mail",
|
||||
"views.index.AccountSettings.changePassword": "Alterar senha",
|
||||
"views.admin.UserManagement.changeRole": "Alterar Função",
|
||||
"components.SendBox.showCode": "Alternar visualização do código original",
|
||||
"views.admin.Telegram.status": "Ver status",
|
||||
"views.admin.AccountSettings.create_address_subdomain_match_follow_env_note": "Escolher \"Seguir variável de ambiente\" remove a sobrescrita do administrador e retorna ao estado não definido. O resultado efetivo continua sendo controlado pelas variáveis do Worker e pelas regras de precedência.",
|
||||
"views.admin.Maintenance.cleanupNow": "Limpar agora",
|
||||
"views.admin.Maintenance.cleanupSuccess": "Limpeza concluída",
|
||||
"views.admin.Maintenance.addressCreateLabel": "Limpar os endereços criados há mais de n dias",
|
||||
"views.admin.Maintenance.emptyAddressLabel": "Limpar os endereços vazios de mais de n dias",
|
||||
"views.admin.Maintenance.inactiveAddressLabel": "Limpar os endereços inativos de mais de n dias",
|
||||
"views.admin.Maintenance.mailBoxLabel": "Limpar a caixa de entrada de mais de n dias",
|
||||
"views.admin.Maintenance.sendBoxLabel": "Limpar a caixa de saída de mais de n dias",
|
||||
"views.admin.Maintenance.unboundAddressLabel": "Limpar endereços desvinculados há n dias",
|
||||
"views.admin.Maintenance.mailUnknowLabel": "Limpar os e-mails com destinatário desconhecido de mais de n dias",
|
||||
"views.index.AccountSettings.clearInbox": "Limpar caixa de entrada",
|
||||
"views.admin.Account.clearInbox": "Limpar caixa de entrada",
|
||||
"views.index.AccountSettings.clearSentItems": "Limpar itens enviados",
|
||||
"views.admin.Account.clearSentItems": "Limpar itens enviados",
|
||||
"views.Header.title": "Cloudflare E-mail Temporário",
|
||||
"views.admin.DatabaseManager.code_db_version": "Versão do banco exigida pelo código",
|
||||
"views.user.UserOauth2Callback.codeNotFound": "código não encontrado",
|
||||
"views.admin.AccountSettings.config": "Configuração",
|
||||
"views.admin.RoleAddressConfig.roleConfigDesc": "Configure a quantidade máxima de endereços para cada função. Os limites por função têm prioridade sobre a configuração global. Use 0 para ilimitado.",
|
||||
"views.Admin.confirm": "Confirmar",
|
||||
"views.Admin.logoutConfirmTitle": "Confirmar saída",
|
||||
"views.index.AccountSettings.confirmPassword": "Confirmar senha",
|
||||
"views.index.SendMail.content": "Conteúdo",
|
||||
"views.admin.SendMail.content": "Conteúdo",
|
||||
"views.index.SendMail.contentEmpty": "O conteúdo está vazio",
|
||||
"views.admin.SendMail.contentEmpty": "O conteúdo está vazio",
|
||||
"components.AddressSelect.copied": "Copiado",
|
||||
"components.AiExtractInfo.copySuccess": "Copiado com sucesso",
|
||||
"components.AddressSelect.copy": "Copiar",
|
||||
"views.index.SimpleIndex.copyAddress": "Copiar",
|
||||
"components.AiExtractInfo.copyFailed": "Falha ao copiar",
|
||||
"views.Footer.copyright": "Direitos autorais",
|
||||
"views.Admin.account_create": "Criar endereço de e-mail",
|
||||
"views.admin.CreateAccount.creatNewEmail": "Criar novo e-mail",
|
||||
"views.common.Login.getNewEmail": "Criar novo e-mail",
|
||||
"views.user.AddressManagement.create_or_bind": "Criar ou vincular",
|
||||
"views.index.LocalAddress.create_or_bind": "Criar ou vincular",
|
||||
"views.user.UserSettings.createPasskey": "Criar passkey",
|
||||
"views.admin.UserManagement.createUser": "Criar conta de usuário",
|
||||
"views.user.UserSettings.created_at": "Criado em",
|
||||
"views.admin.Account.created_at": "Criado em",
|
||||
"views.admin.SenderAccess.created_at": "Criado em",
|
||||
"views.admin.UserManagement.created_at": "Criado em",
|
||||
"views.common.Login.credentialLogin": "Login com credencial",
|
||||
"views.admin.DatabaseManager.current_db_version": "Versão atual do banco",
|
||||
"views.user.UserBar.currentUser": "Conta de usuário atual",
|
||||
"views.admin.UserManagement.roleDonotExist": "A função atual não existe",
|
||||
"views.admin.Maintenance.customSqlCleanup": "Limpeza SQL personalizada",
|
||||
"views.admin.AccountSettings.send_mail_daily_limit": "Limite diário",
|
||||
"views.admin.AccountSettings.send_mail_daily_limit_invalid": "O limite diário deve ser um inteiro maior ou igual a -1",
|
||||
"views.admin.IpBlacklistSettings.tip_daily_limit": "Limite diário: restringe o número máximo de requisições por IP por dia (1-1000000).",
|
||||
"views.admin.IpBlacklistSettings.daily_request_limit": "Limite diário de solicitações",
|
||||
"views.Header.dark": "Escuro",
|
||||
"views.Admin.database": "Banco de dados",
|
||||
"views.admin.DatabaseManager.need_initialization_tip": "É necessário inicializar o banco de dados. Faça a inicialização primeiro.",
|
||||
"views.admin.DatabaseManager.initializationSuccess": "Banco de dados inicializado com sucesso",
|
||||
"views.admin.DatabaseManager.migrationSuccess": "Banco de dados migrado com sucesso",
|
||||
"views.admin.DatabaseManager.need_migration_tip": "É necessário migrar o banco de dados. Execute a migração primeiro.",
|
||||
"components.MailBox.delete": "Excluir",
|
||||
"components.MailContentRenderer.delete": "Excluir",
|
||||
"components.SendBox.delete": "Excluir",
|
||||
"views.index.Attachment.delete": "Excluir",
|
||||
"views.admin.Account.delete": "Excluir",
|
||||
"views.admin.UserOauth2Settings.delete": "Excluir",
|
||||
"views.admin.SenderAccess.delete": "Excluir",
|
||||
"views.admin.UserManagement.delete": "Excluir",
|
||||
"views.admin.AccountSettings.delete_rule": "Excluir",
|
||||
"views.admin.Maintenance.deleteCustomSql": "Excluir",
|
||||
"views.index.AccountSettings.deleteAccount": "Excluir endereço de e-mail",
|
||||
"views.admin.Account.deleteAccount": "Excluir endereço de e-mail",
|
||||
"views.user.UserSettings.deletePasskey": "Excluir passkey",
|
||||
"views.admin.AccountSettings.delete_success": "Excluído com sucesso",
|
||||
"views.admin.UserManagement.deleteUser": "Excluir conta de usuário",
|
||||
"views.index.Attachment.deleteSuccess": "Excluído com sucesso",
|
||||
"views.admin.SenderAccess.disable": "Desativar",
|
||||
"views.Admin.loginViaDisabledCheck": "Verificação de senha desativada",
|
||||
"views.common.Appearance.preferShowTextMail": "Exibir e-mail em texto por padrão",
|
||||
"views.admin.AccountSettings.domain_list": "Lista de domínios (opcional)",
|
||||
"views.admin.AccountSettings.source_patterns_tip": "A lista de domínios filtra pelo destinatário e o regex de origem filtra pelo remetente. Ambas as condições precisam corresponder para encaminhar (lógica AND). Deixe qualquer uma vazia para ignorá-la.",
|
||||
"views.admin.UserManagement.domains": "Domínios",
|
||||
"views.index.Attachment.download": "Baixar",
|
||||
"components.MailBox.downloadMail": "Baixar E-mail",
|
||||
"components.MailContentRenderer.downloadMail": "Baixar E-mail",
|
||||
"views.admin.Maintenance.sqlNamePlaceholder": "ex.: limpar logs antigos",
|
||||
"views.admin.Maintenance.sqlPlaceholder": "ex.: DELETE FROM raw_mails WHERE source GLOB '*{'@'}example.com' AND created_at < datetime('now', '-3 day')",
|
||||
"views.admin.AccountSettings.source_patterns_placeholder": "ex.: gmail.com",
|
||||
"views.admin.AccountSettings.forwarding_rule_warning": "Cada regra é executada de forma independente. O endereço de encaminhamento deve ser verificado.",
|
||||
"views.index.SendMail.edit": "Editar",
|
||||
"views.admin.SendMail.edit": "Editar",
|
||||
"views.user.UserLogin.email": "E-mail",
|
||||
"views.admin.UserManagement.email": "E-mail",
|
||||
"views.common.Login.email": "E-mail",
|
||||
"views.common.Login.credential": "Credencial do endereço de e-mail",
|
||||
"views.common.Login.emailPasswordRequired": "E-mail e senha são obrigatórios",
|
||||
"views.admin.UserSettings.emailCheckRegex": "Regex de verificação de e-mail (ex.: ^[^.]+{'@'}.+$ para impedir pontos antes de {'@'})",
|
||||
"views.admin.AccountSettings.email_forwarding_config": "Configuração de encaminhamento de e-mail",
|
||||
"views.admin.UserOauth2Settings.userEmailFormat": "Padrão regex de e-mail",
|
||||
"views.Admin.mails": "E-mails",
|
||||
"views.index.AutoReply.sourcePrefixPlaceholder": "Vazio=todos, prefixo ou /regex/",
|
||||
"components.WebhookComponent.enable": "Ativar",
|
||||
"views.admin.UserOauth2Settings.enable": "Ativar",
|
||||
"views.admin.SenderAccess.enable": "Ativar",
|
||||
"views.admin.Telegram.enable": "Ativar",
|
||||
"views.admin.UserSettings.enable": "Ativar",
|
||||
"views.admin.AiExtractSettings.enableAllowList": "Ativar Lista branca de endereços",
|
||||
"views.admin.Webhook.enableAllowList": "Ativar lista de permissão (restringir o webhook a endereços específicos)",
|
||||
"views.index.AutoReply.enableAutoReply": "Ativar resposta automática",
|
||||
"views.admin.Maintenance.cronTip": "Para ativar a limpeza por cron, configure [crons] no worker. Consulte a documentação; 0 dias significa limpar tudo.",
|
||||
"views.admin.IpBlacklistSettings.enable_daily_limit": "Ativar limite diário de solicitações",
|
||||
"views.admin.UserSettings.enableEmailCheckRegex": "Ativar regex de verificação de e-mail",
|
||||
"views.admin.UserOauth2Settings.enableEmailFormat": "Ativar formato de e-mail",
|
||||
"views.admin.Telegram.enableGlobalMailPush": "Ativar envio global de e-mails (informar manualmente o Chat ID do Telegram)",
|
||||
"views.admin.IpBlacklistSettings.enable_ip_blacklist": "Ativar Lista negra de IP",
|
||||
"views.admin.IpBlacklistSettings.enable_ip_whitelist": "Ativar Lista branca de IP (estrito)",
|
||||
"views.admin.UserOauth2Settings.enableMailAllowList": "Ativar lista branca de endereços de e-mail (editável manualmente)",
|
||||
"views.admin.UserSettings.enableMailAllowList": "Ativar lista branca de endereços de e-mail (editável manualmente)",
|
||||
"views.admin.UserSettings.enableMailVerify": "Habilitar verificação de e-mail (o endereço remetente deve existir no sistema, ter saldo e poder enviar normalmente)",
|
||||
"views.admin.Telegram.enableTelegramAllowList": "Ativar lista branca do Telegram (informar manualmente o Chat ID)",
|
||||
"views.admin.IpBlacklistSettings.asn_blacklist_placeholder": "Informe a organização ASN (ex.: Google, Amazon)",
|
||||
"views.admin.IpBlacklistSettings.fingerprint_blacklist_placeholder": "Informe o ID da impressão (ex.: a1b2c3d4e5f6g7h8)",
|
||||
"views.admin.IpBlacklistSettings.daily_request_limit_placeholder": "Informe o limite (ex.: 1000)",
|
||||
"views.admin.IpBlacklistSettings.ip_blacklist_placeholder": "Informe o padrão (ex.: 192.168.1 ou ^10\\.0\\.0\\.5$)",
|
||||
"views.common.Login.bindUserAddressError": "Erro ao vincular o endereço ao usuário",
|
||||
"views.admin.IpBlacklistSettings.ip_whitelist_placeholder": "IP exato (ex.: 1.2.3.4) ou regex ancorada (ex.: ^192\\.168\\.1\\.\\d+$)",
|
||||
"views.index.SimpleIndex.exitSimpleIndex": "Sair do modo simples",
|
||||
"components.MailBox.keywordQueryTip": "Filtrar página atual",
|
||||
"views.admin.AccountSettings.create_address_subdomain_match_follow_env": "Seguir variável de ambiente",
|
||||
"views.admin.AccountSettings.create_address_subdomain_match_force_disable": "Forçar desativação",
|
||||
"views.admin.AccountSettings.create_address_subdomain_match_force_enable": "Forçar ativação",
|
||||
"views.user.UserLogin.forgotPassword": "Esqueci a senha",
|
||||
"components.MailBox.forwardMail": "Encaminhar",
|
||||
"components.MailContentRenderer.forward": "Encaminhar",
|
||||
"views.admin.AccountSettings.forward_address": "Endereço de encaminhamento",
|
||||
"views.admin.AccountSettings.forward_address_required": "O endereço de encaminhamento é obrigatório",
|
||||
"views.admin.AccountSettings.forward_placeholder": "forward@example.com",
|
||||
"components.MailContentRenderer.fullscreen": "Tela cheia",
|
||||
"views.common.Login.generateName": "Gerar nome aleatório",
|
||||
"views.admin.Telegram.globalMailPushList": "Lista global de chat ID para envio de e-mails",
|
||||
"views.common.Appearance.globalTabplacement": "Posição global das abas",
|
||||
"views.common.Login.help": "Ajuda",
|
||||
"views.Header.home": "Início",
|
||||
"views.index.SendMail.html": "HTML",
|
||||
"views.admin.SendMail.html": "HTML",
|
||||
"views.admin.UserOauth2Settings.icon": "Ícone (SVG, use apenas fonte confiável)",
|
||||
"views.admin.CreateAccount.enablePrefix": "Se ativar o prefixo",
|
||||
"views.common.AdminContact.adminContact": "Se precisar de ajuda, entre em contato com o administrador ({msg})",
|
||||
"views.admin.Telegram.init": "Inicializar",
|
||||
"views.admin.DatabaseManager.init": "Inicializar banco de dados",
|
||||
"views.admin.AccountSettings.regex_invalid": "Padrão regex inválido",
|
||||
"views.Admin.ipBlacklistSettings": "Lista negra de IP",
|
||||
"views.admin.IpBlacklistSettings.ip_blacklist": "Padrões da lista negra de IP",
|
||||
"views.admin.IpBlacklistSettings.title": "Configurações da lista negra de IP",
|
||||
"views.admin.IpBlacklistSettings.tip_ip": "Lista negra de IP: aceita correspondência por texto (ex.: \"192.168.1\") ou regex (ex.: \"^10\\.0\\.0\\.5$\").",
|
||||
"views.admin.IpBlacklistSettings.whitelist_empty_warning": "A lista branca de IP está ativada, mas está vazia. O servidor irá ignorá-la para evitar bloqueio de acesso. Adicione pelo menos uma entrada antes de ativar.",
|
||||
"views.admin.IpBlacklistSettings.ip_whitelist": "Lista branca de IP Padrões",
|
||||
"views.admin.IpBlacklistSettings.tip_whitelist": "Lista branca de IP: modo estrito. Entradas simples devem corresponder exatamente ao IP (sem substring). Use regex ancorada (^...$) para intervalos. IPs permitidos ignoram a verificação da lista negra.",
|
||||
"views.admin.SenderAccess.is_enabled": "Ativado",
|
||||
"views.admin.Account.itemCount": "itemCount",
|
||||
"views.admin.SenderAccess.itemCount": "itemCount",
|
||||
"views.admin.UserManagement.itemCount": "itemCount",
|
||||
"views.user.UserMailBox.addressQueryTip": "Deixe em branco para consultar todos os endereços",
|
||||
"views.admin.Account.addressQueryTip": "Deixe em branco para consultar todos os endereços",
|
||||
"views.admin.Mails.addressQueryTip": "Deixe em branco para consultar todos os endereços",
|
||||
"views.common.Appearance.left": "esquerda",
|
||||
"views.common.Login.getNewEmailTip2": "Deixar em branco gerará um endereço de e-mail aleatório.",
|
||||
"views.Header.light": "Claro",
|
||||
"views.admin.IpBlacklistSettings.enable_daily_limit_tip": "Limitar o número de requisições de API por IP por dia",
|
||||
"components.AddressSelect.localAddresses": "Endereços locais",
|
||||
"views.common.Login.bindUserInfo": "Usuário logado: entrar sem vincular e-mail ou criar um novo endereço vinculará ao usuário atual",
|
||||
"views.user.UserOauth2Callback.logging": "Entrando...",
|
||||
"views.user.UserLogin.login": "Entrar",
|
||||
"views.common.Login.login": "Entrar",
|
||||
"views.common.Login.loginAndBind": "Entrar e vincular",
|
||||
"views.Admin.loginMethod": "Método de login",
|
||||
"views.user.UserBar.fetchUserSettingsError": "A senha de login é inválida ou a conta não existe; também pode ser um problema de conexão. Tente novamente mais tarde.",
|
||||
"views.user.UserLogin.loginWith": "Entrar com {provider}",
|
||||
"views.user.UserLogin.loginWithPasskey": "Entrar com passkey",
|
||||
"views.user.UserSettings.logout": "Sair",
|
||||
"views.user.BindAddress.logout": "Sair",
|
||||
"views.Admin.logout": "Sair",
|
||||
"views.index.AccountSettings.logout": "Sair",
|
||||
"views.Admin.logoutSuccess": "Logout realizado com sucesso",
|
||||
"views.admin.UserOauth2Settings.mailAllowList": "Lista branca de endereços de e-mail",
|
||||
"views.admin.UserSettings.mailAllowList": "Lista branca de endereços de e-mail",
|
||||
"views.index.SimpleIndex.addressCredential": "Credencial do endereço de e-mail",
|
||||
"views.index.AddressBar.addressCredential": "Credencial do endereço de e-mail",
|
||||
"views.admin.Account.addressCredential": "Credencial do endereço de e-mail",
|
||||
"views.admin.CreateAccount.addressCredential": "Credencial do endereço de e-mail",
|
||||
"views.index.AddressBar.fetchAddressError": "A credencial do e-mail é inválida ou a conta não existe; também pode ser um problema de rede. Tente novamente mais tarde.",
|
||||
"views.Index.mailbox": "Caixa de entrada",
|
||||
"views.User.user_mail_box_tab": "Caixa de entrada",
|
||||
"views.user.AddressManagement.mail_count": "Quantidade de e-mails",
|
||||
"views.admin.Account.mail_count": "Quantidade de e-mails",
|
||||
"views.admin.Statistics.mailCount": "Quantidade de e-mails",
|
||||
"views.admin.UserAddressManagement.mail_count": "Quantidade de e-mails",
|
||||
"views.index.SimpleIndex.deleteSuccess": "E-mail excluído com sucesso",
|
||||
"views.user.UserLogin.cannotForgotPassword": "A verificação por e-mail ou o registro está desativado; não é possível redefinir a senha. Entre em contato com o administrador.",
|
||||
"views.Admin.mailWebhook": "Webhook de e-mail",
|
||||
"views.common.Appearance.mailboxSplitSize": "Largura da lista esquerda na visualização de duas colunas da caixa de correio",
|
||||
"views.common.Appearance.mailListView": "Visualização de lista da caixa de correio em largura total",
|
||||
"views.common.Appearance.mailListPreviewLineClamp": "Linhas da prévia do corpo",
|
||||
"views.common.Appearance.off": "Desativado",
|
||||
"views.index.SimpleIndex.refreshSuccess": "E-mails atualizados com sucesso",
|
||||
"views.Admin.unknow": "E-mails com destinatário desconhecido",
|
||||
"views.Admin.maintenance": "Manutenção",
|
||||
"views.index.AddressBar.addressManage": "Gerenciar",
|
||||
"views.admin.AccountSettings.source_match_mode": "Modo de correspondência",
|
||||
"views.admin.RoleAddressConfig.maxAddressCount": "Quantidade máxima de endereços (0 = ilimitado)",
|
||||
"views.admin.UserSettings.maxAddressCount": "Número máximo de endereços que podem ser vinculados (0 = ilimitado)",
|
||||
"views.Header.menu": "Menu",
|
||||
"views.admin.DatabaseManager.migration": "Migrar banco de dados",
|
||||
"views.admin.SenderAccess.modify": "Modificar",
|
||||
"views.admin.AccountSettings.send_mail_monthly_limit": "Limite mensal",
|
||||
"views.admin.AccountSettings.send_mail_monthly_limit_invalid": "O limite mensal deve ser um inteiro maior ou igual a -1",
|
||||
"components.MailBox.multiAction": "Ação múltipla",
|
||||
"components.SendBox.multiAction": "Ação múltipla",
|
||||
"views.admin.Account.multiClearInbox": "Limpar várias caixas de entrada",
|
||||
"views.admin.Account.multiClearSentItems": "Limpar vários itens enviados",
|
||||
"views.admin.Account.multiDelete": "Excluir em lote",
|
||||
"views.user.AddressManagement.name": "Nome",
|
||||
"views.index.AutoReply.name": "Nome",
|
||||
"views.admin.Account.name": "Nome",
|
||||
"views.admin.UserOauth2Settings.name": "Nome",
|
||||
"views.admin.UserAddressManagement.name": "Nome",
|
||||
"views.admin.Maintenance.sqlName": "Nome",
|
||||
"views.index.AccountSettings.newPassword": "Nova senha",
|
||||
"views.admin.Account.newPassword": "Nova senha",
|
||||
"components.MailBox.nextMail": "Próximo",
|
||||
"views.index.SimpleIndex.nextPage": "Próximo",
|
||||
"views.admin.AccountSettings.noLimitSendAddressList": "Lista de endereços sem limite de saldo",
|
||||
"views.index.SimpleIndex.noMails": "Nenhum e-mail encontrado",
|
||||
"views.admin.RoleAddressConfig.noRolesAvailable": "Nenhuma função disponível na configuração do sistema",
|
||||
"views.index.SendMail.requestAccessTip": "O acesso e o saldo de envio pertencem ao endereço atual, não à conta de usuário. Solicite acesso para este endereço ou fale com o administrador.",
|
||||
"components.SendBox.emptySent": "Nenhum e-mail enviado",
|
||||
"views.admin.RoleAddressConfig.notConfigured": "Não configurado (usar configurações globais)",
|
||||
"views.Admin.userOauth2Settings": "Configurações de OAuth2",
|
||||
"views.admin.UserOauth2Settings.oauth2Type": "Tipo de OAuth2",
|
||||
"views.Header.ok": "OK",
|
||||
"views.Admin.ok": "OK",
|
||||
"views.index.AddressBar.ok": "OK",
|
||||
"views.admin.SenderAccess.ok": "OK",
|
||||
"views.common.Login.ok": "OK",
|
||||
"views.admin.AccountSettings.create_address_subdomain_match_tip": "Afeta apenas a validação de domínio em /api/new_address e /admin/new_address. Exemplo: com isso ativado, foo.example.com pode corresponder a example.com.",
|
||||
"components.AiExtractInfo.open": "Abrir",
|
||||
"views.index.AddressBar.linkWithAddressCredential": "Abrir link de login automático por e-mail",
|
||||
"views.admin.CreateAccount.linkWithAddressCredential": "Abrir link de login automático por e-mail",
|
||||
"views.index.SendMail.options": "Opções",
|
||||
"views.admin.SendMail.options": "Opções",
|
||||
"components.AiExtractInfo.otherLink": "Outro link",
|
||||
"views.user.UserSettings.passkeyCreated": "Passkey criada com sucesso",
|
||||
"views.user.UserSettings.passkey_name": "Nome da passkey",
|
||||
"views.user.UserLogin.password": "Senha",
|
||||
"views.admin.UserManagement.password": "Senha",
|
||||
"views.common.Login.password": "Senha",
|
||||
"views.index.AccountSettings.passwordChanged": "Senha alterada com sucesso",
|
||||
"views.common.Login.passwordLogin": "Login com senha",
|
||||
"views.admin.Account.passwordResetSuccess": "Senha redefinida com sucesso",
|
||||
"views.index.AccountSettings.passwordMismatch": "As senhas não coincidem",
|
||||
"views.index.SendMail.successSend": "Verifique a caixa de saída. Se falhar, confira seu saldo ou tente novamente mais tarde.",
|
||||
"views.admin.SendMail.successSend": "Verifique sua caixa de saída. Se falhar, tente novamente mais tarde.",
|
||||
"views.user.UserLogin.pleaseCompleteTurnstile": "Conclua o turnstile",
|
||||
"views.index.SimpleIndex.addressCredentialTip": "Copie a credencial do endereço de e-mail; você poderá usá-la para fazer login.",
|
||||
"views.index.AddressBar.addressCredentialTip": "Copie a credencial do endereço de e-mail; você poderá usá-la para entrar na sua conta de e-mail.",
|
||||
"views.admin.Account.addressCredentialTip": "Copie a credencial do endereço de e-mail; você poderá usá-la para entrar na sua conta de e-mail.",
|
||||
"views.admin.CreateAccount.addressCredentialTip": "Copie a credencial do endereço de e-mail; você poderá usá-la para entrar na sua conta de e-mail.",
|
||||
"views.Admin.accessTip": "Digite a senha de administrador",
|
||||
"views.Header.accessTip": "Informe a senha de acesso correta",
|
||||
"views.admin.AccountSettings.address_block_list_placeholder": "Informe as palavras-chave que deseja bloquear",
|
||||
"views.user.UserSettings.renamePasskeyNamePlaceholder": "Informe o novo nome da passkey",
|
||||
"views.user.UserSettings.passkeyNamePlaceholder": "Informe o nome da passkey ou deixe em branco para gerar um aleatório",
|
||||
"views.admin.CreateAccount.fillInAllFields": "Preencha todos os campos",
|
||||
"views.admin.SendBox.queryTip": "Informe o endereço para consulta; deixe em branco para consultar todos",
|
||||
"views.user.UserLogin.pleaseInputCode": "Informe o código",
|
||||
"views.admin.UserManagement.pleaseInput": "Informe todas as informações",
|
||||
"views.user.UserLogin.pleaseInputEmail": "Informe o e-mail",
|
||||
"views.user.UserLogin.pleaseInput": "Informe e-mail e senha",
|
||||
"views.admin.Maintenance.tip": "Informe os dias",
|
||||
"views.common.Login.getNewEmailTip1": "Informe o e-mail que deseja usar. Permitido apenas: ",
|
||||
"views.common.Login.credentialInput": "Digite a credencial do endereço de e-mail",
|
||||
"views.admin.SenderAccess.modalTip": "Informe o saldo de envio",
|
||||
"views.user.UserLogin.pleaseLogin": "Faça login",
|
||||
"views.common.Login.pleaseGetNewEmail": "Faça login ou clique em \"Criar novo e-mail\" para obter um novo endereço.",
|
||||
"components.WebhookComponent.fillInDemoTip": "Altere a URL e as demais configurações para as suas",
|
||||
"components.SendBox.pleaseSelectMail": "Selecione um e-mail para visualizar.",
|
||||
"views.admin.Account.pleaseSelectAddress": "Selecione um endereço",
|
||||
"components.MailBox.pleaseSelectMail": "Selecione um e-mail",
|
||||
"views.admin.UserManagement.prefix": "Prefixo",
|
||||
"components.WebhookComponent.presets": "Modelos",
|
||||
"views.index.SendMail.preview": "Pré-visualização",
|
||||
"views.admin.SendMail.preview": "Pré-visualização",
|
||||
"views.admin.UserOauth2Settings.iconPreview": "Pré-visualização",
|
||||
"components.MailBox.prevMail": "Anterior",
|
||||
"views.index.SimpleIndex.prevPage": "Anterior",
|
||||
"components.MailBox.query": "Consultar",
|
||||
"views.user.UserMailBox.query": "Consultar",
|
||||
"views.Index.query": "Consultar",
|
||||
"views.admin.SendBox.query": "Consultar",
|
||||
"views.admin.Account.query": "Consultar",
|
||||
"views.admin.SenderAccess.query": "Consultar",
|
||||
"views.admin.UserManagement.query": "Consultar",
|
||||
"views.admin.Mails.query": "Consultar",
|
||||
"views.Admin.qucickSetup": "Configuração rápida",
|
||||
"views.index.SendMail.toMailEmpty": "O endereço do destinatário está vazio",
|
||||
"views.admin.SendMail.toMailEmpty": "O endereço do destinatário está vazio",
|
||||
"views.index.SendMail.toName": "Nome e endereço do destinatário; deixe o nome em branco para usar o e-mail",
|
||||
"views.admin.SendMail.toName": "Nome e endereço do destinatário; deixe o nome em branco para usar o e-mail",
|
||||
"components.MailBox.refresh": "Atualizar",
|
||||
"components.Turnstile.refresh": "Atualizar",
|
||||
"components.SendBox.refresh": "Atualizar",
|
||||
"views.index.SimpleIndex.refreshMails": "Atualizar",
|
||||
"components.MailBox.refreshAfter": "Atualizar em {msg} segundos",
|
||||
"views.index.SimpleIndex.refreshAfter": "Atualizar em {msg} segundos",
|
||||
"views.admin.AccountSettings.regex_too_long": "O padrão regex é muito longo (máx. 200 caracteres)",
|
||||
"views.user.UserLogin.register": "Registrar",
|
||||
"views.user.UserSettings.renamePasskey": "Renomear passkey",
|
||||
"views.admin.UserOauth2Settings.userEmailReplace": "Modelo de substituição",
|
||||
"components.MailBox.reply": "Responder",
|
||||
"components.MailContentRenderer.reply": "Responder",
|
||||
"views.index.SendMail.requestAccess": "Solicitar acesso para este endereço",
|
||||
"views.user.UserLogin.resetPassword": "Redefinir Senha",
|
||||
"views.admin.Account.resetPassword": "Redefinir Senha",
|
||||
"views.admin.UserManagement.resetPassword": "Redefinir Senha",
|
||||
"views.index.SendMail.rich text": "Texto rico",
|
||||
"views.admin.SendMail.rich text": "Texto rico",
|
||||
"views.common.Appearance.right": "direita",
|
||||
"views.admin.UserManagement.role": "Função",
|
||||
"views.admin.RoleAddressConfig.role": "Função",
|
||||
"views.Admin.roleAddressConfig": "Configuração de endereço por função",
|
||||
"views.admin.AccountSettings.rule_index": "Regra",
|
||||
"views.Index.s3Attachment": "Anexo S3",
|
||||
"components.WebhookComponent.save": "Salvar",
|
||||
"views.index.AutoReply.save": "Salvar",
|
||||
"views.admin.AiExtractSettings.save": "Salvar",
|
||||
"views.admin.UserOauth2Settings.save": "Salvar",
|
||||
"views.admin.IpBlacklistSettings.save": "Salvar",
|
||||
"views.admin.Telegram.save": "Salvar",
|
||||
"views.admin.UserSettings.save": "Salvar",
|
||||
"views.admin.RoleAddressConfig.save": "Salvar",
|
||||
"views.admin.AccountSettings.save": "Salvar",
|
||||
"views.admin.Maintenance.save": "Salvar",
|
||||
"views.admin.Webhook.save": "Salvar",
|
||||
"views.admin.Maintenance.saveSuccess": "Salvo com sucesso",
|
||||
"views.admin.UserOauth2Settings.successTip": "Salvo com sucesso",
|
||||
"views.admin.IpBlacklistSettings.successTip": "Salvo com sucesso",
|
||||
"views.admin.UserSettings.successTip": "Salvo com sucesso",
|
||||
"views.admin.AccountSettings.successTip": "Salvo com sucesso",
|
||||
"components.MailBox.saveToS3": "Salvar no S3",
|
||||
"components.MailContentRenderer.saveToS3": "Salvar no S3",
|
||||
"views.Index.saveToS3Success": "salvo no S3 com sucesso",
|
||||
"components.MailBox.selectAll": "Selecionar toda esta página",
|
||||
"components.SendBox.selectAll": "Selecionar toda esta página",
|
||||
"views.admin.Account.selectAll": "Selecionar toda esta página",
|
||||
"views.admin.AccountSettings.select_domain": "Selecionar domínio",
|
||||
"views.admin.Account.selectedItems": "Selecionado",
|
||||
"views.index.SendMail.send": "Enviar",
|
||||
"views.admin.SendMail.send": "Enviar",
|
||||
"views.Index.sendbox": "Caixa de saída",
|
||||
"views.Admin.sendBox": "Caixa de saída",
|
||||
"views.user.AddressManagement.send_count": "Quantidade enviada",
|
||||
"views.admin.Account.send_count": "Quantidade enviada",
|
||||
"views.admin.UserAddressManagement.send_count": "Quantidade enviada",
|
||||
"views.Index.sendmail": "Enviar e-mail",
|
||||
"views.Admin.sendMail": "Enviar e-mail",
|
||||
"views.index.SendMail.send_balance": "Saldo restante de envio",
|
||||
"views.admin.Statistics.sendMailCount": "Quantidade de e-mails enviados",
|
||||
"views.admin.AccountSettings.send_mail_limit": "Limite de envio",
|
||||
"views.user.UserLogin.sendVerificationCode": "Enviar código de verificação",
|
||||
"views.Admin.senderAccess": "Controle de acesso de envio",
|
||||
"views.admin.SendMail.fromMailEmpty": "O endereço do remetente está vazio",
|
||||
"views.index.AutoReply.sourcePrefix": "Filtro de remetente",
|
||||
"components.AiExtractInfo.serviceLink": "Link do serviço",
|
||||
"views.index.AutoReply.settings": "Configurações",
|
||||
"views.index.AccountSettings.showAddressCredential": "Mostrar credencial do endereço",
|
||||
"components.MailBox.attachments": "Mostrar anexos",
|
||||
"components.MailBox.showHtmlMail": "Mostrar e-mail HTML",
|
||||
"components.MailContentRenderer.showHtmlMail": "Mostrar e-mail HTML",
|
||||
"views.admin.Account.showCredential": "Mostrar credencial do endereço de e-mail",
|
||||
"views.user.UserSettings.showPasskeyList": "Mostrar lista de passkeys",
|
||||
"components.MailBox.showTextMail": "Mostrar e-mail em texto",
|
||||
"components.MailContentRenderer.showTextMail": "Mostrar e-mail em texto",
|
||||
"views.Index.enterSimpleMode": "Modo simples",
|
||||
"components.MailContentRenderer.size": "Tamanho",
|
||||
"views.admin.Account.source_meta": "Origem",
|
||||
"views.admin.AccountSettings.source_patterns": "Regex do endereço de origem (opcional)",
|
||||
"views.admin.Maintenance.sqlStatement": "Comando SQL (somente DELETE)",
|
||||
"views.user.UserOauth2Callback.stateNotMatch": "estado não corresponde",
|
||||
"views.Admin.statistics": "Estatísticas",
|
||||
"views.Header.status": "Status",
|
||||
"views.admin.IpBlacklistSettings.enable_whitelist_tip": "Modo estrito: apenas IPs que corresponderem à lista branca poderão acessar APIs com limite. Todos os outros IPs serão negados.",
|
||||
"views.index.AutoReply.subject": "Assunto",
|
||||
"views.index.SendMail.subject": "Assunto",
|
||||
"views.admin.SendMail.subject": "Assunto",
|
||||
"views.index.SendMail.subjectEmpty": "O assunto está vazio",
|
||||
"views.admin.SendMail.subjectEmpty": "O assunto está vazio",
|
||||
"components.AiExtractInfo.subscriptionLink": "Link de assinatura",
|
||||
"views.user.AddressManagement.success": "sucesso",
|
||||
"views.index.TelegramAddress.success": "sucesso",
|
||||
"views.index.LocalAddress.success": "sucesso",
|
||||
"views.admin.UserAddressManagement.success": "sucesso",
|
||||
"components.WebhookComponent.successTip": "Sucesso",
|
||||
"components.MailBox.success": "Sucesso",
|
||||
"components.SendBox.success": "Sucesso",
|
||||
"views.index.AccountSettings.success": "Sucesso",
|
||||
"views.index.AutoReply.success": "Sucesso",
|
||||
"views.admin.AiExtractSettings.successTip": "Sucesso",
|
||||
"views.admin.Account.success": "Sucesso",
|
||||
"views.admin.SenderAccess.success": "Sucesso",
|
||||
"views.admin.UserManagement.success": "Sucesso",
|
||||
"views.admin.Telegram.successTip": "Sucesso",
|
||||
"views.admin.RoleAddressConfig.successTip": "Sucesso",
|
||||
"views.admin.Webhook.successTip": "Sucesso",
|
||||
"views.admin.CreateAccount.successTip": "Criado com sucesso",
|
||||
"views.admin.Telegram.globalMailPushListTip": "Suporta chat_id de conversa privada/grupo/canal. Envie uma mensagem ao bot e acesse este link para ver o chat_id: https://api.telegram.org/bot<Substitua pelo seu BOT TOKEN>/getUpdates",
|
||||
"views.user.AddressManagement.targetUserEmail": "E-mail do usuário de destino",
|
||||
"views.admin.Telegram.telegramAllowList": "Lista branca do Telegram (informar manualmente o Chat ID)",
|
||||
"views.Admin.telegram": "Bot do Telegram",
|
||||
"views.admin.Telegram.miniAppUrl": "URL do Mini App do Telegram",
|
||||
"components.WebhookComponent.test": "Teste",
|
||||
"views.index.SendMail.text": "Texto",
|
||||
"views.admin.SendMail.text": "Texto",
|
||||
"views.user.UserSettings.passordTip": "O servidor recebe apenas o hash da senha, nunca a senha em texto puro, portanto não pode vê-la nem recuperá-la. Se o administrador ativar a verificação por e-mail, você poderá redefini-la no modo anônimo.",
|
||||
"views.index.LocalAddress.tip": "Esses endereços ficam armazenados no navegador e podem ser perdidos se você limpar o cache.",
|
||||
"views.admin.UserOauth2Settings.tip": "O login de terceiros usará automaticamente o e-mail do usuário para registrar uma conta (o mesmo e-mail será considerado a mesma conta). Você também pode definir a senha via “esqueci minha senha”.",
|
||||
"views.admin.AccountSettings.send_mail_limit_tip": "Aplica-se a todos os canais de envio. Use -1 para ilimitado e 0 para bloquear o envio.",
|
||||
"views.admin.AccountSettings.create_address_subdomain_match_note": "Isso é diferente de RANDOM_SUBDOMAIN_DOMAINS: esta opção permite informar subdomínios personalizados; o subdomínio aleatório apenas gera um durante a criação.",
|
||||
"views.index.SendMail.tooLarge": "Arquivo muito grande; envie um arquivo menor que 1 MB.",
|
||||
"views.admin.SendMail.tooLarge": "Arquivo muito grande; envie um arquivo menor que 1 MB.",
|
||||
"views.common.Appearance.top": "topo",
|
||||
"views.user.AddressManagement.transferAddress": "Transferir endereço",
|
||||
"views.user.AddressManagement.transferAddressTip": "Transferir este endereço para outro usuário irá removê-lo da sua conta e movê-lo para a conta dele. Tem certeza de que deseja transferi-lo?",
|
||||
"views.common.Appearance.useSideMargin": "Ativar margens laterais à esquerda e à direita da página",
|
||||
"views.admin.AiExtractSettings.manualInputPrompt": "Digite e pressione Enter para adicionar",
|
||||
"views.admin.UserOauth2Settings.manualInputPrompt": "Digite e pressione Enter para adicionar",
|
||||
"views.admin.Telegram.manualInputPrompt": "Digite e pressione Enter para adicionar",
|
||||
"views.admin.UserSettings.manualInputPrompt": "Digite e pressione Enter para adicionar",
|
||||
"views.admin.AccountSettings.manualInputPrompt": "Digite e pressione Enter para adicionar",
|
||||
"views.admin.Webhook.manualInputPrompt": "Digite e pressione Enter para adicionar",
|
||||
"views.admin.IpBlacklistSettings.manualInputPrompt": "Digite o padrão e pressione Enter para adicionar",
|
||||
"views.user.AddressManagement.unbindAddress": "Desvincular endereço",
|
||||
"views.index.TelegramAddress.unbindMailAddress": "Desvincular endereço de e-mail",
|
||||
"views.index.LocalAddress.unbindMailAddress": "Desvincular credencial do endereço de e-mail",
|
||||
"components.MailBox.unselectAll": "Desmarcar tudo",
|
||||
"components.SendBox.unselectAll": "Desmarcar tudo",
|
||||
"views.admin.Account.unselectAll": "Desmarcar tudo",
|
||||
"views.admin.Account.updated_at": "Atualizado em",
|
||||
"views.user.UserSettings.updated_at": "Atualizado em",
|
||||
"components.WebhookComponent.urlMissing": "URL é obrigatória",
|
||||
"views.common.Appearance.useIframeShowMail": "Usar iframe para mostrar e-mail HTML",
|
||||
"views.admin.CreateAccount.enableRandomSubdomain": "Usar subdomínio aleatório",
|
||||
"views.common.Login.enableRandomSubdomain": "Usar subdomínio aleatório",
|
||||
"views.admin.UserOauth2Settings.userEmailFormatTip": "Use regex para transformar o e-mail. Exemplo: ^(.+)@old\\.com$ com $1@new.com",
|
||||
"views.common.Appearance.useSimpleIndex": "Usar índice simples",
|
||||
"views.common.Appearance.useUTCDate": "Usar data UTC",
|
||||
"views.Header.user": "Usuário",
|
||||
"views.Admin.user": "Contas de usuário",
|
||||
"components.AddressSelect.userAddresses": "Endereços vinculados à conta",
|
||||
"views.Admin.loginViaUserAdmin": "Permissão de administrador do usuário",
|
||||
"views.admin.Statistics.userCount": "Quantidade de usuários",
|
||||
"views.admin.UserManagement.user_email": "E-mail da conta de usuário",
|
||||
"views.index.AddressBar.userLogin": "Login da conta de usuário",
|
||||
"views.Admin.user_management": "Gerenciamento de contas de usuário",
|
||||
"views.Admin.user_settings": "Configurações de contas de usuário",
|
||||
"views.User.user_settings": "Configurações da conta de usuário",
|
||||
"components.AiExtractInfo.authCode": "Código de verificação",
|
||||
"views.user.UserLogin.verifyCode": "Código de verificação",
|
||||
"views.user.UserLogin.verifyCodeSent": "Código de verificação enviado, expira em {timeout} segundos",
|
||||
"views.admin.AccountSettings.verified_address_list": "Lista de endereços verificados (pode enviar e-mail pela API interna do cf)",
|
||||
"views.admin.UserSettings.verifyMailSender": "Verificar remetente do e-mail",
|
||||
"components.MailContentRenderer.attachments": "Ver anexos",
|
||||
"views.admin.Account.viewMails": "Ver e-mails",
|
||||
"views.admin.Account.viewSendBox": "Ver caixa de saída",
|
||||
"views.user.UserLogin.waitforVerifyCode": "Aguarde {timeout} segundos",
|
||||
"views.admin.Webhook.webhookAllowList": "Lista de permissão do webhook (digite o e-mail autorizado e pressione Enter)",
|
||||
"views.admin.Webhook.notEnabled": "O webhook não está habilitado",
|
||||
"components.WebhookComponent.notEnabled": "O webhook não está habilitado para você",
|
||||
"views.Index.webhookSettings": "Configurações de webhook",
|
||||
"views.Admin.webhookSettings": "Configurações de webhook",
|
||||
"views.admin.AiExtractSettings.disabledTip": "Quando desativado, a extração por IA processará todos os endereços",
|
||||
"views.admin.AiExtractSettings.enableAllowListTip": "Quando ativado, a extração por IA só processará e-mails enviados aos endereços permitidos",
|
||||
"views.admin.CreateAccount.randomSubdomainTip": "Quando ativado, o endereço criado usará um subdomínio aleatório. Recomendado apenas para recebimento. Requer um registro MX curinga no DNS do domínio base — consulte a documentação de subdomínios aleatórios.",
|
||||
"views.common.Login.randomSubdomainTip": "Quando ativado, o endereço criado usará um subdomínio aleatório. Recomendado apenas para recebimento. Requer um registro MX curinga no DNS do domínio base — consulte a documentação de subdomínios aleatórios.",
|
||||
"views.admin.AiExtractSettings.allowListTip": "O curinga * corresponde a quaisquer caracteres; ex.: *{'@'}example.com corresponde a todos os endereços do domínio example.com",
|
||||
"views.Admin.workerconfig": "Configuração do Worker",
|
||||
"views.admin.AccountSettings.create_address_subdomain_match_env_locked": "A variável ENABLE_CREATE_ADDRESS_SUBDOMAIN_MATCH está em false. O botão salvo pode ser alterado, mas só terá efeito quando a variável for ativada ou removida.",
|
||||
"views.common.Login.getNewEmailTip3": "Você pode escolher um domínio na lista suspensa.",
|
||||
"views.admin.AccountSettings.tip": "Você pode inserir manualmente os seguintes valores e pressionar Enter para adicioná-los",
|
||||
"components.MailBox.emptyInbox": "Sua caixa de entrada está vazia",
|
||||
"views.index.SendMail.fromName": "Seu nome e endereço; deixe o nome em branco para usar o e-mail",
|
||||
"views.admin.SendMail.fromName": "Seu nome e endereço; deixe o nome em branco para usar o e-mail",
|
||||
"components.AddressCredentialModal.addressCredential": "Credencial do endereço",
|
||||
"components.AddressCredentialModal.addressCredentialLabel": "Address JWT",
|
||||
"components.AddressCredentialModal.addressPassword": "Senha do endereço",
|
||||
"components.AddressCredentialModal.agentAccess": "AI Agent",
|
||||
"components.AddressCredentialModal.agentAccessTip": "Use esta caixa de entrada em um AI Agent com o Address JWT e as APIs parsed-mail.",
|
||||
"components.AddressCredentialModal.agentConfig": "Configuração do Agent",
|
||||
"components.AddressCredentialModal.agentSkill": "Agent skill",
|
||||
"components.AddressCredentialModal.apiBase": "Base da API",
|
||||
"components.AddressCredentialModal.autoLoginLink": "Link de login automático",
|
||||
"components.AddressCredentialModal.copyFailed": "Falha ao copiar",
|
||||
"components.AddressCredentialModal.copySection": "Copiar",
|
||||
"components.AddressCredentialModal.copySuccess": "Copiado",
|
||||
"components.AddressCredentialModal.currentAddress": "Endereço atual",
|
||||
"components.AddressCredentialModal.docs": "Documentação",
|
||||
"components.AddressCredentialModal.imapHost": "Host IMAP",
|
||||
"components.AddressCredentialModal.imapPort": "Porta IMAP",
|
||||
"components.AddressCredentialModal.password": "Senha",
|
||||
"components.AddressCredentialModal.plainOrProxyTls": "Texto puro ou TLS do proxy",
|
||||
"components.AddressCredentialModal.security": "Segurança",
|
||||
"components.AddressCredentialModal.smtpHost": "Host SMTP",
|
||||
"components.AddressCredentialModal.smtpImapAccess": "SMTP / IMAP",
|
||||
"components.AddressCredentialModal.smtpImapTip": "Use estes valores em clientes de e-mail depois que o administrador configurar o proxy SMTP/IMAP. Como senha, use o Address JWT mostrado aqui ou a senha do endereço quando você a tiver.",
|
||||
"components.AddressCredentialModal.smtpPort": "Porta SMTP",
|
||||
"components.AddressCredentialModal.starttls": "STARTTLS",
|
||||
"components.AddressCredentialModal.tip": "Use estas credenciais somente com clientes e agents confiáveis.",
|
||||
"components.AddressCredentialModal.title": "Credenciais do endereço e métodos de conexão",
|
||||
"components.AddressCredentialModal.username": "Nome de usuário"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,81 @@
|
||||
import { MESSAGE_REGISTRY, getMessageSource } from './message-registry'
|
||||
|
||||
import type { MessageKey, MessageNamespace } from './message-registry'
|
||||
|
||||
import { deMessages } from './locales/source/de'
|
||||
import { esMessages } from './locales/source/es'
|
||||
import { jaMessages } from './locales/source/ja'
|
||||
import { ptBRMessages } from './locales/source/ptBR'
|
||||
|
||||
import type { SupportedLocale } from './locale-registry'
|
||||
|
||||
type LocaleTree = Record<string, unknown>
|
||||
type SourceLocale = Extract<SupportedLocale, 'en' | 'zh'>
|
||||
type AdditionalLocale = Exclude<SupportedLocale, SourceLocale>
|
||||
|
||||
const additionalLocaleSources: Record<AdditionalLocale, Record<string, string>> = {
|
||||
es: esMessages,
|
||||
'pt-BR': ptBRMessages,
|
||||
ja: jaMessages,
|
||||
de: deMessages,
|
||||
}
|
||||
|
||||
const setNestedValue = (target: LocaleTree, path: string, value: unknown) => {
|
||||
const segments = path.split('.')
|
||||
let current: LocaleTree = target
|
||||
|
||||
for (const segment of segments.slice(0, -1)) {
|
||||
const existing = current[segment]
|
||||
if (typeof existing === 'object' && existing !== null && !Array.isArray(existing)) {
|
||||
current = existing as LocaleTree
|
||||
continue
|
||||
}
|
||||
|
||||
current[segment] = {}
|
||||
current = current[segment] as LocaleTree
|
||||
}
|
||||
|
||||
current[segments.at(-1) as string] = value
|
||||
}
|
||||
|
||||
const buildSourceLocaleMessages = (locale: SourceLocale) => {
|
||||
const messages: LocaleTree = {}
|
||||
|
||||
for (const namespace of Object.keys(MESSAGE_REGISTRY) as MessageNamespace[]) {
|
||||
const keys = Object.keys(MESSAGE_REGISTRY[namespace]) as MessageKey<typeof namespace>[]
|
||||
for (const key of keys) {
|
||||
const message = getMessageSource(namespace, key, locale)
|
||||
if (message === undefined) continue
|
||||
setNestedValue(messages, `${namespace}.${key}`, message)
|
||||
}
|
||||
}
|
||||
|
||||
return messages
|
||||
}
|
||||
|
||||
const buildAdditionalLocaleMessages = (locale: AdditionalLocale) => {
|
||||
const messages: LocaleTree = {}
|
||||
|
||||
for (const [key, value] of Object.entries(additionalLocaleSources[locale])) {
|
||||
setNestedValue(messages, key, value)
|
||||
}
|
||||
|
||||
return messages
|
||||
}
|
||||
|
||||
export const I18N_MESSAGES: Record<SupportedLocale, LocaleTree> = {
|
||||
zh: buildSourceLocaleMessages('zh'),
|
||||
en: buildSourceLocaleMessages('en'),
|
||||
es: buildAdditionalLocaleMessages('es'),
|
||||
'pt-BR': buildAdditionalLocaleMessages('pt-BR'),
|
||||
ja: buildAdditionalLocaleMessages('ja'),
|
||||
de: buildAdditionalLocaleMessages('de'),
|
||||
}
|
||||
|
||||
export const getLocalizedMessage = (
|
||||
locale: AdditionalLocale,
|
||||
namespace: MessageNamespace,
|
||||
key: string,
|
||||
) => {
|
||||
return additionalLocaleSources[locale][`${namespace}.${key}`]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { NDateLocale, NLocale } from 'naive-ui'
|
||||
import { getNaiveLocaleConfig as getRegistryNaiveLocaleConfig } from './locale-registry'
|
||||
|
||||
import type { SupportedLocale } from './locale-registry'
|
||||
|
||||
export type NaiveLocaleConfig = {
|
||||
locale: NLocale
|
||||
dateLocale: NDateLocale
|
||||
}
|
||||
|
||||
export const getNaiveLocaleConfig = (locale: SupportedLocale): NaiveLocaleConfig => {
|
||||
return getRegistryNaiveLocaleConfig(locale)
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
import { LOCALE_REGISTRY, SUPPORTED_LOCALES } from './locale-registry'
|
||||
|
||||
export { SUPPORTED_LOCALES } from './locale-registry'
|
||||
export type { SupportedLocale } from './locale-registry'
|
||||
|
||||
import type { SupportedLocale } from './locale-registry'
|
||||
|
||||
export const DEFAULT_LOCALE: SupportedLocale = 'zh'
|
||||
export const FALLBACK_LOCALE: SupportedLocale = 'zh'
|
||||
export const PREFERRED_LOCALE_STORAGE_KEY = 'preferredLocale'
|
||||
export const EMPTY_LOCALE_MESSAGES = Object.fromEntries(
|
||||
SUPPORTED_LOCALES.map((supportedLocale) => [supportedLocale, {}]),
|
||||
) as Record<SupportedLocale, Record<string, never>>
|
||||
|
||||
export const isSupportedLocale = (locale: unknown): locale is SupportedLocale => {
|
||||
return typeof locale === 'string' && SUPPORTED_LOCALES.includes(locale as SupportedLocale)
|
||||
}
|
||||
|
||||
export const resolveSupportedLocale = (locale: string | null | undefined): SupportedLocale | null => {
|
||||
if (!locale) return null
|
||||
const normalizedLocale = locale.trim().toLowerCase()
|
||||
|
||||
for (const supportedLocale of SUPPORTED_LOCALES) {
|
||||
if (supportedLocale.toLowerCase() === normalizedLocale) {
|
||||
return supportedLocale
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export const matchSupportedLocale = (locale: string | null | undefined): SupportedLocale | null => {
|
||||
if (!locale) return null
|
||||
const normalizedLocale = locale.trim().toLowerCase()
|
||||
|
||||
for (const entry of LOCALE_REGISTRY) {
|
||||
if (entry.browserMatches.some((prefix) => normalizedLocale === prefix || normalizedLocale.startsWith(`${prefix}-`))) {
|
||||
return entry.locale
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export const getBrowserLocales = (): string[] => {
|
||||
if (typeof navigator === 'undefined') return []
|
||||
|
||||
const locales = Array.isArray(navigator.languages) && navigator.languages.length > 0
|
||||
? navigator.languages
|
||||
: [navigator.language]
|
||||
|
||||
return locales.filter(Boolean)
|
||||
}
|
||||
|
||||
export const getStoredLocale = (): SupportedLocale | '' => {
|
||||
if (typeof window === 'undefined') return ''
|
||||
|
||||
const locale = window.localStorage.getItem(PREFERRED_LOCALE_STORAGE_KEY)
|
||||
return isSupportedLocale(locale) ? locale : ''
|
||||
}
|
||||
|
||||
export const getPreferredLocale = (
|
||||
storedLocale: string | null | undefined,
|
||||
browserLocales: string[] = [],
|
||||
): SupportedLocale => {
|
||||
if (isSupportedLocale(storedLocale)) return storedLocale
|
||||
|
||||
for (const browserLocale of browserLocales) {
|
||||
const matchedLocale = matchSupportedLocale(browserLocale)
|
||||
if (matchedLocale) return matchedLocale
|
||||
}
|
||||
|
||||
return FALLBACK_LOCALE
|
||||
}
|
||||
|
||||
export const getInitialLocale = () => DEFAULT_LOCALE
|
||||
|
||||
const splitPathSuffix = (fullPath: string) => {
|
||||
const match = fullPath.match(/^([^?#]*)(.*)$/)
|
||||
return {
|
||||
path: match?.[1] || '/',
|
||||
suffix: match?.[2] || '',
|
||||
}
|
||||
}
|
||||
|
||||
export const stripLocaleFromPath = (path: string): string => {
|
||||
if (!path || path === '/') return '/'
|
||||
|
||||
const pathLocale = resolveSupportedLocale(path.split('/')[1])
|
||||
if (!pathLocale) {
|
||||
return path
|
||||
}
|
||||
|
||||
const localePrefix = `/${path.split('/')[1]}`
|
||||
if (path === localePrefix || path === `${localePrefix}/`) {
|
||||
return '/'
|
||||
}
|
||||
if (path.startsWith(`${localePrefix}/`)) {
|
||||
return path.slice(localePrefix.length) || '/'
|
||||
}
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
export const getPathWithLocale = (path: string, locale: SupportedLocale): string => {
|
||||
const normalizedPath = path.startsWith('/') ? path : `/${path}`
|
||||
const basePath = stripLocaleFromPath(normalizedPath)
|
||||
|
||||
if (locale === DEFAULT_LOCALE) {
|
||||
return basePath
|
||||
}
|
||||
|
||||
if (basePath === '/') {
|
||||
return `/${locale}/`
|
||||
}
|
||||
|
||||
return `/${locale}${basePath}`
|
||||
}
|
||||
|
||||
export const replaceLocaleInFullPath = (fullPath: string, locale: SupportedLocale): string => {
|
||||
const { path, suffix } = splitPathSuffix(fullPath)
|
||||
return `${getPathWithLocale(path, locale)}${suffix}`
|
||||
}
|
||||
|
||||
const getLocaleAliasPath = (path: string, locale: SupportedLocale): string => {
|
||||
const normalizedPath = path.startsWith('/') ? path : `/${path}`
|
||||
const basePath = stripLocaleFromPath(normalizedPath)
|
||||
|
||||
if (locale === DEFAULT_LOCALE) {
|
||||
if (basePath === '/') {
|
||||
return `/${locale}/`
|
||||
}
|
||||
|
||||
return `/${locale}${basePath}`
|
||||
}
|
||||
|
||||
return getPathWithLocale(basePath, locale)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user