package mail import ( "strings" "testing" "time" ) func TestBuildMIMERoundTrip(t *testing.T) { msg := OutgoingMessage{ From: "emcli@stevecliff.com", To: []string{"me@stevecliff.com"}, Cc: []string{"cc@stevecliff.com"}, Subject: "hello from emcli", BodyText: "this is the body\nwith two lines", Date: time.Date(2026, 6, 22, 12, 0, 0, 0, time.UTC), InReplyTo: "abc123@origin.example", References: []string{"root@origin.example", "abc123@origin.example"}, Attachments: []Attachment{ {Name: "note.txt", MIME: "text/plain", Content: []byte("attached bytes")}, }, } raw, err := BuildMIME(msg) if err != nil { t.Fatalf("BuildMIME: %v", err) } // Headers present in the raw bytes. rawStr := string(raw) for _, want := range []string{ "From:", "emcli@stevecliff.com", "To:", "me@stevecliff.com", "Cc:", "cc@stevecliff.com", "Subject:", "hello from emcli", "In-Reply-To:", "abc123@origin.example", "References:", "root@origin.example", } { if !strings.Contains(rawStr, want) { t.Fatalf("MIME missing %q in:\n%s", want, rawStr) } } // Round-trips back through the parser: body and attachment survive. parsed, err := ParseMessage(0, raw) if err != nil { t.Fatalf("ParseMessage: %v", err) } gotBody := strings.ReplaceAll(strings.TrimSpace(parsed.BodyText), "\r\n", "\n") if gotBody != "this is the body\nwith two lines" { t.Fatalf("body not preserved: %q", parsed.BodyText) } if len(parsed.Attachments) != 1 { t.Fatalf("want 1 attachment, got %d", len(parsed.Attachments)) } if parsed.Attachments[0].Name != "note.txt" || string(parsed.Attachments[0].Content) != "attached bytes" { t.Fatalf("attachment not preserved: %+v", parsed.Attachments[0]) } } func TestBuildMIMENoAttachments(t *testing.T) { msg := OutgoingMessage{ From: "emcli@stevecliff.com", To: []string{"me@stevecliff.com"}, Subject: "plain", BodyText: "just text", Date: time.Date(2026, 6, 22, 12, 0, 0, 0, time.UTC), } raw, err := BuildMIME(msg) if err != nil { t.Fatalf("BuildMIME: %v", err) } parsed, err := ParseMessage(0, raw) if err != nil { t.Fatalf("ParseMessage: %v", err) } if strings.TrimSpace(parsed.BodyText) != "just text" { t.Fatalf("body not preserved: %q", parsed.BodyText) } if len(parsed.Attachments) != 0 { t.Fatalf("want 0 attachments, got %d", len(parsed.Attachments)) } } func TestRecipientsCombinesAllFields(t *testing.T) { msg := OutgoingMessage{ To: []string{"a@x.com"}, Cc: []string{"b@x.com"}, Bcc: []string{"c@x.com"}, } got := msg.Recipients() want := []string{"a@x.com", "b@x.com", "c@x.com"} if len(got) != len(want) { t.Fatalf("Recipients()=%v want %v", got, want) } for i := range want { if got[i] != want[i] { t.Fatalf("Recipients()=%v want %v", got, want) } } } func TestEnvelopeFromStripsDisplayName(t *testing.T) { cases := map[string]string{ "Steve Cliff ": "me@stevecliff.com", "me@stevecliff.com": "me@stevecliff.com", "": "me@stevecliff.com", "not a valid address": "not a valid address", // unparseable ⇒ passthrough } for in, want := range cases { if got := envelopeFrom(in); got != want { t.Fatalf("envelopeFrom(%q) = %q, want %q", in, got, want) } } } func TestBuildMIMEKeepsDisplayNameInHeader(t *testing.T) { raw, err := BuildMIME(OutgoingMessage{ From: "Steve Cliff ", To: []string{"you@example.com"}, Subject: "hi", BodyText: "body", Date: time.Date(2026, 6, 23, 12, 0, 0, 0, time.UTC), }) if err != nil { t.Fatalf("BuildMIME: %v", err) } if !strings.Contains(string(raw), "Steve Cliff") { t.Fatalf("From header lost display name:\n%s", raw) } } func TestReadHeaderParsesReferences(t *testing.T) { raw := "From: a@x.com\r\n" + "To: b@x.com\r\n" + "Subject: re: hi\r\n" + "Message-Id: \r\n" + "References: \r\n" + "\r\nbody\r\n" h, err := ParseHeaderBytes(7, []byte(raw)) if err != nil { t.Fatalf("ParseHeaderBytes: %v", err) } if len(h.References) != 2 || h.References[0] != "root@x.com" || h.References[1] != "mid@x.com" { t.Fatalf("References not parsed: %v", h.References) } }