MFA fix, added IP blacklist, update webmail client
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"github.com/emersion/go-smtp"
|
||||
"gopkg.in/ini.v1"
|
||||
"mailgoserver/internal/abuseguard"
|
||||
"mailgoserver/internal/db"
|
||||
"mailgoserver/internal/dkim"
|
||||
"mailgoserver/internal/mailstore"
|
||||
@@ -122,6 +123,7 @@ func (s *Session) validateSenderAuthorization(mailFrom string) (accept, authoriz
|
||||
return true, true, fmt.Sprintf("Sender authorized to send as %s", mailFrom)
|
||||
}
|
||||
_ = s.backend.DB.LogAuthAttempt("sender_validation", fmt.Sprintf("%s -> %s", sender.Email, mailFrom), s.peerIP, false, "")
|
||||
abuseguard.RecordFailureAndMaybeBlacklist(s.backend.DB, s.backend.Cfg, s.backend.Logger, s.peerIP)
|
||||
return false, false, fmt.Sprintf("Sender %s not authorized to send as %s", sender.Email, mailFrom)
|
||||
}
|
||||
|
||||
@@ -137,6 +139,7 @@ func (s *Session) validateSenderAuthorization(mailFrom string) (accept, authoriz
|
||||
return true, true, fmt.Sprintf("Mailbox authorized to send as alias %s", mailFrom)
|
||||
}
|
||||
_ = s.backend.DB.LogAuthAttempt("mailbox_validation", fmt.Sprintf("%s -> %s", mbox.Email, mailFrom), s.peerIP, false, "")
|
||||
abuseguard.RecordFailureAndMaybeBlacklist(s.backend.DB, s.backend.Cfg, s.backend.Logger, s.peerIP)
|
||||
return false, false, fmt.Sprintf("Mailbox %s not authorized to send as %s", mbox.Email, mailFrom)
|
||||
}
|
||||
|
||||
@@ -153,6 +156,7 @@ func (s *Session) validateSenderAuthorization(mailFrom string) (accept, authoriz
|
||||
return true, true, fmt.Sprintf("IP authorized for domain %s", fromDomain)
|
||||
}
|
||||
_ = s.backend.DB.LogAuthAttempt("ip", fmt.Sprintf("%s -> %s", s.peerIP, fromDomain), s.peerIP, false, fmt.Sprintf("IP %s not authorized for domain %s", s.peerIP, fromDomain))
|
||||
abuseguard.RecordFailureAndMaybeBlacklist(s.backend.DB, s.backend.Cfg, s.backend.Logger, s.peerIP)
|
||||
return false, false, fmt.Sprintf("Not authorized to send for domain %s", fromDomain)
|
||||
}
|
||||
|
||||
@@ -370,39 +374,60 @@ func (s *Session) deliverLocally(rcpts, types []string, signedContent, messageID
|
||||
results := make([]relay.Result, 0, len(rcpts))
|
||||
for i, rcpt := range rcpts {
|
||||
mbox := s.localMailboxes[strings.ToLower(rcpt)]
|
||||
folder := "INBOX"
|
||||
markRead := false
|
||||
|
||||
// An explicit per-mailbox allow-list entry bypasses spam scoring entirely —
|
||||
// the built-in heuristic and optional rspamd check both run regardless of each
|
||||
// other (additive, not either/or), but neither runs at all once allow-listed.
|
||||
spamGated := false
|
||||
if allowed, _ := s.backend.DB.IsAllowed(mbox.ID, s.mailFrom); !allowed {
|
||||
reject := heuristicScore >= rejectScore
|
||||
if !reject && rspamdEnabled {
|
||||
if score, action, err := mailstore.CheckRspamd(rspamdURL, []byte(signedContent), s.mailFrom, rcpt); err == nil {
|
||||
if action == "reject" || score >= float64(rspamdRejectScore) {
|
||||
reject = true
|
||||
quarantine := heuristicScore >= rejectScore
|
||||
hardReject := false
|
||||
if rspamdEnabled {
|
||||
if score, rAction, err := mailstore.CheckRspamd(rspamdURL, []byte(signedContent), s.mailFrom, rcpt); err == nil {
|
||||
// rspamd's own "reject" action is a considered policy decision
|
||||
// (DNSBL hit, greylisting, etc.) worth still hard-rejecting at
|
||||
// SMTP time to avoid backscatter; a bare score threshold hit
|
||||
// (from either scorer) is quarantined instead of rejected, so a
|
||||
// false positive is recoverable from the Spam folder rather than
|
||||
// silently bounced with no trace.
|
||||
if rAction == "reject" {
|
||||
hardReject = true
|
||||
} else if score >= float64(rspamdRejectScore) {
|
||||
quarantine = true
|
||||
}
|
||||
}
|
||||
// rspamd unreachable/erroring must not block mail — errors are swallowed,
|
||||
// the built-in heuristic above is still the baseline gate either way.
|
||||
}
|
||||
if reject {
|
||||
if hardReject {
|
||||
results = append(results, relay.Result{Recipient: rcpt, RecipientType: types[i], Status: "failed", ErrorCode: "550", ErrorMessage: "Message rejected as spam"})
|
||||
continue
|
||||
}
|
||||
if quarantine {
|
||||
folder = "Spam"
|
||||
spamGated = true
|
||||
}
|
||||
}
|
||||
|
||||
action, err := s.backend.Mailstore.ApplyRules(mbox.ID, map[string]string{"from": s.mailFrom, "to": rcpt, "subject": subject})
|
||||
if err != nil {
|
||||
results = append(results, relay.Result{Recipient: rcpt, RecipientType: types[i], Status: "failed", ErrorCode: "450", ErrorMessage: err.Error()})
|
||||
continue
|
||||
}
|
||||
if action.Drop {
|
||||
results = append(results, relay.Result{Recipient: rcpt, RecipientType: types[i], Status: "success", ServerResponse: "Discarded by filter rule"})
|
||||
continue
|
||||
}
|
||||
folder := "INBOX"
|
||||
if action.Folder != "" {
|
||||
folder = action.Folder
|
||||
// Filter rules organize legitimate mail the recipient already trusts arriving
|
||||
// in their INBOX — a quarantined message skips them entirely and always lands
|
||||
// in Spam, rather than a rule accidentally routing spam back into view.
|
||||
if !spamGated {
|
||||
action, err := s.backend.Mailstore.ApplyRules(mbox.ID, map[string]string{"from": s.mailFrom, "to": rcpt, "subject": subject})
|
||||
if err != nil {
|
||||
results = append(results, relay.Result{Recipient: rcpt, RecipientType: types[i], Status: "failed", ErrorCode: "450", ErrorMessage: err.Error()})
|
||||
continue
|
||||
}
|
||||
if action.Drop {
|
||||
results = append(results, relay.Result{Recipient: rcpt, RecipientType: types[i], Status: "success", ServerResponse: "Discarded by filter rule"})
|
||||
continue
|
||||
}
|
||||
if action.Folder != "" {
|
||||
folder = action.Folder
|
||||
}
|
||||
markRead = action.MarkRead
|
||||
}
|
||||
|
||||
uid, err := s.backend.Mailstore.StoreMessage(mbox.ID, folder, []byte(signedContent), messageID, s.mailFrom, subject)
|
||||
@@ -414,12 +439,16 @@ func (s *Session) deliverLocally(rcpts, types []string, signedContent, messageID
|
||||
results = append(results, relay.Result{Recipient: rcpt, RecipientType: types[i], Status: "failed", ErrorCode: errCode, ErrorMessage: errMsg})
|
||||
continue
|
||||
}
|
||||
if action.MarkRead {
|
||||
if markRead {
|
||||
if err := s.backend.DB.SetMessageFlags(mbox.ID, uid, `\Seen`); err != nil {
|
||||
s.backend.Logger.Error("mark_read rule failed to set flag for message %d: %v", uid, err)
|
||||
}
|
||||
}
|
||||
results = append(results, relay.Result{Recipient: rcpt, RecipientType: types[i], Status: "success", ServerResponse: "Delivered to local mailbox"})
|
||||
serverResponse := "Delivered to local mailbox"
|
||||
if spamGated {
|
||||
serverResponse = "Quarantined to Spam folder"
|
||||
}
|
||||
results = append(results, relay.Result{Recipient: rcpt, RecipientType: types[i], Status: "success", ServerResponse: serverResponse})
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user