From 6684bd676b01b9fbd260ea0e79ad91828dcfd38b Mon Sep 17 00:00:00 2001 From: nanookclaw Date: Sat, 16 May 2026 05:19:22 +0000 Subject: [PATCH] fix: SMTP HELO hostname (#1284) ## What? Fixes #1084 by using the local OS hostname for the SMTP `HELO`/`EHLO` greeting instead of always sending `localhost`. Both normal message sending and calendar replies now call a shared helper that returns `os.Hostname()` when available, with `localhost` kept as the fallback for hostname lookup failures or empty hostnames. ## Why? Some SMTP anti-spam checks reject or penalize public clients that identify as `localhost`. Sending the real client hostname matches the issue's requested behavior while preserving a safe fallback if hostname lookup is unavailable. --------- Signed-off-by: Nanook --- sender/sender.go | 19 ++++++++++++++++--- sender/sender_test.go | 20 ++++++++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/sender/sender.go b/sender/sender.go index 824a40fb3b62f6524fceebe2e9aa1351043bd6f7..4eea308d6edd3beafcab863d0143b05c8621ff24 100644 --- a/sender/sender.go +++ b/sender/sender.go @@ -79,7 +79,10 @@ func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) { // randReader is the source of randomness for boundary generation. It is a // variable so tests can swap it with a deterministic or failing reader. By // default it is crypto/rand.Reader. -var randReader io.Reader = rand.Reader +var ( + randReader io.Reader = rand.Reader + osHostname = os.Hostname +) // smimeOuterBoundary returns a fresh, high-entropy MIME boundary for an S/MIME // multipart/signed wrapper. If crypto/rand cannot supply randomness it returns @@ -92,6 +95,16 @@ func smimeOuterBoundary() (string, error) { return "signed-" + fmt.Sprintf("%x", rb[:]), nil } +// smtpHelloHostname returns the hostname used in the SMTP HELO/EHLO greeting. +// It falls back to localhost when the OS hostname cannot be read. +func smtpHelloHostname() string { + hostname, err := osHostname() + if err != nil || strings.TrimSpace(hostname) == "" { + return "localhost" + } + return hostname +} + // generateMessageID creates a unique Message-ID header. func generateMessageID(from string) string { buf := make([]byte, 16) @@ -672,7 +685,7 @@ func SendEmail(account *config.Account, to, cc, bcc []string, subject, plainBody } defer c.Close() - if err = c.Hello("localhost"); err != nil { + if err = c.Hello(smtpHelloHostname()); err != nil { return nil, err } @@ -884,7 +897,7 @@ func SendCalendarReply(account *config.Account, to []string, subject, plainBody } defer c.Close() - if err = c.Hello("localhost"); err != nil { + if err = c.Hello(smtpHelloHostname()); err != nil { return nil, err } diff --git a/sender/sender_test.go b/sender/sender_test.go index 61eb95ec34a0753a86d7283d63eaccb77a6b2317..d3fc7542fb91e1d89f603fa9b1df8d48de890ffc 100644 --- a/sender/sender_test.go +++ b/sender/sender_test.go @@ -56,6 +56,26 @@ func TestSMIMEOuterBoundary_Success(t *testing.T) { // Ensure io is referenced even if a future refactor removes it indirectly. var _ io.Reader = failingReader{} +func TestSMTPHelloHostname(t *testing.T) { + orig := osHostname + t.Cleanup(func() { osHostname = orig }) + + osHostname = func() (string, error) { return "mail.example.com", nil } + if got := smtpHelloHostname(); got != "mail.example.com" { + t.Fatalf("expected hostname, got %q", got) + } + + osHostname = func() (string, error) { return "", nil } + if got := smtpHelloHostname(); got != "localhost" { + t.Fatalf("expected localhost fallback for empty hostname, got %q", got) + } + + osHostname = func() (string, error) { return "ignored", errors.New("hostname unavailable") } + if got := smtpHelloHostname(); got != "localhost" { + t.Fatalf("expected localhost fallback on error, got %q", got) + } +} + // TestGenerateMessageID ensures the Message-ID has the correct format. func TestGenerateMessageID(t *testing.T) { from := "test@example.com"