admin dash - added relay
This commit is contained in:
+62
-34
@@ -20,7 +20,7 @@ import (
|
||||
)
|
||||
|
||||
// SubmissionBackend implements gosmtp.Backend for ports 587/465.
|
||||
// Requires authenticated users. Signs outbound mail with DKIM. Queues for delivery.
|
||||
// Supports: regular users, relay accounts (SMTP-only), and unauthenticated IP relay.
|
||||
type SubmissionBackend struct {
|
||||
deps *Deps
|
||||
}
|
||||
@@ -45,39 +45,57 @@ func (b *SubmissionBackend) NewSession(c *gosmtp.Conn) (gosmtp.Session, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SubmissionSession handles one authenticated submission connection.
|
||||
// SubmissionSession handles one authenticated or IP-relay submission connection.
|
||||
type SubmissionSession struct {
|
||||
deps *Deps
|
||||
clientIP string
|
||||
user *models.User // set after AUTH
|
||||
ipRelayMode bool // set when IP relay authorization succeeds (no AUTH)
|
||||
from string
|
||||
rcpts []string
|
||||
deps *Deps
|
||||
clientIP string
|
||||
user *models.User // set after AUTH as regular user
|
||||
relayAccount *models.RelayAccount // set after AUTH as relay account
|
||||
ipRelayMode bool // set when IP relay authorization succeeds (no AUTH)
|
||||
from string
|
||||
rcpts []string
|
||||
}
|
||||
|
||||
func (s *SubmissionSession) AuthPlain(username, password string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Try regular user first.
|
||||
user, err := s.deps.DB.GetUserByEmail(ctx, username)
|
||||
if err != nil {
|
||||
log.Printf("[smtp/submission] auth lookup error %s: %v", username, err)
|
||||
return &gosmtp.SMTPError{Code: 535, Message: "authentication failed"}
|
||||
}
|
||||
if user == nil || !user.Enabled {
|
||||
s.logAttempt(ctx, username, false)
|
||||
return &gosmtp.SMTPError{Code: 535, Message: "authentication failed"}
|
||||
if user != nil && user.Enabled {
|
||||
if err := crypto.CheckPassword(user.PasswordHash, password); err != nil {
|
||||
s.logAttempt(ctx, username, false)
|
||||
log.Printf("[smtp/submission] auth failed for user %s from %s", username, s.clientIP)
|
||||
return &gosmtp.SMTPError{Code: 535, Message: "authentication failed"}
|
||||
}
|
||||
s.user = user
|
||||
s.deps.DB.UpdateLastLogin(ctx, user.ID)
|
||||
log.Printf("[smtp/submission] auth OK for user %s from %s", username, s.clientIP)
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := crypto.CheckPassword(user.PasswordHash, password); err != nil {
|
||||
s.logAttempt(ctx, username, false)
|
||||
log.Printf("[smtp/submission] auth failed for %s from %s", username, s.clientIP)
|
||||
// Try relay account.
|
||||
relayAcc, err := s.deps.DB.GetRelayAccountByUsername(ctx, username)
|
||||
if err != nil {
|
||||
log.Printf("[smtp/submission] relay auth lookup error %s: %v", username, err)
|
||||
return &gosmtp.SMTPError{Code: 535, Message: "authentication failed"}
|
||||
}
|
||||
|
||||
s.user = user
|
||||
s.deps.DB.UpdateLastLogin(ctx, user.ID)
|
||||
log.Printf("[smtp/submission] auth OK for %s from %s", username, s.clientIP)
|
||||
if relayAcc == nil || !relayAcc.Enabled {
|
||||
s.logAttempt(ctx, username, false)
|
||||
log.Printf("[smtp/submission] auth failed for relay %s from %s", username, s.clientIP)
|
||||
return &gosmtp.SMTPError{Code: 535, Message: "authentication failed"}
|
||||
}
|
||||
if err := crypto.CheckPassword(relayAcc.PasswordHash, password); err != nil {
|
||||
s.logAttempt(ctx, username, false)
|
||||
log.Printf("[smtp/submission] auth failed for relay %s from %s", username, s.clientIP)
|
||||
return &gosmtp.SMTPError{Code: 535, Message: "authentication failed"}
|
||||
}
|
||||
s.relayAccount = relayAcc
|
||||
log.Printf("[smtp/submission] relay auth OK for %s from %s", username, s.clientIP)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -88,11 +106,24 @@ func (s *SubmissionSession) Mail(from string, opts *gosmtp.MailOptions) error {
|
||||
}
|
||||
fromEmail := strings.ToLower(addr.Address)
|
||||
|
||||
if s.user == nil {
|
||||
// Unauthenticated — check IP relay rules.
|
||||
if s.user == nil && s.relayAccount == nil {
|
||||
// Unauthenticated — check relay account IP whitelist first, then domain-level IP rules.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
acc, err := s.deps.DB.FindRelayAccountByIP(ctx, s.clientIP, fromEmail)
|
||||
if err != nil {
|
||||
log.Printf("[smtp/submission] relay account ip check from %s: %v", s.clientIP, err)
|
||||
return &gosmtp.SMTPError{Code: 451, EnhancedCode: gosmtp.EnhancedCode{4, 3, 0}, Message: "temporary error"}
|
||||
}
|
||||
if acc != nil {
|
||||
// Treat as authenticated relay account — no password required.
|
||||
s.relayAccount = acc
|
||||
s.from = addr.Address
|
||||
log.Printf("[smtp/submission] ip-relay (account %s) from %s as %s", acc.Username, s.clientIP, fromEmail)
|
||||
return nil
|
||||
}
|
||||
|
||||
allowed, err := s.deps.DB.CheckIPRelay(ctx, s.clientIP, fromEmail)
|
||||
if err != nil {
|
||||
log.Printf("[smtp/submission] ip relay check error from %s: %v", s.clientIP, err)
|
||||
@@ -106,24 +137,24 @@ func (s *SubmissionSession) Mail(from string, opts *gosmtp.MailOptions) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if s.user.IsRelay {
|
||||
// Relay account — validate sender against allowed patterns.
|
||||
if s.relayAccount != nil {
|
||||
// Relay account — validate sender against allowed send-as patterns.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
allowed, err := s.deps.DB.IsRelaySenderAllowed(ctx, s.user.ID, fromEmail)
|
||||
allowed, err := s.deps.DB.IsRelaySenderAllowed(ctx, s.relayAccount.ID, fromEmail)
|
||||
if err != nil {
|
||||
log.Printf("[smtp/submission] relay sender check error for %s: %v", s.user.Email, err)
|
||||
log.Printf("[smtp/submission] relay sender check error for account %d: %v", s.relayAccount.ID, err)
|
||||
return &gosmtp.SMTPError{Code: 451, EnhancedCode: gosmtp.EnhancedCode{4, 3, 0}, Message: "temporary error"}
|
||||
}
|
||||
if !allowed {
|
||||
return &gosmtp.SMTPError{Code: 553, EnhancedCode: gosmtp.EnhancedCode{5, 1, 8}, Message: "sender not permitted for this relay account"}
|
||||
return &gosmtp.SMTPError{Code: 553, EnhancedCode: gosmtp.EnhancedCode{5, 1, 8}, Message: "sender address not permitted for this relay account"}
|
||||
}
|
||||
s.from = addr.Address
|
||||
return nil
|
||||
}
|
||||
|
||||
// Regular user — sender must be own email or an alias.
|
||||
// Regular user — sender must be own email or an alias they own.
|
||||
if !strings.EqualFold(fromEmail, s.user.Email) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
@@ -139,7 +170,7 @@ func (s *SubmissionSession) Mail(from string, opts *gosmtp.MailOptions) error {
|
||||
}
|
||||
|
||||
func (s *SubmissionSession) Rcpt(to string, opts *gosmtp.RcptOptions) error {
|
||||
if s.user == nil && !s.ipRelayMode {
|
||||
if s.user == nil && s.relayAccount == nil && !s.ipRelayMode {
|
||||
return &gosmtp.SMTPError{Code: 530, Message: "authentication required"}
|
||||
}
|
||||
|
||||
@@ -153,7 +184,7 @@ func (s *SubmissionSession) Rcpt(to string, opts *gosmtp.RcptOptions) error {
|
||||
}
|
||||
|
||||
func (s *SubmissionSession) Data(r io.Reader) error {
|
||||
if s.user == nil && !s.ipRelayMode {
|
||||
if s.user == nil && s.relayAccount == nil && !s.ipRelayMode {
|
||||
return &gosmtp.SMTPError{Code: 530, Message: "authentication required"}
|
||||
}
|
||||
if len(s.rcpts) == 0 {
|
||||
@@ -178,7 +209,6 @@ func (s *SubmissionSession) Data(r io.Reader) error {
|
||||
|
||||
senderDomain := domainOf(s.from)
|
||||
raw = s.signDKIM(ctx, raw, senderDomain)
|
||||
|
||||
msgID := extractMsgID(raw)
|
||||
|
||||
dom, err := s.deps.DB.GetDomain(ctx, senderDomain)
|
||||
@@ -205,8 +235,8 @@ func (s *SubmissionSession) Data(r io.Reader) error {
|
||||
log.Printf("[smtp/submission] queued %s → %s", s.from, rcpt)
|
||||
}
|
||||
|
||||
// Save a Sent copy only for regular (non-relay) authenticated users.
|
||||
if s.user != nil && !s.user.IsRelay {
|
||||
// Save Sent copy only for regular users (relay accounts have no mailboxes).
|
||||
if s.user != nil {
|
||||
s.saveSentCopy(ctx, raw)
|
||||
}
|
||||
|
||||
@@ -222,11 +252,10 @@ func (s *SubmissionSession) Reset() {
|
||||
func (s *SubmissionSession) Logout() error { return nil }
|
||||
|
||||
// signDKIM signs the message with the sender domain's DKIM key if available.
|
||||
// Returns the original raw on any error (DKIM is best-effort).
|
||||
func (s *SubmissionSession) signDKIM(ctx context.Context, raw []byte, senderDomain string) []byte {
|
||||
dom, err := s.deps.DB.GetDomain(ctx, senderDomain)
|
||||
if err != nil || dom == nil || dom.DKIMPrivateEnc == nil {
|
||||
return raw // no key configured
|
||||
return raw
|
||||
}
|
||||
|
||||
privPEM, err := s.deps.Crypt.DecryptGlobal("dkim", dom.DKIMPrivateEnc)
|
||||
@@ -247,7 +276,6 @@ func (s *SubmissionSession) signDKIM(ctx context.Context, raw []byte, senderDoma
|
||||
return raw
|
||||
}
|
||||
|
||||
// Prepend DKIM-Signature header.
|
||||
return append([]byte(header+"\r\n"), raw...)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user