From 8c774c0372c289c6838565fcbba33c8d72904b94 Mon Sep 17 00:00:00 2001 From: Ahmed El Moden Date: Thu, 3 Aug 2023 16:47:06 +0200 Subject: [PATCH 1/2] Add: Ability to specify delivery status notification when supported by the server. --- email.go | 33 +++++++++++++++++++++++++++++++-- smtp.go | 2 +- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/email.go b/email.go index 2949568..e5833ed 100644 --- a/email.go +++ b/email.go @@ -34,6 +34,7 @@ type Email struct { DkimMsg string AllowDuplicateAddress bool AddBccToHeader bool + dsn string } /* @@ -613,6 +614,18 @@ func (email *Email) AddAlternativeData(contentType ContentType, body []byte) *Em return email } +// SetDSN sets the delivery status notification +func (email *Email) SetDSN(dsn string) *Email { + if email.Error != nil { + return email + } + + email.dsn = dsn + + return email +} + + // GetFrom returns the sender of the email, if any func (email *Email) GetFrom() string { from := email.returnPath @@ -699,7 +712,7 @@ func (email *Email) SendEnvelopeFrom(from string, client *SMTPClient) error { from = email.from } - if len(email.recipients) < 1 { + if len(email.getRecipientsWithDsn(client)) < 1 { return errors.New("Mail Error: No recipient specified") } @@ -710,7 +723,7 @@ func (email *Email) SendEnvelopeFrom(from string, client *SMTPClient) error { msg = email.GetMessage() } - return send(from, email.recipients, msg, client) + return send(from, email.getRecipientsWithDsn(client), msg, client) } // dial connects to the smtp server with the request encryption type @@ -1001,3 +1014,19 @@ func checkKeepAlive(client *SMTPClient) { client.Close() } } + +// GetRecipientsWithDSN returns a slice of recipients emails with DSN appended to the end +func (email *Email) getRecipientsWithDsn(client *SMTPClient) []string { + _, foundDSNExtension := client.Client.ext["DSN"] + if !foundDSNExtension || email.headers.Get("Return-Receipt-To") == "" { + return email.recipients + } + + recipientsWithDSN := make([]string, len(email.recipients)) + for i, recipient := range email.recipients { + recipientsWithDSN[i] = recipient + " " + email.dsn + } + + return recipientsWithDSN +} + diff --git a/smtp.go b/smtp.go index b2187f3..e967765 100644 --- a/smtp.go +++ b/smtp.go @@ -248,7 +248,7 @@ func (c *smtpClient) rcpt(to 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", to) return err } From 720579663ec3915abb6b5f4373262fe83125e56c Mon Sep 17 00:00:00 2001 From: Santiago De la Cruz <51337247+xhit@users.noreply.github.com> Date: Thu, 3 Aug 2023 13:17:47 -0400 Subject: [PATCH 2/2] refactor support for DSN --- README.md | 4 ++ email.go | 139 +++++++++++++++++++++++++++++++++++++-------------- smtp.go | 4 +- smtp_test.go | 9 ++-- 4 files changed, 112 insertions(+), 44 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 e5833ed..5da20dd 100644 --- a/email.go +++ b/email.go @@ -18,23 +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 - dsn string + 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 } /* @@ -60,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. @@ -159,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{ @@ -614,18 +646,20 @@ func (email *Email) AddAlternativeData(contentType ContentType, body []byte) *Em return email } -// SetDSN sets the delivery status notification -func (email *Email) SetDSN(dsn string) *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 @@ -712,7 +746,7 @@ func (email *Email) SendEnvelopeFrom(from string, client *SMTPClient) error { from = email.from } - if len(email.getRecipientsWithDsn(client)) < 1 { + if len(email.recipients) < 1 { return errors.New("Mail Error: No recipient specified") } @@ -723,7 +757,10 @@ func (email *Email) SendEnvelopeFrom(from string, client *SMTPClient) error { msg = email.GetMessage() } - return send(from, email.getRecipientsWithDsn(client), msg, client) + client.dsn = email.dsn + client.preserveOriginalRecipient = email.preserveOriginalRecipient + + return send(from, email.recipients, msg, client) } // dial connects to the smtp server with the request encryption type @@ -877,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) } @@ -978,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 } } @@ -1015,18 +1077,19 @@ func checkKeepAlive(client *SMTPClient) { } } -// GetRecipientsWithDSN returns a slice of recipients emails with DSN appended to the end -func (email *Email) getRecipientsWithDsn(client *SMTPClient) []string { - _, foundDSNExtension := client.Client.ext["DSN"] - if !foundDSNExtension || email.headers.Get("Return-Receipt-To") == "" { - return email.recipients +func hasNeverDSN(dsnList []DSN) bool { + for i := range dsnList { + if dsnList[i] == NEVER { + return true + } } + return false +} - recipientsWithDSN := make([]string, len(email.recipients)) - for i, recipient := range email.recipients { - recipientsWithDSN[i] = recipient + " " + email.dsn +func dsnToString(dsnList []DSN) []string { + dsnString := make([]string, len(dsnList)) + for i := range dsnList { + dsnString[i] = dsnList[i].String() } - - return recipientsWithDSN + return dsnString } - diff --git a/smtp.go b/smtp.go index e967765..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