Files
mailgo/internal/web/templates/view.html
kevin 8ea4a623a9 fix(security): 修复 P3 低危项(开放重定向/配额TOCTOU/safeJS/会话治理)
- Referer 开放重定向:safeRedirectPath 仅放行同站相对路径,
  外部 URL/协议跳转一律回退 /inbox
- 发信配额 TOCTOU:新增 TryReserveQuota 原子预扣
  (UPDATE ... WHERE used_bytes + n <= quota_bytes),超配额即拒发;
  附件保存失败按大小补偿回退
- 移除危险模板函数 safeHTML/safeJS:新增 jsonify(json.Marshal,
  < > & 转义为 \u003c 等,无法逃出 </script>),compose 页
  quill.innerHTML 改用 jsonify;srcdoc 改回默认属性转义
- 会话治理:登录成功后 session.Clear() 清旧状态;记录 loginAt,
  绝对过期 7 天 + 滑动续期(活跃会话 12h 写回刷新)
- 确认 #15 Content-Disposition 编码随 P1 #4 已完成
- 新增 12 个测试:重定向路径矩阵、配额原子性(含超额不部分扣费)、
  jsonify 逃逸防护、会话绝对过期/有效访问(签名会话构造)

至此 16 项安全审计项(P0-P3)全部修复完成。
2026-08-19 16:56:23 +08:00

90 lines
5.3 KiB
HTML

{{define "view"}}
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>查看邮件 - MailGo</title>
{{template "styles" .}}
</head>
<body class="page-view">
{{template "navbar" .}}
<div class="app-body">
{{template "sidebar" .}}
<main class="mail-main">
<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>
返回
</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>
回复
</a>
<form method="POST" action="/mail/delete/{{.message.ID}}" style="display:inline;"
onsubmit="return confirm('确定要删除这封邮件吗?');">
<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>
删除
</button>
</form>
</div>
<div class="mail-head">
<h1 class="mail-title">{{if .message.Subject}}{{.message.Subject}}{{else}}(无主题){{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">{{.message.Date.Format "2006-01-02 15:04:05"}}</span>
</div>
{{if .message.CcAddr}}
<div class="mail-from-addr" style="margin:-8px 0 16px 48px;">
抄送:{{.message.CcAddr}}
</div>
{{end}}
</div>
<div class="mail-body-wrap">
<div class="mail-body">
{{if .message.HtmlBody}}
<iframe class="mail-body-iframe" srcdoc="{{.message.HtmlBody}}" sandbox="allow-same-origin" onload="this.style.height=this.contentDocument.body.scrollHeight+20+'px'"></iframe>
{{else}}
<pre>{{.message.TextBody}}</pre>
{{end}}
</div>
{{if .attachments}}
<div class="attachment-list">
{{range .attachments}}
<span class="attachment-item">
<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>
<a href="/attachment/{{.ID}}">{{.FileName}}</a>
<span style="color:var(--text-3);font-size:12px;">({{formatBytes .FileSize}})</span>
</span>
{{end}}
</div>
{{end}}
</div>
<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>
回复
</a>
<form method="POST" action="/mail/delete/{{.message.ID}}" style="display:inline;"
onsubmit="return confirm('确定要删除这封邮件吗?');">
<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>
删除邮件
</button>
</form>
</div>
</main>
</div>
</body>
</html>
{{end}}