go-mail/client.go
Winni Neessen 6809084e80
Add enhanced status code and error code to SendError
Enhance error handling by adding error code and enhanced status code to the SendError struct. This allows for better troubleshooting and debugging by providing more detailed SMTP server responses.
2024-11-13 16:11:28 +01:00

1398 lines
49 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 responsible for connecting and interacting with an SMTP server.
//
// This struct represents the go-mail client, which manages the connection, authentication, and communication
// with an SMTP server. It contains various configuration options, including connection timeouts, encryption
// settings, authentication methods, and Delivery Status Notification (DSN) preferences.
//
// References:
// - https://datatracker.ietf.org/doc/html/rfc3207#section-2
// - https://datatracker.ietf.org/doc/html/rfc8314
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
// logAuthData indicates whether authentication-related data should be logged.
logAuthData 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 is returned when the specified port for the SMTP connection is not valid
ErrInvalidPort = errors.New("invalid port number")
// ErrInvalidTimeout is returned when the specified timeout is zero or negative.
ErrInvalidTimeout = errors.New("timeout cannot be zero or negative")
// ErrInvalidHELO is returned when the HELO/EHLO value is invalid due to being empty.
ErrInvalidHELO = errors.New("invalid HELO/EHLO value - must not be empty")
// ErrInvalidTLSConfig is returned when the provided TLS configuration is invalid or nil.
ErrInvalidTLSConfig = errors.New("invalid TLS config")
// ErrNoHostname is returned when the hostname for the client is not provided or empty.
ErrNoHostname = errors.New("hostname for client cannot be empty")
// ErrDeadlineExtendFailed is returned when an attempt to extend the connection deadline fails.
ErrDeadlineExtendFailed = errors.New("connection deadline extension failed")
// ErrNoActiveConnection indicates that there is no active connection to the SMTP server.
ErrNoActiveConnection = errors.New("not connected to SMTP server")
// ErrServerNoUnencoded indicates that the server does not support 8BITMIME for unencoded 8-bit messages.
ErrServerNoUnencoded = errors.New("message is 8bit unencoded, but server does not support 8BITMIME")
// ErrInvalidDSNMailReturnOption is returned when an invalid DSNMailReturnOption is provided as argument
// to the WithDSN Option.
ErrInvalidDSNMailReturnOption = errors.New("DSN mail return option can only be HDRS or FULL")
// ErrInvalidDSNRcptNotifyOption is returned when an invalid DSNRcptNotifyOption is provided as argument
// to the WithDSN Option.
ErrInvalidDSNRcptNotifyOption = errors.New("DSN rcpt notify option can only be: NEVER, " +
"SUCCESS, FAILURE or DELAY")
// ErrInvalidDSNRcptNotifyCombination is returned when an invalid combination of DSNRcptNotifyOption is
// provided as argument to the WithDSN Option.
ErrInvalidDSNRcptNotifyCombination = errors.New("DSN rcpt notify option NEVER cannot be " +
"combined with any of SUCCESS, FAILURE or DELAY")
// ErrSMTPAuthMethodIsNil indicates that the SMTP authentication method provided is nil
ErrSMTPAuthMethodIsNil = errors.New("SMTP auth method is nil")
// ErrDialContextFuncIsNil indicates that a required dial context function is not provided.
ErrDialContextFuncIsNil = errors.New("dial context function is nil")
)
// NewClient creates a new Client instance with the provided host and optional configuration Option functions.
//
// This function initializes a Client with default values, such as connection timeout, port, TLS settings,
// and the HELO/EHLO hostname. Option functions, if provided, can override the default configuration.
// It ensures that essential values, like the host, are set. An error is returned if critical defaults are unset.
//
// Parameters:
// - host: The hostname of the SMTP server to connect to.
// - opts: Optional configuration functions to override default settings.
//
// Returns:
// - A pointer to the initialized Client.
// - An error if any critical default values are missing or options fail to apply.
func NewClient(host string, opts ...Option) (*Client, error) {
c := &Client{
smtpAuthType: SMTPAuthNoAuth,
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 sets the port number for the Client and overrides the default port.
//
// This function sets the specified port number for the Client, ensuring that the port number is valid
// (between 1 and 65535). If the provided port number is invalid, an error is returned.
//
// Parameters:
// - port: The port number to be used by the Client. Must be between 1 and 65535.
//
// Returns:
// - An Option function that applies the port setting to the Client.
// - An error if the port number is outside the valid range.
func WithPort(port int) Option {
return func(c *Client) error {
if port < 1 || port > 65535 {
return ErrInvalidPort
}
c.port = port
return nil
}
}
// WithTimeout sets the connection timeout for the Client and overrides the default timeout.
//
// This function configures the Client with a specified connection timeout duration. It validates that the
// provided timeout is greater than zero. If the timeout is invalid, an error is returned.
//
// Parameters:
// - timeout: The duration to be set as the connection timeout. Must be greater than zero.
//
// Returns:
// - An Option function that applies the timeout setting to the Client.
// - An error if the timeout duration is invalid.
func WithTimeout(timeout time.Duration) Option {
return func(c *Client) error {
if timeout <= 0 {
return ErrInvalidTimeout
}
c.connTimeout = timeout
return nil
}
}
// WithSSL enables implicit SSL/TLS for the Client.
//
// This function configures the Client to use implicit SSL/TLS for secure communication.
//
// Returns:
// - An Option function that enables SSL/TLS for the Client.
func WithSSL() Option {
return func(c *Client) error {
c.useSSL = true
return nil
}
}
// WithSSLPort enables implicit SSL/TLS with an optional fallback for the Client. The correct port is
// automatically set.
//
// When this option is used with NewClient, the default port 25 is overridden with port 465 for SSL/TLS connections.
// If fallback is set to true and the SSL/TLS connection fails, the Client attempts to connect on port 25 using an
// unencrypted connection. If WithPort has already been used to set a different port, that port takes precedence,
// and the automatic fallback mechanism is skipped.
//
// Parameters:
// - fallback: A boolean indicating whether to fall back to port 25 without SSL/TLS if the connection fails.
//
// Returns:
// - An Option function that enables SSL/TLS and configures the fallback mechanism for the Client.
func WithSSLPort(fallback bool) Option {
return func(c *Client) error {
c.SetSSLPort(true, fallback)
return nil
}
}
// WithDebugLog enables debug logging for the Client.
//
// This function activates debug logging, which logs incoming and outgoing communication between the
// Client and the SMTP server to os.Stderr. By default the debug logging will redact any kind of SMTP
// authentication data. If you need access to the actual authentication data in your logs, you can
// enable authentication data logging with the WithLogAuthData option or by setting it with the
// Client.SetLogAuthData method.
//
// Returns:
// - An Option function that enables debug logging for the Client.
func WithDebugLog() Option {
return func(c *Client) error {
c.useDebugLog = true
return nil
}
}
// WithLogger defines a custom logger for the Client.
//
// This function sets a custom logger for the Client, which must satisfy the log.Logger interface. The custom
// logger is used only when debug logging is enabled. By default, log.Stdlog is used if no custom logger is provided.
//
// Parameters:
// - logger: A logger that satisfies the log.Logger interface.
//
// Returns:
// - An Option function that sets the custom logger for the Client.
func WithLogger(logger log.Logger) Option {
return func(c *Client) error {
c.logger = logger
return nil
}
}
// WithHELO sets the HELO/EHLO string used by the Client.
//
// This function configures the HELO/EHLO string sent by the Client when initiating communication
// with the SMTP server. By default, os.Hostname is used to identify the HELO/EHLO string.
//
// Parameters:
// - helo: The string to be used for the HELO/EHLO greeting. Must not be empty.
//
// Returns:
// - An Option function that sets the HELO/EHLO string for the Client.
// - An error if the provided HELO string is empty.
func WithHELO(helo string) Option {
return func(c *Client) error {
if helo == "" {
return ErrInvalidHELO
}
c.helo = helo
return nil
}
}
// WithTLSPolicy sets the TLSPolicy of the Client and overrides the DefaultTLSPolicy.
//
// This function configures the Client's TLSPolicy, specifying how the Client handles TLS for SMTP connections.
// It overrides the default policy. For best practices regarding SMTP TLS connections, it is recommended to use
// WithTLSPortPolicy instead.
//
// Parameters:
// - policy: The TLSPolicy to be applied to the Client.
//
// Returns:
// - An Option function that sets the TLSPolicy for the Client.
//
// WithTLSPortPolicy instead.
func WithTLSPolicy(policy TLSPolicy) Option {
return func(c *Client) error {
c.tlspolicy = policy
return nil
}
}
// WithTLSPortPolicy enables explicit TLS via STARTTLS for the Client using the provided TLSPolicy. The
// correct port is automatically set.
//
// When TLSMandatory or TLSOpportunistic is provided as the TLSPolicy, port 587 is used for the connection.
// If the connection fails with TLSOpportunistic, the Client attempts to connect on port 25 using an unencrypted
// connection as a fallback. If NoTLS is specified, the Client will always use port 25.
// If WithPort has already been used to set a different port, that port takes precedence, and the automatic fallback
// mechanism is skipped.
//
// Parameters:
// - policy: The TLSPolicy to be used for STARTTLS communication.
//
// Returns:
// - An Option function that sets the TLSPortPolicy for the Client.
func WithTLSPortPolicy(policy TLSPolicy) Option {
return func(c *Client) error {
c.SetTLSPortPolicy(policy)
return nil
}
}
// WithTLSConfig sets the tls.Config for the Client and overrides the default configuration.
//
// This function configures the Client with a custom tls.Config. It overrides the default TLS settings.
// An error is returned if the provided tls.Config is nil or invalid.
//
// Parameters:
// - tlsconfig: A pointer to a tls.Config struct to be used for the Client. Must not be nil.
//
// Returns:
// - An Option function that sets the tls.Config for the Client.
// - An error if the provided tls.Config is invalid.
func WithTLSConfig(tlsconfig *tls.Config) Option {
return func(c *Client) error {
if tlsconfig == nil {
return ErrInvalidTLSConfig
}
c.tlsconfig = tlsconfig
return nil
}
}
// WithSMTPAuth configures the Client to use the specified SMTPAuthType for SMTP authentication.
//
// This function sets the Client to use the specified SMTPAuthType for authenticating with the SMTP server.
//
// Parameters:
// - authtype: The SMTPAuthType to be used for SMTP authentication.
//
// Returns:
// - An Option function that configures the Client to use the specified SMTPAuthType.
func WithSMTPAuth(authtype SMTPAuthType) Option {
return func(c *Client) error {
c.smtpAuthType = authtype
return nil
}
}
// WithSMTPAuthCustom sets a custom SMTP authentication mechanism for the Client.
//
// This function configures the Client to use a custom SMTP authentication mechanism. The provided
// mechanism must satisfy the smtp.Auth interface.
//
// Parameters:
// - smtpAuth: The custom SMTP authentication mechanism, which must implement the smtp.Auth interface.
//
// Returns:
// - An Option function that sets the custom SMTP authentication for the Client.
func WithSMTPAuthCustom(smtpAuth smtp.Auth) Option {
return func(c *Client) error {
if smtpAuth == nil {
return ErrSMTPAuthMethodIsNil
}
c.smtpAuth = smtpAuth
c.smtpAuthType = SMTPAuthCustom
return nil
}
}
// WithUsername sets the username that the Client will use for SMTP authentication.
//
// This function configures the Client with the specified username for SMTP authentication.
//
// Parameters:
// - username: The username to be used for SMTP authentication.
//
// Returns:
// - An Option function that sets the username for the Client.
func WithUsername(username string) Option {
return func(c *Client) error {
c.user = username
return nil
}
}
// WithPassword sets the password that the Client will use for SMTP authentication.
//
// This function configures the Client with the specified password for SMTP authentication.
//
// Parameters:
// - password: The password to be used for SMTP authentication.
//
// Returns:
// - An Option function that sets the password for the Client.
func WithPassword(password string) Option {
return func(c *Client) error {
c.pass = password
return nil
}
}
// WithDSN enables DSN (Delivery Status Notifications) for the Client as described in RFC 1891.
//
// This function configures the Client to request DSN, which provides status notifications for email delivery.
// DSN is only effective if the SMTP server supports it. By default, DSNMailReturnOption is set to DSNMailReturnFull,
// and DSNRcptNotifyOption is set to DSNRcptNotifySuccess and DSNRcptNotifyFailure.
//
// Returns:
// - An Option function that enables DSN for the Client.
//
// References:
// - https://datatracker.ietf.org/doc/html/rfc1891
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 DSN (Delivery Status Notifications) for the Client as described in RFC 1891.
//
// This function configures the Client to request DSN and sets the DSNMailReturnOption to the provided value.
// DSN is only effective if the SMTP server supports it. The provided option must be either DSNMailReturnHeadersOnly
// or DSNMailReturnFull; otherwise, an error is returned.
//
// Parameters:
// - option: The DSNMailReturnOption to be used (DSNMailReturnHeadersOnly or DSNMailReturnFull).
//
// Returns:
// - An Option function that sets the DSNMailReturnOption for the Client.
// - An error if an invalid DSNMailReturnOption is provided.
//
// References:
// - https://datatracker.ietf.org/doc/html/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 DSN (Delivery Status Notifications) for the Client as described in RFC 1891.
//
// This function configures the Client to request DSN and sets the DSNRcptNotifyOption to the provided values.
// The provided options must be valid DSNRcptNotifyOption types. If DSNRcptNotifyNever is combined with
// any other notification type (such as DSNRcptNotifySuccess, DSNRcptNotifyFailure, or DSNRcptNotifyDelay),
// an error is returned.
//
// Parameters:
// - opts: A variadic list of DSNRcptNotifyOption values (e.g., DSNRcptNotifySuccess, DSNRcptNotifyFailure).
//
// Returns:
// - An Option function that sets the DSNRcptNotifyOption for the Client.
// - An error if invalid DSNRcptNotifyOption values are provided or incompatible combinations are used.
//
// References:
// - https://datatracker.ietf.org/doc/html/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 indicates that the Client should skip the "NOOP" command during the dial.
//
// This option is useful for servers that delay potentially unwanted clients when they perform
// commands other than AUTH, such as Microsoft's Exchange Tarpit.
//
// Returns:
// - An Option function that configures the Client to skip the "NOOP" command.
func WithoutNoop() Option {
return func(c *Client) error {
c.noNoop = true
return nil
}
}
// WithDialContextFunc sets the provided DialContextFunc as the DialContext for connecting to the SMTP server.
//
// This function overrides the default DialContext function used by the Client when establishing a connection
// to the SMTP server with the provided DialContextFunc.
//
// Parameters:
// - dialCtxFunc: The custom DialContextFunc to be used for connecting to the SMTP server.
//
// Returns:
// - An Option function that sets the custom DialContextFunc for the Client.
func WithDialContextFunc(dialCtxFunc DialContextFunc) Option {
return func(c *Client) error {
if dialCtxFunc == nil {
return ErrDialContextFuncIsNil
}
c.dialContextFunc = dialCtxFunc
return nil
}
}
// WithLogAuthData enables logging of authentication data.
//
// This function sets the logAuthData field of the Client to true, enabling the logging of authentication data.
//
// Be cautious when using this option, as the logs may include unencrypted authentication data, depending on
// the SMTP authentication method in use, which could pose a data protection risk.
//
// Returns:
// - An Option function that configures the Client to enable authentication data logging.
func WithLogAuthData() Option {
return func(c *Client) error {
c.logAuthData = true
return nil
}
}
// TLSPolicy returns the TLSPolicy that is currently set on the Client as a string.
//
// This method retrieves the current TLSPolicy configured for the Client and returns it as a string representation.
//
// Returns:
// - A string representing the currently set TLSPolicy for the Client.
func (c *Client) TLSPolicy() string {
return c.tlspolicy.String()
}
// ServerAddr returns the server address that is currently set on the Client in the format "host:port".
//
// This method constructs and returns the server address using the host and port currently configured
// for the Client.
//
// Returns:
// - A string representing the server address in the format "host:port".
func (c *Client) ServerAddr() string {
return fmt.Sprintf("%s:%d", c.host, c.port)
}
// SetTLSPolicy sets or overrides the TLSPolicy currently configured on the Client with the given TLSPolicy.
//
// This method allows the user to set a new TLSPolicy for the Client. For best practices regarding
// SMTP TLS connections, it is recommended to use SetTLSPortPolicy instead.
//
// Parameters:
// - policy: The TLSPolicy to be set for the Client.
func (c *Client) SetTLSPolicy(policy TLSPolicy) {
c.tlspolicy = policy
}
// SetTLSPortPolicy sets or overrides the TLSPolicy currently configured on the Client with the given TLSPolicy.
// The correct port is automatically set based on the specified policy.
//
// If TLSMandatory or TLSOpportunistic is provided as the TLSPolicy, port 587 will be used for the connection.
// If the connection fails with TLSOpportunistic, the Client will attempt to connect on port 25 using
// an unencrypted connection as a fallback. If NoTLS is provided, the Client will always use port 25.
//
// Note: If a different port has already been set using WithPort, that port takes precedence and is used
// to establish the SSL/TLS connection, skipping the automatic fallback mechanism.
//
// Parameters:
// - policy: The TLSPolicy to be set for the Client.
func (c *Client) SetTLSPortPolicy(policy TLSPolicy) {
if c.port == DefaultPort {
c.port = DefaultPortTLS
c.fallbackPort = 0
if policy == TLSOpportunistic {
c.fallbackPort = DefaultPort
}
if policy == NoTLS {
c.port = DefaultPort
}
}
c.tlspolicy = policy
}
// SetSSL sets or overrides whether the Client should use implicit SSL/TLS.
//
// This method configures the Client to either enable or disable implicit SSL/TLS for secure communication.
//
// Parameters:
// - ssl: A boolean value indicating whether to enable (true) or disable (false) implicit SSL/TLS.
func (c *Client) SetSSL(ssl bool) {
c.useSSL = ssl
}
// SetSSLPort sets or overrides whether the Client should use implicit SSL/TLS with optional fallback.
// The correct port is automatically set.
//
// If ssl is set to true, the default port 25 will be overridden with port 465. If fallback is set to true
// and the SSL/TLS connection fails, the Client will attempt to connect on port 25 using an unencrypted
// connection.
//
// Note: If a different port has already been set using WithPort, that port takes precedence and is used
// to establish the SSL/TLS connection, skipping the automatic fallback mechanism.
//
// Parameters:
// - ssl: A boolean value indicating whether to enable implicit SSL/TLS.
// - fallback: A boolean value indicating whether to enable fallback to an unencrypted connection.
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 sets or overrides whether the Client is using debug logging. The debug logger will log incoming
// and outgoing communication between the Client and the server to os.Stderr.
//
// Note: The SMTP communication might include unencrypted authentication data, depending on whether you are using
// SMTP authentication and the type of authentication mechanism. This could pose a data protection risk. Use
// debug logging with caution.
//
// Parameters:
// - val: A boolean value indicating whether to enable (true) or disable (false) debug logging.
func (c *Client) SetDebugLog(val bool) {
c.useDebugLog = val
if c.smtpClient != nil {
c.smtpClient.SetDebugLog(val)
}
}
// SetLogger sets or overrides the custom logger currently used by the Client. The logger must
// satisfy the log.Logger interface and is only utilized when debug logging is enabled on the
// Client.
//
// By default, log.Stdlog is used if no custom logger is provided.
//
// Parameters:
// - logger: A logger that satisfies the log.Logger interface to be set for the Client.
func (c *Client) SetLogger(logger log.Logger) {
c.logger = logger
if c.smtpClient != nil {
c.smtpClient.SetLogger(logger)
}
}
// SetTLSConfig sets or overrides the tls.Config currently configured for the Client with the
// given value. An error is returned if the provided tls.Config is invalid.
//
// This method ensures that the provided tls.Config is not nil before updating the Client's
// TLS configuration.
//
// Parameters:
// - tlsconfig: A pointer to the tls.Config struct to be set for the Client. Must not be nil.
//
// Returns:
// - An error if the provided tls.Config is invalid or nil.
func (c *Client) SetTLSConfig(tlsconfig *tls.Config) error {
if tlsconfig == nil {
return ErrInvalidTLSConfig
}
c.tlsconfig = tlsconfig
return nil
}
// SetUsername sets or overrides the username that the Client will use for SMTP authentication.
//
// This method updates the username used by the Client for authenticating with the SMTP server.
//
// Parameters:
// - username: The username to be set for SMTP authentication.
func (c *Client) SetUsername(username string) {
c.user = username
}
// SetPassword sets or overrides the password that the Client will use for SMTP authentication.
//
// This method updates the password used by the Client for authenticating with the SMTP server.
//
// Parameters:
// - password: The password to be set for SMTP authentication.
func (c *Client) SetPassword(password string) {
c.pass = password
}
// SetSMTPAuth sets or overrides the SMTPAuthType currently configured on the Client for SMTP
// authentication.
//
// This method updates the authentication type used by the Client for authenticating with the
// SMTP server and resets any custom SMTP authentication mechanism.
//
// Parameters:
// - authtype: The SMTPAuthType to be set for the Client.
func (c *Client) SetSMTPAuth(authtype SMTPAuthType) {
c.smtpAuthType = authtype
c.smtpAuth = nil
}
// SetSMTPAuthCustom sets or overrides the custom SMTP authentication mechanism currently
// configured for the Client. The provided authentication mechanism must satisfy the
// smtp.Auth interface.
//
// This method updates the authentication mechanism used by the Client for authenticating
// with the SMTP server and sets the authentication type to SMTPAuthCustom.
//
// Parameters:
// - smtpAuth: The custom SMTP authentication mechanism to be set for the Client.
func (c *Client) SetSMTPAuthCustom(smtpAuth smtp.Auth) {
c.smtpAuth = smtpAuth
c.smtpAuthType = SMTPAuthCustom
}
// SetLogAuthData sets or overrides the logging of SMTP authentication data for the Client.
//
// This function sets the logAuthData field of the Client to true, enabling the logging of authentication data.
//
// Be cautious when using this option, as the logs may include unencrypted authentication data, depending on
// the SMTP authentication method in use, which could pose a data protection risk.
//
// Parameters:
// - logAuth: Set wether or not to log SMTP authentication data for the Client.
func (c *Client) SetLogAuthData(logAuth bool) {
c.logAuthData = logAuth
}
// DialWithContext establishes a connection to the server using the provided context.Context.
//
// This function adds a deadline based on the Client's timeout to the provided context.Context
// before connecting to the server. After dialing the defined DialContextFunc and successfully
// establishing the connection, it sends the HELO/EHLO SMTP command, followed by optional
// STARTTLS and SMTP AUTH commands. If debug logging is enabled, it attaches the log.Logger.
//
// After this method is called, the Client will have an active (cancelable) connection to the
// SMTP server.
//
// Parameters:
// - dialCtx: The context.Context used to control the connection timeout and cancellation.
//
// Returns:
// - An error if the connection to the SMTP server fails or any subsequent command fails.
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 c.logAuthData {
c.smtpClient.SetLogAuthData()
}
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 terminates the connection to the SMTP server, returning an error if the disconnection
// fails. If the connection is already closed, this method is a no-op and disregards any error.
//
// This function checks if the Client's SMTP connection is active. If not, it simply returns
// without any action. If the connection is active, it attempts to gracefully close the
// connection using the Quit method.
//
// Returns:
// - An error if the disconnection fails; otherwise, returns nil.
func (c *Client) Close() error {
if c.smtpClient == nil || !c.smtpClient.HasConnection() {
return nil
}
if err := c.smtpClient.Quit(); err != nil {
return fmt.Errorf("failed to close SMTP client: %w", err)
}
return nil
}
// Reset sends an SMTP RSET command to reset the state of the current SMTP session.
//
// This method checks the connection to the SMTP server and, if the connection is valid,
// it sends an RSET command to reset the session state. If the connection is invalid or
// the command fails, an error is returned.
//
// Returns:
// - An error if the connection check fails or if sending the RSET command fails; otherwise, returns nil.
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 server and sends out the provided Msg.
// It calls DialAndSendWithContext with an empty Context.Background.
//
// This method simplifies the process of connecting to the SMTP server and sending messages
// by using a default context. It prepares the messages for sending and ensures the connection
// is established before attempting to send them.
//
// Parameters:
// - messages: A variadic list of pointers to Msg objects to be sent.
//
// Returns:
// - An error if the connection fails or if sending the messages fails; otherwise, returns nil.
func (c *Client) DialAndSend(messages ...*Msg) error {
ctx := context.Background()
return c.DialAndSendWithContext(ctx, messages...)
}
// DialAndSendWithContext establishes a connection to the SMTP server using DialWithContext
// with the provided context.Context, then sends out the given Msg. After successful delivery,
// the Client will close the connection to the server.
//
// This method first attempts to connect to the SMTP server using the provided context.
// Upon successful connection, it sends the specified messages and ensures that the connection
// is closed after the operation, regardless of success or failure in sending the messages.
//
// Parameters:
// - ctx: The context.Context to control the connection timeout and cancellation.
// - messages: A variadic list of pointers to Msg objects to be sent.
//
// Returns:
// - An error if the connection fails, if sending the messages fails, or if closing the
// connection fails; otherwise, returns nil.
func (c *Client) DialAndSendWithContext(ctx context.Context, messages ...*Msg) error {
if err := c.DialWithContext(ctx); err != nil {
return fmt.Errorf("dial failed: %w", err)
}
defer func() {
_ = c.Close()
}()
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
}
// auth attempts to authenticate the client using SMTP AUTH mechanisms. It checks the connection,
// determines the supported authentication methods, and applies the appropriate authentication
// type. An error is returned if authentication fails.
//
// By default NewClient sets the SMTP authentication type to SMTPAuthNoAuth, meaning, that no
// SMTP authentication will be performed. If the user makes use of SetSMTPAuth or initialzes the
// client with WithSMTPAuth, the SMTP authentication type will be set in the Client, forcing
// this method to determine if the server supports the selected authentication method and
// assigning the corresponding smtp.Auth function to it.
//
// If the user set a custom SMTP authentication function using SetSMTPAuthCustom or
// WithSMTPAuthCustom, we will not perform any detection and assignment logic and will trust
// the user with their provided smtp.Auth function.
//
// Finally, it attempts to authenticate the client using the selected method.
//
// Returns:
// - An error if the connection check fails, if no supported authentication method is found,
// or if the authentication process fails.
func (c *Client) auth() error {
if c.smtpAuth == nil && c.smtpAuthType != SMTPAuthNoAuth {
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, false)
case SMTPAuthPlainNoEnc:
if !strings.Contains(smtpAuthType, string(SMTPAuthPlain)) {
return ErrPlainAuthNotSupported
}
c.smtpAuth = smtp.PlainAuth("", c.user, c.pass, c.host, true)
case SMTPAuthLogin:
if !strings.Contains(smtpAuthType, string(SMTPAuthLogin)) {
return ErrLoginAuthNotSupported
}
c.smtpAuth = smtp.LoginAuth(c.user, c.pass, c.host, false)
case SMTPAuthLoginNoEnc:
if !strings.Contains(smtpAuthType, string(SMTPAuthLogin)) {
return ErrLoginAuthNotSupported
}
c.smtpAuth = smtp.LoginAuth(c.user, c.pass, c.host, true)
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 or
// delivery fails. It is invoked by the public Send methods.
//
// This method handles the process of sending a single email message through the SMTP
// client. It performs several checks and operations, including verifying the encoding,
// retrieving the sender and recipient addresses, and managing delivery status notifications
// (DSN). It attempts to send the message and handles any errors that occur during the
// transmission process, ensuring that any necessary cleanup is performed (such as resetting
// the SMTP client if an error occurs).
//
// Parameters:
// - message: A pointer to the Msg object representing the email message to be sent.
//
// Returns:
// - An error if any part of the sending process fails; otherwise, returns nil.
func (c *Client) sendSingleMsg(message *Msg) error {
c.mutex.Lock()
defer c.mutex.Unlock()
escSupport, _ := c.smtpClient.Extension("ENHANCEDSTATUSCODES")
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, errcode: getErrorCode(err),
enhancedStatusCode: getEnhancedStatusCode(err, escSupport),
}
}
rcpts, err := message.GetRecipients()
if err != nil {
return &SendError{
Reason: ErrGetRcpts, errlist: []error{err}, isTemp: isTempError(err),
affectedMsg: message, errcode: getErrorCode(err),
enhancedStatusCode: getEnhancedStatusCode(err, escSupport),
}
}
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, errcode: getErrorCode(err),
enhancedStatusCode: getEnhancedStatusCode(err, escSupport),
}
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)
rcptSendErr.errcode = getErrorCode(err)
rcptSendErr.enhancedStatusCode = getEnhancedStatusCode(err, escSupport)
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, errcode: getErrorCode(err),
enhancedStatusCode: getEnhancedStatusCode(err, escSupport),
}
}
_, err = message.WriteTo(writer)
if err != nil {
return &SendError{
Reason: ErrWriteContent, errlist: []error{err}, isTemp: isTempError(err),
affectedMsg: message, errcode: getErrorCode(err),
enhancedStatusCode: getEnhancedStatusCode(err, escSupport),
}
}
if err = writer.Close(); err != nil {
return &SendError{
Reason: ErrSMTPDataClose, errlist: []error{err}, isTemp: isTempError(err),
affectedMsg: message, errcode: getErrorCode(err),
enhancedStatusCode: getEnhancedStatusCode(err, escSupport),
}
}
message.isDelivered = true
if err = c.Reset(); err != nil {
return &SendError{
Reason: ErrSMTPReset, errlist: []error{err}, isTemp: isTempError(err),
affectedMsg: message, errcode: getErrorCode(err),
enhancedStatusCode: getEnhancedStatusCode(err, escSupport),
}
}
return nil
}
// checkConn ensures that a required server connection is available and extends the connection
// deadline.
//
// This method verifies whether there is an active connection to the SMTP server. If there is no
// connection, it returns an error. If the "noNoop" flag is not set, it sends a NOOP command to
// the server to confirm the connection is still valid. Finally, it updates the connection
// deadline based on the specified timeout value. If any operation fails, the appropriate error
// is returned.
//
// Returns:
// - An error if there is no active connection, if the NOOP command fails, or if extending
// the deadline fails; otherwise, returns nil.
func (c *Client) checkConn() error {
if c.smtpClient == nil {
return ErrNoActiveConnection
}
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.
//
// This method constructs and returns the server address using the host and fallback port
// currently configured for the Client. It is useful for establishing a connection when
// the primary port is unavailable.
//
// Returns:
// - A string representing the server address in the format "host:fallbackPort".
func (c *Client) serverFallbackAddr() string {
return fmt.Sprintf("%s:%d", c.host, c.fallbackPort)
}
// setDefaultHelo sets the HELO/EHLO hostname to the local machine's hostname.
//
// This method retrieves the local hostname using the operating system's hostname function
// and sets it as the HELO/EHLO string for the Client. If retrieving the hostname fails,
// an error is returned.
//
// Returns:
// - An error if there is a failure in reading the local hostname; otherwise, returns nil.
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
}
// tls establishes a TLS connection based on the client's TLS policy and configuration.
// Returns an error if no active connection exists or if a TLS error occurs.
//
// This method first checks if there is an active connection to the SMTP server. If SSL is not
// being used and the TLS policy is not set to NoTLS, it checks for STARTTLS support. Depending
// on the TLS policy (mandatory or opportunistic), it may initiate a TLS connection using the
// StartTLS method. The method also retrieves the TLS connection state to determine if the
// connection is encrypted and returns any errors encountered during these processes.
//
// Returns:
// - An error if there is no active connection, if STARTTLS is required but not supported,
// or if there are issues during the TLS handshake; otherwise, returns nil.
func (c *Client) tls() error {
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
}