Files
tamabox/internal/mail/mail.go
T
2026-10-19 03:01:17 +08:00

214 lines
8.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright 2022 E99p1ant. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package mail
import (
"bytes"
"crypto/tls"
"embed"
"fmt"
"html/template"
"strings"
"time"
"github.com/pkg/errors"
"gopkg.in/gomail.v2"
"github.com/tamakyi/TamaBox/internal/conf"
"github.com/tamakyi/TamaBox/templates"
)
func SendNewQuestionMail(email, domain string, questionID uint, questionContent string, token string) error {
params := map[string]string{
"link": fmt.Sprintf("%s/_/%s/%d?t=%s", conf.App.ExternalURL, domain, questionID, token),
"question": questionContent,
}
return sendTemplateMail(email, "【狼的提问箱】您有一个新的提问", templates.FS, "mail/new-question.html", params)
}
func SendNewQuestionMailToUser(email, domain string, questionID uint, questionContent string, token string) error {
params := map[string]string{
"link": fmt.Sprintf("%s/_/%s/%d?t=%s", conf.App.ExternalURL, domain, questionID, token),
"question": questionContent,
}
return sendTemplateMail(email, "【狼的提问箱】您发送了一个新的提问", templates.FS, "mail/new-question-to-user.html", params)
}
func SendNewAnswerMail(email, domain string, questionID uint, question, answer string, token string) error {
params := map[string]string{
"link": fmt.Sprintf("%s/_/%s/%d?t=%s", conf.App.ExternalURL, domain, questionID, token),
"question": question,
"answer": answer,
}
return sendTemplateMail(email, "【狼的提问箱】您的提问有了回复", templates.FS, "mail/new-answer.html", params)
}
func SendPasswordRecoveryMail(email, code string) error {
params := map[string]string{
"link": fmt.Sprintf("%s/recover-password?code=%s", conf.App.ExternalURL, code),
"email": email,
}
return sendTemplateMail(email, "【狼的提问箱】账号密码找回", templates.FS, "mail/password-recovery.html", params)
}
// SendNewFollowUpMail 通知提问箱主人:有人对某条已回答的提问进行了追问。
// 链接带 token,与 SendNewQuestionMail 对齐——主人于未登录设备(手机邮件客户端)点开私密提问时
// 也能凭 token 查看,否则会被 Questioner 中间件重定向到首页。
func SendNewFollowUpMail(email, domain string, questionID uint, questionContent string, followUpContent string, token string) error {
params := map[string]string{
"link": fmt.Sprintf("%s/_/%s/%d?t=%s", conf.App.ExternalURL, domain, questionID, token),
"question": questionContent,
"followup": followUpContent,
}
return sendTemplateMail(email, "【狼的提问箱】您的提问有了追问", templates.FS, "mail/new-follow-up.html", params)
}
// SendNewConversationMail 通知有新私聊发起(使用 token 而非 conversationID)
func SendNewConversationMail(email, askerName, domain string, token string, firstMessage string) error {
params := map[string]string{
"link": fmt.Sprintf("%s/_/%s/chat/%s", conf.App.ExternalURL, domain, token),
"asker": askerName,
"content": firstMessage,
}
return sendTemplateMail(email, "【狼的提问箱】有人发起了私聊", templates.FS, "mail/new-conversation.html", params)
}
// SendNewMessageMail 通知私聊中有新消息(使用 token 而非 conversationID)
func SendNewMessageMail(email, senderName, domain string, token string, content string) error {
params := map[string]string{
"link": fmt.Sprintf("%s/_/%s/chat/%s", conf.App.ExternalURL, domain, token),
"sender": senderName,
"content": content,
}
return sendTemplateMail(email, "【狼的提问箱】私聊新消息", templates.FS, "mail/new-message.html", params)
}
// SendNewVoteSubmissionMail 通知箱主:表单投票收到新回复(链接指向管理后台结果页)。
func SendNewVoteSubmissionMail(email, voteTitle string, voteID uint) error {
params := map[string]string{
"link": fmt.Sprintf("%s/user/votes/%d/results", conf.App.ExternalURL, voteID),
"title": voteTitle,
}
return sendTemplateMail(email, "【狼的提问箱】您的投票收到了新回复", templates.FS, "mail/new-vote.html", params)
}
// SendSecurityAlertMail 发送安全告警:检测到疑似批量扫描/探测并触发自动封禁时通知管理员。
// provider 为来源 IP 的网络归属标签(见 internal/security/ipinfo),proxied 表示
// IP 属于 CDN/WAF/代理网络(此时封禁出口 IP 可能无法拦截真实攻击者,邮件中额外提示)。
func SendSecurityAlertMail(ip, provider string, proxied bool, samplePaths []string, durationMinutes int) error {
proxyHint := ""
if proxied {
proxyHint = "1"
}
params := map[string]string{
"ip": ip,
"provider": provider,
"proxy_hint": proxyHint,
"paths": strings.Join(samplePaths, "、"),
"duration": fmt.Sprintf("%d", durationMinutes),
"time": time.Now().Format("2006-01-02 15:04:05"),
}
return sendTemplateMail(conf.Security.SecurityAlertEmail, "【狼的提问箱】检测到疑似批量扫描,已自动封禁 IP", templates.FS, "mail/security-alert.html", params)
}
// SendAbuseAlertMail 发送「某箱主收到集中举报」的阈值告警。
//
// 与 SendSecurityAlertMail 共用 conf.Security.SecurityAlertEmail 收件人,不新增配置项。
// 邮件只给计数与下一步命令,不贴举报内容原文:邮件可能被转发/归档,贴原文等于把挂人内容
// 又复制了一份出去;要看内容请用 reports --domain。
func SendAbuseAlertMail(domain, name string, count24, count7d int64) error {
params := map[string]string{
"domain": domain,
"name": name,
"count24": fmt.Sprintf("%d", count24),
"count7d": fmt.Sprintf("%d", count7d),
"time": time.Now().Format("2006-01-02 15:04:05"),
}
return sendTemplateMail(conf.Security.SecurityAlertEmail, "【狼的提问箱】某箱主收到集中举报", templates.FS, "mail/abuse-alert.html", params)
}
// SendQuestionReportedMail 通知箱主「你的某条内容被多人举报」。
//
// 与给运维的告警不同:这封是给**箱主**的自查提醒,语气必须中性 ——
// 明确写出「不代表平台已判定违规」,避免变成变相的处罚通知。
func SendQuestionReportedMail(email, domain string, questionID uint, count int64, link string) error {
params := map[string]string{
"domain": domain,
"question_id": fmt.Sprintf("%d", questionID),
"count": fmt.Sprintf("%d", count),
"link": link,
"time": time.Now().Format("2006-01-02 15:04:05"),
}
return sendTemplateMail(email, "【狼的提问箱】你的内容被举报了", templates.FS, "mail/question-reported.html", params)
}
// RestrictionNotice 处置通知邮件的内容。
//
// 全部文案由调用方按「箱主自己的界面语言」预先翻译好再传进来:
// 邮件模板本身是单语言的,而箱主的语言存在 users.language 里,只能在调用侧解析。
//
// 刻意不包含 --reason 原文:那是运维的审计备注(可能写得很直白),
// 对外只用「级别 + 到期时间 + 规则链接」,与公告模板的口径一致。
type RestrictionNotice struct {
To string
Heading string
Greeting string
Body string
Note string
RulesText string
RulesURL string
Appeal string
}
// SendRestrictionNoticeMail 告知箱主「你的提问箱已被限制」。
func SendRestrictionNoticeMail(n RestrictionNotice) error {
params := map[string]string{
"heading": n.Heading,
// time 由邮件包统一注入:调用方(CLI / 未来可能的自动流程)不必各自拼时间,
// 也保证与其他通知邮件(question-reported / abuse-alert)的头部格式一致。
"time": time.Now().Format("2006-01-02 15:04:05"),
"greeting": n.Greeting,
"body": n.Body,
"note": n.Note,
"rulesText": n.RulesText,
"rulesURL": n.RulesURL,
"appeal": n.Appeal,
}
return sendTemplateMail(n.To, n.Heading, templates.FS, "mail/restriction-notice.html", params)
}
func sendTemplateMail(email, title string, templateFS embed.FS, templatePath string, params map[string]string) error {
var content bytes.Buffer
t, err := template.ParseFS(templateFS, templatePath)
if err != nil {
return errors.Wrap(err, "parse template file")
}
params["year"] = time.Now().Format("2006")
if err := t.Execute(&content, params); err != nil {
return errors.Wrap(err, "execute template")
}
return sendMail(email, title, content.String())
}
func sendMail(to, title, content string) error {
m := gomail.NewMessage()
m.SetHeader("From", fmt.Sprintf("TamaBox <%s>", conf.Mail.Account))
m.SetHeader("To", to)
m.SetHeader("Subject", title)
m.SetBody("text/html", content)
d := gomail.NewDialer(
conf.Mail.SMTP,
conf.Mail.Port,
conf.Mail.Account,
conf.Mail.Password,
)
// 默认校验证书;仅当配置了 skip_tls_verify=true(自签名/内网 SMTP)时跳过。
if conf.Mail.SkipTLSVerify {
d.TLSConfig = &tls.Config{InsecureSkipVerify: true}
}
return d.DialAndSend(m)
}