diff --git a/.idea/workspace.xml b/.idea/workspace.xml index ed8b9b9..a1964bf 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -4,8 +4,13 @@ - + + + + + + + + + - - - - + - - - - - true diff --git a/README.md b/README.md index a95d818..1109df4 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,45 @@ [![Go Report Card](https://goreportcard.com/badge/github.com/wneessen/go-mail)](https://goreportcard.com/report/github.com/wneessen/go-mail) [![Build Status](https://api.cirrus-ci.com/github/wneessen/go-mail.svg)](https://cirrus-ci.com/github/wneessen/go-mail) buy ma a coffee -The main idea of this library is to provide a simple interface to sending mails for -my [JS-Mailer](https://github.com/wneessen/js-mailer) project. +The main idea of this library was to provide a simple interface to sending mails for +my [JS-Mailer](https://github.com/wneessen/js-mailer) project. It quickly evolved into a +full-fledged mail library. -This library is **WIP** an should not be considered "production ready" before v1.0 is released. \ No newline at end of file +**This library is "WIP" an should not be considered "production ready", yet.** + +go-mail follows idiomatic Go style and best practice. + +## Example +```go +package main + +import ( + "context" + "fmt" + "github.com/wneessen/go-mail" + "os" + "time" +) + +func main() { + c, err := mail.NewClient("mail.example.com", mail.WithTimeout(time.Millisecond*500), + mail.WithTLSPolicy(mail.TLSMandatory), mail.WithSMTPAuth(mail.SMTPAuthDigestMD5), + mail.WithUsername("tester@example.com"), mail.WithPassword("secureP4ssW0rd!")) + 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) + } + if err := c.Send(); err != nil { + fmt.Printf("failed to send: %s\n", err) + os.Exit(1) + } +} +``` \ No newline at end of file diff --git a/auth.go b/auth.go new file mode 100644 index 0000000..fb63b29 --- /dev/null +++ b/auth.go @@ -0,0 +1,36 @@ +package mail + +import "errors" + +// SMTPAuthType represents a string to any SMTP AUTH type +type SMTPAuthType string + +// Supported SMTP AUTH types +const ( + // SMTPAuthLogin is the "LOGIN" SASL authentication mechanism + SMTPAuthLogin SMTPAuthType = "LOGIN" + + // SMTPAuthPlain is the "PLAIN" authentication mechanism as described in RFC 4616 + SMTPAuthPlain SMTPAuthType = "PLAIN" + + // SMTPAuthCramMD5 is the "CRAM-MD5" SASL authentication mechanism as described in RFC 4954 + SMTPAuthCramMD5 SMTPAuthType = "CRAM-MD5" + + // SMTPAuthDigestMD5 is the "DIGEST-MD5" SASL authentication mechanism as described in RFC 4954 + SMTPAuthDigestMD5 SMTPAuthType = "DIGEST-MD5" +) + +// SMTP Auth related static errors +var ( + // ErrPlainAuthNotSupported should be used if the target server does not support the "PLAIN" schema + ErrPlainAuthNotSupported = errors.New("server does not support SMTP AUTH type: PLAIN") + + // ErrLoginAuthNotSupported should be used if the target server does not support the "LOGIN" schema + ErrLoginAuthNotSupported = errors.New("server does not support SMTP AUTH type: LOGIN") + + // ErrCramMD5AuthNotSupported should be used if the target server does not support the "CRAM-MD5" schema + ErrCramMD5AuthNotSupported = errors.New("server does not support SMTP AUTH type: CRAM-MD5") + + // ErrDigestMD5AuthNotSupported should be used if the target server does not support the "DIGEST-MD5" schema + ErrDigestMD5AuthNotSupported = errors.New("server does not support SMTP AUTH type: DIGEST-MD5") +) diff --git a/client.go b/client.go index cef5b18..1def948 100644 --- a/client.go +++ b/client.go @@ -8,6 +8,7 @@ import ( "net" "net/smtp" "os" + "strings" "time" ) @@ -43,6 +44,18 @@ type Client struct { // enc indicates if a Client connection is encrypted or not enc bool + // user is the SMTP AUTH username + user string + + // pass is the corresponding SMTP AUTH password + pass string + + // satype represents the authentication type for SMTP AUTH + satype SMTPAuthType + + // smtpauth is a pointer to smtp.Auth + sa smtp.Auth + // The SMTP client that is set up when using the Dial*() methods sc *smtp.Client } @@ -129,6 +142,34 @@ func WithTLSConfig(co *tls.Config) Option { } } +// WithSMTPAuth tells the client to use the provided SMTPAuthType for authentication +func WithSMTPAuth(t SMTPAuthType) Option { + return func(c *Client) { + c.satype = t + } +} + +// WithSMTPAuthCustom tells the client to use the provided smtp.Auth for SMTP authentication +func WithSMTPAuthCustom(a smtp.Auth) Option { + return func(c *Client) { + c.sa = a + } +} + +// WithUsername tells the client to use the provided string as username for authentication +func WithUsername(u string) Option { + return func(c *Client) { + c.user = u + } +} + +// WithPassword tells the client to use the provided string as password/secret for authentication +func WithPassword(p string) Option { + return func(c *Client) { + c.pass = p + } +} + // TLSPolicy returns the currently set TLSPolicy as string func (c *Client) TLSPolicy() string { return c.tlspolicy.String() @@ -226,5 +267,51 @@ func (c *Client) DialWithContext(uctx context.Context) error { _, c.enc = c.sc.TLSConnectionState() } + if err := c.auth(); err != nil { + return err + } + + return nil +} + +// auth will try to perform SMTP AUTH if requested +func (c *Client) auth() error { + if c.sa == nil && c.satype != "" { + sa, sat := c.sc.Extension("AUTH") + if !sa { + return fmt.Errorf("server does not support SMTP AUTH") + } + + switch c.satype { + case SMTPAuthPlain: + if !strings.Contains(sat, string(SMTPAuthPlain)) { + return ErrPlainAuthNotSupported + } + c.sa = smtp.PlainAuth("", c.user, c.pass, c.host) + case SMTPAuthLogin: + if !strings.Contains(sat, string(SMTPAuthLogin)) { + return ErrLoginAuthNotSupported + } + c.sa = smtp.PlainAuth("", c.user, c.pass, c.host) + case SMTPAuthCramMD5: + if !strings.Contains(sat, string(SMTPAuthCramMD5)) { + return ErrCramMD5AuthNotSupported + } + c.sa = smtp.CRAMMD5Auth(c.user, c.pass) + case SMTPAuthDigestMD5: + if !strings.Contains(sat, string(SMTPAuthDigestMD5)) { + return ErrDigestMD5AuthNotSupported + } + c.sa = smtp.CRAMMD5Auth(c.user, c.pass) + default: + return fmt.Errorf("unsupported SMTP AUTH type %q", c.satype) + } + } + + if c.sa != nil { + if err := c.sc.Auth(c.sa); err != nil { + return fmt.Errorf("SMTP AUTH failed: %w", err) + } + } return nil } diff --git a/cmd/main.go b/cmd/main.go index c7e4f12..b2aadce 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -14,7 +14,11 @@ func main() { fmt.Printf("$TEST_HOST env variable cannot be empty\n") os.Exit(1) } - c, err := mail.NewClient(th, mail.WithTimeout(time.Millisecond*500)) + + tu := os.Getenv("TEST_USER") + tp := os.Getenv("TEST_PASS") + c, err := mail.NewClient(th, mail.WithTimeout(time.Millisecond*500), mail.WithTLSPolicy(mail.TLSMandatory), + mail.WithSMTPAuth(mail.SMTPAuthDigestMD5), mail.WithUsername(tu), mail.WithPassword(tp)) if err != nil { fmt.Printf("failed to create new client: %s\n", err) os.Exit(1)