Skip to content

Commit b1fb802

Browse files
Herz3hxhit
andauthored
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 <[email protected]>
1 parent 47471be commit b1fb802

File tree

4 files changed

+124
-27
lines changed

4 files changed

+124
-27
lines changed

README.md

+4
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ Go Simple Mail supports:
7878
- Support add a List-Unsubscribe header (since v2.11.0)
7979
- Support to add a DKIM signarure (since v2.11.0)
8080
- Support to send using custom connection, ideal for proxy (since v2.15.0)
81+
- Support Delivery Status Notification (DSN) (since v2.16.0)
8182

8283
## Documentation
8384

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

207+
// also you can set Delivery Status Notification (DSN) (only is set when server supports DSN)
208+
email.SetDSN([]mail.DSN{mail.SUCCESS, mail.FAILURE}, false)
209+
206210
// you can add dkim signature to the email.
207211
// to add dkim, you need a private key already created one.
208212
if privateKey != "" {

email.go

+113-21
Original file line numberDiff line numberDiff line change
@@ -18,22 +18,24 @@ import (
1818

1919
// Email represents an email message.
2020
type Email struct {
21-
from string
22-
sender string
23-
replyTo string
24-
returnPath string
25-
recipients []string
26-
headers textproto.MIMEHeader
27-
parts []part
28-
attachments []*File
29-
inlines []*File
30-
Charset string
31-
Encoding encoding
32-
Error error
33-
SMTPServer *smtpClient
34-
DkimMsg string
35-
AllowDuplicateAddress bool
36-
AddBccToHeader bool
21+
from string
22+
sender string
23+
replyTo string
24+
returnPath string
25+
recipients []string
26+
headers textproto.MIMEHeader
27+
parts []part
28+
attachments []*File
29+
inlines []*File
30+
Charset string
31+
Encoding encoding
32+
Error error
33+
SMTPServer *smtpClient
34+
DkimMsg string
35+
AllowDuplicateAddress bool
36+
AddBccToHeader bool
37+
preserveOriginalRecipient bool
38+
dsn []DSN
3739
}
3840

3941
/*
@@ -59,10 +61,13 @@ type SMTPServer struct {
5961

6062
// SMTPClient represents a SMTP Client for send email
6163
type SMTPClient struct {
62-
mu sync.Mutex
63-
Client *smtpClient
64-
KeepAlive bool
65-
SendTimeout time.Duration
64+
mu sync.Mutex
65+
Client *smtpClient
66+
SendTimeout time.Duration
67+
KeepAlive bool
68+
hasDSNExt bool
69+
preserveOriginalRecipient bool
70+
dsn []DSN
6671
}
6772

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

166+
/*
167+
DSN notifications
168+
169+
- 'NEVER' under no circumstances a DSN must be returned to the sender. If you use NEVER all other notifications will be ignored.
170+
171+
- 'SUCCESS' will notify you when your mail has arrived at its destination.
172+
173+
- 'FAILURE' will arrive if an error occurred during delivery.
174+
175+
- '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.
176+
177+
see https://tools.ietf.org/html/rfc3461 See section 4.1 for more information about NOTIFY
178+
*/
179+
type DSN int
180+
181+
const (
182+
NEVER DSN = iota
183+
FAILURE
184+
DELAY
185+
SUCCESS
186+
)
187+
188+
var dsnTypes = [...]string{"NEVER", "FAILURE", "DELAY", "SUCCESS"}
189+
190+
func (dsn DSN) String() string {
191+
return dsnTypes[dsn]
192+
}
193+
161194
// NewMSG creates a new email. It uses UTF-8 by default. All charsets: http://webcheatsheet.com/HTML/character_sets_list.php
162195
func NewMSG() *Email {
163196
email := &Email{
@@ -613,6 +646,20 @@ func (email *Email) AddAlternativeData(contentType ContentType, body []byte) *Em
613646
return email
614647
}
615648

649+
// SetDSN sets the delivery status notification list, only is set when SMTP server supports DSN extension
650+
//
651+
// To preserve the original recipient of an email message, for example, if it is forwarded to another address, set preserveOriginalRecipient to true
652+
func (email *Email) SetDSN(dsn []DSN, preserveOriginalRecipient bool) *Email {
653+
if email.Error != nil {
654+
return email
655+
}
656+
657+
email.dsn = dsn
658+
email.preserveOriginalRecipient = preserveOriginalRecipient
659+
660+
return email
661+
}
662+
616663
// GetFrom returns the sender of the email, if any
617664
func (email *Email) GetFrom() string {
618665
from := email.returnPath
@@ -710,6 +757,9 @@ func (email *Email) SendEnvelopeFrom(from string, client *SMTPClient) error {
710757
msg = email.GetMessage()
711758
}
712759

760+
client.dsn = email.dsn
761+
client.preserveOriginalRecipient = email.preserveOriginalRecipient
762+
713763
return send(from, email.recipients, msg, client)
714764
}
715765

@@ -864,10 +914,13 @@ func (server *SMTPServer) Connect() (*SMTPClient, error) {
864914
}
865915
}
866916

917+
_, hasDSN := c.ext["DSN"]
918+
867919
return &SMTPClient{
868920
Client: c,
869921
KeepAlive: server.KeepAlive,
870922
SendTimeout: server.SendTimeout,
923+
hasDSNExt: hasDSN,
871924
}, server.validateAuth(c)
872925
}
873926

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

1021+
var dsn string
1022+
var dsnSet bool
1023+
1024+
if c.hasDSNExt && len(c.dsn) > 0 {
1025+
dsn = " NOTIFY="
1026+
if hasNeverDSN(c.dsn) {
1027+
dsn += NEVER.String()
1028+
} else {
1029+
dsn += strings.Join(dsnToString(c.dsn), ",")
1030+
}
1031+
1032+
if c.preserveOriginalRecipient {
1033+
dsn += " ORCPT=rfc822;"
1034+
}
1035+
1036+
dsnSet = true
1037+
}
1038+
9681039
// Set the recipients
9691040
for _, address := range to {
970-
if err := c.Client.rcpt(address); err != nil {
1041+
if dsnSet && c.preserveOriginalRecipient {
1042+
dsn += address
1043+
}
1044+
1045+
if err := c.Client.rcpt(address, dsn); err != nil {
9711046
return err
9721047
}
9731048
}
@@ -1001,3 +1076,20 @@ func checkKeepAlive(client *SMTPClient) {
10011076
client.Close()
10021077
}
10031078
}
1079+
1080+
func hasNeverDSN(dsnList []DSN) bool {
1081+
for i := range dsnList {
1082+
if dsnList[i] == NEVER {
1083+
return true
1084+
}
1085+
}
1086+
return false
1087+
}
1088+
1089+
func dsnToString(dsnList []DSN) []string {
1090+
dsnString := make([]string, len(dsnList))
1091+
for i := range dsnList {
1092+
dsnString[i] = dsnList[i].String()
1093+
}
1094+
return dsnString
1095+
}

smtp.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -244,11 +244,11 @@ func (c *smtpClient) mail(from string, extArgs ...map[string]string) error {
244244
// rcpt issues a RCPT command to the server using the provided email address.
245245
// A call to Rcpt must be preceded by a call to Mail and may be followed by
246246
// a Data call or another Rcpt call.
247-
func (c *smtpClient) rcpt(to string) error {
247+
func (c *smtpClient) rcpt(to, dsn string) error {
248248
if err := validateLine(to); err != nil {
249249
return err
250250
}
251-
_, _, err := c.cmd(25, "RCPT TO:<%s>", to)
251+
_, _, err := c.cmd(25, "RCPT TO:<%s>%s", to, dsn)
252252
return err
253253
}
254254

smtp_test.go

+5-4
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ func TestBasic(t *testing.T) {
231231
t.Fatalf("AUTH failed: %s", err)
232232
}
233233

234-
if err := c.rcpt("[email protected]>\r\nDATA\r\nInjected message body\r\n.\r\nQUIT\r\n"); err == nil {
234+
if err := c.rcpt("[email protected]>\r\nDATA\r\nInjected message body\r\n.\r\nQUIT\r\n", ""); err == nil {
235235
t.Fatalf("RCPT should have failed due to a message injection attempt")
236236
}
237237
if err := c.mail("[email protected]>\r\nDATA\r\nAnother injected message body\r\n.\r\nQUIT\r\n"); err == nil {
@@ -240,7 +240,7 @@ func TestBasic(t *testing.T) {
240240
if err := c.mail("[email protected]"); err != nil {
241241
t.Fatalf("MAIL failed: %s", err)
242242
}
243-
if err := c.rcpt("[email protected]"); err != nil {
243+
if err := c.rcpt("[email protected]", ""); err != nil {
244244
t.Fatalf("RCPT failed: %s", err)
245245
}
246246
msg := `From: [email protected]
@@ -1025,8 +1025,9 @@ func init() {
10251025
}
10261026

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

0 commit comments

Comments
 (0)