1 Commits

Author SHA1 Message Date
Maxim
9e907f8455 Allow optional SMTP TLS verification bypass 2026-07-31 23:31:22 +03:00
3 changed files with 7 additions and 5 deletions

View File

@@ -58,9 +58,10 @@ func sendAlertEmail(settings EmailSettings, alerts []Alert, test bool) error {
}
message := []byte("From: " + settings.From + "\r\nTo: " + settings.To + "\r\nSubject: " + subject + "\r\nMIME-Version: 1.0\r\nContent-Type: text/plain; charset=UTF-8\r\nContent-Transfer-Encoding: 8bit\r\n\r\n" + body + "\r\n")
address := net.JoinHostPort(settings.Host, fmt.Sprint(settings.Port))
tlsConfig := &tls.Config{ServerName: settings.Host, MinVersion: tls.VersionTLS12, InsecureSkipVerify: settings.SkipTLSVerify} //nolint:gosec -- explicit private-network option
var client *smtp.Client
if settings.Port == 465 {
connection, err := tls.Dial("tcp", address, &tls.Config{ServerName: settings.Host, MinVersion: tls.VersionTLS12})
connection, err := tls.Dial("tcp", address, tlsConfig)
if err != nil {
return err
}
@@ -75,7 +76,7 @@ func sendAlertEmail(settings EmailSettings, alerts []Alert, test bool) error {
return err
}
if ok, _ := client.Extension("STARTTLS"); ok {
if err = client.StartTLS(&tls.Config{ServerName: settings.Host, MinVersion: tls.VersionTLS12}); err != nil {
if err = client.StartTLS(tlsConfig); err != nil {
return err
}
}

View File

@@ -67,6 +67,7 @@ type EmailSettings struct {
Username string `json:"username"`
Password string `json:"password,omitempty"`
HasPassword bool `json:"hasPassword"`
SkipTLSVerify bool `json:"skipTlsVerify"`
From string `json:"from"`
To string `json:"to"`
IntervalMinutes int `json:"intervalMinutes"`

6
web.go
View File

@@ -378,7 +378,7 @@ footer{color:#657581;font-size:12px;margin-top:17px}
<button class="save-settings" id="saveThresholds">Сохранить пороги</button><div class="settings-note" id="thresholdStatus">Настройки хранятся на сервере и применяются для всех браузеров.</div>
</div>
<div class="settings-body settings-panel" data-settings-panel="email" hidden>
<div class="settings-group"><label class="setting-item"><input type="checkbox" id="emailEnabled"> Включить почтовые дайджесты</label></div>
<div class="settings-group"><div class="setting-list"><label class="setting-item"><input type="checkbox" id="emailEnabled"> Включить почтовые дайджесты</label><label class="setting-item"><input type="checkbox" id="emailSkipTLS"> Не проверять TLS-сертификат</label></div><div class="settings-note">Только для доверенного сервера во внутренней сети.</div></div>
<div class="settings-group"><div class="label">SMTP и получатель</div><div class="threshold-grid">
<label class="threshold-item"><span>SMTP-сервер</span><input id="emailHost" type="text"></label><label class="threshold-item"><span>Порт</span><input id="emailPort" type="number"></label>
<label class="threshold-item"><span>Логин</span><input id="emailUsername" type="text"></label><label class="threshold-item"><span>Пароль</span><input id="emailPassword" type="password" placeholder="Не изменять"></label>
@@ -444,8 +444,8 @@ function setTheme(theme){document.documentElement.dataset.theme=theme;localStora
document.querySelectorAll('[data-settings-tab]').forEach(tab=>tab.onclick=()=>{document.querySelectorAll('[data-settings-tab]').forEach(t=>t.classList.toggle('active',t===tab));document.querySelectorAll('[data-settings-panel]').forEach(p=>p.hidden=p.dataset.settingsPanel!==tab.dataset.settingsTab)});
async function loadThresholds(){const response=await fetch('/api/settings/alerts');if(!response.ok)return;alertSettings=await response.json();document.querySelectorAll('[data-threshold]').forEach(input=>input.value=alertSettings[input.dataset.threshold]??'')}
el('saveThresholds').onclick=async()=>{const values={};document.querySelectorAll('[data-threshold]').forEach(input=>values[input.dataset.threshold]=Number(input.value));const status=el('thresholdStatus');status.textContent='Сохранение…';const response=await fetch('/api/settings/alerts',{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify(values)});if(response.ok){alertSettings=await response.json();status.textContent='Пороги сохранены и уже применяются.'}else{status.textContent=await response.text()||'Не удалось сохранить настройки.'}};loadThresholds().catch(()=>{});
function emailForm(){return {enabled:el('emailEnabled').checked,host:el('emailHost').value.trim(),port:Number(el('emailPort').value),username:el('emailUsername').value.trim(),password:el('emailPassword').value,from:el('emailFrom').value.trim(),to:el('emailTo').value.trim(),intervalMinutes:Number(el('emailInterval').value),severities:[...document.querySelectorAll('[data-email-severity]:checked')].map(x=>x.dataset.emailSeverity),sources:[...document.querySelectorAll('[data-email-source]:checked')].map(x=>x.dataset.emailSource)}}
async function loadEmail(){const response=await fetch('/api/settings/email');if(!response.ok)return;const s=await response.json();el('emailEnabled').checked=s.enabled;el('emailHost').value=s.host||'';el('emailPort').value=s.port||587;el('emailUsername').value=s.username||'';el('emailPassword').value='';el('emailPassword').placeholder=s.hasPassword?'Сохранён · не изменять':'Пароль';el('emailFrom').value=s.from||'';el('emailTo').value=s.to||'';el('emailInterval').value=s.intervalMinutes||60;document.querySelectorAll('[data-email-severity]').forEach(x=>x.checked=(s.severities||[]).includes(x.dataset.emailSeverity));document.querySelectorAll('[data-email-source]').forEach(x=>x.checked=(s.sources||[]).includes(x.dataset.emailSource))}
function emailForm(){return {enabled:el('emailEnabled').checked,skipTlsVerify:el('emailSkipTLS').checked,host:el('emailHost').value.trim(),port:Number(el('emailPort').value),username:el('emailUsername').value.trim(),password:el('emailPassword').value,from:el('emailFrom').value.trim(),to:el('emailTo').value.trim(),intervalMinutes:Number(el('emailInterval').value),severities:[...document.querySelectorAll('[data-email-severity]:checked')].map(x=>x.dataset.emailSeverity),sources:[...document.querySelectorAll('[data-email-source]:checked')].map(x=>x.dataset.emailSource)}}
async function loadEmail(){const response=await fetch('/api/settings/email');if(!response.ok)return;const s=await response.json();el('emailEnabled').checked=s.enabled;el('emailSkipTLS').checked=!!s.skipTlsVerify;el('emailHost').value=s.host||'';el('emailPort').value=s.port||587;el('emailUsername').value=s.username||'';el('emailPassword').value='';el('emailPassword').placeholder=s.hasPassword?'Сохранён · не изменять':'Пароль';el('emailFrom').value=s.from||'';el('emailTo').value=s.to||'';el('emailInterval').value=s.intervalMinutes||60;document.querySelectorAll('[data-email-severity]').forEach(x=>x.checked=(s.severities||[]).includes(x.dataset.emailSeverity));document.querySelectorAll('[data-email-source]').forEach(x=>x.checked=(s.sources||[]).includes(x.dataset.emailSource))}
el('saveEmail').onclick=async()=>{const status=el('emailStatus');status.textContent='Сохранение…';const response=await fetch('/api/settings/email',{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify(emailForm())});status.textContent=response.ok?'Настройки сохранены.':await response.text();if(response.ok)loadEmail()};
el('testEmail').onclick=async()=>{const status=el('emailStatus');status.textContent='Отправляем тестовое письмо…';const saved=await fetch('/api/settings/email',{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify(emailForm())});if(!saved.ok){status.textContent=await saved.text();return}const response=await fetch('/api/settings/email/test',{method:'POST'});status.textContent=response.ok?'Тестовое письмо отправлено.':await response.text()};loadEmail().catch(()=>{});
el('installUpdate').onclick=async()=>{const button=el('installUpdate');button.disabled=true;button.textContent='Ставим в очередь…';el('updateMessage').textContent='Создаём запрос на обновление…';try{const response=await fetch('/api/update/request',{method:'POST'});if(!response.ok)throw new Error(await response.text());const status=await response.json();el('updateMessage').textContent=status.message;button.textContent='Обновление выполняется…'}catch(e){button.disabled=false;button.textContent='Попробовать ещё раз';el('updateMessage').textContent='Не удалось запустить обновление.'}};