summaryrefslogtreecommitdiffstats
path: root/internal/smtpclient/client.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/smtpclient/client.go')
-rw-r--r--internal/smtpclient/client.go139
1 files changed, 139 insertions, 0 deletions
diff --git a/internal/smtpclient/client.go b/internal/smtpclient/client.go
new file mode 100644
index 0000000..e294323
--- /dev/null
+++ b/internal/smtpclient/client.go
@@ -0,0 +1,139 @@
+// 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
+}