...
Run Format

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

View as plain text