go-mail/client.go
Winni Neessen 6cd3cfd2f7
Refactor comments for clarity and consistency
Rephrase comments to enhance clarity and maintain consistent style. Improved explanations for fields such as `smtpAuth`, `helo`, and `noNoop`, and standardize grammar and format across all comments. This helps in better understanding the code and its functionality.
2024-10-04 20:57:51 +02:00

963 lines
28 KiB
Go

// SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
//
// SPDX-License-Identifier: MIT
package mail
import (
"context"
"crypto/tls"
"errors"
"fmt"
"net"
"os"
"strings"
"sync"
"time"
"github.com/wneessen/go-mail/log"
"github.com/wneessen/go-mail/smtp"
)
const (
// DefaultPort is the default connection port to the SMTP server.
DefaultPort = 25
// DefaultPortSSL is the default connection port for SSL/TLS to the SMTP server.
DefaultPortSSL = 465
// DefaultPortTLS is the default connection port for STARTTLS to the SMTP server.
DefaultPortTLS = 587
// DefaultTimeout is the default connection timeout.
DefaultTimeout = time.Second * 15
// DefaultTLSPolicy specifies the default TLS policy for connections.
DefaultTLSPolicy = TLSMandatory
// DefaultTLSMinVersion defines the minimum TLS version to be used for secure connections.
// Nowadays TLS 1.2 is assumed be a sane default.
DefaultTLSMinVersion = tls.VersionTLS12
)
const (
// DSNMailReturnHeadersOnly requests that only the message headers of the mail message are returned in
// a DSN (Delivery Status Notification).
// https://datatracker.ietf.org/doc/html/rfc1891#section-5.3
DSNMailReturnHeadersOnly DSNMailReturnOption = "HDRS"
// DSNMailReturnFull requests that the entire mail message is returned in any failed DSN
// (Delivery Status Notification) issued for this recipient.
// https://datatracker.ietf.org/doc/html/rfc1891/#section-5.3
DSNMailReturnFull DSNMailReturnOption = "FULL"
// DSNRcptNotifyNever indicates that no DSN (Delivery Status Notifications) should be sent for the
// recipient under any condition.
// https://datatracker.ietf.org/doc/html/rfc1891/#section-5.1
DSNRcptNotifyNever DSNRcptNotifyOption = "NEVER"
// DSNRcptNotifySuccess indicates that the sender requests a DSN (Delivery Status Notification) if the
// message is successfully delivered.
// https://datatracker.ietf.org/doc/html/rfc1891/#section-5.1
DSNRcptNotifySuccess DSNRcptNotifyOption = "SUCCESS"
// DSNRcptNotifyFailure requests that a DSN (Delivery Status Notification) is issued if delivery of
// a message fails.
// https://datatracker.ietf.org/doc/html/rfc1891/#section-5.1
DSNRcptNotifyFailure DSNRcptNotifyOption = "FAILURE"
// DSNRcptNotifyDelay indicates the sender's willingness to receive "delayed" DSNs.
//
// Delayed DSNs may be issued if delivery of a message has been delayed for an unusual amount of time
// (as determined by the MTA at which the message is delayed), but the final delivery status (whether
// successful or failure) cannot be determined. The absence of the DELAY keyword in a NOTIFY parameter
// requests that a "delayed" DSN NOT be issued under any conditions.
// https://datatracker.ietf.org/doc/html/rfc1891/#section-5.1
DSNRcptNotifyDelay DSNRcptNotifyOption = "DELAY"
)
type (
// DialContextFunc defines a function type for establishing a network connection using context, network
// type, and address. It is used to specify custom DialContext function.
//
// By default we use net.Dial or tls.Dial respectively.
DialContextFunc func(ctx context.Context, network, address string) (net.Conn, error)
// DSNMailReturnOption is a type wrapper for a string and specifies the type of return content requested
// in a Delivery Status Notification (DSN).
// https://datatracker.ietf.org/doc/html/rfc1891/
DSNMailReturnOption string
// DSNRcptNotifyOption is a type wrapper for a string and specifies the notification options for a
// recipient in DSNs.
// https://datatracker.ietf.org/doc/html/rfc1891/
DSNRcptNotifyOption string
// Option is a function type that modifies the configuration or behavior of a Client instance.
Option func(*Client) error
// Client is the go-mail client that is responsible for connecting and interacting with an SMTP server.
Client struct {
// connTimeout specifies timeout for the connection to the SMTP server.
connTimeout time.Duration
// dialContextFunc is the DialContextFunc that is used by the Client to connect to the SMTP server.
dialContextFunc DialContextFunc
// dsnRcptNotifyType represents the different types of notifications for DSN (Delivery Status Notifications)
// receipts.
dsnRcptNotifyType []string
// dsnReturnType specifies the type of Delivery Status Notification (DSN) that should be requested for an
// email.
dsnReturnType DSNMailReturnOption
// fallbackPort is used as an alternative port number in case the primary port is unavailable or
// fails to bind.
//
// The fallbackPort is only used in combination with SetTLSPortPolicy and SetSSLPort correspondingly.
fallbackPort int
// helo is the hostname used in the HELO/EHLO greeting, that is sent to the target SMTP server.
//
// helo might be different as host. This can be useful in a shared-hosting scenario.
helo string
// host is the hostname of the SMTP server we are connecting to.
host string
// isEncrypted indicates wether the Client connection is encrypted or not.
isEncrypted bool
// logger is a logger that satisfies the log.Logger interface.
logger log.Logger
// mutex is used to synchronize access to shared resources, ensuring that only one goroutine can
// modify them at a time.
mutex sync.RWMutex
// noNoop indicates that the Client should skip the "NOOP" command during the dial.
//
// This is useful for servers which delay potentially unwanted clients when they perform commands
// other than AUTH.
noNoop bool
// pass represents a password or a secret token used for the SMTP authentication.
pass string
// port specifies the network port that is used to establish the connection with the SMTP server.
port int
// requestDSN indicates wether we want to request DSN (Delivery Status Notifications).
requestDSN bool
// smtpAuth is the authentication type that is used to authenticate the user with SMTP server. It
// satisfies the smtp.Auth interface.
//
// Unless you plan to write you own custom authentication method, it is advised to not set this manually.
// You should use one of go-mail's SMTPAuthType, instead.
smtpAuth smtp.Auth
// smtpAuthType specifies the authentication type to be used for SMTP authentication.
smtpAuthType SMTPAuthType
// smtpClient is an instance of smtp.Client used for handling the communication with the SMTP server.
smtpClient *smtp.Client
// tlspolicy defines the TLSPolicy configuration the Client uses for the STARTTLS protocol.
// https://datatracker.ietf.org/doc/html/rfc3207#section-2
tlspolicy TLSPolicy
// tlsconfig is a pointer to tls.Config that specifies the TLS configuration for the STARTTLS communication.
tlsconfig *tls.Config
// useDebugLog indicates whether debug level logging is enabled for the Client.
useDebugLog bool
// user represents a username used for the SMTP authentication.
user string
// useSSL indicates whether to use SSL/TLS encryption for network communication.
// https://datatracker.ietf.org/doc/html/rfc8314
useSSL bool
}
)
var (
// ErrInvalidPort should be used if a port is specified that is not valid
ErrInvalidPort = errors.New("invalid port number")
// ErrInvalidTimeout should be used if a timeout is set that is zero or negative
ErrInvalidTimeout = errors.New("timeout cannot be zero or negative")
// ErrInvalidHELO should be used if an empty HELO sting is provided
ErrInvalidHELO = errors.New("invalid HELO/EHLO value - must not be empty")
// ErrInvalidTLSConfig should be used if an empty tls.Config is provided
ErrInvalidTLSConfig = errors.New("invalid TLS config")
// ErrNoHostname should be used if a Client has no hostname set
ErrNoHostname = errors.New("hostname for client cannot be empty")
// ErrDeadlineExtendFailed should be used if the extension of the connection deadline fails
ErrDeadlineExtendFailed = errors.New("connection deadline extension failed")
// ErrNoActiveConnection should be used when a method is used that requies a server connection
// but is not yet connected
ErrNoActiveConnection = errors.New("not connected to SMTP server")
// ErrServerNoUnencoded should be used when 8BIT encoding is selected for a message, but
// the server does not offer 8BITMIME mode
ErrServerNoUnencoded = errors.New("message is 8bit unencoded, but server does not support 8BITMIME")
// ErrInvalidDSNMailReturnOption should be used when an invalid option is provided for the
// DSNMailReturnOption in WithDSN
ErrInvalidDSNMailReturnOption = errors.New("DSN mail return option can only be HDRS or FULL")
// ErrInvalidDSNRcptNotifyOption should be used when an invalid option is provided for the
// DSNRcptNotifyOption in WithDSN
ErrInvalidDSNRcptNotifyOption = errors.New("DSN rcpt notify option can only be: NEVER, " +
"SUCCESS, FAILURE or DELAY")
// ErrInvalidDSNRcptNotifyCombination should be used when an invalid option is provided for the
// DSNRcptNotifyOption in WithDSN
ErrInvalidDSNRcptNotifyCombination = errors.New("DSN rcpt notify option NEVER cannot be " +
"combined with any of SUCCESS, FAILURE or DELAY")
)
// NewClient returns a new Session client object
func NewClient(host string, opts ...Option) (*Client, error) {
c := &Client{
connTimeout: DefaultTimeout,
host: host,
port: DefaultPort,
tlsconfig: &tls.Config{ServerName: host, MinVersion: DefaultTLSMinVersion},
tlspolicy: DefaultTLSPolicy,
}
// Set default HELO/EHLO hostname
if err := c.setDefaultHelo(); err != nil {
return c, err
}
// Override defaults with optionally provided Option functions
for _, opt := range opts {
if opt == nil {
continue
}
if err := opt(c); err != nil {
return c, fmt.Errorf("failed to apply option: %w", err)
}
}
// Some settings in a Client cannot be empty/unset
if c.host == "" {
return c, ErrNoHostname
}
return c, nil
}
// WithPort overrides the default connection port
func WithPort(port int) Option {
return func(c *Client) error {
if port < 1 || port > 65535 {
return ErrInvalidPort
}
c.port = port
return nil
}
}
// WithTimeout overrides the default connection timeout
func WithTimeout(timeout time.Duration) Option {
return func(c *Client) error {
if timeout <= 0 {
return ErrInvalidTimeout
}
c.connTimeout = timeout
return nil
}
}
// WithSSL tells the client to use a SSL/TLS connection
func WithSSL() Option {
return func(c *Client) error {
c.useSSL = true
return nil
}
}
// WithSSLPort tells the Client wether or not to use SSL and fallback.
// The correct port is automatically set.
//
// Port 465 is used when SSL set (true).
// Port 25 is used when SSL is unset (false).
// When the SSL connection fails and fb is set to true,
// the client will attempt to connect on port 25 using plaintext.
//
// Note: If a different port has already been set otherwise, the port-choosing
// and fallback automatism will be skipped.
func WithSSLPort(fallback bool) Option {
return func(c *Client) error {
c.SetSSLPort(true, fallback)
return nil
}
}
// WithDebugLog tells the client to log incoming and outgoing messages of the SMTP client
// to StdErr
func WithDebugLog() Option {
return func(c *Client) error {
c.useDebugLog = true
return nil
}
}
// WithLogger overrides the default log.Logger that is used for debug logging
func WithLogger(logger log.Logger) Option {
return func(c *Client) error {
c.logger = logger
return nil
}
}
// WithHELO tells the client to use the provided string as HELO/EHLO greeting host
func WithHELO(helo string) Option {
return func(c *Client) error {
if helo == "" {
return ErrInvalidHELO
}
c.helo = helo
return nil
}
}
// WithTLSPolicy tells the client to use the provided TLSPolicy
//
// Note: To follow best-practices for SMTP TLS connections, it is recommended
// to use WithTLSPortPolicy instead.
func WithTLSPolicy(policy TLSPolicy) Option {
return func(c *Client) error {
c.tlspolicy = policy
return nil
}
}
// WithTLSPortPolicy tells the client to use the provided TLSPolicy,
// The correct port is automatically set.
//
// Port 587 is used for TLSMandatory and TLSOpportunistic.
// If the connection fails with TLSOpportunistic,
// a plaintext connection is attempted on port 25 as a fallback.
// NoTLS will allways use port 25.
//
// Note: If a different port has already been set otherwise, the port-choosing
// and fallback automatism will be skipped.
func WithTLSPortPolicy(policy TLSPolicy) Option {
return func(c *Client) error {
c.SetTLSPortPolicy(policy)
return nil
}
}
// WithTLSConfig tells the client to use the provided *tls.Config
func WithTLSConfig(tlsconfig *tls.Config) Option {
return func(c *Client) error {
if tlsconfig == nil {
return ErrInvalidTLSConfig
}
c.tlsconfig = tlsconfig
return nil
}
}
// WithSMTPAuth tells the client to use the provided SMTPAuthType for authentication
func WithSMTPAuth(authtype SMTPAuthType) Option {
return func(c *Client) error {
c.smtpAuthType = authtype
return nil
}
}
// WithSMTPAuthCustom tells the client to use the provided smtp.Auth for SMTP authentication
func WithSMTPAuthCustom(smtpAuth smtp.Auth) Option {
return func(c *Client) error {
c.smtpAuth = smtpAuth
return nil
}
}
// WithUsername tells the client to use the provided string as username for authentication
func WithUsername(username string) Option {
return func(c *Client) error {
c.user = username
return nil
}
}
// WithPassword tells the client to use the provided string as password/secret for authentication
func WithPassword(password string) Option {
return func(c *Client) error {
c.pass = password
return nil
}
}
// WithDSN enables the Client to request DSNs (if the server supports it)
// as described in the RFC 1891 and set defaults for DSNMailReturnOption
// to DSNMailReturnFull and DSNRcptNotifyOption to DSNRcptNotifySuccess
// and DSNRcptNotifyFailure
func WithDSN() Option {
return func(c *Client) error {
c.requestDSN = true
c.dsnReturnType = DSNMailReturnFull
c.dsnRcptNotifyType = []string{string(DSNRcptNotifyFailure), string(DSNRcptNotifySuccess)}
return nil
}
}
// WithDSNMailReturnType enables the Client to request DSNs (if the server supports it)
// as described in the RFC 1891 and set the MAIL FROM Return option type to the
// given DSNMailReturnOption
// See: https://www.rfc-editor.org/rfc/rfc1891
func WithDSNMailReturnType(option DSNMailReturnOption) Option {
return func(c *Client) error {
switch option {
case DSNMailReturnHeadersOnly:
case DSNMailReturnFull:
default:
return ErrInvalidDSNMailReturnOption
}
c.requestDSN = true
c.dsnReturnType = option
return nil
}
}
// WithDSNRcptNotifyType enables the Client to request DSNs as described in the RFC 1891
// and sets the RCPT TO notify options to the given list of DSNRcptNotifyOption
// See: https://www.rfc-editor.org/rfc/rfc1891
func WithDSNRcptNotifyType(opts ...DSNRcptNotifyOption) Option {
return func(c *Client) error {
var rcptOpts []string
var ns, nns bool
if len(opts) > 0 {
for _, opt := range opts {
switch opt {
case DSNRcptNotifyNever:
ns = true
case DSNRcptNotifySuccess:
nns = true
case DSNRcptNotifyFailure:
nns = true
case DSNRcptNotifyDelay:
nns = true
default:
return ErrInvalidDSNRcptNotifyOption
}
rcptOpts = append(rcptOpts, string(opt))
}
}
if ns && nns {
return ErrInvalidDSNRcptNotifyCombination
}
c.requestDSN = true
c.dsnRcptNotifyType = rcptOpts
return nil
}
}
// WithoutNoop disables the Client Noop check during connections. This is primarily for servers which delay responses
// to SMTP commands that are not the AUTH command. For example Microsoft Exchange's Tarpit.
func WithoutNoop() Option {
return func(c *Client) error {
c.noNoop = true
return nil
}
}
// WithDialContextFunc overrides the default DialContext for connecting SMTP server
func WithDialContextFunc(dialCtxFunc DialContextFunc) Option {
return func(c *Client) error {
c.dialContextFunc = dialCtxFunc
return nil
}
}
// TLSPolicy returns the currently set TLSPolicy as string
func (c *Client) TLSPolicy() string {
return c.tlspolicy.String()
}
// ServerAddr returns the currently set combination of hostname and port
func (c *Client) ServerAddr() string {
return fmt.Sprintf("%s:%d", c.host, c.port)
}
// SetTLSPolicy overrides the current TLSPolicy with the given TLSPolicy value
//
// Note: To follow best-practices for SMTP TLS connections, it is recommended
// to use SetTLSPortPolicy instead.
func (c *Client) SetTLSPolicy(policy TLSPolicy) {
c.tlspolicy = policy
}
// SetTLSPortPolicy overrides the current TLSPolicy with the given TLSPolicy
// value. The correct port is automatically set.
//
// Port 587 is used for TLSMandatory and TLSOpportunistic.
// If the connection fails with TLSOpportunistic, a plaintext connection is
// attempted on port 25 as a fallback.
// NoTLS will allways use port 25.
//
// Note: If a different port has already been set otherwise, the port-choosing
// and fallback automatism will be skipped.
func (c *Client) SetTLSPortPolicy(policy TLSPolicy) {
if c.port == DefaultPort {
c.port = DefaultPortTLS
if policy == TLSOpportunistic {
c.fallbackPort = DefaultPort
}
if policy == NoTLS {
c.port = DefaultPort
}
}
c.tlspolicy = policy
}
// SetSSL tells the Client wether to use SSL or not
func (c *Client) SetSSL(ssl bool) {
c.useSSL = ssl
}
// SetSSLPort tells the Client wether or not to use SSL and fallback.
// The correct port is automatically set.
//
// Port 465 is used when SSL set (true).
// Port 25 is used when SSL is unset (false).
// When the SSL connection fails and fb is set to true,
// the client will attempt to connect on port 25 using plaintext.
//
// Note: If a different port has already been set otherwise, the port-choosing
// and fallback automatism will be skipped.
func (c *Client) SetSSLPort(ssl bool, fallback bool) {
if c.port == DefaultPort {
if ssl {
c.port = DefaultPortSSL
}
c.fallbackPort = 0
if fallback {
c.fallbackPort = DefaultPort
}
}
c.useSSL = ssl
}
// SetDebugLog tells the Client whether debug logging is enabled or not
func (c *Client) SetDebugLog(val bool) {
c.useDebugLog = val
if c.smtpClient != nil {
c.smtpClient.SetDebugLog(val)
}
}
// SetLogger tells the Client which log.Logger to use
func (c *Client) SetLogger(logger log.Logger) {
c.logger = logger
if c.smtpClient != nil {
c.smtpClient.SetLogger(logger)
}
}
// SetTLSConfig overrides the current *tls.Config with the given *tls.Config value
func (c *Client) SetTLSConfig(tlsconfig *tls.Config) error {
c.mutex.Lock()
defer c.mutex.Unlock()
if tlsconfig == nil {
return ErrInvalidTLSConfig
}
c.tlsconfig = tlsconfig
return nil
}
// SetUsername overrides the current username string with the given value
func (c *Client) SetUsername(username string) {
c.user = username
}
// SetPassword overrides the current password string with the given value
func (c *Client) SetPassword(password string) {
c.pass = password
}
// SetSMTPAuth overrides the current SMTP AUTH type setting with the given value
func (c *Client) SetSMTPAuth(authtype SMTPAuthType) {
c.smtpAuthType = authtype
c.smtpAuth = nil
}
// SetSMTPAuthCustom overrides the current SMTP AUTH setting with the given custom smtp.Auth
func (c *Client) SetSMTPAuthCustom(smtpAuth smtp.Auth) {
c.smtpAuth = smtpAuth
}
// setDefaultHelo retrieves the current hostname and sets it as HELO/EHLO hostname
func (c *Client) setDefaultHelo() error {
hostname, err := os.Hostname()
if err != nil {
return fmt.Errorf("failed to read local hostname: %w", err)
}
c.helo = hostname
return nil
}
// DialWithContext establishes a connection to the SMTP server with a given context.Context
func (c *Client) DialWithContext(dialCtx context.Context) error {
c.mutex.Lock()
defer c.mutex.Unlock()
ctx, cancel := context.WithDeadline(dialCtx, time.Now().Add(c.connTimeout))
defer cancel()
if c.dialContextFunc == nil {
netDialer := net.Dialer{}
c.dialContextFunc = netDialer.DialContext
if c.useSSL {
tlsDialer := tls.Dialer{NetDialer: &netDialer, Config: c.tlsconfig}
c.isEncrypted = true
c.dialContextFunc = tlsDialer.DialContext
}
}
connection, err := c.dialContextFunc(ctx, "tcp", c.ServerAddr())
if err != nil && c.fallbackPort != 0 {
// TODO: should we somehow log or append the previous error?
connection, err = c.dialContextFunc(ctx, "tcp", c.serverFallbackAddr())
}
if err != nil {
return err
}
client, err := smtp.NewClient(connection, c.host)
if err != nil {
return err
}
if client == nil {
return fmt.Errorf("SMTP client is nil")
}
c.smtpClient = client
if c.logger != nil {
c.smtpClient.SetLogger(c.logger)
}
if c.useDebugLog {
c.smtpClient.SetDebugLog(true)
}
if err = c.smtpClient.Hello(c.helo); err != nil {
return err
}
if err = c.tls(); err != nil {
return err
}
if err = c.auth(); err != nil {
return err
}
return nil
}
// Close closes the Client connection
func (c *Client) Close() error {
if err := c.checkConn(); err != nil {
return err
}
if err := c.smtpClient.Quit(); err != nil {
return fmt.Errorf("failed to close SMTP client: %w", err)
}
return nil
}
// Reset sends the RSET command to the SMTP client
func (c *Client) Reset() error {
if err := c.checkConn(); err != nil {
return err
}
if err := c.smtpClient.Reset(); err != nil {
return fmt.Errorf("failed to send RSET to SMTP client: %w", err)
}
return nil
}
// DialAndSend establishes a connection to the SMTP server with a
// default context.Background and sends the mail
func (c *Client) DialAndSend(messages ...*Msg) error {
ctx := context.Background()
return c.DialAndSendWithContext(ctx, messages...)
}
// DialAndSendWithContext establishes a connection to the SMTP server with a
// custom context and sends the mail
func (c *Client) DialAndSendWithContext(ctx context.Context, messages ...*Msg) error {
if err := c.DialWithContext(ctx); err != nil {
return fmt.Errorf("dial failed: %w", err)
}
if err := c.Send(messages...); err != nil {
return fmt.Errorf("send failed: %w", err)
}
if err := c.Close(); err != nil {
return fmt.Errorf("failed to close connection: %w", err)
}
return nil
}
// checkConn makes sure that a required server connection is available and extends the
// connection deadline
func (c *Client) checkConn() error {
if !c.smtpClient.HasConnection() {
return ErrNoActiveConnection
}
if !c.noNoop {
if err := c.smtpClient.Noop(); err != nil {
return ErrNoActiveConnection
}
}
if err := c.smtpClient.UpdateDeadline(c.connTimeout); err != nil {
return ErrDeadlineExtendFailed
}
return nil
}
// serverFallbackAddr returns the currently set combination of hostname
// and fallback port.
func (c *Client) serverFallbackAddr() string {
return fmt.Sprintf("%s:%d", c.host, c.fallbackPort)
}
// tls tries to make sure that the STARTTLS requirements are satisfied
func (c *Client) tls() error {
if !c.smtpClient.HasConnection() {
return ErrNoActiveConnection
}
if !c.useSSL && c.tlspolicy != NoTLS {
hasStartTLS := false
extension, _ := c.smtpClient.Extension("STARTTLS")
if c.tlspolicy == TLSMandatory {
hasStartTLS = true
if !extension {
return fmt.Errorf("STARTTLS mode set to: %q, but target host does not support STARTTLS",
c.tlspolicy)
}
}
if c.tlspolicy == TLSOpportunistic {
if extension {
hasStartTLS = true
}
}
if hasStartTLS {
if err := c.smtpClient.StartTLS(c.tlsconfig); err != nil {
return err
}
}
tlsConnState, err := c.smtpClient.GetTLSConnectionState()
if err != nil {
switch {
case errors.Is(err, smtp.ErrNonTLSConnection):
c.isEncrypted = false
return nil
default:
return fmt.Errorf("failed to get TLS connection state: %w", err)
}
}
c.isEncrypted = tlsConnState.HandshakeComplete
}
return nil
}
// auth will try to perform SMTP AUTH if requested
func (c *Client) auth() error {
if err := c.checkConn(); err != nil {
return fmt.Errorf("failed to authenticate: %w", err)
}
if c.smtpAuth == nil && c.smtpAuthType != "" {
hasSMTPAuth, smtpAuthType := c.smtpClient.Extension("AUTH")
if !hasSMTPAuth {
return fmt.Errorf("server does not support SMTP AUTH")
}
switch c.smtpAuthType {
case SMTPAuthPlain:
if !strings.Contains(smtpAuthType, string(SMTPAuthPlain)) {
return ErrPlainAuthNotSupported
}
c.smtpAuth = smtp.PlainAuth("", c.user, c.pass, c.host)
case SMTPAuthLogin:
if !strings.Contains(smtpAuthType, string(SMTPAuthLogin)) {
return ErrLoginAuthNotSupported
}
c.smtpAuth = smtp.LoginAuth(c.user, c.pass, c.host)
case SMTPAuthCramMD5:
if !strings.Contains(smtpAuthType, string(SMTPAuthCramMD5)) {
return ErrCramMD5AuthNotSupported
}
c.smtpAuth = smtp.CRAMMD5Auth(c.user, c.pass)
case SMTPAuthXOAUTH2:
if !strings.Contains(smtpAuthType, string(SMTPAuthXOAUTH2)) {
return ErrXOauth2AuthNotSupported
}
c.smtpAuth = smtp.XOAuth2Auth(c.user, c.pass)
case SMTPAuthSCRAMSHA1:
if !strings.Contains(smtpAuthType, string(SMTPAuthSCRAMSHA1)) {
return ErrSCRAMSHA1AuthNotSupported
}
c.smtpAuth = smtp.ScramSHA1Auth(c.user, c.pass)
case SMTPAuthSCRAMSHA256:
if !strings.Contains(smtpAuthType, string(SMTPAuthSCRAMSHA256)) {
return ErrSCRAMSHA256AuthNotSupported
}
c.smtpAuth = smtp.ScramSHA256Auth(c.user, c.pass)
case SMTPAuthSCRAMSHA1PLUS:
if !strings.Contains(smtpAuthType, string(SMTPAuthSCRAMSHA1PLUS)) {
return ErrSCRAMSHA1PLUSAuthNotSupported
}
tlsConnState, err := c.smtpClient.GetTLSConnectionState()
if err != nil {
return err
}
c.smtpAuth = smtp.ScramSHA1PlusAuth(c.user, c.pass, tlsConnState)
case SMTPAuthSCRAMSHA256PLUS:
if !strings.Contains(smtpAuthType, string(SMTPAuthSCRAMSHA256PLUS)) {
return ErrSCRAMSHA256PLUSAuthNotSupported
}
tlsConnState, err := c.smtpClient.GetTLSConnectionState()
if err != nil {
return err
}
c.smtpAuth = smtp.ScramSHA256PlusAuth(c.user, c.pass, tlsConnState)
default:
return fmt.Errorf("unsupported SMTP AUTH type %q", c.smtpAuthType)
}
}
if c.smtpAuth != nil {
if err := c.smtpClient.Auth(c.smtpAuth); err != nil {
return fmt.Errorf("SMTP AUTH failed: %w", err)
}
}
return nil
}
// sendSingleMsg sends out a single message and returns an error if the transmission/delivery fails.
// It is invoked by the public Send methods
func (c *Client) sendSingleMsg(message *Msg) error {
c.mutex.Lock()
defer c.mutex.Unlock()
if message.encoding == NoEncoding {
if ok, _ := c.smtpClient.Extension("8BITMIME"); !ok {
return &SendError{Reason: ErrNoUnencoded, isTemp: false, affectedMsg: message}
}
}
from, err := message.GetSender(false)
if err != nil {
return &SendError{
Reason: ErrGetSender, errlist: []error{err}, isTemp: isTempError(err),
affectedMsg: message,
}
}
rcpts, err := message.GetRecipients()
if err != nil {
return &SendError{
Reason: ErrGetRcpts, errlist: []error{err}, isTemp: isTempError(err),
affectedMsg: message,
}
}
if c.requestDSN {
if c.dsnReturnType != "" {
c.smtpClient.SetDSNMailReturnOption(string(c.dsnReturnType))
}
}
if err = c.smtpClient.Mail(from); err != nil {
retError := &SendError{
Reason: ErrSMTPMailFrom, errlist: []error{err}, isTemp: isTempError(err),
affectedMsg: message,
}
if resetSendErr := c.smtpClient.Reset(); resetSendErr != nil {
retError.errlist = append(retError.errlist, resetSendErr)
}
return retError
}
hasError := false
rcptSendErr := &SendError{affectedMsg: message}
rcptSendErr.errlist = make([]error, 0)
rcptSendErr.rcpt = make([]string, 0)
rcptNotifyOpt := strings.Join(c.dsnRcptNotifyType, ",")
c.smtpClient.SetDSNRcptNotifyOption(rcptNotifyOpt)
for _, rcpt := range rcpts {
if err = c.smtpClient.Rcpt(rcpt); err != nil {
rcptSendErr.Reason = ErrSMTPRcptTo
rcptSendErr.errlist = append(rcptSendErr.errlist, err)
rcptSendErr.rcpt = append(rcptSendErr.rcpt, rcpt)
rcptSendErr.isTemp = isTempError(err)
hasError = true
}
}
if hasError {
if resetSendErr := c.smtpClient.Reset(); resetSendErr != nil {
rcptSendErr.errlist = append(rcptSendErr.errlist, resetSendErr)
}
return rcptSendErr
}
writer, err := c.smtpClient.Data()
if err != nil {
return &SendError{
Reason: ErrSMTPData, errlist: []error{err}, isTemp: isTempError(err),
affectedMsg: message,
}
}
_, err = message.WriteTo(writer)
if err != nil {
return &SendError{
Reason: ErrWriteContent, errlist: []error{err}, isTemp: isTempError(err),
affectedMsg: message,
}
}
message.isDelivered = true
if err = writer.Close(); err != nil {
return &SendError{
Reason: ErrSMTPDataClose, errlist: []error{err}, isTemp: isTempError(err),
affectedMsg: message,
}
}
if err = c.Reset(); err != nil {
return &SendError{
Reason: ErrSMTPReset, errlist: []error{err}, isTemp: isTempError(err),
affectedMsg: message,
}
}
if err = c.checkConn(); err != nil {
return &SendError{
Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err),
affectedMsg: message,
}
}
return nil
}