1Panel/agent/utils/email/smtp_sender.go

272 lines
7.2 KiB
Go

package email
import (
"context"
"crypto/tls"
"fmt"
"net"
"net/http"
"net/smtp"
"strings"
"time"
)
type SMTPConfig struct {
Host string
Port int
Username string
Password string
From string
Encryption string
Recipient string
}
type EmailMessage struct {
Subject string
Body string
IsHTML bool
}
type loginAuth struct {
username, password string
}
func LoginAuth(username, password string) smtp.Auth {
return &loginAuth{username, password}
}
func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
return "LOGIN", []byte{}, nil
}
func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
if more {
switch string(fromServer) {
case "Username:":
return []byte(a.username), nil
case "Password:":
return []byte(a.password), nil
default:
return nil, fmt.Errorf("unknown server challenge: %s", fromServer)
}
}
return nil, nil
}
func SendMail(config SMTPConfig, message EmailMessage, transport *http.Transport) error {
if err := validateConfig(config); err != nil {
return err
}
addr := fmt.Sprintf("%s:%d", config.Host, config.Port)
toList := parseRecipients(config.Recipient)
msg, err := buildMessage(config, message, toList)
if err != nil {
return err
}
switch strings.ToLower(config.Encryption) {
case "ssl":
return sendWithSSL(config, addr, toList, msg, transport)
case "starttls", "tls":
return sendWithStartTLS(config, addr, toList, msg, transport)
case "none":
return sendPlaintext(config, addr, toList, msg, transport)
default:
return fmt.Errorf("unsupported encryption type: %s", config.Encryption)
}
}
func validateConfig(config SMTPConfig) error {
if config.Host == "" {
return fmt.Errorf("SMTP host is required")
}
if config.Port <= 0 {
return fmt.Errorf("invalid SMTP port: %d", config.Port)
}
if config.Username == "" {
return fmt.Errorf("SMTP username is required")
}
if config.Password == "" {
return fmt.Errorf("SMTP password is required")
}
if config.From == "" {
return fmt.Errorf("SMTP from address is required")
}
if config.Recipient == "" {
return fmt.Errorf("SMTP recipient is required")
}
if !isValidEncryption(config.Encryption) {
return fmt.Errorf("invalid encryption type: %s. Allowed: ssl, starttls, none", config.Encryption)
}
return nil
}
func isValidEncryption(enc string) bool {
enc = strings.ToLower(enc)
return enc == "ssl" || enc == "starttls" || enc == "none" || enc == "tls"
}
func parseRecipients(recipient string) []string {
toList := strings.Split(recipient, ",")
for i := range toList {
toList[i] = strings.TrimSpace(toList[i])
}
return toList
}
func buildMessage(config SMTPConfig, message EmailMessage, toList []string) (string, error) {
headers := make(map[string]string)
headers["From"] = config.From
headers["To"] = strings.Join(toList, ",")
headers["Subject"] = message.Subject
headers["Date"] = time.Now().UTC().Format(time.RFC1123Z)
if message.IsHTML {
headers["MIME-version"] = "1.0"
headers["Content-Type"] = "text/html; charset=\"UTF-8\""
} else {
headers["Content-Type"] = "text/plain; charset=\"UTF-8\""
}
var msg strings.Builder
for k, v := range headers {
if !isValidHeader(k, v) {
return "", fmt.Errorf("invalid header: %s: %s", k, v)
}
msg.WriteString(fmt.Sprintf("%s: %s\r\n", k, v))
}
msg.WriteString("\r\n" + message.Body)
return msg.String(), nil
}
func isValidHeader(key, value string) bool {
return !strings.ContainsAny(key, "\r\n") && !strings.ContainsAny(value, "\r\n")
}
func sendWithSSL(config SMTPConfig, addr string, toList []string, msg string, transport *http.Transport) error {
var err error
var conn net.Conn
if transport != nil && transport.DialContext != nil {
conn, err = transport.DialContext(context.Background(), "tcp", addr)
} else {
conn, err = net.Dial("tcp", addr)
}
if err != nil {
return fmt.Errorf("failed to connect to SMTP server: %w", err)
}
defer conn.Close()
tlsConfig := &tls.Config{
ServerName: config.Host,
}
tlsConn := tls.Client(conn, tlsConfig)
if err := tlsConn.Handshake(); err != nil {
return fmt.Errorf("TLS handshake failed: %w", err)
}
client, err := smtp.NewClient(tlsConn, config.Host)
if err != nil {
return fmt.Errorf("failed to create SMTP client: %w", err)
}
defer client.Quit()
if err := tryAuth(client, config.Username, config.Password, config.Host); err != nil {
return fmt.Errorf("authentication failed: %w", err)
}
return sendEmailWithClient(client, config, toList, msg)
}
func sendWithStartTLS(config SMTPConfig, addr string, toList []string, msg string, transport *http.Transport) error {
var err error
var conn net.Conn
if transport != nil && transport.DialContext != nil {
conn, err = transport.DialContext(context.Background(), "tcp", addr)
} else {
conn, err = net.Dial("tcp", addr)
}
if err != nil {
return fmt.Errorf("failed to connect to SMTP server: %w", err)
}
defer conn.Close()
client, err := smtp.NewClient(conn, config.Host)
if err != nil {
return fmt.Errorf("failed to create SMTP client: %w", err)
}
defer client.Quit()
if err = client.StartTLS(&tls.Config{ServerName: config.Host}); err != nil {
return fmt.Errorf("failed to start TLS: %w", err)
}
if err := tryAuth(client, config.Username, config.Password, config.Host); err != nil {
return fmt.Errorf("authentication failed: %w", err)
}
return sendEmailWithClient(client, config, toList, msg)
}
func sendPlaintext(config SMTPConfig, addr string, toList []string, msg string, transport *http.Transport) error {
var err error
var conn net.Conn
if transport != nil && transport.DialContext != nil {
conn, err = transport.DialContext(context.Background(), "tcp", addr)
} else {
conn, err = net.Dial("tcp", addr)
}
if err != nil {
return fmt.Errorf("failed to connect to SMTP server: %w", err)
}
defer conn.Close()
client, err := smtp.NewClient(conn, config.Host)
if err != nil {
return fmt.Errorf("failed to create SMTP client: %w", err)
}
return sendEmailWithClient(client, config, toList, msg)
}
func sendEmailWithClient(client *smtp.Client, config SMTPConfig, toList []string, msg string) error {
if err := client.Mail(config.Username); err != nil {
return fmt.Errorf("setting sender failed: %w", err)
}
for _, addr := range toList {
if err := client.Rcpt(addr); err != nil {
return fmt.Errorf("adding recipient %s failed: %w", addr, err)
}
}
w, err := client.Data()
if err != nil {
return fmt.Errorf("preparing data failed: %w", err)
}
defer w.Close()
if _, err := w.Write([]byte(msg)); err != nil {
return fmt.Errorf("writing message failed: %w", err)
}
return nil
}
func tryAuth(client *smtp.Client, username, password, host string) error {
ok, authCap := client.Extension("AUTH")
if !ok {
return fmt.Errorf("server does not support AUTH")
}
authCap = strings.ToUpper(authCap)
if strings.Contains(authCap, "PLAIN") {
auth := smtp.PlainAuth("", username, password, host)
if err := client.Auth(auth); err != nil {
return fmt.Errorf("plain auth failed: %w", err)
}
return nil
}
if strings.Contains(authCap, "LOGIN") {
if err := client.Auth(LoginAuth(username, password)); err != nil {
return fmt.Errorf("login auth failed: %w", err)
}
return nil
}
return fmt.Errorf("no supported auth mechanism, server supports: %s", authCap)
}