Skip to content

Commit dc4e0b6

Browse files
committed
smtp multiline
1 parent 478d2b6 commit dc4e0b6

File tree

4 files changed

+85
-13
lines changed

4 files changed

+85
-13
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
All notable changes to this project will be documented in this file.
44

5+
## [22.6] - 2026-01-06
6+
7+
### Bug Fixes
8+
9+
- **SMTP Multi-line Banner**: Fixed SMTP connections failing with "EHLO rejected with code: 220" error when server sends multi-line 220 greeting banner (RFC 5321 compliant fix for #183)
10+
511
## [22.5] - 2026-01-06
612

713
### Bug Fixes

config/config.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import (
1414
"github.com/spf13/viper"
1515
)
1616

17-
const VERSION = "22.5"
17+
const VERSION = "22.6"
1818

1919
type Config struct {
2020
Server ServerConfig

internal/service/smtp_service.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,8 @@ func sendRawEmail(host string, port int, username, password string, useTLS bool,
126126
smtpConn := newSMTPConnection(conn)
127127
defer smtpConn.Close()
128128

129-
// Read greeting
130-
code, _, err := smtpConn.readResponse()
129+
// Read greeting (use multiline to handle RFC 5321 multi-line banners - issue #183)
130+
code, err := smtpConn.readMultilineResponse()
131131
if err != nil {
132132
return fmt.Errorf("failed to read greeting: %w", err)
133133
}

internal/service/smtp_service_test.go

Lines changed: 76 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,15 @@ import (
1717

1818
// mockSMTPServer is a test SMTP server that captures commands and messages
1919
type mockSMTPServer struct {
20-
listener net.Listener
21-
mu sync.Mutex
22-
commands []string
23-
messages []capturedMessage
24-
authSuccess bool
25-
closed bool
26-
wg sync.WaitGroup
27-
mailFromCmd string // captures the exact MAIL FROM command
20+
listener net.Listener
21+
mu sync.Mutex
22+
commands []string
23+
messages []capturedMessage
24+
authSuccess bool
25+
closed bool
26+
wg sync.WaitGroup
27+
mailFromCmd string // captures the exact MAIL FROM command
28+
multilineBanner bool // send multi-line 220 banner (RFC 5321 compliant)
2829
}
2930

3031
type capturedMessage struct {
@@ -49,6 +50,26 @@ func newMockSMTPServer(t *testing.T, authSuccess bool) *mockSMTPServer {
4950
return server
5051
}
5152

53+
// newMockSMTPServerWithMultilineBanner creates a mock SMTP server that sends
54+
// a multi-line 220 greeting banner (RFC 5321 Section 4.2 compliant).
55+
// This tests the fix for issue #183.
56+
func newMockSMTPServerWithMultilineBanner(t *testing.T, authSuccess bool) *mockSMTPServer {
57+
listener, err := net.Listen("tcp", "127.0.0.1:0")
58+
require.NoError(t, err)
59+
60+
server := &mockSMTPServer{
61+
listener: listener,
62+
authSuccess: authSuccess,
63+
commands: make([]string, 0),
64+
messages: make([]capturedMessage, 0),
65+
multilineBanner: true,
66+
}
67+
68+
server.wg.Add(1)
69+
go server.serve()
70+
return server
71+
}
72+
5273
func (s *mockSMTPServer) serve() {
5374
defer s.wg.Done()
5475
for {
@@ -73,8 +94,16 @@ func (s *mockSMTPServer) handleConnection(conn net.Conn) {
7394

7495
reader := bufio.NewReader(conn)
7596

76-
// Send greeting
77-
conn.Write([]byte("220 localhost SMTP Mock Server\r\n"))
97+
// Send greeting (multi-line or single-line based on configuration)
98+
if s.multilineBanner {
99+
// RFC 5321 multi-line 220 banner (issue #183)
100+
// Realistic example based on enterprise SMTP relays and ISP servers
101+
conn.Write([]byte("220-mail.example.com ESMTP Postfix\r\n"))
102+
conn.Write([]byte("220-Authorized use only. All activity may be monitored.\r\n"))
103+
conn.Write([]byte("220 Service ready\r\n"))
104+
} else {
105+
conn.Write([]byte("220 localhost SMTP Mock Server\r\n"))
106+
}
78107

79108
var from string
80109
var recipients []string
@@ -315,6 +344,43 @@ func TestSendRawEmail_MultipleRecipients(t *testing.T) {
315344
assert.Len(t, messages[0].recipients, 3)
316345
}
317346

347+
func TestSendRawEmail_MultilineBanner(t *testing.T) {
348+
// Test fix for issue #183: Multi-line 220 banner handling
349+
// RFC 5321 Section 4.2 allows multi-line greetings like:
350+
// 220-smtp.example.com ESMTP
351+
// 220-Additional info
352+
// 220 Service ready
353+
354+
server := newMockSMTPServerWithMultilineBanner(t, true)
355+
defer server.Close()
356+
357+
port := server.Port()
358+
msg := []byte("From: [email protected]\r\nTo: [email protected]\r\nSubject: Test\r\n\r\nTest body")
359+
360+
err := sendRawEmail("127.0.0.1", port, "", "", false, "[email protected]", []string{"[email protected]"}, msg)
361+
require.NoError(t, err, "Should handle multi-line 220 banner without error")
362+
363+
messages := server.GetMessages()
364+
require.Len(t, messages, 1)
365+
assert.Equal(t, "[email protected]", messages[0].from)
366+
assert.Contains(t, messages[0].recipients, "[email protected]")
367+
}
368+
369+
func TestSendRawEmail_MultilineBannerWithAuth(t *testing.T) {
370+
// Test multi-line banner with authentication
371+
server := newMockSMTPServerWithMultilineBanner(t, true)
372+
defer server.Close()
373+
374+
port := server.Port()
375+
msg := []byte("From: [email protected]\r\nTo: [email protected]\r\nSubject: Test\r\n\r\nTest body")
376+
377+
err := sendRawEmail("127.0.0.1", port, "user", "pass", false, "[email protected]", []string{"[email protected]"}, msg)
378+
require.NoError(t, err, "Should handle multi-line 220 banner with auth without error")
379+
380+
messages := server.GetMessages()
381+
require.Len(t, messages, 1)
382+
}
383+
318384
// ============================================================================
319385
// Tests for SMTPService.SendEmail with real message composition
320386
// ============================================================================

0 commit comments

Comments
 (0)