Repository: jhillyerd/enmime
Pull request #288 (2023-04-15 23:51:51)

feat: add support for multiple Reply-To addresses
Hello @jhillyerd and enmime maintainers,

I've been working on a feature enhancement for the enmime library to add support for multiple reply-to addresses, as described in RFC 5322, section 3.6.2. I believe this functionality will improve the flexibility of the library and provide users with the ability to correctly handle multiple reply-to addresses as per the email specification.

Here is a brief overview of the changes I've made in this commit:

- Updated the ``MailBuilder`` struct to store multiple reply-to addresses.
- Modified the ``Build`` function to correctly write multiple reply-to addresses.
- Updated the ``GetReplyTo`` function to return the first email in the list for backward compatibility, and added a new ``GetReplyToAddrs`` function to return a slice copy of all addresses.
- Updated the existing ``ReplyTo`` function to support backward compatibility by handling single email addresses while using the new reply-to address list, and added a new ``ReplyToAddrs`` function to support setting the whole reply-to list.
- Added new tests to validate the proper handling of multiple reply-to addresses.

Thank you for your time and consideration. I'm looking forward to your feedback and the opportunity to contribute to this project.


Patch:
diff --git a/builder.go b/builder.go
index fe64311..25f4aee 100644
--- a/builder.go
+++ b/builder.go
@@ -22,7 +22,7 @@ import (
 type MailBuilder struct {
 	to, cc, bcc          []mail.Address
 	from                 mail.Address
-	replyTo              mail.Address
+	replyTo              []mail.Address
 	subject              string
 	date                 time.Time
 	header               textproto.MIMEHeader
@@ -153,13 +153,26 @@ func (p *MailBuilder) GetBCC() []mail.Address {
 // ReplyTo returns a copy of MailBuilder with this name & address appended to the To header.  name
 // may be empty.
 func (p MailBuilder) ReplyTo(name, addr string) MailBuilder {
-	p.replyTo = mail.Address{Name: name, Address: addr}
+	if len(addr) > 0 {
+		p.replyTo = append(p.replyTo, mail.Address{Name: name, Address: addr})
+	}
+	return p
+}
+
+// ReplyToAddrs returns a copy of MailBuilder with the new reply to header list. This method only
+// has an effect if the Send method is used to transmit the message, there is no effect on the parts
+// returned by Build().
+func (p MailBuilder) ReplyToAddrs(replyTo []mail.Address) MailBuilder {
+	p.replyTo = replyTo
 	return p
 }
 
-// GetReplyTo returns the stored replyTo header.
-func (p *MailBuilder) GetReplyTo() mail.Address {
-	return p.replyTo
+// GetReplyTo returns a copy of the stored replyTo header addresses.
+func (p *MailBuilder) GetReplyTo() []mail.Address {
+	replyTo := make([]mail.Address, len(p.replyTo))
+	copy(replyTo, p.replyTo)
+
+	return replyTo
 }
 
 // Header returns a copy of MailBuilder with the specified value added to the named header.
@@ -386,8 +399,8 @@ func (p MailBuilder) Build() (*Part, error) {
 	if len(p.cc) > 0 {
 		h.Set("Cc", stringutil.JoinAddress(p.cc))
 	}
-	if p.replyTo.Address != "" {
-		h.Set("Reply-To", p.replyTo.String())
+	if len(p.replyTo) > 0 {
+		h.Set("Reply-To", stringutil.JoinAddress(p.replyTo))
 	}
 	date := p.date
 	if date.IsZero() {


Test patch:
diff --git a/builder_test.go b/builder_test.go
index 437b0c2..bb0d52c 100644
--- a/builder_test.go
+++ b/builder_test.go
@@ -348,6 +348,12 @@ func TestBuilderReplyTo(t *testing.T) {
 		t.Error("Same ReplyTo(value) should be equal")
 	}
 
+	if !reflect.DeepEqual(a.GetReplyTo(), b.GetReplyTo()) {
+		t.Error("Same GetReplyTo() should be equal")
+	} else if len(a.GetReplyTo()) == 0 {
+		t.Error("GetReplyTo() shouldn't be empty")
+	}
+
 	a = enmime.Builder().ReplyTo("name", "foo")
 	b = enmime.Builder().ReplyTo("name", "bar")
 	if a.Equals(b) {
@@ -360,6 +366,12 @@ func TestBuilderReplyTo(t *testing.T) {
 		t.Error("ReplyTo() should not mutate receiver, failed")
 	}
 
+	a = enmime.Builder().ReplyToAddrs([]mail.Address{{Name: "name", Address: "foo"}})
+	b = a.ReplyToAddrs([]mail.Address{{Name: "name", Address: "bar"}})
+	if a.Equals(b) {
+		t.Error("ReplyToAddrs() should not mutate receiver, failed")
+	}
+
 	a = enmime.Builder().ToAddrs(addrSlice).From("name", "foo").Subject("foo")
 	a = a.ReplyTo("one", "one@inbucket.org")
 	want := "\"one\" <one@inbucket.org>"
@@ -371,6 +383,21 @@ func TestBuilderReplyTo(t *testing.T) {
 	if got != want {
 		t.Errorf("Reply-To: %q, want: %q", got, want)
 	}
+
+	input := []mail.Address{
+		{Name: "one", Address: "one@inbucket.org"},
+		{Name: "two", Address: "two@inbucket.org"},
+	}
+	a = enmime.Builder().ReplyToAddrs(input).ToAddrs(input).From("name", "foo").Subject("foo")
+	want = "\"one\" <one@inbucket.org>, \"two\" <two@inbucket.org>"
+	p, err = a.Build()
+	if err != nil {
+		t.Fatal(err)
+	}
+	got = p.Header.Get("Reply-To")
+	if got != want {
+		t.Errorf("Reply-To: %q, want: %q", got, want)
+	}
 }
 
 func TestBuilderText(t *testing.T) {