2026-08-02 07:37:07 +08:00
package app
import (
"context"
"database/sql"
"errors"
2026-08-02 09:08:20 +08:00
"fmt"
2026-08-02 07:37:07 +08:00
"net/http"
2026-08-02 09:08:20 +08:00
"net/url"
2026-08-02 07:37:07 +08:00
"strings"
"time"
"github.com/go-chi/chi/v5"
)
2026-08-02 09:08:20 +08:00
const forwardingVerificationTTL = 24 * time . Hour
2026-08-02 07:37:07 +08:00
type ForwardingVerifiedEmail struct {
2026-08-02 09:08:20 +08:00
ID string `json:"id"`
Email string `json:"email"`
Verified bool `json:"verified"`
CreatedAt time . Time `json:"createdAt"`
VerifiedAt * time . Time `json:"verifiedAt,omitempty"`
VerificationSentAt * time . Time `json:"verificationSentAt,omitempty"`
VerificationExpiresAt * time . Time `json:"verificationExpiresAt,omitempty"`
DeliveryStatus string `json:"deliveryStatus,omitempty"`
DeliveryError string `json:"deliveryError,omitempty"`
2026-08-02 07:37:07 +08:00
}
type MailboxForwardingRule struct {
2026-08-02 18:33:25 +08:00
MailboxID string `json:"mailboxId"`
TargetEmail string `json:"targetEmail"`
TargetEmails [] string `json:"targetEmails"`
2026-08-02 07:37:07 +08:00
}
type ForwardingSettings struct {
VerifiedEmails [] ForwardingVerifiedEmail `json:"verifiedEmails"`
AccountTargetEmail string `json:"accountTargetEmail"`
2026-08-02 18:33:25 +08:00
AccountTargetEmails [] string `json:"accountTargetEmails"`
2026-08-02 07:37:07 +08:00
MailboxRules [] MailboxForwardingRule `json:"mailboxRules"`
}
func ( a * App ) handleForwardingSettings ( w http . ResponseWriter , r * http . Request ) {
user := currentUser ( r )
settings , err := a . forwardingSettings ( r . Context (), user . ID )
if err != nil {
respondError ( w , http . StatusInternalServerError , "failed to load forwarding settings" )
return
}
respondJSON ( w , http . StatusOK , settings )
}
func ( a * App ) handleAddForwardingVerifiedEmail ( w http . ResponseWriter , r * http . Request ) {
user := currentUser ( r )
var req struct {
Email string `json:"email"`
}
if err := decodeJSON ( r , & req ); err != nil {
badRequest ( w , err )
return
}
2026-08-02 09:08:20 +08:00
email , ok := a . cleanForwardingVerificationEmail ( w , r , user . ID , req . Email )
if ! ok {
2026-08-02 07:37:07 +08:00
return
}
2026-08-02 09:08:20 +08:00
id , verified , err := a . forwardingVerifiedEmailState ( r . Context (), user . ID , email )
if err != nil && ! errors . Is ( err , sql . ErrNoRows ) {
respondError ( w , http . StatusInternalServerError , "failed to load verified email" )
2026-08-02 07:37:07 +08:00
return
2026-08-02 09:08:20 +08:00
}
if verified {
settings , err := a . forwardingSettings ( r . Context (), user . ID )
if err != nil {
respondError ( w , http . StatusInternalServerError , "failed to load forwarding settings" )
return
}
respondJSON ( w , http . StatusOK , settings )
2026-08-02 07:37:07 +08:00
return
}
2026-08-02 09:08:20 +08:00
if id == "" {
id = newID ( "fwd" )
}
if err := a . issueForwardingVerification ( r . Context (), user . ID , id , email , errors . Is ( err , sql . ErrNoRows )); err != nil {
2026-08-02 07:37:07 +08:00
respondError ( w , http . StatusInternalServerError , "failed to save verified email" )
return
}
settings , err := a . forwardingSettings ( r . Context (), user . ID )
if err != nil {
respondError ( w , http . StatusInternalServerError , "failed to load forwarding settings" )
return
}
respondJSON ( w , http . StatusCreated , settings )
}
2026-08-02 09:08:20 +08:00
func ( a * App ) handleResendForwardingVerifiedEmail ( w http . ResponseWriter , r * http . Request ) {
user := currentUser ( r )
id := strings . TrimSpace ( chi . URLParam ( r , "id" ))
if id == "" {
respondError ( w , http . StatusNotFound , "verified email not found" )
return
}
var email string
var verified int
err := a . db . QueryRowContext ( r . Context (), `SELECT email,verified FROM forwarding_verified_emails WHERE id=? AND user_id=?` , id , user . ID ). Scan ( & email , & verified )
if errors . Is ( err , sql . ErrNoRows ) {
respondError ( w , http . StatusNotFound , "verified email not found" )
return
}
if err != nil {
respondError ( w , http . StatusInternalServerError , "failed to load verified email" )
return
}
if intBool ( verified ) {
settings , err := a . forwardingSettings ( r . Context (), user . ID )
if err != nil {
respondError ( w , http . StatusInternalServerError , "failed to load forwarding settings" )
return
}
respondJSON ( w , http . StatusOK , settings )
return
}
if err := a . issueForwardingVerification ( r . Context (), user . ID , id , email , false ); err != nil {
respondError ( w , http . StatusInternalServerError , "failed to resend verification email" )
return
}
settings , err := a . forwardingSettings ( r . Context (), user . ID )
if err != nil {
respondError ( w , http . StatusInternalServerError , "failed to load forwarding settings" )
return
}
respondJSON ( w , http . StatusOK , settings )
}
func ( a * App ) handleVerifyForwardingEmail ( w http . ResponseWriter , r * http . Request ) {
token := strings . TrimSpace ( r . URL . Query (). Get ( "token" ))
if token == "" {
a . renderForwardingVerificationPage ( w , http . StatusBadRequest , false , "" , "验证链接无效" )
return
}
var id , email string
var verified int
var expiresRaw sql . NullString
err := a . db . QueryRowContext ( r . Context (), `SELECT id,email,verified,verification_expires_at FROM forwarding_verified_emails WHERE verification_token_hash=?` , hashToken ( token )). Scan ( & id , & email , & verified , & expiresRaw )
if errors . Is ( err , sql . ErrNoRows ) {
a . renderForwardingVerificationPage ( w , http . StatusBadRequest , false , "" , "验证链接无效或已使用" )
return
}
if err != nil {
a . renderForwardingVerificationPage ( w , http . StatusInternalServerError , false , "" , "验证失败,请稍后重试" )
return
}
if intBool ( verified ) {
a . renderForwardingVerificationPage ( w , http . StatusOK , true , email , "该邮箱已经验证完成" )
return
}
if expiresRaw . Valid && expiresRaw . String != "" && parseTime ( expiresRaw . String ). Before ( a . now (). UTC ()) {
a . renderForwardingVerificationPage ( w , http . StatusBadRequest , false , email , "验证链接已过期,请回到设置页重新发送" )
return
}
now := a . now (). UTC (). Format ( time . RFC3339Nano )
_ , err = a . db . ExecContext ( r . Context (), `UPDATE forwarding_verified_emails
2026-08-02 09:36:30 +08:00
SET verified=1,verified_at=?,delivery_status='verified',delivery_error='',updated_at=?
2026-08-02 09:08:20 +08:00
WHERE id=?` , now , now , id )
if err != nil {
a . renderForwardingVerificationPage ( w , http . StatusInternalServerError , false , email , "验证失败,请稍后重试" )
return
}
a . renderForwardingVerificationPage ( w , http . StatusOK , true , email , "验证完成,可以回到设置页选择此转发目标" )
}
2026-08-02 07:37:07 +08:00
func ( a * App ) handleDeleteForwardingVerifiedEmail ( w http . ResponseWriter , r * http . Request ) {
user := currentUser ( r )
id := strings . TrimSpace ( chi . URLParam ( r , "id" ))
if id == "" {
respondError ( w , http . StatusNotFound , "verified email not found" )
return
}
var email string
if err := a . db . QueryRowContext ( r . Context (), `SELECT email FROM forwarding_verified_emails WHERE id=? AND user_id=?` , id , user . ID ). Scan ( & email ); err != nil {
respondError ( w , http . StatusNotFound , "verified email not found" )
return
}
tx , err := a . db . BeginTx ( r . Context (), nil )
if err != nil {
respondError ( w , http . StatusInternalServerError , "failed to start transaction" )
return
}
defer tx . Rollback ()
now := a . now (). UTC (). Format ( time . RFC3339Nano )
if _ , err := tx . ExecContext ( r . Context (), `DELETE FROM forwarding_verified_emails WHERE id=? AND user_id=?` , id , user . ID ); err != nil {
respondError ( w , http . StatusInternalServerError , "failed to delete verified email" )
return
}
2026-08-02 18:33:25 +08:00
if err := a . removeForwardingTargetFromSettings ( r . Context (), tx , user . ID , email , now ); err != nil {
2026-08-02 07:37:07 +08:00
respondError ( w , http . StatusInternalServerError , "failed to update mailbox forwarding" )
return
}
if err := tx . Commit (); err != nil {
respondError ( w , http . StatusInternalServerError , "failed to save forwarding settings" )
return
}
settings , err := a . forwardingSettings ( r . Context (), user . ID )
if err != nil {
respondError ( w , http . StatusInternalServerError , "failed to load forwarding settings" )
return
}
respondJSON ( w , http . StatusOK , settings )
}
func ( a * App ) handleUpdateAccountForwarding ( w http . ResponseWriter , r * http . Request ) {
user := currentUser ( r )
var req struct {
2026-08-02 18:33:25 +08:00
TargetEmail string `json:"targetEmail"`
TargetEmails [] string `json:"targetEmails"`
2026-08-02 07:37:07 +08:00
}
if err := decodeJSON ( r , & req ); err != nil {
badRequest ( w , err )
return
}
2026-08-02 18:33:25 +08:00
targets , err := a . cleanForwardingTargets ( r . Context (), user . ID , forwardingTargetsFromRequest ( req . TargetEmail , req . TargetEmails ))
2026-08-02 07:37:07 +08:00
if err != nil {
badRequest ( w , err )
return
}
2026-08-02 18:33:25 +08:00
target := firstForwardingTarget ( targets )
2026-08-02 07:37:07 +08:00
now := a . now (). UTC (). Format ( time . RFC3339Nano )
2026-08-02 18:33:25 +08:00
_ , err = a . db . ExecContext ( r . Context (), `INSERT INTO account_forwarding_settings(user_id,target_email,target_emails,updated_at)
VALUES(?,?,?,?)
ON CONFLICT(user_id) DO UPDATE SET target_email=excluded.target_email,target_emails=excluded.target_emails,updated_at=excluded.updated_at` ,
user . ID , target , jsonEncode ( targets ), now )
2026-08-02 07:37:07 +08:00
if err != nil {
respondError ( w , http . StatusInternalServerError , "failed to save account forwarding" )
return
}
settings , err := a . forwardingSettings ( r . Context (), user . ID )
if err != nil {
respondError ( w , http . StatusInternalServerError , "failed to load forwarding settings" )
return
}
respondJSON ( w , http . StatusOK , settings )
}
func ( a * App ) handleUpdateMailboxForwarding ( w http . ResponseWriter , r * http . Request ) {
user := currentUser ( r )
mailboxID := strings . TrimSpace ( chi . URLParam ( r , "id" ))
if mailboxID == "" {
respondError ( w , http . StatusNotFound , "mailbox not found" )
return
}
if ok , err := a . userOwnsMailboxID ( r . Context (), user . ID , mailboxID ); err != nil {
respondError ( w , http . StatusInternalServerError , "failed to check mailbox" )
return
} else if ! ok {
respondError ( w , http . StatusNotFound , "mailbox not found" )
return
}
var req struct {
2026-08-02 18:33:25 +08:00
TargetEmail string `json:"targetEmail"`
TargetEmails [] string `json:"targetEmails"`
2026-08-02 07:37:07 +08:00
}
if err := decodeJSON ( r , & req ); err != nil {
badRequest ( w , err )
return
}
2026-08-02 18:33:25 +08:00
targets , err := a . cleanForwardingTargets ( r . Context (), user . ID , forwardingTargetsFromRequest ( req . TargetEmail , req . TargetEmails ))
2026-08-02 07:37:07 +08:00
if err != nil {
badRequest ( w , err )
return
}
2026-08-02 18:33:25 +08:00
target := firstForwardingTarget ( targets )
if len ( targets ) == 0 {
2026-08-02 07:37:07 +08:00
if _ , err := a . db . ExecContext ( r . Context (), `DELETE FROM mailbox_forwarding_settings WHERE mailbox_id=?` , mailboxID ); err != nil {
respondError ( w , http . StatusInternalServerError , "failed to save mailbox forwarding" )
return
}
} else {
now := a . now (). UTC (). Format ( time . RFC3339Nano )
2026-08-02 18:33:25 +08:00
if _ , err := a . db . ExecContext ( r . Context (), `INSERT INTO mailbox_forwarding_settings(mailbox_id,target_email,target_emails,updated_at)
VALUES(?,?,?,?)
ON CONFLICT(mailbox_id) DO UPDATE SET target_email=excluded.target_email,target_emails=excluded.target_emails,updated_at=excluded.updated_at` ,
mailboxID , target , jsonEncode ( targets ), now ); err != nil {
2026-08-02 07:37:07 +08:00
respondError ( w , http . StatusInternalServerError , "failed to save mailbox forwarding" )
return
}
}
settings , err := a . forwardingSettings ( r . Context (), user . ID )
if err != nil {
respondError ( w , http . StatusInternalServerError , "failed to load forwarding settings" )
return
}
respondJSON ( w , http . StatusOK , settings )
}
func ( a * App ) forwardingSettings ( ctx context . Context , userID string ) ( ForwardingSettings , error ) {
settings := ForwardingSettings {
VerifiedEmails : [] ForwardingVerifiedEmail {},
MailboxRules : [] MailboxForwardingRule {},
}
2026-08-02 09:08:20 +08:00
rows , err := a . db . QueryContext ( ctx , `SELECT fve.id,fve.email,fve.verified,fve.created_at,
fve.verified_at,fve.verification_sent_at,fve.verification_expires_at,
COALESCE(NULLIF(sq.status,''), fve.delivery_status),
COALESCE(NULLIF(sq.last_error,''), fve.delivery_error)
FROM forwarding_verified_emails fve
LEFT JOIN send_queue sq ON sq.id=fve.delivery_queue_id
WHERE fve.user_id=?
ORDER BY fve.created_at DESC,fve.email` , userID )
2026-08-02 07:37:07 +08:00
if err != nil {
return settings , err
}
defer rows . Close ()
for rows . Next () {
var item ForwardingVerifiedEmail
var verified int
var created string
2026-08-02 09:08:20 +08:00
var verifiedAt , sentAt , expiresAt sql . NullString
if err := rows . Scan ( & item . ID , & item . Email , & verified , & created , & verifiedAt , & sentAt , & expiresAt , & item . DeliveryStatus , & item . DeliveryError ); err != nil {
2026-08-02 07:37:07 +08:00
return settings , err
}
item . Verified = intBool ( verified )
item . CreatedAt = parseTime ( created )
2026-08-02 09:08:20 +08:00
item . VerifiedAt = nullableTime ( verifiedAt )
item . VerificationSentAt = nullableTime ( sentAt )
item . VerificationExpiresAt = nullableTime ( expiresAt )
2026-08-02 07:37:07 +08:00
settings . VerifiedEmails = append ( settings . VerifiedEmails , item )
}
if err := rows . Err (); err != nil {
return settings , err
}
2026-08-02 18:33:25 +08:00
var accountTarget , accountTargetsJSON string
err = a . db . QueryRowContext ( ctx , `SELECT target_email,target_emails FROM account_forwarding_settings WHERE user_id=?` , userID ). Scan ( & accountTarget , & accountTargetsJSON )
2026-08-02 07:37:07 +08:00
if err != nil && ! errors . Is ( err , sql . ErrNoRows ) {
return settings , err
}
2026-08-02 18:33:25 +08:00
settings . AccountTargetEmails = forwardingTargetsFromStored ( accountTarget , accountTargetsJSON )
settings . AccountTargetEmail = firstForwardingTarget ( settings . AccountTargetEmails )
rows , err = a . db . QueryContext ( ctx , `SELECT mfs.mailbox_id,mfs.target_email,mfs.target_emails
2026-08-02 07:37:07 +08:00
FROM mailbox_forwarding_settings mfs
JOIN mailboxes mb ON mb.id=mfs.mailbox_id
2026-08-02 18:33:25 +08:00
WHERE mb.user_id=? AND (mfs.target_email<>'' OR mfs.target_emails<>'[]')
2026-08-02 07:37:07 +08:00
ORDER BY mb.address` , userID )
if err != nil {
return settings , err
}
defer rows . Close ()
for rows . Next () {
var item MailboxForwardingRule
2026-08-02 18:33:25 +08:00
var target , targetsJSON string
if err := rows . Scan ( & item . MailboxID , & target , & targetsJSON ); err != nil {
2026-08-02 07:37:07 +08:00
return settings , err
}
2026-08-02 18:33:25 +08:00
item . TargetEmails = forwardingTargetsFromStored ( target , targetsJSON )
item . TargetEmail = firstForwardingTarget ( item . TargetEmails )
if len ( item . TargetEmails ) > 0 {
settings . MailboxRules = append ( settings . MailboxRules , item )
}
2026-08-02 07:37:07 +08:00
}
return settings , rows . Err ()
}
2026-08-02 09:08:20 +08:00
func ( a * App ) issueForwardingVerification ( ctx context . Context , userID , id , email string , insert bool ) error {
now := a . now (). UTC ()
expires := now . Add ( forwardingVerificationTTL )
token := randomToken ()
nowRaw := now . Format ( time . RFC3339Nano )
expiresRaw := expires . Format ( time . RFC3339Nano )
if insert {
if _ , err := a . db . ExecContext ( ctx , `INSERT INTO forwarding_verified_emails(id,user_id,email,verified,verified_at,verification_token_hash,verification_sent_at,verification_expires_at,delivery_queue_id,delivery_status,delivery_error,created_at,updated_at)
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?)` ,
id , userID , email , 0 , nil , hashToken ( token ), nowRaw , expiresRaw , "" , sendQueueStatusQueued , "" , nowRaw , nowRaw ); err != nil {
return err
}
} else {
if _ , err := a . db . ExecContext ( ctx , `UPDATE forwarding_verified_emails
SET verified=0,verified_at=NULL,verification_token_hash=?,verification_sent_at=?,verification_expires_at=?,delivery_queue_id='',delivery_status=?,delivery_error='',updated_at=?
WHERE id=? AND user_id=?` ,
hashToken ( token ), nowRaw , expiresRaw , sendQueueStatusQueued , nowRaw , id , userID ); err != nil {
return err
}
}
queueID , err := a . sendForwardingVerificationEmail ( ctx , userID , email , token , now )
if err != nil {
_ , _ = a . db . ExecContext ( ctx , `UPDATE forwarding_verified_emails SET delivery_status=?,delivery_error=?,updated_at=? WHERE id=? AND user_id=?` , sendQueueStatusFailed , err . Error (), nowRaw , id , userID )
return nil
}
if queueID != "" {
_ , _ = a . db . ExecContext ( ctx , `UPDATE forwarding_verified_emails SET delivery_queue_id=?,delivery_status=?,delivery_error='',updated_at=? WHERE id=? AND user_id=?` , queueID , sendQueueStatusQueued , nowRaw , id , userID )
}
return nil
}
func ( a * App ) sendForwardingVerificationEmail ( ctx context . Context , userID , targetEmail , token string , now time . Time ) ( string , error ) {
if strings . TrimSpace ( a . cfg . SMTPHost ) == "" {
return "" , errors . New ( "SMTP 未配置,无法发送验证邮件" )
}
mb , err := a . primaryMailboxForUser ( ctx , userID )
if err != nil {
return "" , err
}
fromDomain := domainPart ( mb . Address )
from := "noreply@" + fromDomain
link := a . forwardingVerificationURL ( token )
text := "邮箱转发验证\n\n您正在将此邮箱添加为邮件转发目标地址。请打开以下链接完成验证:\n" + link + "\n\n此链接 24 小时内有效。如果您没有发起此操作,请忽略此邮件。"
html := `<div style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Arial,sans-serif;color:#111827;line-height:1.6;padding:32px 24px">
<div style="max-width:640px;margin:0 auto">
<h1 style="font-size:28px;line-height:1.25;margin:0 0 28px;font-weight:700">邮箱转发验证</h1>
<p style="font-size:17px;margin:0 0 28px">您正在将此邮箱添加为邮件转发目标地址。请点击下方按钮完成验证:</p>
<p style="text-align:center;margin:0 0 34px"><a href="` + htmlEscape ( link ) + `" style="display:inline-block;background:#2563eb;color:#ffffff;text-decoration:none;border-radius:8px;padding:14px 38px;font-size:18px;font-weight:700">确认验证</a></p>
<p style="font-size:15px;color:#6b7280;margin:0 0 12px">如果按钮无法点击,请复制以下链接到浏览器:</p>
<p style="font-size:15px;color:#6b7280;word-break:break-all;margin:0 0 28px">` + htmlEscape ( link ) + `</p>
<p style="font-size:15px;color:#9ca3af;margin:0">此链接 24 小时内有效。如果您没有发起此操作,请忽略此邮件。</p>
</div></div>`
messageID := fmt . Sprintf ( "<%s@%s>" , newID ( "fwdverify" ), fromDomain )
mimeBytes , err := BuildMIME ( MIMEMessage { From : from , FromName : "noreply" , To : [] string { targetEmail }, Subject : "邮箱转发验证" , Text : text , HTML : html , MessageID : messageID , Date : now })
if err != nil {
return "" , err
}
return a . enqueueSend ( ctx , sendQueueInput {
UserID : userID ,
MailboxID : mb . ID ,
MessageID : messageID ,
Source : sendSourceForwardingVerification ,
MailFrom : from ,
HeaderFrom : from ,
Recipients : [] string { targetEmail },
MIMEBytes : mimeBytes ,
Now : now ,
})
}
func ( a * App ) forwardingVerificationURL ( token string ) string {
base := strings . TrimRight ( strings . TrimSpace ( a . cfg . PublicBaseURL ), "/" )
if base == "" {
base = "https://" + strings . Trim ( strings . TrimSpace ( a . cfg . PublicHostname ), "/" )
}
return base + "/api/verify-email?token=" + url . QueryEscape ( token )
}
func ( a * App ) renderForwardingVerificationPage ( w http . ResponseWriter , status int , ok bool , email , message string ) {
title := "邮箱转发验证"
heading := "验证失败"
color := "#dc2626"
if ok {
heading = "验证完成"
color = "#2563eb"
}
w . Header (). Set ( "Content-Type" , "text/html; charset=utf-8" )
w . WriteHeader ( status )
_ , _ = fmt . Fprintf ( w , `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>%s</title></head><body style="margin:0;background:#f8fafc;color:#0f172a;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Arial,sans-serif"><main style="min-height:100vh;display:grid;place-items:center;padding:24px"><section style="width:min(100%%,520px);background:white;border:1px solid #e2e8f0;border-radius:14px;padding:34px 30px;box-shadow:0 18px 45px rgba(15,23,42,.08)"><h1 style="margin:0 0 14px;font-size:28px">%s</h1><p style="margin:0 0 10px;font-size:17px;color:#475569">%s</p><p style="margin:0 0 26px;font-size:15px;color:#64748b">%s</p><a href="/" style="display:inline-block;border-radius:8px;background:%s;color:white;text-decoration:none;padding:12px 18px;font-weight:700">返回邮箱</a></section></main></body></html>` ,
title , heading , htmlEscape ( message ), htmlEscape ( email ), color )
}
func ( a * App ) cleanForwardingVerificationEmail ( w http . ResponseWriter , r * http . Request , userID , value string ) ( string , bool ) {
email := normalizeEmail ( value )
if email == "" || ! strings . Contains ( email , "@" ) {
badRequest ( w , errors . New ( "邮箱地址无效" ))
return "" , false
}
if owns , err := a . userOwnsMailboxAddress ( r . Context (), userID , email ); err != nil {
respondError ( w , http . StatusInternalServerError , "failed to check mailbox" )
return "" , false
} else if owns {
badRequest ( w , errors . New ( "不能把当前账号邮箱作为转发验证邮箱" ))
return "" , false
}
return email , true
}
func ( a * App ) forwardingVerifiedEmailState ( ctx context . Context , userID , email string ) ( id string , verified bool , err error ) {
var verifiedInt int
err = a . db . QueryRowContext ( ctx , `SELECT id,verified FROM forwarding_verified_emails WHERE user_id=? AND email=?` , userID , normalizeEmail ( email )). Scan ( & id , & verifiedInt )
return id , intBool ( verifiedInt ), err
}
func ( a * App ) primaryMailboxForUser ( ctx context . Context , userID string ) ( Mailbox , error ) {
var mb Mailbox
var created string
err := a . db . QueryRowContext ( ctx , `SELECT id,user_id,domain_id,local_part,address,display_name,quota_mb,status,created_at
FROM mailboxes WHERE user_id=? AND status='active' ORDER BY created_at,id LIMIT 1` , userID ).
Scan ( & mb . ID , & mb . UserID , & mb . DomainID , & mb . LocalPart , & mb . Address , & mb . DisplayName , & mb . QuotaMB , & mb . Status , & created )
if errors . Is ( err , sql . ErrNoRows ) {
return mb , errors . New ( "当前账号没有可用于发送验证邮件的邮箱" )
}
mb . CreatedAt = parseTime ( created )
return mb , err
}
2026-08-02 18:33:25 +08:00
func ( a * App ) forwardingEmailVerified ( ctx context . Context , userID , email string ) ( bool , error ) {
var count int
err := a . db . QueryRowContext ( ctx , `SELECT COUNT(1) FROM forwarding_verified_emails WHERE user_id=? AND email=? AND verified=1` , userID , normalizeEmail ( email )). Scan ( & count )
if err != nil {
return false , err
2026-08-02 07:37:07 +08:00
}
2026-08-02 18:33:25 +08:00
return count > 0 , nil
}
func forwardingTargetsFromRequest ( targetEmail string , targetEmails [] string ) [] string {
if len ( targetEmails ) > 0 {
return targetEmails
2026-08-02 07:37:07 +08:00
}
2026-08-02 18:33:25 +08:00
if strings . TrimSpace ( targetEmail ) == "" {
return nil
2026-08-02 07:37:07 +08:00
}
2026-08-02 18:33:25 +08:00
return [] string { targetEmail }
}
func ( a * App ) cleanForwardingTargets ( ctx context . Context , userID string , values [] string ) ([] string , error ) {
targets := make ([] string , 0 , len ( values ))
seen := map [ string ] bool {}
for _ , value := range values {
value = strings . TrimSpace ( value )
if value == "" || strings . EqualFold ( value , "none" ) {
continue
}
target := normalizeEmail ( value )
if target == "" || ! strings . Contains ( target , "@" ) {
return nil , errors . New ( "转发邮箱无效" )
}
if seen [ target ] {
continue
}
ok , err := a . forwardingEmailVerified ( ctx , userID , target )
if err != nil {
return nil , err
}
if ! ok {
return nil , errors . New ( "请先完成邮箱验证" )
}
seen [ target ] = true
targets = append ( targets , target )
2026-08-02 07:37:07 +08:00
}
2026-08-02 18:33:25 +08:00
return targets , nil
2026-08-02 07:37:07 +08:00
}
2026-08-02 18:33:25 +08:00
func forwardingTargetsFromStored ( targetEmail , targetsJSON string ) [] string {
targets := dedupeEmails ( jsonDecodeSlice ( targetsJSON ))
if len ( targets ) > 0 {
return targets
}
target := normalizeEmail ( targetEmail )
if target == "" {
return nil
}
return [] string { target }
}
func firstForwardingTarget ( targets [] string ) string {
if len ( targets ) == 0 {
return ""
}
return targets [ 0 ]
}
func removeForwardingTarget ( targets [] string , email string ) [] string {
email = normalizeEmail ( email )
next := make ([] string , 0 , len ( targets ))
for _ , target := range targets {
if normalizeEmail ( target ) == email {
continue
}
next = append ( next , normalizeEmail ( target ))
}
return dedupeEmails ( next )
}
func ( a * App ) removeForwardingTargetFromSettings ( ctx context . Context , tx * sql . Tx , userID , email , now string ) error {
var accountTarget , accountTargetsJSON string
if err := tx . QueryRowContext ( ctx , `SELECT target_email,target_emails FROM account_forwarding_settings WHERE user_id=?` , userID ). Scan ( & accountTarget , & accountTargetsJSON ); err != nil && ! errors . Is ( err , sql . ErrNoRows ) {
return err
} else if err == nil {
targets := removeForwardingTarget ( forwardingTargetsFromStored ( accountTarget , accountTargetsJSON ), email )
if _ , err := tx . ExecContext ( ctx , `UPDATE account_forwarding_settings SET target_email=?,target_emails=?,updated_at=? WHERE user_id=?` , firstForwardingTarget ( targets ), jsonEncode ( targets ), now , userID ); err != nil {
return err
}
}
rows , err := tx . QueryContext ( ctx , `SELECT mfs.mailbox_id,mfs.target_email,mfs.target_emails
FROM mailbox_forwarding_settings mfs
JOIN mailboxes mb ON mb.id=mfs.mailbox_id
WHERE mb.user_id=?` , userID )
2026-08-02 07:37:07 +08:00
if err != nil {
2026-08-02 18:33:25 +08:00
return err
2026-08-02 07:37:07 +08:00
}
2026-08-02 18:33:25 +08:00
type mailboxRow struct {
id string
target string
targetsJSON string
}
var items [] mailboxRow
for rows . Next () {
var item mailboxRow
if err := rows . Scan ( & item . id , & item . target , & item . targetsJSON ); err != nil {
rows . Close ()
return err
}
items = append ( items , item )
}
if err := rows . Close (); err != nil {
return err
}
if err := rows . Err (); err != nil {
return err
}
for _ , item := range items {
targets := removeForwardingTarget ( forwardingTargetsFromStored ( item . target , item . targetsJSON ), email )
if len ( targets ) == 0 {
if _ , err := tx . ExecContext ( ctx , `DELETE FROM mailbox_forwarding_settings WHERE mailbox_id=?` , item . id ); err != nil {
return err
}
continue
}
if _ , err := tx . ExecContext ( ctx , `UPDATE mailbox_forwarding_settings SET target_email=?,target_emails=?,updated_at=? WHERE mailbox_id=?` , firstForwardingTarget ( targets ), jsonEncode ( targets ), now , item . id ); err != nil {
return err
}
}
return nil
2026-08-02 07:37:07 +08:00
}
func ( a * App ) userOwnsMailboxID ( ctx context . Context , userID , mailboxID string ) ( bool , error ) {
var count int
err := a . db . QueryRowContext ( ctx , `SELECT COUNT(1) FROM mailboxes WHERE id=? AND user_id=? AND status='active'` , mailboxID , userID ). Scan ( & count )
if err != nil {
return false , err
}
return count > 0 , nil
}
func ( a * App ) userOwnsMailboxAddress ( ctx context . Context , userID , address string ) ( bool , error ) {
var count int
err := a . db . QueryRowContext ( ctx , `SELECT COUNT(1) FROM mailboxes WHERE user_id=? AND address=? AND status='active'` , userID , normalizeEmail ( address )). Scan ( & count )
if err != nil {
return false , err
}
return count > 0 , nil
}