%s,您好:
+您的验证码是:
+%s
+验证码将在 %d 分钟后失效。
+如果不是您本人操作,请忽略此邮件。
+diff --git a/backend/res_init/verification_delivery.go b/backend/res_init/verification_delivery.go index 941aa5e..581a986 100644 --- a/backend/res_init/verification_delivery.go +++ b/backend/res_init/verification_delivery.go @@ -152,6 +152,7 @@ func deliverVerificationWithProjectMailContext(deliveryContext context.Context, }, Subject: subject, Text: []byte(text), + HTML: []byte(verification.CodeHTML(challenge, code)), MsgID: messageID, } ctx := &appcontext.Context{ diff --git a/backend/services/verification/delivery.go b/backend/services/verification/delivery.go index 8d01f35..63a2c43 100644 --- a/backend/services/verification/delivery.go +++ b/backend/services/verification/delivery.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "html" "mime" "net" "net/mail" @@ -72,7 +73,7 @@ func (d *SMTPDelivery) Delivery() Delivery { if d == nil || d.send == nil || challenge.Email == "" || code == "" { return ErrInvalidChallenge } - message := buildCodeMessage(d.settings.From, challenge.Email, code, challenge.Purpose) + message := buildCodeMessage(d.settings.From, challenge, code) defer clearBytes(message) err := d.send(d.settings, challenge.Email, message) result := "accepted" @@ -212,9 +213,80 @@ func SendSMTPMessageContext(ctx context.Context, settings SMTPSettings, recipien } } -func buildCodeMessage(from, recipient string, code string, purpose Purpose) []byte { - title, text := CodeMessage(purpose, code) - return []byte(fmt.Sprintf("From: %s\r\nTo: %s\r\nSubject: %s\r\nContent-Type: text/plain; charset=UTF-8\r\n\r\n%s\r\n", from, recipient, mime.QEncoding.Encode("UTF-8", title), text)) +func buildCodeMessage(from string, challenge Challenge, code string) []byte { + title, text := CodeMessage(challenge.Purpose, code) + boundary := "=_SmailVerification_" + htmlBody := CodeHTML(challenge, code) + + return []byte(fmt.Sprintf( + "From: %s\r\nTo: %s\r\nSubject: %s\r\nMIME-Version: 1.0\r\nContent-Type: multipart/alternative; boundary=\"%s\"\r\n\r\n--%s\r\nContent-Type: text/plain; charset=UTF-8\r\nContent-Transfer-Encoding: 8bit\r\n\r\n%s\r\n--%s\r\nContent-Type: text/html; charset=UTF-8\r\nContent-Transfer-Encoding: 8bit\r\n\r\n%s\r\n--%s--\r\n", + from, + challenge.Email, + mime.QEncoding.Encode("UTF-8", title), + boundary, + boundary, + text, + boundary, + htmlBody, + boundary, + )) +} + +// CodeHTML 返回验证码邮件的 HTML 正文。 +func CodeHTML(challenge Challenge, code string) string { + recipientName := challenge.Account + if strings.TrimSpace(recipientName) == "" { + recipientName = challenge.Email + } + return buildCodeHTML(recipientName, code, verificationExpiryMinutes(challenge)) +} + +func verificationExpiryMinutes(challenge Challenge) int { + if challenge.ExpiresAt.After(challenge.IssuedAt) && !challenge.IssuedAt.IsZero() { + minutes := int((challenge.ExpiresAt.Sub(challenge.IssuedAt) + time.Minute - 1) / time.Minute) + if minutes > 0 { + return minutes + } + } + return int(DefaultTTL / time.Minute) +} + +func buildCodeHTML(recipientName, code string, expiresInMinutes int) string { + return fmt.Sprintf(` + + +
+ + + + + +%s,您好:
+您的验证码是:
+%s
+验证码将在 %d 分钟后失效。
+如果不是您本人操作,请忽略此邮件。
+<xuans>,您好:
", + "123456
", + "7 分钟", + "本邮件由 Smail 发送", + } { + if !strings.Contains(raw, want) { + t.Fatalf("message does not contain %q: %s", want, raw) + } + } + if strings.Contains(raw, "