Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions mail/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,17 +178,20 @@ func SendExternalEmail(displayName, from, to, subject, bodyPlain, bodyHTML strin
// Apply DKIM signing if configured
if dkimConfig != nil {
options := &dkim.SignOptions{
Domain: dkimConfig.Domain,
Selector: dkimConfig.Selector,
Signer: dkimConfig.PrivateKey,
Domain: dkimConfig.Domain,
Selector: dkimConfig.Selector,
Signer: dkimConfig.PrivateKey,
HeaderCanonicalization: dkim.CanonicalizationRelaxed,
BodyCanonicalization: dkim.CanonicalizationRelaxed,
HeaderKeys: []string{"from", "to", "subject", "date", "message-id", "mime-version", "content-type"},
}

var signedBuf bytes.Buffer
if err := dkim.Sign(&signedBuf, bytes.NewReader(message), options); err != nil {
app.Log("dkim", "WARNING: DKIM signing failed: %v", err)
} else {
message = signedBuf.Bytes()
app.Log("dkim", "✓ Email signed with DKIM successfully")
app.Log("dkim", "Signed with DKIM (d=%s s=%s relaxed/relaxed)", dkimConfig.Domain, dkimConfig.Selector)
}
}

Expand Down
43 changes: 34 additions & 9 deletions mail/smtp.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,16 +125,41 @@ func (s *Session) Mail(from string, opts *smtpd.MailOptions) error {

app.Log("mail", "Mail from: %s (IP: %s)", from, s.remoteIP)

// Reject external senders claiming to be from our domain (anti-spoofing)
// Parse and validate the sender address.
fromAddr, _ := mail.ParseAddress(from)
if fromAddr != nil {
fromParts := strings.Split(fromAddr.Address, "@")
if len(fromParts) == 2 && strings.EqualFold(fromParts[1], GetConfiguredDomain()) {
app.Log("mail", "Rejected domain spoofing: external IP %s claiming to send from %s", s.remoteIP, from)
return &smtpd.SMTPError{
Code: 550,
Message: "Sender address rejected: not authorized to send from this domain",
}
if fromAddr == nil {
app.Log("mail", "Rejected invalid sender address: %s", from)
return &smtpd.SMTPError{
Code: 550,
Message: "Sender address rejected: invalid format",
}
}
fromParts := strings.Split(fromAddr.Address, "@")
if len(fromParts) != 2 || fromParts[1] == "" {
app.Log("mail", "Rejected sender without domain: %s", from)
return &smtpd.SMTPError{
Code: 550,
Message: "Sender address rejected: missing domain",
}
}
senderDomain := fromParts[1]

// Reject domains without a dot — "wetransfer" is not an FQDN,
// "wetransfer.com" is. This blocks a common spam pattern.
if !strings.Contains(senderDomain, ".") {
app.Log("mail", "Rejected non-FQDN sender domain: %s (from %s)", senderDomain, from)
return &smtpd.SMTPError{
Code: 550,
Message: "Sender address rejected: domain must be fully qualified",
}
}

// Reject external senders claiming to be from our domain (anti-spoofing)
if strings.EqualFold(senderDomain, GetConfiguredDomain()) {
app.Log("mail", "Rejected domain spoofing: external IP %s claiming to send from %s", s.remoteIP, from)
return &smtpd.SMTPError{
Code: 550,
Message: "Sender address rejected: not authorized to send from this domain",
}
}

Expand Down
Loading