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

View as plain text