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 out io.Writer
} }
var newlineBytes = []byte(SingleNewLine) var nl = []byte(SingleNewLine)
// Write writes the data stream and inserts a SingleNewLine when the maximum // Write writes the data stream and inserts a SingleNewLine when the maximum
// line length is reached // 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 { if l.out == nil {
err = fmt.Errorf(ErrNoOutWriter) err = fmt.Errorf(ErrNoOutWriter)
return return
} }
if l.used+len(data) < MaxBodyLength { if l.used+len(b) < MaxBodyLength {
copy(l.line[l.used:], data) copy(l.line[l.used:], b)
l.used += len(data) l.used += len(b)
return len(data), nil 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 { if err != nil {
return return
} }
excess := MaxBodyLength - l.used excess := MaxBodyLength - l.used
l.used = 0 l.used = 0
numBytes, err = l.out.Write(data[0:excess]) n, err = l.out.Write(b[0:excess])
if err != nil { if err != nil {
return return
} }
numBytes, err = l.out.Write(newlineBytes) n, err = l.out.Write(nl)
if err != nil { if err != nil {
return return
} }
return l.Write(data[excess:]) return l.Write(b[excess:])
} }
// Close closes the Base64LineBreaker and writes any access data that is still // 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 { if err != nil {
return return
} }
_, err = l.out.Write(newlineBytes) _, err = l.out.Write(nl)
} }
return 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 // Client is the SMTP client struct
type Client struct { type Client struct {
// connection is the net.Conn that the smtp.Client is based on // co is the net.Conn that the smtp.Client is based on
connection net.Conn co net.Conn
// Timeout for the SMTP server connection // Timeout for the SMTP server connection
connTimeout time.Duration cto time.Duration
// dsn indicates that we want to use DSN for the Client // dsn indicates that we want to use DSN for the Client
dsn bool dsn bool
@ -102,8 +102,8 @@ type Client struct {
// dsnrntype defines the DSNRcptNotifyOption in case DSN is enabled // dsnrntype defines the DSNRcptNotifyOption in case DSN is enabled
dsnrntype []string dsnrntype []string
// isEncrypted indicates if a Client connection is encrypted or not // enc indicates if a Client connection is encrypted or not
isEncrypted bool enc bool
// noNoop indicates the Noop is to be skipped // noNoop indicates the Noop is to be skipped
noNoop bool noNoop bool
@ -121,17 +121,17 @@ type Client struct {
port int port int
fallbackPort int fallbackPort int
// smtpAuth is a pointer to smtp.Auth // sa is a pointer to smtp.Auth
smtpAuth smtp.Auth sa smtp.Auth
// smtpAuthType represents the authentication type for SMTP AUTH // satype represents the authentication type for SMTP AUTH
smtpAuthType SMTPAuthType satype SMTPAuthType
// smtpClient is the smtp.Client that is set up when using the Dial*() methods // sc is the smtp.Client that is set up when using the Dial*() methods
smtpClient *smtp.Client sc *smtp.Client
// Use SSL for the connection // Use SSL for the connection
useSSL bool ssl bool
// tlspolicy sets the client to use the provided TLSPolicy for the STARTTLS protocol // tlspolicy sets the client to use the provided TLSPolicy for the STARTTLS protocol
tlspolicy TLSPolicy tlspolicy TLSPolicy
@ -142,11 +142,11 @@ type Client struct {
// user is the SMTP AUTH username // user is the SMTP AUTH username
user string user string
// useDebugLog enables the debug logging on the SMTP client // dl enables the debug logging on the SMTP client
useDebugLog bool dl bool
// logger is a logger that implements the log.Logger interface // l is a logger that implements the log.Logger interface
logger log.Logger l log.Logger
// dialContextFunc is a custom DialContext function to dial target SMTP server // dialContextFunc is a custom DialContext function to dial target SMTP server
dialContextFunc DialContextFunc dialContextFunc DialContextFunc
@ -198,12 +198,12 @@ var (
) )
// NewClient returns a new Session client object // 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{ c := &Client{
connTimeout: DefaultTimeout, cto: DefaultTimeout,
host: host, host: h,
port: DefaultPort, port: DefaultPort,
tlsconfig: &tls.Config{ServerName: host, MinVersion: DefaultTLSMinVersion}, tlsconfig: &tls.Config{ServerName: h, MinVersion: DefaultTLSMinVersion},
tlspolicy: DefaultTLSPolicy, tlspolicy: DefaultTLSPolicy,
} }
@ -213,11 +213,11 @@ func NewClient(host string, opts ...Option) (*Client, error) {
} }
// Override defaults with optionally provided Option functions // Override defaults with optionally provided Option functions
for _, opt := range opts { for _, co := range o {
if opt == nil { if co == nil {
continue continue
} }
if err := opt(c); err != nil { if err := co(c); err != nil {
return c, fmt.Errorf("failed to apply option: %w", err) 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 // WithPort overrides the default connection port
func WithPort(port int) Option { func WithPort(p int) Option {
return func(c *Client) error { return func(c *Client) error {
if port < 1 || port > 65535 { if p < 1 || p > 65535 {
return ErrInvalidPort return ErrInvalidPort
} }
c.port = port c.port = p
return nil return nil
} }
} }
// WithTimeout overrides the default connection timeout // WithTimeout overrides the default connection timeout
func WithTimeout(timeout time.Duration) Option { func WithTimeout(t time.Duration) Option {
return func(c *Client) error { return func(c *Client) error {
if timeout <= 0 { if t <= 0 {
return ErrInvalidTimeout return ErrInvalidTimeout
} }
c.connTimeout = timeout c.cto = t
return nil return nil
} }
} }
@ -257,7 +257,7 @@ func WithTimeout(timeout time.Duration) Option {
// Deprecated: use WithSSLPort instead. // Deprecated: use WithSSLPort instead.
func WithSSL() Option { func WithSSL() Option {
return func(c *Client) error { return func(c *Client) error {
c.useSSL = true c.ssl = true
return nil return nil
} }
} }
@ -267,9 +267,9 @@ func WithSSL() Option {
// //
// When the SSL connection fails and fallback is set to true, // When the SSL connection fails and fallback is set to true,
// the client will attempt to connect on port 25 using plaintext. // 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 { return func(c *Client) error {
c.SetSSLPort(true, fallback) c.SetSSLPort(true, fb)
return nil return nil
} }
} }
@ -278,26 +278,26 @@ func WithSSLPort(fallback bool) Option {
// to StdErr // to StdErr
func WithDebugLog() Option { func WithDebugLog() Option {
return func(c *Client) error { return func(c *Client) error {
c.useDebugLog = true c.dl = true
return nil return nil
} }
} }
// WithLogger overrides the default log.Logger that is used for debug logging // 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 { return func(c *Client) error {
c.logger = logger c.l = l
return nil return nil
} }
} }
// WithHELO tells the client to use the provided string as HELO/EHLO greeting host // 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 { return func(c *Client) error {
if helo == "" { if h == "" {
return ErrInvalidHELO return ErrInvalidHELO
} }
c.helo = helo c.helo = h
return nil return nil
} }
} }
@ -305,9 +305,9 @@ func WithHELO(helo string) Option {
// WithTLSPolicy tells the client to use the provided TLSPolicy // WithTLSPolicy tells the client to use the provided TLSPolicy
// //
// Deprecated: use WithTLSPortPolicy instead. // Deprecated: use WithTLSPortPolicy instead.
func WithTLSPolicy(policy TLSPolicy) Option { func WithTLSPolicy(p TLSPolicy) Option {
return func(c *Client) error { return func(c *Client) error {
c.tlspolicy = policy c.tlspolicy = p
return nil return nil
} }
} }
@ -319,52 +319,52 @@ func WithTLSPolicy(policy TLSPolicy) Option {
// If the connection fails with TLSOpportunistic, // If the connection fails with TLSOpportunistic,
// a plaintext connection is attempted on port 25 as a fallback. // a plaintext connection is attempted on port 25 as a fallback.
// NoTLS will allways use port 25. // NoTLS will allways use port 25.
func WithTLSPortPolicy(policy TLSPolicy) Option { func WithTLSPortPolicy(p TLSPolicy) Option {
return func(c *Client) error { return func(c *Client) error {
c.SetTLSPortPolicy(policy) c.SetTLSPortPolicy(p)
return nil return nil
} }
} }
// WithTLSConfig tells the client to use the provided *tls.Config // 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 { return func(c *Client) error {
if tlsconfig == nil { if co == nil {
return ErrInvalidTLSConfig return ErrInvalidTLSConfig
} }
c.tlsconfig = tlsconfig c.tlsconfig = co
return nil return nil
} }
} }
// WithSMTPAuth tells the client to use the provided SMTPAuthType for authentication // 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 { return func(c *Client) error {
c.smtpAuthType = authtype c.satype = t
return nil return nil
} }
} }
// WithSMTPAuthCustom tells the client to use the provided smtp.Auth for SMTP authentication // 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 { return func(c *Client) error {
c.smtpAuth = smtpAuth c.sa = a
return nil return nil
} }
} }
// WithUsername tells the client to use the provided string as username for authentication // 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 { return func(c *Client) error {
c.user = username c.user = u
return nil return nil
} }
} }
// WithPassword tells the client to use the provided string as password/secret for authentication // 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 { return func(c *Client) error {
c.pass = password c.pass = p
return nil 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 // as described in the RFC 1891 and set the MAIL FROM Return option type to the
// given DSNMailReturnOption // given DSNMailReturnOption
// See: https://www.rfc-editor.org/rfc/rfc1891 // See: https://www.rfc-editor.org/rfc/rfc1891
func WithDSNMailReturnType(option DSNMailReturnOption) Option { func WithDSNMailReturnType(mro DSNMailReturnOption) Option {
return func(c *Client) error { return func(c *Client) error {
switch option { switch mro {
case DSNMailReturnHeadersOnly: case DSNMailReturnHeadersOnly:
case DSNMailReturnFull: case DSNMailReturnFull:
default: default:
@ -396,7 +396,7 @@ func WithDSNMailReturnType(option DSNMailReturnOption) Option {
} }
c.dsn = true c.dsn = true
c.dsnmrtype = option c.dsnmrtype = mro
return nil return nil
} }
} }
@ -404,13 +404,13 @@ func WithDSNMailReturnType(option DSNMailReturnOption) Option {
// WithDSNRcptNotifyType enables the Client to request DSNs as described in the RFC 1891 // 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 // and sets the RCPT TO notify options to the given list of DSNRcptNotifyOption
// See: https://www.rfc-editor.org/rfc/rfc1891 // See: https://www.rfc-editor.org/rfc/rfc1891
func WithDSNRcptNotifyType(opts ...DSNRcptNotifyOption) Option { func WithDSNRcptNotifyType(rno ...DSNRcptNotifyOption) Option {
return func(c *Client) error { return func(c *Client) error {
var rcptOpts []string var rnol []string
var ns, nns bool var ns, nns bool
if len(opts) > 0 { if len(rno) > 0 {
for _, opt := range opts { for _, crno := range rno {
switch opt { switch crno {
case DSNRcptNotifyNever: case DSNRcptNotifyNever:
ns = true ns = true
case DSNRcptNotifySuccess: case DSNRcptNotifySuccess:
@ -422,7 +422,7 @@ func WithDSNRcptNotifyType(opts ...DSNRcptNotifyOption) Option {
default: default:
return ErrInvalidDSNRcptNotifyOption return ErrInvalidDSNRcptNotifyOption
} }
rcptOpts = append(rcptOpts, string(opt)) rnol = append(rnol, string(crno))
} }
} }
if ns && nns { if ns && nns {
@ -430,7 +430,7 @@ func WithDSNRcptNotifyType(opts ...DSNRcptNotifyOption) Option {
} }
c.dsn = true c.dsn = true
c.dsnrntype = rcptOpts c.dsnrntype = rnol
return nil return nil
} }
} }
@ -445,9 +445,9 @@ func WithoutNoop() Option {
} }
// WithDialContextFunc overrides the default DialContext for connecting SMTP server // WithDialContextFunc overrides the default DialContext for connecting SMTP server
func WithDialContextFunc(dialCtxFunc DialContextFunc) Option { func WithDialContextFunc(f DialContextFunc) Option {
return func(c *Client) error { return func(c *Client) error {
c.dialContextFunc = dialCtxFunc c.dialContextFunc = f
return nil return nil
} }
} }
@ -463,8 +463,8 @@ func (c *Client) ServerAddr() string {
} }
// SetTLSPolicy overrides the current TLSPolicy with the given TLSPolicy value // SetTLSPolicy overrides the current TLSPolicy with the given TLSPolicy value
func (c *Client) SetTLSPolicy(policy TLSPolicy) { func (c *Client) SetTLSPolicy(p TLSPolicy) {
c.tlspolicy = policy c.tlspolicy = p
} }
// SetTLSPortPolicy overrides the current TLSPolicy with the given TLSPolicy // 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 // If the connection fails with TLSOpportunistic, a plaintext connection is
// attempted on port 25 as a fallback. // attempted on port 25 as a fallback.
// NoTLS will allways use port 25. // NoTLS will allways use port 25.
func (c *Client) SetTLSPortPolicy(policy TLSPolicy) { func (c *Client) SetTLSPortPolicy(p TLSPolicy) {
c.port = DefaultPortTLS c.port = DefaultPortTLS
if policy == TLSOpportunistic { if p == TLSOpportunistic {
c.fallbackPort = DefaultPort c.fallbackPort = DefaultPort
} }
if policy == NoTLS { if p == NoTLS {
c.port = DefaultPort c.port = DefaultPort
} }
c.tlspolicy = policy c.tlspolicy = p
} }
// SetSSL tells the Client wether to use SSL or not // SetSSL tells the Client wether to use SSL or not
func (c *Client) SetSSL(ssl bool) { func (c *Client) SetSSL(s bool) {
c.useSSL = ssl c.ssl = s
} }
// SetSSLPort tells the Client wether or not to use SSL and fallback. // 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). // Port 25 is used when SSL is unset (false).
// When the SSL connection fails and fb is set to true, // When the SSL connection fails and fb is set to true,
// the client will attempt to connect on port 25 using plaintext. // 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 c.port = DefaultPort
if ssl { if ssl {
c.port = DefaultPortSSL c.port = DefaultPortSSL
} }
c.fallbackPort = 0 c.fallbackPort = 0
if fallback { if fb {
c.fallbackPort = DefaultPort c.fallbackPort = DefaultPort
} }
c.useSSL = ssl c.ssl = ssl
} }
// SetDebugLog tells the Client whether debug logging is enabled or not // SetDebugLog tells the Client whether debug logging is enabled or not
func (c *Client) SetDebugLog(val bool) { func (c *Client) SetDebugLog(v bool) {
c.useDebugLog = val c.dl = v
if c.smtpClient != nil { if c.sc != nil {
c.smtpClient.SetDebugLog(val) c.sc.SetDebugLog(v)
} }
} }
// SetLogger tells the Client which log.Logger to use // SetLogger tells the Client which log.Logger to use
func (c *Client) SetLogger(logger log.Logger) { func (c *Client) SetLogger(l log.Logger) {
c.logger = logger c.l = l
if c.smtpClient != nil { if c.sc != nil {
c.smtpClient.SetLogger(logger) c.sc.SetLogger(l)
} }
} }
// SetTLSConfig overrides the current *tls.Config with the given *tls.Config value // SetTLSConfig overrides the current *tls.Config with the given *tls.Config value
func (c *Client) SetTLSConfig(tlsconfig *tls.Config) error { func (c *Client) SetTLSConfig(co *tls.Config) error {
if tlsconfig == nil { if co == nil {
return ErrInvalidTLSConfig return ErrInvalidTLSConfig
} }
c.tlsconfig = tlsconfig c.tlsconfig = co
return nil return nil
} }
// SetUsername overrides the current username string with the given value // SetUsername overrides the current username string with the given value
func (c *Client) SetUsername(username string) { func (c *Client) SetUsername(u string) {
c.user = username c.user = u
} }
// SetPassword overrides the current password string with the given value // SetPassword overrides the current password string with the given value
func (c *Client) SetPassword(password string) { func (c *Client) SetPassword(p string) {
c.pass = password c.pass = p
} }
// SetSMTPAuth overrides the current SMTP AUTH type setting with the given value // SetSMTPAuth overrides the current SMTP AUTH type setting with the given value
func (c *Client) SetSMTPAuth(authtype SMTPAuthType) { func (c *Client) SetSMTPAuth(a SMTPAuthType) {
c.smtpAuthType = authtype c.satype = a
} }
// SetSMTPAuthCustom overrides the current SMTP AUTH setting with the given custom smtp.Auth // SetSMTPAuthCustom overrides the current SMTP AUTH setting with the given custom smtp.Auth
func (c *Client) SetSMTPAuthCustom(smtpAuth smtp.Auth) { func (c *Client) SetSMTPAuthCustom(sa smtp.Auth) {
c.smtpAuth = smtpAuth c.sa = sa
} }
// setDefaultHelo retrieves the current hostname and sets it as HELO/EHLO hostname // setDefaultHelo retrieves the current hostname and sets it as HELO/EHLO hostname
func (c *Client) setDefaultHelo() error { func (c *Client) setDefaultHelo() error {
hostname, err := os.Hostname() hn, err := os.Hostname()
if err != nil { 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 return nil
} }
// DialWithContext establishes a connection to the SMTP server with a given context.Context // DialWithContext establishes a connection cto the SMTP server with a given context.Context
func (c *Client) DialWithContext(dialCtx context.Context) error { func (c *Client) DialWithContext(pc context.Context) error {
ctx, cancel := context.WithDeadline(dialCtx, time.Now().Add(c.connTimeout)) ctx, cfn := context.WithDeadline(pc, time.Now().Add(c.cto))
defer cancel() defer cfn()
if c.dialContextFunc == nil { if c.dialContextFunc == nil {
netDialer := net.Dialer{} nd := net.Dialer{}
c.dialContextFunc = netDialer.DialContext c.dialContextFunc = nd.DialContext
if c.useSSL { if c.ssl {
tlsDialer := tls.Dialer{NetDialer: &netDialer, Config: c.tlsconfig} td := tls.Dialer{NetDialer: &nd, Config: c.tlsconfig}
c.isEncrypted = true c.enc = true
c.dialContextFunc = tlsDialer.DialContext c.dialContextFunc = td.DialContext
} }
} }
var err error 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 { if err != nil && c.fallbackPort != 0 {
// TODO: should we somehow log or append the previous error? // 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 { if err != nil {
return err return err
} }
client, err := smtp.NewClient(c.connection, c.host) sc, err := smtp.NewClient(c.co, c.host)
if err != nil { if err != nil {
return err return err
} }
if client == nil { if sc == nil {
return fmt.Errorf("SMTP client is nil") return fmt.Errorf("SMTP client is nil")
} }
c.smtpClient = client c.sc = sc
if c.logger != nil { if c.l != nil {
c.smtpClient.SetLogger(c.logger) c.sc.SetLogger(c.l)
} }
if c.useDebugLog { if c.dl {
c.smtpClient.SetDebugLog(true) c.sc.SetDebugLog(true)
} }
if err = c.smtpClient.Hello(c.helo); err != nil { if err := c.sc.Hello(c.helo); err != nil {
return err return err
} }
if err = c.tls(); err != nil { if err := c.tls(); err != nil {
return err return err
} }
if err = c.auth(); err != nil { if err := c.auth(); err != nil {
return err return err
} }
@ -628,7 +628,7 @@ func (c *Client) Close() error {
if err := c.checkConn(); err != nil { if err := c.checkConn(); err != nil {
return err 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) 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 { if err := c.checkConn(); err != nil {
return err 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) 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 // checkConn makes sure that a required server connection is available and extends the
// connection deadline // connection deadline
func (c *Client) checkConn() error { func (c *Client) checkConn() error {
if c.connection == nil { if c.co == nil {
return ErrNoActiveConnection return ErrNoActiveConnection
} }
if !c.noNoop { if !c.noNoop {
if err := c.smtpClient.Noop(); err != nil { if err := c.sc.Noop(); err != nil {
return ErrNoActiveConnection 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 ErrDeadlineExtendFailed
} }
return nil return nil
@ -696,12 +696,12 @@ func (c *Client) serverFallbackAddr() string {
// tls tries to make sure that the STARTTLS requirements are satisfied // tls tries to make sure that the STARTTLS requirements are satisfied
func (c *Client) tls() error { func (c *Client) tls() error {
if c.connection == nil { if c.co == nil {
return ErrNoActiveConnection return ErrNoActiveConnection
} }
if !c.useSSL && c.tlspolicy != NoTLS { if !c.ssl && c.tlspolicy != NoTLS {
est := false est := false
st, _ := c.smtpClient.Extension("STARTTLS") st, _ := c.sc.Extension("STARTTLS")
if c.tlspolicy == TLSMandatory { if c.tlspolicy == TLSMandatory {
est = true est = true
if !st { if !st {
@ -715,11 +715,11 @@ func (c *Client) tls() error {
} }
} }
if est { if est {
if err := c.smtpClient.StartTLS(c.tlsconfig); err != nil { if err := c.sc.StartTLS(c.tlsconfig); err != nil {
return err return err
} }
} }
_, c.isEncrypted = c.smtpClient.TLSConnectionState() _, c.enc = c.sc.TLSConnectionState()
} }
return nil return nil
} }
@ -729,40 +729,40 @@ func (c *Client) auth() error {
if err := c.checkConn(); err != nil { if err := c.checkConn(); err != nil {
return fmt.Errorf("failed to authenticate: %w", err) return fmt.Errorf("failed to authenticate: %w", err)
} }
if c.smtpAuth == nil && c.smtpAuthType != "" { if c.sa == nil && c.satype != "" {
sa, sat := c.smtpClient.Extension("AUTH") sa, sat := c.sc.Extension("AUTH")
if !sa { if !sa {
return fmt.Errorf("server does not support SMTP AUTH") return fmt.Errorf("server does not support SMTP AUTH")
} }
switch c.smtpAuthType { switch c.satype {
case SMTPAuthPlain: case SMTPAuthPlain:
if !strings.Contains(sat, string(SMTPAuthPlain)) { if !strings.Contains(sat, string(SMTPAuthPlain)) {
return ErrPlainAuthNotSupported return ErrPlainAuthNotSupported
} }
c.smtpAuth = smtp.PlainAuth("", c.user, c.pass, c.host) c.sa = smtp.PlainAuth("", c.user, c.pass, c.host)
case SMTPAuthLogin: case SMTPAuthLogin:
if !strings.Contains(sat, string(SMTPAuthLogin)) { if !strings.Contains(sat, string(SMTPAuthLogin)) {
return ErrLoginAuthNotSupported return ErrLoginAuthNotSupported
} }
c.smtpAuth = smtp.LoginAuth(c.user, c.pass, c.host) c.sa = smtp.LoginAuth(c.user, c.pass, c.host)
case SMTPAuthCramMD5: case SMTPAuthCramMD5:
if !strings.Contains(sat, string(SMTPAuthCramMD5)) { if !strings.Contains(sat, string(SMTPAuthCramMD5)) {
return ErrCramMD5AuthNotSupported return ErrCramMD5AuthNotSupported
} }
c.smtpAuth = smtp.CRAMMD5Auth(c.user, c.pass) c.sa = smtp.CRAMMD5Auth(c.user, c.pass)
case SMTPAuthXOAUTH2: case SMTPAuthXOAUTH2:
if !strings.Contains(sat, string(SMTPAuthXOAUTH2)) { if !strings.Contains(sat, string(SMTPAuthXOAUTH2)) {
return ErrXOauth2AuthNotSupported return ErrXOauth2AuthNotSupported
} }
c.smtpAuth = smtp.XOAuth2Auth(c.user, c.pass) c.sa = smtp.XOAuth2Auth(c.user, c.pass)
default: 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 c.sa != nil {
if err := c.smtpClient.Auth(c.smtpAuth); err != nil { if err := c.sc.Auth(c.sa); err != nil {
return fmt.Errorf("SMTP AUTH failed: %w", err) 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 { for _, m := range ml {
m.sendError = nil m.sendError = nil
if m.encoding == NoEncoding { if m.encoding == NoEncoding {
if ok, _ := c.smtpClient.Extension("8BITMIME"); !ok { if ok, _ := c.sc.Extension("8BITMIME"); !ok {
se := &SendError{Reason: ErrNoUnencoded, isTemp: false} se := &SendError{Reason: ErrNoUnencoded, isTemp: false}
m.sendError = se m.sendError = se
errs = append(errs, se) errs = append(errs, se)
@ -42,12 +42,12 @@ func (c *Client) Send(ml ...*Msg) error {
if c.dsn { if c.dsn {
if c.dsnmrtype != "" { 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)} 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) se.errlist = append(se.errlist, reserr)
} }
m.sendError = se m.sendError = se
@ -59,9 +59,9 @@ func (c *Client) Send(ml ...*Msg) error {
rse.errlist = make([]error, 0) rse.errlist = make([]error, 0)
rse.rcpt = make([]string, 0) rse.rcpt = make([]string, 0)
rno := strings.Join(c.dsnrntype, ",") rno := strings.Join(c.dsnrntype, ",")
c.smtpClient.SetDSNRcptNotifyOption(rno) c.sc.SetDSNRcptNotifyOption(rno)
for _, r := range rl { for _, r := range rl {
if err := c.smtpClient.Rcpt(r); err != nil { if err := c.sc.Rcpt(r); err != nil {
rse.Reason = ErrSMTPRcptTo rse.Reason = ErrSMTPRcptTo
rse.errlist = append(rse.errlist, err) rse.errlist = append(rse.errlist, err)
rse.rcpt = append(rse.rcpt, r) rse.rcpt = append(rse.rcpt, r)
@ -70,14 +70,14 @@ func (c *Client) Send(ml ...*Msg) error {
} }
} }
if failed { if failed {
if reserr := c.smtpClient.Reset(); reserr != nil { if reserr := c.sc.Reset(); reserr != nil {
rse.errlist = append(rse.errlist, err) rse.errlist = append(rse.errlist, err)
} }
m.sendError = rse m.sendError = rse
errs = append(errs, rse) errs = append(errs, rse)
continue continue
} }
w, err := c.smtpClient.Data() w, err := c.sc.Data()
if err != nil { if err != nil {
se := &SendError{Reason: ErrSMTPData, errlist: []error{err}, isTemp: isTempError(err)} se := &SendError{Reason: ErrSMTPData, errlist: []error{err}, isTemp: isTempError(err)}
m.sendError = se m.sendError = se

View file

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

View file

@ -49,9 +49,9 @@ func TestNewClient(t *testing.T) {
if c.host != tt.host { if c.host != tt.host {
t.Errorf("failed to create new client. Host expected: %s, got: %s", host, c.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(), t.Errorf("failed to create new client. Timeout expected: %s, got: %s", DefaultTimeout.String(),
c.connTimeout.String()) c.cto.String())
} }
if c.port != DefaultPort { if c.port != DefaultPort {
t.Errorf("failed to create new client. Port expected: %d, got: %d", DefaultPort, c.port) 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) t.Errorf("failed to create new client: %s", err)
return return
} }
if c.connTimeout != tt.want { if c.cto != tt.want {
t.Errorf("failed to set custom timeout. Want: %d, got: %d", tt.want, c.connTimeout) 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 return
} }
c.SetSSL(tt.value) c.SetSSL(tt.value)
if c.useSSL != tt.value { if c.ssl != tt.value {
t.Errorf("failed to set SSL setting. Got: %t, want: %t", c.useSSL, 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 return
} }
c.SetSSLPort(tt.value, tt.fb) c.SetSSLPort(tt.value, tt.fb)
if c.useSSL != tt.value { if c.ssl != tt.value {
t.Errorf("failed to set SSL setting. Got: %t, want: %t", c.useSSL, tt.value) t.Errorf("failed to set SSL setting. Got: %t, want: %t", c.ssl, tt.value)
} }
if c.port != tt.port { if c.port != tt.port {
t.Errorf("failed to set SSLPort, wanted port: %d, got: %d", 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 return
} }
c.SetSMTPAuth(tt.value) c.SetSMTPAuth(tt.value)
if string(c.smtpAuthType) != tt.want { if string(c.satype) != tt.want {
t.Errorf("failed to set SMTP auth type. Expected %s, got: %s", tt.want, string(c.smtpAuthType)) 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 return
} }
c.SetSMTPAuthCustom(tt.value) 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") 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 { if err != nil {
t.Errorf("SMTP Auth Start() method returned error: %s", err) 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) t.Errorf("failed to dial with context: %s", err)
return return
} }
if c.connection == nil { if c.co == nil {
t.Errorf("DialWithContext didn't fail but no connection found.") 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.") t.Errorf("DialWithContext didn't fail but no SMTP client found.")
} }
if err := c.Close(); err != nil { 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) t.Errorf("failed to dial with context: %s", err)
return return
} }
if c.connection == nil { if c.co == nil {
t.Errorf("DialWithContext didn't fail but no connection found.") 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.") t.Errorf("DialWithContext didn't fail but no SMTP client found.")
} }
if err := c.Close(); err != nil { 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) t.Errorf("failed to dial with context: %s", err)
return return
} }
if c.connection == nil { if c.co == nil {
t.Errorf("DialWithContext didn't fail but no connection found.") 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.") t.Errorf("DialWithContext didn't fail but no SMTP client found.")
} }
c.SetDebugLog(true) c.SetDebugLog(true)
@ -694,10 +694,10 @@ func TestClient_DialWithContext_Debug_custom(t *testing.T) {
t.Errorf("failed to dial with context: %s", err) t.Errorf("failed to dial with context: %s", err)
return return
} }
if c.connection == nil { if c.co == nil {
t.Errorf("DialWithContext didn't fail but no connection found.") 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.") t.Errorf("DialWithContext didn't fail but no SMTP client found.")
} }
c.SetDebugLog(true) c.SetDebugLog(true)
@ -714,7 +714,7 @@ func TestClient_DialWithContextInvalidHost(t *testing.T) {
if err != nil { if err != nil {
t.Skipf("failed to create test client: %s. Skipping tests", err) t.Skipf("failed to create test client: %s. Skipping tests", err)
} }
c.connection = nil c.co = nil
c.host = "invalid.addr" c.host = "invalid.addr"
ctx := context.Background() ctx := context.Background()
if err := c.DialWithContext(ctx); err == nil { if err := c.DialWithContext(ctx); err == nil {
@ -730,7 +730,7 @@ func TestClient_DialWithContextInvalidHELO(t *testing.T) {
if err != nil { if err != nil {
t.Skipf("failed to create test client: %s. Skipping tests", err) t.Skipf("failed to create test client: %s. Skipping tests", err)
} }
c.connection = nil c.co = nil
c.helo = "" c.helo = ""
ctx := context.Background() ctx := context.Background()
if err := c.DialWithContext(ctx); err == nil { if err := c.DialWithContext(ctx); err == nil {
@ -762,7 +762,7 @@ func TestClient_checkConn(t *testing.T) {
if err != nil { if err != nil {
t.Skipf("failed to create test client: %s. Skipping tests", err) t.Skipf("failed to create test client: %s. Skipping tests", err)
} }
c.connection = nil c.co = nil
if err := c.checkConn(); err == nil { if err := c.checkConn(); err == nil {
t.Errorf("connCheck() should fail but succeeded") t.Errorf("connCheck() should fail but succeeded")
} }
@ -799,10 +799,10 @@ func TestClient_DialWithContextOptions(t *testing.T) {
return return
} }
if !tt.sf { 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.") 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.") t.Errorf("DialWithContext didn't fail but no SMTP client found.")
} }
if err := c.Reset(); err != nil { if err := c.Reset(); err != nil {
@ -1002,16 +1002,16 @@ func TestClient_DialSendCloseBroken(t *testing.T) {
return return
} }
if tt.closestart { if tt.closestart {
_ = c.smtpClient.Close() _ = c.sc.Close()
_ = c.connection.Close() _ = c.co.Close()
} }
if err := c.Send(m); err != nil && !tt.sf { if err := c.Send(m); err != nil && !tt.sf {
t.Errorf("Send() failed: %s", err) t.Errorf("Send() failed: %s", err)
return return
} }
if tt.closeearly { if tt.closeearly {
_ = c.smtpClient.Close() _ = c.sc.Close()
_ = c.connection.Close() _ = c.co.Close()
} }
if err := c.Close(); err != nil && !tt.sf { if err := c.Close(); err != nil && !tt.sf {
t.Errorf("Close() failed: %s", err) t.Errorf("Close() failed: %s", err)
@ -1062,16 +1062,16 @@ func TestClient_DialSendCloseBrokenWithDSN(t *testing.T) {
return return
} }
if tt.closestart { if tt.closestart {
_ = c.smtpClient.Close() _ = c.sc.Close()
_ = c.connection.Close() _ = c.co.Close()
} }
if err := c.Send(m); err != nil && !tt.sf { if err := c.Send(m); err != nil && !tt.sf {
t.Errorf("Send() failed: %s", err) t.Errorf("Send() failed: %s", err)
return return
} }
if tt.closeearly { if tt.closeearly {
_ = c.smtpClient.Close() _ = c.sc.Close()
_ = c.connection.Close() _ = c.co.Close()
} }
if err := c.Close(); err != nil && !tt.sf { if err := c.Close(); err != nil && !tt.sf {
t.Errorf("Close() failed: %s", err) 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. // SetBodyWriter sets the body of the message.
func (m *Msg) SetBodyWriter( func (m *Msg) SetBodyWriter(contentType ContentType, writeFunc func(io.Writer) (int64, error), opts ...PartOption) {
contentType ContentType, writeFunc func(io.Writer) (int64, error),
opts ...PartOption,
) {
p := m.newPart(contentType, opts...) p := m.newPart(contentType, opts...)
p.w = writeFunc p.w = writeFunc
m.parts = []*Part{p} 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 // SetBodyHTMLTemplate sets the body of the message from a given html/template.Template pointer
// The content type will be set to text/html automatically // The content type will be set to text/html automatically
func (m *Msg) SetBodyHTMLTemplate(tpl *ht.Template, data interface{}, opts ...PartOption) error { func (m *Msg) SetBodyHTMLTemplate(t *ht.Template, d interface{}, o ...PartOption) error {
if tpl == nil { if t == nil {
return fmt.Errorf(errTplPointerNil) return fmt.Errorf(errTplPointerNil)
} }
buffer := bytes.Buffer{} buf := bytes.Buffer{}
if err := tpl.Execute(&buffer, data); err != nil { if err := t.Execute(&buf, d); err != nil {
return fmt.Errorf(errTplExecuteFailed, err) return fmt.Errorf(errTplExecuteFailed, err)
} }
writeFunc := writeFuncFromBuffer(&buffer) w := writeFuncFromBuffer(&buf)
m.SetBodyWriter(TypeTextHTML, writeFunc, opts...) m.SetBodyWriter(TypeTextHTML, w, o...)
return nil return nil
} }
// SetBodyTextTemplate sets the body of the message from a given text/template.Template pointer // SetBodyTextTemplate sets the body of the message from a given text/template.Template pointer
// The content type will be set to text/plain automatically // The content type will be set to text/plain automatically
func (m *Msg) SetBodyTextTemplate(tpl *tt.Template, data interface{}, opts ...PartOption) error { func (m *Msg) SetBodyTextTemplate(t *tt.Template, d interface{}, o ...PartOption) error {
if tpl == nil { if t == nil {
return fmt.Errorf(errTplPointerNil) return fmt.Errorf(errTplPointerNil)
} }
buf := bytes.Buffer{} buf := bytes.Buffer{}
if err := tpl.Execute(&buf, data); err != nil { if err := t.Execute(&buf, d); err != nil {
return fmt.Errorf(errTplExecuteFailed, err) return fmt.Errorf(errTplExecuteFailed, err)
} }
writeFunc := writeFuncFromBuffer(&buf) w := writeFuncFromBuffer(&buf)
m.SetBodyWriter(TypeTextPlain, writeFunc, opts...) m.SetBodyWriter(TypeTextPlain, w, o...)
return nil return nil
} }
// AddAlternativeString sets the alternative body of the message. // AddAlternativeString sets the alternative body of the message.
func (m *Msg) AddAlternativeString(contentType ContentType, content string, opts ...PartOption) { func (m *Msg) AddAlternativeString(ct ContentType, b string, o ...PartOption) {
buffer := bytes.NewBufferString(content) buf := bytes.NewBufferString(b)
writeFunc := writeFuncFromBuffer(buffer) w := writeFuncFromBuffer(buf)
m.AddAlternativeWriter(contentType, writeFunc, opts...) m.AddAlternativeWriter(ct, w, o...)
} }
// AddAlternativeWriter sets the body of the message. // AddAlternativeWriter sets the body of the message.
func (m *Msg) AddAlternativeWriter( func (m *Msg) AddAlternativeWriter(ct ContentType, w func(io.Writer) (int64, error), o ...PartOption) {
contentType ContentType, writeFunc func(io.Writer) (int64, error), p := m.newPart(ct, o...)
opts ...PartOption, p.w = w
) { m.parts = append(m.parts, p)
part := m.newPart(contentType, opts...)
part.w = writeFunc
m.parts = append(m.parts, part)
} }
// AddAlternativeHTMLTemplate sets the alternative body of the message to a html/template.Template output // AddAlternativeHTMLTemplate sets the alternative body of the message to a html/template.Template output
// The content type will be set to text/html automatically // The content type will be set to text/html automatically
func (m *Msg) AddAlternativeHTMLTemplate(tpl *ht.Template, data interface{}, opts ...PartOption) error { func (m *Msg) AddAlternativeHTMLTemplate(t *ht.Template, d interface{}, o ...PartOption) error {
if tpl == nil { if t == nil {
return fmt.Errorf(errTplPointerNil) return fmt.Errorf(errTplPointerNil)
} }
buffer := bytes.Buffer{} buf := bytes.Buffer{}
if err := tpl.Execute(&buffer, data); err != nil { if err := t.Execute(&buf, d); err != nil {
return fmt.Errorf(errTplExecuteFailed, err) return fmt.Errorf(errTplExecuteFailed, err)
} }
writeFunc := writeFuncFromBuffer(&buffer) w := writeFuncFromBuffer(&buf)
m.AddAlternativeWriter(TypeTextHTML, writeFunc, opts...) m.AddAlternativeWriter(TypeTextHTML, w, o...)
return nil return nil
} }
// AddAlternativeTextTemplate sets the alternative body of the message to a text/template.Template output // AddAlternativeTextTemplate sets the alternative body of the message to a text/template.Template output
// The content type will be set to text/plain automatically // The content type will be set to text/plain automatically
func (m *Msg) AddAlternativeTextTemplate(tpl *tt.Template, data interface{}, opts ...PartOption) error { func (m *Msg) AddAlternativeTextTemplate(t *tt.Template, d interface{}, o ...PartOption) error {
if tpl == nil { if t == nil {
return fmt.Errorf(errTplPointerNil) return fmt.Errorf(errTplPointerNil)
} }
buffer := bytes.Buffer{} buf := bytes.Buffer{}
if err := tpl.Execute(&buffer, data); err != nil { if err := t.Execute(&buf, d); err != nil {
return fmt.Errorf(errTplExecuteFailed, err) return fmt.Errorf(errTplExecuteFailed, err)
} }
writeFunc := writeFuncFromBuffer(&buffer) w := writeFuncFromBuffer(&buf)
m.AddAlternativeWriter(TypeTextPlain, writeFunc, opts...) m.AddAlternativeWriter(TypeTextPlain, w, o...)
return nil return nil
} }
// AttachFile adds an attachment File to the Msg // AttachFile adds an attachment File to the Msg
func (m *Msg) AttachFile(name string, opts ...FileOption) { func (m *Msg) AttachFile(n string, o ...FileOption) {
file := fileFromFS(name) f := fileFromFS(n)
if file == nil { if f == nil {
return 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 // 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 // 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 // data on the io.Reader should be avoided. For such, it is recommended to
// either use AttachFile or AttachReadSeeker instead // either use AttachFile or AttachReadSeeker instead
func (m *Msg) AttachReader(name string, reader io.Reader, opts ...FileOption) error { func (m *Msg) AttachReader(n string, r io.Reader, o ...FileOption) error {
file, err := fileFromReader(name, reader) f, err := fileFromReader(n, r)
if err != nil { if err != nil {
return err return err
} }
m.attachments = m.appendFile(m.attachments, file, opts...) m.attachments = m.appendFile(m.attachments, f, o...)
return nil return nil
} }
// AttachReadSeeker adds an attachment File via io.ReadSeeker to the Msg // AttachReadSeeker adds an attachment File via io.ReadSeeker to the Msg
func (m *Msg) AttachReadSeeker(name string, reader io.ReadSeeker, opts ...FileOption) { func (m *Msg) AttachReadSeeker(n string, r io.ReadSeeker, o ...FileOption) {
file := fileFromReadSeeker(name, reader) f := fileFromReadSeeker(n, r)
m.attachments = m.appendFile(m.attachments, file, opts...) m.attachments = m.appendFile(m.attachments, f, o...)
} }
// AttachHTMLTemplate adds the output of a html/template.Template pointer as File attachment to the Msg // AttachHTMLTemplate adds the output of a html/template.Template pointer as File attachment to the Msg
func (m *Msg) AttachHTMLTemplate( func (m *Msg) AttachHTMLTemplate(n string, t *ht.Template, d interface{}, o ...FileOption) error {
name string, tpl *ht.Template, data interface{}, opts ...FileOption, f, err := fileFromHTMLTemplate(n, t, d)
) error {
file, err := fileFromHTMLTemplate(name, tpl, data)
if err != nil { if err != nil {
return fmt.Errorf("failed to attach template: %w", err) 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 return nil
} }
// AttachTextTemplate adds the output of a text/template.Template pointer as File attachment to the Msg // AttachTextTemplate adds the output of a text/template.Template pointer as File attachment to the Msg
func (m *Msg) AttachTextTemplate( func (m *Msg) AttachTextTemplate(n string, t *tt.Template, d interface{}, o ...FileOption) error {
name string, tpl *tt.Template, data interface{}, opts ...FileOption, f, err := fileFromTextTemplate(n, t, d)
) error {
file, err := fileFromTextTemplate(name, tpl, data)
if err != nil { if err != nil {
return fmt.Errorf("failed to attach template: %w", err) 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 return nil
} }
// AttachFromEmbedFS adds an attachment File from an embed.FS to the Msg // AttachFromEmbedFS adds an attachment File from an embed.FS to the Msg
func (m *Msg) AttachFromEmbedFS(name string, fs *embed.FS, opts ...FileOption) error { func (m *Msg) AttachFromEmbedFS(n string, f *embed.FS, o ...FileOption) error {
if fs == nil { if f == nil {
return fmt.Errorf("embed.FS must not be nil") return fmt.Errorf("embed.FS must not be nil")
} }
file, err := fileFromEmbedFS(name, fs) ef, err := fileFromEmbedFS(n, f)
if err != nil { if err != nil {
return err return err
} }
m.attachments = m.appendFile(m.attachments, file, opts...) m.attachments = m.appendFile(m.attachments, ef, o...)
return nil return nil
} }
// EmbedFile adds an embedded File to the Msg // EmbedFile adds an embedded File to the Msg
func (m *Msg) EmbedFile(name string, opts ...FileOption) { func (m *Msg) EmbedFile(n string, o ...FileOption) {
file := fileFromFS(name) f := fileFromFS(n)
if file == nil { if f == nil {
return 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 // 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 // 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 // data on the io.Reader should be avoided. For such, it is recommended to
// either use EmbedFile or EmbedReadSeeker instead // either use EmbedFile or EmbedReadSeeker instead
func (m *Msg) EmbedReader(name string, reader io.Reader, opts ...FileOption) error { func (m *Msg) EmbedReader(n string, r io.Reader, o ...FileOption) error {
file, err := fileFromReader(name, reader) f, err := fileFromReader(n, r)
if err != nil { if err != nil {
return err return err
} }
m.embeds = m.appendFile(m.embeds, file, opts...) m.embeds = m.appendFile(m.embeds, f, o...)
return nil return nil
} }
// EmbedReadSeeker adds an embedded File from an io.ReadSeeker to the Msg // EmbedReadSeeker adds an embedded File from an io.ReadSeeker to the Msg
func (m *Msg) EmbedReadSeeker(name string, reader io.ReadSeeker, opts ...FileOption) { func (m *Msg) EmbedReadSeeker(n string, r io.ReadSeeker, o ...FileOption) {
file := fileFromReadSeeker(name, reader) f := fileFromReadSeeker(n, r)
m.embeds = m.appendFile(m.embeds, file, opts...) m.embeds = m.appendFile(m.embeds, f, o...)
} }
// EmbedHTMLTemplate adds the output of a html/template.Template pointer as embedded File to the Msg // EmbedHTMLTemplate adds the output of a html/template.Template pointer as embedded File to the Msg
func (m *Msg) EmbedHTMLTemplate( func (m *Msg) EmbedHTMLTemplate(n string, t *ht.Template, d interface{}, o ...FileOption) error {
name string, tpl *ht.Template, data interface{}, opts ...FileOption, f, err := fileFromHTMLTemplate(n, t, d)
) error {
file, err := fileFromHTMLTemplate(name, tpl, data)
if err != nil { if err != nil {
return fmt.Errorf("failed to embed template: %w", err) 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 return nil
} }
// EmbedTextTemplate adds the output of a text/template.Template pointer as embedded File to the Msg // EmbedTextTemplate adds the output of a text/template.Template pointer as embedded File to the Msg
func (m *Msg) EmbedTextTemplate( func (m *Msg) EmbedTextTemplate(n string, t *tt.Template, d interface{}, o ...FileOption) error {
name string, tpl *tt.Template, data interface{}, opts ...FileOption, f, err := fileFromTextTemplate(n, t, d)
) error {
file, err := fileFromTextTemplate(name, tpl, data)
if err != nil { if err != nil {
return fmt.Errorf("failed to embed template: %w", err) 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 return nil
} }
// EmbedFromEmbedFS adds an embedded File from an embed.FS to the Msg // EmbedFromEmbedFS adds an embedded File from an embed.FS to the Msg
func (m *Msg) EmbedFromEmbedFS(name string, fs *embed.FS, opts ...FileOption) error { func (m *Msg) EmbedFromEmbedFS(n string, f *embed.FS, o ...FileOption) error {
if fs == nil { if f == nil {
return fmt.Errorf("embed.FS must not be nil") return fmt.Errorf("embed.FS must not be nil")
} }
file, err := fileFromEmbedFS(name, fs) ef, err := fileFromEmbedFS(n, f)
if err != nil { if err != nil {
return err return err
} }
m.embeds = m.appendFile(m.embeds, file, opts...) m.embeds = m.appendFile(m.embeds, ef, o...)
return nil return nil
} }
@ -939,73 +925,73 @@ func (m *Msg) Reset() {
} }
// ApplyMiddlewares apply the list of middlewares to a Msg // ApplyMiddlewares apply the list of middlewares to a Msg
func (m *Msg) applyMiddlewares(msg *Msg) *Msg { func (m *Msg) applyMiddlewares(ms *Msg) *Msg {
for _, middleware := range m.middlewares { for _, mw := range m.middlewares {
msg = middleware.Handle(msg) ms = mw.Handle(ms)
} }
return msg return ms
} }
// WriteTo writes the formated Msg into a give io.Writer and satisfies the io.WriteTo interface // 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) { func (m *Msg) WriteTo(w io.Writer) (int64, error) {
mw := &msgWriter{writer: writer, charset: m.charset, encoder: m.encoder} mw := &msgWriter{writer: w, charset: m.charset, encoder: m.encoder}
mw.writeMsg(m.applyMiddlewares(m)) mw.writeMsg(m.applyMiddlewares(m))
return mw.bytesWritten, mw.err return mw.bytesWritten, mw.err
} }
// WriteToSkipMiddleware writes the formated Msg into a give io.Writer and satisfies // WriteToSkipMiddleware writes the formated Msg into a give io.Writer and satisfies
// the io.WriteTo interface but will skip the given Middleware // the io.WriteTo interface but will skip the given Middleware
func (m *Msg) WriteToSkipMiddleware(writer io.Writer, middleWareType MiddlewareType) (int64, error) { func (m *Msg) WriteToSkipMiddleware(w io.Writer, mt MiddlewareType) (int64, error) {
var origMiddlewares, middlewares []Middleware var omwl, mwl []Middleware
origMiddlewares = m.middlewares omwl = m.middlewares
for i := range m.middlewares { for i := range m.middlewares {
if m.middlewares[i].Type() == middleWareType { if m.middlewares[i].Type() == mt {
continue continue
} }
middlewares = append(middlewares, m.middlewares[i]) mwl = append(mwl, m.middlewares[i])
} }
m.middlewares = middlewares m.middlewares = mwl
mw := &msgWriter{writer: writer, charset: m.charset, encoder: m.encoder} mw := &msgWriter{writer: w, charset: m.charset, encoder: m.encoder}
mw.writeMsg(m.applyMiddlewares(m)) mw.writeMsg(m.applyMiddlewares(m))
m.middlewares = origMiddlewares m.middlewares = omwl
return mw.bytesWritten, mw.err return mw.bytesWritten, mw.err
} }
// Write is an alias method to WriteTo due to compatibility reasons // Write is an alias method to WriteTo due to compatibility reasons
func (m *Msg) Write(writer io.Writer) (int64, error) { func (m *Msg) Write(w io.Writer) (int64, error) {
return m.WriteTo(writer) return m.WriteTo(w)
} }
// appendFile adds a File to the Msg (as attachment or embed) // 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 // Override defaults with optionally provided FileOption functions
for _, opt := range opts { for _, co := range o {
if opt == nil { if co == nil {
continue continue
} }
opt(file) co(f)
} }
if files == nil { if c == nil {
return []*File{file} 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 // WriteToFile stores the Msg as file on disk. It will try to create the given filename
// Already existing files will be overwritten // Already existing files will be overwritten
func (m *Msg) WriteToFile(name string) error { func (m *Msg) WriteToFile(n string) error {
file, err := os.Create(name) f, err := os.Create(n)
if err != nil { if err != nil {
return fmt.Errorf("failed to create output file: %w", err) return fmt.Errorf("failed to create output file: %w", err)
} }
defer func() { _ = file.Close() }() defer func() { _ = f.Close() }()
_, err = m.WriteTo(file) _, err = m.WriteTo(f)
if err != nil { if err != nil {
return fmt.Errorf("failed to write to output file: %w", err) 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 // WriteToSendmail returns WriteToSendmailWithCommand with a default sendmail path
@ -1015,38 +1001,38 @@ func (m *Msg) WriteToSendmail() error {
// WriteToSendmailWithCommand returns WriteToSendmailWithContext with a default timeout // WriteToSendmailWithCommand returns WriteToSendmailWithContext with a default timeout
// of 5 seconds and a given sendmail path // of 5 seconds and a given sendmail path
func (m *Msg) WriteToSendmailWithCommand(sendmailPath string) error { func (m *Msg) WriteToSendmailWithCommand(sp string) error {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) tctx, tcfn := context.WithTimeout(context.Background(), time.Second*5)
defer cancel() defer tcfn()
return m.WriteToSendmailWithContext(ctx, sendmailPath) return m.WriteToSendmailWithContext(tctx, sp)
} }
// WriteToSendmailWithContext opens an pipe to the local sendmail binary and tries to send the // 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 // mail though that. It takes a context.Context, the path to the sendmail binary and additional
// arguments for the sendmail binary as parameters // arguments for the sendmail binary as parameters
func (m *Msg) WriteToSendmailWithContext(ctx context.Context, sendmailPath string, args ...string) error { func (m *Msg) WriteToSendmailWithContext(ctx context.Context, sp string, a ...string) error {
cmdCtx := exec.CommandContext(ctx, sendmailPath) ec := exec.CommandContext(ctx, sp)
cmdCtx.Args = append(cmdCtx.Args, "-oi", "-t") ec.Args = append(ec.Args, "-oi", "-t")
cmdCtx.Args = append(cmdCtx.Args, args...) ec.Args = append(ec.Args, a...)
stdErr, err := cmdCtx.StderrPipe() se, err := ec.StderrPipe()
if err != nil { if err != nil {
return fmt.Errorf("failed to set STDERR pipe: %w", err) return fmt.Errorf("failed to set STDERR pipe: %w", err)
} }
stdIn, err := cmdCtx.StdinPipe() si, err := ec.StdinPipe()
if err != nil { if err != nil {
return fmt.Errorf("failed to set STDIN pipe: %w", err) 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") return fmt.Errorf("received nil for STDERR or STDIN pipe")
} }
// Start the execution and write to STDIN // 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) return fmt.Errorf("could not start sendmail execution: %w", err)
} }
_, err = m.WriteTo(stdIn) _, err = m.WriteTo(si)
if err != nil { if err != nil {
if !errors.Is(err, syscall.EPIPE) { if !errors.Is(err, syscall.EPIPE) {
return fmt.Errorf("failed to write mail to buffer: %w", err) 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 // 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) return fmt.Errorf("failed to close STDIN pipe: %w", err)
} }
// Read the stderr pipe for possible errors // Read the stderr pipe for possible errors
sendmailErr, err := io.ReadAll(stdErr) serr, err := io.ReadAll(se)
if err != nil { if err != nil {
return fmt.Errorf("failed to read STDERR pipe: %w", err) return fmt.Errorf("failed to read STDERR pipe: %w", err)
} }
if len(sendmailErr) > 0 { if len(serr) > 0 {
return fmt.Errorf("sendmail command failed: %s", string(sendmailErr)) 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) 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 // 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 // first to update the Reader's buffer with the current Msg content
func (m *Msg) NewReader() *Reader { func (m *Msg) NewReader() *Reader {
reader := &Reader{} r := &Reader{}
buffer := bytes.Buffer{} wbuf := bytes.Buffer{}
_, err := m.Write(&buffer) _, err := m.Write(&wbuf)
if err != nil { 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() r.buf = wbuf.Bytes()
return reader return r
} }
// UpdateReader will update a Reader with the content of the given Msg and reset the // UpdateReader will update a Reader with the content of the given Msg and reset the
// Reader position to the start // Reader position to the start
func (m *Msg) UpdateReader(reader *Reader) { func (m *Msg) UpdateReader(r *Reader) {
buffer := bytes.Buffer{} wbuf := bytes.Buffer{}
_, err := m.Write(&buffer) _, err := m.Write(&wbuf)
reader.Reset() r.Reset()
reader.buf = buffer.Bytes() r.buf = wbuf.Bytes()
reader.err = err r.err = err
} }
// HasSendError returns true if the Msg experienced an error during the message delivery and the // 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 // 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 // corresponding error was of temporary nature and should be retried later
func (m *Msg) SendErrorIsTemp() bool { func (m *Msg) SendErrorIsTemp() bool {
var err *SendError var e *SendError
if errors.As(m.sendError, &err) && err != nil { if errors.As(m.sendError, &e) && e != nil {
return err.isTemp return e.isTemp
} }
return false return false
} }
@ -1124,19 +1110,19 @@ func (m *Msg) SendError() error {
// encodeString encodes a string based on the configured message encoder and the corresponding // encodeString encodes a string based on the configured message encoder and the corresponding
// charset for the Msg // charset for the Msg
func (m *Msg) encodeString(str string) string { func (m *Msg) encodeString(s string) string {
return m.encoder.Encode(string(m.charset), str) return m.encoder.Encode(string(m.charset), s)
} }
// hasAlt returns true if the Msg has more than one part // hasAlt returns true if the Msg has more than one part
func (m *Msg) hasAlt() bool { func (m *Msg) hasAlt() bool {
count := 0 c := 0
for _, part := range m.parts { for _, p := range m.parts {
if !part.del { if !p.del {
count++ c++
} }
} }
return count > 1 && m.pgptype == 0 return c > 1 && m.pgptype == 0
} }
// hasMixed returns true if the Msg has mixed parts // 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 // 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{ p := &Part{
ctype: contentType, ctype: ct,
cset: m.charset, cset: m.charset,
enc: m.encoding, enc: m.encoding,
} }
// Override defaults with optionally provided MsgOption functions // Override defaults with optionally provided MsgOption functions
for _, opt := range opts { for _, co := range o {
if opt == nil { if co == nil {
continue continue
} }
opt(p) co(p)
} }
return 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 // fileFromEmbedFS returns a File pointer from a given file in the provided embed.FS
func fileFromEmbedFS(name string, fs *embed.FS) (*File, error) { func fileFromEmbedFS(n string, f *embed.FS) (*File, error) {
_, err := fs.Open(name) _, err := f.Open(n)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to open file from embed.FS: %w", err) return nil, fmt.Errorf("failed to open file from embed.FS: %w", err)
} }
return &File{ return &File{
Name: filepath.Base(name), Name: filepath.Base(n),
Header: make(map[string][]string), Header: make(map[string][]string),
Writer: func(writer io.Writer) (int64, error) { Writer: func(w io.Writer) (int64, error) {
file, err := fs.Open(name) h, err := f.Open(n)
if err != nil { if err != nil {
return 0, err return 0, err
} }
numBytes, err := io.Copy(writer, file) nb, err := io.Copy(w, h)
if err != nil { if err != nil {
_ = file.Close() _ = h.Close()
return numBytes, fmt.Errorf("failed to copy file to io.Writer: %w", err) return nb, fmt.Errorf("failed to copy file to io.Writer: %w", err)
} }
return numBytes, file.Close() return nb, h.Close()
}, },
}, nil }, nil
} }
// fileFromFS returns a File pointer from a given file in the system's file system // fileFromFS returns a File pointer from a given file in the system's file system
func fileFromFS(name string) *File { func fileFromFS(n string) *File {
_, err := os.Stat(name) _, err := os.Stat(n)
if err != nil { if err != nil {
return nil return nil
} }
return &File{ return &File{
Name: filepath.Base(name), Name: filepath.Base(n),
Header: make(map[string][]string), Header: make(map[string][]string),
Writer: func(writer io.Writer) (int64, error) { Writer: func(w io.Writer) (int64, error) {
file, err := os.Open(name) h, err := os.Open(n)
if err != nil { if err != nil {
return 0, err return 0, err
} }
numBytes, err := io.Copy(writer, file) nb, err := io.Copy(w, h)
if err != nil { if err != nil {
_ = file.Close() _ = h.Close()
return numBytes, fmt.Errorf("failed to copy file to io.Writer: %w", err) 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 // fileFromReader returns a File pointer from a given io.Reader
func fileFromReader(name string, reader io.Reader) (*File, error) { func fileFromReader(n string, r io.Reader) (*File, error) {
d, err := io.ReadAll(reader) d, err := io.ReadAll(r)
if err != nil { if err != nil {
return &File{}, err return &File{}, err
} }
byteReader := bytes.NewReader(d) br := bytes.NewReader(d)
return &File{ return &File{
Name: name, Name: n,
Header: make(map[string][]string), Header: make(map[string][]string),
Writer: func(writer io.Writer) (int64, error) { Writer: func(w io.Writer) (int64, error) {
readBytes, copyErr := io.Copy(writer, byteReader) rb, cerr := io.Copy(w, br)
if copyErr != nil { if cerr != nil {
return readBytes, copyErr return rb, cerr
} }
_, copyErr = byteReader.Seek(0, io.SeekStart) _, cerr = br.Seek(0, io.SeekStart)
return readBytes, copyErr return rb, cerr
}, },
}, nil }, nil
} }
// fileFromReadSeeker returns a File pointer from a given io.ReadSeeker // 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{ return &File{
Name: name, Name: n,
Header: make(map[string][]string), Header: make(map[string][]string),
Writer: func(writer io.Writer) (int64, error) { Writer: func(w io.Writer) (int64, error) {
readBytes, err := io.Copy(writer, reader) rb, err := io.Copy(w, r)
if err != nil { if err != nil {
return readBytes, err return rb, err
} }
_, err = reader.Seek(0, io.SeekStart) _, err = r.Seek(0, io.SeekStart)
return readBytes, err return rb, err
}, },
} }
} }
// fileFromHTMLTemplate returns a File pointer form a given html/template.Template // fileFromHTMLTemplate returns a File pointer form a given html/template.Template
func fileFromHTMLTemplate(name string, tpl *ht.Template, data interface{}) (*File, error) { func fileFromHTMLTemplate(n string, t *ht.Template, d interface{}) (*File, error) {
if tpl == nil { if t == nil {
return nil, fmt.Errorf(errTplPointerNil) return nil, fmt.Errorf(errTplPointerNil)
} }
buffer := bytes.Buffer{} buf := bytes.Buffer{}
if err := tpl.Execute(&buffer, data); err != nil { if err := t.Execute(&buf, d); err != nil {
return nil, fmt.Errorf(errTplExecuteFailed, err) 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 // fileFromTextTemplate returns a File pointer form a given text/template.Template
func fileFromTextTemplate(name string, tpl *tt.Template, data interface{}) (*File, error) { func fileFromTextTemplate(n string, t *tt.Template, d interface{}) (*File, error) {
if tpl == nil { if t == nil {
return nil, fmt.Errorf(errTplPointerNil) return nil, fmt.Errorf(errTplPointerNil)
} }
buffer := bytes.Buffer{} buf := bytes.Buffer{}
if err := tpl.Execute(&buffer, data); err != nil { if err := t.Execute(&buf, d); err != nil {
return nil, fmt.Errorf(errTplExecuteFailed, err) 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 // getEncoder creates a new mime.WordEncoder based on the encoding setting of the message
func getEncoder(enc Encoding) mime.WordEncoder { func getEncoder(e Encoding) mime.WordEncoder {
switch enc { switch e {
case EncodingQP: case EncodingQP:
return mime.QEncoding return mime.QEncoding
case EncodingB64: 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 // writeFuncFromBuffer is a common method to convert a byte buffer into a writeFunc as
// often required by this library // often required by this library
func writeFuncFromBuffer(buffer *bytes.Buffer) func(io.Writer) (int64, error) { func writeFuncFromBuffer(buf *bytes.Buffer) func(io.Writer) (int64, error) {
writeFunc := func(w io.Writer) (int64, error) { w := func(w io.Writer) (int64, error) {
numBytes, err := w.Write(buffer.Bytes()) nb, err := w.Write(buf.Bytes())
return int64(numBytes), err return int64(nb), err
} }
return writeFunc return w
} }

8
tls.go
View file

@ -8,17 +8,17 @@ package mail
type TLSPolicy int type TLSPolicy int
const ( 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 // encrypting using STARTTLS. If the server does not support STARTTLS
// the connection will be terminated with an error // the connection will be terminated with an error
TLSMandatory TLSPolicy = iota 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 // 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 TLSOpportunistic
// NoTLS forces the transaction to be not encrypted // NoTLS forces the transaction cto be not encrypted
NoTLS NoTLS
) )