141 lines
3.7 KiB
Go
141 lines
3.7 KiB
Go
// Package smtp sends OTP mail via Aliyun DirectMail SMTP.
|
|
package smtp
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/tls"
|
|
"fmt"
|
|
"net"
|
|
"net/smtp"
|
|
"strings"
|
|
"time"
|
|
|
|
smsentity "spark-server/internal/module/sms/entity"
|
|
"spark-server/internal/pkg/errs"
|
|
"spark-server/internal/pkg/i18n"
|
|
)
|
|
|
|
// Options configures the DirectMail SMTP relay.
|
|
type Options struct {
|
|
Host string
|
|
Port int
|
|
Username string
|
|
Password string
|
|
From string
|
|
}
|
|
|
|
// Mailer implements smslogic.EmailSender.
|
|
type Mailer struct {
|
|
host string
|
|
addr string
|
|
username string
|
|
password string
|
|
from string
|
|
}
|
|
|
|
func New(opts Options) *Mailer {
|
|
host := strings.TrimSpace(opts.Host)
|
|
port := opts.Port
|
|
if port <= 0 {
|
|
port = 465
|
|
}
|
|
from := strings.TrimSpace(opts.From)
|
|
if from == "" {
|
|
from = strings.TrimSpace(opts.Username)
|
|
}
|
|
return &Mailer{
|
|
host: host,
|
|
addr: fmt.Sprintf("%s:%d", host, port),
|
|
username: strings.TrimSpace(opts.Username),
|
|
password: opts.Password,
|
|
from: from,
|
|
}
|
|
}
|
|
|
|
func (m *Mailer) Ready() bool {
|
|
return m != nil && m.host != "" && m.username != "" && m.password != ""
|
|
}
|
|
|
|
func (m *Mailer) SendOTP(ctx context.Context, to string, purpose smsentity.Purpose, code, lang string) error {
|
|
if m == nil || !m.Ready() {
|
|
return errs.ErrAvailable
|
|
}
|
|
to = strings.TrimSpace(to)
|
|
if to == "" {
|
|
return errs.ErrInvalidArgument
|
|
}
|
|
subject, body := mailCopy(purpose, code, i18n.Parse(lang))
|
|
msg := buildMessage(m.from, to, subject, body)
|
|
return m.send(ctx, to, msg)
|
|
}
|
|
|
|
func (m *Mailer) send(ctx context.Context, to string, msg []byte) error {
|
|
dialer := &net.Dialer{Timeout: 15 * time.Second}
|
|
conn, err := tls.DialWithDialer(dialer, "tcp", m.addr, &tls.Config{ServerName: m.host, MinVersion: tls.VersionTLS12})
|
|
if err != nil {
|
|
return errs.ErrOperatFail.WithError("smtp-dial", err)
|
|
}
|
|
defer conn.Close()
|
|
if err := conn.SetDeadline(deadline(ctx)); err != nil {
|
|
return errs.ErrOperatFail.WithError("smtp-deadline", err)
|
|
}
|
|
c, err := smtp.NewClient(conn, m.host)
|
|
if err != nil {
|
|
return errs.ErrOperatFail.WithError("smtp-client", err)
|
|
}
|
|
defer c.Close()
|
|
auth := smtp.PlainAuth("", m.username, m.password, m.host)
|
|
if err := c.Auth(auth); err != nil {
|
|
return errs.ErrOperatFail.WithError("smtp-auth", err)
|
|
}
|
|
if err := c.Mail(m.username); err != nil {
|
|
return errs.ErrOperatFail.WithError("smtp-mail-from", err)
|
|
}
|
|
if err := c.Rcpt(to); err != nil {
|
|
return errs.ErrOperatFail.WithError("smtp-rcpt", err)
|
|
}
|
|
w, err := c.Data()
|
|
if err != nil {
|
|
return errs.ErrOperatFail.WithError("smtp-data", err)
|
|
}
|
|
if _, err := w.Write(msg); err != nil {
|
|
return errs.ErrOperatFail.WithError("smtp-write", err)
|
|
}
|
|
if err := w.Close(); err != nil {
|
|
return errs.ErrOperatFail.WithError("smtp-data-close", err)
|
|
}
|
|
return c.Quit()
|
|
}
|
|
|
|
func deadline(ctx context.Context) time.Time {
|
|
if d, ok := ctx.Deadline(); ok {
|
|
return d
|
|
}
|
|
return time.Now().Add(30 * time.Second)
|
|
}
|
|
|
|
func buildMessage(from, to, subject, body string) []byte {
|
|
var buf bytes.Buffer
|
|
buf.WriteString("From: " + from + "\r\n")
|
|
buf.WriteString("To: " + to + "\r\n")
|
|
buf.WriteString("Subject: " + subject + "\r\n")
|
|
buf.WriteString("MIME-Version: 1.0\r\n")
|
|
buf.WriteString("Content-Type: text/plain; charset=UTF-8\r\n")
|
|
buf.WriteString("\r\n")
|
|
buf.WriteString(body)
|
|
return buf.Bytes()
|
|
}
|
|
|
|
func mailCopy(purpose smsentity.Purpose, code string, lang i18n.Lang) (subject, body string) {
|
|
switch purpose {
|
|
case smsentity.PurposeAccountDelete:
|
|
subject = i18n.T(lang, i18n.KeySMSEmailSubjectDelete, nil)
|
|
body = i18n.T(lang, i18n.KeySMSEmailBodyDelete, map[string]string{"code": code})
|
|
default:
|
|
subject = i18n.T(lang, i18n.KeySMSEmailSubjectLogin, nil)
|
|
body = i18n.T(lang, i18n.KeySMSEmailBodyLogin, map[string]string{"code": code})
|
|
}
|
|
return subject, body
|
|
}
|