Source file src/net/mail/message_test.go

     1  // Copyright 2011 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package mail
     6  
     7  import (
     8  	"bytes"
     9  	"io"
    10  	"mime"
    11  	"reflect"
    12  	"slices"
    13  	"strings"
    14  	"testing"
    15  	"time"
    16  )
    17  
    18  var parseTests = []struct {
    19  	in     string
    20  	header Header
    21  	body   string
    22  }{
    23  	{
    24  		// RFC 5322, Appendix A.1.1
    25  		in: `From: John Doe <jdoe@machine.example>
    26  To: Mary Smith <mary@example.net>
    27  Subject: Saying Hello
    28  Date: Fri, 21 Nov 1997 09:55:06 -0600
    29  Message-ID: <1234@local.machine.example>
    30  
    31  This is a message just to say hello.
    32  So, "Hello".
    33  `,
    34  		header: Header{
    35  			"From":       []string{"John Doe <jdoe@machine.example>"},
    36  			"To":         []string{"Mary Smith <mary@example.net>"},
    37  			"Subject":    []string{"Saying Hello"},
    38  			"Date":       []string{"Fri, 21 Nov 1997 09:55:06 -0600"},
    39  			"Message-Id": []string{"<1234@local.machine.example>"},
    40  		},
    41  		body: "This is a message just to say hello.\nSo, \"Hello\".\n",
    42  	},
    43  	{
    44  		// RFC 5965, Appendix B.1, a part of the multipart message (a header-only sub message)
    45  		in: `Feedback-Type: abuse
    46  User-Agent: SomeGenerator/1.0
    47  Version: 1
    48  `,
    49  		header: Header{
    50  			"Feedback-Type": []string{"abuse"},
    51  			"User-Agent":    []string{"SomeGenerator/1.0"},
    52  			"Version":       []string{"1"},
    53  		},
    54  		body: "",
    55  	},
    56  	{
    57  		// RFC 5322 permits any printable ASCII character,
    58  		// except colon, in a header key. Issue #58862.
    59  		in: `From: iant@golang.org
    60  Custom/Header: v
    61  
    62  Body
    63  `,
    64  		header: Header{
    65  			"From":          []string{"iant@golang.org"},
    66  			"Custom/Header": []string{"v"},
    67  		},
    68  		body: "Body\n",
    69  	},
    70  	{
    71  		// RFC 4155 mbox format. We've historically permitted this,
    72  		// so we continue to permit it. Issue #60332.
    73  		in: `From iant@golang.org Mon Jun 19 00:00:00 2023
    74  From: iant@golang.org
    75  
    76  Hello, gophers!
    77  `,
    78  		header: Header{
    79  			"From":                               []string{"iant@golang.org"},
    80  			"From iant@golang.org Mon Jun 19 00": []string{"00:00 2023"},
    81  		},
    82  		body: "Hello, gophers!\n",
    83  	},
    84  }
    85  
    86  func TestParsing(t *testing.T) {
    87  	for i, test := range parseTests {
    88  		msg, err := ReadMessage(bytes.NewBuffer([]byte(test.in)))
    89  		if err != nil {
    90  			t.Errorf("test #%d: Failed parsing message: %v", i, err)
    91  			continue
    92  		}
    93  		if !headerEq(msg.Header, test.header) {
    94  			t.Errorf("test #%d: Incorrectly parsed message header.\nGot:\n%+v\nWant:\n%+v",
    95  				i, msg.Header, test.header)
    96  		}
    97  		body, err := io.ReadAll(msg.Body)
    98  		if err != nil {
    99  			t.Errorf("test #%d: Failed reading body: %v", i, err)
   100  			continue
   101  		}
   102  		bodyStr := string(body)
   103  		if bodyStr != test.body {
   104  			t.Errorf("test #%d: Incorrectly parsed message body.\nGot:\n%+v\nWant:\n%+v",
   105  				i, bodyStr, test.body)
   106  		}
   107  	}
   108  }
   109  
   110  func headerEq(a, b Header) bool {
   111  	if len(a) != len(b) {
   112  		return false
   113  	}
   114  	for k, as := range a {
   115  		bs, ok := b[k]
   116  		if !ok {
   117  			return false
   118  		}
   119  		if !slices.Equal(as, bs) {
   120  			return false
   121  		}
   122  	}
   123  	return true
   124  }
   125  
   126  func TestDateParsing(t *testing.T) {
   127  	tests := []struct {
   128  		dateStr string
   129  		exp     time.Time
   130  	}{
   131  		// RFC 5322, Appendix A.1.1
   132  		{
   133  			"Fri, 21 Nov 1997 09:55:06 -0600",
   134  			time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)),
   135  		},
   136  		// RFC 5322, Appendix A.6.2
   137  		// Obsolete date.
   138  		{
   139  			"21 Nov 97 09:55:06 GMT",
   140  			time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("GMT", 0)),
   141  		},
   142  		// Commonly found format not specified by RFC 5322.
   143  		{
   144  			"Fri, 21 Nov 1997 09:55:06 -0600 (MDT)",
   145  			time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)),
   146  		},
   147  		{
   148  			"Thu, 20 Nov 1997 09:55:06 -0600 (MDT)",
   149  			time.Date(1997, 11, 20, 9, 55, 6, 0, time.FixedZone("", -6*60*60)),
   150  		},
   151  		{
   152  			"Thu, 20 Nov 1997 09:55:06 GMT (GMT)",
   153  			time.Date(1997, 11, 20, 9, 55, 6, 0, time.UTC),
   154  		},
   155  		{
   156  			"Fri, 21 Nov 1997 09:55:06 +1300 (TOT)",
   157  			time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", +13*60*60)),
   158  		},
   159  	}
   160  	for _, test := range tests {
   161  		hdr := Header{
   162  			"Date": []string{test.dateStr},
   163  		}
   164  		date, err := hdr.Date()
   165  		if err != nil {
   166  			t.Errorf("Header(Date: %s).Date(): %v", test.dateStr, err)
   167  		} else if !date.Equal(test.exp) {
   168  			t.Errorf("Header(Date: %s).Date() = %+v, want %+v", test.dateStr, date, test.exp)
   169  		}
   170  
   171  		date, err = ParseDate(test.dateStr)
   172  		if err != nil {
   173  			t.Errorf("ParseDate(%s): %v", test.dateStr, err)
   174  		} else if !date.Equal(test.exp) {
   175  			t.Errorf("ParseDate(%s) = %+v, want %+v", test.dateStr, date, test.exp)
   176  		}
   177  	}
   178  }
   179  
   180  func TestDateParsingCFWS(t *testing.T) {
   181  	tests := []struct {
   182  		dateStr string
   183  		exp     time.Time
   184  		valid   bool
   185  	}{
   186  		// FWS-only. No date.
   187  		{
   188  			"   ",
   189  			// nil is not allowed
   190  			time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)),
   191  			false,
   192  		},
   193  		// FWS is allowed before optional day of week.
   194  		{
   195  			"   Fri, 21 Nov 1997 09:55:06 -0600",
   196  			time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)),
   197  			true,
   198  		},
   199  		{
   200  			"21 Nov 1997 09:55:06 -0600",
   201  			time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)),
   202  			true,
   203  		},
   204  		{
   205  			"Fri 21 Nov 1997 09:55:06 -0600",
   206  			time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)),
   207  			false, // missing ,
   208  		},
   209  		// FWS is allowed before day of month but HTAB fails.
   210  		{
   211  			"Fri,        21 Nov 1997 09:55:06 -0600",
   212  			time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)),
   213  			true,
   214  		},
   215  		// FWS is allowed before and after year but HTAB fails.
   216  		{
   217  			"Fri, 21 Nov       1997     09:55:06 -0600",
   218  			time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)),
   219  			true,
   220  		},
   221  		// FWS is allowed before zone but HTAB is not handled. Obsolete timezone is handled.
   222  		{
   223  			"Fri, 21 Nov 1997 09:55:06           CST",
   224  			time.Time{},
   225  			true,
   226  		},
   227  		// FWS is allowed after date and a CRLF is already replaced.
   228  		{
   229  			"Fri, 21 Nov 1997 09:55:06           CST (no leading FWS and a trailing CRLF) \r\n",
   230  			time.Time{},
   231  			true,
   232  		},
   233  		// CFWS is a reduced set of US-ASCII where space and accentuated are obsolete. No error.
   234  		{
   235  			"Fri, 21    Nov 1997    09:55:06 -0600 (MDT and non-US-ASCII signs éèç )",
   236  			time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)),
   237  			true,
   238  		},
   239  		// CFWS is allowed after zone including a nested comment.
   240  		// Trailing FWS is allowed.
   241  		{
   242  			"Fri, 21 Nov 1997 09:55:06 -0600    \r\n (thisisa(valid)cfws)   \t ",
   243  			time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)),
   244  			true,
   245  		},
   246  		// CRLF is incomplete and misplaced.
   247  		{
   248  			"Fri, 21 Nov 1997 \r 09:55:06 -0600    \r\n (thisisa(valid)cfws)   \t ",
   249  			time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)),
   250  			false,
   251  		},
   252  		// CRLF is complete but misplaced. No error is returned.
   253  		{
   254  			"Fri, 21 Nov 199\r\n7  09:55:06 -0600    \r\n (thisisa(valid)cfws)   \t ",
   255  			time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)),
   256  			true, // should be false in the strict interpretation of RFC 5322.
   257  		},
   258  		// Invalid ASCII in date.
   259  		{
   260  			"Fri, 21 Nov 1997 ù 09:55:06 -0600    \r\n (thisisa(valid)cfws)   \t ",
   261  			time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)),
   262  			false,
   263  		},
   264  		// CFWS chars () in date.
   265  		{
   266  			"Fri, 21 Nov () 1997 09:55:06 -0600    \r\n (thisisa(valid)cfws)   \t ",
   267  			time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)),
   268  			false,
   269  		},
   270  		// Timezone is invalid but T is found in comment.
   271  		{
   272  			"Fri, 21 Nov 1997 09:55:06 -060    \r\n (Thisisa(valid)cfws)   \t ",
   273  			time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)),
   274  			false,
   275  		},
   276  		// Date has no month.
   277  		{
   278  			"Fri, 21  1997 09:55:06 -0600",
   279  			time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)),
   280  			false,
   281  		},
   282  		// Invalid month : OCT iso Oct
   283  		{
   284  			"Fri, 21 OCT 1997 09:55:06 CST",
   285  			time.Time{},
   286  			false,
   287  		},
   288  		// A too short time zone.
   289  		{
   290  			"Fri, 21 Nov 1997 09:55:06 -060",
   291  			time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)),
   292  			false,
   293  		},
   294  		// A too short obsolete time zone.
   295  		{
   296  			"Fri, 21  1997 09:55:06 GT",
   297  			time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)),
   298  			false,
   299  		},
   300  		// Ensure that the presence of "T" in the date
   301  		// doesn't trip out ParseDate, as per issue 39260.
   302  		{
   303  			"Tue, 26 May 2020 14:04:40 GMT",
   304  			time.Date(2020, 05, 26, 14, 04, 40, 0, time.UTC),
   305  			true,
   306  		},
   307  		{
   308  			"Tue, 26 May 2020 14:04:40 UT",
   309  			time.Date(2020, 05, 26, 14, 04, 40, 0, time.UTC),
   310  			true,
   311  		},
   312  		{
   313  			"Thu, 21 May 2020 14:04:40 UT",
   314  			time.Date(2020, 05, 21, 14, 04, 40, 0, time.UTC),
   315  			true,
   316  		},
   317  		{
   318  			"Tue, 26 May 2020 14:04:40 XT",
   319  			time.Date(2020, 05, 26, 14, 04, 40, 0, time.UTC),
   320  			false,
   321  		},
   322  		{
   323  			"Thu, 21 May 2020 14:04:40 XT",
   324  			time.Date(2020, 05, 21, 14, 04, 40, 0, time.UTC),
   325  			false,
   326  		},
   327  		{
   328  			"Thu, 21 May 2020 14:04:40 UTC",
   329  			time.Date(2020, 05, 21, 14, 04, 40, 0, time.UTC),
   330  			true,
   331  		},
   332  		{
   333  			"Fri, 21 Nov 1997 09:55:06 GMT (GMT)",
   334  			time.Date(1997, 11, 21, 9, 55, 6, 0, time.UTC),
   335  			true,
   336  		},
   337  	}
   338  	for _, test := range tests {
   339  		hdr := Header{
   340  			"Date": []string{test.dateStr},
   341  		}
   342  		date, err := hdr.Date()
   343  		if err != nil && test.valid {
   344  			t.Errorf("Header(Date: %s).Date(): %v", test.dateStr, err)
   345  		} else if err == nil && test.exp.IsZero() {
   346  			// OK.  Used when exact result depends on the
   347  			// system's local zoneinfo.
   348  		} else if err == nil && !date.Equal(test.exp) && test.valid {
   349  			t.Errorf("Header(Date: %s).Date() = %+v, want %+v", test.dateStr, date, test.exp)
   350  		} else if err == nil && !test.valid { // an invalid expression was tested
   351  			t.Errorf("Header(Date: %s).Date() did not return an error but %v", test.dateStr, date)
   352  		}
   353  
   354  		date, err = ParseDate(test.dateStr)
   355  		if err != nil && test.valid {
   356  			t.Errorf("ParseDate(%s): %v", test.dateStr, err)
   357  		} else if err == nil && test.exp.IsZero() {
   358  			// OK.  Used when exact result depends on the
   359  			// system's local zoneinfo.
   360  		} else if err == nil && !test.valid { // an invalid expression was tested
   361  			t.Errorf("ParseDate(%s) did not return an error but %v", test.dateStr, date)
   362  		} else if err == nil && test.valid && !date.Equal(test.exp) {
   363  			t.Errorf("ParseDate(%s) = %+v, want %+v", test.dateStr, date, test.exp)
   364  		}
   365  	}
   366  }
   367  
   368  func TestAddressParsingError(t *testing.T) {
   369  	mustErrTestCases := [...]struct {
   370  		text        string
   371  		wantErrText string
   372  	}{
   373  		0:  {"=?iso-8859-2?Q?Bogl=E1rka_Tak=E1cs?= <unknown@gmail.com>", "charset not supported"},
   374  		1:  {"a@gmail.com b@gmail.com", "expected single address"},
   375  		2:  {string([]byte{0xed, 0xa0, 0x80}) + " <micro@example.net>", "invalid utf-8 in address"},
   376  		3:  {"\"" + string([]byte{0xed, 0xa0, 0x80}) + "\" <half-surrogate@example.com>", "invalid utf-8 in quoted-string"},
   377  		4:  {"\"\\" + string([]byte{0x80}) + "\" <escaped-invalid-unicode@example.net>", "invalid utf-8 in quoted-string"},
   378  		5:  {"\"\x00\" <null@example.net>", "bad character in quoted-string"},
   379  		6:  {"\"\\\x00\" <escaped-null@example.net>", "bad character in quoted-string"},
   380  		7:  {"John Doe", "no angle-addr"},
   381  		8:  {`<jdoe#machine.example>`, "missing @ in addr-spec"},
   382  		9:  {`John <middle> Doe <jdoe@machine.example>`, "missing @ in addr-spec"},
   383  		10: {"cfws@example.com (", "misformatted parenthetical comment"},
   384  		11: {"empty group: ;", "empty group"},
   385  		12: {"root group: embed group: null@example.com;", "no angle-addr"},
   386  		13: {"group not closed: null@example.com", "expected comma"},
   387  		14: {"group: first@example.com, second@example.com;", "group with multiple addresses"},
   388  		15: {"john.doe", "missing '@' or angle-addr"},
   389  		16: {"john.doe@", "missing '@' or angle-addr"},
   390  		17: {"John Doe@foo.bar", "no angle-addr"},
   391  		18: {" group: null@example.com; (asd", "misformatted parenthetical comment"},
   392  		19: {" group: ; (asd", "misformatted parenthetical comment"},
   393  		20: {`(John) Doe <jdoe@machine.example>`, "missing word in phrase:"},
   394  		21: {"<jdoe@[" + string([]byte{0xed, 0xa0, 0x80}) + "192.168.0.1]>", "invalid utf-8 in domain-literal"},
   395  		22: {"<jdoe@[[192.168.0.1]>", "bad character in domain-literal"},
   396  		23: {"<jdoe@[192.168.0.1>", "unclosed domain-literal"},
   397  		24: {"<jdoe@[256.0.0.1]>", "invalid IP address in domain-literal"},
   398  	}
   399  
   400  	for i, tc := range mustErrTestCases {
   401  		_, err := ParseAddress(tc.text)
   402  		if err == nil || !strings.Contains(err.Error(), tc.wantErrText) {
   403  			t.Errorf(`mail.ParseAddress(%q) #%d want %q, got %v`, tc.text, i, tc.wantErrText, err)
   404  		}
   405  	}
   406  
   407  	t.Run("CustomWordDecoder", func(t *testing.T) {
   408  		p := &AddressParser{WordDecoder: &mime.WordDecoder{}}
   409  		for i, tc := range mustErrTestCases {
   410  			_, err := p.Parse(tc.text)
   411  			if err == nil || !strings.Contains(err.Error(), tc.wantErrText) {
   412  				t.Errorf(`p.Parse(%q) #%d want %q, got %v`, tc.text, i, tc.wantErrText, err)
   413  			}
   414  		}
   415  	})
   416  
   417  }
   418  
   419  func TestAddressParsing(t *testing.T) {
   420  	tests := []struct {
   421  		addrsStr string
   422  		exp      []*Address
   423  	}{
   424  		// Bare address
   425  		{
   426  			`jdoe@machine.example`,
   427  			[]*Address{{
   428  				Address: "jdoe@machine.example",
   429  			}},
   430  		},
   431  		// RFC 5322, Appendix A.1.1
   432  		{
   433  			`John Doe <jdoe@machine.example>`,
   434  			[]*Address{{
   435  				Name:    "John Doe",
   436  				Address: "jdoe@machine.example",
   437  			}},
   438  		},
   439  		// RFC 5322, Appendix A.1.2
   440  		{
   441  			`"Joe Q. Public" <john.q.public@example.com>`,
   442  			[]*Address{{
   443  				Name:    "Joe Q. Public",
   444  				Address: "john.q.public@example.com",
   445  			}},
   446  		},
   447  		// Comment in display name
   448  		{
   449  			`John (middle) Doe <jdoe@machine.example>`,
   450  			[]*Address{{
   451  				Name:    "John Doe",
   452  				Address: "jdoe@machine.example",
   453  			}},
   454  		},
   455  		// Display name is quoted string, so comment is not a comment
   456  		{
   457  			`"John (middle) Doe" <jdoe@machine.example>`,
   458  			[]*Address{{
   459  				Name:    "John (middle) Doe",
   460  				Address: "jdoe@machine.example",
   461  			}},
   462  		},
   463  		{
   464  			`"John <middle> Doe" <jdoe@machine.example>`,
   465  			[]*Address{{
   466  				Name:    "John <middle> Doe",
   467  				Address: "jdoe@machine.example",
   468  			}},
   469  		},
   470  		{
   471  			`Mary Smith <mary@x.test>, jdoe@example.org, Who? <one@y.test>`,
   472  			[]*Address{
   473  				{
   474  					Name:    "Mary Smith",
   475  					Address: "mary@x.test",
   476  				},
   477  				{
   478  					Address: "jdoe@example.org",
   479  				},
   480  				{
   481  					Name:    "Who?",
   482  					Address: "one@y.test",
   483  				},
   484  			},
   485  		},
   486  		{
   487  			`<boss@nil.test>, "Giant; \"Big\" Box" <sysservices@example.net>`,
   488  			[]*Address{
   489  				{
   490  					Address: "boss@nil.test",
   491  				},
   492  				{
   493  					Name:    `Giant; "Big" Box`,
   494  					Address: "sysservices@example.net",
   495  				},
   496  			},
   497  		},
   498  		// RFC 5322, Appendix A.6.1
   499  		{
   500  			`Joe Q. Public <john.q.public@example.com>`,
   501  			[]*Address{{
   502  				Name:    "Joe Q. Public",
   503  				Address: "john.q.public@example.com",
   504  			}},
   505  		},
   506  		// RFC 5322, Appendix A.1.3
   507  		{
   508  			`group1: groupaddr1@example.com;`,
   509  			[]*Address{
   510  				{
   511  					Name:    "",
   512  					Address: "groupaddr1@example.com",
   513  				},
   514  			},
   515  		},
   516  		{
   517  			`empty group: ;`,
   518  			[]*Address(nil),
   519  		},
   520  		{
   521  			`A Group:Ed Jones <c@a.test>,joe@where.test,John <jdoe@one.test>;`,
   522  			[]*Address{
   523  				{
   524  					Name:    "Ed Jones",
   525  					Address: "c@a.test",
   526  				},
   527  				{
   528  					Name:    "",
   529  					Address: "joe@where.test",
   530  				},
   531  				{
   532  					Name:    "John",
   533  					Address: "jdoe@one.test",
   534  				},
   535  			},
   536  		},
   537  		// RFC5322 4.4 obs-addr-list
   538  		{
   539  			` , joe@where.test,,John <jdoe@one.test>,`,
   540  			[]*Address{
   541  				{
   542  					Name:    "",
   543  					Address: "joe@where.test",
   544  				},
   545  				{
   546  					Name:    "John",
   547  					Address: "jdoe@one.test",
   548  				},
   549  			},
   550  		},
   551  		{
   552  			` , joe@where.test,,John <jdoe@one.test>,,`,
   553  			[]*Address{
   554  				{
   555  					Name:    "",
   556  					Address: "joe@where.test",
   557  				},
   558  				{
   559  					Name:    "John",
   560  					Address: "jdoe@one.test",
   561  				},
   562  			},
   563  		},
   564  		{
   565  			`Group1: <addr1@example.com>;, Group 2: addr2@example.com;, John <addr3@example.com>`,
   566  			[]*Address{
   567  				{
   568  					Name:    "",
   569  					Address: "addr1@example.com",
   570  				},
   571  				{
   572  					Name:    "",
   573  					Address: "addr2@example.com",
   574  				},
   575  				{
   576  					Name:    "John",
   577  					Address: "addr3@example.com",
   578  				},
   579  			},
   580  		},
   581  		// RFC 2047 "Q"-encoded ISO-8859-1 address.
   582  		{
   583  			`=?iso-8859-1?q?J=F6rg_Doe?= <joerg@example.com>`,
   584  			[]*Address{
   585  				{
   586  					Name:    `Jörg Doe`,
   587  					Address: "joerg@example.com",
   588  				},
   589  			},
   590  		},
   591  		// RFC 2047 "Q"-encoded US-ASCII address. Dumb but legal.
   592  		{
   593  			`=?us-ascii?q?J=6Frg_Doe?= <joerg@example.com>`,
   594  			[]*Address{
   595  				{
   596  					Name:    `Jorg Doe`,
   597  					Address: "joerg@example.com",
   598  				},
   599  			},
   600  		},
   601  		// RFC 2047 "Q"-encoded UTF-8 address.
   602  		{
   603  			`=?utf-8?q?J=C3=B6rg_Doe?= <joerg@example.com>`,
   604  			[]*Address{
   605  				{
   606  					Name:    `Jörg Doe`,
   607  					Address: "joerg@example.com",
   608  				},
   609  			},
   610  		},
   611  		// RFC 2047 "Q"-encoded UTF-8 address with multiple encoded-words.
   612  		{
   613  			`=?utf-8?q?J=C3=B6rg?=  =?utf-8?q?Doe?= <joerg@example.com>`,
   614  			[]*Address{
   615  				{
   616  					Name:    `JörgDoe`,
   617  					Address: "joerg@example.com",
   618  				},
   619  			},
   620  		},
   621  		// RFC 2047, Section 8.
   622  		{
   623  			`=?ISO-8859-1?Q?Andr=E9?= Pirard <PIRARD@vm1.ulg.ac.be>`,
   624  			[]*Address{
   625  				{
   626  					Name:    `André Pirard`,
   627  					Address: "PIRARD@vm1.ulg.ac.be",
   628  				},
   629  			},
   630  		},
   631  		// Custom example of RFC 2047 "B"-encoded ISO-8859-1 address.
   632  		{
   633  			`=?ISO-8859-1?B?SvZyZw==?= <joerg@example.com>`,
   634  			[]*Address{
   635  				{
   636  					Name:    `Jörg`,
   637  					Address: "joerg@example.com",
   638  				},
   639  			},
   640  		},
   641  		// Custom example of RFC 2047 "B"-encoded UTF-8 address.
   642  		{
   643  			`=?UTF-8?B?SsO2cmc=?= <joerg@example.com>`,
   644  			[]*Address{
   645  				{
   646  					Name:    `Jörg`,
   647  					Address: "joerg@example.com",
   648  				},
   649  			},
   650  		},
   651  		// Custom example with "." in name. For issue 4938
   652  		{
   653  			`Asem H. <noreply@example.com>`,
   654  			[]*Address{
   655  				{
   656  					Name:    `Asem H.`,
   657  					Address: "noreply@example.com",
   658  				},
   659  			},
   660  		},
   661  		// RFC 6532 3.2.3, qtext /= UTF8-non-ascii
   662  		{
   663  			`"Gø Pher" <gopher@example.com>`,
   664  			[]*Address{
   665  				{
   666  					Name:    `Gø Pher`,
   667  					Address: "gopher@example.com",
   668  				},
   669  			},
   670  		},
   671  		// RFC 6532 3.2, atext /= UTF8-non-ascii
   672  		{
   673  			`µ <micro@example.com>`,
   674  			[]*Address{
   675  				{
   676  					Name:    `µ`,
   677  					Address: "micro@example.com",
   678  				},
   679  			},
   680  		},
   681  		// RFC 6532 3.2.2, local address parts allow UTF-8
   682  		{
   683  			`Micro <µ@example.com>`,
   684  			[]*Address{
   685  				{
   686  					Name:    `Micro`,
   687  					Address: "µ@example.com",
   688  				},
   689  			},
   690  		},
   691  		// RFC 6532 3.2.4, domains parts allow UTF-8
   692  		{
   693  			`Micro <micro@µ.example.com>`,
   694  			[]*Address{
   695  				{
   696  					Name:    `Micro`,
   697  					Address: "micro@µ.example.com",
   698  				},
   699  			},
   700  		},
   701  		// Issue 14866
   702  		{
   703  			`"" <emptystring@example.com>`,
   704  			[]*Address{
   705  				{
   706  					Name:    "",
   707  					Address: "emptystring@example.com",
   708  				},
   709  			},
   710  		},
   711  		// CFWS
   712  		{
   713  			`<cfws@example.com> (CFWS (cfws))  (another comment)`,
   714  			[]*Address{
   715  				{
   716  					Name:    "",
   717  					Address: "cfws@example.com",
   718  				},
   719  			},
   720  		},
   721  		{
   722  			`<cfws@example.com> ()  (another comment), <cfws2@example.com> (another)`,
   723  			[]*Address{
   724  				{
   725  					Name:    "",
   726  					Address: "cfws@example.com",
   727  				},
   728  				{
   729  					Name:    "",
   730  					Address: "cfws2@example.com",
   731  				},
   732  			},
   733  		},
   734  		// Comment as display name
   735  		{
   736  			`john@example.com (John Doe)`,
   737  			[]*Address{
   738  				{
   739  					Name:    "John Doe",
   740  					Address: "john@example.com",
   741  				},
   742  			},
   743  		},
   744  		// Comment and display name
   745  		{
   746  			`John Doe <john@example.com> (Joey)`,
   747  			[]*Address{
   748  				{
   749  					Name:    "John Doe",
   750  					Address: "john@example.com",
   751  				},
   752  			},
   753  		},
   754  		// Comment as display name, no space
   755  		{
   756  			`john@example.com(John Doe)`,
   757  			[]*Address{
   758  				{
   759  					Name:    "John Doe",
   760  					Address: "john@example.com",
   761  				},
   762  			},
   763  		},
   764  		// Comment as display name, Q-encoded
   765  		{
   766  			`asjo@example.com (Adam =?utf-8?Q?Sj=C3=B8gren?=)`,
   767  			[]*Address{
   768  				{
   769  					Name:    "Adam Sjøgren",
   770  					Address: "asjo@example.com",
   771  				},
   772  			},
   773  		},
   774  		// Comment as display name, Q-encoded and tab-separated
   775  		{
   776  			`asjo@example.com (Adam	=?utf-8?Q?Sj=C3=B8gren?=)`,
   777  			[]*Address{
   778  				{
   779  					Name:    "Adam Sjøgren",
   780  					Address: "asjo@example.com",
   781  				},
   782  			},
   783  		},
   784  		// Nested comment as display name, Q-encoded
   785  		{
   786  			`asjo@example.com (Adam =?utf-8?Q?Sj=C3=B8gren?= (Debian))`,
   787  			[]*Address{
   788  				{
   789  					Name:    "Adam Sjøgren (Debian)",
   790  					Address: "asjo@example.com",
   791  				},
   792  			},
   793  		},
   794  		// Comment in group display name
   795  		{
   796  			`group (comment:): a@example.com, b@example.com;`,
   797  			[]*Address{
   798  				{
   799  					Address: "a@example.com",
   800  				},
   801  				{
   802  					Address: "b@example.com",
   803  				},
   804  			},
   805  		},
   806  		{
   807  			`x(:"):"@a.example;("@b.example;`,
   808  			[]*Address{
   809  				{
   810  					Address: `@a.example;(@b.example`,
   811  				},
   812  			},
   813  		},
   814  		// Domain-literal
   815  		{
   816  			`jdoe@[192.168.0.1]`,
   817  			[]*Address{{
   818  				Address: "jdoe@[192.168.0.1]",
   819  			}},
   820  		},
   821  		{
   822  			`John Doe <jdoe@[192.168.0.1]>`,
   823  			[]*Address{{
   824  				Name:    "John Doe",
   825  				Address: "jdoe@[192.168.0.1]",
   826  			}},
   827  		},
   828  	}
   829  	for _, test := range tests {
   830  		if len(test.exp) == 1 {
   831  			addr, err := ParseAddress(test.addrsStr)
   832  			if err != nil {
   833  				t.Errorf("Failed parsing (single) %q: %v", test.addrsStr, err)
   834  				continue
   835  			}
   836  			if !reflect.DeepEqual([]*Address{addr}, test.exp) {
   837  				t.Errorf("Parse (single) of %q: got %+v, want %+v", test.addrsStr, addr, test.exp)
   838  			}
   839  		}
   840  
   841  		addrs, err := ParseAddressList(test.addrsStr)
   842  		if err != nil {
   843  			t.Errorf("Failed parsing (list) %q: %v", test.addrsStr, err)
   844  			continue
   845  		}
   846  		if !reflect.DeepEqual(addrs, test.exp) {
   847  			t.Errorf("Parse (list) of %q: got %+v, want %+v", test.addrsStr, addrs, test.exp)
   848  		}
   849  	}
   850  }
   851  
   852  func TestAddressParser(t *testing.T) {
   853  	tests := []struct {
   854  		addrsStr string
   855  		exp      []*Address
   856  	}{
   857  		// Bare address
   858  		{
   859  			`jdoe@machine.example`,
   860  			[]*Address{{
   861  				Address: "jdoe@machine.example",
   862  			}},
   863  		},
   864  		// RFC 5322, Appendix A.1.1
   865  		{
   866  			`John Doe <jdoe@machine.example>`,
   867  			[]*Address{{
   868  				Name:    "John Doe",
   869  				Address: "jdoe@machine.example",
   870  			}},
   871  		},
   872  		// RFC 5322, Appendix A.1.2
   873  		{
   874  			`"Joe Q. Public" <john.q.public@example.com>`,
   875  			[]*Address{{
   876  				Name:    "Joe Q. Public",
   877  				Address: "john.q.public@example.com",
   878  			}},
   879  		},
   880  		{
   881  			`Mary Smith <mary@x.test>, jdoe@example.org, Who? <one@y.test>`,
   882  			[]*Address{
   883  				{
   884  					Name:    "Mary Smith",
   885  					Address: "mary@x.test",
   886  				},
   887  				{
   888  					Address: "jdoe@example.org",
   889  				},
   890  				{
   891  					Name:    "Who?",
   892  					Address: "one@y.test",
   893  				},
   894  			},
   895  		},
   896  		{
   897  			`<boss@nil.test>, "Giant; \"Big\" Box" <sysservices@example.net>`,
   898  			[]*Address{
   899  				{
   900  					Address: "boss@nil.test",
   901  				},
   902  				{
   903  					Name:    `Giant; "Big" Box`,
   904  					Address: "sysservices@example.net",
   905  				},
   906  			},
   907  		},
   908  		// RFC 2047 "Q"-encoded ISO-8859-1 address.
   909  		{
   910  			`=?iso-8859-1?q?J=F6rg_Doe?= <joerg@example.com>`,
   911  			[]*Address{
   912  				{
   913  					Name:    `Jörg Doe`,
   914  					Address: "joerg@example.com",
   915  				},
   916  			},
   917  		},
   918  		// RFC 2047 "Q"-encoded US-ASCII address. Dumb but legal.
   919  		{
   920  			`=?us-ascii?q?J=6Frg_Doe?= <joerg@example.com>`,
   921  			[]*Address{
   922  				{
   923  					Name:    `Jorg Doe`,
   924  					Address: "joerg@example.com",
   925  				},
   926  			},
   927  		},
   928  		// RFC 2047 "Q"-encoded ISO-8859-15 address.
   929  		{
   930  			`=?ISO-8859-15?Q?J=F6rg_Doe?= <joerg@example.com>`,
   931  			[]*Address{
   932  				{
   933  					Name:    `Jörg Doe`,
   934  					Address: "joerg@example.com",
   935  				},
   936  			},
   937  		},
   938  		// RFC 2047 "B"-encoded windows-1252 address.
   939  		{
   940  			`=?windows-1252?q?Andr=E9?= Pirard <PIRARD@vm1.ulg.ac.be>`,
   941  			[]*Address{
   942  				{
   943  					Name:    `André Pirard`,
   944  					Address: "PIRARD@vm1.ulg.ac.be",
   945  				},
   946  			},
   947  		},
   948  		// Custom example of RFC 2047 "B"-encoded ISO-8859-15 address.
   949  		{
   950  			`=?ISO-8859-15?B?SvZyZw==?= <joerg@example.com>`,
   951  			[]*Address{
   952  				{
   953  					Name:    `Jörg`,
   954  					Address: "joerg@example.com",
   955  				},
   956  			},
   957  		},
   958  		// Custom example of RFC 2047 "B"-encoded UTF-8 address.
   959  		{
   960  			`=?UTF-8?B?SsO2cmc=?= <joerg@example.com>`,
   961  			[]*Address{
   962  				{
   963  					Name:    `Jörg`,
   964  					Address: "joerg@example.com",
   965  				},
   966  			},
   967  		},
   968  		// Custom example with "." in name. For issue 4938
   969  		{
   970  			`Asem H. <noreply@example.com>`,
   971  			[]*Address{
   972  				{
   973  					Name:    `Asem H.`,
   974  					Address: "noreply@example.com",
   975  				},
   976  			},
   977  		},
   978  		// Domain-literal
   979  		{
   980  			`jdoe@[192.168.0.1]`,
   981  			[]*Address{{
   982  				Address: "jdoe@[192.168.0.1]",
   983  			}},
   984  		},
   985  		{
   986  			`John Doe <jdoe@[192.168.0.1]>`,
   987  			[]*Address{{
   988  				Name:    "John Doe",
   989  				Address: "jdoe@[192.168.0.1]",
   990  			}},
   991  		},
   992  	}
   993  
   994  	ap := AddressParser{WordDecoder: &mime.WordDecoder{
   995  		CharsetReader: func(charset string, input io.Reader) (io.Reader, error) {
   996  			in, err := io.ReadAll(input)
   997  			if err != nil {
   998  				return nil, err
   999  			}
  1000  
  1001  			switch charset {
  1002  			case "iso-8859-15":
  1003  				in = bytes.ReplaceAll(in, []byte("\xf6"), []byte("ö"))
  1004  			case "windows-1252":
  1005  				in = bytes.ReplaceAll(in, []byte("\xe9"), []byte("é"))
  1006  			}
  1007  
  1008  			return bytes.NewReader(in), nil
  1009  		},
  1010  	}}
  1011  
  1012  	for _, test := range tests {
  1013  		if len(test.exp) == 1 {
  1014  			addr, err := ap.Parse(test.addrsStr)
  1015  			if err != nil {
  1016  				t.Errorf("Failed parsing (single) %q: %v", test.addrsStr, err)
  1017  				continue
  1018  			}
  1019  			if !reflect.DeepEqual([]*Address{addr}, test.exp) {
  1020  				t.Errorf("Parse (single) of %q: got %+v, want %+v", test.addrsStr, addr, test.exp)
  1021  			}
  1022  		}
  1023  
  1024  		addrs, err := ap.ParseList(test.addrsStr)
  1025  		if err != nil {
  1026  			t.Errorf("Failed parsing (list) %q: %v", test.addrsStr, err)
  1027  			continue
  1028  		}
  1029  		if !reflect.DeepEqual(addrs, test.exp) {
  1030  			t.Errorf("Parse (list) of %q: got %+v, want %+v", test.addrsStr, addrs, test.exp)
  1031  		}
  1032  	}
  1033  }
  1034  
  1035  func TestAddressString(t *testing.T) {
  1036  	tests := []struct {
  1037  		addr *Address
  1038  		exp  string
  1039  	}{
  1040  		{
  1041  			&Address{Address: "bob@example.com"},
  1042  			"<bob@example.com>",
  1043  		},
  1044  		{ // quoted local parts: RFC 5322, 3.4.1. and 3.2.4.
  1045  			&Address{Address: `my@idiot@address@example.com`},
  1046  			`<"my@idiot@address"@example.com>`,
  1047  		},
  1048  		{ // quoted local parts
  1049  			&Address{Address: ` @example.com`},
  1050  			`<" "@example.com>`,
  1051  		},
  1052  		{
  1053  			&Address{Name: "Bob", Address: "bob@example.com"},
  1054  			`"Bob" <bob@example.com>`,
  1055  		},
  1056  		{
  1057  			// note the ö (o with an umlaut)
  1058  			&Address{Name: "Böb", Address: "bob@example.com"},
  1059  			`=?utf-8?q?B=C3=B6b?= <bob@example.com>`,
  1060  		},
  1061  		{
  1062  			&Address{Name: "Bob Jane", Address: "bob@example.com"},
  1063  			`"Bob Jane" <bob@example.com>`,
  1064  		},
  1065  		{
  1066  			&Address{Name: "Böb Jacöb", Address: "bob@example.com"},
  1067  			`=?utf-8?q?B=C3=B6b_Jac=C3=B6b?= <bob@example.com>`,
  1068  		},
  1069  		{ // https://golang.org/issue/12098
  1070  			&Address{Name: "Rob", Address: ""},
  1071  			`"Rob" <@>`,
  1072  		},
  1073  		{ // https://golang.org/issue/12098
  1074  			&Address{Name: "Rob", Address: "@"},
  1075  			`"Rob" <@>`,
  1076  		},
  1077  		{
  1078  			&Address{Name: "Böb, Jacöb", Address: "bob@example.com"},
  1079  			`=?utf-8?b?QsO2YiwgSmFjw7Zi?= <bob@example.com>`,
  1080  		},
  1081  		{
  1082  			&Address{Name: "=??Q?x?=", Address: "hello@world.com"},
  1083  			`"=??Q?x?=" <hello@world.com>`,
  1084  		},
  1085  		{
  1086  			&Address{Name: "=?hello", Address: "hello@world.com"},
  1087  			`"=?hello" <hello@world.com>`,
  1088  		},
  1089  		{
  1090  			&Address{Name: "world?=", Address: "hello@world.com"},
  1091  			`"world?=" <hello@world.com>`,
  1092  		},
  1093  		{
  1094  			// should q-encode even for invalid utf-8.
  1095  			&Address{Name: string([]byte{0xed, 0xa0, 0x80}), Address: "invalid-utf8@example.net"},
  1096  			"=?utf-8?q?=ED=A0=80?= <invalid-utf8@example.net>",
  1097  		},
  1098  		// Domain-literal
  1099  		{
  1100  			&Address{Address: "bob@[192.168.0.1]"},
  1101  			"<bob@[192.168.0.1]>",
  1102  		},
  1103  		{
  1104  			&Address{Name: "Bob", Address: "bob@[192.168.0.1]"},
  1105  			`"Bob" <bob@[192.168.0.1]>`,
  1106  		},
  1107  	}
  1108  	for _, test := range tests {
  1109  		s := test.addr.String()
  1110  		if s != test.exp {
  1111  			t.Errorf("Address%+v.String() = %v, want %v", *test.addr, s, test.exp)
  1112  			continue
  1113  		}
  1114  
  1115  		// Check round-trip.
  1116  		if test.addr.Address != "" && test.addr.Address != "@" {
  1117  			a, err := ParseAddress(test.exp)
  1118  			if err != nil {
  1119  				t.Errorf("ParseAddress(%#q): %v", test.exp, err)
  1120  				continue
  1121  			}
  1122  			if a.Name != test.addr.Name || a.Address != test.addr.Address {
  1123  				t.Errorf("ParseAddress(%#q) = %#v, want %#v", test.exp, a, test.addr)
  1124  			}
  1125  		}
  1126  	}
  1127  }
  1128  
  1129  // Check if all valid addresses can be parsed, formatted and parsed again
  1130  func TestAddressParsingAndFormatting(t *testing.T) {
  1131  
  1132  	// Should pass
  1133  	tests := []string{
  1134  		`<Bob@example.com>`,
  1135  		`<bob.bob@example.com>`,
  1136  		`<".bob"@example.com>`,
  1137  		`<" "@example.com>`,
  1138  		`<some.mail-with-dash@example.com>`,
  1139  		`<"dot.and space"@example.com>`,
  1140  		`<"very.unusual.@.unusual.com"@example.com>`,
  1141  		`<admin@mailserver1>`,
  1142  		`<postmaster@localhost>`,
  1143  		"<#!$%&'*+-/=?^_`{}|~@example.org>",
  1144  		`<"very.(),:;<>[]\".VERY.\"very@\\ \"very\".unusual"@strange.example.com>`, // escaped quotes
  1145  		`<"()<>[]:,;@\\\"!#$%&'*+-/=?^_{}| ~.a"@example.org>`,                      // escaped backslashes
  1146  		`<"Abc\\@def"@example.com>`,
  1147  		`<"Joe\\Blow"@example.com>`,
  1148  		`<test1/test2=test3@example.com>`,
  1149  		`<def!xyz%abc@example.com>`,
  1150  		`<_somename@example.com>`,
  1151  		`<joe@uk>`,
  1152  		`<~@example.com>`,
  1153  		`<"..."@test.com>`,
  1154  		`<"john..doe"@example.com>`,
  1155  		`<"john.doe."@example.com>`,
  1156  		`<".john.doe"@example.com>`,
  1157  		`<"."@example.com>`,
  1158  		`<".."@example.com>`,
  1159  		`<"0:"@0>`,
  1160  		`<Bob@[192.168.0.1]>`,
  1161  	}
  1162  
  1163  	for _, test := range tests {
  1164  		addr, err := ParseAddress(test)
  1165  		if err != nil {
  1166  			t.Errorf("Couldn't parse address %s: %s", test, err.Error())
  1167  			continue
  1168  		}
  1169  		str := addr.String()
  1170  		addr, err = ParseAddress(str)
  1171  		if err != nil {
  1172  			t.Errorf("ParseAddr(%q) error: %v", test, err)
  1173  			continue
  1174  		}
  1175  
  1176  		if addr.String() != test {
  1177  			t.Errorf("String() round-trip = %q; want %q", addr, test)
  1178  			continue
  1179  		}
  1180  
  1181  	}
  1182  
  1183  	// Should fail
  1184  	badTests := []string{
  1185  		`<Abc.example.com>`,
  1186  		`<A@b@c@example.com>`,
  1187  		`<a"b(c)d,e:f;g<h>i[j\k]l@example.com>`,
  1188  		`<just"not"right@example.com>`,
  1189  		`<this is"not\allowed@example.com>`,
  1190  		`<this\ still\"not\\allowed@example.com>`,
  1191  		`<john..doe@example.com>`,
  1192  		`<john.doe@example..com>`,
  1193  		`<john.doe@example..com>`,
  1194  		`<john.doe.@example.com>`,
  1195  		`<john.doe.@.example.com>`,
  1196  		`<.john.doe@example.com>`,
  1197  		`<@example.com>`,
  1198  		`<.@example.com>`,
  1199  		`<test@.>`,
  1200  		`< @example.com>`,
  1201  		`<""test""blah""@example.com>`,
  1202  		`<""@0>`,
  1203  	}
  1204  
  1205  	for _, test := range badTests {
  1206  		_, err := ParseAddress(test)
  1207  		if err == nil {
  1208  			t.Errorf("Should have failed to parse address: %s", test)
  1209  			continue
  1210  		}
  1211  
  1212  	}
  1213  
  1214  }
  1215  
  1216  func TestAddressFormattingAndParsing(t *testing.T) {
  1217  	tests := []*Address{
  1218  		{Name: "@lïce", Address: "alice@example.com"},
  1219  		{Name: "Böb O'Connor", Address: "bob@example.com"},
  1220  		{Name: "???", Address: "bob@example.com"},
  1221  		{Name: "Böb ???", Address: "bob@example.com"},
  1222  		{Name: "Böb (Jacöb)", Address: "bob@example.com"},
  1223  		{Name: "à#$%&'(),.:;<>@[]^`{|}~'", Address: "bob@example.com"},
  1224  		// https://golang.org/issue/11292
  1225  		{Name: "\"\\\x1f,\"", Address: "0@0"},
  1226  		// https://golang.org/issue/12782
  1227  		{Name: "naé, mée", Address: "test.mail@gmail.com"},
  1228  	}
  1229  
  1230  	for i, test := range tests {
  1231  		parsed, err := ParseAddress(test.String())
  1232  		if err != nil {
  1233  			t.Errorf("test #%d: ParseAddr(%q) error: %v", i, test.String(), err)
  1234  			continue
  1235  		}
  1236  		if parsed.Name != test.Name {
  1237  			t.Errorf("test #%d: Parsed name = %q; want %q", i, parsed.Name, test.Name)
  1238  		}
  1239  		if parsed.Address != test.Address {
  1240  			t.Errorf("test #%d: Parsed address = %q; want %q", i, parsed.Address, test.Address)
  1241  		}
  1242  	}
  1243  }
  1244  
  1245  func TestEmptyAddress(t *testing.T) {
  1246  	parsed, err := ParseAddress("")
  1247  	if parsed != nil || err == nil {
  1248  		t.Errorf(`ParseAddress("") = %v, %v, want nil, error`, parsed, err)
  1249  	}
  1250  	list, err := ParseAddressList("")
  1251  	if len(list) > 0 || err == nil {
  1252  		t.Errorf(`ParseAddressList("") = %v, %v, want nil, error`, list, err)
  1253  	}
  1254  	list, err = ParseAddressList(",")
  1255  	if len(list) > 0 || err == nil {
  1256  		t.Errorf(`ParseAddressList("") = %v, %v, want nil, error`, list, err)
  1257  	}
  1258  	list, err = ParseAddressList("a@b c@d")
  1259  	if len(list) > 0 || err == nil {
  1260  		t.Errorf(`ParseAddressList("") = %v, %v, want nil, error`, list, err)
  1261  	}
  1262  }
  1263  

View as plain text