added relay
This commit is contained in:
+50
-22
@@ -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 }
|
||||
|
||||
Reference in New Issue
Block a user