315 lines
9.5 KiB
Go
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,
|
|
})
|
|
}
|