added relay

This commit is contained in:
2026-05-25 14:30:41 +00:00
parent 063b3b643f
commit 3d46ccde33
12 changed files with 599 additions and 37 deletions
+50 -22
View File
@@ -47,11 +47,12 @@ func (b *SubmissionBackend) NewSession(c *gosmtp.Conn) (gosmtp.Session, error) {
// SubmissionSession handles one authenticated submission connection.
type SubmissionSession struct {
deps *Deps
clientIP string
user *models.User // set after AUTH
from string
rcpts []string
deps *Deps
clientIP string
user *models.User // set after AUTH
ipRelayMode bool // set when IP relay authorization succeeds (no AUTH)
from string
rcpts []string
}
func (s *SubmissionSession) AuthPlain(username, password string) error {
@@ -81,19 +82,49 @@ func (s *SubmissionSession) AuthPlain(username, password string) error {
}
func (s *SubmissionSession) Mail(from string, opts *gosmtp.MailOptions) error {
if s.user == nil {
return &gosmtp.SMTPError{Code: 530, Message: "authentication required"}
}
addr, err := mail.ParseAddress(from)
if err != nil {
return &gosmtp.SMTPError{Code: 501, EnhancedCode: gosmtp.EnhancedCode{5, 1, 7}, Message: "invalid sender"}
}
// Sender must be user's own address or an alias they own.
fromEmail := strings.ToLower(addr.Address)
if s.user == nil {
// Unauthenticated — check IP relay rules.
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
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)
return &gosmtp.SMTPError{Code: 451, EnhancedCode: gosmtp.EnhancedCode{4, 3, 0}, Message: "temporary error"}
}
if !allowed {
return &gosmtp.SMTPError{Code: 530, Message: "authentication required"}
}
s.ipRelayMode = true
s.from = addr.Address
return nil
}
if s.user.IsRelay {
// Relay account — validate sender against allowed patterns.
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
allowed, err := s.deps.DB.IsRelaySenderAllowed(ctx, s.user.ID, fromEmail)
if err != nil {
log.Printf("[smtp/submission] relay sender check error for %s: %v", s.user.Email, 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"}
}
s.from = addr.Address
return nil
}
// Regular user — sender must be own email or an alias.
if !strings.EqualFold(fromEmail, s.user.Email) {
// Check aliases.
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
@@ -108,7 +139,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 {
if s.user == nil && !s.ipRelayMode {
return &gosmtp.SMTPError{Code: 530, Message: "authentication required"}
}
@@ -122,7 +153,7 @@ func (s *SubmissionSession) Rcpt(to string, opts *gosmtp.RcptOptions) error {
}
func (s *SubmissionSession) Data(r io.Reader) error {
if s.user == nil {
if s.user == nil && !s.ipRelayMode {
return &gosmtp.SMTPError{Code: 530, Message: "authentication required"}
}
if len(s.rcpts) == 0 {
@@ -137,7 +168,6 @@ func (s *SubmissionSession) Data(r io.Reader) error {
return &gosmtp.SMTPError{Code: 552, EnhancedCode: gosmtp.EnhancedCode{5, 3, 4}, Message: "message too large"}
}
// Parse for basic header validation.
_, err = mail.ReadMessage(bytes.NewReader(raw))
if err != nil {
return &gosmtp.SMTPError{Code: 550, EnhancedCode: gosmtp.EnhancedCode{5, 6, 0}, Message: "malformed message"}
@@ -146,22 +176,17 @@ func (s *SubmissionSession) Data(r io.Reader) error {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// DKIM-sign the message if the sender's domain has keys configured.
senderDomain := domainOf(s.from)
raw = s.signDKIM(ctx, raw, senderDomain)
msgID := extractMsgID(raw)
// Queue each recipient for delivery.
// For local recipients we could deliver directly, but queuing is simpler and
// provides a consistent audit trail.
dom, err := s.deps.DB.GetDomain(ctx, senderDomain)
var domainID int64
if err == nil && dom != nil {
domainID = dom.ID
}
// Encrypt raw for queue storage using a global (non-user) key.
queueKey, err := s.deps.Crypt.DeriveKeyGlobal("queue")
if err != nil {
return fmt.Errorf("queue key: %w", err)
@@ -180,8 +205,10 @@ func (s *SubmissionSession) Data(r io.Reader) error {
log.Printf("[smtp/submission] queued %s → %s", s.from, rcpt)
}
// Also save a copy in sender's Sent folder.
s.saveSentCopy(ctx, raw)
// Save a Sent copy only for regular (non-relay) authenticated users.
if s.user != nil && !s.user.IsRelay {
s.saveSentCopy(ctx, raw)
}
return nil
}
@@ -189,6 +216,7 @@ func (s *SubmissionSession) Data(r io.Reader) error {
func (s *SubmissionSession) Reset() {
s.from = ""
s.rcpts = s.rcpts[:0]
s.ipRelayMode = false
}
func (s *SubmissionSession) Logout() error { return nil }