feat: SMTP security selector (none/STARTTLS/TLS) instead of port-based detection

This commit is contained in:
mirivlad 2026-06-02 00:18:04 +08:00
parent fa6f988368
commit daed8e0aba
1 changed files with 105 additions and 99 deletions

View File

@ -396,124 +396,122 @@ func (s *Server) smtpSet(key, val string) error {
return err
}
func sel(v, want string) string {
if v == want {
return " selected"
}
return ""
}
func (s *Server) smtpConnect(host, port, user, pass, security string) (*smtp.Client, error) {
addr := net.JoinHostPort(host, port)
switch security {
case "tls":
tlsCfg := &tls.Config{ServerName: host}
conn, err := tls.Dial("tcp", addr, tlsCfg)
if err != nil {
return nil, fmt.Errorf("tls dial: %w", err)
}
cl, err := smtp.NewClient(conn, host)
if err != nil {
conn.Close()
return nil, fmt.Errorf("smtp client: %w", err)
}
return cl, nil
default:
conn, err := net.DialTimeout("tcp", addr, 10*time.Second)
if err != nil {
return nil, fmt.Errorf("connect: %w", err)
}
cl, err := smtp.NewClient(conn, host)
if err != nil {
conn.Close()
return nil, fmt.Errorf("smtp client: %w", err)
}
if security != "none" {
if ok, _ := cl.Extension("STARTTLS"); ok {
tlsCfg := &tls.Config{ServerName: host}
if err := cl.StartTLS(tlsCfg); err != nil {
cl.Close()
return nil, fmt.Errorf("starttls: %w", err)
}
}
}
return cl, nil
}
}
func (s *Server) smtpSendMsg(cl *smtp.Client, user, pass, host, from, to string, msg []byte) error {
if user != "" {
auth := smtp.PlainAuth("", user, pass, host)
if err := cl.Auth(auth); err != nil {
return fmt.Errorf("auth: %w", err)
}
}
if err := cl.Mail(from); err != nil {
return fmt.Errorf("mail from: %w", err)
}
if err := cl.Rcpt(to); err != nil {
return fmt.Errorf("rcpt: %w", err)
}
w, err := cl.Data()
if err != nil {
return fmt.Errorf("data: %w", err)
}
if _, err := w.Write(msg); err != nil {
w.Close()
return fmt.Errorf("write: %w", err)
}
if err := w.Close(); err != nil {
return fmt.Errorf("send: %w", err)
}
return nil
}
func (s *Server) smtpSend(to, subject, body string) error {
host := s.smtpGet("smtp_host")
port := s.smtpGet("smtp_port")
user := s.smtpGet("smtp_user")
pass := s.smtpGet("smtp_pass")
from := s.smtpGet("smtp_from")
security := s.smtpGet("smtp_security")
if host == "" || port == "" || from == "" {
err := fmt.Errorf("SMTP not configured")
log.Printf("smtp: %v (to=%s)", err, to)
return err
}
addr := net.JoinHostPort(host, port)
log.Printf("smtp: sending to %s via %s:%s", to, host, port)
log.Printf("smtp: sending to %s via %s:%s (security=%s)", to, host, port, security)
msg := []byte("From: " + from + "\r\n" +
"To: " + to + "\r\n" +
"Subject: " + subject + "\r\n" +
"MIME-Version: 1.0\r\n" +
"Content-Type: text/plain; charset=UTF-8\r\n" +
"\r\n" + body + "\r\n")
if user != "" {
auth := smtp.PlainAuth("", user, pass, host)
if port == "465" {
tlsCfg := &tls.Config{ServerName: host}
conn, err := tls.Dial("tcp", addr, tlsCfg)
if err != nil {
log.Printf("smtp: tls dial error: %v", err)
return err
}
cl, err := smtp.NewClient(conn, host)
if err != nil {
log.Printf("smtp: new client error: %v", err)
return err
}
defer cl.Close()
if err := cl.Auth(auth); err != nil {
log.Printf("smtp: auth error: %v", err)
return err
}
if err := cl.Mail(from); err != nil {
log.Printf("smtp: mail from error: %v", err)
return err
}
if err := cl.Rcpt(to); err != nil {
log.Printf("smtp: rcpt error: %v", err)
return err
}
w, err := cl.Data()
if err != nil {
log.Printf("smtp: data error: %v", err)
return err
}
_, err = w.Write(msg)
if err != nil {
log.Printf("smtp: write error: %v", err)
return err
}
if err := w.Close(); err != nil {
log.Printf("smtp: close error: %v", err)
return err
}
log.Printf("smtp: sent OK to %s", to)
return nil
}
err := smtp.SendMail(addr, auth, from, []string{to}, msg)
if err != nil {
log.Printf("smtp: sendmail error (auth): %v", err)
} else {
log.Printf("smtp: sent OK to %s", to)
}
cl, err := s.smtpConnect(host, port, user, pass, security)
if err != nil {
log.Printf("smtp: connect error: %v", err)
return err
}
err := smtp.SendMail(addr, nil, from, []string{to}, msg)
if err != nil {
log.Printf("smtp: sendmail error (no auth): %v", err)
} else {
log.Printf("smtp: sent OK to %s", to)
defer cl.Close()
if err := s.smtpSendMsg(cl, user, pass, host, from, to, msg); err != nil {
log.Printf("smtp: send error: %v", err)
return err
}
return err
log.Printf("smtp: sent OK to %s", to)
return nil
}
func (s *Server) smtpTest(host, port, user, pass, from, to string) error {
func (s *Server) smtpTest(host, port, user, pass, security, from, to string) error {
if host == "" || port == "" || from == "" {
return fmt.Errorf("SMTP not configured")
}
addr := net.JoinHostPort(host, port)
msg := []byte("From: " + from + "\r\nTo: " + to + "\r\nSubject: Test from Verstak Sync\r\n\r\nThis is a test email from Verstak Sync Server.\r\n")
if user != "" {
auth := smtp.PlainAuth("", user, pass, host)
if port == "465" {
tlsCfg := &tls.Config{ServerName: host}
conn, err := tls.Dial("tcp", addr, tlsCfg)
if err != nil {
return err
}
cl, err := smtp.NewClient(conn, host)
if err != nil {
return err
}
defer cl.Close()
if err := cl.Auth(auth); err != nil {
return err
}
if err := cl.Mail(from); err != nil {
return err
}
if err := cl.Rcpt(to); err != nil {
return err
}
w, err := cl.Data()
if err != nil {
return err
}
w.Write(msg)
return w.Close()
}
return smtp.SendMail(addr, auth, from, []string{to}, msg)
cl, err := s.smtpConnect(host, port, user, pass, security)
if err != nil {
return err
}
return smtp.SendMail(addr, nil, from, []string{to}, msg)
defer cl.Close()
return s.smtpSendMsg(cl, user, pass, host, from, to, msg)
}
// ============================================================
@ -1409,6 +1407,7 @@ func (s *Server) handleAdminDashboard(w http.ResponseWriter, r *http.Request) {
smtpPort := s.smtpGet("smtp_port")
smtpUser := s.smtpGet("smtp_user")
smtpFrom := s.smtpGet("smtp_from")
smtpSecurity := s.smtpGet("smtp_security")
srvURL := s.smtpGet("server_url")
html := fmt.Sprintf(`<!DOCTYPE html>
@ -1509,6 +1508,11 @@ function testSMTP(){
<form action="/admin/api/smtp" method="POST">
<div class="form-row"><label>Сервер</label><input name="smtp_host" value="` + smtpHost + `" placeholder="smtp.example.com"></div>
<div class="form-row"><label>Порт</label><input name="smtp_port" value="` + smtpPort + `" placeholder="587"></div>
<div class="form-row"><label>Тип</label><select name="smtp_security" style="font-family:inherit;font-size:14px;padding:8px 12px;border:1px solid #2a2a3c;background:#13131f;color:#e4e4ef;border-radius:6px;flex:1;box-sizing:border-box">
<option value="starttls"` + sel(smtpSecurity, "starttls") + `>STARTTLS</option>
<option value="tls"` + sel(smtpSecurity, "tls") + `>TLS</option>
<option value="none"` + sel(smtpSecurity, "none") + `>Без шифрования</option>
</select></div>
<div class="form-row"><label>Логин</label><input name="smtp_user" value="` + smtpUser + `" placeholder="user@example.com"></div>
<div class="form-row"><label>Пароль</label><input type="password" name="smtp_pass" placeholder="••••••••"></div>
<div class="form-row"><label>От кого</label><input name="smtp_from" value="` + smtpFrom + `" placeholder="noreply@example.com"></div>
@ -1548,12 +1552,13 @@ func (s *Server) handleAdminSMTPTest(w http.ResponseWriter, r *http.Request) {
return
}
var req struct {
Host string `json:"smtp_host"`
Port string `json:"smtp_port"`
User string `json:"smtp_user"`
Pass string `json:"smtp_pass"`
From string `json:"smtp_from"`
To string `json:"test_to"`
Host string `json:"smtp_host"`
Port string `json:"smtp_port"`
User string `json:"smtp_user"`
Pass string `json:"smtp_pass"`
Security string `json:"smtp_security"`
From string `json:"smtp_from"`
To string `json:"test_to"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
jsonErr(w, 400, "bad json")
@ -1563,6 +1568,7 @@ func (s *Server) handleAdminSMTPTest(w http.ResponseWriter, r *http.Request) {
port := req.Port
user := req.User
pass := req.Pass
security := req.Security
from := req.From
to := req.To
if to == "" {
@ -1572,7 +1578,7 @@ func (s *Server) handleAdminSMTPTest(w http.ResponseWriter, r *http.Request) {
jsonOK(w, map[string]interface{}{"ok": false, "error": "host, port and from required"})
return
}
if err := s.smtpTest(host, port, user, pass, from, to); err != nil {
if err := s.smtpTest(host, port, user, pass, security, from, to); err != nil {
jsonOK(w, map[string]interface{}{"ok": false, "error": err.Error()})
return
}
@ -1640,7 +1646,7 @@ func (s *Server) handleAdminAPI(w http.ResponseWriter, r *http.Request) {
jsonErr(w, 400, "bad form")
return
}
for _, key := range []string{"smtp_host", "smtp_port", "smtp_user", "smtp_pass", "smtp_from", "server_url"} {
for _, key := range []string{"smtp_host", "smtp_port", "smtp_user", "smtp_pass", "smtp_security", "smtp_from", "server_url"} {
val := r.FormValue(key)
if val != "" {
s.smtpSet(key, val)