summaryrefslogtreecommitdiffstats
path: root/internal/smtpclient/client.go
blob: e29432313ab5fe4f5a061973d30d568309c1badf (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
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
}