feat: SMTP security selector (none/STARTTLS/TLS) instead of port-based detection
This commit is contained in:
parent
fa6f988368
commit
daed8e0aba
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in New Issue