|
|
@@ -1,11 +1,20 @@
|
|
|
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) {
|
|
|
@@ -62,6 +71,115 @@ func TestBuildMessageFromWithoutName(t *testing.T) {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+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]",
|