Black Lives Matter. Support the Equal Justice Initiative.

Source file src/net/mail/message.go

Documentation: net/mail

     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  /*
     6  Package mail implements parsing of mail messages.
     7  
     8  For the most part, this package follows the syntax as specified by RFC 5322 and
     9  extended by RFC 6532.
    10  Notable divergences:
    11  	* Obsolete address formats are not parsed, including addresses with
    12  	  embedded route information.
    13  	* The full range of spacing (the CFWS syntax element) is not supported,
    14  	  such as breaking addresses across lines.
    15  	* No unicode normalization is performed.
    16  	* The special characters ()[]:;@\, are allowed to appear unquoted in names.
    17  */
    18  package mail
    19  
    20  import (
    21  	"bufio"
    22  	"errors"
    23  	"fmt"
    24  	"io"
    25  	"log"
    26  	"mime"
    27  	"net/textproto"
    28  	"strings"
    29  	"sync"
    30  	"time"
    31  	"unicode/utf8"
    32  )
    33  
    34  var debug = debugT(false)
    35  
    36  type debugT bool
    37  
    38  func (d debugT) Printf(format string, args ...interface{}) {
    39  	if d {
    40  		log.Printf(format, args...)
    41  	}
    42  }
    43  
    44  // A Message represents a parsed mail message.
    45  type Message struct {
    46  	Header Header
    47  	Body   io.Reader
    48  }
    49  
    50  // ReadMessage reads a message from r.
    51  // The headers are parsed, and the body of the message will be available
    52  // for reading from msg.Body.
    53  func ReadMessage(r io.Reader) (msg *Message, err error) {
    54  	tp := textproto.NewReader(bufio.NewReader(r))
    55  
    56  	hdr, err := tp.ReadMIMEHeader()
    57  	if err != nil {
    58  		return nil, err
    59  	}
    60  
    61  	return &Message{
    62  		Header: Header(hdr),
    63  		Body:   tp.R,
    64  	}, nil
    65  }
    66  
    67  // Layouts suitable for passing to time.Parse.
    68  // These are tried in order.
    69  var (
    70  	dateLayoutsBuildOnce sync.Once
    71  	dateLayouts          []string
    72  )
    73  
    74  func buildDateLayouts() {
    75  	// Generate layouts based on RFC 5322, section 3.3.
    76  
    77  	dows := [...]string{"", "Mon, "}   // day-of-week
    78  	days := [...]string{"2", "02"}     // day = 1*2DIGIT
    79  	years := [...]string{"2006", "06"} // year = 4*DIGIT / 2*DIGIT
    80  	seconds := [...]string{":05", ""}  // second
    81  	// "-0700 (MST)" is not in RFC 5322, but is common.
    82  	zones := [...]string{"-0700", "MST"} // zone = (("+" / "-") 4DIGIT) / "GMT" / ...
    83  
    84  	for _, dow := range dows {
    85  		for _, day := range days {
    86  			for _, year := range years {
    87  				for _, second := range seconds {
    88  					for _, zone := range zones {
    89  						s := dow + day + " Jan " + year + " 15:04" + second + " " + zone
    90  						dateLayouts = append(dateLayouts, s)
    91  					}
    92  				}
    93  			}
    94  		}
    95  	}
    96  }
    97  
    98  // ParseDate parses an RFC 5322 date string.
    99  func ParseDate(date string) (time.Time, error) {
   100  	dateLayoutsBuildOnce.Do(buildDateLayouts)
   101  	// CR and LF must match and are tolerated anywhere in the date field.
   102  	date = strings.ReplaceAll(date, "\r\n", "")
   103  	if strings.Index(date, "\r") != -1 {
   104  		return time.Time{}, errors.New("mail: header has a CR without LF")
   105  	}
   106  	// Re-using some addrParser methods which support obsolete text, i.e. non-printable ASCII
   107  	p := addrParser{date, nil}
   108  	p.skipSpace()
   109  
   110  	// RFC 5322: zone = (FWS ( "+" / "-" ) 4DIGIT) / obs-zone
   111  	// zone length is always 5 chars unless obsolete (obs-zone)
   112  	if ind := strings.IndexAny(p.s, "+-"); ind != -1 && len(p.s) >= ind+5 {
   113  		date = p.s[:ind+5]
   114  		p.s = p.s[ind+5:]
   115  	} else if ind := strings.Index(p.s, "T"); ind != -1 && len(p.s) >= ind+1 {
   116  		// The last letter T of the obsolete time zone is checked when no standard time zone is found.
   117  		// If T is misplaced, the date to parse is garbage.
   118  		date = p.s[:ind+1]
   119  		p.s = p.s[ind+1:]
   120  	}
   121  	if !p.skipCFWS() {
   122  		return time.Time{}, errors.New("mail: misformatted parenthetical comment")
   123  	}
   124  	for _, layout := range dateLayouts {
   125  		t, err := time.Parse(layout, date)
   126  		if err == nil {
   127  			return t, nil
   128  		}
   129  	}
   130  	return time.Time{}, errors.New("mail: header could not be parsed")
   131  }
   132  
   133  // A Header represents the key-value pairs in a mail message header.
   134  type Header map[string][]string
   135  
   136  // Get gets the first value associated with the given key.
   137  // It is case insensitive; CanonicalMIMEHeaderKey is used
   138  // to canonicalize the provided key.
   139  // If there are no values associated with the key, Get returns "".
   140  // To access multiple values of a key, or to use non-canonical keys,
   141  // access the map directly.
   142  func (h Header) Get(key string) string {
   143  	return textproto.MIMEHeader(h).Get(key)
   144  }
   145  
   146  var ErrHeaderNotPresent = errors.New("mail: header not in message")
   147  
   148  // Date parses the Date header field.
   149  func (h Header) Date() (time.Time, error) {
   150  	hdr := h.Get("Date")
   151  	if hdr == "" {
   152  		return time.Time{}, ErrHeaderNotPresent
   153  	}
   154  	return ParseDate(hdr)
   155  }
   156  
   157  // AddressList parses the named header field as a list of addresses.
   158  func (h Header) AddressList(key string) ([]*Address, error) {
   159  	hdr := h.Get(key)
   160  	if hdr == "" {
   161  		return nil, ErrHeaderNotPresent
   162  	}
   163  	return ParseAddressList(hdr)
   164  }
   165  
   166  // Address represents a single mail address.
   167  // An address such as "Barry Gibbs <bg@example.com>" is represented
   168  // as Address{Name: "Barry Gibbs", Address: "bg@example.com"}.
   169  type Address struct {
   170  	Name    string // Proper name; may be empty.
   171  	Address string // user@domain
   172  }
   173  
   174  // ParseAddress parses a single RFC 5322 address, e.g. "Barry Gibbs <bg@example.com>"
   175  func ParseAddress(address string) (*Address, error) {
   176  	return (&addrParser{s: address}).parseSingleAddress()
   177  }
   178  
   179  // ParseAddressList parses the given string as a list of addresses.
   180  func ParseAddressList(list string) ([]*Address, error) {
   181  	return (&addrParser{s: list}).parseAddressList()
   182  }
   183  
   184  // An AddressParser is an RFC 5322 address parser.
   185  type AddressParser struct {
   186  	// WordDecoder optionally specifies a decoder for RFC 2047 encoded-words.
   187  	WordDecoder *mime.WordDecoder
   188  }
   189  
   190  // Parse parses a single RFC 5322 address of the
   191  // form "Gogh Fir <gf@example.com>" or "foo@example.com".
   192  func (p *AddressParser) Parse(address string) (*Address, error) {
   193  	return (&addrParser{s: address, dec: p.WordDecoder}).parseSingleAddress()
   194  }
   195  
   196  // ParseList parses the given string as a list of comma-separated addresses
   197  // of the form "Gogh Fir <gf@example.com>" or "foo@example.com".
   198  func (p *AddressParser) ParseList(list string) ([]*Address, error) {
   199  	return (&addrParser{s: list, dec: p.WordDecoder}).parseAddressList()
   200  }
   201  
   202  // String formats the address as a valid RFC 5322 address.
   203  // If the address's name contains non-ASCII characters
   204  // the name will be rendered according to RFC 2047.
   205  func (a *Address) String() string {
   206  	// Format address local@domain
   207  	at := strings.LastIndex(a.Address, "@")
   208  	var local, domain string
   209  	if at < 0 {
   210  		// This is a malformed address ("@" is required in addr-spec);
   211  		// treat the whole address as local-part.
   212  		local = a.Address
   213  	} else {
   214  		local, domain = a.Address[:at], a.Address[at+1:]
   215  	}
   216  
   217  	// Add quotes if needed
   218  	quoteLocal := false
   219  	for i, r := range local {
   220  		if isAtext(r, false, false) {
   221  			continue
   222  		}
   223  		if r == '.' {
   224  			// Dots are okay if they are surrounded by atext.
   225  			// We only need to check that the previous byte is
   226  			// not a dot, and this isn't the end of the string.
   227  			if i > 0 && local[i-1] != '.' && i < len(local)-1 {
   228  				continue
   229  			}
   230  		}
   231  		quoteLocal = true
   232  		break
   233  	}
   234  	if quoteLocal {
   235  		local = quoteString(local)
   236  
   237  	}
   238  
   239  	s := "<" + local + "@" + domain + ">"
   240  
   241  	if a.Name == "" {
   242  		return s
   243  	}
   244  
   245  	// If every character is printable ASCII, quoting is simple.
   246  	allPrintable := true
   247  	for _, r := range a.Name {
   248  		// isWSP here should actually be isFWS,
   249  		// but we don't support folding yet.
   250  		if !isVchar(r) && !isWSP(r) || isMultibyte(r) {
   251  			allPrintable = false
   252  			break
   253  		}
   254  	}
   255  	if allPrintable {
   256  		return quoteString(a.Name) + " " + s
   257  	}
   258  
   259  	// Text in an encoded-word in a display-name must not contain certain
   260  	// characters like quotes or parentheses (see RFC 2047 section 5.3).
   261  	// When this is the case encode the name using base64 encoding.
   262  	if strings.ContainsAny(a.Name, "\"#$%&'(),.:;<>@[]^`{|}~") {
   263  		return mime.BEncoding.Encode("utf-8", a.Name) + " " + s
   264  	}
   265  	return mime.QEncoding.Encode("utf-8", a.Name) + " " + s
   266  }
   267  
   268  type addrParser struct {
   269  	s   string
   270  	dec *mime.WordDecoder // may be nil
   271  }
   272  
   273  func (p *addrParser) parseAddressList() ([]*Address, error) {
   274  	var list []*Address
   275  	for {
   276  		p.skipSpace()
   277  
   278  		// allow skipping empty entries (RFC5322 obs-addr-list)
   279  		if p.consume(',') {
   280  			continue
   281  		}
   282  
   283  		addrs, err := p.parseAddress(true)
   284  		if err != nil {
   285  			return nil, err
   286  		}
   287  		list = append(list, addrs...)
   288  
   289  		if !p.skipCFWS() {
   290  			return nil, errors.New("mail: misformatted parenthetical comment")
   291  		}
   292  		if p.empty() {
   293  			break
   294  		}
   295  		if p.peek() != ',' {
   296  			return nil, errors.New("mail: expected comma")
   297  		}
   298  
   299  		// Skip empty entries for obs-addr-list.
   300  		for p.consume(',') {
   301  			p.skipSpace()
   302  		}
   303  		if p.empty() {
   304  			break
   305  		}
   306  	}
   307  	return list, nil
   308  }
   309  
   310  func (p *addrParser) parseSingleAddress() (*Address, error) {
   311  	addrs, err := p.parseAddress(true)
   312  	if err != nil {
   313  		return nil, err
   314  	}
   315  	if !p.skipCFWS() {
   316  		return nil, errors.New("mail: misformatted parenthetical comment")
   317  	}
   318  	if !p.empty() {
   319  		return nil, fmt.Errorf("mail: expected single address, got %q", p.s)
   320  	}
   321  	if len(addrs) == 0 {
   322  		return nil, errors.New("mail: empty group")
   323  	}
   324  	if len(addrs) > 1 {
   325  		return nil, errors.New("mail: group with multiple addresses")
   326  	}
   327  	return addrs[0], nil
   328  }
   329  
   330  // parseAddress parses a single RFC 5322 address at the start of p.
   331  func (p *addrParser) parseAddress(handleGroup bool) ([]*Address, error) {
   332  	debug.Printf("parseAddress: %q", p.s)
   333  	p.skipSpace()
   334  	if p.empty() {
   335  		return nil, errors.New("mail: no address")
   336  	}
   337  
   338  	// address = mailbox / group
   339  	// mailbox = name-addr / addr-spec
   340  	// group = display-name ":" [group-list] ";" [CFWS]
   341  
   342  	// addr-spec has a more restricted grammar than name-addr,
   343  	// so try parsing it first, and fallback to name-addr.
   344  	// TODO(dsymonds): Is this really correct?
   345  	spec, err := p.consumeAddrSpec()
   346  	if err == nil {
   347  		var displayName string
   348  		p.skipSpace()
   349  		if !p.empty() && p.peek() == '(' {
   350  			displayName, err = p.consumeDisplayNameComment()
   351  			if err != nil {
   352  				return nil, err
   353  			}
   354  		}
   355  
   356  		return []*Address{{
   357  			Name:    displayName,
   358  			Address: spec,
   359  		}}, err
   360  	}
   361  	debug.Printf("parseAddress: not an addr-spec: %v", err)
   362  	debug.Printf("parseAddress: state is now %q", p.s)
   363  
   364  	// display-name
   365  	var displayName string
   366  	if p.peek() != '<' {
   367  		displayName, err = p.consumePhrase()
   368  		if err != nil {
   369  			return nil, err
   370  		}
   371  	}
   372  	debug.Printf("parseAddress: displayName=%q", displayName)
   373  
   374  	p.skipSpace()
   375  	if handleGroup {
   376  		if p.consume(':') {
   377  			return p.consumeGroupList()
   378  		}
   379  	}
   380  	// angle-addr = "<" addr-spec ">"
   381  	if !p.consume('<') {
   382  		atext := true
   383  		for _, r := range displayName {
   384  			if !isAtext(r, true, false) {
   385  				atext = false
   386  				break
   387  			}
   388  		}
   389  		if atext {
   390  			// The input is like "foo.bar"; it's possible the input
   391  			// meant to be "foo.bar@domain", or "foo.bar <...>".
   392  			return nil, errors.New("mail: missing '@' or angle-addr")
   393  		}
   394  		// The input is like "Full Name", which couldn't possibly be a
   395  		// valid email address if followed by "@domain"; the input
   396  		// likely meant to be "Full Name <...>".
   397  		return nil, errors.New("mail: no angle-addr")
   398  	}
   399  	spec, err = p.consumeAddrSpec()
   400  	if err != nil {
   401  		return nil, err
   402  	}
   403  	if !p.consume('>') {
   404  		return nil, errors.New("mail: unclosed angle-addr")
   405  	}
   406  	debug.Printf("parseAddress: spec=%q", spec)
   407  
   408  	return []*Address{{
   409  		Name:    displayName,
   410  		Address: spec,
   411  	}}, nil
   412  }
   413  
   414  func (p *addrParser) consumeGroupList() ([]*Address, error) {
   415  	var group []*Address
   416  	// handle empty group.
   417  	p.skipSpace()
   418  	if p.consume(';') {
   419  		p.skipCFWS()
   420  		return group, nil
   421  	}
   422  
   423  	for {
   424  		p.skipSpace()
   425  		// embedded groups not allowed.
   426  		addrs, err := p.parseAddress(false)
   427  		if err != nil {
   428  			return nil, err
   429  		}
   430  		group = append(group, addrs...)
   431  
   432  		if !p.skipCFWS() {
   433  			return nil, errors.New("mail: misformatted parenthetical comment")
   434  		}
   435  		if p.consume(';') {
   436  			p.skipCFWS()
   437  			break
   438  		}
   439  		if !p.consume(',') {
   440  			return nil, errors.New("mail: expected comma")
   441  		}
   442  	}
   443  	return group, nil
   444  }
   445  
   446  // consumeAddrSpec parses a single RFC 5322 addr-spec at the start of p.
   447  func (p *addrParser) consumeAddrSpec() (spec string, err error) {
   448  	debug.Printf("consumeAddrSpec: %q", p.s)
   449  
   450  	orig := *p
   451  	defer func() {
   452  		if err != nil {
   453  			*p = orig
   454  		}
   455  	}()
   456  
   457  	// local-part = dot-atom / quoted-string
   458  	var localPart string
   459  	p.skipSpace()
   460  	if p.empty() {
   461  		return "", errors.New("mail: no addr-spec")
   462  	}
   463  	if p.peek() == '"' {
   464  		// quoted-string
   465  		debug.Printf("consumeAddrSpec: parsing quoted-string")
   466  		localPart, err = p.consumeQuotedString()
   467  		if localPart == "" {
   468  			err = errors.New("mail: empty quoted string in addr-spec")
   469  		}
   470  	} else {
   471  		// dot-atom
   472  		debug.Printf("consumeAddrSpec: parsing dot-atom")
   473  		localPart, err = p.consumeAtom(true, false)
   474  	}
   475  	if err != nil {
   476  		debug.Printf("consumeAddrSpec: failed: %v", err)
   477  		return "", err
   478  	}
   479  
   480  	if !p.consume('@') {
   481  		return "", errors.New("mail: missing @ in addr-spec")
   482  	}
   483  
   484  	// domain = dot-atom / domain-literal
   485  	var domain string
   486  	p.skipSpace()
   487  	if p.empty() {
   488  		return "", errors.New("mail: no domain in addr-spec")
   489  	}
   490  	// TODO(dsymonds): Handle domain-literal
   491  	domain, err = p.consumeAtom(true, false)
   492  	if err != nil {
   493  		return "", err
   494  	}
   495  
   496  	return localPart + "@" + domain, nil
   497  }
   498  
   499  // consumePhrase parses the RFC 5322 phrase at the start of p.
   500  func (p *addrParser) consumePhrase() (phrase string, err error) {
   501  	debug.Printf("consumePhrase: [%s]", p.s)
   502  	// phrase = 1*word
   503  	var words []string
   504  	var isPrevEncoded bool
   505  	for {
   506  		// word = atom / quoted-string
   507  		var word string
   508  		p.skipSpace()
   509  		if p.empty() {
   510  			break
   511  		}
   512  		isEncoded := false
   513  		if p.peek() == '"' {
   514  			// quoted-string
   515  			word, err = p.consumeQuotedString()
   516  		} else {
   517  			// atom
   518  			// We actually parse dot-atom here to be more permissive
   519  			// than what RFC 5322 specifies.
   520  			word, err = p.consumeAtom(true, true)
   521  			if err == nil {
   522  				word, isEncoded, err = p.decodeRFC2047Word(word)
   523  			}
   524  		}
   525  
   526  		if err != nil {
   527  			break
   528  		}
   529  		debug.Printf("consumePhrase: consumed %q", word)
   530  		if isPrevEncoded && isEncoded {
   531  			words[len(words)-1] += word
   532  		} else {
   533  			words = append(words, word)
   534  		}
   535  		isPrevEncoded = isEncoded
   536  	}
   537  	// Ignore any error if we got at least one word.
   538  	if err != nil && len(words) == 0 {
   539  		debug.Printf("consumePhrase: hit err: %v", err)
   540  		return "", fmt.Errorf("mail: missing word in phrase: %v", err)
   541  	}
   542  	phrase = strings.Join(words, " ")
   543  	return phrase, nil
   544  }
   545  
   546  // consumeQuotedString parses the quoted string at the start of p.
   547  func (p *addrParser) consumeQuotedString() (qs string, err error) {
   548  	// Assume first byte is '"'.
   549  	i := 1
   550  	qsb := make([]rune, 0, 10)
   551  
   552  	escaped := false
   553  
   554  Loop:
   555  	for {
   556  		r, size := utf8.DecodeRuneInString(p.s[i:])
   557  
   558  		switch {
   559  		case size == 0:
   560  			return "", errors.New("mail: unclosed quoted-string")
   561  
   562  		case size == 1 && r == utf8.RuneError:
   563  			return "", fmt.Errorf("mail: invalid utf-8 in quoted-string: %q", p.s)
   564  
   565  		case escaped:
   566  			//  quoted-pair = ("\" (VCHAR / WSP))
   567  
   568  			if !isVchar(r) && !isWSP(r) {
   569  				return "", fmt.Errorf("mail: bad character in quoted-string: %q", r)
   570  			}
   571  
   572  			qsb = append(qsb, r)
   573  			escaped = false
   574  
   575  		case isQtext(r) || isWSP(r):
   576  			// qtext (printable US-ASCII excluding " and \), or
   577  			// FWS (almost; we're ignoring CRLF)
   578  			qsb = append(qsb, r)
   579  
   580  		case r == '"':
   581  			break Loop
   582  
   583  		case r == '\\':
   584  			escaped = true
   585  
   586  		default:
   587  			return "", fmt.Errorf("mail: bad character in quoted-string: %q", r)
   588  
   589  		}
   590  
   591  		i += size
   592  	}
   593  	p.s = p.s[i+1:]
   594  	return string(qsb), nil
   595  }
   596  
   597  // consumeAtom parses an RFC 5322 atom at the start of p.
   598  // If dot is true, consumeAtom parses an RFC 5322 dot-atom instead.
   599  // If permissive is true, consumeAtom will not fail on:
   600  // - leading/trailing/double dots in the atom (see golang.org/issue/4938)
   601  // - special characters (RFC 5322 3.2.3) except '<', '>', ':' and '"' (see golang.org/issue/21018)
   602  func (p *addrParser) consumeAtom(dot bool, permissive bool) (atom string, err error) {
   603  	i := 0
   604  
   605  Loop:
   606  	for {
   607  		r, size := utf8.DecodeRuneInString(p.s[i:])
   608  		switch {
   609  		case size == 1 && r == utf8.RuneError:
   610  			return "", fmt.Errorf("mail: invalid utf-8 in address: %q", p.s)
   611  
   612  		case size == 0 || !isAtext(r, dot, permissive):
   613  			break Loop
   614  
   615  		default:
   616  			i += size
   617  
   618  		}
   619  	}
   620  
   621  	if i == 0 {
   622  		return "", errors.New("mail: invalid string")
   623  	}
   624  	atom, p.s = p.s[:i], p.s[i:]
   625  	if !permissive {
   626  		if strings.HasPrefix(atom, ".") {
   627  			return "", errors.New("mail: leading dot in atom")
   628  		}
   629  		if strings.Contains(atom, "..") {
   630  			return "", errors.New("mail: double dot in atom")
   631  		}
   632  		if strings.HasSuffix(atom, ".") {
   633  			return "", errors.New("mail: trailing dot in atom")
   634  		}
   635  	}
   636  	return atom, nil
   637  }
   638  
   639  func (p *addrParser) consumeDisplayNameComment() (string, error) {
   640  	if !p.consume('(') {
   641  		return "", errors.New("mail: comment does not start with (")
   642  	}
   643  	comment, ok := p.consumeComment()
   644  	if !ok {
   645  		return "", errors.New("mail: misformatted parenthetical comment")
   646  	}
   647  
   648  	// TODO(stapelberg): parse quoted-string within comment
   649  	words := strings.FieldsFunc(comment, func(r rune) bool { return r == ' ' || r == '\t' })
   650  	for idx, word := range words {
   651  		decoded, isEncoded, err := p.decodeRFC2047Word(word)
   652  		if err != nil {
   653  			return "", err
   654  		}
   655  		if isEncoded {
   656  			words[idx] = decoded
   657  		}
   658  	}
   659  
   660  	return strings.Join(words, " "), nil
   661  }
   662  
   663  func (p *addrParser) consume(c byte) bool {
   664  	if p.empty() || p.peek() != c {
   665  		return false
   666  	}
   667  	p.s = p.s[1:]
   668  	return true
   669  }
   670  
   671  // skipSpace skips the leading space and tab characters.
   672  func (p *addrParser) skipSpace() {
   673  	p.s = strings.TrimLeft(p.s, " \t")
   674  }
   675  
   676  func (p *addrParser) peek() byte {
   677  	return p.s[0]
   678  }
   679  
   680  func (p *addrParser) empty() bool {
   681  	return p.len() == 0
   682  }
   683  
   684  func (p *addrParser) len() int {
   685  	return len(p.s)
   686  }
   687  
   688  // skipCFWS skips CFWS as defined in RFC5322.
   689  func (p *addrParser) skipCFWS() bool {
   690  	p.skipSpace()
   691  
   692  	for {
   693  		if !p.consume('(') {
   694  			break
   695  		}
   696  
   697  		if _, ok := p.consumeComment(); !ok {
   698  			return false
   699  		}
   700  
   701  		p.skipSpace()
   702  	}
   703  
   704  	return true
   705  }
   706  
   707  func (p *addrParser) consumeComment() (string, bool) {
   708  	// '(' already consumed.
   709  	depth := 1
   710  
   711  	var comment string
   712  	for {
   713  		if p.empty() || depth == 0 {
   714  			break
   715  		}
   716  
   717  		if p.peek() == '\\' && p.len() > 1 {
   718  			p.s = p.s[1:]
   719  		} else if p.peek() == '(' {
   720  			depth++
   721  		} else if p.peek() == ')' {
   722  			depth--
   723  		}
   724  		if depth > 0 {
   725  			comment += p.s[:1]
   726  		}
   727  		p.s = p.s[1:]
   728  	}
   729  
   730  	return comment, depth == 0
   731  }
   732  
   733  func (p *addrParser) decodeRFC2047Word(s string) (word string, isEncoded bool, err error) {
   734  	if p.dec != nil {
   735  		word, err = p.dec.Decode(s)
   736  	} else {
   737  		word, err = rfc2047Decoder.Decode(s)
   738  	}
   739  
   740  	if err == nil {
   741  		return word, true, nil
   742  	}
   743  
   744  	if _, ok := err.(charsetError); ok {
   745  		return s, true, err
   746  	}
   747  
   748  	// Ignore invalid RFC 2047 encoded-word errors.
   749  	return s, false, nil
   750  }
   751  
   752  var rfc2047Decoder = mime.WordDecoder{
   753  	CharsetReader: func(charset string, input io.Reader) (io.Reader, error) {
   754  		return nil, charsetError(charset)
   755  	},
   756  }
   757  
   758  type charsetError string
   759  
   760  func (e charsetError) Error() string {
   761  	return fmt.Sprintf("charset not supported: %q", string(e))
   762  }
   763  
   764  // isAtext reports whether r is an RFC 5322 atext character.
   765  // If dot is true, period is included.
   766  // If permissive is true, RFC 5322 3.2.3 specials is included,
   767  // except '<', '>', ':' and '"'.
   768  func isAtext(r rune, dot, permissive bool) bool {
   769  	switch r {
   770  	case '.':
   771  		return dot
   772  
   773  	// RFC 5322 3.2.3. specials
   774  	case '(', ')', '[', ']', ';', '@', '\\', ',':
   775  		return permissive
   776  
   777  	case '<', '>', '"', ':':
   778  		return false
   779  	}
   780  	return isVchar(r)
   781  }
   782  
   783  // isQtext reports whether r is an RFC 5322 qtext character.
   784  func isQtext(r rune) bool {
   785  	// Printable US-ASCII, excluding backslash or quote.
   786  	if r == '\\' || r == '"' {
   787  		return false
   788  	}
   789  	return isVchar(r)
   790  }
   791  
   792  // quoteString renders a string as an RFC 5322 quoted-string.
   793  func quoteString(s string) string {
   794  	var buf strings.Builder
   795  	buf.WriteByte('"')
   796  	for _, r := range s {
   797  		if isQtext(r) || isWSP(r) {
   798  			buf.WriteRune(r)
   799  		} else if isVchar(r) {
   800  			buf.WriteByte('\\')
   801  			buf.WriteRune(r)
   802  		}
   803  	}
   804  	buf.WriteByte('"')
   805  	return buf.String()
   806  }
   807  
   808  // isVchar reports whether r is an RFC 5322 VCHAR character.
   809  func isVchar(r rune) bool {
   810  	// Visible (printing) characters.
   811  	return '!' <= r && r <= '~' || isMultibyte(r)
   812  }
   813  
   814  // isMultibyte reports whether r is a multi-byte UTF-8 character
   815  // as supported by RFC 6532
   816  func isMultibyte(r rune) bool {
   817  	return r >= utf8.RuneSelf
   818  }
   819  
   820  // isWSP reports whether r is a WSP (white space).
   821  // WSP is a space or horizontal tab (RFC 5234 Appendix B).
   822  func isWSP(r rune) bool {
   823  	return r == ' ' || r == '\t'
   824  }
   825  

View as plain text