Skip to content

Add: Ability to specify delivery status notification #87

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Aug 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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