feat: SMTP test button in admin modal — sends real test email, shows result

This commit is contained in:
mirivlad 2026-06-02 00:12:41 +08:00
parent 4afcc0e135
commit c8cdb089a6
1 changed files with 85 additions and 2 deletions

View File

@ -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'})
}
</script>
<h3>Новый ключ</h3>
@ -1461,7 +1512,11 @@ function closeHealth(e){if(!e||e.target.id==='health-modal')document.getElementB
<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>
<div class="form-row"><label>URL сервера</label><input name="server_url" value="` + srvURL + `" placeholder="https://example.com:47732"></div>
<div style="margin-top:12px"><button class="btn btn-primary">Сохранить SMTP</button></div>
<div style="margin-top:12px;display:flex;gap:8px;align-items:center">
<button class="btn btn-primary">Сохранить SMTP</button>
<button class="btn" type="button" onclick="testSMTP()">Test</button>
<span id="smtp-test-result" style="font-size:12px"></span>
</div>
</form>
</div>
</div>
@ -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