diff --git a/README.md b/README.md index 02c7124..1ec5605 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,7 @@ Some of the features of this library: * [X] Support for attachments and inline embeds (from file system, `io.Reader` or `embed.FS`) * [X] Support for different encodings * [X] Support sending mails via a local sendmail command +* [X] Support for requestng MDNs * [X] Message object satisfies `io.WriteTo` and `io.Reader` interfaces * [X] Support for Go's `html/template` and `text/template` (as message body, alternative part or attachment/emebed) * [X] Output to file support which allows storing mail messages as e. g. `.eml` files to disk to open them in a MUA diff --git a/header.go b/header.go index bb143ad..dfd2f32 100644 --- a/header.go +++ b/header.go @@ -37,6 +37,10 @@ const ( // See: https://www.rfc-editor.org/rfc/rfc822#section-5.1 HeaderDate Header = "Date" + // HeaderDispositionNotificationTo is the MDN header as described in RFC8098 + // See: https://www.rfc-editor.org/rfc/rfc8098.html#section-2.1 + HeaderDispositionNotificationTo Header = "Disposition-Notification-To" + // HeaderImportance represents the "Importance" field HeaderImportance Header = "Importance" diff --git a/msg.go b/msg.go index 7ec8dbb..f45ad29 100644 --- a/msg.go +++ b/msg.go @@ -37,6 +37,9 @@ const ( // errTplPointerNil is issued when a template pointer is expected but it is nil errTplPointerNil = "template pointer is nil" + + // errParseMailAddr is used when a mail address could not be validated + errParseMailAddr = "failed to parse mail address %q: %w" ) // Msg is the mail message struct @@ -175,7 +178,7 @@ func (m *Msg) SetAddrHeader(h AddrHeader, v ...string) error { for _, av := range v { a, err := mail.ParseAddress(av) if err != nil { - return fmt.Errorf("failed to parse mail address header %q: %w", av, err) + return fmt.Errorf(errParseMailAddr, av, err) } al = append(al, a) } @@ -382,6 +385,51 @@ func (m *Msg) SetUserAgent(a string) { m.SetHeader(HeaderXMailer, a) } +// RequestMDNTo adds the Disposition-Notification-To header to request a MDN from the receiving end +// as described in RFC8098. It allows to provide a list recipient addresses. +// Address validation is performed +// See: https://www.rfc-editor.org/rfc/rfc8098.html +func (m *Msg) RequestMDNTo(t ...string) error { + var tl []string + for _, at := range t { + a, err := mail.ParseAddress(at) + if err != nil { + return fmt.Errorf(errParseMailAddr, at, err) + } + tl = append(tl, a.String()) + } + m.genHeader[HeaderDispositionNotificationTo] = tl + return nil +} + +// RequestMDNToFormat adds the Disposition-Notification-To header to request a MDN from the receiving end +// as described in RFC8098. It allows to provide a recipient address with name and address and will format +// accordingly. Address validation is performed +// See: https://www.rfc-editor.org/rfc/rfc8098.html +func (m *Msg) RequestMDNToFormat(n, a string) error { + return m.RequestMDNTo(fmt.Sprintf(`%s <%s>`, n, a)) +} + +// RequestMDNAddTo adds an additional recipient to the recipient list of the MDN +func (m *Msg) RequestMDNAddTo(t string) error { + a, err := mail.ParseAddress(t) + if err != nil { + return fmt.Errorf(errParseMailAddr, t, err) + } + var tl []string + for _, cl := range m.genHeader[HeaderDispositionNotificationTo] { + tl = append(tl, cl) + } + tl = append(tl, a.String()) + m.genHeader[HeaderDispositionNotificationTo] = tl + return nil +} + +// RequestMDNAddToFormat adds an additional formated recipient to the recipient list of the MDN +func (m *Msg) RequestMDNAddToFormat(n, a string) error { + return m.RequestMDNAddTo(fmt.Sprintf(`"%s" <%s>`, n, a)) +} + // GetSender returns the currently set envelope FROM address. If no envelope FROM is set it will use // the first mail body FROM address. If ff is true, it will return the full address string including // the address name, if set diff --git a/msg_test.go b/msg_test.go index 14178e8..fae112b 100644 --- a/msg_test.go +++ b/msg_test.go @@ -927,6 +927,90 @@ func TestMsg_SetUserAgent(t *testing.T) { } } +// TestMsg_RequestMDN tests the different RequestMDN* related methods of Msg +func TestMsg_RequestMDN(t *testing.T) { + n := "Toni Tester" + n2 := "Melanie Tester" + v := "toni.tester@example.com" + v2 := "melanie.tester@example.com" + iv := "testertest.tld" + vl := []string{v, v2} + m := NewMsg() + + // Single valid address + if err := m.RequestMDNTo(v); err != nil { + t.Errorf("RequestMDNTo with a single valid address failed: %s", err) + } + if m.genHeader[HeaderDispositionNotificationTo][0] != fmt.Sprintf("<%s>", v) { + t.Errorf("RequestMDNTo with a single valid address failed. Expected: %s, got: %s", v, + m.genHeader[HeaderDispositionNotificationTo][0]) + } + m.Reset() + + // Multiples valid addresses + if err := m.RequestMDNTo(vl...); err != nil { + t.Errorf("RequestMDNTo with a multiple valid address failed: %s", err) + } + if m.genHeader[HeaderDispositionNotificationTo][0] != fmt.Sprintf("<%s>", v) { + t.Errorf("RequestMDNTo with a multiple valid addresses failed. Expected 0: %s, got 0: %s", v, + m.genHeader[HeaderDispositionNotificationTo][0]) + } + if m.genHeader[HeaderDispositionNotificationTo][1] != fmt.Sprintf("<%s>", v2) { + t.Errorf("RequestMDNTo with a multiple valid addresses failed. Expected 1: %s, got 1: %s", v2, + m.genHeader[HeaderDispositionNotificationTo][1]) + } + m.Reset() + + // Invalid address + if err := m.RequestMDNTo(iv); err == nil { + t.Errorf("RequestMDNTo with an invalid address was supposed to failed, but didn't") + } + m.Reset() + + // Single valid addresses + AddTo + if err := m.RequestMDNTo(v); err != nil { + t.Errorf("RequestMDNTo with a single valid address failed: %s", err) + } + if err := m.RequestMDNAddTo(v2); err != nil { + t.Errorf("RequestMDNAddTo with a valid address failed: %s", err) + } + if m.genHeader[HeaderDispositionNotificationTo][1] != fmt.Sprintf("<%s>", v2) { + t.Errorf("RequestMDNTo with a multiple valid addresses failed. Expected 1: %s, got 1: %s", v2, + m.genHeader[HeaderDispositionNotificationTo][1]) + } + m.Reset() + + // Single valid address formated + AddToFromat + if err := m.RequestMDNToFormat(n, v); err != nil { + t.Errorf("RequestMDNToFormat with a single valid address failed: %s", err) + } + if m.genHeader[HeaderDispositionNotificationTo][0] != fmt.Sprintf(`"%s" <%s>`, n, v) { + t.Errorf(`RequestMDNToFormat with a single valid address failed. Expected: "%s" <%s>, got: %s`, n, v, + m.genHeader[HeaderDispositionNotificationTo][0]) + } + if err := m.RequestMDNAddToFormat(n2, v2); err != nil { + t.Errorf("RequestMDNAddToFormat with a valid address failed: %s", err) + } + if m.genHeader[HeaderDispositionNotificationTo][1] != fmt.Sprintf(`"%s" <%s>`, n2, v2) { + t.Errorf(`RequestMDNAddToFormat with a single valid address failed. Expected: "%s" <%s>, got: %s`, n2, v2, + m.genHeader[HeaderDispositionNotificationTo][1]) + } + m.Reset() + + // Invalid formated address + if err := m.RequestMDNToFormat(n, iv); err == nil { + t.Errorf("RequestMDNToFormat with an invalid address was supposed to failed, but didn't") + } + + // Invalid address AddTo + AddToFormat + if err := m.RequestMDNAddTo(iv); err == nil { + t.Errorf("RequestMDNAddTo with an invalid address was supposed to failed, but didn't") + } + if err := m.RequestMDNAddToFormat(n, iv); err == nil { + t.Errorf("RequestMDNAddToFormat with an invalid address was supposed to failed, but didn't") + } +} + // TestMsg_SetBodyString tests the Msg.SetBodyString method func TestMsg_SetBodyString(t *testing.T) { tests := []struct {