// Package smtpclient sends classic RFC 5322 messages to mail2news gateways. package smtpclient import ( "crypto/tls" "errors" "fmt" "net" "net/mail" "net/smtp" "strings" "time" "golang.org/x/net/proxy" ) type Config struct { Host string Port string Mode string Username string Password string InsecureSkipVerify bool ProxyType string ProxyAddress string Timeout time.Duration } func Send(cfg Config, from string, recipients []string, message []byte) error { if err := validate(cfg, from, recipients); err != nil { return err } timeout := cfg.Timeout if timeout <= 0 { timeout = 30 * time.Second } target := net.JoinHostPort(cfg.Host, cfg.Port) dialer := &net.Dialer{Timeout: timeout, KeepAlive: 30 * time.Second} var conn net.Conn var err error if strings.EqualFold(cfg.ProxyType, "SOCKS5") { socks, proxyErr := proxy.SOCKS5("tcp", cfg.ProxyAddress, nil, dialer) if proxyErr != nil { return fmt.Errorf("configure SMTP SOCKS5 proxy: %w", proxyErr) } conn, err = socks.Dial("tcp", target) } else { conn, err = dialer.Dial("tcp", target) } if err != nil { return fmt.Errorf("connect SMTP server: %w", err) } defer conn.Close() tlsConfig := &tls.Config{ServerName: cfg.Host, MinVersion: tls.VersionTLS12, InsecureSkipVerify: cfg.InsecureSkipVerify} // #nosec G402, explicit user opt-in if strings.EqualFold(cfg.Mode, "TLS") { tlsConn := tls.Client(conn, tlsConfig) if err := tlsConn.Handshake(); err != nil { return fmt.Errorf("SMTP TLS handshake: %w", err) } conn = tlsConn } client, err := smtp.NewClient(conn, cfg.Host) if err != nil { return fmt.Errorf("initialize SMTP client: %w", err) } defer client.Close() if strings.EqualFold(cfg.Mode, "STARTTLS") { if ok, _ := client.Extension("STARTTLS"); !ok { return errors.New("SMTP server does not advertise STARTTLS") } if err := client.StartTLS(tlsConfig); err != nil { return fmt.Errorf("SMTP STARTTLS: %w", err) } } if cfg.Username != "" { state, ok := client.TLSConnectionState() if !ok || !state.HandshakeComplete { return errors.New("SMTP authentication requires TLS") } if err := client.Auth(smtp.PlainAuth("", cfg.Username, cfg.Password, cfg.Host)); err != nil { return fmt.Errorf("SMTP authentication: %w", err) } } if err := client.Mail(from); err != nil { return fmt.Errorf("SMTP MAIL FROM: %w", err) } for _, recipient := range recipients { if err := client.Rcpt(recipient); err != nil { return fmt.Errorf("SMTP RCPT TO: %w", err) } } writer, err := client.Data() if err != nil { return fmt.Errorf("SMTP DATA: %w", err) } if _, err := writer.Write(message); err != nil { _ = writer.Close() return fmt.Errorf("write SMTP message: %w", err) } if err := writer.Close(); err != nil { return fmt.Errorf("finish SMTP message: %w", err) } return client.Quit() } func validate(cfg Config, from string, recipients []string) error { if strings.TrimSpace(cfg.Host) == "" || strings.TrimSpace(cfg.Port) == "" { return errors.New("SMTP host and port are required") } if cfg.Mode != "" && cfg.Mode != "TLS" && cfg.Mode != "STARTTLS" { return errors.New("SMTP mode must be cleartext, TLS or STARTTLS") } if cfg.ProxyType != "DIRECT" && cfg.ProxyType != "SOCKS5" { return errors.New("SMTP proxy type must be DIRECT or SOCKS5") } if strings.HasSuffix(strings.ToLower(cfg.Host), ".onion") && cfg.ProxyType != "SOCKS5" { return errors.New(".onion SMTP server requires SOCKS5") } if cfg.ProxyType == "SOCKS5" { if _, _, err := net.SplitHostPort(cfg.ProxyAddress); err != nil { return fmt.Errorf("invalid SMTP SOCKS5 address: %w", err) } } if cfg.Username != "" && cfg.Mode == "" { return errors.New("SMTP authentication requires TLS or STARTTLS") } if _, err := mail.ParseAddress(from); err != nil { return fmt.Errorf("invalid SMTP sender: %w", err) } if len(recipients) == 0 { return errors.New("at least one SMTP recipient is required") } for _, recipient := range recipients { if _, err := mail.ParseAddress(recipient); err != nil { return fmt.Errorf("invalid SMTP recipient: %w", err) } } return nil }