feat(web): 多语言界面(中/英/日)与用户语言偏好字段

- 新增 internal/i18n 包:以原始中文字符串为 key 的翻译目录(zh 恒等,
  en 兜底,ja 缺译回退 en),t/tf 模板函数与 Accept-Language 解析
  (按 q 值排序,兜底英语)
- User 模型新增 language 字段(auto|en|zh|ja,默认 auto),
  AutoMigrate 自动加列;UserStore 新增 UpdateLanguage
- 语言解析:登录用户按偏好(auto 时取浏览器 Accept-Language),
  登录/封禁页按浏览器语言;所有 c.HTML 渲染注入 Lang 模板数据
- 全部页面模板(用户页 + 管理后台 12 页)接入翻译:导航、文件夹、
  邮件列表/阅读/写信、设置、登录、封禁页、表单、表格、JS 确认框等;
  html lang 属性随语言切换;日期函数(shortDate/time12/time12m)
  与 folderLabel 改为语言感知(上午/下午 vs AM/PM vs 午前/午後)
- 设置页新增「界面语言」选择(自动/English/中文/日本語),
  /settings 表单支持单独提交语言偏好;管理后台用户表单可预设语言
- 封禁原因等入库中文文案在展示时经 t 翻译(未知 key 原样回退)
- 测试:i18n 单测(解析/回退/译文)、render 测试补 Lang 数据、
  时间格式化测试适配新签名;全量 go test 通过
This commit is contained in:
dsh
2026-08-20 00:21:10 -04:00
parent d1ffd10408
commit c881146a40
38 changed files with 1909 additions and 768 deletions
+31 -31
View File
@@ -1,10 +1,10 @@
{{define "admin_bans"}}
<!DOCTYPE html>
<html lang="zh-CN">
<html lang="{{htmlLang .Lang}}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>IP黑名单 - MailGo</title>
<title>{{t .Lang "IP黑名单"}} - MailGo</title>
{{template "styles" .}}
</head>
<body>
@@ -12,32 +12,32 @@
<div class="container">
<div class="clearfix">
<div class="sidebar">
<a href="/inbox">返回邮箱</a>
<a href="/admin" {{if eq .activeFolder "admin"}}class="active"{{end}}>控制面板</a>
<a href="/admin/domains" {{if eq .activeFolder "domains"}}class="active"{{end}}>域名管理</a>
<a href="/admin/users" {{if eq .activeFolder "users"}}class="active"{{end}}>用户管理</a>
<a href="/admin/mails" {{if eq .activeFolder "mails"}}class="active"{{end}}>所有邮件</a>
<a href="/admin/outbound" {{if eq .activeFolder "outbound"}}class="active"{{end}}>外发队列</a>
<a href="/admin/protocol-logs" {{if eq .activeFolder "protocol-logs"}}class="active"{{end}}>协议日志</a>
<a href="/admin/connections" {{if eq .activeFolder "connections"}}class="active"{{end}}>当前连接</a>
<a href="/admin/bans" {{if eq .activeFolder "bans"}}class="active"{{end}}>IP黑名单</a>
<a href="/inbox">{{t .Lang "返回邮箱"}}</a>
<a href="/admin" {{if eq .activeFolder "admin"}}class="active"{{end}}>{{t .Lang "控制面板"}}</a>
<a href="/admin/domains" {{if eq .activeFolder "domains"}}class="active"{{end}}>{{t .Lang "域名管理"}}</a>
<a href="/admin/users" {{if eq .activeFolder "users"}}class="active"{{end}}>{{t .Lang "用户管理"}}</a>
<a href="/admin/mails" {{if eq .activeFolder "mails"}}class="active"{{end}}>{{t .Lang "所有邮件"}}</a>
<a href="/admin/outbound" {{if eq .activeFolder "outbound"}}class="active"{{end}}>{{t .Lang "外发队列"}}</a>
<a href="/admin/protocol-logs" {{if eq .activeFolder "protocol-logs"}}class="active"{{end}}>{{t .Lang "协议日志"}}</a>
<a href="/admin/connections" {{if eq .activeFolder "connections"}}class="active"{{end}}>{{t .Lang "当前连接"}}</a>
<a href="/admin/bans" {{if eq .activeFolder "bans"}}class="active"{{end}}>{{t .Lang "IP黑名单"}}</a>
</div>
<div class="content">
<div class="card">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;">
<h2>IP 黑名单</h2>
<span style="color:#7f8c8d;font-size:13px;">阶段性封禁:第 4 次触发起封禁,30分钟 → 3小时 → 3个月 → 半年(上限)</span>
<h2>{{t .Lang "IP 黑名单"}}</h2>
<span style="color:#7f8c8d;font-size:13px;">{{t .Lang "阶段性封禁:第 4 次触发起封禁,30分钟 → 3小时 → 3个月 → 半年(上限)"}}</span>
</div>
<table>
<thead>
<tr>
<th>IP 地址</th>
<th>状态</th>
<th>封禁次数</th>
<th>失败次数</th>
<th>原因</th>
<th>到期时间</th>
<th>操作</th>
<th>{{t .Lang "IP 地址"}}</th>
<th>{{t .Lang "状态"}}</th>
<th>{{t .Lang "封禁次数"}}</th>
<th>{{t .Lang "失败次数"}}</th>
<th>{{t .Lang "原因"}}</th>
<th>{{t .Lang "到期时间"}}</th>
<th>{{t .Lang "操作"}}</th>
</tr>
</thead>
<tbody>
@@ -45,21 +45,21 @@
<tr>
<td>{{.IPAddress}}</td>
<td>
{{if .Active}}<span class="badge" style="background:#e74c3c;color:#fff;">封禁中</span>
{{else}}<span class="badge" style="background:#95a5a6;color:#fff;">已过期</span>{{end}}
{{if .Active}}<span class="badge" style="background:#e74c3c;color:#fff;">{{t $.Lang "封禁中"}}</span>
{{else}}<span class="badge" style="background:#95a5a6;color:#fff;">{{t $.Lang "已过期"}}</span>{{end}}
</td>
<td>
{{if .BanCount}}
{{if gt .BanCount 3}}{{sub .BanCount 3}}次封禁{{else}}仅计数{{end}}
{{if gt .BanCount 3}}{{tf $.Lang "第 %d 次封禁" (sub .BanCount 3)}}{{else}}{{t $.Lang "仅计数"}}{{end}}
{{else}}—{{end}}
</td>
<td>{{.FailCount}}</td>
<td>{{if .Reason}}{{.Reason}}{{else}}—{{end}}</td>
<td>{{time12 .ExpiresAt}}{{if not .Active}}(已过期){{end}}</td>
<td>{{if .Reason}}{{t $.Lang .Reason}}{{else}}—{{end}}</td>
<td>{{time12 $.Lang .ExpiresAt}}{{if not .Active}}{{t $.Lang "(已过期)"}}{{end}}</td>
<td>
<form method="POST" action="/admin/bans/{{.ID}}/unban" style="display:inline;"
onsubmit="return confirm('确定要解封 IP {{.IPAddress}} 吗?解封后该 IP 的封禁档位将清零。');">
<button type="submit" class="btn btn-primary btn-sm">解封</button>
onsubmit="return confirm('{{tf $.Lang "确定要解封 IP %s 解封后该 IP 的封禁档位将清零" .IPAddress}}');">
<button type="submit" class="btn btn-primary btn-sm">{{t $.Lang "解封"}}</button>
</form>
</td>
</tr>
@@ -67,17 +67,17 @@
</tbody>
</table>
{{if not .rows}}
<p style="color:#7f8c8d;text-align:center;padding:40px 0;">暂无封禁记录</p>
<p style="color:#7f8c8d;text-align:center;padding:40px 0;">{{t $.Lang "暂无封禁记录"}}</p>
{{end}}
</div>
{{if .totalPages}}
<div class="pagination">
{{if gt .page 1}}
<a href="/admin/bans?page={{sub .page 1}}">上一页</a>
<a href="/admin/bans?page={{sub .page 1}}">{{t $.Lang "上一页"}}</a>
{{end}}
<span>第 {{.page}} / {{.totalPages}} 页(共 {{.total}} 条)</span>
<span>{{tf $.Lang "第 %d / %d 页(共 %d 条)" .page .totalPages .total}}</span>
{{if lt .page .totalPages}}
<a href="/admin/bans?page={{add .page 1}}">下一页</a>
<a href="/admin/bans?page={{add .page 1}}">{{t $.Lang "下一页"}}</a>
{{end}}
</div>
{{end}}
+28 -28
View File
@@ -1,10 +1,10 @@
{{define "admin_connections"}}
<!DOCTYPE html>
<html lang="zh-CN">
<html lang="{{htmlLang .Lang}}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>当前连接 - MailGo</title>
<title>{{t .Lang "当前连接"}} - MailGo</title>
<meta http-equiv="refresh" content="5">
{{template "styles" .}}
</head>
@@ -13,26 +13,26 @@
<div class="container">
<div class="clearfix">
<div class="sidebar">
<a href="/inbox">返回邮箱</a>
<a href="/admin" {{if eq .activeFolder "admin"}}class="active"{{end}}>控制面板</a>
<a href="/admin/domains" {{if eq .activeFolder "domains"}}class="active"{{end}}>域名管理</a>
<a href="/admin/users" {{if eq .activeFolder "users"}}class="active"{{end}}>用户管理</a>
<a href="/admin/mails" {{if eq .activeFolder "mails"}}class="active"{{end}}>所有邮件</a>
<a href="/admin/outbound" {{if eq .activeFolder "outbound"}}class="active"{{end}}>外发队列</a>
<a href="/admin/protocol-logs" {{if eq .activeFolder "protocol-logs"}}class="active"{{end}}>协议日志</a>
<a href="/admin/connections" {{if eq .activeFolder "connections"}}class="active"{{end}}>当前连接</a>
<a href="/admin/bans" {{if eq .activeFolder "bans"}}class="active"{{end}}>IP黑名单</a>
<a href="/inbox">{{t .Lang "返回邮箱"}}</a>
<a href="/admin" {{if eq .activeFolder "admin"}}class="active"{{end}}>{{t .Lang "控制面板"}}</a>
<a href="/admin/domains" {{if eq .activeFolder "domains"}}class="active"{{end}}>{{t .Lang "域名管理"}}</a>
<a href="/admin/users" {{if eq .activeFolder "users"}}class="active"{{end}}>{{t .Lang "用户管理"}}</a>
<a href="/admin/mails" {{if eq .activeFolder "mails"}}class="active"{{end}}>{{t .Lang "所有邮件"}}</a>
<a href="/admin/outbound" {{if eq .activeFolder "outbound"}}class="active"{{end}}>{{t .Lang "外发队列"}}</a>
<a href="/admin/protocol-logs" {{if eq .activeFolder "protocol-logs"}}class="active"{{end}}>{{t .Lang "协议日志"}}</a>
<a href="/admin/connections" {{if eq .activeFolder "connections"}}class="active"{{end}}>{{t .Lang "当前连接"}}</a>
<a href="/admin/bans" {{if eq .activeFolder "bans"}}class="active"{{end}}>{{t .Lang "IP黑名单"}}</a>
</div>
<div class="content">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;">
<h2>当前连接(SMTP / IMAP / POP3</h2>
<span style="color:#7f8c8d;font-size:13px;">每 5 秒自动刷新</span>
<h2>{{t .Lang "当前连接(SMTP / IMAP / POP3"}}</h2>
<span style="color:#7f8c8d;font-size:13px;">{{t .Lang "每 5 秒自动刷新"}}</span>
</div>
<div style="margin-bottom:24px;">
<div class="stat-card">
<h3>{{.total}}</h3>
<p>当前连接总数</p>
<p>{{t .Lang "当前连接总数"}}</p>
</div>
<div class="stat-card">
<h3>{{.smtpCount}}</h3>
@@ -53,15 +53,15 @@
<thead>
<tr>
<th>ID</th>
<th>协议</th>
<th>来源 IP</th>
<th>端口</th>
<th>用户名</th>
<th>{{t .Lang "协议"}}</th>
<th>{{t .Lang "来源 IP"}}</th>
<th>{{t .Lang "端口"}}</th>
<th>{{t .Lang "用户名"}}</th>
<th>TLS</th>
<th>连接时间</th>
<th>时长</th>
<th>最后活跃</th>
<th>操作</th>
<th>{{t .Lang "连接时间"}}</th>
<th>{{t .Lang "时长"}}</th>
<th>{{t .Lang "最后活跃"}}</th>
<th>{{t .Lang "操作"}}</th>
</tr>
</thead>
<tbody>
@@ -76,19 +76,19 @@
<td>{{.IP}}</td>
<td>{{.Port}}</td>
<td>{{if .User}}{{.User}}{{else}}—{{end}}</td>
<td>{{if .TLS}}<span class="badge" style="background:#27ae60;color:#fff;">TLS</span>{{else}}<span class="badge" style="background:#95a5a6;color:#fff;">明文</span>{{end}}</td>
<td>{{time12 .Connected}}</td>
<td>{{if .TLS}}<span class="badge" style="background:#27ae60;color:#fff;">TLS</span>{{else}}<span class="badge" style="background:#95a5a6;color:#fff;">{{t $.Lang "明文"}}</span>{{end}}</td>
<td>{{time12 $.Lang .Connected}}</td>
<td>{{durationSeconds ($.now.Sub .Connected)}}s</td>
<td>{{time12 .LastActive}}</td>
<td>{{time12 $.Lang .LastActive}}</td>
<td>
<form method="POST" action="/admin/connections/{{.ID}}/disconnect" style="display:inline;"
onsubmit="return confirm('确定要断开 IP {{.IP}} 的所有连接并加入黑名单(180 天)吗?');">
<button type="submit" class="btn btn-sm btn-danger">断开并封禁</button>
onsubmit="return confirm('{{tf $.Lang "确定要断开 IP %s 的所有连接并加入黑名单180 " .IP}}');">
<button type="submit" class="btn btn-sm btn-danger">{{t $.Lang "断开并封禁"}}</button>
</form>
</td>
</tr>
{{else}}
<tr><td colspan="10" style="text-align:center;color:#7f8c8d;">当前没有活动连接</td></tr>
<tr><td colspan="10" style="text-align:center;color:#7f8c8d;">{{t $.Lang "当前没有活动连接"}}</td></tr>
{{end}}
</tbody>
</table>
+34 -34
View File
@@ -1,10 +1,10 @@
{{define "admin_dashboard"}}
<!DOCTYPE html>
<html lang="zh-CN">
<html lang="{{htmlLang .Lang}}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>管理后台 - MailGo</title>
<title>{{t .Lang "管理后台"}} - MailGo</title>
{{template "styles" .}}
</head>
<body>
@@ -12,74 +12,74 @@
<div class="container">
<div class="clearfix">
<div class="sidebar">
<a href="/inbox">返回邮箱</a>
<a href="/admin" {{if eq .activeFolder "admin"}}class="active"{{end}}>控制面板</a>
<a href="/admin/domains" {{if eq .activeFolder "domains"}}class="active"{{end}}>域名管理</a>
<a href="/admin/users" {{if eq .activeFolder "users"}}class="active"{{end}}>用户管理</a>
<a href="/admin/mails" {{if eq .activeFolder "mails"}}class="active"{{end}}>所有邮件</a>
<a href="/admin/outbound" {{if eq .activeFolder "outbound"}}class="active"{{end}}>外发队列</a>
<a href="/admin/protocol-logs" {{if eq .activeFolder "protocol-logs"}}class="active"{{end}}>协议日志</a>
<a href="/admin/connections" {{if eq .activeFolder "connections"}}class="active"{{end}}>当前连接</a>
<a href="/admin/bans" {{if eq .activeFolder "bans"}}class="active"{{end}}>IP黑名单</a>
<a href="/inbox">{{t .Lang "返回邮箱"}}</a>
<a href="/admin" {{if eq .activeFolder "admin"}}class="active"{{end}}>{{t .Lang "控制面板"}}</a>
<a href="/admin/domains" {{if eq .activeFolder "domains"}}class="active"{{end}}>{{t .Lang "域名管理"}}</a>
<a href="/admin/users" {{if eq .activeFolder "users"}}class="active"{{end}}>{{t .Lang "用户管理"}}</a>
<a href="/admin/mails" {{if eq .activeFolder "mails"}}class="active"{{end}}>{{t .Lang "所有邮件"}}</a>
<a href="/admin/outbound" {{if eq .activeFolder "outbound"}}class="active"{{end}}>{{t .Lang "外发队列"}}</a>
<a href="/admin/protocol-logs" {{if eq .activeFolder "protocol-logs"}}class="active"{{end}}>{{t .Lang "协议日志"}}</a>
<a href="/admin/connections" {{if eq .activeFolder "connections"}}class="active"{{end}}>{{t .Lang "当前连接"}}</a>
<a href="/admin/bans" {{if eq .activeFolder "bans"}}class="active"{{end}}>{{t .Lang "IP黑名单"}}</a>
</div>
<div class="content">
<h2 style="margin-bottom:24px;">管理后台</h2>
<h2 style="margin-bottom:24px;">{{t .Lang "管理后台"}}</h2>
<div style="margin-bottom:24px;">
<div class="stat-card">
<h3>{{.domainCount}}</h3>
<p>域名数</p>
<p>{{t .Lang "域名数"}}</p>
</div>
<div class="stat-card">
<h3>{{.userCount}}</h3>
<p>用户数</p>
<p>{{t .Lang "用户数"}}</p>
</div>
<div class="stat-card">
<h3>{{.totalMails}}</h3>
<p>邮件总数</p>
<p>{{t .Lang "邮件总数"}}</p>
</div>
<div class="stat-card">
<h3>{{.banCount}}</h3>
<p>被封IP</p>
<p>{{t .Lang "被封IP"}}</p>
</div>
</div>
<div class="card">
<h3>邮件分布</h3>
<h3>{{t .Lang "邮件分布"}}</h3>
<table style="margin-top:12px;">
<thead>
<tr>
<th>文件夹</th>
<th>邮件数</th>
<th>占用空间</th>
<th>{{t .Lang "文件夹"}}</th>
<th>{{t .Lang "邮件数"}}</th>
<th>{{t .Lang "占用空间"}}</th>
</tr>
</thead>
<tbody>
<tr><td>收件箱 (INBOX)</td><td>{{.inboxCount}}</td><td>{{formatBytes .inboxSize}}</td></tr>
<tr><td>发件箱 (Sent)</td><td>{{.sentCount}}</td><td>{{formatBytes .sentSize}}</td></tr>
<tr><td>草稿箱 (Drafts)</td><td>{{.draftsCount}}</td><td></td></tr>
<tr><td>垃圾箱 (Trash)</td><td>{{.trashCount}}</td><td></td></tr>
<tr style="font-weight:bold;"><td>合计</td><td>{{.totalMails}}</td><td>{{formatBytes .totalSize}}</td></tr>
<tr><td>{{folderLabel .Lang "INBOX"}} (INBOX)</td><td>{{.inboxCount}}</td><td>{{formatBytes .inboxSize}}</td></tr>
<tr><td>{{folderLabel .Lang "Sent"}} (Sent)</td><td>{{.sentCount}}</td><td>{{formatBytes .sentSize}}</td></tr>
<tr><td>{{folderLabel .Lang "Drafts"}} (Drafts)</td><td>{{.draftsCount}}</td><td></td></tr>
<tr><td>{{folderLabel .Lang "Trash"}} (Trash)</td><td>{{.trashCount}}</td><td></td></tr>
<tr style="font-weight:bold;"><td>{{t .Lang "合计"}}</td><td>{{.totalMails}}</td><td>{{formatBytes .totalSize}}</td></tr>
</tbody>
</table>
</div>
<div class="card">
<h3>收发统计</h3>
<h3>{{t .Lang "收发统计"}}</h3>
<table style="margin-top:12px;">
<thead>
<tr><th>时间段</th><th>收件</th><th>发件</th></tr>
<tr><th>{{t .Lang "时间段"}}</th><th>{{t .Lang "收件"}}</th><th>{{t .Lang "发件"}}</th></tr>
</thead>
<tbody>
<tr><td>今日</td><td>{{.todayReceived}}</td><td>{{.todaySent}}</td></tr>
<tr><td>近 7 天</td><td>{{.weekReceived}}</td><td>{{.weekSent}}</td></tr>
<tr><td>{{t .Lang "今日"}}</td><td>{{.todayReceived}}</td><td>{{.todaySent}}</td></tr>
<tr><td>{{t .Lang "近 7 天"}}</td><td>{{.weekReceived}}</td><td>{{.weekSent}}</td></tr>
</tbody>
</table>
</div>
<div class="card">
<h3>快捷操作</h3>
<h3>{{t .Lang "快捷操作"}}</h3>
<p style="margin-top:12px;">
<a href="/admin/domains/new" class="btn btn-primary">新增域名</a>
<a href="/admin/users/new" class="btn btn-primary" style="margin-left:8px;">新增用户</a>
<a href="/admin/mails" class="btn btn-primary" style="margin-left:8px;">查看所有邮件</a>
<a href="/admin/bans" class="btn btn-primary" style="margin-left:8px;">IP黑名单</a>
<a href="/admin/domains/new" class="btn btn-primary">{{t .Lang "新增域名"}}</a>
<a href="/admin/users/new" class="btn btn-primary" style="margin-left:8px;">{{t .Lang "新增用户"}}</a>
<a href="/admin/mails" class="btn btn-primary" style="margin-left:8px;">{{t .Lang "查看所有邮件"}}</a>
<a href="/admin/bans" class="btn btn-primary" style="margin-left:8px;">{{t .Lang "IP黑名单"}}</a>
</p>
</div>
</div>
+19 -19
View File
@@ -1,10 +1,10 @@
{{define "admin_dns_hint"}}
<!DOCTYPE html>
<html lang="zh-CN">
<html lang="{{htmlLang .Lang}}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>DNS配置 - MailGo</title>
<title>{{t .Lang "DNS配置"}} - MailGo</title>
{{template "styles" .}}
</head>
<body>
@@ -12,39 +12,39 @@
<div class="container">
<div class="clearfix">
<div class="sidebar">
<a href="/inbox">返回邮箱</a>
<a href="/admin" {{if eq .activeFolder "admin"}}class="active"{{end}}>控制面板</a>
<a href="/admin/domains" {{if eq .activeFolder "domains"}}class="active"{{end}}>域名管理</a>
<a href="/admin/users" {{if eq .activeFolder "users"}}class="active"{{end}}>用户管理</a>
<a href="/admin/mails" {{if eq .activeFolder "mails"}}class="active"{{end}}>所有邮件</a>
<a href="/admin/outbound" {{if eq .activeFolder "outbound"}}class="active"{{end}}>外发队列</a>
<a href="/admin/protocol-logs" {{if eq .activeFolder "protocol-logs"}}class="active"{{end}}>协议日志</a>
<a href="/admin/connections" {{if eq .activeFolder "connections"}}class="active"{{end}}>当前连接</a>
<a href="/admin/bans" {{if eq .activeFolder "bans"}}class="active"{{end}}>IP黑名单</a>
<a href="/inbox">{{t .Lang "返回邮箱"}}</a>
<a href="/admin" {{if eq .activeFolder "admin"}}class="active"{{end}}>{{t .Lang "控制面板"}}</a>
<a href="/admin/domains" {{if eq .activeFolder "domains"}}class="active"{{end}}>{{t .Lang "域名管理"}}</a>
<a href="/admin/users" {{if eq .activeFolder "users"}}class="active"{{end}}>{{t .Lang "用户管理"}}</a>
<a href="/admin/mails" {{if eq .activeFolder "mails"}}class="active"{{end}}>{{t .Lang "所有邮件"}}</a>
<a href="/admin/outbound" {{if eq .activeFolder "outbound"}}class="active"{{end}}>{{t .Lang "外发队列"}}</a>
<a href="/admin/protocol-logs" {{if eq .activeFolder "protocol-logs"}}class="active"{{end}}>{{t .Lang "协议日志"}}</a>
<a href="/admin/connections" {{if eq .activeFolder "connections"}}class="active"{{end}}>{{t .Lang "当前连接"}}</a>
<a href="/admin/bans" {{if eq .activeFolder "bans"}}class="active"{{end}}>{{t .Lang "IP黑名单"}}</a>
</div>
<div class="content">
<div class="card">
<h2 style="margin-bottom:16px;">DNS 配置提示 — {{.domain.Name}}</h2>
<p style="margin-bottom:16px;color:#7f8c8d;">请在您的 DNS 服务商处添加以下记录:</p>
<h2 style="margin-bottom:16px;">{{tf .Lang "DNS 配置提示 — %s" .domain.Name}}</h2>
<p style="margin-bottom:16px;color:#7f8c8d;">{{t .Lang "请在您的 DNS 服务商处添加以下记录:"}}</p>
<h4 style="margin-bottom:8px;">MX 记录</h4>
<h4 style="margin-bottom:8px;">{{t .Lang "MX 记录"}}</h4>
<div class="dns-record">@ IN MX 10 mail.{{.domain.Name}}.</div>
<h4 style="margin-bottom:8px;">SPF 记录 (TXT)</h4>
<h4 style="margin-bottom:8px;">{{t .Lang "SPF 记录 (TXT)"}}</h4>
<div class="dns-record">@ IN TXT "v=spf1 mx -all"</div>
<h4 style="margin-bottom:8px;">DKIM 记录 (TXT)</h4>
<h4 style="margin-bottom:8px;">{{t .Lang "DKIM 记录 (TXT)"}}</h4>
{{if .domain.DkimPublicKey}}
<div class="dns-record">default._domainkey.{{.domain.Name}}. IN TXT "{{.dkimRecord}}"</div>
{{else}}
<div class="dns-record" style="color:#e67e22;">⚠️ DKIM 密钥尚未生成,请编辑域名重新生成。</div>
<div class="dns-record" style="color:#e67e22;">{{t .Lang "⚠️ DKIM 密钥尚未生成,请编辑域名重新生成。"}}</div>
{{end}}
<h4 style="margin-bottom:8px;">DMARC 记录</h4>
<h4 style="margin-bottom:8px;">{{t .Lang "DMARC 记录"}}</h4>
<div class="dns-record">_dmarc.{{.domain.Name}}. IN TXT "v=DMARC1; p=none; rua=mailto:postmaster@{{.domain.Name}}"</div>
<div style="margin-top:24px;">
<a href="/admin/domains" class="btn" style="background:#bdc3c7;color:#fff;">返回域名列表</a>
<a href="/admin/domains" class="btn" style="background:#bdc3c7;color:#fff;">{{t .Lang "返回域名列表"}}</a>
</div>
</div>
</div>
+34 -34
View File
@@ -1,10 +1,10 @@
{{define "admin_domain_form"}}
<!DOCTYPE html>
<html lang="zh-CN">
<html lang="{{htmlLang .Lang}}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>{{if .isEdit}}编辑域名{{else}}新增域名{{end}} - MailGo</title>
<title>{{if .isEdit}}{{t .Lang "编辑域名"}}{{else}}{{t .Lang "新增域名"}}{{end}} - MailGo</title>
{{template "styles" .}}
</head>
<body>
@@ -12,24 +12,24 @@
<div class="container">
<div class="clearfix">
<div class="sidebar">
<a href="/inbox">返回邮箱</a>
<a href="/admin" {{if eq .activeFolder "admin"}}class="active"{{end}}>控制面板</a>
<a href="/admin/domains" {{if eq .activeFolder "domains"}}class="active"{{end}}>域名管理</a>
<a href="/admin/users" {{if eq .activeFolder "users"}}class="active"{{end}}>用户管理</a>
<a href="/admin/mails" {{if eq .activeFolder "mails"}}class="active"{{end}}>所有邮件</a>
<a href="/admin/outbound" {{if eq .activeFolder "outbound"}}class="active"{{end}}>外发队列</a>
<a href="/admin/protocol-logs" {{if eq .activeFolder "protocol-logs"}}class="active"{{end}}>协议日志</a>
<a href="/admin/connections" {{if eq .activeFolder "connections"}}class="active"{{end}}>当前连接</a>
<a href="/admin/bans" {{if eq .activeFolder "bans"}}class="active"{{end}}>IP黑名单</a>
<a href="/inbox">{{t .Lang "返回邮箱"}}</a>
<a href="/admin" {{if eq .activeFolder "admin"}}class="active"{{end}}>{{t .Lang "控制面板"}}</a>
<a href="/admin/domains" {{if eq .activeFolder "domains"}}class="active"{{end}}>{{t .Lang "域名管理"}}</a>
<a href="/admin/users" {{if eq .activeFolder "users"}}class="active"{{end}}>{{t .Lang "用户管理"}}</a>
<a href="/admin/mails" {{if eq .activeFolder "mails"}}class="active"{{end}}>{{t .Lang "所有邮件"}}</a>
<a href="/admin/outbound" {{if eq .activeFolder "outbound"}}class="active"{{end}}>{{t .Lang "外发队列"}}</a>
<a href="/admin/protocol-logs" {{if eq .activeFolder "protocol-logs"}}class="active"{{end}}>{{t .Lang "协议日志"}}</a>
<a href="/admin/connections" {{if eq .activeFolder "connections"}}class="active"{{end}}>{{t .Lang "当前连接"}}</a>
<a href="/admin/bans" {{if eq .activeFolder "bans"}}class="active"{{end}}>{{t .Lang "IP黑名单"}}</a>
</div>
<div class="content">
<div class="card">
<h2 style="margin-bottom:16px;">{{if .isEdit}}编辑域名{{else}}新增域名{{end}}</h2>
<h2 style="margin-bottom:16px;">{{if .isEdit}}{{t .Lang "编辑域名"}}{{else}}{{t .Lang "新增域名"}}{{end}}</h2>
{{if .error}}<div class="alert alert-error">{{.error}}</div>{{end}}
{{if .caddyMsg}}<div class="alert {{if eq .caddyMsgType "success"}}alert-success{{else}}alert-error{{end}}">{{.caddyMsg}}</div>{{end}}
<form method="POST" action="{{if .isEdit}}/admin/domains/{{.domain.ID}}{{else}}/admin/domains{{end}}">
<div class="form-group">
<label>域名</label>
<label>{{t .Lang "域名"}}</label>
{{if .isEdit}}
<input type="text" name="name" value="{{.domain.Name}}" disabled style="background:#f5f5f5;">
{{else}}
@@ -37,45 +37,45 @@
{{end}}
</div>
<div class="form-group">
<label>SMTP 端口</label>
<label>{{t .Lang "SMTP 端口"}}</label>
<input type="number" name="smtp_port" value="{{.domain.SmtpPort}}" placeholder="25">
</div>
<div class="form-group">
<label>IMAP 端口</label>
<label>{{t .Lang "IMAP 端口"}}</label>
<input type="number" name="imap_port" value="{{.domain.ImapPort}}" placeholder="143">
</div>
<div class="form-group">
<label>POP3 端口</label>
<label>{{t .Lang "POP3 端口"}}</label>
<input type="number" name="pop3_port" value="{{.domain.Pop3Port}}" placeholder="110">
</div>
<div class="form-group">
<label>
<input type="checkbox" name="tls_enabled" id="tls_enabled" {{if .domain.TlsEnabled}}checked{{end}} onchange="togglePorts()">
启用 TLS
{{t .Lang "启用 TLS"}}
</label>
<p style="color:#7f8c8d;font-size:12px;margin-top:4px;">勾选后端口将自动切换为 SSL/TLS 标准端口</p>
<p style="color:#7f8c8d;font-size:12px;margin-top:4px;">{{t .Lang "勾选后端口将自动切换为 SSL/TLS 标准端口"}}</p>
</div>
{{if .isEdit}}
<div id="tls_cert_fields" style="display:none;">
<div class="form-group">
<label>TLS 私钥 PEM</label>
<label>{{t .Lang "TLS 私钥 PEM"}}</label>
<textarea name="tls_private_key" rows="8" placeholder="-----BEGIN PRIVATE KEY-----&#10;...&#10;-----END PRIVATE KEY-----" style="font-family:monospace;"></textarea>
<p style="color:#7f8c8d;font-size:12px;margin-top:4px;">私钥不会回显;留空则保留现有私钥。</p>
<p style="color:#7f8c8d;font-size:12px;margin-top:4px;">{{t .Lang "私钥不会回显;留空则保留现有私钥。"}}</p>
</div>
<div class="form-group">
<label>TLS 公钥证书 PEM</label>
<label>{{t .Lang "TLS 公钥证书 PEM"}}</label>
<textarea name="tls_public_cert" rows="8" placeholder="-----BEGIN CERTIFICATE-----&#10;...&#10;-----END CERTIFICATE-----" style="font-family:monospace;">{{.tlsPublicCert}}</textarea>
{{if .tlsCertConfigured}}
<p style="color:#27ae60;font-size:12px;margin-top:4px;">✅ TLS 证书已配置;上传新证书后自动热加载生效。</p>
<p style="color:#27ae60;font-size:12px;margin-top:4px;">{{t .Lang "✅ TLS 证书已配置;上传新证书后自动热加载生效。"}}</p>
{{else}}
<p style="color:#e67e22;font-size:12px;margin-top:4px;">⚠️ TLS 证书未配置,启用 TLS 时必须同时填写私钥和证书。</p>
<p style="color:#e67e22;font-size:12px;margin-top:4px;">{{t .Lang "⚠️ TLS 证书未配置,启用 TLS 时必须同时填写私钥和证书。"}}</p>
{{end}}
</div>
</div>
<div class="form-group" style="margin-top:4px;">
<label>从 Caddy 获取证书</label>
<p style="color:#7f8c8d;font-size:12px;margin-top:2px;margin-bottom:8px;">若该域名已由本机 Caddy 托管 HTTPS(自动签发证书),可一键导入其证书与私钥,并自动启用 TLS;证书热加载,无需重启服务。</p>
<button type="button" class="btn" id="btn_fetch_caddy" onclick="fetchCaddyCert()" style="background:#2e86de;color:#fff;">🔒 从 Caddy 获取证书</button>
<label>{{t .Lang "从 Caddy 获取证书"}}</label>
<p style="color:#7f8c8d;font-size:12px;margin-top:2px;margin-bottom:8px;">{{t .Lang "若该域名已由本机 Caddy 托管 HTTPS(自动签发证书),可一键导入其证书与私钥,并自动启用 TLS;证书热加载,无需重启服务。"}}</p>
<button type="button" class="btn" id="btn_fetch_caddy" onclick="fetchCaddyCert()" style="background:#2e86de;color:#fff;">{{t .Lang "🔒 从 Caddy 获取证书"}}</button>
<span id="caddy_fetch_msg" style="margin-left:10px;font-size:12px;color:#7f8c8d;"></span>
<script>
async function fetchCaddyCert() {
@@ -83,7 +83,7 @@
var msg = document.getElementById('caddy_fetch_msg');
btn.disabled = true;
var oldText = btn.textContent;
btn.textContent = '获取中…';
btn.textContent = '{{t .Lang "获取中…"}}';
msg.textContent = '';
try {
var resp = await fetch('/admin/domains/{{.domain.ID}}/fetch-caddy-cert', { method: 'POST' });
@@ -91,9 +91,9 @@
window.location.href = resp.url;
return;
}
msg.textContent = '获取失败: ' + await resp.text();
msg.textContent = '{{t .Lang "获取失败"}}: ' + await resp.text();
} catch (e) {
msg.textContent = '请求失败: ' + e;
msg.textContent = '{{t .Lang "请求失败"}}: ' + e;
}
btn.disabled = false;
btn.textContent = oldText;
@@ -118,17 +118,17 @@
<div class="form-group">
<label>
<input type="checkbox" name="regenerate_dkim">
重新生成 DKIM 密钥
{{t .Lang "重新生成 DKIM 密钥"}}
</label>
{{if .domain.DkimPublicKey}}
<p style="color:#27ae60;font-size:12px;margin-top:4px;">✅ DKIM 密钥已配置</p>
<p style="color:#27ae60;font-size:12px;margin-top:4px;">{{t .Lang "✅ DKIM 密钥已配置"}}</p>
{{else}}
<p style="color:#e67e22;font-size:12px;margin-top:4px;">⚠️ DKIM 密钥未配置</p>
<p style="color:#e67e22;font-size:12px;margin-top:4px;">{{t .Lang "⚠️ DKIM 密钥未配置"}}</p>
{{end}}
</div>
{{end}}
<button type="submit" class="btn btn-primary">{{if .isEdit}}保存更改{{else}}创建域名{{end}}</button>
<a href="/admin/domains" class="btn" style="margin-left:8px;background:#bdc3c7;color:#fff;">取消</a>
<button type="submit" class="btn btn-primary">{{if .isEdit}}{{t .Lang "保存更改"}}{{else}}{{t .Lang "创建域名"}}{{end}}</button>
<a href="/admin/domains" class="btn" style="margin-left:8px;background:#bdc3c7;color:#fff;">{{t .Lang "取消"}}</a>
</form>
</div>
</div>
+22 -22
View File
@@ -1,10 +1,10 @@
{{define "admin_domains"}}
<!DOCTYPE html>
<html lang="zh-CN">
<html lang="{{htmlLang .Lang}}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>域名管理 - MailGo</title>
<title>{{t .Lang "域名管理"}} - MailGo</title>
{{template "styles" .}}
</head>
<body>
@@ -12,35 +12,35 @@
<div class="container">
<div class="clearfix">
<div class="sidebar">
<a href="/inbox">返回邮箱</a>
<a href="/admin" {{if eq .activeFolder "admin"}}class="active"{{end}}>控制面板</a>
<a href="/admin/domains" {{if eq .activeFolder "domains"}}class="active"{{end}}>域名管理</a>
<a href="/admin/users" {{if eq .activeFolder "users"}}class="active"{{end}}>用户管理</a>
<a href="/admin/mails" {{if eq .activeFolder "mails"}}class="active"{{end}}>所有邮件</a>
<a href="/admin/outbound" {{if eq .activeFolder "outbound"}}class="active"{{end}}>外发队列</a>
<a href="/admin/protocol-logs" {{if eq .activeFolder "protocol-logs"}}class="active"{{end}}>协议日志</a>
<a href="/admin/connections" {{if eq .activeFolder "connections"}}class="active"{{end}}>当前连接</a>
<a href="/admin/bans" {{if eq .activeFolder "bans"}}class="active"{{end}}>IP黑名单</a>
<a href="/inbox">{{t .Lang "返回邮箱"}}</a>
<a href="/admin" {{if eq .activeFolder "admin"}}class="active"{{end}}>{{t .Lang "控制面板"}}</a>
<a href="/admin/domains" {{if eq .activeFolder "domains"}}class="active"{{end}}>{{t .Lang "域名管理"}}</a>
<a href="/admin/users" {{if eq .activeFolder "users"}}class="active"{{end}}>{{t .Lang "用户管理"}}</a>
<a href="/admin/mails" {{if eq .activeFolder "mails"}}class="active"{{end}}>{{t .Lang "所有邮件"}}</a>
<a href="/admin/outbound" {{if eq .activeFolder "outbound"}}class="active"{{end}}>{{t .Lang "外发队列"}}</a>
<a href="/admin/protocol-logs" {{if eq .activeFolder "protocol-logs"}}class="active"{{end}}>{{t .Lang "协议日志"}}</a>
<a href="/admin/connections" {{if eq .activeFolder "connections"}}class="active"{{end}}>{{t .Lang "当前连接"}}</a>
<a href="/admin/bans" {{if eq .activeFolder "bans"}}class="active"{{end}}>{{t .Lang "IP黑名单"}}</a>
</div>
<div class="content">
<div class="card">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;">
<h2>域名列表</h2>
<a href="/admin/domains/new" class="btn btn-primary">新增域名</a>
<h2>{{t .Lang "域名列表"}}</h2>
<a href="/admin/domains/new" class="btn btn-primary">{{t .Lang "新增域名"}}</a>
</div>
{{if not .domains}}
<p style="color:#7f8c8d;text-align:center;padding:40px 0;">暂无域名</p>
<p style="color:#7f8c8d;text-align:center;padding:40px 0;">{{t $.Lang "暂无域名"}}</p>
{{else}}
<table>
<thead>
<tr>
<th>ID</th>
<th>域名</th>
<th>{{t $.Lang "域名"}}</th>
<th>SMTP</th>
<th>IMAP</th>
<th>POP3</th>
<th>TLS</th>
<th>操作</th>
<th>{{t $.Lang "操作"}}</th>
</tr>
</thead>
<tbody>
@@ -53,11 +53,11 @@
<td>{{.Pop3Port}}</td>
<td>{{if .TlsEnabled}}✅{{else}}❌{{end}}</td>
<td>
<a href="/admin/domains/{{.ID}}/edit" class="btn btn-primary btn-sm">编辑</a>
<a href="/admin/domains/{{.ID}}/edit" class="btn btn-primary btn-sm">{{t $.Lang "编辑"}}</a>
<a href="/admin/domains/{{.ID}}/dns" class="btn btn-primary btn-sm">DNS</a>
<form method="POST" action="/admin/domains/{{.ID}}/delete" style="display:inline;"
onsubmit="return confirm('确定要删除域名 {{.Name}} 吗?删除域名将导致关联用户无法收发邮件!');">
<button type="submit" class="btn btn-danger btn-sm">删除</button>
onsubmit="return confirm('{{tf $.Lang "确定要删除域名 %s 删除域名将导致关联用户无法收发邮件" .Name}}');">
<button type="submit" class="btn btn-danger btn-sm">{{t $.Lang "删除"}}</button>
</form>
</td>
</tr>
@@ -69,11 +69,11 @@
{{if .totalPages}}
<div class="pagination">
{{if gt .page 1}}
<a href="/admin/domains?page={{sub .page 1}}">上一页</a>
<a href="/admin/domains?page={{sub .page 1}}">{{t $.Lang "上一页"}}</a>
{{end}}
<span>第 {{.page}} / {{.totalPages}}</span>
<span>{{tf $.Lang "第 %d / %d 页" .page .totalPages}}</span>
{{if lt .page .totalPages}}
<a href="/admin/domains?page={{add .page 1}}">下一页</a>
<a href="/admin/domains?page={{add .page 1}}">{{t $.Lang "下一页"}}</a>
{{end}}
</div>
{{end}}
+20 -20
View File
@@ -1,10 +1,10 @@
{{define "admin_mail_view"}}
<!DOCTYPE html>
<html lang="zh-CN">
<html lang="{{htmlLang .Lang}}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>查看邮件 - MailGo 管理后台</title>
<title>{{t .Lang "查看邮件"}} - MailGo {{t .Lang "管理后台"}}</title>
{{template "styles" .}}
<style>
.mail-body-iframe {
@@ -21,29 +21,29 @@
<div class="container">
<div class="clearfix">
<div class="sidebar">
<a href="/inbox">返回邮箱</a>
<a href="/admin" {{if eq .activeFolder "admin"}}class="active"{{end}}>控制面板</a>
<a href="/admin/domains" {{if eq .activeFolder "domains"}}class="active"{{end}}>域名管理</a>
<a href="/admin/users" {{if eq .activeFolder "users"}}class="active"{{end}}>用户管理</a>
<a href="/admin/mails" class="active">所有邮件</a>
<a href="/admin/outbound" {{if eq .activeFolder "outbound"}}class="active"{{end}}>外发队列</a>
<a href="/admin/protocol-logs" {{if eq .activeFolder "protocol-logs"}}class="active"{{end}}>协议日志</a>
<a href="/admin/connections" {{if eq .activeFolder "connections"}}class="active"{{end}}>当前连接</a>
<a href="/admin/bans" {{if eq .activeFolder "bans"}}class="active"{{end}}>IP黑名单</a>
<a href="/inbox">{{t .Lang "返回邮箱"}}</a>
<a href="/admin" {{if eq .activeFolder "admin"}}class="active"{{end}}>{{t .Lang "控制面板"}}</a>
<a href="/admin/domains" {{if eq .activeFolder "domains"}}class="active"{{end}}>{{t .Lang "域名管理"}}</a>
<a href="/admin/users" {{if eq .activeFolder "users"}}class="active"{{end}}>{{t .Lang "用户管理"}}</a>
<a href="/admin/mails" class="active">{{t .Lang "所有邮件"}}</a>
<a href="/admin/outbound" {{if eq .activeFolder "outbound"}}class="active"{{end}}>{{t .Lang "外发队列"}}</a>
<a href="/admin/protocol-logs" {{if eq .activeFolder "protocol-logs"}}class="active"{{end}}>{{t .Lang "协议日志"}}</a>
<a href="/admin/connections" {{if eq .activeFolder "connections"}}class="active"{{end}}>{{t .Lang "当前连接"}}</a>
<a href="/admin/bans" {{if eq .activeFolder "bans"}}class="active"{{end}}>{{t .Lang "IP黑名单"}}</a>
</div>
<div class="content">
<div class="card">
<div style="margin-bottom:16px;">
<a href="/admin/mails" class="btn" style="background:#bdc3c7;color:#fff;">返回邮件列表</a>
<a href="/admin/mails" class="btn" style="background:#bdc3c7;color:#fff;">{{t .Lang "返回邮件列表"}}</a>
</div>
<h2>{{if .message.Subject}}{{.message.Subject}}{{else}}(无主题){{end}}</h2>
<h2>{{if .message.Subject}}{{.message.Subject}}{{else}}{{t .Lang "(无主题)"}}{{end}}</h2>
<div class="mail-meta" style="margin-top:12px;">
<p><strong>发件人:</strong> {{decodeHeader .message.FromAddr}}</p>
<p><strong>收件人:</strong> {{.message.ToAddr}}</p>
{{if .message.CcAddr}}<p><strong>抄送:</strong> {{.message.CcAddr}}</p>{{end}}
<p><strong>所属用户:</strong> {{if .message.User.ID}}{{.message.User.Username}}{{else}}—{{end}}</p>
<p><strong>文件夹:</strong> {{.message.Folder}}</p>
<p><strong>时间:</strong> {{time12 .message.Date}}</p>
<p><strong>{{t .Lang "发件人:"}}</strong> {{decodeHeader .message.FromAddr}}</p>
<p><strong>{{t .Lang "收件人:"}}</strong> {{.message.ToAddr}}</p>
{{if .message.CcAddr}}<p><strong>{{t .Lang "抄送:"}}</strong> {{.message.CcAddr}}</p>{{end}}
<p><strong>{{t .Lang "所属用户:"}}</strong> {{if .message.User.ID}}{{.message.User.Username}}{{else}}—{{end}}</p>
<p><strong>{{t .Lang "文件夹:"}}</strong> {{.message.Folder}}</p>
<p><strong>{{t .Lang "时间:"}}</strong> {{time12 .Lang .message.Date}}</p>
</div>
<div class="mail-body">
{{if .message.HtmlBody}}
@@ -54,7 +54,7 @@
</div>
{{if .attachments}}
<div class="attachment-list">
<h4 style="margin-bottom:8px;">附件</h4>
<h4 style="margin-bottom:8px;">{{t .Lang "附件列表"}}</h4>
{{range .attachments}}
<div class="attachment-item">
📎 <a href="/admin/attachment/{{.ID}}">{{.FileName}}</a>
+24 -24
View File
@@ -1,10 +1,10 @@
{{define "admin_mails"}}
<!DOCTYPE html>
<html lang="zh-CN">
<html lang="{{htmlLang .Lang}}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>所有邮件 - MailGo</title>
<title>{{t .Lang "所有邮件"}} - MailGo</title>
{{template "styles" .}}
</head>
<body>
@@ -12,38 +12,38 @@
<div class="container">
<div class="clearfix">
<div class="sidebar">
<a href="/inbox">返回邮箱</a>
<a href="/admin" {{if eq .activeFolder "admin"}}class="active"{{end}}>控制面板</a>
<a href="/admin/domains" {{if eq .activeFolder "domains"}}class="active"{{end}}>域名管理</a>
<a href="/admin/users" {{if eq .activeFolder "users"}}class="active"{{end}}>用户管理</a>
<a href="/admin/mails" {{if eq .activeFolder "mails"}}class="active"{{end}}>所有邮件</a>
<a href="/admin/outbound" {{if eq .activeFolder "outbound"}}class="active"{{end}}>外发队列</a>
<a href="/admin/protocol-logs" {{if eq .activeFolder "protocol-logs"}}class="active"{{end}}>协议日志</a>
<a href="/admin/connections" {{if eq .activeFolder "connections"}}class="active"{{end}}>当前连接</a>
<a href="/admin/bans" {{if eq .activeFolder "bans"}}class="active"{{end}}>IP黑名单</a>
<a href="/inbox">{{t .Lang "返回邮箱"}}</a>
<a href="/admin" {{if eq .activeFolder "admin"}}class="active"{{end}}>{{t .Lang "控制面板"}}</a>
<a href="/admin/domains" {{if eq .activeFolder "domains"}}class="active"{{end}}>{{t .Lang "域名管理"}}</a>
<a href="/admin/users" {{if eq .activeFolder "users"}}class="active"{{end}}>{{t .Lang "用户管理"}}</a>
<a href="/admin/mails" {{if eq .activeFolder "mails"}}class="active"{{end}}>{{t .Lang "所有邮件"}}</a>
<a href="/admin/outbound" {{if eq .activeFolder "outbound"}}class="active"{{end}}>{{t .Lang "外发队列"}}</a>
<a href="/admin/protocol-logs" {{if eq .activeFolder "protocol-logs"}}class="active"{{end}}>{{t .Lang "协议日志"}}</a>
<a href="/admin/connections" {{if eq .activeFolder "connections"}}class="active"{{end}}>{{t .Lang "当前连接"}}</a>
<a href="/admin/bans" {{if eq .activeFolder "bans"}}class="active"{{end}}>{{t .Lang "IP黑名单"}}</a>
</div>
<div class="content">
<div class="card">
<h2 style="margin-bottom:16px;">所有邮件</h2>
<h2 style="margin-bottom:16px;">{{t .Lang "所有邮件"}}</h2>
<div style="margin-bottom:16px;">
<a href="/admin/mails" {{if eq .folder ""}}class="btn btn-primary"{{else}}class="btn" style="background:#ecf0f1;color:#333;"{{end}}>全部</a>
<a href="/admin/mails" {{if eq .folder ""}}class="btn btn-primary"{{else}}class="btn" style="background:#ecf0f1;color:#333;"{{end}}>{{t .Lang "全部"}}</a>
<a href="/admin/mails?folder=INBOX" {{if eq .folder "INBOX"}}class="btn btn-primary"{{else}}class="btn" style="background:#ecf0f1;color:#333;"{{end}}>INBOX</a>
<a href="/admin/mails?folder=Sent" {{if eq .folder "Sent"}}class="btn btn-primary"{{else}}class="btn" style="background:#ecf0f1;color:#333;"{{end}}>Sent</a>
<a href="/admin/mails?folder=Drafts" {{if eq .folder "Drafts"}}class="btn btn-primary"{{else}}class="btn" style="background:#ecf0f1;color:#333;"{{end}}>Drafts</a>
<a href="/admin/mails?folder=Trash" {{if eq .folder "Trash"}}class="btn btn-primary"{{else}}class="btn" style="background:#ecf0f1;color:#333;"{{end}}>Trash</a>
</div>
{{if not .messages}}
<p style="color:#7f8c8d;text-align:center;padding:40px 0;">暂无邮件</p>
<p style="color:#7f8c8d;text-align:center;padding:40px 0;">{{t $.Lang "暂无邮件"}}</p>
{{else}}
<table>
<thead>
<tr>
<th>发件人</th>
<th>收件人</th>
<th>主题</th>
<th>所属用户</th>
<th>文件夹</th>
<th>日期</th>
<th>{{t $.Lang "发件人"}}</th>
<th>{{t $.Lang "收件人"}}</th>
<th>{{t $.Lang "主题"}}</th>
<th>{{t $.Lang "所属用户"}}</th>
<th>{{t $.Lang "文件夹"}}</th>
<th>{{t $.Lang "日期"}}</th>
</tr>
</thead>
<tbody>
@@ -54,7 +54,7 @@
<td>{{.Subject}}</td>
<td>{{if .User.ID}}{{.User.Username}}{{else}}—{{end}}</td>
<td>{{.Folder}}</td>
<td>{{time12m .Date}}</td>
<td>{{time12m $.Lang .Date}}</td>
</tr>
{{end}}
</tbody>
@@ -64,11 +64,11 @@
{{if .totalPages}}
<div class="pagination">
{{if gt .page 1}}
<a href="/admin/mails?page={{sub .page 1}}{{if .folder}}&folder={{.folder}}{{end}}">上一页</a>
<a href="/admin/mails?page={{sub .page 1}}{{if .folder}}&folder={{.folder}}{{end}}">{{t $.Lang "上一页"}}</a>
{{end}}
<span>第 {{.page}} / {{.totalPages}}</span>
<span>{{tf $.Lang "第 %d / %d 页" .page .totalPages}}</span>
{{if lt .page .totalPages}}
<a href="/admin/mails?page={{add .page 1}}{{if .folder}}&folder={{.folder}}{{end}}">下一页</a>
<a href="/admin/mails?page={{add .page 1}}{{if .folder}}&folder={{.folder}}{{end}}">{{t $.Lang "下一页"}}</a>
{{end}}
</div>
{{end}}
+44 -44
View File
@@ -1,10 +1,10 @@
{{define "admin_outbound"}}
<!DOCTYPE html>
<html lang="zh-CN">
<html lang="{{htmlLang .Lang}}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>外发队列 - MailGo</title>
<title>{{t .Lang "外发队列"}} - MailGo</title>
{{template "styles" .}}
</head>
<body>
@@ -12,60 +12,60 @@
<div class="container">
<div class="clearfix">
<div class="sidebar">
<a href="/inbox">返回邮箱</a>
<a href="/admin" {{if eq .activeFolder "admin"}}class="active"{{end}}>控制面板</a>
<a href="/admin/domains" {{if eq .activeFolder "domains"}}class="active"{{end}}>域名管理</a>
<a href="/admin/users" {{if eq .activeFolder "users"}}class="active"{{end}}>用户管理</a>
<a href="/admin/mails" {{if eq .activeFolder "mails"}}class="active"{{end}}>所有邮件</a>
<a href="/admin/outbound" {{if eq .activeFolder "outbound"}}class="active"{{end}}>外发队列</a>
<a href="/admin/protocol-logs" {{if eq .activeFolder "protocol-logs"}}class="active"{{end}}>协议日志</a>
<a href="/admin/connections" {{if eq .activeFolder "connections"}}class="active"{{end}}>当前连接</a>
<a href="/admin/bans" {{if eq .activeFolder "bans"}}class="active"{{end}}>IP黑名单</a>
<a href="/inbox">{{t .Lang "返回邮箱"}}</a>
<a href="/admin" {{if eq .activeFolder "admin"}}class="active"{{end}}>{{t .Lang "控制面板"}}</a>
<a href="/admin/domains" {{if eq .activeFolder "domains"}}class="active"{{end}}>{{t .Lang "域名管理"}}</a>
<a href="/admin/users" {{if eq .activeFolder "users"}}class="active"{{end}}>{{t .Lang "用户管理"}}</a>
<a href="/admin/mails" {{if eq .activeFolder "mails"}}class="active"{{end}}>{{t .Lang "所有邮件"}}</a>
<a href="/admin/outbound" {{if eq .activeFolder "outbound"}}class="active"{{end}}>{{t .Lang "外发队列"}}</a>
<a href="/admin/protocol-logs" {{if eq .activeFolder "protocol-logs"}}class="active"{{end}}>{{t .Lang "协议日志"}}</a>
<a href="/admin/connections" {{if eq .activeFolder "connections"}}class="active"{{end}}>{{t .Lang "当前连接"}}</a>
<a href="/admin/bans" {{if eq .activeFolder "bans"}}class="active"{{end}}>{{t .Lang "IP黑名单"}}</a>
</div>
<div class="content">
<h2 style="margin-bottom:24px;">外发队列</h2>
<h2 style="margin-bottom:24px;">{{t .Lang "外发队列"}}</h2>
<div style="margin-bottom:24px;">
<div class="stat-card">
<h3>{{index .statCounts "pending"}}</h3>
<p>待发送</p>
<p>{{t .Lang "待发送"}}</p>
</div>
<div class="stat-card">
<h3>{{index .statCounts "deferred"}}</h3>
<p>等待重试</p>
<p>{{t .Lang "等待重试"}}</p>
</div>
<div class="stat-card">
<h3>{{index .statCounts "sent"}}</h3>
<p>已送达</p>
<p>{{t .Lang "已送达"}}</p>
</div>
<div class="stat-card">
<h3>{{index .statCounts "failed"}}</h3>
<p>失败</p>
<p>{{t .Lang "失败"}}</p>
</div>
</div>
<div class="card">
<div style="margin-bottom:12px;">
<a href="/admin/outbound" class="btn btn-sm {{if eq .status ""}}btn-primary{{end}}" style="background:{{if eq .status ""}}#3498db{{else}}#ecf0f1{{end}};color:{{if eq .status ""}}#fff{{else}}#333{{end}};">全部</a>
<a href="/admin/outbound?status=pending" class="btn btn-sm">待发送</a>
<a href="/admin/outbound?status=deferred" class="btn btn-sm">等待重试</a>
<a href="/admin/outbound?status=sent" class="btn btn-sm">已送达</a>
<a href="/admin/outbound?status=failed" class="btn btn-sm">失败</a>
<a href="/admin/outbound?status=canceled" class="btn btn-sm">已取消</a>
<a href="/admin/outbound" class="btn btn-sm {{if eq .status ""}}btn-primary{{end}}" style="background:{{if eq .status ""}}#3498db{{else}}#ecf0f1{{end}};color:{{if eq .status ""}}#fff{{else}}#333{{end}};">{{t .Lang "全部"}}</a>
<a href="/admin/outbound?status=pending" class="btn btn-sm">{{t .Lang "待发送"}}</a>
<a href="/admin/outbound?status=deferred" class="btn btn-sm">{{t .Lang "等待重试"}}</a>
<a href="/admin/outbound?status=sent" class="btn btn-sm">{{t .Lang "已送达"}}</a>
<a href="/admin/outbound?status=failed" class="btn btn-sm">{{t .Lang "失败"}}</a>
<a href="/admin/outbound?status=canceled" class="btn btn-sm">{{t .Lang "已取消"}}</a>
</div>
<table>
<thead>
<tr>
<th>ID</th>
<th>发件人</th>
<th>收件人</th>
<th>状态</th>
<th>尝试次数</th>
<th>下次重试</th>
<th>最后响应 / 错误</th>
<th>创建时间</th>
<th>操作</th>
<th>{{t .Lang "发件人"}}</th>
<th>{{t .Lang "收件人"}}</th>
<th>{{t .Lang "状态"}}</th>
<th>{{t .Lang "尝试次数"}}</th>
<th>{{t .Lang "下次重试"}}</th>
<th>{{t .Lang "最后响应 / 错误"}}</th>
<th>{{t .Lang "创建时间"}}</th>
<th>{{t .Lang "操作"}}</th>
</tr>
</thead>
<tbody>
@@ -75,40 +75,40 @@
<td>{{.FromAddr}}</td>
<td>{{.ToAddr}}</td>
<td>
{{if eq .Status "pending"}}<span class="badge" style="background:#f39c12;color:#fff;">{{call $.statusText .Status}}</span>
{{else if eq .Status "deferred"}}<span class="badge" style="background:#e67e22;color:#fff;">{{call $.statusText .Status}}</span>
{{else if eq .Status "sent"}}<span class="badge" style="background:#27ae60;color:#fff;">{{call $.statusText .Status}}</span>
{{else if eq .Status "failed"}}<span class="badge badge-unread">{{call $.statusText .Status}}</span>
{{else}}<span class="badge" style="background:#95a5a6;color:#fff;">{{call $.statusText .Status}}</span>{{end}}
{{if eq .Status "pending"}}<span class="badge" style="background:#f39c12;color:#fff;">{{t $.Lang (call $.statusText .Status)}}</span>
{{else if eq .Status "deferred"}}<span class="badge" style="background:#e67e22;color:#fff;">{{t $.Lang (call $.statusText .Status)}}</span>
{{else if eq .Status "sent"}}<span class="badge" style="background:#27ae60;color:#fff;">{{t $.Lang (call $.statusText .Status)}}</span>
{{else if eq .Status "failed"}}<span class="badge badge-unread">{{t $.Lang (call $.statusText .Status)}}</span>
{{else}}<span class="badge" style="background:#95a5a6;color:#fff;">{{t $.Lang (call $.statusText .Status)}}</span>{{end}}
</td>
<td>{{.Attempts}}</td>
<td>{{if or (eq .Status "pending") (eq .Status "deferred")}}{{time12m .NextAttemptAt}}{{else}}—{{end}}</td>
<td>{{if or (eq .Status "pending") (eq .Status "deferred")}}{{time12m $.Lang .NextAttemptAt}}{{else}}—{{end}}</td>
<td style="max-width:280px;word-break:break-all;">{{if .LastResponse}}{{.LastResponse}}{{else}}{{.LastError}}{{end}}</td>
<td>{{time12m .CreatedAt}}</td>
<td>{{time12m $.Lang .CreatedAt}}</td>
<td>
{{if or (eq .Status "failed") (eq .Status "deferred") (eq .Status "canceled") (eq .Status "pending")}}
<form method="POST" action="/admin/outbound/{{.ID}}/retry" style="display:inline;">
<button type="submit" class="btn btn-sm btn-primary">重试</button>
<button type="submit" class="btn btn-sm btn-primary">{{t $.Lang "重试"}}</button>
</form>
{{end}}
{{if or (eq .Status "pending") (eq .Status "deferred")}}
<form method="POST" action="/admin/outbound/{{.ID}}/cancel" style="display:inline;" onsubmit="return confirm('确认取消该投递任务?');">
<button type="submit" class="btn btn-sm btn-danger">取消</button>
<form method="POST" action="/admin/outbound/{{.ID}}/cancel" style="display:inline;" onsubmit="return confirm('{{t $.Lang "确认取消该投递任务"}}');">
<button type="submit" class="btn btn-sm btn-danger">{{t $.Lang "取消"}}</button>
</form>
{{end}}
</td>
</tr>
{{else}}
<tr><td colspan="9" style="text-align:center;color:#7f8c8d;">队列为空</td></tr>
<tr><td colspan="9" style="text-align:center;color:#7f8c8d;">{{t $.Lang "队列为空"}}</td></tr>
{{end}}
</tbody>
</table>
{{if gt .totalPages 1}}
<div class="pagination">
{{if gt .page 1}}<a href="/admin/outbound?page={{sub .page 1}}&status={{.status}}">上一页</a>{{end}}
<span class="current">第 {{.page}} / {{.totalPages}}</span>
{{if lt .page .totalPages}}<a href="/admin/outbound?page={{add .page 1}}&status={{.status}}">下一页</a>{{end}}
{{if gt .page 1}}<a href="/admin/outbound?page={{sub .page 1}}&status={{.status}}">{{t $.Lang "上一页"}}</a>{{end}}
<span class="current">{{tf $.Lang "第 %d / %d 页" .page .totalPages}}</span>
{{if lt .page .totalPages}}<a href="/admin/outbound?page={{add .page 1}}&status={{.status}}">{{t $.Lang "下一页"}}</a>{{end}}
</div>
{{end}}
</div>
+44 -44
View File
@@ -1,10 +1,10 @@
{{define "admin_protocol_logs"}}
<!DOCTYPE html>
<html lang="zh-CN">
<html lang="{{htmlLang .Lang}}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>协议日志 - MailGo</title>
<title>{{t .Lang "协议日志"}} - MailGo</title>
{{template "styles" .}}
</head>
<body>
@@ -12,41 +12,41 @@
<div class="container">
<div class="clearfix">
<div class="sidebar">
<a href="/inbox">返回邮箱</a>
<a href="/admin" {{if eq .activeFolder "admin"}}class="active"{{end}}>控制面板</a>
<a href="/admin/domains" {{if eq .activeFolder "domains"}}class="active"{{end}}>域名管理</a>
<a href="/admin/users" {{if eq .activeFolder "users"}}class="active"{{end}}>用户管理</a>
<a href="/admin/mails" {{if eq .activeFolder "mails"}}class="active"{{end}}>所有邮件</a>
<a href="/admin/outbound" {{if eq .activeFolder "outbound"}}class="active"{{end}}>外发队列</a>
<a href="/admin/protocol-logs" {{if eq .activeFolder "protocol-logs"}}class="active"{{end}}>协议日志</a>
<a href="/admin/connections" {{if eq .activeFolder "connections"}}class="active"{{end}}>当前连接</a>
<a href="/admin/bans" {{if eq .activeFolder "bans"}}class="active"{{end}}>IP黑名单</a>
<a href="/inbox">{{t .Lang "返回邮箱"}}</a>
<a href="/admin" {{if eq .activeFolder "admin"}}class="active"{{end}}>{{t .Lang "控制面板"}}</a>
<a href="/admin/domains" {{if eq .activeFolder "domains"}}class="active"{{end}}>{{t .Lang "域名管理"}}</a>
<a href="/admin/users" {{if eq .activeFolder "users"}}class="active"{{end}}>{{t .Lang "用户管理"}}</a>
<a href="/admin/mails" {{if eq .activeFolder "mails"}}class="active"{{end}}>{{t .Lang "所有邮件"}}</a>
<a href="/admin/outbound" {{if eq .activeFolder "outbound"}}class="active"{{end}}>{{t .Lang "外发队列"}}</a>
<a href="/admin/protocol-logs" {{if eq .activeFolder "protocol-logs"}}class="active"{{end}}>{{t .Lang "协议日志"}}</a>
<a href="/admin/connections" {{if eq .activeFolder "connections"}}class="active"{{end}}>{{t .Lang "当前连接"}}</a>
<a href="/admin/bans" {{if eq .activeFolder "bans"}}class="active"{{end}}>{{t .Lang "IP黑名单"}}</a>
</div>
<div class="content">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;">
<h2>协议日志(SMTP / IMAP / POP3</h2>
<h2>{{t .Lang "协议日志(SMTP / IMAP / POP3"}}</h2>
<form method="POST" action="/admin/protocol-logs/cleanup" style="display:inline;"
onsubmit="return confirm('确认清理 {{.keepDays}} 天前的协议日志?');">
<button type="submit" class="btn btn-primary">清理旧日志</button>
onsubmit="return confirm('{{tf .Lang "确认清理 %d 天前的协议日志" .keepDays}}');">
<button type="submit" class="btn btn-primary">{{t .Lang "清理旧日志"}}</button>
</form>
</div>
<div style="margin-bottom:24px;">
<div class="stat-card">
<h3>{{index .todayStats "smtp" "fail"}}</h3>
<p>今日 SMTP 失败</p>
<p>{{t .Lang "今日 SMTP 失败"}}</p>
</div>
<div class="stat-card">
<h3>{{index .todayStats "imap" "fail"}}</h3>
<p>今日 IMAP 失败</p>
<p>{{t .Lang "今日 IMAP 失败"}}</p>
</div>
<div class="stat-card">
<h3>{{index .todayStats "pop3" "fail"}}</h3>
<p>今日 POP3 失败</p>
<p>{{t .Lang "今日 POP3 失败"}}</p>
</div>
<div class="stat-card">
<h3>{{add (add (index .allStats "smtp" "fail") (index .allStats "imap" "fail")) (index .allStats "pop3" "fail")}}</h3>
<p>历史失败(全部)</p>
<p>{{t .Lang "历史失败(全部)"}}</p>
</div>
</div>
@@ -54,45 +54,45 @@
<form method="GET" action="/admin/protocol-logs" style="margin-bottom:16px;">
<div style="display:flex;flex-wrap:wrap;gap:12px;align-items:center;">
<select name="protocol" style="padding:6px 10px;">
<option value="">全部协议</option>
<option value="">{{t .Lang "全部协议"}}</option>
<option value="smtp" {{if eq .filter.protocol "smtp"}}selected{{end}}>SMTP</option>
<option value="imap" {{if eq .filter.protocol "imap"}}selected{{end}}>IMAP</option>
<option value="pop3" {{if eq .filter.protocol "pop3"}}selected{{end}}>POP3</option>
</select>
<select name="success" style="padding:6px 10px;">
<option value="">全部状态</option>
<option value="success" {{if eq .filter.success "success"}}selected{{end}}>成功</option>
<option value="fail" {{if eq .filter.success "fail"}}selected{{end}}>失败</option>
<option value="">{{t .Lang "全部状态"}}</option>
<option value="success" {{if eq .filter.success "success"}}selected{{end}}>{{t .Lang "成功"}}</option>
<option value="fail" {{if eq .filter.success "fail"}}selected{{end}}>{{t .Lang "失败"}}</option>
</select>
<input type="text" name="ip" placeholder="来源 IP(模糊)" value="{{.filter.ip}}" style="padding:6px 10px;width:160px;">
<input type="text" name="username" placeholder="用户名(模糊)" value="{{.filter.username}}" style="padding:6px 10px;width:160px;">
<input type="text" name="ip" placeholder="{{t .Lang "来源 IP模糊"}}" value="{{.filter.ip}}" style="padding:6px 10px;width:160px;">
<input type="text" name="username" placeholder="{{t .Lang "用户名模糊"}}" value="{{.filter.username}}" style="padding:6px 10px;width:160px;">
<input type="date" name="from" value="{{.filter.from}}" style="padding:6px 10px;">
<span></span>
<span>{{t .Lang "至"}}</span>
<input type="date" name="to" value="{{.filter.to}}" style="padding:6px 10px;">
<button type="submit" class="btn btn-sm btn-primary">筛选</button>
<a href="/admin/protocol-logs" class="btn btn-sm">重置</a>
<button type="submit" class="btn btn-sm btn-primary">{{t .Lang "筛选"}}</button>
<a href="/admin/protocol-logs" class="btn btn-sm">{{t .Lang "重置"}}</a>
</div>
</form>
<table>
<thead>
<tr>
<th>时间</th>
<th>协议</th>
<th>端口</th>
<th>来源 IP</th>
<th>用户名</th>
<th>状态</th>
<th>失败原因</th>
<th>操作摘要</th>
<th>消息数</th>
<th>时长</th>
<th>{{t .Lang "时间"}}</th>
<th>{{t .Lang "协议"}}</th>
<th>{{t .Lang "端口"}}</th>
<th>{{t .Lang "来源 IP"}}</th>
<th>{{t .Lang "用户名"}}</th>
<th>{{t .Lang "状态"}}</th>
<th>{{t .Lang "失败原因"}}</th>
<th>{{t .Lang "操作摘要"}}</th>
<th>{{t .Lang "消息数"}}</th>
<th>{{t .Lang "时长"}}</th>
</tr>
</thead>
<tbody>
{{range .logs}}
<tr>
<td style="white-space:nowrap;">{{time12 .CreatedAt}}</td>
<td style="white-space:nowrap;">{{time12 $.Lang .CreatedAt}}</td>
<td>
{{if eq .Protocol "smtp"}}<span class="badge" style="background:#3498db;color:#fff;">SMTP</span>
{{else if eq .Protocol "imap"}}<span class="badge" style="background:#9b59b6;color:#fff;">IMAP</span>
@@ -102,8 +102,8 @@
<td>{{.ClientIP}}</td>
<td>{{if .Username}}{{.Username}}{{else}}—{{end}}</td>
<td>
{{if .Success}}<span class="badge" style="background:#27ae60;color:#fff;">成功</span>
{{else}}<span class="badge badge-unread">失败</span>{{end}}
{{if .Success}}<span class="badge" style="background:#27ae60;color:#fff;">{{t $.Lang "成功"}}</span>
{{else}}<span class="badge badge-unread">{{t $.Lang "失败"}}</span>{{end}}
</td>
<td style="max-width:200px;">{{if .FailReason}}{{.FailReason}}{{else}}—{{end}}</td>
<td style="max-width:320px;word-break:break-all;font-size:13px;color:#555;">{{.Detail}}</td>
@@ -111,16 +111,16 @@
<td>{{if .DurationMs}}{{div .DurationMs 1000}}s{{else}}—{{end}}</td>
</tr>
{{else}}
<tr><td colspan="10" style="text-align:center;color:#7f8c8d;">暂无记录</td></tr>
<tr><td colspan="10" style="text-align:center;color:#7f8c8d;">{{t $.Lang "暂无记录"}}</td></tr>
{{end}}
</tbody>
</table>
{{if gt .totalPages 1}}
<div class="pagination">
{{if gt .page 1}}<a href="/admin/protocol-logs?page={{sub .page 1}}&protocol={{.filter.protocol}}&success={{.filter.success}}&ip={{.filter.ip}}&username={{.filter.username}}&from={{.filter.from}}&to={{.filter.to}}">上一页</a>{{end}}
<span class="current">第 {{.page}} / {{.totalPages}} 页(共 {{.total}} 条)</span>
{{if lt .page .totalPages}}<a href="/admin/protocol-logs?page={{add .page 1}}&protocol={{.filter.protocol}}&success={{.filter.success}}&ip={{.filter.ip}}&username={{.filter.username}}&from={{.filter.from}}&to={{.filter.to}}">下一页</a>{{end}}
{{if gt .page 1}}<a href="/admin/protocol-logs?page={{sub .page 1}}&protocol={{.filter.protocol}}&success={{.filter.success}}&ip={{.filter.ip}}&username={{.filter.username}}&from={{.filter.from}}&to={{.filter.to}}">{{t $.Lang "上一页"}}</a>{{end}}
<span class="current">{{tf $.Lang "第 %d / %d 页(共 %d 条)" .page .totalPages .total}}</span>
{{if lt .page .totalPages}}<a href="/admin/protocol-logs?page={{add .page 1}}&protocol={{.filter.protocol}}&success={{.filter.success}}&ip={{.filter.ip}}&username={{.filter.username}}&from={{.filter.from}}&to={{.filter.to}}">{{t $.Lang "下一页"}}</a>{{end}}
</div>
{{end}}
</div>
+31 -22
View File
@@ -1,10 +1,10 @@
{{define "admin_user_form"}}
<!DOCTYPE html>
<html lang="zh-CN">
<html lang="{{htmlLang .Lang}}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>{{if .isEdit}}编辑用户{{else}}新增用户{{end}} - MailGo</title>
<title>{{if .isEdit}}{{t .Lang "编辑用户"}}{{else}}{{t .Lang "新增用户"}}{{end}} - MailGo</title>
{{template "styles" .}}
</head>
<body>
@@ -12,58 +12,67 @@
<div class="container">
<div class="clearfix">
<div class="sidebar">
<a href="/inbox">返回邮箱</a>
<a href="/admin" {{if eq .activeFolder "admin"}}class="active"{{end}}>控制面板</a>
<a href="/admin/domains" {{if eq .activeFolder "domains"}}class="active"{{end}}>域名管理</a>
<a href="/admin/users" {{if eq .activeFolder "users"}}class="active"{{end}}>用户管理</a>
<a href="/admin/mails" {{if eq .activeFolder "mails"}}class="active"{{end}}>所有邮件</a>
<a href="/admin/outbound" {{if eq .activeFolder "outbound"}}class="active"{{end}}>外发队列</a>
<a href="/admin/protocol-logs" {{if eq .activeFolder "protocol-logs"}}class="active"{{end}}>协议日志</a>
<a href="/admin/connections" {{if eq .activeFolder "connections"}}class="active"{{end}}>当前连接</a>
<a href="/admin/bans" {{if eq .activeFolder "bans"}}class="active"{{end}}>IP黑名单</a>
<a href="/inbox">{{t .Lang "返回邮箱"}}</a>
<a href="/admin" {{if eq .activeFolder "admin"}}class="active"{{end}}>{{t .Lang "控制面板"}}</a>
<a href="/admin/domains" {{if eq .activeFolder "domains"}}class="active"{{end}}>{{t .Lang "域名管理"}}</a>
<a href="/admin/users" {{if eq .activeFolder "users"}}class="active"{{end}}>{{t .Lang "用户管理"}}</a>
<a href="/admin/mails" {{if eq .activeFolder "mails"}}class="active"{{end}}>{{t .Lang "所有邮件"}}</a>
<a href="/admin/outbound" {{if eq .activeFolder "outbound"}}class="active"{{end}}>{{t .Lang "外发队列"}}</a>
<a href="/admin/protocol-logs" {{if eq .activeFolder "protocol-logs"}}class="active"{{end}}>{{t .Lang "协议日志"}}</a>
<a href="/admin/connections" {{if eq .activeFolder "connections"}}class="active"{{end}}>{{t .Lang "当前连接"}}</a>
<a href="/admin/bans" {{if eq .activeFolder "bans"}}class="active"{{end}}>{{t .Lang "IP黑名单"}}</a>
</div>
<div class="content">
<div class="card">
<h2 style="margin-bottom:16px;">{{if .isEdit}}编辑用户{{else}}新增用户{{end}}</h2>
<h2 style="margin-bottom:16px;">{{if .isEdit}}{{t .Lang "编辑用户"}}{{else}}{{t .Lang "新增用户"}}{{end}}</h2>
{{if .error}}<div class="alert alert-error">{{.error}}</div>{{end}}
<form method="POST" action="{{if .isEdit}}/admin/users/{{.user.ID}}{{else}}/admin/users{{end}}">
<div class="form-group">
<label>用户名</label>
<label>{{t .Lang "用户名"}}</label>
<input type="text" name="username" required value="{{.user.Username}}" placeholder="username">
</div>
<div class="form-group">
<label>{{if .isEdit}}新密码(留空则不修改){{else}}密码{{end}}</label>
<input type="password" name="password" {{if not .isEdit}}required{{end}} placeholder="请输入密码">
<label>{{if .isEdit}}{{t .Lang "新密码(留空则不修改)"}}{{else}}{{t .Lang "密码"}}{{end}}</label>
<input type="password" name="password" {{if not .isEdit}}required{{end}} placeholder="{{t .Lang "请输入密码"}}">
</div>
<div class="form-group">
<label>域名</label>
<label>{{t .Lang "界面语言"}}</label>
<select name="language">
<option value="auto" {{if eq .user.Language "auto"}}selected{{end}}>{{t .Lang "自动(跟随浏览器)"}}</option>
<option value="en" {{if eq .user.Language "en"}}selected{{end}}>English</option>
<option value="zh" {{if eq .user.Language "zh"}}selected{{end}}>中文</option>
<option value="ja" {{if eq .user.Language "ja"}}selected{{end}}>日本語</option>
</select>
</div>
<div class="form-group">
<label>{{t .Lang "域名"}}</label>
<select name="domain_id" required>
<option value="">请选择域名</option>
<option value="">{{t .Lang "请选择域名"}}</option>
{{range .domains}}
<option value="{{.ID}}" {{if eq .ID $.user.DomainID}}selected{{end}}>{{.Name}}</option>
{{end}}
</select>
</div>
<div class="form-group">
<label>配额 (GB)</label>
<label>{{t .Lang "配额 (GB)"}}</label>
<input type="number" name="quota_gb" min="1" value="5" placeholder="5">
</div>
<div class="form-group">
<label>
<input type="checkbox" name="is_admin" {{if .user.IsAdmin}}checked{{end}}>
管理员
{{t .Lang "管理员"}}
</label>
</div>
{{if .isEdit}}
<div class="form-group">
<label>
<input type="checkbox" name="is_active" {{if .user.IsActive}}checked{{end}}>
启用账户
{{t .Lang "启用账户"}}
</label>
</div>
{{end}}
<button type="submit" class="btn btn-primary">{{if .isEdit}}保存更改{{else}}创建用户{{end}}</button>
<a href="/admin/users" class="btn" style="margin-left:8px;background:#bdc3c7;color:#fff;">取消</a>
<button type="submit" class="btn btn-primary">{{if .isEdit}}{{t .Lang "保存更改"}}{{else}}{{t .Lang "创建用户"}}{{end}}</button>
<a href="/admin/users" class="btn" style="margin-left:8px;background:#bdc3c7;color:#fff;">{{t .Lang "取消"}}</a>
</form>
</div>
</div>
+27 -27
View File
@@ -1,10 +1,10 @@
{{define "admin_users"}}
<!DOCTYPE html>
<html lang="zh-CN">
<html lang="{{htmlLang .Lang}}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>用户管理 - MailGo</title>
<title>{{t .Lang "用户管理"}} - MailGo</title>
{{template "styles" .}}
</head>
<body>
@@ -12,36 +12,36 @@
<div class="container">
<div class="clearfix">
<div class="sidebar">
<a href="/inbox">返回邮箱</a>
<a href="/admin" {{if eq .activeFolder "admin"}}class="active"{{end}}>控制面板</a>
<a href="/admin/domains" {{if eq .activeFolder "domains"}}class="active"{{end}}>域名管理</a>
<a href="/admin/users" {{if eq .activeFolder "users"}}class="active"{{end}}>用户管理</a>
<a href="/admin/mails" {{if eq .activeFolder "mails"}}class="active"{{end}}>所有邮件</a>
<a href="/admin/outbound" {{if eq .activeFolder "outbound"}}class="active"{{end}}>外发队列</a>
<a href="/admin/protocol-logs" {{if eq .activeFolder "protocol-logs"}}class="active"{{end}}>协议日志</a>
<a href="/admin/connections" {{if eq .activeFolder "connections"}}class="active"{{end}}>当前连接</a>
<a href="/admin/bans" {{if eq .activeFolder "bans"}}class="active"{{end}}>IP黑名单</a>
<a href="/inbox">{{t .Lang "返回邮箱"}}</a>
<a href="/admin" {{if eq .activeFolder "admin"}}class="active"{{end}}>{{t .Lang "控制面板"}}</a>
<a href="/admin/domains" {{if eq .activeFolder "domains"}}class="active"{{end}}>{{t .Lang "域名管理"}}</a>
<a href="/admin/users" {{if eq .activeFolder "users"}}class="active"{{end}}>{{t .Lang "用户管理"}}</a>
<a href="/admin/mails" {{if eq .activeFolder "mails"}}class="active"{{end}}>{{t .Lang "所有邮件"}}</a>
<a href="/admin/outbound" {{if eq .activeFolder "outbound"}}class="active"{{end}}>{{t .Lang "外发队列"}}</a>
<a href="/admin/protocol-logs" {{if eq .activeFolder "protocol-logs"}}class="active"{{end}}>{{t .Lang "协议日志"}}</a>
<a href="/admin/connections" {{if eq .activeFolder "connections"}}class="active"{{end}}>{{t .Lang "当前连接"}}</a>
<a href="/admin/bans" {{if eq .activeFolder "bans"}}class="active"{{end}}>{{t .Lang "IP黑名单"}}</a>
</div>
<div class="content">
<div class="card">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;">
<h2>用户列表</h2>
<a href="/admin/users/new" class="btn btn-primary">新增用户</a>
<h2>{{t .Lang "用户列表"}}</h2>
<a href="/admin/users/new" class="btn btn-primary">{{t .Lang "新增用户"}}</a>
</div>
{{if not .users}}
<p style="color:#7f8c8d;text-align:center;padding:40px 0;">暂无用户</p>
<p style="color:#7f8c8d;text-align:center;padding:40px 0;">{{t $.Lang "暂无用户"}}</p>
{{else}}
<table>
<thead>
<tr>
<th>ID</th>
<th>用户名</th>
<th>域名ID</th>
<th>配额</th>
<th>已用</th>
<th>状态</th>
<th>管理员</th>
<th>操作</th>
<th>{{t $.Lang "用户名"}}</th>
<th>{{t $.Lang "域名ID"}}</th>
<th>{{t $.Lang "配额"}}</th>
<th>{{t $.Lang "已用"}}</th>
<th>{{t $.Lang "状态"}}</th>
<th>{{t $.Lang "管理员"}}</th>
<th>{{t $.Lang "操作"}}</th>
</tr>
</thead>
<tbody>
@@ -55,10 +55,10 @@
<td>{{if .IsActive}}✅{{else}}❌{{end}}</td>
<td>{{if .IsAdmin}}✅{{else}}—{{end}}</td>
<td>
<a href="/admin/users/{{.ID}}/edit" class="btn btn-primary btn-sm">编辑</a>
<a href="/admin/users/{{.ID}}/edit" class="btn btn-primary btn-sm">{{t $.Lang "编辑"}}</a>
<form method="POST" action="/admin/users/{{.ID}}/delete" style="display:inline;"
onsubmit="return confirm('确定要删除用户 {{.Username}} 吗?');">
<button type="submit" class="btn btn-danger btn-sm">删除</button>
onsubmit="return confirm('{{tf $.Lang "确定要删除用户 %s " .Username}}');">
<button type="submit" class="btn btn-danger btn-sm">{{t $.Lang "删除"}}</button>
</form>
</td>
</tr>
@@ -70,11 +70,11 @@
{{if .totalPages}}
<div class="pagination">
{{if gt .page 1}}
<a href="/admin/users?page={{sub .page 1}}">上一页</a>
<a href="/admin/users?page={{sub .page 1}}">{{t $.Lang "上一页"}}</a>
{{end}}
<span>第 {{.page}} / {{.totalPages}}</span>
<span>{{tf $.Lang "第 %d / %d 页" .page .totalPages}}</span>
{{if lt .page .totalPages}}
<a href="/admin/users?page={{add .page 1}}">下一页</a>
<a href="/admin/users?page={{add .page 1}}">{{t $.Lang "下一页"}}</a>
{{end}}
</div>
{{end}}
+15 -15
View File
@@ -1,10 +1,10 @@
{{define "banned"}}
<!DOCTYPE html>
<html lang="zh-CN">
<html lang="{{htmlLang .Lang}}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>访问被禁止 - MailGo</title>
<title>{{t .Lang "访问被禁止"}} - MailGo</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
@@ -43,34 +43,34 @@
<body>
<div class="banned-card">
<div class="banned-icon">🚫</div>
<h1>您的 IP 已被暂时禁止访问</h1>
<p>由于登录失败次数过多,您的 IP 地址已被暂时封禁。</p>
<h1>{{t .Lang "您的 IP 已被暂时禁止访问"}}</h1>
<p>{{t .Lang "由于登录失败次数过多,您的 IP 地址已被暂时封禁。"}}</p>
{{if .entry}}
<div class="detail">
<div class="detail-row">
<span class="detail-label">IP 地址</span>
<span class="detail-label">{{t .Lang "IP 地址"}}</span>
<span class="detail-value">{{.entry.IPAddress}}</span>
</div>
<div class="detail-row">
<span class="detail-label">封禁档位</span>
<span class="detail-value">{{if gt .entry.BanCount 3}}{{sub .entry.BanCount 3}} 次封禁{{else}}—{{end}}</span>
<span class="detail-label">{{t .Lang "封禁档位"}}</span>
<span class="detail-value">{{if gt .entry.BanCount 3}}{{tf .Lang "第 %d 次封禁" (sub .entry.BanCount 3)}}{{else}}—{{end}}</span>
</div>
<div class="detail-row">
<span class="detail-label">原因</span>
<span class="detail-value">{{.entry.Reason}}</span>
<span class="detail-label">{{t .Lang "原因"}}</span>
<span class="detail-value">{{t .Lang .entry.Reason}}</span>
</div>
<div class="detail-row">
<span class="detail-label">失败次数</span>
<span class="detail-value">{{.entry.FailCount}}</span>
<span class="detail-label">{{t .Lang "失败次数"}}</span>
<span class="detail-value">{{tf .Lang "%d 次" .entry.FailCount}}</span>
</div>
<div class="detail-row">
<span class="detail-label">解封时间</span>
<span class="detail-value">{{time12 .entry.ExpiresAt}}</span>
<span class="detail-label">{{t .Lang "解封时间"}}</span>
<span class="detail-value">{{time12 .Lang .entry.ExpiresAt}}</span>
</div>
</div>
{{end}}
<p style="font-size:13px;">请在解封时间后重试,或联系管理员。</p>
<a href="/login" class="back-link">返回登录页</a>
<p style="font-size:13px;">{{t .Lang "请在解封时间后重试,或联系管理员。"}}</p>
<a href="/login" class="back-link">{{t .Lang "返回登录页"}}</a>
</div>
</body>
</html>
+9 -9
View File
@@ -433,22 +433,22 @@
<div class="topbar-left">
<a class="logo" href="/inbox">
<span class="logo-icon"></span>
<span class="logo-text">MailGo<em>邮箱</em></span>
<span class="logo-text">MailGo<em>{{t .Lang "邮箱"}}</em></span>
</a>
</div>
<div class="topbar-search">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
<input id="mail-search" type="text" placeholder="搜索邮件(发件人 / 主题)" autocomplete="off">
<input id="mail-search" type="text" placeholder="{{t .Lang "搜索邮件发件人 / 主题"}}" autocomplete="off">
</div>
<div class="topbar-right">
{{if .currentUser.IsAdmin}}<a class="topbar-link" href="/admin">管理后台</a>{{end}}
<a class="topbar-link" href="/settings">设置</a>
{{if .currentUser.IsAdmin}}<a class="topbar-link" href="/admin">{{t .Lang "管理后台"}}</a>{{end}}
<a class="topbar-link" href="/settings">{{t .Lang "设置"}}</a>
<span class="user-chip">
<span class="avatar avatar-sm" style="{{avatarStyle .currentUser.Username}}">{{initial .currentUser.Username}}</span>
<span class="user-email" title="{{.currentUser.Username}}@{{.currentUser.Domain.Name}}">{{.currentUser.Username}}@{{.currentUser.Domain.Name}}</span>
</span>
<form method="POST" action="/logout" class="logout-form">
<button type="submit" class="icon-btn" title="退出登录">
<button type="submit" class="icon-btn" title="{{t .Lang "退出登录"}}">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>
</button>
</form>
@@ -461,7 +461,7 @@
<aside class="mail-sidebar">
<a href="/compose" class="compose-btn">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
写信
{{t .Lang "写信"}}
</a>
<nav class="folder-nav">
{{range .folders}}
@@ -477,7 +477,7 @@
{{else}}
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
{{end}}
{{folderLabel .Name}}
{{folderLabel $.Lang .Name}}
{{if eq .Name "INBOX"}}
{{if .Unseen}}<span class="badge">{{.Unseen}}</span>{{end}}
{{else if .Total}}<span class="count">{{.Total}}</span>{{end}}
@@ -488,7 +488,7 @@
{{if .currentUser.IsAdmin}}
<a class="folder" href="/admin">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
管理后台
{{t .Lang "管理后台"}}
</a>
{{end}}
</div>
@@ -541,7 +541,7 @@
var ids = rows.filter(function (r) { return r.isConnected && r.querySelector('.row-check').checked; })
.map(function (r) { return r.dataset.id; });
if (!ids.length) return;
if (!confirm('确定要删除选中的 ' + ids.length + ' 封邮件吗?')) return;
if (!confirm('{{t .Lang "确定要删除选中的 %s 封邮件吗?"}}'.replace('%s', ids.length))) return;
var done = 0;
ids.forEach(function (id) {
fetch('/mail/delete/' + id, { method: 'POST', body: new FormData() })
+16 -16
View File
@@ -1,10 +1,10 @@
{{define "compose"}}
<!DOCTYPE html>
<html lang="zh-CN">
<html lang="{{htmlLang .Lang}}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>写信 - MailGo</title>
<title>{{t .Lang "写信"}} - MailGo</title>
<link href="/static/vendor/quill/quill.snow.css" rel="stylesheet">
{{template "styles" .}}
</head>
@@ -16,44 +16,44 @@
<div class="compose-toolbar">
<button type="submit" form="compose-form" class="btn btn-primary">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:4px;"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>
发送
{{t .Lang "发送"}}
</button>
<label class="tb-btn" style="cursor:pointer;">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg>
附件
{{t .Lang "附件"}}
<input type="file" name="attachments" id="attach-input" multiple style="display:none;">
</label>
<a href="/inbox" class="tb-btn">取消</a>
<a href="/inbox" class="tb-btn">{{t .Lang "取消"}}</a>
<div class="toolbar-spacer"></div>
<span class="page-info">撰写新邮件</span>
<span class="page-info">{{t .Lang "撰写新邮件"}}</span>
</div>
{{if .error}}<div class="alert alert-error" style="margin:12px 18px 0;">{{.error}}</div>{{end}}
<form id="compose-form" class="compose-form" method="POST" action="/compose" enctype="multipart/form-data">
<div class="compose-field">
<label>收件人</label>
<input type="email" name="to" required value="{{.to}}" placeholder="输入收件人邮箱地址">
<label>{{t .Lang "收件人"}}</label>
<input type="email" name="to" required value="{{.to}}" placeholder="{{t .Lang "输入收件人邮箱地址"}}">
</div>
<div class="compose-field">
<label>抄送</label>
<input type="text" name="cc" value="{{.cc}}" placeholder="多个地址用逗号分隔(可选)">
<label>{{t .Lang "抄送"}}</label>
<input type="text" name="cc" value="{{.cc}}" placeholder="{{t .Lang "多个地址用逗号分隔可选"}}">
</div>
<div class="compose-field">
<label>主题</label>
<input type="text" name="subject" value="{{.subject}}" placeholder="输入邮件主题">
<label>{{t .Lang "主题"}}</label>
<input type="text" name="subject" value="{{.subject}}" placeholder="{{t .Lang "输入邮件主题"}}">
</div>
<div id="attach-chips" class="attach-chips" style="{{if not .attachments}}display:none;{{end}}"></div>
<div class="editor-wrap">
<div id="editor" data-placeholder="请输入邮件内容..."></div>
<div id="editor" data-placeholder="{{t .Lang "请输入邮件内容..."}}"></div>
<input type="hidden" name="body" id="body-hidden">
<input type="hidden" name="html_body" id="html-body-hidden">
</div>
<div class="compose-footer">
<span>附件配额</span>
<span>{{t .Lang "附件配额"}}</span>
<span class="quota-bar" id="quota-bar" data-used="{{.usedBytes}}" data-quota="{{.quotaBytes}}"><i></i></span>
<span id="quota-text">{{formatBytes .usedBytes}} / {{formatBytes .quotaBytes}}</span>
</div>
@@ -64,7 +64,7 @@
<script>
var quill = new Quill('#editor', {
theme: 'snow',
placeholder: document.getElementById('editor').dataset.placeholder || '请输入邮件内容...',
placeholder: document.getElementById('editor').dataset.placeholder || '{{t .Lang "请输入邮件内容..."}}',
modules: {
toolbar: [
[{ 'header': [1, 2, 3, false] }],
@@ -96,7 +96,7 @@
var chip = document.createElement('span');
chip.className = 'attach-chip';
chip.innerHTML = '📎 ' + f.name + ' (' + (f.size / 1024).toFixed(1) + ' KB)' +
'<button type="button" class="chip-del" data-i="' + i + '" title="移除">✕</button>';
'<button type="button" class="chip-del" data-i="' + i + '" title="{{t .Lang "移除"}}">✕</button>';
chipsBox.appendChild(chip);
});
}
+25 -25
View File
@@ -1,10 +1,10 @@
{{define "folder"}}
<!DOCTYPE html>
<html lang="zh-CN">
<html lang="{{htmlLang .Lang}}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>{{folderLabel .folder}} - MailGo</title>
<title>{{folderLabel .Lang .folder}} - MailGo</title>
{{template "styles" .}}
</head>
<body class="page-list">
@@ -13,31 +13,31 @@
{{template "sidebar" .}}
<main class="mail-main">
<div class="list-toolbar">
<label class="check-all" title="全选/取消全选">
<label class="check-all" title="{{t .Lang "全选/取消全选"}}">
<input type="checkbox" id="select-all">
全选
{{t .Lang "全选"}}
</label>
<button type="button" class="tb-btn" id="btn-refresh" title="刷新">
<button type="button" class="tb-btn" id="btn-refresh" title="{{t .Lang "刷新"}}">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>
刷新
{{t .Lang "刷新"}}
</button>
<button type="button" class="tb-btn danger" id="btn-delete" disabled>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
{{if .isTrash}}彻底删除{{else}}删除{{end}}
{{if .isTrash}}{{t $.Lang "彻底删除"}}{{else}}{{t $.Lang "删除"}}{{end}}
</button>
{{if .isTrash}}
<form method="POST" action="/folder/Trash/empty" style="display:inline;"
onsubmit="return confirm('确定要清空「已删除」文件夹吗?此操作不可恢复。');">
<button type="submit" class="tb-btn danger">清空文件夹</button>
onsubmit="return confirm('{{t $.Lang "确定要清空已删除文件夹吗此操作不可恢复"}}');">
<button type="submit" class="tb-btn danger">{{t $.Lang "清空文件夹"}}</button>
</form>
{{end}}
<div class="toolbar-spacer"></div>
<span class="page-info">{{.total}}</span>
<span class="page-info">{{tf .Lang "共 %d 封" .total}}</span>
</div>
{{if not .messages}}
<div class="mail-list">
<div class="empty-tip"><span class="empty-icon">{{if .isTrash}}🗑{{else}}📭{{end}}</span>{{folderLabel .folder}}暂无邮件</div>
<div class="empty-tip"><span class="empty-icon">{{if .isTrash}}🗑{{else}}📭{{end}}</span>{{folderLabel $.Lang $.folder}} · {{t $.Lang "暂无邮件"}}</div>
</div>
{{else}}
<ul class="mail-list">
@@ -53,9 +53,9 @@
<span class="avatar" style="{{avatarStyle .FromAddr}}">{{initial (mailName (decodeHeader .FromAddr))}}</span>
{{end}}
</span>
<span class="cell-from" {{if or (eq $.folder "Sent") (eq $.folder "Drafts")}}title="收件人:{{.ToAddr}}"{{else}}title="{{decodeHeader .FromAddr}}"{{end}}>
<span class="cell-from" {{if or (eq $.folder "Sent") (eq $.folder "Drafts")}}title="{{tf $.Lang "收件人%s" .ToAddr}}"{{else}}title="{{decodeHeader .FromAddr}}"{{end}}>
{{if or (eq $.folder "Sent") (eq $.folder "Drafts")}}
{{if eq $.folder "Drafts"}}致:{{end}}{{mailName .ToAddr}}
{{if eq $.folder "Drafts"}}{{t $.Lang "致:"}}{{end}}{{mailName .ToAddr}}
{{else}}
{{mailName (decodeHeader .FromAddr)}}
{{end}}
@@ -63,24 +63,24 @@
<span class="cell-subject-wrap">
{{if not .IsRead}}<span class="unread-dot"></span>{{end}}
<a class="cell-subject" href="/folder/{{urlPath $.folder}}/{{.ID}}">
{{if .Subject}}{{.Subject}}{{else}}(无主题){{end}}
{{if .Subject}}{{.Subject}}{{else}}{{t $.Lang "(无主题)"}}{{end}}
</a>
</span>
<span class="cell-snippet">
{{if .TextBody}}{{truncate .TextBody 80}}{{else if .HtmlBody}}[HTML 邮件]{{end}}
{{if .TextBody}}{{truncate .TextBody 80}}{{else if .HtmlBody}}{{t $.Lang "[HTML 邮件]"}}{{end}}
</span>
<span class="cell-date">{{shortDate .Date}}</span>
<span class="cell-date">{{shortDate $.Lang .Date}}</span>
{{if $.isTrash}}
<form method="POST" action="/mail/restore/{{.ID}}" class="row-del"
onsubmit="event.stopPropagation(); return confirm('确定要恢复这封邮件吗?');">
<button type="submit" class="icon-btn" title="恢复到收件箱">
onsubmit="event.stopPropagation(); return confirm('{{t $.Lang "确定要恢复这封邮件吗"}}');">
<button type="submit" class="icon-btn" title="{{t $.Lang "恢复到收件箱"}}">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg>
</button>
</form>
{{else}}
<form method="POST" action="/mail/delete/{{.ID}}" class="row-del"
onsubmit="event.stopPropagation(); return confirm('确定要删除这封邮件吗?');">
<button type="submit" class="icon-btn" title="删除">
onsubmit="event.stopPropagation(); return confirm('{{t $.Lang "确定要删除这封邮件吗"}}');">
<button type="submit" class="icon-btn" title="{{t $.Lang "删除"}}">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
</button>
</form>
@@ -91,23 +91,23 @@
{{end}}
<div class="list-footer">
<span class="page-num">第 {{.page}} / {{if .totalPages}}{{.totalPages}}{{else}}1{{end}} 页</span>
<span class="page-num">{{tf .Lang "第 %d / %d 页" .page (or .totalPages 1)}}</span>
<div class="pager">
{{if gt .page 1}}
<a class="page-btn" href="/folder/{{urlPath .folder}}?page={{sub .page 1}}">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg>
上一页
{{t $.Lang "上一页"}}
</a>
{{else}}
<span class="page-btn disabled">上一页</span>
<span class="page-btn disabled">{{t $.Lang "上一页"}}</span>
{{end}}
{{if lt .page .totalPages}}
<a class="page-btn" href="/folder/{{urlPath .folder}}?page={{add .page 1}}">
下一页
{{t $.Lang "下一页"}}
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>
</a>
{{else}}
<span class="page-btn disabled">下一页</span>
<span class="page-btn disabled">{{t $.Lang "下一页"}}</span>
{{end}}
</div>
</div>
+15 -15
View File
@@ -1,55 +1,55 @@
{{define "login"}}
<!DOCTYPE html>
<html lang="zh-CN">
<html lang="{{htmlLang .Lang}}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>登录 - MailGo 邮箱</title>
<title>{{t .Lang "登录"}} - MailGo {{t .Lang "邮箱"}}</title>
{{template "styles" .}}
</head>
<body class="login-page">
<div class="login-card">
<div class="login-logo">
<span class="logo-icon" style="width:44px;height:44px;font-size:22px;border-radius:12px;"></span>
<span style="font-size:26px;font-weight:700;color:var(--primary);">MailGo<em style="font-style:normal;font-weight:400;font-size:15px;color:var(--text-2);margin-left:4px;">邮箱</em></span>
<span style="font-size:26px;font-weight:700;color:var(--primary);">MailGo<em style="font-style:normal;font-weight:400;font-size:15px;color:var(--text-2);margin-left:4px;">{{t .Lang "邮箱"}}</em></span>
</div>
<div class="login-sub">登录您的邮箱账户</div>
<div class="login-sub">{{t .Lang "登录您的邮箱账户"}}</div>
{{if .error}}<div class="alert alert-error">{{.error}}</div>{{end}}
<form method="POST" action="/login">
<div class="form-group">
<label>邮箱地址</label>
<label>{{t .Lang "邮箱地址"}}</label>
<input type="email" name="email" required autofocus placeholder="user@example.com" style="padding:11px 14px;border-radius:8px;">
</div>
<div class="form-group">
<label>密码</label>
<input type="password" name="password" required placeholder="请输入密码" style="padding:11px 14px;border-radius:8px;">
<label>{{t .Lang "密码"}}</label>
<input type="password" name="password" required placeholder="{{t .Lang "请输入密码"}}" style="padding:11px 14px;border-radius:8px;">
</div>
<button type="submit" class="btn btn-primary" style="width:100%;padding:11px 16px;font-size:15px;border-radius:8px;">登 录</button>
<button type="submit" class="btn btn-primary" style="width:100%;padding:11px 16px;font-size:15px;border-radius:8px;">{{t .Lang "登 录"}}</button>
</form>
{{if or .oauth2Enabled .ldapEnabled}}
<div class="divider"></div>
<div class="divider">{{t .Lang "或"}}</div>
{{end}}
{{if .ldapEnabled}}
<form method="POST" action="/login/ldap" style="margin-bottom:10px;">
<div class="form-group">
<label>LDAP 用户名</label>
<input type="text" name="username" placeholder="LDAP 用户名">
<label>{{t .Lang "LDAP 用户名"}}</label>
<input type="text" name="username" placeholder="{{t .Lang "LDAP 用户名"}}">
</div>
<div class="form-group">
<label>LDAP 密码</label>
<input type="password" name="password" placeholder="LDAP 密码">
<label>{{t .Lang "LDAP 密码"}}</label>
<input type="password" name="password" placeholder="{{t .Lang "LDAP 密码"}}">
</div>
<button type="submit" class="btn" style="width:100%;background:#7b5cd6;color:#fff;">LDAP 登录</button>
<button type="submit" class="btn" style="width:100%;background:#7b5cd6;color:#fff;">{{t .Lang "LDAP 登录"}}</button>
</form>
{{end}}
{{if .oauth2Enabled}}
<a href="/auth/oauth2" class="btn btn-primary" style="width:100%;text-align:center;display:block;margin-top:4px;">
{{if eq .oauth2Provider "google"}}Google{{else if eq .oauth2Provider "github"}}GitHub{{else}}OAuth2{{end}} 登录
{{if eq .oauth2Provider "google"}}Google{{else if eq .oauth2Provider "github"}}GitHub{{else}}OAuth2{{end}} {{t .Lang "登录"}}
</a>
{{end}}
</div>
+38 -22
View File
@@ -1,10 +1,10 @@
{{define "settings"}}
<!DOCTYPE html>
<html lang="zh-CN">
<html lang="{{htmlLang .Lang}}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>设置 - MailGo</title>
<title>{{t .Lang "设置"}} - MailGo</title>
{{template "styles" .}}
</head>
<body class="page-settings">
@@ -15,21 +15,21 @@
{{if .error}}<div class="alert alert-error">{{.error}}</div>{{end}}
{{if .success}}<div class="alert alert-success">{{.success}}</div>{{end}}
{{if .mustChange}}<div class="alert" style="border:1px solid #ffa940;background:#fff7e6;color:#d46b08;border-radius:8px;padding:12px 16px;margin-bottom:16px;font-size:13.5px;">
⚠️ 首次登录/密码已被重置,请立即修改密码后再继续使用邮箱功能。
{{t .Lang "⚠️ 首次登录/密码已被重置,请立即修改密码后再继续使用邮箱功能。"}}
</div>{{end}}
<div class="card" style="max-width:720px;">
<h2 style="font-size:17px;margin-bottom:18px;display:flex;align-items:center;gap:10px;">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#1677ff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
账号信息
{{t .Lang "账号信息"}}
</h2>
<div style="display:flex;align-items:center;gap:14px;margin-bottom:20px;">
<span class="avatar" style="width:52px;height:52px;font-size:22px;{{avatarStyle .currentUser.Username}}">{{initial .currentUser.Username}}</span>
<div>
<div style="font-size:16px;font-weight:600;">{{.currentUser.Username}}@{{.currentUser.Domain.Name}}</div>
<div style="color:var(--text-3);font-size:12.5px;margin-top:3px;">
已用 {{formatBytes .currentUser.UsedBytes}} / 配额 {{formatBytes .currentUser.QuotaBytes}}
{{if .currentUser.IsAdmin}} · 管理员{{end}}
{{tf .Lang "已用 %s / 配额 %s" (formatBytes .currentUser.UsedBytes) (formatBytes .currentUser.QuotaBytes)}}
{{if .currentUser.IsAdmin}} · {{t .Lang "管理员"}}{{end}}
</div>
</div>
</div>
@@ -37,30 +37,46 @@
</div>
<div class="card" style="max-width:720px;">
<h2 style="font-size:17px;margin-bottom:18px;">修改密码</h2>
<h2 style="font-size:17px;margin-bottom:18px;">{{t .Lang "界面语言"}}</h2>
<form method="POST" action="/settings" style="max-width:420px;">
<div class="form-group">
<label>当前密码</label>
<input type="password" name="old_password" required placeholder="请输入当前密码" autocomplete="current-password">
<label>{{t .Lang "界面语言"}}</label>
<select name="language">
<option value="auto" {{if eq .currentUser.Language "auto"}}selected{{end}}>{{t .Lang "自动(跟随浏览器)"}}</option>
<option value="en" {{if eq .currentUser.Language "en"}}selected{{end}}>English</option>
<option value="zh" {{if eq .currentUser.Language "zh"}}selected{{end}}>中文</option>
<option value="ja" {{if eq .currentUser.Language "ja"}}selected{{end}}>日本語</option>
</select>
</div>
<div class="form-group">
<label>新密码</label>
<input type="password" name="new_password" required placeholder="请输入新密码" autocomplete="new-password">
</div>
<div class="form-group">
<label>确认新密码</label>
<input type="password" name="confirm_password" required placeholder="请再次输入新密码" autocomplete="new-password">
</div>
<button type="submit" class="btn btn-primary">修改密码</button>
<button type="submit" class="btn btn-primary">{{t .Lang "保存"}}</button>
</form>
</div>
<div class="card" style="max-width:720px;">
<h2 style="font-size:17px;margin-bottom:10px;">帮助</h2>
<h2 style="font-size:17px;margin-bottom:18px;">{{t .Lang "修改密码"}}</h2>
<form method="POST" action="/settings" style="max-width:420px;">
<div class="form-group">
<label>{{t .Lang "当前密码"}}</label>
<input type="password" name="old_password" required placeholder="{{t .Lang "请输入当前密码"}}" autocomplete="current-password">
</div>
<div class="form-group">
<label>{{t .Lang "新密码"}}</label>
<input type="password" name="new_password" required placeholder="{{t .Lang "请输入新密码"}}" autocomplete="new-password">
</div>
<div class="form-group">
<label>{{t .Lang "确认新密码"}}</label>
<input type="password" name="confirm_password" required placeholder="{{t .Lang "请再次输入新密码"}}" autocomplete="new-password">
</div>
<button type="submit" class="btn btn-primary">{{t .Lang "修改密码"}}</button>
</form>
</div>
<div class="card" style="max-width:720px;">
<h2 style="font-size:17px;margin-bottom:10px;">{{t .Lang "帮助"}}</h2>
<p style="color:var(--text-2);font-size:13.5px;line-height:1.9;">
客户端收发信(IMAP / SMTP)配置:<br>
IMAP 服务器:{{.currentUser.Domain.Name}} 端口 143 / SSL 993<br>
SMTP 服务器:{{.currentUser.Domain.Name}} 端口 587(提交)/ SSL 465
{{t .Lang "客户端收发信(IMAP / SMTP)配置:"}}<br>
{{tf .Lang "IMAP 服务器:%s 端口 %d / SSL %d" .currentUser.Domain.Name 143 993}}<br>
{{tf .Lang "SMTP 服务器:%s 端口 %d(提交)/ SSL %d" .currentUser.Domain.Name 587 465}}
</p>
</div>
</main>
+18 -18
View File
@@ -1,10 +1,10 @@
{{define "view"}}
<!DOCTYPE html>
<html lang="zh-CN">
<html lang="{{htmlLang .Lang}}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>查看邮件 - MailGo</title>
<title>{{t .Lang "查看邮件"}} - MailGo</title>
{{template "styles" .}}
</head>
<body class="page-view">
@@ -15,50 +15,50 @@
<div class="view-toolbar">
<a href="javascript:history.back()" class="tb-btn">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg>
返回
{{t .Lang "返回"}}
</a>
<a href="/compose?to={{mailEmail .message.FromAddr}}&subject={{if .message.Subject}}Re: {{.message.Subject}}{{end}}" class="tb-btn">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 17 4 12 9 7"/><path d="M20 18v-2a4 4 0 0 0-4-4H4"/></svg>
回复
{{t .Lang "回复"}}
</a>
{{if .inTrash}}
<form method="POST" action="/mail/restore/{{.message.ID}}" style="display:inline;">
<button type="submit" class="tb-btn">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg>
恢复
{{t .Lang "恢复"}}
</button>
</form>
<form method="POST" action="/mail/purge/{{.message.ID}}" style="display:inline;"
onsubmit="return confirm('确定要彻底删除这封邮件吗此操作不可恢复。');">
onsubmit="return confirm('{{t .Lang "确定要彻底删除这封邮件吗此操作不可恢复"}}');">
<button type="submit" class="tb-btn danger">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
彻底删除
{{t .Lang "彻底删除"}}
</button>
</form>
{{else}}
<form method="POST" action="/mail/delete/{{.message.ID}}" style="display:inline;"
onsubmit="return confirm('确定要删除这封邮件吗?');">
onsubmit="return confirm('{{t .Lang "确定要删除这封邮件吗"}}');">
<button type="submit" class="tb-btn danger">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
删除
{{t .Lang "删除"}}
</button>
</form>
{{end}}
</div>
<div class="mail-head">
<h1 class="mail-title">{{if .message.Subject}}{{.message.Subject}}{{else}}(无主题){{end}}</h1>
<h1 class="mail-title">{{if .message.Subject}}{{.message.Subject}}{{else}}{{t .Lang "(无主题)"}}{{end}}</h1>
<div class="mail-from-row">
<span class="avatar" style="{{avatarStyle .message.FromAddr}}">{{initial (mailName (decodeHeader .message.FromAddr))}}</span>
<div>
<div class="mail-from-name">{{mailName (decodeHeader .message.FromAddr)}}</div>
<div class="mail-from-addr" title="{{decodeHeader .message.FromAddr}}">{{mailEmail .message.FromAddr}}</div>
</div>
<span class="mail-date">{{time12 .message.Date}}</span>
<span class="mail-date">{{time12 .Lang .message.Date}}</span>
</div>
{{if .message.CcAddr}}
<div class="mail-from-addr" style="margin:-8px 0 16px 48px;">
抄送:{{.message.CcAddr}}
{{t .Lang "抄送:"}}{{.message.CcAddr}}
</div>
{{end}}
</div>
@@ -88,28 +88,28 @@
<div class="view-actions">
<a href="/compose?to={{mailEmail .message.FromAddr}}&subject={{if .message.Subject}}Re: {{.message.Subject}}{{end}}" class="btn btn-primary">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:4px;"><polyline points="9 17 4 12 9 7"/><path d="M20 18v-2a4 4 0 0 0-4-4H4"/></svg>
回复
{{t .Lang "回复"}}
</a>
{{if .inTrash}}
<form method="POST" action="/mail/restore/{{.message.ID}}" style="display:inline;">
<button type="submit" class="btn">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:4px;"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg>
恢复到收件箱
{{t .Lang "恢复到收件箱"}}
</button>
</form>
<form method="POST" action="/mail/purge/{{.message.ID}}" style="display:inline;"
onsubmit="return confirm('确定要彻底删除这封邮件吗此操作不可恢复。');">
onsubmit="return confirm('{{t .Lang "确定要彻底删除这封邮件吗此操作不可恢复"}}');">
<button type="submit" class="btn btn-danger">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:4px;"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
彻底删除
{{t .Lang "彻底删除"}}
</button>
</form>
{{else}}
<form method="POST" action="/mail/delete/{{.message.ID}}" style="display:inline;"
onsubmit="return confirm('确定要删除这封邮件吗?');">
onsubmit="return confirm('{{t .Lang "确定要删除这封邮件吗"}}');">
<button type="submit" class="btn btn-danger">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:4px;"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
删除邮件
{{t .Lang "删除邮件"}}
</button>
</form>
{{end}}