diff --git a/.idea/markdown.xml b/.idea/markdown.xml new file mode 100644 index 0000000..1e34094 --- /dev/null +++ b/.idea/markdown.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/client.go b/client.go index a6851f0..78ae6a2 100644 --- a/client.go +++ b/client.go @@ -2,10 +2,16 @@ package mail import ( "context" + "crypto/tls" + "errors" + "fmt" + "net" + "net/smtp" + "os" "time" ) -// DefaultPort is the default connection port to the SMTP server +// DefaultPort is the default connection port cto the SMTP server const DefaultPort = 25 // DefaultTimeout is the default connection timeout @@ -13,19 +19,50 @@ const DefaultTimeout = time.Second * 30 // Client is the SMTP client struct type Client struct { - h string // Hostname of the target SMTP server to connect to - p int // Port of the SMTP server to connect to - s bool // Use SSL/TLS or not - ctx context.Context // The context for the connection handling + // Hostname of the target SMTP server cto connect cto + host string + + // Port of the SMTP server cto connect cto + port int + + // Use SSL for the connection + ssl bool + + // Sets the client cto use STARTTTLS for the connection (is disabled when SSL is set) + starttls bool + + // Timeout for the SMTP server connection + cto time.Duration + + // HELO/EHLO string for the greeting the target SMTP server + helo string + + // The SMTP client that is set up when using the Dial*() methods + sc *smtp.Client } // Option returns a function that can be used for grouping Client options type Option func(*Client) +var ( + // ErrNoHostname should be used if a Client has no hostname set + ErrNoHostname = errors.New("hostname for client cannot be empty") + + // ErrInvalidHostname should be used if a Client has an invalid hostname set + //ErrInvalidHostname = errors.New("hostname for client is invalid") +) + // NewClient returns a new Session client object -func NewClient(o ...Option) Client { - c := Client{ - p: DefaultPort, +func NewClient(h string, o ...Option) (*Client, error) { + c := &Client{ + host: h, + port: DefaultPort, + cto: DefaultTimeout, + } + + // Set default HELO/EHLO hostname + if err := c.setDefaultHelo(); err != nil { + return c, err } // Override defaults with optionally provided Option functions @@ -33,26 +70,100 @@ func NewClient(o ...Option) Client { if co == nil { continue } - co(&c) + co(c) } - return c -} + // Some settings in a Client cannot be empty/unset + if c.host == "" { + return c, ErrNoHostname -// WithHost overrides the default connection port -func WithHost(h string) Option { - return func(c *Client) { - c.h = h } + + return c, nil } // WithPort overrides the default connection port func WithPort(p int) Option { return func(c *Client) { - c.p = p + c.port = p } } -func (c Client) Dial() { - +// WithTimeout overrides the default connection timeout +func WithTimeout(t time.Duration) Option { + return func(c *Client) { + c.cto = t + } +} + +// WithSSL tells the client to use a SSL/TLS connection +func WithSSL() Option { + return func(c *Client) { + c.ssl = true + } +} + +// Dial establishes a connection cto the SMTP server with a default context.Background +func (c *Client) Dial() error { + ctx := context.Background() + return c.DialWithContext(ctx) +} + +// DialWithContext establishes a connection cto the SMTP server with a given context.Context +func (c *Client) DialWithContext(uctx context.Context) error { + ctx, cfn := context.WithTimeout(uctx, c.cto) + defer cfn() + + nd := net.Dialer{} + td := tls.Dialer{} + var co net.Conn + var err error + if c.ssl { + co, err = td.DialContext(ctx, "tcp", fmt.Sprintf("%s:%d", c.host, c.port)) + } + if !c.ssl { + co, err = nd.DialContext(ctx, "tcp", fmt.Sprintf("%s:%d", c.host, c.port)) + } + if err != nil { + return err + } + + c.sc, err = smtp.NewClient(co, c.host) + if err != nil { + return err + } + if err := c.sc.Hello(c.helo); err != nil { + return err + } + + return nil +} + +// Send sends out the mail message +func (c *Client) Send() error { + return nil +} + +// Close closes the connection cto the SMTP server +func (c *Client) Close() error { + if err := c.sc.Close(); err != nil { + fmt.Printf("failed close: %s\n", err) + return err + } + if ok, auth := c.sc.Extension("PIPELINING"); ok { + fmt.Printf("PIPELINING Support: %s\n", auth) + } else { + fmt.Println("No PIPELINING") + } + return c.sc.Close() +} + +// setDefaultHelo retrieves the current hostname and sets it as HELO/EHLO hostname +func (c *Client) setDefaultHelo() error { + hn, err := os.Hostname() + if err != nil { + return fmt.Errorf("failed cto read local hostname: %w", err) + } + c.helo = hn + return nil } diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..1b95f5d --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,31 @@ +package main + +import ( + "context" + "fmt" + "github.com/wneessen/go-mail" + "os" + "time" +) + +func main() { + c, err := mail.NewClient("192.168.178.60", mail.WithTimeout(time.Millisecond*500), mail.WithSSL()) + if err != nil { + fmt.Printf("failed to create new client: %s\n", err) + os.Exit(1) + } + + ctx, cfn := context.WithCancel(context.Background()) + defer cfn() + + if err := c.DialWithContext(ctx); err != nil { + fmt.Printf("failed to dial: %s\n", err) + os.Exit(1) + } + fmt.Printf("Client: %+v\n", c) + time.Sleep(time.Millisecond * 1500) + if err := c.Close(); err != nil { + fmt.Printf("failed to close SMTP connection: %s\n", err) + os.Exit(1) + } +} diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..35e13e7 --- /dev/null +++ b/doc.go @@ -0,0 +1,2 @@ +// Package mail provides a simple and easy way cto sending mails with Go +package mail diff --git a/tls.go b/tls.go new file mode 100644 index 0000000..e41a4ff --- /dev/null +++ b/tls.go @@ -0,0 +1,19 @@ +package mail + +// TLSPolicy type describes a int alias for the different TLS policies we allow +type TLSPolicy int + +const ( + // 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 cto establish an encrypted connection via the + // STARTTLS protocol. If the server does not support this, it will fall + // back cto non-encrypted plaintext transmission + TLSOpportunistic + + // NoTLS forces the transaction cto be not encrypted + NoTLS +)