Implemented SMTP AUTH

This commit is contained in:
Winni Neessen 2022-03-10 10:53:38 +01:00
parent 1f6fc2cadc
commit 4babc309fb
Signed by: wneessen
GPG key ID: 385AC9889632126E
5 changed files with 190 additions and 32 deletions

View file

@ -4,8 +4,13 @@
<option name="autoReloadType" value="ALL" /> <option name="autoReloadType" value="ALL" />
</component> </component>
<component name="ChangeListManager"> <component name="ChangeListManager">
<list default="true" id="b7733b42-c5ae-46b3-8a96-8b973d69a11d" name="Changes" comment="Make GoLint happy"> <list default="true" id="b79e8e7a-d892-4ce4-8bf4-f9e45415b803" name="Changes" comment="Progress">
<change beforePath="$PROJECT_DIR$/doc.go" beforeDir="false" afterPath="$PROJECT_DIR$/doc.go" afterDir="false" /> <change afterPath="$PROJECT_DIR$/auth.go" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/README.md" beforeDir="false" afterPath="$PROJECT_DIR$/README.md" afterDir="false" />
<change beforePath="$PROJECT_DIR$/client.go" beforeDir="false" afterPath="$PROJECT_DIR$/client.go" afterDir="false" />
<change beforePath="$PROJECT_DIR$/cmd/main.go" beforeDir="false" afterPath="$PROJECT_DIR$/cmd/main.go" afterDir="false" />
<change beforePath="$PROJECT_DIR$/mailmsg.go" beforeDir="false" afterPath="$PROJECT_DIR$/mailmsg.go" afterDir="false" />
</list> </list>
<option name="SHOW_DIALOG" value="false" /> <option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" /> <option name="HIGHLIGHT_CONFLICTS" value="true" />
@ -19,17 +24,22 @@
</list> </list>
</option> </option>
</component> </component>
<component name="GOROOT" url="file://$PROJECT_DIR$/../../go1.17.8" /> <component name="GOROOT" url="file://$USER_HOME$/go/go1.17.8" />
<component name="Git.Settings"> <component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" /> <option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component> </component>
<component name="GitSEFilterConfiguration">
<file-type-list>
<filtered-out-file-type name="LOCAL_BRANCH" />
<filtered-out-file-type name="REMOTE_BRANCH" />
<filtered-out-file-type name="TAG" />
<filtered-out-file-type name="COMMIT_BY_MESSAGE" />
</file-type-list>
</component>
<component name="MarkdownSettingsMigration"> <component name="MarkdownSettingsMigration">
<option name="stateVersion" value="1" /> <option name="stateVersion" value="1" />
</component> </component>
<component name="ProjectId" id="25y1BKo9v06G2DMj9UoZG0ZxDb1" /> <component name="ProjectId" id="265yKSOR8vrrhyM3NTraVXli3pO" />
<component name="ProjectLevelVcsManager">
<OptionsSetting value="false" id="Update" />
</component>
<component name="ProjectViewState"> <component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" /> <option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" /> <option name="showLibraryContents" value="true" />
@ -38,9 +48,7 @@
<property name="DefaultGoTemplateProperty" value="Go File" /> <property name="DefaultGoTemplateProperty" value="Go File" />
<property name="RunOnceActivity.OpenProjectViewOnStart" value="true" /> <property name="RunOnceActivity.OpenProjectViewOnStart" value="true" />
<property name="RunOnceActivity.ShowReadmeOnStart" value="true" /> <property name="RunOnceActivity.ShowReadmeOnStart" value="true" />
<property name="TF_FMT" value="false" /> <property name="WebServerToolWindowFactoryState" value="false" />
<property name="WebServerToolWindowFactoryState" value="true" />
<property name="go.format.on.save.advertiser.fired" value="true" />
<property name="go.formatter.settings.were.checked" value="true" /> <property name="go.formatter.settings.were.checked" value="true" />
<property name="go.import.settings.migrated" value="true" /> <property name="go.import.settings.migrated" value="true" />
<property name="go.modules.go.list.on.any.changes.was.set" value="true" /> <property name="go.modules.go.list.on.any.changes.was.set" value="true" />
@ -54,8 +62,7 @@
<module name="go-mail" /> <module name="go-mail" />
<working_directory value="$PROJECT_DIR$" /> <working_directory value="$PROJECT_DIR$" />
<go_parameters value="-i" /> <go_parameters value="-i" />
<kind value="PACKAGE" /> <kind value="FILE" />
<package value="go-mail" />
<directory value="$PROJECT_DIR$" /> <directory value="$PROJECT_DIR$" />
<filePath value="$PROJECT_DIR$" /> <filePath value="$PROJECT_DIR$" />
<method v="2" /> <method v="2" />
@ -65,17 +72,6 @@
<working_directory value="$PROJECT_DIR$" /> <working_directory value="$PROJECT_DIR$" />
<go_parameters value="-i" /> <go_parameters value="-i" />
<kind value="DIRECTORY" /> <kind value="DIRECTORY" />
<package value="go-mail" />
<directory value="$PROJECT_DIR$" />
<filePath value="$PROJECT_DIR$" />
<framework value="gotest" />
<method v="2" />
</configuration>
<configuration name="go test github.com/wneessen/go-mail" type="GoTestRunConfiguration" factoryName="Go Test" nameIsGenerated="true">
<module name="go-mail" />
<working_directory value="$PROJECT_DIR$" />
<kind value="PACKAGE" />
<package value="github.com/wneessen/go-mail" />
<directory value="$PROJECT_DIR$" /> <directory value="$PROJECT_DIR$" />
<filePath value="$PROJECT_DIR$" /> <filePath value="$PROJECT_DIR$" />
<framework value="gotest" /> <framework value="gotest" />
@ -98,11 +94,8 @@
</option> </option>
</component> </component>
<component name="VcsManagerConfiguration"> <component name="VcsManagerConfiguration">
<MESSAGE value="Makeing some progress..." /> <MESSAGE value="Progress" />
<MESSAGE value="Calling it a day..." /> <option name="LAST_COMMIT_MESSAGE" value="Progress" />
<MESSAGE value="More progress... calling it a day." />
<MESSAGE value="Make GoLint happy" />
<option name="LAST_COMMIT_MESSAGE" value="Make GoLint happy" />
</component> </component>
<component name="VgoProject"> <component name="VgoProject">
<integration-enabled>true</integration-enabled> <integration-enabled>true</integration-enabled>

View file

@ -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) <a href="https://ko-fi.com/D1D24V9IX"><img src="https://uploads-ssl.webflow.com/5c14e387dab576fe667689cf/5cbed8a4ae2b88347c06c923_BuyMeACoffee_blue.png" height="20" alt="buy ma a coffee"></a> [![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) <a href="https://ko-fi.com/D1D24V9IX"><img src="https://uploads-ssl.webflow.com/5c14e387dab576fe667689cf/5cbed8a4ae2b88347c06c923_BuyMeACoffee_blue.png" height="20" alt="buy ma a coffee"></a>
The main idea of this library is to provide a simple interface to sending mails for 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. 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. **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)
}
}
```

36
auth.go Normal file
View file

@ -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")
)

View file

@ -8,6 +8,7 @@ import (
"net" "net"
"net/smtp" "net/smtp"
"os" "os"
"strings"
"time" "time"
) )
@ -43,6 +44,18 @@ type Client struct {
// enc indicates if a Client connection is encrypted or not // enc indicates if a Client connection is encrypted or not
enc bool 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 // The SMTP client that is set up when using the Dial*() methods
sc *smtp.Client 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 // TLSPolicy returns the currently set TLSPolicy as string
func (c *Client) TLSPolicy() string { func (c *Client) TLSPolicy() string {
return c.tlspolicy.String() return c.tlspolicy.String()
@ -226,5 +267,51 @@ func (c *Client) DialWithContext(uctx context.Context) error {
_, c.enc = c.sc.TLSConnectionState() _, 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 return nil
} }

View file

@ -14,7 +14,11 @@ func main() {
fmt.Printf("$TEST_HOST env variable cannot be empty\n") fmt.Printf("$TEST_HOST env variable cannot be empty\n")
os.Exit(1) 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 { if err != nil {
fmt.Printf("failed to create new client: %s\n", err) fmt.Printf("failed to create new client: %s\n", err)
os.Exit(1) os.Exit(1)