Files
2026-08-12 12:56:22 +01:00

315 lines
9.5 KiB
Go

package webui
import (
"encoding/json"
"net/http"
"regexp"
"strings"
"time"
"mailgoserver/internal/db"
"mailgoserver/internal/dkim"
)
func writeJSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(v)
}
func isAjax(r *http.Request) bool {
return r.Header.Get("X-Requested-With") == "XMLHttpRequest" || strings.Contains(r.Header.Get("Content-Type"), "application/json")
}
// dkimList mirrors dkim.py's dkim_list().
func (a *App) dkimList(w http.ResponseWriter, r *http.Request) {
active, err := a.DB.ListActiveDKIMKeysWithDomain()
if err != nil {
setFlash(w, "error", "Error loading DKIM keys")
}
inactive, _ := a.DB.ListInactiveDKIMKeysWithDomain()
publicIP := getPublicIP(a.Cfg)
scope := scopeFromContext(r)
var dkimData []M
for _, k := range active {
if !scope.Allowed(k.DomainID) {
continue
}
rec, _ := a.DKIM.GetDKIMPublicKeyRecord(k.DomainName)
if rec == nil {
rec = &dkim.DNSRecord{}
}
spfCheck := checkDNSRecord(k.DomainName)
existingSPF := ""
for _, txt := range spfCheck.Records {
if strings.Contains(txt, "v=spf1") {
existingSPF = txt
break
}
}
dkimData = append(dkimData, M{
"domain": M{"id": k.DomainID, "domain_name": k.DomainName},
"dkim_key": k.DKIMKey,
"dns_record": M{"name": rec.Name, "value": rec.Value},
"existing_spf": existingSPF,
"recommended_spf": generateSPFRecord(publicIP, existingSPF),
"public_ip": publicIP,
})
}
var oldData []M
for _, k := range inactive {
if !scope.Allowed(k.DomainID) {
continue
}
status := "Disabled"
if k.ReplacedAt != nil {
status = "Replaced"
}
oldData = append(oldData, M{
"domain": M{"id": k.DomainID, "domain_name": k.DomainName},
"dkim_key": k.DKIMKey,
"status_text": status,
})
}
a.render(w, r, "dkim.html", M{"active": "dkim", "dkim_data": dkimData, "old_dkim_data": oldData})
}
// createDKIM mirrors dkim.py's create_dkim().
func (a *App) createDKIM(w http.ResponseWriter, r *http.Request) {
domain := r.FormValue("domain")
selector := r.FormValue("selector")
if domain == "" {
writeJSON(w, http.StatusBadRequest, M{"success": false, "message": "Domain is required"})
return
}
dom, err := a.DB.GetDomainByNameExact(domain)
if err != nil || dom == nil {
writeJSON(w, http.StatusNotFound, M{"success": false, "message": "Domain not found"})
return
}
if !scopeFromContext(r).Allowed(dom.ID) {
writeJSON(w, http.StatusNotFound, M{"success": false, "message": "Domain not found"})
return
}
if err := a.DB.DeactivateActiveDKIMKeysForDomain(dom.ID, time.Now()); err != nil {
writeJSON(w, http.StatusInternalServerError, M{"success": false, "message": "Failed to create DKIM key"})
return
}
ok, err := a.DKIM.GenerateDKIMKeypair(domain, selector, true)
if err != nil || !ok {
writeJSON(w, http.StatusInternalServerError, M{"success": false, "message": "Failed to create DKIM key"})
return
}
writeJSON(w, http.StatusOK, M{"success": true, "message": "DKIM key created successfully"})
}
// regenerateDKIM mirrors dkim.py's regenerate_dkim() — path id is a DOMAIN id.
func (a *App) regenerateDKIM(w http.ResponseWriter, r *http.Request) {
domainID := pathID(r)
dom, err := a.DB.GetDomainByID(domainID)
if err != nil || dom == nil {
http.NotFound(w, r)
return
}
if !requireDomainAccess(w, r, dom.ID) {
return
}
current, _ := a.DB.GetActiveDKIMKeyByDomainID(domainID)
selector := ""
if current != nil {
selector = current.Selector
}
if err := a.DB.DeactivateActiveDKIMKeysForDomain(domainID, time.Now()); err != nil {
a.dkimActionFailed(w, r, "Failed to regenerate DKIM key")
return
}
ok, err := a.DKIM.GenerateDKIMKeypair(dom.DomainName, selector, true)
if err != nil || !ok {
a.dkimActionFailed(w, r, "Failed to regenerate DKIM key")
return
}
if isAjax(r) {
publicIP := getPublicIP(a.Cfg)
rec, _ := a.DKIM.GetDKIMPublicKeyRecord(dom.DomainName)
spfCheck := checkDNSRecord(dom.DomainName)
existingSPF := ""
for _, txt := range spfCheck.Records {
if strings.Contains(txt, "v=spf1") {
existingSPF = txt
break
}
}
newKey, _ := a.DB.GetActiveDKIMKeyByDomainID(domainID)
writeJSON(w, http.StatusOK, M{
"success": true, "message": "DKIM key regenerated successfully",
"new_key": newKey, "dns_record": rec, "existing_spf": existingSPF,
"recommended_spf": generateSPFRecord(publicIP, existingSPF),
"public_ip": publicIP, "domain": dom.DomainName,
})
return
}
setFlash(w, "success", "DKIM key regenerated successfully")
http.Redirect(w, r, Prefix+"/dkim", http.StatusFound)
}
func (a *App) dkimActionFailed(w http.ResponseWriter, r *http.Request, message string) {
if isAjax(r) {
writeJSON(w, http.StatusInternalServerError, M{"success": false, "message": message})
return
}
setFlash(w, "error", message)
http.Redirect(w, r, Prefix+"/dkim", http.StatusFound)
}
func (a *App) editDKIMForm(w http.ResponseWriter, r *http.Request) {
key, ok := a.dkimKeyWithAccess(w, r)
if !ok {
return
}
dom, _ := a.DB.GetDomainByID(key.DomainID)
a.render(w, r, "edit_dkim.html", M{"active": "dkim", "dkim_key": key, "domain": dom})
}
var selectorPattern = regexp.MustCompile(`^[a-zA-Z0-9_-]+$`)
// dkimKeyWithAccess fetches a DKIM key by path ID and confirms it belongs to a domain
// the current admin can manage.
func (a *App) dkimKeyWithAccess(w http.ResponseWriter, r *http.Request) (key *db.DKIMKey, ok bool) {
key, err := a.DB.GetDKIMKeyByID(pathID(r))
if err != nil || key == nil {
http.NotFound(w, r)
return nil, false
}
if !requireDomainAccess(w, r, key.DomainID) {
return nil, false
}
return key, true
}
// editDKIM mirrors dkim.py's edit_dkim() POST branch.
func (a *App) editDKIM(w http.ResponseWriter, r *http.Request) {
key, ok := a.dkimKeyWithAccess(w, r)
if !ok {
return
}
id := key.ID
dom, _ := a.DB.GetDomainByID(key.DomainID)
selector := strings.TrimSpace(r.FormValue("selector"))
if selector == "" || !selectorPattern.MatchString(selector) {
setFlash(w, "error", "Selector must contain only letters, numbers, hyphens, and underscores")
a.render(w, r, "edit_dkim.html", M{"active": "dkim", "dkim_key": key, "domain": dom})
return
}
if exists, _ := a.DB.SelectorExistsForDomain(key.DomainID, selector, id); exists {
setFlash(w, "error", "This selector is already in use for this domain")
a.render(w, r, "edit_dkim.html", M{"active": "dkim", "dkim_key": key, "domain": dom})
return
}
if err := a.DB.UpdateDKIMKeySelector(id, selector); err != nil {
setFlash(w, "error", "Error updating selector")
http.Redirect(w, r, Prefix+"/dkim", http.StatusFound)
return
}
setFlash(w, "success", "DKIM selector updated successfully")
http.Redirect(w, r, Prefix+"/dkim", http.StatusFound)
}
// toggleDKIM mirrors dkim.py's toggle_dkim().
func (a *App) toggleDKIM(w http.ResponseWriter, r *http.Request) {
key, ok := a.dkimKeyWithAccess(w, r)
if !ok {
return
}
id := key.ID
newActive := !key.IsActive
if newActive {
if err := a.DB.DeactivateActiveDKIMKeysForDomain(key.DomainID, time.Now()); err != nil {
a.dkimActionFailed(w, r, "Error toggling DKIM status")
return
}
}
if err := a.DB.SetDKIMKeyActive(id, newActive, time.Now()); err != nil {
a.dkimActionFailed(w, r, "Error toggling DKIM status")
return
}
message := "DKIM key disabled"
if newActive {
message = "DKIM key enabled"
}
if isAjax(r) {
writeJSON(w, http.StatusOK, M{"success": true, "message": message, "is_active": newActive})
return
}
setFlash(w, "success", message)
http.Redirect(w, r, Prefix+"/dkim", http.StatusFound)
}
func (a *App) removeDKIM(w http.ResponseWriter, r *http.Request) {
key, ok := a.dkimKeyWithAccess(w, r)
if !ok {
return
}
if err := a.DB.RemoveDKIMKey(key.ID); err != nil {
setFlash(w, "error", "Error removing DKIM key")
} else {
setFlash(w, "success", "DKIM key permanently removed")
}
http.Redirect(w, r, Prefix+"/dkim", http.StatusFound)
}
// checkDKIMDNS mirrors dkim.py's check_dkim_dns().
func (a *App) checkDKIMDNS(w http.ResponseWriter, r *http.Request) {
domain := r.FormValue("domain")
selector := r.FormValue("selector")
rec, err := a.DKIM.GetDKIMPublicKeyRecord(domain)
if err != nil || rec == nil {
writeJSON(w, http.StatusOK, M{"success": false, "message": "No active DKIM key for domain"})
return
}
dnsName := selector + "._domainkey." + domain
result := checkDNSRecord(dnsName)
found := false
expected := strings.Trim(rec.Value, `"`)
for _, txt := range result.Records {
if strings.Contains(txt, expected) || strings.Contains(expected, txt) {
found = true
break
}
}
message := "DKIM record not found or does not match"
if found {
message = "DKIM record found and matches"
} else if !result.Success {
message = result.Message
}
writeJSON(w, http.StatusOK, M{"success": found, "message": message, "records": result.Records})
}
// checkSPFDNS mirrors dkim.py's check_spf_dns().
func (a *App) checkSPFDNS(w http.ResponseWriter, r *http.Request) {
domain := r.FormValue("domain")
result := checkDNSRecord(domain)
var spfRecord string
for _, txt := range result.Records {
if strings.Contains(txt, "v=spf1") {
spfRecord = txt
break
}
}
publicIP := getPublicIP(a.Cfg)
validForServer := spfRecord != "" && strings.Contains(spfRecord, "ip4:"+publicIP)
message := "SPF record not found"
if spfRecord != "" {
message = "SPF record found"
}
writeJSON(w, http.StatusOK, M{
"success": spfRecord != "", "message": message, "records": result.Records,
"spf_record": spfRecord, "spf_valid_for_server": validForServer,
"spf_check_message": message, "public_ip": publicIP,
})
}