| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199 |
- package email
- import (
- "bufio"
- "fmt"
- "io"
- "mime"
- "net"
- "net/mail"
- "path/filepath"
- "strconv"
- "strings"
- "sync"
- "testing"
- "github.com/mhsanaei/3x-ui/v3/internal/database"
- "github.com/mhsanaei/3x-ui/v3/internal/web/service"
- )
- func TestBuildMessageIsRFC5322(t *testing.T) {
- raw := buildMessage("[email protected]", "3x-ui", []string{"[email protected]", "[email protected]"}, "Тест", "<b>hi</b>")
- msg, err := mail.ReadMessage(strings.NewReader(string(raw)))
- if err != nil {
- t.Fatalf("message does not parse as RFC 5322: %v", err)
- }
- from, err := mail.ParseAddress(msg.Header.Get("From"))
- if err != nil {
- t.Fatalf("From header does not parse: %v", err)
- }
- if from.Name != "3x-ui" || from.Address != "[email protected]" {
- t.Errorf("From = %q <%q>, want name %q addr %q", from.Name, from.Address, "3x-ui", "[email protected]")
- }
- if _, err := msg.Header.Date(); err != nil {
- t.Errorf("Date header missing or unparseable: %v", err)
- }
- id := msg.Header.Get("Message-ID")
- if !strings.HasPrefix(id, "<") || !strings.HasSuffix(id, "@example.com>") {
- t.Errorf("Message-ID = %q, want <[email protected]>", id)
- }
- subject, err := (&mime.WordDecoder{}).DecodeHeader(msg.Header.Get("Subject"))
- if err != nil {
- t.Fatalf("Subject does not decode: %v", err)
- }
- if subject != "Тест" {
- t.Errorf("Subject = %q, want %q", subject, "Тест")
- }
- body, _ := io.ReadAll(msg.Body)
- if string(body) != "<b>hi</b>" {
- t.Errorf("body = %q, want %q", body, "<b>hi</b>")
- }
- }
- func TestBuildMessageFromWithoutName(t *testing.T) {
- raw := buildMessage("[email protected]", "", []string{"[email protected]"}, "s", "b")
- msg, err := mail.ReadMessage(strings.NewReader(string(raw)))
- if err != nil {
- t.Fatalf("parse: %v", err)
- }
- from, err := mail.ParseAddress(msg.Header.Get("From"))
- if err != nil {
- t.Fatalf("From header does not parse: %v", err)
- }
- if from.Name != "" || from.Address != "[email protected]" {
- t.Errorf("From = %q <%q>, want bare addr", from.Name, from.Address)
- }
- }
- func startFakeSMTPServer(t *testing.T) (string, func() []string) {
- t.Helper()
- ln, err := net.Listen("tcp", "127.0.0.1:0")
- if err != nil {
- t.Fatal(err)
- }
- t.Cleanup(func() { _ = ln.Close() })
- var mu sync.Mutex
- var lines []string
- record := func(line string) {
- mu.Lock()
- defer mu.Unlock()
- lines = append(lines, line)
- }
- go func() {
- conn, err := ln.Accept()
- if err != nil {
- return
- }
- defer conn.Close()
- reader := bufio.NewReader(conn)
- fmt.Fprint(conn, "220 fake ready\r\n")
- inData := false
- for {
- line, err := reader.ReadString('\n')
- if err != nil {
- return
- }
- line = strings.TrimRight(line, "\r\n")
- record(line)
- if inData {
- if line == "." {
- inData = false
- fmt.Fprint(conn, "250 ok\r\n")
- }
- continue
- }
- switch {
- case strings.HasPrefix(line, "DATA"):
- inData = true
- fmt.Fprint(conn, "354 send\r\n")
- case strings.HasPrefix(line, "QUIT"):
- fmt.Fprint(conn, "221 bye\r\n")
- return
- default:
- fmt.Fprint(conn, "250 ok\r\n")
- }
- }
- }()
- return ln.Addr().String(), func() []string {
- mu.Lock()
- defer mu.Unlock()
- return append([]string(nil), lines...)
- }
- }
- func TestSendUsesBareAddressFromNameAddrSmtpFrom(t *testing.T) {
- if err := database.InitDB(filepath.Join(t.TempDir(), "x-ui.db")); err != nil {
- t.Fatal(err)
- }
- t.Cleanup(func() { _ = database.CloseDB() })
- addr, recordedLines := startFakeSMTPServer(t)
- host, portStr, err := net.SplitHostPort(addr)
- if err != nil {
- t.Fatal(err)
- }
- port, err := strconv.Atoi(portStr)
- if err != nil {
- t.Fatal(err)
- }
- settingService := service.SettingService{}
- mustSet := func(name string, err error) {
- t.Helper()
- if err != nil {
- t.Fatalf("set %s: %v", name, err)
- }
- }
- mustSet("host", settingService.SetSmtpHost(host))
- mustSet("port", settingService.SetSmtpPort(port))
- mustSet("from", settingService.SetSmtpFrom("3x-ui Panel <[email protected]>"))
- mustSet("to", settingService.SetSmtpTo("[email protected]"))
- mustSet("encryption", settingService.SetSmtpEncryptionType("none"))
- if err := NewEmailService(settingService).Send("subject", "<b>hi</b>"); err != nil {
- t.Fatalf("send: %v", err)
- }
- var mailFrom, fromHeader string
- for _, line := range recordedLines() {
- if strings.HasPrefix(line, "MAIL FROM:") {
- mailFrom = line
- }
- if strings.HasPrefix(line, "From: ") {
- fromHeader = line
- }
- }
- if want := "MAIL FROM:<[email protected]>"; mailFrom != want {
- t.Errorf("envelope sender = %q, want %q", mailFrom, want)
- }
- if want := `From: "3x-ui Panel" <[email protected]>`; fromHeader != want {
- t.Errorf("from header = %q, want %q", fromHeader, want)
- }
- }
- func TestBuildMessageStripsHeaderInjection(t *testing.T) {
- raw := buildMessage(
- "[email protected]\r\nBcc: [email protected]",
- "Name\r\nX-Evil: 1",
- []string{"[email protected]"}, "s", "b",
- )
- msg, err := mail.ReadMessage(strings.NewReader(string(raw)))
- if err != nil {
- t.Fatalf("parse: %v", err)
- }
- if got := msg.Header.Get("Bcc"); got != "" {
- t.Errorf("injected Bcc header leaked: %q", got)
- }
- if got := msg.Header.Get("X-Evil"); got != "" {
- t.Errorf("injected X-Evil header leaked: %q", got)
- }
- }
|