sender_test.go

 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}