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
 59func TestSMTPHelloHostname(t *testing.T) {
 60	orig := osHostname
 61	t.Cleanup(func() { osHostname = orig })
 62
 63	osHostname = func() (string, error) { return "mail.example.com", nil }
 64	if got := smtpHelloHostname(); got != "mail.example.com" {
 65		t.Fatalf("expected hostname, got %q", got)
 66	}
 67
 68	osHostname = func() (string, error) { return "", nil }
 69	if got := smtpHelloHostname(); got != "localhost" {
 70		t.Fatalf("expected localhost fallback for empty hostname, got %q", got)
 71	}
 72
 73	osHostname = func() (string, error) { return "ignored", errors.New("hostname unavailable") }
 74	if got := smtpHelloHostname(); got != "localhost" {
 75		t.Fatalf("expected localhost fallback on error, got %q", got)
 76	}
 77}
 78
 79// TestGenerateMessageID ensures the Message-ID has the correct format.
 80func TestGenerateMessageID(t *testing.T) {
 81	from := "test@example.com"
 82	msgID := generateMessageID(from)
 83
 84	// Check if the message ID is enclosed in angle brackets.
 85	if !strings.HasPrefix(msgID, "<") || !strings.HasSuffix(msgID, ">") {
 86		t.Errorf("Message-ID should be enclosed in angle brackets, got %s", msgID)
 87	}
 88
 89	// Check if the 'from' address is part of the message ID.
 90	if !strings.Contains(msgID, from) {
 91		t.Errorf("Message-ID should contain the from address, got %s", msgID)
 92	}
 93
 94	// The original check was too simple and failed because the 'from' address itself contains an '@'.
 95	// A Message-ID is generally <unique-part@domain>. The current implementation uses the full 'from' address as the domain part.
 96	// This revised check validates that structure correctly.
 97	unwrappedID := strings.Trim(msgID, "<>")
 98
 99	// Ensure there's at least one '@' symbol.
100	if !strings.Contains(unwrappedID, "@") {
101		t.Errorf("Message-ID should contain an '@' symbol, got %s", msgID)
102	}
103
104	// Check that the ID ends with the full 'from' address, preceded by an '@'.
105	// This confirms the structure is <random_part>@<from_address>.
106	expectedSuffix := "@" + from
107	if !strings.HasSuffix(unwrappedID, expectedSuffix) {
108		t.Errorf("Message-ID should end with '@' + from address. Got %s, expected suffix %s", unwrappedID, expectedSuffix)
109	}
110
111	// Check that the part before the suffix is not empty.
112	randomPart := strings.TrimSuffix(unwrappedID, expectedSuffix)
113	if randomPart == "" {
114		t.Errorf("Message-ID has an empty random part, got %s", msgID)
115	}
116}