package email import ( "context" "encoding/base64" "net/mail" "strings" "testing" "time" ) func testFrom() mail.Address { return mail.Address{Address: "no-reply@example.com"} } func TestLoginAuthChallengeResponse(t *testing.T) { a := &loginAuth{username: "user", password: "pass"} mech, initial, err := a.Start(nil) if err != nil { t.Fatalf("Start: %v", err) } if mech != "LOGIN" || initial != nil { t.Fatalf("unexpected Start result: %q %v", mech, initial) } resp, err := a.Next([]byte("Username:"), true) if err != nil || string(resp) != "user" { t.Fatalf("username challenge: %q %v", resp, err) } resp, err = a.Next([]byte("Password"), true) if err != nil || string(resp) != "pass" { t.Fatalf("password challenge: %q %v", resp, err) } if _, err := a.Next([]byte("Bogus"), true); err == nil { t.Fatal("expected error on unexpected challenge") } } func TestPlainAuthSendsCredentialsInClear(t *testing.T) { // Internal relays advertise only PLAIN over an unencrypted connection; // stdlib smtp.PlainAuth refuses that (ErrUnencryptedConnection), so the // sender uses the cleartext-capable plainAuth instead. a := &plainAuth{username: "user", password: "pass"} mech, initial, err := a.Start(nil) if err != nil { t.Fatalf("Start: %v", err) } if mech != "PLAIN" { t.Fatalf("mechanism = %q, want PLAIN", mech) } want := "\x00user\x00pass" if string(initial) != want { t.Fatalf("initial response = %q, want %q", initial, want) } if resp, err := a.Next(nil, false); err != nil || resp != nil { t.Fatalf("Next(done) = %q %v, want nil nil", resp, err) } if _, err := a.Next(nil, true); err == nil { t.Fatal("expected error on unexpected challenge") } } func TestNewSMTPValidation(t *testing.T) { if _, err := NewSMTP(SMTPConfig{Port: 587}); err == nil || !strings.Contains(err.Error(), "missing host") { t.Fatalf("want missing-host error, got %v", err) } if _, err := NewSMTP(SMTPConfig{Host: "relay.internal", Port: 0, DefaultFrom: testFrom()}); err == nil || !strings.Contains(err.Error(), "port") { t.Fatalf("want invalid-port error, got %v", err) } s, err := NewSMTP(SMTPConfig{ Host: "relay.internal", Port: 25, SSL: false, DefaultFrom: testFrom(), Timeout: time.Second, }) if err != nil { t.Fatalf("NewSMTP: %v", err) } smtpS, ok := s.(*smtpSender) if !ok { t.Fatalf("unexpected sender type %T", s) } if smtpS.ssl || smtpS.addr != "relay.internal:25" { t.Fatalf("unexpected config: %+v", smtpS) } } func TestBuildMessageMultipartAlternative(t *testing.T) { msg := Message{ From: mail.Address{Name: "go-auth", Address: "no-reply@example.com"}, To: []mail.Address{{Name: "Ada", Address: "ada@example.com"}}, Subject: "Reset your password — Åke & Östen", TextBody: "Hello Ada,\nreset link: https://auth.example/reset?token=abc\n", HTMLBody: "

Hello Ada,

reset link: https://auth.example/reset?token=abc

", Headers: map[string]string{"List-Unsubscribe": ""}, } raw, err := buildMessage(msg) if err != nil { t.Fatal(err) } out := string(raw) for _, want := range []string{ "From: \"go-auth\" ", "To: \"Ada\" ", "Subject: =?utf-8?q?", "MIME-Version: 1.0", "multipart/alternative;", "Content-Type: text/plain; charset=utf-8", "Content-Type: text/html; charset=utf-8", "List-Unsubscribe: ", "Hello Ada,", } { if !strings.Contains(out, want) { t.Errorf("message missing %q\n%s", want, out) } } // The multipart must terminate properly. if !strings.Contains(out, "--alt-") || !strings.HasSuffix(out, "--\r\n") { t.Errorf("multipart envelope not closed properly:\n%s", out) } } func TestBuildMessageTextOnly(t *testing.T) { raw, err := buildMessage(Message{ From: testFrom(), To: []mail.Address{{Address: "ada@example.com"}}, Subject: "hi", TextBody: "plain only", }) if err != nil { t.Fatal(err) } out := string(raw) if strings.Contains(out, "multipart") { t.Errorf("text-only message must not be multipart:\n%s", out) } if !strings.Contains(out, "Content-Type: text/plain; charset=utf-8") { t.Errorf("missing text/plain content type:\n%s", out) } } func TestBuildMessageRejectsHeaderInjection(t *testing.T) { raw, err := buildMessage(Message{ From: testFrom(), To: []mail.Address{{Address: "ada@example.com"}}, Subject: "hi", TextBody: "body", Headers: map[string]string{"X-Evil": "a\r\nBcc: victim@example.com"}, }) if err != nil { t.Fatal(err) } if strings.Contains(string(raw), "Bcc: victim@example.com") { t.Error("CR/LF injection survived into the message") } } func TestBuildMessageAttachment(t *testing.T) { body, err := buildMessage(Message{ From: mail.Address{Name: "B2B Demo AB", Address: "faktura@demo.se"}, To: []mail.Address{{Address: "faktura@falk.se"}}, Subject: "Faktura INV-000001", TextBody: "Hej,\nFaktura bifogas.", Attachments: []Attachment{{ Filename: "INV-000001.pdf", Content: []byte("%PDF-1.4 fake"), }}, }) if err != nil { t.Fatalf("buildMessage: %v", err) } s := string(body) for _, want := range []string{ "multipart/mixed;", `Content-Type: application/pdf; name="INV-000001.pdf"`, `Content-Disposition: attachment; filename="INV-000001.pdf"`, "Content-Transfer-Encoding: base64", } { if !strings.Contains(s, want) { t.Errorf("message missing %q", want) } } // The attachment payload round-trips: extract the base64 blob between the // attachment part header and the closing CRLF, decode, compare. idx := strings.Index(s, `Content-Disposition: attachment; filename="INV-000001.pdf"`) rest := s[idx:] lineEnd := strings.Index(rest, "\r\n\r\n") if lineEnd < 0 { t.Fatal("attachment part has no body separator") } b64line := strings.SplitN(rest[lineEnd+4:], "\r\n", 2)[0] decoded, err := base64.StdEncoding.DecodeString(strings.TrimSpace(b64line)) if err != nil { t.Fatalf("decode attachment base64: %v", err) } if string(decoded) != "%PDF-1.4 fake" { t.Fatalf("attachment content = %q", decoded) } } func TestBuildMessageMissingBody(t *testing.T) { if _, err := buildMessage(Message{ From: testFrom(), To: []mail.Address{{Address: "a@b"}}, Subject: "x", }); err == nil { t.Fatal("want missing-body error") } } func TestLogSenderNeverSends(t *testing.T) { s := NewLogSender(nil, testFrom()) if err := s.Send(context.Background(), Message{ From: testFrom(), To: []mail.Address{{Address: "a@b"}}, Subject: "x", TextBody: "y", }); err != nil { t.Fatalf("log sender: %v", err) } if s.DefaultFrom().Address != "no-reply@example.com" { t.Fatalf("DefaultFrom = %q", s.DefaultFrom().Address) } }