diff --git a/cmd/verstak-server/server.go b/cmd/verstak-server/server.go index c36e379..4980059 100644 --- a/cmd/verstak-server/server.go +++ b/cmd/verstak-server/server.go @@ -332,6 +332,7 @@ func (s *Server) routes() *http.ServeMux { mux.HandleFunc("/admin/login", s.handleAdminLogin) mux.HandleFunc("/admin/dashboard", s.handleAdminDashboard) mux.HandleFunc("/admin/api/stats", s.handleAdminStats) + mux.HandleFunc("/admin/api/smtp/test", s.handleAdminSMTPTest) mux.HandleFunc("/admin/", s.handleAdminAPI) mux.HandleFunc("/", s.handleNotFound) return mux @@ -475,6 +476,46 @@ func (s *Server) smtpSend(to, subject, body string) error { return err } +func (s *Server) smtpTest(host, port, user, pass, 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) + } + return smtp.SendMail(addr, nil, from, []string{to}, msg) +} + // ============================================================ // User helpers // ============================================================ @@ -1438,10 +1479,20 @@ function copyKey(key,btn){ }) } function delKey(id){if(confirm('Удалить ключ?'))fetch('/admin/api/keys/'+id,{method:'DELETE'}).then(()=>location.reload())} -function openSMTP(){document.getElementById('smtp-modal').style.display='flex'} +function openSMTP(){document.getElementById('smtp-modal').style.display='flex';document.getElementById('smtp-test-result').textContent=''} function closeSMTP(e){if(!e||e.target.id==='smtp-modal')document.getElementById('smtp-modal').style.display='none'} function openHealth(){var m=document.getElementById('health-modal');m.style.display='flex';document.getElementById('health-result').textContent='Загрузка...';fetch('/api/v1/health').then(function(r){return r.text()}).then(function(t){document.getElementById('health-result').textContent=t})} function closeHealth(e){if(!e||e.target.id==='health-modal')document.getElementById('health-modal').style.display='none'} +function testSMTP(){ + var f=document.querySelector('#smtp-modal form') + var fd=new FormData(f) + var r=document.getElementById('smtp-test-result') + r.textContent='⏳ Тестируем...';r.style.color='#888' + fetch('/admin/api/smtp/test',{method:'POST',body:fd}).then(function(r2){return r2.json()}).then(function(d){ + r.textContent=d.ok?'✓ Тест пройден':'✗ '+d.error + r.style.color=d.ok?'#4ade80':'#ff6b6b' + }).catch(function(e){r.textContent='✗ '+e;r.style.color='#ff6b6b'}) +}

Новый ключ

@@ -1461,7 +1512,11 @@ function closeHealth(e){if(!e||e.target.id==='health-modal')document.getElementB
-
+
+ + + +
@@ -1487,6 +1542,34 @@ func (s *Server) handleAdminStats(w http.ResponseWriter, r *http.Request) { jsonOK(w, map[string]int{"ops": opsCount}) } +func (s *Server) handleAdminSMTPTest(w http.ResponseWriter, r *http.Request) { + if !s.requireAdmin(w, r) { + return + } + if err := r.ParseForm(); err != nil { + jsonErr(w, 400, "bad form") + return + } + host := r.FormValue("smtp_host") + port := r.FormValue("smtp_port") + user := r.FormValue("smtp_user") + pass := r.FormValue("smtp_pass") + from := r.FormValue("smtp_from") + to := r.FormValue("test_to") + if to == "" { + to = from + } + if host == "" || port == "" || from == "" { + 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 { + jsonOK(w, map[string]interface{}{"ok": false, "error": err.Error()}) + return + } + jsonOK(w, map[string]interface{}{"ok": true}) +} + func (s *Server) handleAdminAPI(w http.ResponseWriter, r *http.Request) { if !s.requireAdmin(w, r) { return