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
}
|