From b1fb80242210c43e2546398d57257efce64c2c43 Mon Sep 17 00:00:00 2001 From: Ahmed Date: Fri, 4 Aug 2023 16:13:32 +0200 Subject: [PATCH] Add: Ability to specify delivery status notification (#87) * Add: Ability to specify delivery status notification when supported by the server. * refactor support for DSN --------- Co-authored-by: Santiago De la Cruz <51337247+xhit@users.noreply.github.com> --- README.md | 4 ++ email.go | 134 +++++++++++++++++++++++++++++++++++++++++++-------- smtp.go | 4 +- smtp_test.go | 9 ++-- 4 files changed, 124 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 54b58c9..98012e1 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 != "" { diff --git a/email.go b/email.go index 2949568..5da20dd 100644 --- a/email.go +++ b/email.go @@ -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 } /* @@ -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. @@ -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{ @@ -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 @@ -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) } @@ -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) } @@ -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 } } @@ -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 +} diff --git a/smtp.go b/smtp.go index b2187f3..41d136f 100644 --- a/smtp.go +++ b/smtp.go @@ -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 } diff --git a/smtp_test.go b/smtp_test.go index 54feead..2b2a284 100644 --- a/smtp_test.go +++ b/smtp_test.go @@ -231,7 +231,7 @@ func TestBasic(t *testing.T) { t.Fatalf("AUTH failed: %s", err) } - if err := c.rcpt("golang-nuts@googlegroups.com>\r\nDATA\r\nInjected message body\r\n.\r\nQUIT\r\n"); err == nil { + if err := c.rcpt("golang-nuts@googlegroups.com>\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("user@gmail.com>\r\nDATA\r\nAnother injected message body\r\n.\r\nQUIT\r\n"); err == nil { @@ -240,7 +240,7 @@ func TestBasic(t *testing.T) { if err := c.mail("user@gmail.com"); err != nil { t.Fatalf("MAIL failed: %s", err) } - if err := c.rcpt("golang-nuts@googlegroups.com"); err != nil { + if err := c.rcpt("golang-nuts@googlegroups.com", ""); err != nil { t.Fatalf("RCPT failed: %s", err) } msg := `From: user@gmail.com @@ -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