// Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package sender import ( "crypto/tls" "errors" "fmt" "io" "net" "net/mail" "net/smtp" "os" "strings" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" ) // SMTPSender Sender SMTP mail sender type SMTPSender struct{} var _ Sender = &SMTPSender{} // Send send email func (s *SMTPSender) Send(from string, to []string, msg io.WriterTo) error { opts := setting.MailService var network string var address string if opts.Protocol == "smtp+unix" { network = "unix" address = opts.SMTPAddr } else { network = "tcp" address = net.JoinHostPort(opts.SMTPAddr, opts.SMTPPort) } conn, err := net.Dial(network, address) if err != nil { return fmt.Errorf("failed to establish network connection to SMTP server: %w", err) } defer conn.Close() var tlsconfig *tls.Config if opts.Protocol == "smtps" || opts.Protocol == "smtp+starttls" { tlsconfig = &tls.Config{ InsecureSkipVerify: opts.ForceTrustServerCert, ServerName: opts.SMTPAddr, } if opts.UseClientCert { cert, err := tls.LoadX509KeyPair(opts.ClientCertFile, opts.ClientKeyFile) if err != nil { return fmt.Errorf("could not load SMTP client certificate: %w", err) } tlsconfig.Certificates = []tls.Certificate{cert} } } if opts.Protocol == "smtps" { conn = tls.Client(conn, tlsconfig) } host := "localhost" if opts.Protocol == "smtp+unix" { host = opts.SMTPAddr } client, err := smtp.NewClient(conn, host) if err != nil { return fmt.Errorf("could not initiate SMTP session: %w", err) } if opts.EnableHelo { hostname := opts.HeloHostname if len(hostname) == 0 { hostname, err = os.Hostname() if err != nil { return fmt.Errorf("could not retrieve system hostname: %w", err) } } if err = client.Hello(hostname); err != nil { return fmt.Errorf("failed to issue HELO command: %w", err) } } if opts.Protocol == "smtp+starttls" { hasStartTLS, _ := client.Extension("STARTTLS") if hasStartTLS { if err = client.StartTLS(tlsconfig); err != nil { return fmt.Errorf("failed to start TLS connection: %w", err) } } else { log.Warn("StartTLS requested, but SMTP server does not support it; falling back to regular SMTP") } } canAuth, options := client.Extension("AUTH") if len(opts.User) > 0 { if !canAuth { return errors.New("SMTP server does not support AUTH, but credentials provided") } var auth smtp.Auth if strings.Contains(options, "CRAM-MD5") { auth = smtp.CRAMMD5Auth(opts.User, opts.Passwd) } else if strings.Contains(options, "PLAIN") { auth = smtp.PlainAuth("", opts.User, opts.Passwd, host) } else if strings.Contains(options, "LOGIN") { // Patch for AUTH LOGIN auth = LoginAuth(opts.User, opts.Passwd) } else if strings.Contains(options, "NTLM") { auth = NtlmAuth(opts.User, opts.Passwd) } if auth != nil { if err = client.Auth(auth); err != nil { return fmt.Errorf("failed to authenticate SMTP: %w", err) } } } fromAddr := from if opts.OverrideEnvelopeFrom && opts.EnvelopeFrom != "" { fromAddr = opts.EnvelopeFrom } smtpFrom, err := sanitizeEmailAddress(fromAddr) if err != nil { return fmt.Errorf("invalid envelope from address: %w", err) } if err = client.Mail(smtpFrom); err != nil { return fmt.Errorf("failed to issue MAIL command: %w", err) } for _, rec := range to { smtpTo, err := sanitizeEmailAddress(rec) if err != nil { return fmt.Errorf("invalid recipient address %q: %w", rec, err) } if err = client.Rcpt(smtpTo); err != nil { return fmt.Errorf("failed to issue RCPT command: %w", err) } } w, err := client.Data() if err != nil { return fmt.Errorf("failed to issue DATA command: %w", err) } else if _, err = msg.WriteTo(w); err != nil { return fmt.Errorf("SMTP write failed: %w", err) } else if err = w.Close(); err != nil { return fmt.Errorf("SMTP close failed: %w", err) } err = client.Quit() if err != nil { log.Error("Quit client failed: %v", err) } return nil } func sanitizeEmailAddress(raw string) (string, error) { addr, err := mail.ParseAddress(strings.TrimSpace(strings.Trim(raw, "<>"))) if err != nil { return "", err } return addr.Address, nil }