1package sender
2
3import (
4 "errors"
5 "io"
6 "strings"
7 "testing"
8)
9
10type failingReader struct{}
11
12func (failingReader) Read(p []byte) (int, error) {
13 return 0, errors.New("simulated crypto/rand failure")
14}
15
16// TestSMIMEOuterBoundary_RandFailure ensures that a crypto/rand failure surfaces
17// as an error rather than silently producing a predictable, time-based
18// boundary that an attacker could collide with (issue #1127).
19func TestSMIMEOuterBoundary_RandFailure(t *testing.T) {
20 orig := randReader
21 t.Cleanup(func() { randReader = orig })
22 randReader = failingReader{}
23
24 got, err := smimeOuterBoundary()
25 if err == nil {
26 t.Fatalf("expected error when crypto/rand fails, got boundary %q", got)
27 }
28 if got != "" {
29 t.Errorf("expected empty boundary on error, got %q", got)
30 }
31}
32
33// TestSMIMEOuterBoundary_Success ensures the happy path returns a non-empty,
34// random-looking boundary with the expected prefix.
35func TestSMIMEOuterBoundary_Success(t *testing.T) {
36 b1, err := smimeOuterBoundary()
37 if err != nil {
38 t.Fatalf("unexpected error: %v", err)
39 }
40 if !strings.HasPrefix(b1, "signed-") {
41 t.Errorf("boundary should start with 'signed-', got %q", b1)
42 }
43 // 12 random bytes => 24 hex chars; total length 7 + 24 = 31.
44 if len(b1) != len("signed-")+24 {
45 t.Errorf("unexpected boundary length: got %d (%q)", len(b1), b1)
46 }
47 b2, err := smimeOuterBoundary()
48 if err != nil {
49 t.Fatalf("unexpected error on second call: %v", err)
50 }
51 if b1 == b2 {
52 t.Errorf("two consecutive boundaries should differ, both got %q", b1)
53 }
54}
55
56// Ensure io is referenced even if a future refactor removes it indirectly.
57var _ io.Reader = failingReader{}
58
59// TestGenerateMessageID ensures the Message-ID has the correct format.
60func TestGenerateMessageID(t *testing.T) {
61 from := "test@example.com"
62 msgID := generateMessageID(from)
63
64 // Check if the message ID is enclosed in angle brackets.
65 if !strings.HasPrefix(msgID, "<") || !strings.HasSuffix(msgID, ">") {
66 t.Errorf("Message-ID should be enclosed in angle brackets, got %s", msgID)
67 }
68
69 // Check if the 'from' address is part of the message ID.
70 if !strings.Contains(msgID, from) {
71 t.Errorf("Message-ID should contain the from address, got %s", msgID)
72 }
73
74 // The original check was too simple and failed because the 'from' address itself contains an '@'.
75 // A Message-ID is generally <unique-part@domain>. The current implementation uses the full 'from' address as the domain part.
76 // This revised check validates that structure correctly.
77 unwrappedID := strings.Trim(msgID, "<>")
78
79 // Ensure there's at least one '@' symbol.
80 if !strings.Contains(unwrappedID, "@") {
81 t.Errorf("Message-ID should contain an '@' symbol, got %s", msgID)
82 }
83
84 // Check that the ID ends with the full 'from' address, preceded by an '@'.
85 // This confirms the structure is <random_part>@<from_address>.
86 expectedSuffix := "@" + from
87 if !strings.HasSuffix(unwrappedID, expectedSuffix) {
88 t.Errorf("Message-ID should end with '@' + from address. Got %s, expected suffix %s", unwrappedID, expectedSuffix)
89 }
90
91 // Check that the part before the suffix is not empty.
92 randomPart := strings.TrimSuffix(unwrappedID, expectedSuffix)
93 if randomPart == "" {
94 t.Errorf("Message-ID has an empty random part, got %s", msgID)
95 }
96}