Added support for requesting MDNs as described in RFC 8098

This commit is contained in:
Winni Neessen 2022-09-09 11:35:45 +02:00
parent da266bcac4
commit 46001dc691
Signed by: wneessen
GPG key ID: 385AC9889632126E
4 changed files with 138 additions and 1 deletions

View file

@ -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

View file

@ -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"

50
msg.go
View file

@ -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

View file

@ -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 {