client_test.go raw
1 package smtp
2
3 import (
4 "context"
5 "net"
6 "strings"
7 "testing"
8 "time"
9
10 gosmtp "github.com/emersion/go-smtp"
11 )
12
13 func TestNewClient(t *testing.T) {
14 c := NewClient(ClientConfig{FromDomain: "example.com"})
15 if c == nil {
16 t.Fatal("expected non-nil Client")
17 }
18 if c.resolver == nil {
19 t.Error("expected non-nil resolver")
20 }
21 if c.dialer == nil {
22 t.Error("expected non-nil dialer")
23 }
24 }
25
26 func TestGroupByDomain(t *testing.T) {
27 result := groupByDomain([]string{
28 "alice@example.com",
29 "bob@example.com",
30 "charlie@other.com",
31 "invalid-no-at",
32 })
33 if len(result) != 2 {
34 t.Fatalf("expected 2 domains, got %d", len(result))
35 }
36 if len(result["example.com"]) != 2 {
37 t.Errorf("example.com count = %d, want 2", len(result["example.com"]))
38 }
39 if len(result["other.com"]) != 1 {
40 t.Errorf("other.com count = %d, want 1", len(result["other.com"]))
41 }
42 }
43
44 func TestGroupByDomain_CaseInsensitive(t *testing.T) {
45 result := groupByDomain([]string{
46 "alice@Example.COM",
47 "bob@EXAMPLE.com",
48 })
49 if len(result) != 1 {
50 t.Errorf("expected 1 domain, got %d", len(result))
51 }
52 }
53
54 func TestGroupByDomain_Empty(t *testing.T) {
55 result := groupByDomain(nil)
56 if len(result) != 0 {
57 t.Errorf("expected 0 domains, got %d", len(result))
58 }
59 }
60
61 func TestBuildMessage(t *testing.T) {
62 c := NewClient(ClientConfig{FromDomain: "bridge.example.com"})
63 email := &OutboundEmail{
64 From: "npub1abc@bridge.example.com",
65 To: []string{"alice@example.com"},
66 Subject: "Test Subject",
67 Body: "Hello, world!",
68 }
69 msg, err := c.buildMessage(email)
70 if err != nil {
71 t.Fatalf("unexpected error: %v", err)
72 }
73 s := string(msg)
74 if !strings.Contains(s, "From: npub1abc@bridge.example.com") {
75 t.Error("missing From header")
76 }
77 if !strings.Contains(s, "To: alice@example.com") {
78 t.Error("missing To header")
79 }
80 if !strings.Contains(s, "Subject: Test Subject") {
81 t.Error("missing Subject header")
82 }
83 if !strings.Contains(s, "Hello, world!") {
84 t.Error("missing body")
85 }
86 if !strings.Contains(s, "Message-Id:") {
87 t.Error("missing Message-Id")
88 }
89 if !strings.Contains(s, "Mime-Version: 1.0") {
90 t.Error("missing Mime-Version header")
91 }
92 }
93
94 func TestBuildMessage_WithCC(t *testing.T) {
95 c := NewClient(ClientConfig{FromDomain: "bridge.example.com"})
96 email := &OutboundEmail{
97 From: "from@bridge.example.com",
98 To: []string{"alice@example.com"},
99 Cc: []string{"bob@example.com"},
100 Subject: "CC Test",
101 Body: "Body",
102 }
103 msg, err := c.buildMessage(email)
104 if err != nil {
105 t.Fatalf("unexpected error: %v", err)
106 }
107 if !strings.Contains(string(msg), "Cc: bob@example.com") {
108 t.Error("missing Cc header")
109 }
110 }
111
112 func TestBuildMessage_BccNotInHeaders(t *testing.T) {
113 c := NewClient(ClientConfig{FromDomain: "bridge.example.com"})
114 email := &OutboundEmail{
115 From: "from@bridge.example.com",
116 To: []string{"alice@example.com"},
117 Bcc: []string{"secret@example.com"},
118 Subject: "BCC Test",
119 Body: "Body",
120 }
121 msg, err := c.buildMessage(email)
122 if err != nil {
123 t.Fatalf("unexpected error: %v", err)
124 }
125 if strings.Contains(string(msg), "secret@example.com") {
126 t.Error("BCC address should NOT appear in headers")
127 }
128 }
129
130 func TestClient_SendViaLocalServer(t *testing.T) {
131 // Start a local SMTP server
132 var received *InboundEmail
133 server := NewServer(ServerConfig{
134 Domain: "test.example.com",
135 ListenAddr: "127.0.0.1:0",
136 }, func(email *InboundEmail) error {
137 received = email
138 return nil
139 })
140
141 if err := server.Start(); err != nil {
142 t.Fatalf("start server: %v", err)
143 }
144 defer server.Stop(context.Background())
145
146 addr := server.Addr().String()
147 _, port, _ := net.SplitHostPort(addr)
148
149 // Create client with mock resolver pointing to local server
150 client := NewClient(ClientConfig{FromDomain: "bridge.example.com"})
151 client.SetResolver(func(domain string) ([]*net.MX, error) {
152 return []*net.MX{{Host: "127.0.0.1", Pref: 10}}, nil
153 })
154 client.SetDialer(func(a string) (*gosmtp.Client, error) {
155 // Override to connect to our local port
156 return gosmtpDial("127.0.0.1:" + port)
157 })
158
159 email := &OutboundEmail{
160 From: "sender@bridge.example.com",
161 To: []string{"recipient@test.example.com"},
162 Subject: "Integration Test",
163 Body: "This is a test email.",
164 }
165
166 err := client.Send(email)
167 if err != nil {
168 t.Fatalf("Send failed: %v", err)
169 }
170
171 // Wait briefly for async processing
172 time.Sleep(100 * time.Millisecond)
173
174 if received == nil {
175 t.Fatal("expected server to receive email")
176 }
177 if received.From != "sender@bridge.example.com" {
178 t.Errorf("From = %q, want sender@bridge.example.com", received.From)
179 }
180 }
181
182 func TestClient_SendWithDKIM(t *testing.T) {
183 var received *InboundEmail
184 server := NewServer(ServerConfig{
185 Domain: "test.example.com",
186 ListenAddr: "127.0.0.1:0",
187 }, func(email *InboundEmail) error {
188 received = email
189 return nil
190 })
191
192 if err := server.Start(); err != nil {
193 t.Fatalf("start server: %v", err)
194 }
195 defer server.Stop(context.Background())
196
197 addr := server.Addr().String()
198 _, port, _ := net.SplitHostPort(addr)
199
200 // Generate a DKIM key for signing
201 keyPEM, _, err := GenerateDKIMKeyPair()
202 if err != nil {
203 t.Fatalf("generate DKIM: %v", err)
204 }
205 signer, err := NewDKIMSignerFromPEM("bridge.example.com", "test", keyPEM)
206 if err != nil {
207 t.Fatalf("create signer: %v", err)
208 }
209
210 client := NewClient(ClientConfig{
211 FromDomain: "bridge.example.com",
212 DKIMSigner: signer,
213 })
214 client.SetResolver(func(domain string) ([]*net.MX, error) {
215 return []*net.MX{{Host: "127.0.0.1", Pref: 10}}, nil
216 })
217 client.SetDialer(func(a string) (*gosmtp.Client, error) {
218 return gosmtpDial("127.0.0.1:" + port)
219 })
220
221 err = client.Send(&OutboundEmail{
222 From: "sender@bridge.example.com",
223 To: []string{"recipient@test.example.com"},
224 Subject: "DKIM Test",
225 Body: "Signed message.",
226 })
227 if err != nil {
228 t.Fatalf("Send failed: %v", err)
229 }
230
231 time.Sleep(100 * time.Millisecond)
232
233 if received == nil {
234 t.Fatal("expected server to receive email")
235 }
236 if !strings.Contains(string(received.RawMessage), "DKIM-Signature:") {
237 t.Error("expected DKIM-Signature header in received message")
238 }
239 }
240
241 func TestClient_MXLookupFails(t *testing.T) {
242 client := NewClient(ClientConfig{FromDomain: "bridge.example.com"})
243 client.SetResolver(func(domain string) ([]*net.MX, error) {
244 return nil, net.ErrClosed
245 })
246
247 err := client.Send(&OutboundEmail{
248 From: "sender@bridge.example.com",
249 To: []string{"alice@example.com"},
250 Body: "Test",
251 })
252 if err == nil {
253 t.Fatal("expected error from MX lookup failure")
254 }
255 }
256
257 func TestClient_AllMXFail(t *testing.T) {
258 client := NewClient(ClientConfig{FromDomain: "bridge.example.com"})
259 client.SetResolver(func(domain string) ([]*net.MX, error) {
260 return []*net.MX{
261 {Host: "192.0.2.1", Pref: 10}, // unreachable
262 }, nil
263 })
264 client.SetDialer(func(addr string) (*gosmtp.Client, error) {
265 return nil, net.ErrClosed
266 })
267
268 err := client.Send(&OutboundEmail{
269 From: "sender@bridge.example.com",
270 To: []string{"alice@example.com"},
271 Body: "Test",
272 })
273 if err == nil {
274 t.Fatal("expected error when all MX fail")
275 }
276 }
277
278 func TestClient_EmptyMXFallback(t *testing.T) {
279 // Empty MX list should fall back to A record (the domain itself)
280 var dialedAddr string
281 client := NewClient(ClientConfig{FromDomain: "bridge.example.com"})
282 client.SetResolver(func(domain string) ([]*net.MX, error) {
283 return nil, nil // empty, no error
284 })
285 client.SetDialer(func(addr string) (*gosmtp.Client, error) {
286 dialedAddr = addr
287 return nil, net.ErrClosed
288 })
289
290 client.Send(&OutboundEmail{
291 From: "sender@bridge.example.com",
292 To: []string{"alice@example.com"},
293 Body: "Test",
294 })
295 // Should try to connect to example.com:25
296 if !strings.Contains(dialedAddr, "example.com") {
297 t.Errorf("dialedAddr = %q, want example.com:25", dialedAddr)
298 }
299 }
300
301 func TestClient_MultipleDomains(t *testing.T) {
302 var received []*InboundEmail
303 server := NewServer(ServerConfig{
304 Domain: "test.example.com",
305 ListenAddr: "127.0.0.1:0",
306 }, func(email *InboundEmail) error {
307 received = append(received, email)
308 return nil
309 })
310
311 if err := server.Start(); err != nil {
312 t.Fatalf("start server: %v", err)
313 }
314 defer server.Stop(context.Background())
315
316 addr := server.Addr().String()
317 _, port, _ := net.SplitHostPort(addr)
318
319 client := NewClient(ClientConfig{FromDomain: "bridge.example.com"})
320 // Route all domains to our local server
321 client.SetResolver(func(domain string) ([]*net.MX, error) {
322 return []*net.MX{{Host: "127.0.0.1", Pref: 10}}, nil
323 })
324 client.SetDialer(func(a string) (*gosmtp.Client, error) {
325 return gosmtpDial("127.0.0.1:" + port)
326 })
327
328 err := client.Send(&OutboundEmail{
329 From: "sender@bridge.example.com",
330 To: []string{"alice@test.example.com", "bob@test.example.com"},
331 Body: "Multi-recipient",
332 })
333 if err != nil {
334 t.Fatalf("Send failed: %v", err)
335 }
336 }
337
338 func TestClient_SendWithDKIMSignFailure(t *testing.T) {
339 // DKIM signer that always fails — should fall back to sending unsigned
340 var received *InboundEmail
341 server := NewServer(ServerConfig{
342 Domain: "test.example.com",
343 ListenAddr: "127.0.0.1:0",
344 }, func(email *InboundEmail) error {
345 received = email
346 return nil
347 })
348
349 if err := server.Start(); err != nil {
350 t.Fatalf("start server: %v", err)
351 }
352 defer server.Stop(context.Background())
353
354 addr := server.Addr().String()
355 _, port, _ := net.SplitHostPort(addr)
356
357 // Create a DKIM signer with a tiny invalid key that will fail to sign
358 badSigner := &DKIMSigner{
359 domain: "bridge.example.com",
360 selector: "test",
361 key: nil, // nil key will cause sign to fail
362 }
363
364 client := NewClient(ClientConfig{
365 FromDomain: "bridge.example.com",
366 DKIMSigner: badSigner,
367 })
368 client.SetResolver(func(domain string) ([]*net.MX, error) {
369 return []*net.MX{{Host: "127.0.0.1", Pref: 10}}, nil
370 })
371 client.SetDialer(func(a string) (*gosmtp.Client, error) {
372 return gosmtpDial("127.0.0.1:" + port)
373 })
374
375 err := client.Send(&OutboundEmail{
376 From: "sender@bridge.example.com",
377 To: []string{"recipient@test.example.com"},
378 Subject: "DKIM Fail Test",
379 Body: "Should send unsigned.",
380 })
381 if err != nil {
382 t.Fatalf("Send should succeed even with DKIM failure: %v", err)
383 }
384
385 time.Sleep(100 * time.Millisecond)
386
387 if received == nil {
388 t.Fatal("expected server to receive email (unsigned)")
389 }
390 // Should NOT have DKIM-Signature since signing failed
391 if strings.Contains(string(received.RawMessage), "DKIM-Signature:") {
392 t.Error("should NOT have DKIM-Signature when signing fails")
393 }
394 }
395
396 func TestClient_SendWithBcc(t *testing.T) {
397 var received []*InboundEmail
398 server := NewServer(ServerConfig{
399 Domain: "test.example.com",
400 ListenAddr: "127.0.0.1:0",
401 }, func(email *InboundEmail) error {
402 received = append(received, email)
403 return nil
404 })
405
406 if err := server.Start(); err != nil {
407 t.Fatalf("start server: %v", err)
408 }
409 defer server.Stop(context.Background())
410
411 addr := server.Addr().String()
412 _, port, _ := net.SplitHostPort(addr)
413
414 client := NewClient(ClientConfig{FromDomain: "bridge.example.com"})
415 client.SetResolver(func(domain string) ([]*net.MX, error) {
416 return []*net.MX{{Host: "127.0.0.1", Pref: 10}}, nil
417 })
418 client.SetDialer(func(a string) (*gosmtp.Client, error) {
419 return gosmtpDial("127.0.0.1:" + port)
420 })
421
422 err := client.Send(&OutboundEmail{
423 From: "sender@bridge.example.com",
424 To: []string{"alice@test.example.com"},
425 Cc: []string{"bob@test.example.com"},
426 Bcc: []string{"secret@test.example.com"},
427 Subject: "BCC Test",
428 Body: "With BCC recipient.",
429 })
430 if err != nil {
431 t.Fatalf("Send failed: %v", err)
432 }
433
434 time.Sleep(100 * time.Millisecond)
435
436 if len(received) == 0 {
437 t.Fatal("expected server to receive email")
438 }
439 // BCC address should NOT appear in the message headers
440 msg := string(received[0].RawMessage)
441 if strings.Contains(msg, "secret@test.example.com") {
442 t.Error("BCC address should NOT appear in message headers")
443 }
444 }
445
446 func TestClient_SendMultipleDomains_PartialFailure(t *testing.T) {
447 // One domain succeeds, another fails
448 client := NewClient(ClientConfig{FromDomain: "bridge.example.com"})
449 resolveCount := 0
450 client.SetResolver(func(domain string) ([]*net.MX, error) {
451 resolveCount++
452 if domain == "fail.com" {
453 return nil, net.ErrClosed
454 }
455 return []*net.MX{{Host: "127.0.0.1", Pref: 10}}, nil
456 })
457 client.SetDialer(func(addr string) (*gosmtp.Client, error) {
458 return nil, net.ErrClosed
459 })
460
461 err := client.Send(&OutboundEmail{
462 From: "sender@bridge.example.com",
463 To: []string{"alice@fail.com", "bob@other.com"},
464 Body: "Test",
465 })
466 // Should return an error (the last error)
467 if err == nil {
468 t.Fatal("expected error from partial delivery failure")
469 }
470 }
471
472 func TestBuildMessage_WithInReplyTo(t *testing.T) {
473 c := NewClient(ClientConfig{FromDomain: "bridge.example.com"})
474 email := &OutboundEmail{
475 From: "from@bridge.example.com",
476 To: []string{"alice@example.com"},
477 Cc: []string{"bob@example.com", "carol@example.com"},
478 Bcc: []string{"secret@example.com"},
479 Subject: "Reply Test",
480 Body: "Reply body",
481 }
482 msg, err := c.buildMessage(email)
483 if err != nil {
484 t.Fatalf("unexpected error: %v", err)
485 }
486 s := string(msg)
487 // Cc should be in headers
488 if !strings.Contains(s, "bob@example.com") {
489 t.Error("missing Cc addresses in header")
490 }
491 // Bcc should NOT be in headers
492 if strings.Contains(s, "secret@example.com") {
493 t.Error("Bcc address should NOT appear in headers")
494 }
495 // Date header
496 if !strings.Contains(s, "Date:") {
497 t.Error("missing Date header")
498 }
499 }
500
501 func TestClient_SendNoDomains(t *testing.T) {
502 c := NewClient(ClientConfig{FromDomain: "bridge.example.com"})
503 // Send to addresses with no @ sign — they're all skipped
504 err := c.Send(&OutboundEmail{
505 From: "sender@bridge.example.com",
506 To: []string{"invalid-no-at"},
507 Body: "Test",
508 })
509 // groupByDomain returns empty map → no delivery attempts → no error
510 if err != nil {
511 t.Errorf("expected no error for empty domain map, got: %v", err)
512 }
513 }
514
515 func TestClient_DeliverDirect_HelloFails(t *testing.T) {
516 // Start a server that accepts connections but then we can test Hello failure
517 // by using a dialer that connects to a server that rejects EHLO
518 var received *InboundEmail
519 server := NewServer(ServerConfig{
520 Domain: "test.example.com",
521 ListenAddr: "127.0.0.1:0",
522 }, func(email *InboundEmail) error {
523 received = email
524 return nil
525 })
526
527 if err := server.Start(); err != nil {
528 t.Fatalf("start server: %v", err)
529 }
530 defer server.Stop(context.Background())
531
532 addr := server.Addr().String()
533 _, port, _ := net.SplitHostPort(addr)
534
535 // Create client with an empty FromDomain that has a very long invalid hostname
536 // to trigger Hello failure on some SMTP implementations
537 client := NewClient(ClientConfig{FromDomain: "bridge.example.com"})
538 client.SetResolver(func(domain string) ([]*net.MX, error) {
539 return []*net.MX{{Host: "127.0.0.1", Pref: 10}}, nil
540 })
541 client.SetDialer(func(a string) (*gosmtp.Client, error) {
542 return gosmtp.Dial("127.0.0.1:" + port)
543 })
544
545 // This should succeed since our server accepts connections
546 err := client.Send(&OutboundEmail{
547 From: "sender@bridge.example.com",
548 To: []string{"alice@test.example.com"},
549 Subject: "Hello Test",
550 Body: "Body",
551 })
552 // Should succeed with our test server
553 if err != nil {
554 t.Logf("deliverDirect error (may be expected): %v", err)
555 }
556 _ = received
557 }
558
559 func TestClient_DeliverDirect_RcptFails(t *testing.T) {
560 // Start a server that rejects specific recipients
561 server := NewServer(ServerConfig{
562 Domain: "other.example.com", // different domain
563 ListenAddr: "127.0.0.1:0",
564 }, func(email *InboundEmail) error {
565 return nil
566 })
567
568 if err := server.Start(); err != nil {
569 t.Fatalf("start server: %v", err)
570 }
571 defer server.Stop(context.Background())
572
573 addr := server.Addr().String()
574 _, port, _ := net.SplitHostPort(addr)
575
576 client := NewClient(ClientConfig{FromDomain: "bridge.example.com"})
577 client.SetResolver(func(domain string) ([]*net.MX, error) {
578 return []*net.MX{{Host: "127.0.0.1", Pref: 10}}, nil
579 })
580 client.SetDialer(func(a string) (*gosmtp.Client, error) {
581 return gosmtpDial("127.0.0.1:" + port)
582 })
583
584 // The server domain is "other.example.com" so it will reject recipients
585 // for "wrong.example.com" domain
586 err := client.Send(&OutboundEmail{
587 From: "sender@bridge.example.com",
588 To: []string{"alice@wrong.example.com"},
589 Body: "Test",
590 })
591 if err == nil {
592 t.Fatal("expected error from RCPT rejection")
593 }
594 }
595
596 func TestClient_DeliverDirect_HelloFails_Rejection(t *testing.T) {
597 // Raw TCP server that rejects EHLO/HELO
598 ln, err := net.Listen("tcp", "127.0.0.1:0")
599 if err != nil {
600 t.Fatalf("listen: %v", err)
601 }
602 defer ln.Close()
603
604 go func() {
605 conn, err := ln.Accept()
606 if err != nil {
607 return
608 }
609 defer conn.Close()
610 conn.Write([]byte("220 test ESMTP\r\n"))
611 buf := make([]byte, 1024)
612 for {
613 n, err := conn.Read(buf)
614 if err != nil {
615 return
616 }
617 cmd := strings.ToUpper(strings.TrimSpace(string(buf[:n])))
618 switch {
619 case strings.HasPrefix(cmd, "EHLO"):
620 conn.Write([]byte("550 EHLO rejected\r\n"))
621 case strings.HasPrefix(cmd, "HELO"):
622 conn.Write([]byte("550 HELO rejected\r\n"))
623 case strings.HasPrefix(cmd, "QUIT"):
624 conn.Write([]byte("221 Bye\r\n"))
625 return
626 default:
627 conn.Write([]byte("500 Unknown\r\n"))
628 }
629 }
630 }()
631
632 addr := ln.Addr().String()
633 client := NewClient(ClientConfig{FromDomain: "bridge.example.com"})
634 client.SetResolver(func(domain string) ([]*net.MX, error) {
635 return []*net.MX{{Host: "127.0.0.1", Pref: 10}}, nil
636 })
637 client.SetDialer(func(a string) (*gosmtp.Client, error) {
638 return gosmtp.Dial(addr)
639 })
640
641 err = client.Send(&OutboundEmail{
642 From: "sender@bridge.example.com",
643 To: []string{"alice@example.com"},
644 Body: "Test",
645 })
646 if err == nil {
647 t.Fatal("expected error from HELO rejection")
648 }
649 }
650
651 func TestClient_DeliverDirect_WriteDataFails(t *testing.T) {
652 // Raw TCP server that accepts DATA but disconnects during body write
653 ln, err := net.Listen("tcp", "127.0.0.1:0")
654 if err != nil {
655 t.Fatalf("listen: %v", err)
656 }
657 defer ln.Close()
658
659 go func() {
660 conn, err := ln.Accept()
661 if err != nil {
662 return
663 }
664 defer conn.Close()
665 conn.Write([]byte("220 test ESMTP\r\n"))
666 buf := make([]byte, 4096)
667 for {
668 n, err := conn.Read(buf)
669 if err != nil {
670 return
671 }
672 cmd := strings.ToUpper(strings.TrimSpace(string(buf[:n])))
673 switch {
674 case strings.HasPrefix(cmd, "EHLO"), strings.HasPrefix(cmd, "HELO"):
675 conn.Write([]byte("250 OK\r\n"))
676 case strings.HasPrefix(cmd, "MAIL FROM"):
677 conn.Write([]byte("250 OK\r\n"))
678 case strings.HasPrefix(cmd, "RCPT TO"):
679 conn.Write([]byte("250 OK\r\n"))
680 case strings.HasPrefix(cmd, "DATA"):
681 conn.Write([]byte("354 Start mail input\r\n"))
682 // Immediately close connection to cause write error
683 time.Sleep(10 * time.Millisecond)
684 conn.Close()
685 return
686 case strings.HasPrefix(cmd, "QUIT"):
687 conn.Write([]byte("221 Bye\r\n"))
688 return
689 default:
690 conn.Write([]byte("500 Unknown\r\n"))
691 }
692 }
693 }()
694
695 addr := ln.Addr().String()
696 client := NewClient(ClientConfig{FromDomain: "bridge.example.com"})
697 client.SetResolver(func(domain string) ([]*net.MX, error) {
698 return []*net.MX{{Host: "127.0.0.1", Pref: 10}}, nil
699 })
700 client.SetDialer(func(a string) (*gosmtp.Client, error) {
701 return gosmtp.Dial(addr)
702 })
703
704 err = client.Send(&OutboundEmail{
705 From: "sender@bridge.example.com",
706 To: []string{"alice@example.com"},
707 Body: "Test body that needs writing",
708 })
709 if err == nil {
710 t.Fatal("expected error from write data failure")
711 }
712 }
713
714 func TestClient_DeliverDirect_MailFromFails(t *testing.T) {
715 // Raw TCP server that responds to greeting and EHLO but rejects MAIL FROM
716 ln, err := net.Listen("tcp", "127.0.0.1:0")
717 if err != nil {
718 t.Fatalf("listen: %v", err)
719 }
720 defer ln.Close()
721
722 go func() {
723 conn, err := ln.Accept()
724 if err != nil {
725 return
726 }
727 defer conn.Close()
728 conn.Write([]byte("220 test ESMTP\r\n"))
729 buf := make([]byte, 1024)
730 for {
731 n, err := conn.Read(buf)
732 if err != nil {
733 return
734 }
735 cmd := strings.ToUpper(strings.TrimSpace(string(buf[:n])))
736 switch {
737 case strings.HasPrefix(cmd, "EHLO"), strings.HasPrefix(cmd, "HELO"):
738 conn.Write([]byte("250 OK\r\n"))
739 case strings.HasPrefix(cmd, "MAIL FROM"):
740 conn.Write([]byte("550 Sender rejected\r\n"))
741 case strings.HasPrefix(cmd, "QUIT"):
742 conn.Write([]byte("221 Bye\r\n"))
743 return
744 default:
745 conn.Write([]byte("500 Unknown\r\n"))
746 }
747 }
748 }()
749
750 addr := ln.Addr().String()
751 client := NewClient(ClientConfig{FromDomain: "bridge.example.com"})
752 client.SetResolver(func(domain string) ([]*net.MX, error) {
753 return []*net.MX{{Host: "127.0.0.1", Pref: 10}}, nil
754 })
755 client.SetDialer(func(a string) (*gosmtp.Client, error) {
756 return gosmtp.Dial(addr)
757 })
758
759 err = client.Send(&OutboundEmail{
760 From: "sender@bridge.example.com",
761 To: []string{"alice@example.com"},
762 Body: "Test",
763 })
764 if err == nil {
765 t.Fatal("expected error from MAIL FROM rejection")
766 }
767 if !strings.Contains(err.Error(), "MAIL FROM") {
768 t.Logf("error: %v", err)
769 }
770 }
771
772 func TestClient_DeliverDirect_DataFails(t *testing.T) {
773 // Raw TCP server that accepts MAIL FROM and RCPT but rejects DATA
774 ln, err := net.Listen("tcp", "127.0.0.1:0")
775 if err != nil {
776 t.Fatalf("listen: %v", err)
777 }
778 defer ln.Close()
779
780 go func() {
781 conn, err := ln.Accept()
782 if err != nil {
783 return
784 }
785 defer conn.Close()
786 conn.Write([]byte("220 test ESMTP\r\n"))
787 buf := make([]byte, 1024)
788 for {
789 n, err := conn.Read(buf)
790 if err != nil {
791 return
792 }
793 cmd := strings.ToUpper(strings.TrimSpace(string(buf[:n])))
794 switch {
795 case strings.HasPrefix(cmd, "EHLO"), strings.HasPrefix(cmd, "HELO"):
796 conn.Write([]byte("250 OK\r\n"))
797 case strings.HasPrefix(cmd, "MAIL FROM"):
798 conn.Write([]byte("250 OK\r\n"))
799 case strings.HasPrefix(cmd, "RCPT TO"):
800 conn.Write([]byte("250 OK\r\n"))
801 case strings.HasPrefix(cmd, "DATA"):
802 conn.Write([]byte("554 Transaction failed\r\n"))
803 case strings.HasPrefix(cmd, "QUIT"):
804 conn.Write([]byte("221 Bye\r\n"))
805 return
806 default:
807 conn.Write([]byte("500 Unknown\r\n"))
808 }
809 }
810 }()
811
812 addr := ln.Addr().String()
813 client := NewClient(ClientConfig{FromDomain: "bridge.example.com"})
814 client.SetResolver(func(domain string) ([]*net.MX, error) {
815 return []*net.MX{{Host: "127.0.0.1", Pref: 10}}, nil
816 })
817 client.SetDialer(func(a string) (*gosmtp.Client, error) {
818 return gosmtp.Dial(addr)
819 })
820
821 err = client.Send(&OutboundEmail{
822 From: "sender@bridge.example.com",
823 To: []string{"alice@example.com"},
824 Body: "Test",
825 })
826 if err == nil {
827 t.Fatal("expected error from DATA rejection")
828 }
829 }
830
831 func TestClient_DeliverDirect_CloseFails(t *testing.T) {
832 // Raw TCP server that accepts DATA but closes connection during write
833 ln, err := net.Listen("tcp", "127.0.0.1:0")
834 if err != nil {
835 t.Fatalf("listen: %v", err)
836 }
837 defer ln.Close()
838
839 go func() {
840 conn, err := ln.Accept()
841 if err != nil {
842 return
843 }
844 defer conn.Close()
845 conn.Write([]byte("220 test ESMTP\r\n"))
846 buf := make([]byte, 4096)
847 for {
848 n, err := conn.Read(buf)
849 if err != nil {
850 return
851 }
852 cmd := strings.ToUpper(strings.TrimSpace(string(buf[:n])))
853 switch {
854 case strings.HasPrefix(cmd, "EHLO"), strings.HasPrefix(cmd, "HELO"):
855 conn.Write([]byte("250 OK\r\n"))
856 case strings.HasPrefix(cmd, "MAIL FROM"):
857 conn.Write([]byte("250 OK\r\n"))
858 case strings.HasPrefix(cmd, "RCPT TO"):
859 conn.Write([]byte("250 OK\r\n"))
860 case strings.HasPrefix(cmd, "DATA"):
861 conn.Write([]byte("354 Start mail input\r\n"))
862 // Read until we get \r\n.\r\n (end of data)
863 for {
864 n, err := conn.Read(buf)
865 if err != nil {
866 return
867 }
868 if strings.Contains(string(buf[:n]), "\r\n.\r\n") {
869 break
870 }
871 }
872 // Reject at end of data (close error)
873 conn.Write([]byte("554 Message rejected\r\n"))
874 case strings.HasPrefix(cmd, "QUIT"):
875 conn.Write([]byte("221 Bye\r\n"))
876 return
877 default:
878 conn.Write([]byte("500 Unknown\r\n"))
879 }
880 }
881 }()
882
883 addr := ln.Addr().String()
884 client := NewClient(ClientConfig{FromDomain: "bridge.example.com"})
885 client.SetResolver(func(domain string) ([]*net.MX, error) {
886 return []*net.MX{{Host: "127.0.0.1", Pref: 10}}, nil
887 })
888 client.SetDialer(func(a string) (*gosmtp.Client, error) {
889 return gosmtp.Dial(addr)
890 })
891
892 err = client.Send(&OutboundEmail{
893 From: "sender@bridge.example.com",
894 To: []string{"alice@example.com"},
895 Body: "Test body content",
896 })
897 if err == nil {
898 t.Fatal("expected error from message rejection")
899 }
900 }
901
902 // Alias used in test function signatures.
903 var gosmtpDial = gosmtp.Dial
904