Skip to content

Commit

Permalink
Add: Ability to specify delivery status notification (#87)
Browse files Browse the repository at this point in the history
* Add: Ability to specify delivery status notification when supported by
the server.

* refactor support for DSN

---------

Co-authored-by: Santiago De la Cruz <[email protected]>
  • Loading branch information
Herz3h and xhit committed Aug 4, 2023
1 parent 47471be commit b1fb802
Show file tree
Hide file tree
Showing 4 changed files with 124 additions and 27 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ Go Simple Mail supports:
- Support add a List-Unsubscribe header (since v2.11.0)
- Support to add a DKIM signarure (since v2.11.0)
- Support to send using custom connection, ideal for proxy (since v2.15.0)
- Support Delivery Status Notification (DSN) (since v2.16.0)

## Documentation

Expand Down Expand Up @@ -203,6 +204,9 @@ func main() {
// add inline
email.Attach(&mail.File{FilePath: "/path/to/image.png", Name:"Gopher.png", Inline: true})

// also you can set Delivery Status Notification (DSN) (only is set when server supports DSN)
email.SetDSN([]mail.DSN{mail.SUCCESS, mail.FAILURE}, false)

// you can add dkim signature to the email.
// to add dkim, you need a private key already created one.
if privateKey != "" {
Expand Down
134 changes: 113 additions & 21 deletions email.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,24 @@ import (

// Email represents an email message.
type Email struct {
from string
sender string
replyTo string
returnPath string
recipients []string
headers textproto.MIMEHeader
parts []part
attachments []*File
inlines []*File
Charset string
Encoding encoding
Error error
SMTPServer *smtpClient
DkimMsg string
AllowDuplicateAddress bool
AddBccToHeader bool
from string
sender string
replyTo string
returnPath string
recipients []string
headers textproto.MIMEHeader
parts []part
attachments []*File
inlines []*File
Charset string
Encoding encoding
Error error
SMTPServer *smtpClient
DkimMsg string
AllowDuplicateAddress bool
AddBccToHeader bool
preserveOriginalRecipient bool
dsn []DSN
}

/*
Expand All @@ -59,10 +61,13 @@ type SMTPServer struct {

// SMTPClient represents a SMTP Client for send email
type SMTPClient struct {
mu sync.Mutex
Client *smtpClient
KeepAlive bool
SendTimeout time.Duration
mu sync.Mutex
Client *smtpClient
SendTimeout time.Duration
KeepAlive bool
hasDSNExt bool
preserveOriginalRecipient bool
dsn []DSN
}

// part represents the different content parts of an email body.
Expand Down Expand Up @@ -158,6 +163,34 @@ func (at AuthType) String() string {
}
}

/*
DSN notifications
- 'NEVER' under no circumstances a DSN must be returned to the sender. If you use NEVER all other notifications will be ignored.
- 'SUCCESS' will notify you when your mail has arrived at its destination.
- 'FAILURE' will arrive if an error occurred during delivery.
- 'DELAY' will notify you if there is an unusual delay in delivery, but the actual delivery's outcome (success or failure) is not yet decided.
see https://tools.ietf.org/html/rfc3461 See section 4.1 for more information about NOTIFY
*/
type DSN int

const (
NEVER DSN = iota
FAILURE
DELAY
SUCCESS
)

var dsnTypes = [...]string{"NEVER", "FAILURE", "DELAY", "SUCCESS"}

func (dsn DSN) String() string {
return dsnTypes[dsn]
}

// NewMSG creates a new email. It uses UTF-8 by default. All charsets: http://webcheatsheet.com/HTML/character_sets_list.php
func NewMSG() *Email {
email := &Email{
Expand Down Expand Up @@ -613,6 +646,20 @@ func (email *Email) AddAlternativeData(contentType ContentType, body []byte) *Em
return email
}

// SetDSN sets the delivery status notification list, only is set when SMTP server supports DSN extension
//
// To preserve the original recipient of an email message, for example, if it is forwarded to another address, set preserveOriginalRecipient to true
func (email *Email) SetDSN(dsn []DSN, preserveOriginalRecipient bool) *Email {
if email.Error != nil {
return email
}

email.dsn = dsn
email.preserveOriginalRecipient = preserveOriginalRecipient

return email
}

// GetFrom returns the sender of the email, if any
func (email *Email) GetFrom() string {
from := email.returnPath
Expand Down Expand Up @@ -710,6 +757,9 @@ func (email *Email) SendEnvelopeFrom(from string, client *SMTPClient) error {
msg = email.GetMessage()
}

client.dsn = email.dsn
client.preserveOriginalRecipient = email.preserveOriginalRecipient

return send(from, email.recipients, msg, client)
}

Expand Down Expand Up @@ -864,10 +914,13 @@ func (server *SMTPServer) Connect() (*SMTPClient, error) {
}
}

_, hasDSN := c.ext["DSN"]

return &SMTPClient{
Client: c,
KeepAlive: server.KeepAlive,
SendTimeout: server.SendTimeout,
hasDSNExt: hasDSN,
}, server.validateAuth(c)
}

Expand Down Expand Up @@ -965,9 +1018,31 @@ func sendMailProcess(from string, to []string, msg string, c *SMTPClient) error
return err
}

var dsn string
var dsnSet bool

if c.hasDSNExt && len(c.dsn) > 0 {
dsn = " NOTIFY="
if hasNeverDSN(c.dsn) {
dsn += NEVER.String()
} else {
dsn += strings.Join(dsnToString(c.dsn), ",")
}

if c.preserveOriginalRecipient {
dsn += " ORCPT=rfc822;"
}

dsnSet = true
}

// Set the recipients
for _, address := range to {
if err := c.Client.rcpt(address); err != nil {
if dsnSet && c.preserveOriginalRecipient {
dsn += address
}

if err := c.Client.rcpt(address, dsn); err != nil {
return err
}
}
Expand Down Expand Up @@ -1001,3 +1076,20 @@ func checkKeepAlive(client *SMTPClient) {
client.Close()
}
}

func hasNeverDSN(dsnList []DSN) bool {
for i := range dsnList {
if dsnList[i] == NEVER {
return true
}
}
return false
}

func dsnToString(dsnList []DSN) []string {
dsnString := make([]string, len(dsnList))
for i := range dsnList {
dsnString[i] = dsnList[i].String()
}
return dsnString
}
4 changes: 2 additions & 2 deletions smtp.go
Original file line number Diff line number Diff line change
Expand Up @@ -244,11 +244,11 @@ func (c *smtpClient) mail(from string, extArgs ...map[string]string) error {
// rcpt issues a RCPT command to the server using the provided email address.
// A call to Rcpt must be preceded by a call to Mail and may be followed by
// a Data call or another Rcpt call.
func (c *smtpClient) rcpt(to string) error {
func (c *smtpClient) rcpt(to, dsn string) error {
if err := validateLine(to); err != nil {
return err
}
_, _, err := c.cmd(25, "RCPT TO:<%s>", to)
_, _, err := c.cmd(25, "RCPT TO:<%s>%s", to, dsn)
return err
}

Expand Down
9 changes: 5 additions & 4 deletions smtp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ func TestBasic(t *testing.T) {
t.Fatalf("AUTH failed: %s", err)
}

if err := c.rcpt("[email protected]>\r\nDATA\r\nInjected message body\r\n.\r\nQUIT\r\n"); err == nil {
if err := c.rcpt("[email protected]>\r\nDATA\r\nInjected message body\r\n.\r\nQUIT\r\n", ""); err == nil {
t.Fatalf("RCPT should have failed due to a message injection attempt")
}
if err := c.mail("[email protected]>\r\nDATA\r\nAnother injected message body\r\n.\r\nQUIT\r\n"); err == nil {
Expand All @@ -240,7 +240,7 @@ func TestBasic(t *testing.T) {
if err := c.mail("[email protected]"); err != nil {
t.Fatalf("MAIL failed: %s", err)
}
if err := c.rcpt("[email protected]"); err != nil {
if err := c.rcpt("[email protected]", ""); err != nil {
t.Fatalf("RCPT failed: %s", err)
}
msg := `From: [email protected]
Expand Down Expand Up @@ -1025,8 +1025,9 @@ func init() {
}

// localhostCert is a PEM-encoded TLS cert generated from src/crypto/tls:
// go run generate_cert.go --rsa-bits 1024 --host 127.0.0.1,::1,example.com \
// --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h
//
// go run generate_cert.go --rsa-bits 1024 --host 127.0.0.1,::1,example.com \
// --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h
var localhostCert = []byte(`
-----BEGIN CERTIFICATE-----
MIICFDCCAX2gAwIBAgIRAK0xjnaPuNDSreeXb+z+0u4wDQYJKoZIhvcNAQELBQAw
Expand Down

0 comments on commit b1fb802

Please sign in to comment.