diff --git a/client.go b/client.go index e937fb5..df8ef0c 100644 --- a/client.go +++ b/client.go @@ -44,6 +44,26 @@ type Client struct { DebugWriter io.Writer } +type ClientOptions struct { + // the name to use in HELO/EHLO/LHLO + LocalName string + // Time to wait for command responses (this includes 3xx reply to DATA). + CommandTimeout time.Duration + // Time to wait for responses after final dot. + SubmissionTimeout time.Duration +} + +var DefaultOpts = ClientOptions{ + LocalName: "localhost", + // As recommended by RFC 5321. For DATA command reply (3xx one) RFC + // recommends a slightly shorter timeout but we do not bother + // differentiating these. + CommandTimeout: 5 * time.Minute, + // 10 minutes + 2 minute buffer in case the server is doing transparent + // forwarding and also follows recommended timeouts. + SubmissionTimeout: 12 * time.Minute, +} + // 30 seconds was chosen as it's the same duration as http.DefaultTransport's // timeout. var defaultDialer = net.Dialer{Timeout: 30 * time.Second} @@ -54,11 +74,17 @@ var defaultDialer = net.Dialer{Timeout: 30 * time.Second} // This function returns a plaintext connection. To enable TLS, use // DialStartTLS. func Dial(addr string) (*Client, error) { + return DialWithOpts(addr, nil) +} + +// DialWithOpts returns a new Client connected to an SMTP server at addr using +// custom options instead of the defaults. +func DialWithOpts(addr string, opts *ClientOptions) (*Client, error) { conn, err := defaultDialer.Dial("tcp", addr) if err != nil { return nil, err } - client := NewClient(conn) + client := NewClientWithOpts(conn, opts) client.serverName, _, _ = net.SplitHostPort(addr) return client, nil } @@ -68,6 +94,14 @@ func Dial(addr string) (*Client, error) { // // A nil tlsConfig is equivalent to a zero tls.Config. func DialTLS(addr string, tlsConfig *tls.Config) (*Client, error) { + return DialTLSWithOpts(addr, tlsConfig, nil) +} + +// DialTLSWithOpts returns a new Client connected to an SMTP server via TLS at +// addr using custom options instead of the defaults. +func DialTLSWithOpts( + addr string, tlsConfig *tls.Config, opts *ClientOptions, +) (*Client, error) { tlsDialer := tls.Dialer{ NetDialer: &defaultDialer, Config: tlsConfig, @@ -76,7 +110,7 @@ func DialTLS(addr string, tlsConfig *tls.Config) (*Client, error) { if err != nil { return nil, err } - client := NewClient(conn) + client := NewClientWithOpts(conn, opts) client.serverName, _, _ = net.SplitHostPort(addr) return client, nil } @@ -100,17 +134,20 @@ func DialStartTLS(addr string, tlsConfig *tls.Config) (*Client, error) { // NewClient returns a new Client using an existing connection and host as a // server name to be used when authenticating. func NewClient(conn net.Conn) *Client { + return NewClientWithOpts(conn, nil) +} + +// NewClient returns a new Client using an existing connection using custom +// options instead of the defaults. +func NewClientWithOpts(conn net.Conn, opts *ClientOptions) *Client { + if opts == nil { + opts = &DefaultOpts + } c := &Client{ - localName: "localhost", - // As recommended by RFC 5321. For DATA command reply (3xx one) RFC - // recommends a slightly shorter timeout but we do not bother - // differentiating these. - CommandTimeout: 5 * time.Minute, - // 10 minutes + 2 minute buffer in case the server is doing transparent - // forwarding and also follows recommended timeouts. - SubmissionTimeout: 12 * time.Minute, + localName: opts.LocalName, + CommandTimeout: opts.CommandTimeout, + SubmissionTimeout: opts.SubmissionTimeout, } - c.setConn(conn) return c @@ -118,7 +155,15 @@ func NewClient(conn net.Conn) *Client { // NewClientStartTLS creates a new Client and performs a STARTTLS command. func NewClientStartTLS(conn net.Conn, tlsConfig *tls.Config) (*Client, error) { - c := NewClient(conn) + return NewClientStartTLSWithOpts(conn, tlsConfig, nil) +} + +// NewClientStartTLS creates a new Client and performs a STARTTLS command. +// It allows using custom options instead of the defaults. +func NewClientStartTLSWithOpts( + conn net.Conn, tlsConfig *tls.Config, opts *ClientOptions, +) (*Client, error) { + c := NewClientWithOpts(conn, opts) if err := initStartTLS(c, tlsConfig); err != nil { c.Close() return nil, err @@ -142,7 +187,13 @@ func initStartTLS(c *Client, tlsConfig *tls.Config) error { // NewClientLMTP returns a new LMTP Client (as defined in RFC 2033) using an // existing connection and host as a server name to be used when authenticating. func NewClientLMTP(conn net.Conn) *Client { - c := NewClient(conn) + return NewClientLMTPWithOpts(conn, nil) +} + +// NewClientLMTP returns a new LMTP Client (as defined in RFC 2033) using +// custom options instead of the defaults. +func NewClientLMTPWithOpts(conn net.Conn, opts *ClientOptions) *Client { + c := NewClientWithOpts(conn, opts) c.lmtp = true return c }