go-mail/client.go

659 lines
17 KiB
Go
Raw Normal View History

// SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
//
// SPDX-License-Identifier: MIT
2022-03-05 12:36:53 +01:00
package mail
import (
"context"
2022-03-05 16:27:09 +01:00
"crypto/tls"
"errors"
"fmt"
"net"
"os"
2022-03-10 10:53:38 +01:00
"strings"
2022-03-05 12:36:53 +01:00
"time"
2022-10-17 18:12:18 +02:00
"github.com/wneessen/go-mail/log"
"github.com/wneessen/go-mail/smtp"
2022-03-05 12:36:53 +01:00
)
2022-03-10 16:56:41 +01:00
// Defaults
const (
// DefaultPort is the default connection port cto the SMTP server
DefaultPort = 25
2022-03-05 12:36:53 +01:00
2022-03-10 16:56:41 +01:00
// DefaultTimeout is the default connection timeout
DefaultTimeout = time.Second * 15
// DefaultTLSPolicy is the default STARTTLS policy
DefaultTLSPolicy = TLSMandatory
// DefaultTLSMinVersion is the minimum TLS version required for the connection
// Nowadays TLS1.2 should be the sane default
DefaultTLSMinVersion = tls.VersionTLS12
2022-03-10 16:56:41 +01:00
)
2022-03-05 12:36:53 +01:00
// DSNMailReturnOption is a type to define which MAIL RET option is used when a DSN
// is requested
type DSNMailReturnOption string
2022-03-05 16:27:09 +01:00
// DSNRcptNotifyOption is a type to define which RCPT NOTIFY option is used when a DSN
// is requested
type DSNRcptNotifyOption string
2022-03-05 16:27:09 +01:00
const (
// DSNMailReturnHeadersOnly requests that only the headers of the message be returned.
// See: https://www.rfc-editor.org/rfc/rfc1891#section-5.3
DSNMailReturnHeadersOnly DSNMailReturnOption = "HDRS"
// DSNMailReturnFull requests that the entire message be returned in any "failed"
// delivery status notification issued for this recipient
// See: https://www.rfc-editor.org/rfc/rfc1891#section-5.3
DSNMailReturnFull DSNMailReturnOption = "FULL"
// DSNRcptNotifyNever requests that a DSN not be returned to the sender under
// any conditions.
// See: https://www.rfc-editor.org/rfc/rfc1891#section-5.1
DSNRcptNotifyNever DSNRcptNotifyOption = "NEVER"
// DSNRcptNotifySuccess requests that a DSN be issued on successful delivery
// See: https://www.rfc-editor.org/rfc/rfc1891#section-5.1
DSNRcptNotifySuccess DSNRcptNotifyOption = "SUCCESS"
// DSNRcptNotifyFailure requests that a DSN be issued on delivery failure
// See: https://www.rfc-editor.org/rfc/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.
// See: https://www.rfc-editor.org/rfc/rfc1891#section-5.1
DSNRcptNotifyDelay DSNRcptNotifyOption = "DELAY"
)
2022-03-07 16:24:49 +01:00
// Client is the SMTP client struct
type Client struct {
// co is the net.Conn that the smtp.Client is based on
co net.Conn
2022-03-05 16:27:09 +01:00
// Timeout for the SMTP server connection
cto time.Duration
// dsn indicates that we want to use DSN for the Client
dsn bool
// dsnmrtype defines the DSNMailReturnOption in case DSN is enabled
dsnmrtype DSNMailReturnOption
// dsnrntype defines the DSNRcptNotifyOption in case DSN is enabled
dsnrntype []string
2022-03-05 16:27:09 +01:00
2022-03-09 13:20:01 +01:00
// enc indicates if a Client connection is encrypted or not
enc bool
// noNoop indicates the Noop is to be skipped
noNoop bool
// HELO/EHLO string for the greeting the target SMTP server
helo string
// Hostname of the target SMTP server cto connect cto
host string
2022-03-10 10:53:38 +01:00
// pass is the corresponding SMTP AUTH password
pass string
// Port of the SMTP server cto connect cto
port int
2022-03-10 12:10:27 +01:00
// sa is a pointer to smtp.Auth
2022-03-10 10:53:38 +01:00
sa smtp.Auth
// satype represents the authentication type for SMTP AUTH
satype SMTPAuthType
2022-03-10 12:10:27 +01:00
// sc is the smtp.Client that is set up when using the Dial*() methods
2022-03-05 16:27:09 +01:00
sc *smtp.Client
// Use SSL for the connection
ssl bool
// tlspolicy sets the client to use the provided TLSPolicy for the STARTTLS protocol
tlspolicy TLSPolicy
// tlsconfig represents the tls.Config setting for the STARTTLS connection
tlsconfig *tls.Config
// user is the SMTP AUTH username
user string
// dl enables the debug logging on the SMTP client
dl bool
// l is a logger that implements the log.Logger interface
l log.Logger
2022-03-05 12:36:53 +01:00
}
// Option returns a function that can be used for grouping Client options
type Option func(*Client) error
2022-03-05 12:36:53 +01:00
2022-03-05 16:27:09 +01:00
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")
2022-03-05 16:27:09 +01:00
// ErrNoHostname should be used if a Client has no hostname set
ErrNoHostname = errors.New("hostname for client cannot be empty")
2022-03-10 12:10:27 +01:00
// 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")
2022-03-14 10:29:53 +01:00
// 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")
2022-03-05 16:27:09 +01:00
)
2022-03-05 12:36:53 +01:00
// NewClient returns a new Session client object
2022-03-05 16:27:09 +01:00
func NewClient(h string, o ...Option) (*Client, error) {
c := &Client{
2022-03-09 13:20:01 +01:00
cto: DefaultTimeout,
2022-03-07 16:24:49 +01:00
host: h,
port: DefaultPort,
tlsconfig: &tls.Config{ServerName: h, MinVersion: DefaultTLSMinVersion},
2022-03-10 16:56:41 +01:00
tlspolicy: DefaultTLSPolicy,
2022-03-05 16:27:09 +01:00
}
// Set default HELO/EHLO hostname
if err := c.setDefaultHelo(); err != nil {
return c, err
2022-03-05 12:36:53 +01:00
}
// Override defaults with optionally provided Option functions
for _, co := range o {
if co == nil {
continue
}
if err := co(c); err != nil {
return c, fmt.Errorf("failed to apply option: %w", err)
}
2022-03-05 12:36:53 +01:00
}
2022-03-05 16:27:09 +01:00
// Some settings in a Client cannot be empty/unset
if c.host == "" {
return c, ErrNoHostname
2022-03-05 12:36:53 +01:00
}
2022-03-05 16:27:09 +01:00
return c, nil
2022-03-05 12:36:53 +01:00
}
// WithPort overrides the default connection port
func WithPort(p int) Option {
return func(c *Client) error {
if p < 1 || p > 65535 {
return ErrInvalidPort
}
2022-03-05 16:27:09 +01:00
c.port = p
return nil
2022-03-05 16:27:09 +01:00
}
}
// WithTimeout overrides the default connection timeout
func WithTimeout(t time.Duration) Option {
return func(c *Client) error {
if t <= 0 {
return ErrInvalidTimeout
}
2022-03-05 16:27:09 +01:00
c.cto = t
return nil
2022-03-05 16:27:09 +01:00
}
}
// WithSSL tells the client to use a SSL/TLS connection
func WithSSL() Option {
return func(c *Client) error {
2022-03-05 16:27:09 +01:00
c.ssl = true
return nil
2022-03-05 16:27:09 +01:00
}
}
// 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.dl = true
return nil
}
}
// WithLogger overrides the default log.Logger that is used for debug logging
func WithLogger(l log.Logger) Option {
return func(c *Client) error {
c.l = l
return nil
}
}
2022-03-06 15:15:42 +01:00
// WithHELO tells the client to use the provided string as HELO/EHLO greeting host
func WithHELO(h string) Option {
return func(c *Client) error {
if h == "" {
return ErrInvalidHELO
}
2022-03-06 15:15:42 +01:00
c.helo = h
return nil
2022-03-06 15:15:42 +01:00
}
}
2022-03-09 13:20:01 +01:00
// WithTLSPolicy tells the client to use the provided TLSPolicy
func WithTLSPolicy(p TLSPolicy) Option {
return func(c *Client) error {
2022-03-09 13:20:01 +01:00
c.tlspolicy = p
return nil
2022-03-09 13:20:01 +01:00
}
}
// WithTLSConfig tells the client to use the provided *tls.Config
func WithTLSConfig(co *tls.Config) Option {
return func(c *Client) error {
if co == nil {
return ErrInvalidTLSConfig
}
2022-03-09 13:20:01 +01:00
c.tlsconfig = co
return nil
2022-03-09 13:20:01 +01:00
}
}
2022-03-10 10:53:38 +01:00
// WithSMTPAuth tells the client to use the provided SMTPAuthType for authentication
func WithSMTPAuth(t SMTPAuthType) Option {
return func(c *Client) error {
2022-03-10 10:53:38 +01:00
c.satype = t
return nil
2022-03-10 10:53:38 +01:00
}
}
// WithSMTPAuthCustom tells the client to use the provided smtp.Auth for SMTP authentication
func WithSMTPAuthCustom(a smtp.Auth) Option {
return func(c *Client) error {
2022-03-10 10:53:38 +01:00
c.sa = a
return nil
2022-03-10 10:53:38 +01:00
}
}
// WithUsername tells the client to use the provided string as username for authentication
func WithUsername(u string) Option {
return func(c *Client) error {
2022-03-10 10:53:38 +01:00
c.user = u
return nil
2022-03-10 10:53:38 +01:00
}
}
// WithPassword tells the client to use the provided string as password/secret for authentication
func WithPassword(p string) Option {
return func(c *Client) error {
2022-03-10 10:53:38 +01:00
c.pass = p
return nil
2022-03-10 10:53:38 +01:00
}
}
// 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.dsn = true
c.dsnmrtype = DSNMailReturnFull
c.dsnrntype = []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(mro DSNMailReturnOption) Option {
return func(c *Client) error {
switch mro {
case DSNMailReturnHeadersOnly:
case DSNMailReturnFull:
default:
return ErrInvalidDSNMailReturnOption
}
c.dsn = true
c.dsnmrtype = mro
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(rno ...DSNRcptNotifyOption) Option {
return func(c *Client) error {
var rnol []string
var ns, nns bool
if len(rno) > 0 {
for _, crno := range rno {
switch crno {
case DSNRcptNotifyNever:
ns = true
case DSNRcptNotifySuccess:
nns = true
case DSNRcptNotifyFailure:
nns = true
case DSNRcptNotifyDelay:
nns = true
default:
return ErrInvalidDSNRcptNotifyOption
}
rnol = append(rnol, string(crno))
}
}
if ns && nns {
return ErrInvalidDSNRcptNotifyCombination
}
c.dsn = true
c.dsnrntype = rnol
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
}
}
2022-03-07 16:24:49 +01:00
// TLSPolicy returns the currently set TLSPolicy as string
func (c *Client) TLSPolicy() string {
2022-03-09 17:05:38 +01:00
return c.tlspolicy.String()
2022-03-07 16:24:49 +01:00
}
2022-03-07 18:14:38 +01:00
// ServerAddr returns the currently set combination of hostname and port
func (c *Client) ServerAddr() string {
return fmt.Sprintf("%s:%d", c.host, c.port)
}
2022-03-07 16:24:49 +01:00
// SetTLSPolicy overrides the current TLSPolicy with the given TLSPolicy value
func (c *Client) SetTLSPolicy(p TLSPolicy) {
c.tlspolicy = p
}
// SetSSL tells the Client wether to use SSL or not
func (c *Client) SetSSL(s bool) {
c.ssl = s
}
// SetDebugLog tells the Client whether debug logging is enabled or not
func (c *Client) SetDebugLog(v bool) {
c.dl = v
if c.sc != nil {
c.sc.SetDebugLog(v)
}
}
// SetLogger tells the Client which log.Logger to use
func (c *Client) SetLogger(l log.Logger) {
c.l = l
if c.sc != nil {
c.sc.SetLogger(l)
}
}
2022-03-09 13:20:01 +01:00
// SetTLSConfig overrides the current *tls.Config with the given *tls.Config value
2022-03-15 22:37:55 +01:00
func (c *Client) SetTLSConfig(co *tls.Config) error {
if co == nil {
return ErrInvalidTLSConfig
}
2022-03-09 13:20:01 +01:00
c.tlsconfig = co
2022-03-15 22:37:55 +01:00
return nil
2022-03-09 13:20:01 +01:00
}
2022-03-10 12:10:27 +01:00
// SetUsername overrides the current username string with the given value
func (c *Client) SetUsername(u string) {
c.user = u
}
// SetPassword overrides the current password string with the given value
func (c *Client) SetPassword(p string) {
c.pass = p
}
// SetSMTPAuth overrides the current SMTP AUTH type setting with the given value
func (c *Client) SetSMTPAuth(a SMTPAuthType) {
c.satype = a
}
// SetSMTPAuthCustom overrides the current SMTP AUTH setting with the given custom smtp.Auth
func (c *Client) SetSMTPAuthCustom(sa smtp.Auth) {
c.sa = sa
2022-03-07 16:24:49 +01:00
}
// setDefaultHelo retrieves the current hostname and sets it as HELO/EHLO hostname
func (c *Client) setDefaultHelo() error {
hn, err := os.Hostname()
if err != nil {
return fmt.Errorf("failed cto read local hostname: %w", err)
}
c.helo = hn
return nil
}
2022-03-05 16:27:09 +01:00
// DialWithContext establishes a connection cto the SMTP server with a given context.Context
2022-03-10 12:10:27 +01:00
func (c *Client) DialWithContext(pc context.Context) error {
ctx, cfn := context.WithDeadline(pc, time.Now().Add(c.cto))
2022-03-05 16:27:09 +01:00
defer cfn()
nd := net.Dialer{}
2022-03-05 16:27:09 +01:00
var err error
if c.ssl {
td := tls.Dialer{NetDialer: &nd, Config: c.tlsconfig}
2022-03-09 13:20:01 +01:00
c.enc = true
2022-03-10 12:10:27 +01:00
c.co, err = td.DialContext(ctx, "tcp", c.ServerAddr())
2022-03-05 16:27:09 +01:00
}
if !c.ssl {
2022-03-10 12:10:27 +01:00
c.co, err = nd.DialContext(ctx, "tcp", c.ServerAddr())
2022-03-05 16:27:09 +01:00
}
if err != nil {
return err
2022-03-05 12:36:53 +01:00
}
2022-03-05 16:27:09 +01:00
2022-03-10 12:10:27 +01:00
c.sc, err = smtp.NewClient(c.co, c.host)
2022-03-05 16:27:09 +01:00
if err != nil {
return err
}
if c.l != nil {
c.sc.SetLogger(c.l)
}
if c.dl {
c.sc.SetDebugLog(true)
}
2022-03-05 16:27:09 +01:00
if err := c.sc.Hello(c.helo); err != nil {
return err
}
2022-03-10 16:56:41 +01:00
if err := c.tls(); err != nil {
return err
2022-03-05 16:27:09 +01:00
}
2022-03-07 16:24:49 +01:00
2022-03-10 10:53:38 +01:00
if err := c.auth(); err != nil {
return err
}
return nil
}
2022-03-10 16:19:51 +01:00
// Close closes the Client connection
func (c *Client) Close() error {
if err := c.checkConn(); err != nil {
return err
}
if err := c.sc.Quit(); err != nil {
2022-03-10 16:19:51 +01:00
return fmt.Errorf("failed to close SMTP client: %w", err)
}
return nil
}
2022-03-11 19:17:43 +01:00
// 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.sc.Reset(); err != nil {
return fmt.Errorf("failed to send RSET to SMTP client: %w", err)
}
return nil
}
2022-03-10 12:10:27 +01:00
// DialAndSend establishes a connection to the SMTP server with a
// default context.Background and sends the mail
func (c *Client) DialAndSend(ml ...*Msg) error {
2022-03-10 12:10:27 +01:00
ctx := context.Background()
return c.DialAndSendWithContext(ctx, ml...)
}
// DialAndSendWithContext establishes a connection to the SMTP server with a
// custom context and sends the mail
func (c *Client) DialAndSendWithContext(ctx context.Context, ml ...*Msg) error {
2022-03-10 12:10:27 +01:00
if err := c.DialWithContext(ctx); err != nil {
return fmt.Errorf("dial failed: %w", err)
}
if err := c.Send(ml...); err != nil {
2022-03-10 12:10:27 +01:00
return fmt.Errorf("send failed: %w", err)
}
if err := c.Close(); err != nil {
2022-10-17 18:16:00 +02:00
return fmt.Errorf("failed to close connction: %w", err)
}
2022-03-10 12:10:27 +01:00
return nil
}
// checkConn makes sure that a required server connection is available and extends the
// connection deadline
func (c *Client) checkConn() error {
if c.co == nil {
return ErrNoActiveConnection
}
if !c.noNoop {
if err := c.sc.Noop(); err != nil {
return ErrNoActiveConnection
}
2022-03-11 19:17:43 +01:00
}
2022-03-10 12:10:27 +01:00
if err := c.co.SetDeadline(time.Now().Add(c.cto)); err != nil {
return ErrDeadlineExtendFailed
}
return nil
}
2022-03-10 16:56:41 +01:00
// tls tries to make sure that the STARTTLS requirements are satisfied
func (c *Client) tls() error {
if c.co == nil {
return ErrNoActiveConnection
}
2022-03-10 16:56:41 +01:00
if !c.ssl && c.tlspolicy != NoTLS {
est := false
st, _ := c.sc.Extension("STARTTLS")
if c.tlspolicy == TLSMandatory {
est = true
if !st {
return fmt.Errorf("STARTTLS mode set to: %q, but target host does not support STARTTLS",
c.tlspolicy)
}
}
if c.tlspolicy == TLSOpportunistic {
if st {
est = true
}
}
if est {
if err := c.sc.StartTLS(c.tlsconfig); err != nil {
return err
}
}
_, c.enc = c.sc.TLSConnectionState()
}
return nil
}
2022-03-10 10:53:38 +01:00
// auth will try to perform SMTP AUTH if requested
func (c *Client) auth() error {
2022-03-10 12:10:27 +01:00
if err := c.checkConn(); err != nil {
return fmt.Errorf("failed to authenticate: %w", err)
}
2022-03-10 10:53:38 +01:00
if c.sa == nil && c.satype != "" {
sa, sat := c.sc.Extension("AUTH")
if !sa {
return fmt.Errorf("server does not support SMTP AUTH")
}
switch c.satype {
case SMTPAuthPlain:
if !strings.Contains(sat, string(SMTPAuthPlain)) {
return ErrPlainAuthNotSupported
}
c.sa = smtp.PlainAuth("", c.user, c.pass, c.host)
case SMTPAuthLogin:
if !strings.Contains(sat, string(SMTPAuthLogin)) {
return ErrLoginAuthNotSupported
}
c.sa = smtp.LoginAuth(c.user, c.pass, c.host)
2022-03-10 10:53:38 +01:00
case SMTPAuthCramMD5:
if !strings.Contains(sat, string(SMTPAuthCramMD5)) {
return ErrCramMD5AuthNotSupported
}
c.sa = smtp.CRAMMD5Auth(c.user, c.pass)
default:
return fmt.Errorf("unsupported SMTP AUTH type %q", c.satype)
}
}
if c.sa != nil {
if err := c.sc.Auth(c.sa); err != nil {
return fmt.Errorf("SMTP AUTH failed: %w", err)
}
}
2022-03-05 16:27:09 +01:00
return nil
2022-03-05 12:36:53 +01:00
}