Compare commits

..

No commits in common. "e1098091c347f9a814adf250f83910b3d70edd88" and "4e880ab31c0895c6d05256af69247fe32f9b8392" have entirely different histories.

7 changed files with 403 additions and 417 deletions

View file

@ -20,39 +20,39 @@ type Base64LineBreaker struct {
out io.Writer
}
var newlineBytes = []byte(SingleNewLine)
var nl = []byte(SingleNewLine)
// Write writes the data stream and inserts a SingleNewLine when the maximum
// line length is reached
func (l *Base64LineBreaker) Write(data []byte) (numBytes int, err error) {
func (l *Base64LineBreaker) Write(b []byte) (n int, err error) {
if l.out == nil {
err = fmt.Errorf(ErrNoOutWriter)
return
}
if l.used+len(data) < MaxBodyLength {
copy(l.line[l.used:], data)
l.used += len(data)
return len(data), nil
if l.used+len(b) < MaxBodyLength {
copy(l.line[l.used:], b)
l.used += len(b)
return len(b), nil
}
numBytes, err = l.out.Write(l.line[0:l.used])
n, err = l.out.Write(l.line[0:l.used])
if err != nil {
return
}
excess := MaxBodyLength - l.used
l.used = 0
numBytes, err = l.out.Write(data[0:excess])
n, err = l.out.Write(b[0:excess])
if err != nil {
return
}
numBytes, err = l.out.Write(newlineBytes)
n, err = l.out.Write(nl)
if err != nil {
return
}
return l.Write(data[excess:])
return l.Write(b[excess:])
}
// Close closes the Base64LineBreaker and writes any access data that is still
@ -63,7 +63,7 @@ func (l *Base64LineBreaker) Close() (err error) {
if err != nil {
return
}
_, err = l.out.Write(newlineBytes)
_, err = l.out.Write(nl)
}
return

280
client.go
View file

@ -87,11 +87,11 @@ type DialContextFunc func(ctx context.Context, network, address string) (net.Con
// Client is the SMTP client struct
type Client struct {
// connection is the net.Conn that the smtp.Client is based on
connection net.Conn
// co is the net.Conn that the smtp.Client is based on
co net.Conn
// Timeout for the SMTP server connection
connTimeout time.Duration
cto time.Duration
// dsn indicates that we want to use DSN for the Client
dsn bool
@ -102,8 +102,8 @@ type Client struct {
// dsnrntype defines the DSNRcptNotifyOption in case DSN is enabled
dsnrntype []string
// isEncrypted indicates if a Client connection is encrypted or not
isEncrypted bool
// enc indicates if a Client connection is encrypted or not
enc bool
// noNoop indicates the Noop is to be skipped
noNoop bool
@ -121,17 +121,17 @@ type Client struct {
port int
fallbackPort int
// smtpAuth is a pointer to smtp.Auth
smtpAuth smtp.Auth
// sa is a pointer to smtp.Auth
sa smtp.Auth
// smtpAuthType represents the authentication type for SMTP AUTH
smtpAuthType SMTPAuthType
// satype represents the authentication type for SMTP AUTH
satype SMTPAuthType
// smtpClient is the smtp.Client that is set up when using the Dial*() methods
smtpClient *smtp.Client
// sc is the smtp.Client that is set up when using the Dial*() methods
sc *smtp.Client
// Use SSL for the connection
useSSL bool
ssl bool
// tlspolicy sets the client to use the provided TLSPolicy for the STARTTLS protocol
tlspolicy TLSPolicy
@ -142,11 +142,11 @@ type Client struct {
// user is the SMTP AUTH username
user string
// useDebugLog enables the debug logging on the SMTP client
useDebugLog bool
// dl enables the debug logging on the SMTP client
dl bool
// logger is a logger that implements the log.Logger interface
logger log.Logger
// l is a logger that implements the log.Logger interface
l log.Logger
// dialContextFunc is a custom DialContext function to dial target SMTP server
dialContextFunc DialContextFunc
@ -198,12 +198,12 @@ var (
)
// NewClient returns a new Session client object
func NewClient(host string, opts ...Option) (*Client, error) {
func NewClient(h string, o ...Option) (*Client, error) {
c := &Client{
connTimeout: DefaultTimeout,
host: host,
cto: DefaultTimeout,
host: h,
port: DefaultPort,
tlsconfig: &tls.Config{ServerName: host, MinVersion: DefaultTLSMinVersion},
tlsconfig: &tls.Config{ServerName: h, MinVersion: DefaultTLSMinVersion},
tlspolicy: DefaultTLSPolicy,
}
@ -213,11 +213,11 @@ func NewClient(host string, opts ...Option) (*Client, error) {
}
// Override defaults with optionally provided Option functions
for _, opt := range opts {
if opt == nil {
for _, co := range o {
if co == nil {
continue
}
if err := opt(c); err != nil {
if err := co(c); err != nil {
return c, fmt.Errorf("failed to apply option: %w", err)
}
}
@ -231,23 +231,23 @@ func NewClient(host string, opts ...Option) (*Client, error) {
}
// WithPort overrides the default connection port
func WithPort(port int) Option {
func WithPort(p int) Option {
return func(c *Client) error {
if port < 1 || port > 65535 {
if p < 1 || p > 65535 {
return ErrInvalidPort
}
c.port = port
c.port = p
return nil
}
}
// WithTimeout overrides the default connection timeout
func WithTimeout(timeout time.Duration) Option {
func WithTimeout(t time.Duration) Option {
return func(c *Client) error {
if timeout <= 0 {
if t <= 0 {
return ErrInvalidTimeout
}
c.connTimeout = timeout
c.cto = t
return nil
}
}
@ -257,7 +257,7 @@ func WithTimeout(timeout time.Duration) Option {
// Deprecated: use WithSSLPort instead.
func WithSSL() Option {
return func(c *Client) error {
c.useSSL = true
c.ssl = true
return nil
}
}
@ -267,9 +267,9 @@ func WithSSL() Option {
//
// When the SSL connection fails and fallback is set to true,
// the client will attempt to connect on port 25 using plaintext.
func WithSSLPort(fallback bool) Option {
func WithSSLPort(fb bool) Option {
return func(c *Client) error {
c.SetSSLPort(true, fallback)
c.SetSSLPort(true, fb)
return nil
}
}
@ -278,26 +278,26 @@ func WithSSLPort(fallback bool) Option {
// to StdErr
func WithDebugLog() Option {
return func(c *Client) error {
c.useDebugLog = true
c.dl = true
return nil
}
}
// WithLogger overrides the default log.Logger that is used for debug logging
func WithLogger(logger log.Logger) Option {
func WithLogger(l log.Logger) Option {
return func(c *Client) error {
c.logger = logger
c.l = l
return nil
}
}
// WithHELO tells the client to use the provided string as HELO/EHLO greeting host
func WithHELO(helo string) Option {
func WithHELO(h string) Option {
return func(c *Client) error {
if helo == "" {
if h == "" {
return ErrInvalidHELO
}
c.helo = helo
c.helo = h
return nil
}
}
@ -305,9 +305,9 @@ func WithHELO(helo string) Option {
// WithTLSPolicy tells the client to use the provided TLSPolicy
//
// Deprecated: use WithTLSPortPolicy instead.
func WithTLSPolicy(policy TLSPolicy) Option {
func WithTLSPolicy(p TLSPolicy) Option {
return func(c *Client) error {
c.tlspolicy = policy
c.tlspolicy = p
return nil
}
}
@ -319,52 +319,52 @@ func WithTLSPolicy(policy TLSPolicy) Option {
// If the connection fails with TLSOpportunistic,
// a plaintext connection is attempted on port 25 as a fallback.
// NoTLS will allways use port 25.
func WithTLSPortPolicy(policy TLSPolicy) Option {
func WithTLSPortPolicy(p TLSPolicy) Option {
return func(c *Client) error {
c.SetTLSPortPolicy(policy)
c.SetTLSPortPolicy(p)
return nil
}
}
// WithTLSConfig tells the client to use the provided *tls.Config
func WithTLSConfig(tlsconfig *tls.Config) Option {
func WithTLSConfig(co *tls.Config) Option {
return func(c *Client) error {
if tlsconfig == nil {
if co == nil {
return ErrInvalidTLSConfig
}
c.tlsconfig = tlsconfig
c.tlsconfig = co
return nil
}
}
// WithSMTPAuth tells the client to use the provided SMTPAuthType for authentication
func WithSMTPAuth(authtype SMTPAuthType) Option {
func WithSMTPAuth(t SMTPAuthType) Option {
return func(c *Client) error {
c.smtpAuthType = authtype
c.satype = t
return nil
}
}
// WithSMTPAuthCustom tells the client to use the provided smtp.Auth for SMTP authentication
func WithSMTPAuthCustom(smtpAuth smtp.Auth) Option {
func WithSMTPAuthCustom(a smtp.Auth) Option {
return func(c *Client) error {
c.smtpAuth = smtpAuth
c.sa = a
return nil
}
}
// WithUsername tells the client to use the provided string as username for authentication
func WithUsername(username string) Option {
func WithUsername(u string) Option {
return func(c *Client) error {
c.user = username
c.user = u
return nil
}
}
// WithPassword tells the client to use the provided string as password/secret for authentication
func WithPassword(password string) Option {
func WithPassword(p string) Option {
return func(c *Client) error {
c.pass = password
c.pass = p
return nil
}
}
@ -386,9 +386,9 @@ func WithDSN() Option {
// 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 {
func WithDSNMailReturnType(mro DSNMailReturnOption) Option {
return func(c *Client) error {
switch option {
switch mro {
case DSNMailReturnHeadersOnly:
case DSNMailReturnFull:
default:
@ -396,7 +396,7 @@ func WithDSNMailReturnType(option DSNMailReturnOption) Option {
}
c.dsn = true
c.dsnmrtype = option
c.dsnmrtype = mro
return nil
}
}
@ -404,13 +404,13 @@ func WithDSNMailReturnType(option DSNMailReturnOption) Option {
// 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 {
func WithDSNRcptNotifyType(rno ...DSNRcptNotifyOption) Option {
return func(c *Client) error {
var rcptOpts []string
var rnol []string
var ns, nns bool
if len(opts) > 0 {
for _, opt := range opts {
switch opt {
if len(rno) > 0 {
for _, crno := range rno {
switch crno {
case DSNRcptNotifyNever:
ns = true
case DSNRcptNotifySuccess:
@ -422,7 +422,7 @@ func WithDSNRcptNotifyType(opts ...DSNRcptNotifyOption) Option {
default:
return ErrInvalidDSNRcptNotifyOption
}
rcptOpts = append(rcptOpts, string(opt))
rnol = append(rnol, string(crno))
}
}
if ns && nns {
@ -430,7 +430,7 @@ func WithDSNRcptNotifyType(opts ...DSNRcptNotifyOption) Option {
}
c.dsn = true
c.dsnrntype = rcptOpts
c.dsnrntype = rnol
return nil
}
}
@ -445,9 +445,9 @@ func WithoutNoop() Option {
}
// WithDialContextFunc overrides the default DialContext for connecting SMTP server
func WithDialContextFunc(dialCtxFunc DialContextFunc) Option {
func WithDialContextFunc(f DialContextFunc) Option {
return func(c *Client) error {
c.dialContextFunc = dialCtxFunc
c.dialContextFunc = f
return nil
}
}
@ -463,8 +463,8 @@ func (c *Client) ServerAddr() string {
}
// SetTLSPolicy overrides the current TLSPolicy with the given TLSPolicy value
func (c *Client) SetTLSPolicy(policy TLSPolicy) {
c.tlspolicy = policy
func (c *Client) SetTLSPolicy(p TLSPolicy) {
c.tlspolicy = p
}
// SetTLSPortPolicy overrides the current TLSPolicy with the given TLSPolicy
@ -474,22 +474,22 @@ func (c *Client) SetTLSPolicy(policy TLSPolicy) {
// If the connection fails with TLSOpportunistic, a plaintext connection is
// attempted on port 25 as a fallback.
// NoTLS will allways use port 25.
func (c *Client) SetTLSPortPolicy(policy TLSPolicy) {
func (c *Client) SetTLSPortPolicy(p TLSPolicy) {
c.port = DefaultPortTLS
if policy == TLSOpportunistic {
if p == TLSOpportunistic {
c.fallbackPort = DefaultPort
}
if policy == NoTLS {
if p == NoTLS {
c.port = DefaultPort
}
c.tlspolicy = policy
c.tlspolicy = p
}
// SetSSL tells the Client wether to use SSL or not
func (c *Client) SetSSL(ssl bool) {
c.useSSL = ssl
func (c *Client) SetSSL(s bool) {
c.ssl = s
}
// SetSSLPort tells the Client wether or not to use SSL and fallback.
@ -499,124 +499,124 @@ func (c *Client) SetSSL(ssl bool) {
// 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.
func (c *Client) SetSSLPort(ssl bool, fallback bool) {
func (c *Client) SetSSLPort(ssl bool, fb bool) {
c.port = DefaultPort
if ssl {
c.port = DefaultPortSSL
}
c.fallbackPort = 0
if fallback {
if fb {
c.fallbackPort = DefaultPort
}
c.useSSL = ssl
c.ssl = 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)
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(logger log.Logger) {
c.logger = logger
if c.smtpClient != nil {
c.smtpClient.SetLogger(logger)
func (c *Client) SetLogger(l log.Logger) {
c.l = l
if c.sc != nil {
c.sc.SetLogger(l)
}
}
// SetTLSConfig overrides the current *tls.Config with the given *tls.Config value
func (c *Client) SetTLSConfig(tlsconfig *tls.Config) error {
if tlsconfig == nil {
func (c *Client) SetTLSConfig(co *tls.Config) error {
if co == nil {
return ErrInvalidTLSConfig
}
c.tlsconfig = tlsconfig
c.tlsconfig = co
return nil
}
// SetUsername overrides the current username string with the given value
func (c *Client) SetUsername(username string) {
c.user = username
func (c *Client) SetUsername(u string) {
c.user = u
}
// SetPassword overrides the current password string with the given value
func (c *Client) SetPassword(password string) {
c.pass = password
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(authtype SMTPAuthType) {
c.smtpAuthType = authtype
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(smtpAuth smtp.Auth) {
c.smtpAuth = smtpAuth
func (c *Client) SetSMTPAuthCustom(sa smtp.Auth) {
c.sa = sa
}
// setDefaultHelo retrieves the current hostname and sets it as HELO/EHLO hostname
func (c *Client) setDefaultHelo() error {
hostname, err := os.Hostname()
hn, err := os.Hostname()
if err != nil {
return fmt.Errorf("failed to read local hostname: %w", err)
return fmt.Errorf("failed cto read local hostname: %w", err)
}
c.helo = hostname
c.helo = hn
return nil
}
// DialWithContext establishes a connection to the SMTP server with a given context.Context
func (c *Client) DialWithContext(dialCtx context.Context) error {
ctx, cancel := context.WithDeadline(dialCtx, time.Now().Add(c.connTimeout))
defer cancel()
// DialWithContext establishes a connection cto the SMTP server with a given context.Context
func (c *Client) DialWithContext(pc context.Context) error {
ctx, cfn := context.WithDeadline(pc, time.Now().Add(c.cto))
defer cfn()
if c.dialContextFunc == nil {
netDialer := net.Dialer{}
c.dialContextFunc = netDialer.DialContext
nd := net.Dialer{}
c.dialContextFunc = nd.DialContext
if c.useSSL {
tlsDialer := tls.Dialer{NetDialer: &netDialer, Config: c.tlsconfig}
c.isEncrypted = true
c.dialContextFunc = tlsDialer.DialContext
if c.ssl {
td := tls.Dialer{NetDialer: &nd, Config: c.tlsconfig}
c.enc = true
c.dialContextFunc = td.DialContext
}
}
var err error
c.connection, err = c.dialContextFunc(ctx, "tcp", c.ServerAddr())
c.co, err = c.dialContextFunc(ctx, "tcp", c.ServerAddr())
if err != nil && c.fallbackPort != 0 {
// TODO: should we somehow log or append the previous error?
c.connection, err = c.dialContextFunc(ctx, "tcp", c.serverFallbackAddr())
c.co, err = c.dialContextFunc(ctx, "tcp", c.serverFallbackAddr())
}
if err != nil {
return err
}
client, err := smtp.NewClient(c.connection, c.host)
sc, err := smtp.NewClient(c.co, c.host)
if err != nil {
return err
}
if client == nil {
if sc == nil {
return fmt.Errorf("SMTP client is nil")
}
c.smtpClient = client
c.sc = sc
if c.logger != nil {
c.smtpClient.SetLogger(c.logger)
if c.l != nil {
c.sc.SetLogger(c.l)
}
if c.useDebugLog {
c.smtpClient.SetDebugLog(true)
if c.dl {
c.sc.SetDebugLog(true)
}
if err = c.smtpClient.Hello(c.helo); err != nil {
if err := c.sc.Hello(c.helo); err != nil {
return err
}
if err = c.tls(); err != nil {
if err := c.tls(); err != nil {
return err
}
if err = c.auth(); err != nil {
if err := c.auth(); err != nil {
return err
}
@ -628,7 +628,7 @@ func (c *Client) Close() error {
if err := c.checkConn(); err != nil {
return err
}
if err := c.smtpClient.Quit(); err != nil {
if err := c.sc.Quit(); err != nil {
return fmt.Errorf("failed to close SMTP client: %w", err)
}
@ -640,7 +640,7 @@ func (c *Client) Reset() error {
if err := c.checkConn(); err != nil {
return err
}
if err := c.smtpClient.Reset(); err != nil {
if err := c.sc.Reset(); err != nil {
return fmt.Errorf("failed to send RSET to SMTP client: %w", err)
}
@ -672,17 +672,17 @@ func (c *Client) DialAndSendWithContext(ctx context.Context, ml ...*Msg) error {
// checkConn makes sure that a required server connection is available and extends the
// connection deadline
func (c *Client) checkConn() error {
if c.connection == nil {
if c.co == nil {
return ErrNoActiveConnection
}
if !c.noNoop {
if err := c.smtpClient.Noop(); err != nil {
if err := c.sc.Noop(); err != nil {
return ErrNoActiveConnection
}
}
if err := c.connection.SetDeadline(time.Now().Add(c.connTimeout)); err != nil {
if err := c.co.SetDeadline(time.Now().Add(c.cto)); err != nil {
return ErrDeadlineExtendFailed
}
return nil
@ -696,12 +696,12 @@ func (c *Client) serverFallbackAddr() string {
// tls tries to make sure that the STARTTLS requirements are satisfied
func (c *Client) tls() error {
if c.connection == nil {
if c.co == nil {
return ErrNoActiveConnection
}
if !c.useSSL && c.tlspolicy != NoTLS {
if !c.ssl && c.tlspolicy != NoTLS {
est := false
st, _ := c.smtpClient.Extension("STARTTLS")
st, _ := c.sc.Extension("STARTTLS")
if c.tlspolicy == TLSMandatory {
est = true
if !st {
@ -715,11 +715,11 @@ func (c *Client) tls() error {
}
}
if est {
if err := c.smtpClient.StartTLS(c.tlsconfig); err != nil {
if err := c.sc.StartTLS(c.tlsconfig); err != nil {
return err
}
}
_, c.isEncrypted = c.smtpClient.TLSConnectionState()
_, c.enc = c.sc.TLSConnectionState()
}
return nil
}
@ -729,40 +729,40 @@ 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 != "" {
sa, sat := c.smtpClient.Extension("AUTH")
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.smtpAuthType {
switch c.satype {
case SMTPAuthPlain:
if !strings.Contains(sat, string(SMTPAuthPlain)) {
return ErrPlainAuthNotSupported
}
c.smtpAuth = smtp.PlainAuth("", c.user, c.pass, c.host)
c.sa = smtp.PlainAuth("", c.user, c.pass, c.host)
case SMTPAuthLogin:
if !strings.Contains(sat, string(SMTPAuthLogin)) {
return ErrLoginAuthNotSupported
}
c.smtpAuth = smtp.LoginAuth(c.user, c.pass, c.host)
c.sa = smtp.LoginAuth(c.user, c.pass, c.host)
case SMTPAuthCramMD5:
if !strings.Contains(sat, string(SMTPAuthCramMD5)) {
return ErrCramMD5AuthNotSupported
}
c.smtpAuth = smtp.CRAMMD5Auth(c.user, c.pass)
c.sa = smtp.CRAMMD5Auth(c.user, c.pass)
case SMTPAuthXOAUTH2:
if !strings.Contains(sat, string(SMTPAuthXOAUTH2)) {
return ErrXOauth2AuthNotSupported
}
c.smtpAuth = smtp.XOAuth2Auth(c.user, c.pass)
c.sa = smtp.XOAuth2Auth(c.user, c.pass)
default:
return fmt.Errorf("unsupported SMTP AUTH type %q", c.smtpAuthType)
return fmt.Errorf("unsupported SMTP AUTH type %q", c.satype)
}
}
if c.smtpAuth != nil {
if err := c.smtpClient.Auth(c.smtpAuth); err != nil {
if c.sa != nil {
if err := c.sc.Auth(c.sa); err != nil {
return fmt.Errorf("SMTP AUTH failed: %w", err)
}
}

View file

@ -18,7 +18,7 @@ func (c *Client) Send(ml ...*Msg) error {
for _, m := range ml {
m.sendError = nil
if m.encoding == NoEncoding {
if ok, _ := c.smtpClient.Extension("8BITMIME"); !ok {
if ok, _ := c.sc.Extension("8BITMIME"); !ok {
se := &SendError{Reason: ErrNoUnencoded, isTemp: false}
m.sendError = se
errs = append(errs, se)
@ -42,12 +42,12 @@ func (c *Client) Send(ml ...*Msg) error {
if c.dsn {
if c.dsnmrtype != "" {
c.smtpClient.SetDSNMailReturnOption(string(c.dsnmrtype))
c.sc.SetDSNMailReturnOption(string(c.dsnmrtype))
}
}
if err := c.smtpClient.Mail(f); err != nil {
if err := c.sc.Mail(f); err != nil {
se := &SendError{Reason: ErrSMTPMailFrom, errlist: []error{err}, isTemp: isTempError(err)}
if reserr := c.smtpClient.Reset(); reserr != nil {
if reserr := c.sc.Reset(); reserr != nil {
se.errlist = append(se.errlist, reserr)
}
m.sendError = se
@ -59,9 +59,9 @@ func (c *Client) Send(ml ...*Msg) error {
rse.errlist = make([]error, 0)
rse.rcpt = make([]string, 0)
rno := strings.Join(c.dsnrntype, ",")
c.smtpClient.SetDSNRcptNotifyOption(rno)
c.sc.SetDSNRcptNotifyOption(rno)
for _, r := range rl {
if err := c.smtpClient.Rcpt(r); err != nil {
if err := c.sc.Rcpt(r); err != nil {
rse.Reason = ErrSMTPRcptTo
rse.errlist = append(rse.errlist, err)
rse.rcpt = append(rse.rcpt, r)
@ -70,14 +70,14 @@ func (c *Client) Send(ml ...*Msg) error {
}
}
if failed {
if reserr := c.smtpClient.Reset(); reserr != nil {
if reserr := c.sc.Reset(); reserr != nil {
rse.errlist = append(rse.errlist, err)
}
m.sendError = rse
errs = append(errs, rse)
continue
}
w, err := c.smtpClient.Data()
w, err := c.sc.Data()
if err != nil {
se := &SendError{Reason: ErrSMTPData, errlist: []error{err}, isTemp: isTempError(err)}
m.sendError = se

View file

@ -21,7 +21,7 @@ func (c *Client) Send(ml ...*Msg) (rerr error) {
for _, m := range ml {
m.sendError = nil
if m.encoding == NoEncoding {
if ok, _ := c.smtpClient.Extension("8BITMIME"); !ok {
if ok, _ := c.sc.Extension("8BITMIME"); !ok {
m.sendError = &SendError{Reason: ErrNoUnencoded, isTemp: false}
rerr = errors.Join(rerr, m.sendError)
continue
@ -42,13 +42,13 @@ func (c *Client) Send(ml ...*Msg) (rerr error) {
if c.dsn {
if c.dsnmrtype != "" {
c.smtpClient.SetDSNMailReturnOption(string(c.dsnmrtype))
c.sc.SetDSNMailReturnOption(string(c.dsnmrtype))
}
}
if err := c.smtpClient.Mail(f); err != nil {
if err := c.sc.Mail(f); err != nil {
m.sendError = &SendError{Reason: ErrSMTPMailFrom, errlist: []error{err}, isTemp: isTempError(err)}
rerr = errors.Join(rerr, m.sendError)
if reserr := c.smtpClient.Reset(); reserr != nil {
if reserr := c.sc.Reset(); reserr != nil {
rerr = errors.Join(rerr, reserr)
}
continue
@ -58,9 +58,9 @@ func (c *Client) Send(ml ...*Msg) (rerr error) {
rse.errlist = make([]error, 0)
rse.rcpt = make([]string, 0)
rno := strings.Join(c.dsnrntype, ",")
c.smtpClient.SetDSNRcptNotifyOption(rno)
c.sc.SetDSNRcptNotifyOption(rno)
for _, r := range rl {
if err := c.smtpClient.Rcpt(r); err != nil {
if err := c.sc.Rcpt(r); err != nil {
rse.Reason = ErrSMTPRcptTo
rse.errlist = append(rse.errlist, err)
rse.rcpt = append(rse.rcpt, r)
@ -69,14 +69,14 @@ func (c *Client) Send(ml ...*Msg) (rerr error) {
}
}
if failed {
if reserr := c.smtpClient.Reset(); reserr != nil {
if reserr := c.sc.Reset(); reserr != nil {
rerr = errors.Join(rerr, reserr)
}
m.sendError = rse
rerr = errors.Join(rerr, m.sendError)
continue
}
w, err := c.smtpClient.Data()
w, err := c.sc.Data()
if err != nil {
m.sendError = &SendError{Reason: ErrSMTPData, errlist: []error{err}, isTemp: isTempError(err)}
rerr = errors.Join(rerr, m.sendError)

View file

@ -49,9 +49,9 @@ func TestNewClient(t *testing.T) {
if c.host != tt.host {
t.Errorf("failed to create new client. Host expected: %s, got: %s", host, c.host)
}
if c.connTimeout != DefaultTimeout {
if c.cto != DefaultTimeout {
t.Errorf("failed to create new client. Timeout expected: %s, got: %s", DefaultTimeout.String(),
c.connTimeout.String())
c.cto.String())
}
if c.port != DefaultPort {
t.Errorf("failed to create new client. Port expected: %d, got: %d", DefaultPort, c.port)
@ -205,8 +205,8 @@ func TestWithTimeout(t *testing.T) {
t.Errorf("failed to create new client: %s", err)
return
}
if c.connTimeout != tt.want {
t.Errorf("failed to set custom timeout. Want: %d, got: %d", tt.want, c.connTimeout)
if c.cto != tt.want {
t.Errorf("failed to set custom timeout. Want: %d, got: %d", tt.want, c.cto)
}
})
}
@ -345,8 +345,8 @@ func TestSetSSL(t *testing.T) {
return
}
c.SetSSL(tt.value)
if c.useSSL != tt.value {
t.Errorf("failed to set SSL setting. Got: %t, want: %t", c.useSSL, tt.value)
if c.ssl != tt.value {
t.Errorf("failed to set SSL setting. Got: %t, want: %t", c.ssl, tt.value)
}
})
}
@ -374,8 +374,8 @@ func TestClient_SetSSLPort(t *testing.T) {
return
}
c.SetSSLPort(tt.value, tt.fb)
if c.useSSL != tt.value {
t.Errorf("failed to set SSL setting. Got: %t, want: %t", c.useSSL, tt.value)
if c.ssl != tt.value {
t.Errorf("failed to set SSL setting. Got: %t, want: %t", c.ssl, tt.value)
}
if c.port != tt.port {
t.Errorf("failed to set SSLPort, wanted port: %d, got: %d", c.port, tt.port)
@ -460,8 +460,8 @@ func TestSetSMTPAuth(t *testing.T) {
return
}
c.SetSMTPAuth(tt.value)
if string(c.smtpAuthType) != tt.want {
t.Errorf("failed to set SMTP auth type. Expected %s, got: %s", tt.want, string(c.smtpAuthType))
if string(c.satype) != tt.want {
t.Errorf("failed to set SMTP auth type. Expected %s, got: %s", tt.want, string(c.satype))
}
})
}
@ -590,10 +590,10 @@ func TestSetSMTPAuthCustom(t *testing.T) {
return
}
c.SetSMTPAuthCustom(tt.value)
if c.smtpAuth == nil {
if c.sa == nil {
t.Errorf("failed to set custom SMTP auth method. SMTP Auth method is empty")
}
p, _, err := c.smtpAuth.Start(&si)
p, _, err := c.sa.Start(&si)
if err != nil {
t.Errorf("SMTP Auth Start() method returned error: %s", err)
}
@ -615,10 +615,10 @@ func TestClient_DialWithContext(t *testing.T) {
t.Errorf("failed to dial with context: %s", err)
return
}
if c.connection == nil {
if c.co == nil {
t.Errorf("DialWithContext didn't fail but no connection found.")
}
if c.smtpClient == nil {
if c.sc == nil {
t.Errorf("DialWithContext didn't fail but no SMTP client found.")
}
if err := c.Close(); err != nil {
@ -640,10 +640,10 @@ func TestClient_DialWithContext_Fallback(t *testing.T) {
t.Errorf("failed to dial with context: %s", err)
return
}
if c.connection == nil {
if c.co == nil {
t.Errorf("DialWithContext didn't fail but no connection found.")
}
if c.smtpClient == nil {
if c.sc == nil {
t.Errorf("DialWithContext didn't fail but no SMTP client found.")
}
if err := c.Close(); err != nil {
@ -670,10 +670,10 @@ func TestClient_DialWithContext_Debug(t *testing.T) {
t.Errorf("failed to dial with context: %s", err)
return
}
if c.connection == nil {
if c.co == nil {
t.Errorf("DialWithContext didn't fail but no connection found.")
}
if c.smtpClient == nil {
if c.sc == nil {
t.Errorf("DialWithContext didn't fail but no SMTP client found.")
}
c.SetDebugLog(true)
@ -694,10 +694,10 @@ func TestClient_DialWithContext_Debug_custom(t *testing.T) {
t.Errorf("failed to dial with context: %s", err)
return
}
if c.connection == nil {
if c.co == nil {
t.Errorf("DialWithContext didn't fail but no connection found.")
}
if c.smtpClient == nil {
if c.sc == nil {
t.Errorf("DialWithContext didn't fail but no SMTP client found.")
}
c.SetDebugLog(true)
@ -714,7 +714,7 @@ func TestClient_DialWithContextInvalidHost(t *testing.T) {
if err != nil {
t.Skipf("failed to create test client: %s. Skipping tests", err)
}
c.connection = nil
c.co = nil
c.host = "invalid.addr"
ctx := context.Background()
if err := c.DialWithContext(ctx); err == nil {
@ -730,7 +730,7 @@ func TestClient_DialWithContextInvalidHELO(t *testing.T) {
if err != nil {
t.Skipf("failed to create test client: %s. Skipping tests", err)
}
c.connection = nil
c.co = nil
c.helo = ""
ctx := context.Background()
if err := c.DialWithContext(ctx); err == nil {
@ -762,7 +762,7 @@ func TestClient_checkConn(t *testing.T) {
if err != nil {
t.Skipf("failed to create test client: %s. Skipping tests", err)
}
c.connection = nil
c.co = nil
if err := c.checkConn(); err == nil {
t.Errorf("connCheck() should fail but succeeded")
}
@ -799,10 +799,10 @@ func TestClient_DialWithContextOptions(t *testing.T) {
return
}
if !tt.sf {
if c.connection == nil && !tt.sf {
if c.co == nil && !tt.sf {
t.Errorf("DialWithContext didn't fail but no connection found.")
}
if c.smtpClient == nil && !tt.sf {
if c.sc == nil && !tt.sf {
t.Errorf("DialWithContext didn't fail but no SMTP client found.")
}
if err := c.Reset(); err != nil {
@ -1002,16 +1002,16 @@ func TestClient_DialSendCloseBroken(t *testing.T) {
return
}
if tt.closestart {
_ = c.smtpClient.Close()
_ = c.connection.Close()
_ = c.sc.Close()
_ = c.co.Close()
}
if err := c.Send(m); err != nil && !tt.sf {
t.Errorf("Send() failed: %s", err)
return
}
if tt.closeearly {
_ = c.smtpClient.Close()
_ = c.connection.Close()
_ = c.sc.Close()
_ = c.co.Close()
}
if err := c.Close(); err != nil && !tt.sf {
t.Errorf("Close() failed: %s", err)
@ -1062,16 +1062,16 @@ func TestClient_DialSendCloseBrokenWithDSN(t *testing.T) {
return
}
if tt.closestart {
_ = c.smtpClient.Close()
_ = c.connection.Close()
_ = c.sc.Close()
_ = c.co.Close()
}
if err := c.Send(m); err != nil && !tt.sf {
t.Errorf("Send() failed: %s", err)
return
}
if tt.closeearly {
_ = c.smtpClient.Close()
_ = c.connection.Close()
_ = c.sc.Close()
_ = c.co.Close()
}
if err := c.Close(); err != nil && !tt.sf {
t.Errorf("Close() failed: %s", err)

408
msg.go
View file

@ -708,10 +708,7 @@ func (m *Msg) SetBodyString(contentType ContentType, content string, opts ...Par
}
// SetBodyWriter sets the body of the message.
func (m *Msg) SetBodyWriter(
contentType ContentType, writeFunc func(io.Writer) (int64, error),
opts ...PartOption,
) {
func (m *Msg) SetBodyWriter(contentType ContentType, writeFunc func(io.Writer) (int64, error), opts ...PartOption) {
p := m.newPart(contentType, opts...)
p.w = writeFunc
m.parts = []*Part{p}
@ -719,88 +716,85 @@ func (m *Msg) SetBodyWriter(
// SetBodyHTMLTemplate sets the body of the message from a given html/template.Template pointer
// The content type will be set to text/html automatically
func (m *Msg) SetBodyHTMLTemplate(tpl *ht.Template, data interface{}, opts ...PartOption) error {
if tpl == nil {
func (m *Msg) SetBodyHTMLTemplate(t *ht.Template, d interface{}, o ...PartOption) error {
if t == nil {
return fmt.Errorf(errTplPointerNil)
}
buffer := bytes.Buffer{}
if err := tpl.Execute(&buffer, data); err != nil {
buf := bytes.Buffer{}
if err := t.Execute(&buf, d); err != nil {
return fmt.Errorf(errTplExecuteFailed, err)
}
writeFunc := writeFuncFromBuffer(&buffer)
m.SetBodyWriter(TypeTextHTML, writeFunc, opts...)
w := writeFuncFromBuffer(&buf)
m.SetBodyWriter(TypeTextHTML, w, o...)
return nil
}
// SetBodyTextTemplate sets the body of the message from a given text/template.Template pointer
// The content type will be set to text/plain automatically
func (m *Msg) SetBodyTextTemplate(tpl *tt.Template, data interface{}, opts ...PartOption) error {
if tpl == nil {
func (m *Msg) SetBodyTextTemplate(t *tt.Template, d interface{}, o ...PartOption) error {
if t == nil {
return fmt.Errorf(errTplPointerNil)
}
buf := bytes.Buffer{}
if err := tpl.Execute(&buf, data); err != nil {
if err := t.Execute(&buf, d); err != nil {
return fmt.Errorf(errTplExecuteFailed, err)
}
writeFunc := writeFuncFromBuffer(&buf)
m.SetBodyWriter(TypeTextPlain, writeFunc, opts...)
w := writeFuncFromBuffer(&buf)
m.SetBodyWriter(TypeTextPlain, w, o...)
return nil
}
// AddAlternativeString sets the alternative body of the message.
func (m *Msg) AddAlternativeString(contentType ContentType, content string, opts ...PartOption) {
buffer := bytes.NewBufferString(content)
writeFunc := writeFuncFromBuffer(buffer)
m.AddAlternativeWriter(contentType, writeFunc, opts...)
func (m *Msg) AddAlternativeString(ct ContentType, b string, o ...PartOption) {
buf := bytes.NewBufferString(b)
w := writeFuncFromBuffer(buf)
m.AddAlternativeWriter(ct, w, o...)
}
// AddAlternativeWriter sets the body of the message.
func (m *Msg) AddAlternativeWriter(
contentType ContentType, writeFunc func(io.Writer) (int64, error),
opts ...PartOption,
) {
part := m.newPart(contentType, opts...)
part.w = writeFunc
m.parts = append(m.parts, part)
func (m *Msg) AddAlternativeWriter(ct ContentType, w func(io.Writer) (int64, error), o ...PartOption) {
p := m.newPart(ct, o...)
p.w = w
m.parts = append(m.parts, p)
}
// AddAlternativeHTMLTemplate sets the alternative body of the message to a html/template.Template output
// The content type will be set to text/html automatically
func (m *Msg) AddAlternativeHTMLTemplate(tpl *ht.Template, data interface{}, opts ...PartOption) error {
if tpl == nil {
func (m *Msg) AddAlternativeHTMLTemplate(t *ht.Template, d interface{}, o ...PartOption) error {
if t == nil {
return fmt.Errorf(errTplPointerNil)
}
buffer := bytes.Buffer{}
if err := tpl.Execute(&buffer, data); err != nil {
buf := bytes.Buffer{}
if err := t.Execute(&buf, d); err != nil {
return fmt.Errorf(errTplExecuteFailed, err)
}
writeFunc := writeFuncFromBuffer(&buffer)
m.AddAlternativeWriter(TypeTextHTML, writeFunc, opts...)
w := writeFuncFromBuffer(&buf)
m.AddAlternativeWriter(TypeTextHTML, w, o...)
return nil
}
// AddAlternativeTextTemplate sets the alternative body of the message to a text/template.Template output
// The content type will be set to text/plain automatically
func (m *Msg) AddAlternativeTextTemplate(tpl *tt.Template, data interface{}, opts ...PartOption) error {
if tpl == nil {
func (m *Msg) AddAlternativeTextTemplate(t *tt.Template, d interface{}, o ...PartOption) error {
if t == nil {
return fmt.Errorf(errTplPointerNil)
}
buffer := bytes.Buffer{}
if err := tpl.Execute(&buffer, data); err != nil {
buf := bytes.Buffer{}
if err := t.Execute(&buf, d); err != nil {
return fmt.Errorf(errTplExecuteFailed, err)
}
writeFunc := writeFuncFromBuffer(&buffer)
m.AddAlternativeWriter(TypeTextPlain, writeFunc, opts...)
w := writeFuncFromBuffer(&buf)
m.AddAlternativeWriter(TypeTextPlain, w, o...)
return nil
}
// AttachFile adds an attachment File to the Msg
func (m *Msg) AttachFile(name string, opts ...FileOption) {
file := fileFromFS(name)
if file == nil {
func (m *Msg) AttachFile(n string, o ...FileOption) {
f := fileFromFS(n)
if f == nil {
return
}
m.attachments = m.appendFile(m.attachments, file, opts...)
m.attachments = m.appendFile(m.attachments, f, o...)
}
// AttachReader adds an attachment File via io.Reader to the Msg
@ -809,65 +803,61 @@ func (m *Msg) AttachFile(name string, opts ...FileOption) {
// into memory first, so it can seek through it. Using larger amounts of
// data on the io.Reader should be avoided. For such, it is recommended to
// either use AttachFile or AttachReadSeeker instead
func (m *Msg) AttachReader(name string, reader io.Reader, opts ...FileOption) error {
file, err := fileFromReader(name, reader)
func (m *Msg) AttachReader(n string, r io.Reader, o ...FileOption) error {
f, err := fileFromReader(n, r)
if err != nil {
return err
}
m.attachments = m.appendFile(m.attachments, file, opts...)
m.attachments = m.appendFile(m.attachments, f, o...)
return nil
}
// AttachReadSeeker adds an attachment File via io.ReadSeeker to the Msg
func (m *Msg) AttachReadSeeker(name string, reader io.ReadSeeker, opts ...FileOption) {
file := fileFromReadSeeker(name, reader)
m.attachments = m.appendFile(m.attachments, file, opts...)
func (m *Msg) AttachReadSeeker(n string, r io.ReadSeeker, o ...FileOption) {
f := fileFromReadSeeker(n, r)
m.attachments = m.appendFile(m.attachments, f, o...)
}
// AttachHTMLTemplate adds the output of a html/template.Template pointer as File attachment to the Msg
func (m *Msg) AttachHTMLTemplate(
name string, tpl *ht.Template, data interface{}, opts ...FileOption,
) error {
file, err := fileFromHTMLTemplate(name, tpl, data)
func (m *Msg) AttachHTMLTemplate(n string, t *ht.Template, d interface{}, o ...FileOption) error {
f, err := fileFromHTMLTemplate(n, t, d)
if err != nil {
return fmt.Errorf("failed to attach template: %w", err)
}
m.attachments = m.appendFile(m.attachments, file, opts...)
m.attachments = m.appendFile(m.attachments, f, o...)
return nil
}
// AttachTextTemplate adds the output of a text/template.Template pointer as File attachment to the Msg
func (m *Msg) AttachTextTemplate(
name string, tpl *tt.Template, data interface{}, opts ...FileOption,
) error {
file, err := fileFromTextTemplate(name, tpl, data)
func (m *Msg) AttachTextTemplate(n string, t *tt.Template, d interface{}, o ...FileOption) error {
f, err := fileFromTextTemplate(n, t, d)
if err != nil {
return fmt.Errorf("failed to attach template: %w", err)
}
m.attachments = m.appendFile(m.attachments, file, opts...)
m.attachments = m.appendFile(m.attachments, f, o...)
return nil
}
// AttachFromEmbedFS adds an attachment File from an embed.FS to the Msg
func (m *Msg) AttachFromEmbedFS(name string, fs *embed.FS, opts ...FileOption) error {
if fs == nil {
func (m *Msg) AttachFromEmbedFS(n string, f *embed.FS, o ...FileOption) error {
if f == nil {
return fmt.Errorf("embed.FS must not be nil")
}
file, err := fileFromEmbedFS(name, fs)
ef, err := fileFromEmbedFS(n, f)
if err != nil {
return err
}
m.attachments = m.appendFile(m.attachments, file, opts...)
m.attachments = m.appendFile(m.attachments, ef, o...)
return nil
}
// EmbedFile adds an embedded File to the Msg
func (m *Msg) EmbedFile(name string, opts ...FileOption) {
file := fileFromFS(name)
if file == nil {
func (m *Msg) EmbedFile(n string, o ...FileOption) {
f := fileFromFS(n)
if f == nil {
return
}
m.embeds = m.appendFile(m.embeds, file, opts...)
m.embeds = m.appendFile(m.embeds, f, o...)
}
// EmbedReader adds an embedded File from an io.Reader to the Msg
@ -876,55 +866,51 @@ func (m *Msg) EmbedFile(name string, opts ...FileOption) {
// into memory first, so it can seek through it. Using larger amounts of
// data on the io.Reader should be avoided. For such, it is recommended to
// either use EmbedFile or EmbedReadSeeker instead
func (m *Msg) EmbedReader(name string, reader io.Reader, opts ...FileOption) error {
file, err := fileFromReader(name, reader)
func (m *Msg) EmbedReader(n string, r io.Reader, o ...FileOption) error {
f, err := fileFromReader(n, r)
if err != nil {
return err
}
m.embeds = m.appendFile(m.embeds, file, opts...)
m.embeds = m.appendFile(m.embeds, f, o...)
return nil
}
// EmbedReadSeeker adds an embedded File from an io.ReadSeeker to the Msg
func (m *Msg) EmbedReadSeeker(name string, reader io.ReadSeeker, opts ...FileOption) {
file := fileFromReadSeeker(name, reader)
m.embeds = m.appendFile(m.embeds, file, opts...)
func (m *Msg) EmbedReadSeeker(n string, r io.ReadSeeker, o ...FileOption) {
f := fileFromReadSeeker(n, r)
m.embeds = m.appendFile(m.embeds, f, o...)
}
// EmbedHTMLTemplate adds the output of a html/template.Template pointer as embedded File to the Msg
func (m *Msg) EmbedHTMLTemplate(
name string, tpl *ht.Template, data interface{}, opts ...FileOption,
) error {
file, err := fileFromHTMLTemplate(name, tpl, data)
func (m *Msg) EmbedHTMLTemplate(n string, t *ht.Template, d interface{}, o ...FileOption) error {
f, err := fileFromHTMLTemplate(n, t, d)
if err != nil {
return fmt.Errorf("failed to embed template: %w", err)
}
m.embeds = m.appendFile(m.embeds, file, opts...)
m.embeds = m.appendFile(m.embeds, f, o...)
return nil
}
// EmbedTextTemplate adds the output of a text/template.Template pointer as embedded File to the Msg
func (m *Msg) EmbedTextTemplate(
name string, tpl *tt.Template, data interface{}, opts ...FileOption,
) error {
file, err := fileFromTextTemplate(name, tpl, data)
func (m *Msg) EmbedTextTemplate(n string, t *tt.Template, d interface{}, o ...FileOption) error {
f, err := fileFromTextTemplate(n, t, d)
if err != nil {
return fmt.Errorf("failed to embed template: %w", err)
}
m.embeds = m.appendFile(m.embeds, file, opts...)
m.embeds = m.appendFile(m.embeds, f, o...)
return nil
}
// EmbedFromEmbedFS adds an embedded File from an embed.FS to the Msg
func (m *Msg) EmbedFromEmbedFS(name string, fs *embed.FS, opts ...FileOption) error {
if fs == nil {
func (m *Msg) EmbedFromEmbedFS(n string, f *embed.FS, o ...FileOption) error {
if f == nil {
return fmt.Errorf("embed.FS must not be nil")
}
file, err := fileFromEmbedFS(name, fs)
ef, err := fileFromEmbedFS(n, f)
if err != nil {
return err
}
m.embeds = m.appendFile(m.embeds, file, opts...)
m.embeds = m.appendFile(m.embeds, ef, o...)
return nil
}
@ -939,73 +925,73 @@ func (m *Msg) Reset() {
}
// ApplyMiddlewares apply the list of middlewares to a Msg
func (m *Msg) applyMiddlewares(msg *Msg) *Msg {
for _, middleware := range m.middlewares {
msg = middleware.Handle(msg)
func (m *Msg) applyMiddlewares(ms *Msg) *Msg {
for _, mw := range m.middlewares {
ms = mw.Handle(ms)
}
return msg
return ms
}
// WriteTo writes the formated Msg into a give io.Writer and satisfies the io.WriteTo interface
func (m *Msg) WriteTo(writer io.Writer) (int64, error) {
mw := &msgWriter{writer: writer, charset: m.charset, encoder: m.encoder}
func (m *Msg) WriteTo(w io.Writer) (int64, error) {
mw := &msgWriter{writer: w, charset: m.charset, encoder: m.encoder}
mw.writeMsg(m.applyMiddlewares(m))
return mw.bytesWritten, mw.err
}
// WriteToSkipMiddleware writes the formated Msg into a give io.Writer and satisfies
// the io.WriteTo interface but will skip the given Middleware
func (m *Msg) WriteToSkipMiddleware(writer io.Writer, middleWareType MiddlewareType) (int64, error) {
var origMiddlewares, middlewares []Middleware
origMiddlewares = m.middlewares
func (m *Msg) WriteToSkipMiddleware(w io.Writer, mt MiddlewareType) (int64, error) {
var omwl, mwl []Middleware
omwl = m.middlewares
for i := range m.middlewares {
if m.middlewares[i].Type() == middleWareType {
if m.middlewares[i].Type() == mt {
continue
}
middlewares = append(middlewares, m.middlewares[i])
mwl = append(mwl, m.middlewares[i])
}
m.middlewares = middlewares
mw := &msgWriter{writer: writer, charset: m.charset, encoder: m.encoder}
m.middlewares = mwl
mw := &msgWriter{writer: w, charset: m.charset, encoder: m.encoder}
mw.writeMsg(m.applyMiddlewares(m))
m.middlewares = origMiddlewares
m.middlewares = omwl
return mw.bytesWritten, mw.err
}
// Write is an alias method to WriteTo due to compatibility reasons
func (m *Msg) Write(writer io.Writer) (int64, error) {
return m.WriteTo(writer)
func (m *Msg) Write(w io.Writer) (int64, error) {
return m.WriteTo(w)
}
// appendFile adds a File to the Msg (as attachment or embed)
func (m *Msg) appendFile(files []*File, file *File, opts ...FileOption) []*File {
func (m *Msg) appendFile(c []*File, f *File, o ...FileOption) []*File {
// Override defaults with optionally provided FileOption functions
for _, opt := range opts {
if opt == nil {
for _, co := range o {
if co == nil {
continue
}
opt(file)
co(f)
}
if files == nil {
return []*File{file}
if c == nil {
return []*File{f}
}
return append(files, file)
return append(c, f)
}
// WriteToFile stores the Msg as file on disk. It will try to create the given filename
// Already existing files will be overwritten
func (m *Msg) WriteToFile(name string) error {
file, err := os.Create(name)
func (m *Msg) WriteToFile(n string) error {
f, err := os.Create(n)
if err != nil {
return fmt.Errorf("failed to create output file: %w", err)
}
defer func() { _ = file.Close() }()
_, err = m.WriteTo(file)
defer func() { _ = f.Close() }()
_, err = m.WriteTo(f)
if err != nil {
return fmt.Errorf("failed to write to output file: %w", err)
}
return file.Close()
return f.Close()
}
// WriteToSendmail returns WriteToSendmailWithCommand with a default sendmail path
@ -1015,38 +1001,38 @@ func (m *Msg) WriteToSendmail() error {
// WriteToSendmailWithCommand returns WriteToSendmailWithContext with a default timeout
// of 5 seconds and a given sendmail path
func (m *Msg) WriteToSendmailWithCommand(sendmailPath string) error {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
return m.WriteToSendmailWithContext(ctx, sendmailPath)
func (m *Msg) WriteToSendmailWithCommand(sp string) error {
tctx, tcfn := context.WithTimeout(context.Background(), time.Second*5)
defer tcfn()
return m.WriteToSendmailWithContext(tctx, sp)
}
// WriteToSendmailWithContext opens an pipe to the local sendmail binary and tries to send the
// mail though that. It takes a context.Context, the path to the sendmail binary and additional
// arguments for the sendmail binary as parameters
func (m *Msg) WriteToSendmailWithContext(ctx context.Context, sendmailPath string, args ...string) error {
cmdCtx := exec.CommandContext(ctx, sendmailPath)
cmdCtx.Args = append(cmdCtx.Args, "-oi", "-t")
cmdCtx.Args = append(cmdCtx.Args, args...)
func (m *Msg) WriteToSendmailWithContext(ctx context.Context, sp string, a ...string) error {
ec := exec.CommandContext(ctx, sp)
ec.Args = append(ec.Args, "-oi", "-t")
ec.Args = append(ec.Args, a...)
stdErr, err := cmdCtx.StderrPipe()
se, err := ec.StderrPipe()
if err != nil {
return fmt.Errorf("failed to set STDERR pipe: %w", err)
}
stdIn, err := cmdCtx.StdinPipe()
si, err := ec.StdinPipe()
if err != nil {
return fmt.Errorf("failed to set STDIN pipe: %w", err)
}
if stdErr == nil || stdIn == nil {
if se == nil || si == nil {
return fmt.Errorf("received nil for STDERR or STDIN pipe")
}
// Start the execution and write to STDIN
if err = cmdCtx.Start(); err != nil {
if err = ec.Start(); err != nil {
return fmt.Errorf("could not start sendmail execution: %w", err)
}
_, err = m.WriteTo(stdIn)
_, err = m.WriteTo(si)
if err != nil {
if !errors.Is(err, syscall.EPIPE) {
return fmt.Errorf("failed to write mail to buffer: %w", err)
@ -1054,20 +1040,20 @@ func (m *Msg) WriteToSendmailWithContext(ctx context.Context, sendmailPath strin
}
// Close STDIN and wait for completion or cancellation of the sendmail executable
if err = stdIn.Close(); err != nil {
if err = si.Close(); err != nil {
return fmt.Errorf("failed to close STDIN pipe: %w", err)
}
// Read the stderr pipe for possible errors
sendmailErr, err := io.ReadAll(stdErr)
serr, err := io.ReadAll(se)
if err != nil {
return fmt.Errorf("failed to read STDERR pipe: %w", err)
}
if len(sendmailErr) > 0 {
return fmt.Errorf("sendmail command failed: %s", string(sendmailErr))
if len(serr) > 0 {
return fmt.Errorf("sendmail command failed: %s", string(serr))
}
if err = cmdCtx.Wait(); err != nil {
if err = ec.Wait(); err != nil {
return fmt.Errorf("sendmail command execution failed: %w", err)
}
@ -1081,24 +1067,24 @@ func (m *Msg) WriteToSendmailWithContext(ctx context.Context, sendmailPath strin
// changes will not be reflected in the Reader. You will have to use Msg.UpdateReader
// first to update the Reader's buffer with the current Msg content
func (m *Msg) NewReader() *Reader {
reader := &Reader{}
buffer := bytes.Buffer{}
_, err := m.Write(&buffer)
r := &Reader{}
wbuf := bytes.Buffer{}
_, err := m.Write(&wbuf)
if err != nil {
reader.err = fmt.Errorf("failed to write Msg to Reader buffer: %w", err)
r.err = fmt.Errorf("failed to write Msg to Reader buffer: %w", err)
}
reader.buf = buffer.Bytes()
return reader
r.buf = wbuf.Bytes()
return r
}
// UpdateReader will update a Reader with the content of the given Msg and reset the
// Reader position to the start
func (m *Msg) UpdateReader(reader *Reader) {
buffer := bytes.Buffer{}
_, err := m.Write(&buffer)
reader.Reset()
reader.buf = buffer.Bytes()
reader.err = err
func (m *Msg) UpdateReader(r *Reader) {
wbuf := bytes.Buffer{}
_, err := m.Write(&wbuf)
r.Reset()
r.buf = wbuf.Bytes()
r.err = err
}
// HasSendError returns true if the Msg experienced an error during the message delivery and the
@ -1110,9 +1096,9 @@ func (m *Msg) HasSendError() bool {
// SendErrorIsTemp returns true if the Msg experienced an error during the message delivery and the
// corresponding error was of temporary nature and should be retried later
func (m *Msg) SendErrorIsTemp() bool {
var err *SendError
if errors.As(m.sendError, &err) && err != nil {
return err.isTemp
var e *SendError
if errors.As(m.sendError, &e) && e != nil {
return e.isTemp
}
return false
}
@ -1124,19 +1110,19 @@ func (m *Msg) SendError() error {
// encodeString encodes a string based on the configured message encoder and the corresponding
// charset for the Msg
func (m *Msg) encodeString(str string) string {
return m.encoder.Encode(string(m.charset), str)
func (m *Msg) encodeString(s string) string {
return m.encoder.Encode(string(m.charset), s)
}
// hasAlt returns true if the Msg has more than one part
func (m *Msg) hasAlt() bool {
count := 0
for _, part := range m.parts {
if !part.del {
count++
c := 0
for _, p := range m.parts {
if !p.del {
c++
}
}
return count > 1 && m.pgptype == 0
return c > 1 && m.pgptype == 0
}
// hasMixed returns true if the Msg has mixed parts
@ -1155,19 +1141,19 @@ func (m *Msg) hasPGPType() bool {
}
// newPart returns a new Part for the Msg
func (m *Msg) newPart(contentType ContentType, opts ...PartOption) *Part {
func (m *Msg) newPart(ct ContentType, o ...PartOption) *Part {
p := &Part{
ctype: contentType,
ctype: ct,
cset: m.charset,
enc: m.encoding,
}
// Override defaults with optionally provided MsgOption functions
for _, opt := range opts {
if opt == nil {
for _, co := range o {
if co == nil {
continue
}
opt(p)
co(p)
}
return p
@ -1201,118 +1187,118 @@ func (m *Msg) addDefaultHeader() {
}
// fileFromEmbedFS returns a File pointer from a given file in the provided embed.FS
func fileFromEmbedFS(name string, fs *embed.FS) (*File, error) {
_, err := fs.Open(name)
func fileFromEmbedFS(n string, f *embed.FS) (*File, error) {
_, err := f.Open(n)
if err != nil {
return nil, fmt.Errorf("failed to open file from embed.FS: %w", err)
}
return &File{
Name: filepath.Base(name),
Name: filepath.Base(n),
Header: make(map[string][]string),
Writer: func(writer io.Writer) (int64, error) {
file, err := fs.Open(name)
Writer: func(w io.Writer) (int64, error) {
h, err := f.Open(n)
if err != nil {
return 0, err
}
numBytes, err := io.Copy(writer, file)
nb, err := io.Copy(w, h)
if err != nil {
_ = file.Close()
return numBytes, fmt.Errorf("failed to copy file to io.Writer: %w", err)
_ = h.Close()
return nb, fmt.Errorf("failed to copy file to io.Writer: %w", err)
}
return numBytes, file.Close()
return nb, h.Close()
},
}, nil
}
// fileFromFS returns a File pointer from a given file in the system's file system
func fileFromFS(name string) *File {
_, err := os.Stat(name)
func fileFromFS(n string) *File {
_, err := os.Stat(n)
if err != nil {
return nil
}
return &File{
Name: filepath.Base(name),
Name: filepath.Base(n),
Header: make(map[string][]string),
Writer: func(writer io.Writer) (int64, error) {
file, err := os.Open(name)
Writer: func(w io.Writer) (int64, error) {
h, err := os.Open(n)
if err != nil {
return 0, err
}
numBytes, err := io.Copy(writer, file)
nb, err := io.Copy(w, h)
if err != nil {
_ = file.Close()
return numBytes, fmt.Errorf("failed to copy file to io.Writer: %w", err)
_ = h.Close()
return nb, fmt.Errorf("failed to copy file to io.Writer: %w", err)
}
return numBytes, file.Close()
return nb, h.Close()
},
}
}
// fileFromReader returns a File pointer from a given io.Reader
func fileFromReader(name string, reader io.Reader) (*File, error) {
d, err := io.ReadAll(reader)
func fileFromReader(n string, r io.Reader) (*File, error) {
d, err := io.ReadAll(r)
if err != nil {
return &File{}, err
}
byteReader := bytes.NewReader(d)
br := bytes.NewReader(d)
return &File{
Name: name,
Name: n,
Header: make(map[string][]string),
Writer: func(writer io.Writer) (int64, error) {
readBytes, copyErr := io.Copy(writer, byteReader)
if copyErr != nil {
return readBytes, copyErr
Writer: func(w io.Writer) (int64, error) {
rb, cerr := io.Copy(w, br)
if cerr != nil {
return rb, cerr
}
_, copyErr = byteReader.Seek(0, io.SeekStart)
return readBytes, copyErr
_, cerr = br.Seek(0, io.SeekStart)
return rb, cerr
},
}, nil
}
// fileFromReadSeeker returns a File pointer from a given io.ReadSeeker
func fileFromReadSeeker(name string, reader io.ReadSeeker) *File {
func fileFromReadSeeker(n string, r io.ReadSeeker) *File {
return &File{
Name: name,
Name: n,
Header: make(map[string][]string),
Writer: func(writer io.Writer) (int64, error) {
readBytes, err := io.Copy(writer, reader)
Writer: func(w io.Writer) (int64, error) {
rb, err := io.Copy(w, r)
if err != nil {
return readBytes, err
return rb, err
}
_, err = reader.Seek(0, io.SeekStart)
return readBytes, err
_, err = r.Seek(0, io.SeekStart)
return rb, err
},
}
}
// fileFromHTMLTemplate returns a File pointer form a given html/template.Template
func fileFromHTMLTemplate(name string, tpl *ht.Template, data interface{}) (*File, error) {
if tpl == nil {
func fileFromHTMLTemplate(n string, t *ht.Template, d interface{}) (*File, error) {
if t == nil {
return nil, fmt.Errorf(errTplPointerNil)
}
buffer := bytes.Buffer{}
if err := tpl.Execute(&buffer, data); err != nil {
buf := bytes.Buffer{}
if err := t.Execute(&buf, d); err != nil {
return nil, fmt.Errorf(errTplExecuteFailed, err)
}
return fileFromReader(name, &buffer)
return fileFromReader(n, &buf)
}
// fileFromTextTemplate returns a File pointer form a given text/template.Template
func fileFromTextTemplate(name string, tpl *tt.Template, data interface{}) (*File, error) {
if tpl == nil {
func fileFromTextTemplate(n string, t *tt.Template, d interface{}) (*File, error) {
if t == nil {
return nil, fmt.Errorf(errTplPointerNil)
}
buffer := bytes.Buffer{}
if err := tpl.Execute(&buffer, data); err != nil {
buf := bytes.Buffer{}
if err := t.Execute(&buf, d); err != nil {
return nil, fmt.Errorf(errTplExecuteFailed, err)
}
return fileFromReader(name, &buffer)
return fileFromReader(n, &buf)
}
// getEncoder creates a new mime.WordEncoder based on the encoding setting of the message
func getEncoder(enc Encoding) mime.WordEncoder {
switch enc {
func getEncoder(e Encoding) mime.WordEncoder {
switch e {
case EncodingQP:
return mime.QEncoding
case EncodingB64:
@ -1324,10 +1310,10 @@ func getEncoder(enc Encoding) mime.WordEncoder {
// writeFuncFromBuffer is a common method to convert a byte buffer into a writeFunc as
// often required by this library
func writeFuncFromBuffer(buffer *bytes.Buffer) func(io.Writer) (int64, error) {
writeFunc := func(w io.Writer) (int64, error) {
numBytes, err := w.Write(buffer.Bytes())
return int64(numBytes), err
func writeFuncFromBuffer(buf *bytes.Buffer) func(io.Writer) (int64, error) {
w := func(w io.Writer) (int64, error) {
nb, err := w.Write(buf.Bytes())
return int64(nb), err
}
return writeFunc
return w
}

8
tls.go
View file

@ -8,17 +8,17 @@ package mail
type TLSPolicy int
const (
// TLSMandatory requires that the connection to the server is
// TLSMandatory requires that the connection cto the server is
// encrypting using STARTTLS. If the server does not support STARTTLS
// the connection will be terminated with an error
TLSMandatory TLSPolicy = iota
// TLSOpportunistic tries to establish an encrypted connection via the
// TLSOpportunistic tries cto establish an encrypted connection via the
// STARTTLS protocol. If the server does not support this, it will fall
// back to non-encrypted plaintext transmission
// back cto non-encrypted plaintext transmission
TLSOpportunistic
// NoTLS forces the transaction to be not encrypted
// NoTLS forces the transaction cto be not encrypted
NoTLS
)