src/net/mail/message.go GO 993 lines View on github.com → Search inside
1// Copyright 2011 The Go Authors. All rights reserved.2// Use of this source code is governed by a BSD-style3// license that can be found in the LICENSE file.45/*6Package mail implements parsing of mail messages.78For the most part, this package follows the syntax as specified by RFC 5322 and9extended by RFC 6532.10Notable divergences:11  - Obsolete address formats are not parsed, including addresses with12    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  - A leading From line is permitted, as in mbox format (RFC 4155).17*/18package mail1920import (21	"bufio"22	"errors"23	"fmt"24	"io"25	"log"26	"mime"27	"net"28	"net/textproto"29	"strings"30	"sync"31	"time"32	"unicode/utf8"33)3435var debug = debugT(false)3637type debugT bool3839func (d debugT) Printf(format string, args ...any) {40	if d {41		log.Printf(format, args...)42	}43}4445// A Message represents a parsed mail message.46type Message struct {47	Header Header48	Body   io.Reader49}5051// ReadMessage reads a message from r.52// The headers are parsed, and the body of the message will be available53// for reading from msg.Body.54func ReadMessage(r io.Reader) (msg *Message, err error) {55	tp := textproto.NewReader(bufio.NewReader(r))5657	hdr, err := readHeader(tp)58	if err != nil && (err != io.EOF || len(hdr) == 0) {59		return nil, err60	}6162	return &Message{63		Header: Header(hdr),64		Body:   tp.R,65	}, nil66}6768// readHeader reads the message headers from r.69// This is like textproto.ReadMIMEHeader, but doesn't validate.70// The fix for issue #53188 tightened up net/textproto to enforce71// restrictions of RFC 7230.72// This package implements RFC 5322, which does not have those restrictions.73// This function copies the relevant code from net/textproto,74// simplified for RFC 5322.75func readHeader(r *textproto.Reader) (map[string][]string, error) {76	m := make(map[string][]string)7778	// The first line cannot start with a leading space.79	if buf, err := r.R.Peek(1); err == nil && (buf[0] == ' ' || buf[0] == '\t') {80		line, err := r.ReadLine()81		if err != nil {82			return m, err83		}84		return m, fmt.Errorf("malformed initial line: %q", line)85	}8687	for {88		kv, err := r.ReadContinuedLine()89		if kv == "" {90			return m, err91		}9293		// Key ends at first colon.94		k, v, ok := strings.Cut(kv, ":")95		if !ok {96			return m, fmt.Errorf("malformed header line: %q", kv)97		}98		key := textproto.CanonicalMIMEHeaderKey(k)99100		// Permit empty key, because that is what we did in the past.101		if key == "" {102			continue103		}104105		// Skip initial spaces in value.106		value := strings.TrimLeft(v, " \t")107108		m[key] = append(m[key], value)109110		if err != nil {111			return m, err112		}113	}114}115116// Layouts suitable for passing to time.Parse.117// These are tried in order.118var dateLayouts = sync.OnceValue(func() []string {119	// Generate layouts based on RFC 5322, section 3.3.120121	dows := [...]string{"", "Mon, "}   // day-of-week122	days := [...]string{"2", "02"}     // day = 1*2DIGIT123	years := [...]string{"2006", "06"} // year = 4*DIGIT / 2*DIGIT124	seconds := [...]string{":05", ""}  // second125	// "-0700 (MST)" is not in RFC 5322, but is common.126	zones := [...]string{"-0700", "MST", "UT"} // zone = (("+" / "-") 4DIGIT) / "UT" / "GMT" / ...127128	total := len(dows) * len(days) * len(years) * len(seconds) * len(zones)129	layouts := make([]string, 0, total)130131	for _, dow := range dows {132		for _, day := range days {133			for _, year := range years {134				for _, second := range seconds {135					for _, zone := range zones {136						s := dow + day + " Jan " + year + " 15:04" + second + " " + zone137						layouts = append(layouts, s)138					}139				}140			}141		}142	}143144	return layouts145})146147// ParseDate parses an RFC 5322 date string.148func ParseDate(date string) (time.Time, error) {149	// CR and LF must match and are tolerated anywhere in the date field.150	date = strings.ReplaceAll(date, "\r\n", "")151	if strings.Contains(date, "\r") {152		return time.Time{}, errors.New("mail: header has a CR without LF")153	}154	// Re-using some addrParser methods which support obsolete text, i.e. non-printable ASCII155	p := addrParser{date, nil}156	p.skipSpace()157158	// RFC 5322: zone = (FWS ( "+" / "-" ) 4DIGIT) / obs-zone159	// zone length is always 5 chars unless obsolete (obs-zone)160	if ind := strings.IndexAny(p.s, "+-"); ind != -1 && len(p.s) >= ind+5 {161		date = p.s[:ind+5]162		p.s = p.s[ind+5:]163	} else {164		ind := strings.Index(p.s, "T")165		if ind == 0 {166			// In this case we have the following date formats:167			// * Thu, 20 Nov 1997 09:55:06 MDT168			// * Thu, 20 Nov 1997 09:55:06 MDT (MDT)169			// * Thu, 20 Nov 1997 09:55:06 MDT (This comment)170			ind = strings.Index(p.s[1:], "T")171			if ind != -1 {172				ind++173			}174		}175176		if ind != -1 && len(p.s) >= ind+5 {177			// The last letter T of the obsolete time zone is checked when no standard time zone is found.178			// If T is misplaced, the date to parse is garbage.179			date = p.s[:ind+1]180			p.s = p.s[ind+1:]181		}182	}183	if !p.skipCFWS() {184		return time.Time{}, errors.New("mail: misformatted parenthetical comment")185	}186	for _, layout := range dateLayouts() {187		t, err := time.Parse(layout, date)188		if err == nil {189			return t, nil190		}191	}192	return time.Time{}, errors.New("mail: header could not be parsed")193}194195// A Header represents the key-value pairs in a mail message header.196type Header map[string][]string197198// Get gets the first value associated with the given key.199// It is case insensitive; CanonicalMIMEHeaderKey is used200// to canonicalize the provided key.201// If there are no values associated with the key, Get returns "".202// To access multiple values of a key, or to use non-canonical keys,203// access the map directly.204func (h Header) Get(key string) string {205	return textproto.MIMEHeader(h).Get(key)206}207208var ErrHeaderNotPresent = errors.New("mail: header not in message")209210// Date parses the Date header field.211func (h Header) Date() (time.Time, error) {212	hdr := h.Get("Date")213	if hdr == "" {214		return time.Time{}, ErrHeaderNotPresent215	}216	return ParseDate(hdr)217}218219// AddressList parses the named header field as a list of addresses.220func (h Header) AddressList(key string) ([]*Address, error) {221	hdr := h.Get(key)222	if hdr == "" {223		return nil, ErrHeaderNotPresent224	}225	return ParseAddressList(hdr)226}227228// Address represents a single mail address.229// An address such as "Barry Gibbs <bg@example.com>" is represented230// as Address{Name: "Barry Gibbs", Address: "bg@example.com"}.231type Address struct {232	Name    string // Proper name; may be empty.233	Address string // user@domain234}235236// ParseAddress parses a single RFC 5322 address, e.g. "Barry Gibbs <bg@example.com>"237func ParseAddress(address string) (*Address, error) {238	return (&addrParser{s: address}).parseSingleAddress()239}240241// ParseAddressList parses the given string as a list of addresses.242func ParseAddressList(list string) ([]*Address, error) {243	return (&addrParser{s: list}).parseAddressList()244}245246// An AddressParser is an RFC 5322 address parser.247type AddressParser struct {248	// WordDecoder optionally specifies a decoder for RFC 2047 encoded-words.249	WordDecoder *mime.WordDecoder250}251252// Parse parses a single RFC 5322 address of the253// form "Gogh Fir <gf@example.com>" or "foo@example.com".254func (p *AddressParser) Parse(address string) (*Address, error) {255	return (&addrParser{s: address, dec: p.WordDecoder}).parseSingleAddress()256}257258// ParseList parses the given string as a list of comma-separated addresses259// of the form "Gogh Fir <gf@example.com>" or "foo@example.com".260func (p *AddressParser) ParseList(list string) ([]*Address, error) {261	return (&addrParser{s: list, dec: p.WordDecoder}).parseAddressList()262}263264// String formats the address as a valid RFC 5322 address.265// If the address's name contains non-ASCII characters266// the name will be rendered according to RFC 2047.267func (a *Address) String() string {268	// Format address local@domain269	at := strings.LastIndex(a.Address, "@")270	var local, domain string271	if at < 0 {272		// This is a malformed address ("@" is required in addr-spec);273		// treat the whole address as local-part.274		local = a.Address275	} else {276		local, domain = a.Address[:at], a.Address[at+1:]277	}278279	// Add quotes if needed280	quoteLocal := false281	for i, r := range local {282		if isAtext(r, false) {283			continue284		}285		if r == '.' {286			// Dots are okay if they are surrounded by atext.287			// We only need to check that the previous byte is288			// not a dot, and this isn't the end of the string.289			if i > 0 && local[i-1] != '.' && i < len(local)-1 {290				continue291			}292		}293		quoteLocal = true294		break295	}296	if quoteLocal {297		local = quoteString(local)298299	}300301	s := "<" + local + "@" + domain + ">"302303	if a.Name == "" {304		return s305	}306307	// If every character is printable ASCII, quoting is simple.308	allPrintable := true309	for _, r := range a.Name {310		// isWSP here should actually be isFWS,311		// but we don't support folding yet.312		if !isVchar(r) && !isWSP(r) || isMultibyte(r) {313			allPrintable = false314			break315		}316	}317	if allPrintable {318		return quoteString(a.Name) + " " + s319	}320321	// Text in an encoded-word in a display-name must not contain certain322	// characters like quotes or parentheses (see RFC 2047 section 5.3).323	// When this is the case encode the name using base64 encoding.324	if strings.ContainsAny(a.Name, "\\\"#$%&'(),.:;<>@[]^`{|}~") {325		return mime.BEncoding.Encode("utf-8", a.Name) + " " + s326	}327	return mime.QEncoding.Encode("utf-8", a.Name) + " " + s328}329330type addrParser struct {331	s   string332	dec *mime.WordDecoder // may be nil333}334335func (p *addrParser) parseAddressList() ([]*Address, error) {336	var list []*Address337	for {338		p.skipSpace()339340		// allow skipping empty entries (RFC5322 obs-addr-list)341		if p.consume(',') {342			continue343		}344345		addrs, err := p.parseAddress(true)346		if err != nil {347			return nil, err348		}349		list = append(list, addrs...)350351		if !p.skipCFWS() {352			return nil, errors.New("mail: misformatted parenthetical comment")353		}354		if p.empty() {355			break356		}357		if p.peek() != ',' {358			return nil, errors.New("mail: expected comma")359		}360361		// Skip empty entries for obs-addr-list.362		for p.consume(',') {363			p.skipSpace()364		}365		if p.empty() {366			break367		}368	}369	return list, nil370}371372func (p *addrParser) parseSingleAddress() (*Address, error) {373	addrs, err := p.parseAddress(true)374	if err != nil {375		return nil, err376	}377	if !p.skipCFWS() {378		return nil, errors.New("mail: misformatted parenthetical comment")379	}380	if !p.empty() {381		return nil, fmt.Errorf("mail: expected single address, got %q", p.s)382	}383	if len(addrs) == 0 {384		return nil, errors.New("mail: empty group")385	}386	if len(addrs) > 1 {387		return nil, errors.New("mail: group with multiple addresses")388	}389	return addrs[0], nil390}391392// parseAddress parses a single RFC 5322 address at the start of p.393func (p *addrParser) parseAddress(handleGroup bool) ([]*Address, error) {394	debug.Printf("parseAddress: %q", p.s)395	p.skipSpace()396	if p.empty() {397		return nil, errors.New("mail: no address")398	}399400	// address = mailbox / group401	// mailbox = name-addr / addr-spec402	// group = display-name ":" [group-list] ";" [CFWS]403404	// addr-spec has a more restricted grammar than name-addr,405	// so try parsing it first, and fallback to name-addr.406	// TODO(dsymonds): Is this really correct?407	spec, err := p.consumeAddrSpec()408	if err == nil {409		var displayName string410		p.skipSpace()411		if !p.empty() && p.peek() == '(' {412			displayName, err = p.consumeDisplayNameComment()413			if err != nil {414				return nil, err415			}416		}417418		return []*Address{{419			Name:    displayName,420			Address: spec,421		}}, err422	}423	debug.Printf("parseAddress: not an addr-spec: %v", err)424	debug.Printf("parseAddress: state is now %q", p.s)425426	// display-name427	var displayName string428	if p.peek() != '<' {429		displayName, err = p.consumePhrase()430		if err != nil {431			return nil, err432		}433	}434	debug.Printf("parseAddress: displayName=%q", displayName)435436	p.skipSpace()437	if handleGroup {438		if p.consume(':') {439			return p.consumeGroupList()440		}441	}442	// angle-addr = "<" addr-spec ">"443	if !p.consume('<') {444		atext := true445		for _, r := range displayName {446			if !isAtext(r, true) {447				atext = false448				break449			}450		}451		if atext {452			// The input is like "foo.bar"; it's possible the input453			// meant to be "foo.bar@domain", or "foo.bar <...>".454			return nil, errors.New("mail: missing '@' or angle-addr")455		}456		// The input is like "Full Name", which couldn't possibly be a457		// valid email address if followed by "@domain"; the input458		// likely meant to be "Full Name <...>".459		return nil, errors.New("mail: no angle-addr")460	}461	spec, err = p.consumeAddrSpec()462	if err != nil {463		return nil, err464	}465	if !p.consume('>') {466		return nil, errors.New("mail: unclosed angle-addr")467	}468	debug.Printf("parseAddress: spec=%q", spec)469470	return []*Address{{471		Name:    displayName,472		Address: spec,473	}}, nil474}475476func (p *addrParser) consumeGroupList() ([]*Address, error) {477	var group []*Address478	// handle empty group.479	p.skipSpace()480	if p.consume(';') {481		if !p.skipCFWS() {482			return nil, errors.New("mail: misformatted parenthetical comment")483		}484		return group, nil485	}486487	for {488		p.skipSpace()489		// embedded groups not allowed.490		addrs, err := p.parseAddress(false)491		if err != nil {492			return nil, err493		}494		group = append(group, addrs...)495496		if !p.skipCFWS() {497			return nil, errors.New("mail: misformatted parenthetical comment")498		}499		if p.consume(';') {500			if !p.skipCFWS() {501				return nil, errors.New("mail: misformatted parenthetical comment")502			}503			break504		}505		if !p.consume(',') {506			return nil, errors.New("mail: expected comma")507		}508	}509	return group, nil510}511512// consumeAddrSpec parses a single RFC 5322 addr-spec at the start of p.513func (p *addrParser) consumeAddrSpec() (spec string, err error) {514	debug.Printf("consumeAddrSpec: %q", p.s)515516	orig := *p517	defer func() {518		if err != nil {519			*p = orig520		}521	}()522523	// local-part = dot-atom / quoted-string524	var localPart string525	p.skipSpace()526	if p.empty() {527		return "", errors.New("mail: no addr-spec")528	}529	if p.peek() == '"' {530		// quoted-string531		debug.Printf("consumeAddrSpec: parsing quoted-string")532		localPart, err = p.consumeQuotedString()533		if localPart == "" {534			err = errors.New("mail: empty quoted string in addr-spec")535		}536	} else {537		// dot-atom538		debug.Printf("consumeAddrSpec: parsing dot-atom")539		localPart, err = p.consumeAtom(true, false)540	}541	if err != nil {542		debug.Printf("consumeAddrSpec: failed: %v", err)543		return "", err544	}545546	if !p.consume('@') {547		return "", errors.New("mail: missing @ in addr-spec")548	}549550	// domain = dot-atom / domain-literal551	var domain string552	p.skipSpace()553	if p.empty() {554		return "", errors.New("mail: no domain in addr-spec")555	}556557	if p.peek() == '[' {558		// domain-literal559		domain, err = p.consumeDomainLiteral()560		if err != nil {561			return "", err562		}563	} else {564		// dot-atom565		domain, err = p.consumeAtom(true, false)566		if err != nil {567			return "", err568		}569	}570571	return localPart + "@" + domain, nil572}573574// consumePhrase parses the RFC 5322 phrase at the start of p.575func (p *addrParser) consumePhrase() (phrase string, err error) {576	debug.Printf("consumePhrase: [%s]", p.s)577	// phrase = 1*word578	var (579		words []string580		sb    strings.Builder581	)582	for {583		// obs-phrase allows CFWS after one word584		if len(words) > 0 {585			if !p.skipCFWS() {586				return "", errors.New("mail: misformatted parenthetical comment")587			}588		}589		// word = atom / quoted-string590		var word string591		p.skipSpace()592		if p.empty() {593			break594		}595		isEncoded := false596		if p.peek() == '"' {597			// quoted-string598			word, err = p.consumeQuotedString()599		} else {600			// atom601			// We actually parse dot-atom here to be more permissive602			// than what RFC 5322 specifies.603			word, err = p.consumeAtom(true, true)604			if err == nil {605				word, isEncoded, err = p.decodeRFC2047Word(word)606			}607		}608609		if err != nil {610			break611		}612		debug.Printf("consumePhrase: consumed %q", word)613		switch {614		case isEncoded:615			sb.WriteString(word)616		case !isEncoded && sb.Len() > 0:617			words = append(words, sb.String())618			sb.Reset()619			words = append(words, word)620		default:621			words = append(words, word)622		}623	}624625	if sb.Len() > 0 {626		words = append(words, sb.String())627	}628629	// Ignore any error if we got at least one word.630	if err != nil && len(words) == 0 {631		debug.Printf("consumePhrase: hit err: %v", err)632		return "", fmt.Errorf("mail: missing word in phrase: %v", err)633	}634	phrase = strings.Join(words, " ")635	return phrase, nil636}637638// consumeQuotedString parses the quoted string at the start of p.639func (p *addrParser) consumeQuotedString() (qs string, err error) {640	// Assume first byte is '"'.641	i := 1642	qsb := make([]rune, 0, 10)643644	escaped := false645646Loop:647	for {648		r, size := utf8.DecodeRuneInString(p.s[i:])649650		switch {651		case size == 0:652			return "", errors.New("mail: unclosed quoted-string")653654		case size == 1 && r == utf8.RuneError:655			return "", fmt.Errorf("mail: invalid utf-8 in quoted-string: %q", p.s)656657		case escaped:658			//  quoted-pair = ("\" (VCHAR / WSP))659660			if !isVchar(r) && !isWSP(r) {661				return "", fmt.Errorf("mail: bad character in quoted-string: %q", r)662			}663664			qsb = append(qsb, r)665			escaped = false666667		case isQtext(r) || isWSP(r):668			// qtext (printable US-ASCII excluding " and \), or669			// FWS (almost; we're ignoring CRLF)670			qsb = append(qsb, r)671672		case r == '"':673			break Loop674675		case r == '\\':676			escaped = true677678		default:679			return "", fmt.Errorf("mail: bad character in quoted-string: %q", r)680681		}682683		i += size684	}685	p.s = p.s[i+1:]686	return string(qsb), nil687}688689// consumeAtom parses an RFC 5322 atom at the start of p.690// If dot is true, consumeAtom parses an RFC 5322 dot-atom instead.691// If permissive is true, consumeAtom will not fail on:692// - leading/trailing/double dots in the atom (see golang.org/issue/4938)693func (p *addrParser) consumeAtom(dot bool, permissive bool) (atom string, err error) {694	i := 0695696Loop:697	for {698		r, size := utf8.DecodeRuneInString(p.s[i:])699		switch {700		case size == 1 && r == utf8.RuneError:701			return "", fmt.Errorf("mail: invalid utf-8 in address: %q", p.s)702703		case size == 0 || !isAtext(r, dot):704			break Loop705706		default:707			i += size708709		}710	}711712	if i == 0 {713		return "", errors.New("mail: invalid string")714	}715	atom, p.s = p.s[:i], p.s[i:]716	if !permissive {717		if strings.HasPrefix(atom, ".") {718			return "", errors.New("mail: leading dot in atom")719		}720		if strings.Contains(atom, "..") {721			return "", errors.New("mail: double dot in atom")722		}723		if strings.HasSuffix(atom, ".") {724			return "", errors.New("mail: trailing dot in atom")725		}726	}727	return atom, nil728}729730// consumeDomainLiteral parses an RFC 5322 domain-literal at the start of p.731func (p *addrParser) consumeDomainLiteral() (string, error) {732	// Skip the leading [733	if !p.consume('[') {734		return "", errors.New(`mail: missing "[" in domain-literal`)735	}736737	// Parse the dtext738	dtext := p.s739	dtextLen := 0740	for {741		if p.empty() {742			return "", errors.New("mail: unclosed domain-literal")743		}744		if p.peek() == ']' {745			break746		}747748		r, size := utf8.DecodeRuneInString(p.s)749		if size == 1 && r == utf8.RuneError {750			return "", fmt.Errorf("mail: invalid utf-8 in domain-literal: %q", p.s)751		}752		if !isDtext(r) {753			return "", fmt.Errorf("mail: bad character in domain-literal: %q", r)754		}755756		dtextLen += size757		p.s = p.s[size:]758	}759	dtext = dtext[:dtextLen]760761	// Skip the trailing ]762	if !p.consume(']') {763		return "", errors.New("mail: unclosed domain-literal")764	}765766	// Check if the domain literal is an IP address767	if addr, ok := strings.CutPrefix(dtext, "IPv6:"); ok {768		if len(net.ParseIP(addr)) != net.IPv6len {769			return "", fmt.Errorf("mail: invalid IPv6 address in domain-literal: %q", dtext)770		}771772	} else if net.ParseIP(dtext).To4() == nil {773		return "", fmt.Errorf("mail: invalid IP address in domain-literal: %q", dtext)774	}775776	return "[" + dtext + "]", nil777}778779func (p *addrParser) consumeDisplayNameComment() (string, error) {780	if !p.consume('(') {781		return "", errors.New("mail: comment does not start with (")782	}783	comment, ok := p.consumeComment()784	if !ok {785		return "", errors.New("mail: misformatted parenthetical comment")786	}787788	// TODO(stapelberg): parse quoted-string within comment789	words := strings.FieldsFunc(comment, func(r rune) bool { return r == ' ' || r == '\t' })790	for idx, word := range words {791		decoded, isEncoded, err := p.decodeRFC2047Word(word)792		if err != nil {793			return "", err794		}795		if isEncoded {796			words[idx] = decoded797		}798	}799800	return strings.Join(words, " "), nil801}802803func (p *addrParser) consume(c byte) bool {804	if p.empty() || p.peek() != c {805		return false806	}807	p.s = p.s[1:]808	return true809}810811// skipSpace skips the leading space and tab characters.812func (p *addrParser) skipSpace() {813	p.s = strings.TrimLeft(p.s, " \t")814}815816func (p *addrParser) peek() byte {817	return p.s[0]818}819820func (p *addrParser) empty() bool {821	return p.len() == 0822}823824func (p *addrParser) len() int {825	return len(p.s)826}827828// skipCFWS skips CFWS as defined in RFC5322.829func (p *addrParser) skipCFWS() bool {830	p.skipSpace()831832	for {833		if !p.consume('(') {834			break835		}836837		if _, ok := p.consumeComment(); !ok {838			return false839		}840841		p.skipSpace()842	}843844	return true845}846847func (p *addrParser) consumeComment() (string, bool) {848	// '(' already consumed.849	depth := 1850851	var comment strings.Builder852	for {853		if p.empty() || depth == 0 {854			break855		}856857		if p.peek() == '\\' && p.len() > 1 {858			p.s = p.s[1:]859		} else if p.peek() == '(' {860			depth++861		} else if p.peek() == ')' {862			depth--863		}864		if depth > 0 {865			comment.WriteByte(p.s[0])866		}867		p.s = p.s[1:]868	}869870	return comment.String(), depth == 0871}872873func (p *addrParser) decodeRFC2047Word(s string) (word string, isEncoded bool, err error) {874	dec := p.dec875	if dec == nil {876		dec = &rfc2047Decoder877	}878879	// Substitute our own CharsetReader function so that we can tell880	// whether an error from the Decode method was due to the881	// CharsetReader (meaning the charset is invalid).882	// We used to look for the charsetError type in the error result,883	// but that behaves badly with CharsetReaders other than the884	// one in rfc2047Decoder.885	adec := *dec886	charsetReaderError := false887	adec.CharsetReader = func(charset string, input io.Reader) (io.Reader, error) {888		if dec.CharsetReader == nil {889			charsetReaderError = true890			return nil, charsetError(charset)891		}892		r, err := dec.CharsetReader(charset, input)893		if err != nil {894			charsetReaderError = true895		}896		return r, err897	}898	word, err = adec.Decode(s)899	if err == nil {900		return word, true, nil901	}902903	// If the error came from the character set reader904	// (meaning the character set itself is invalid905	// but the decoding worked fine until then),906	// return the original text and the error,907	// with isEncoded=true.908	if charsetReaderError {909		return s, true, err910	}911912	// Ignore invalid RFC 2047 encoded-word errors.913	return s, false, nil914}915916var rfc2047Decoder = mime.WordDecoder{917	CharsetReader: func(charset string, input io.Reader) (io.Reader, error) {918		return nil, charsetError(charset)919	},920}921922type charsetError string923924func (e charsetError) Error() string {925	return fmt.Sprintf("charset not supported: %q", string(e))926}927928// isAtext reports whether r is an RFC 5322 atext character.929// If dot is true, period is included.930func isAtext(r rune, dot bool) bool {931	switch r {932	case '.':933		return dot934935	// RFC 5322 3.2.3. specials936	case '(', ')', '<', '>', '[', ']', ':', ';', '@', '\\', ',', '"': // RFC 5322 3.2.3. specials937		return false938	}939	return isVchar(r)940}941942// isQtext reports whether r is an RFC 5322 qtext character.943func isQtext(r rune) bool {944	// Printable US-ASCII, excluding backslash or quote.945	if r == '\\' || r == '"' {946		return false947	}948	return isVchar(r)949}950951// quoteString renders a string as an RFC 5322 quoted-string.952func quoteString(s string) string {953	var b strings.Builder954	b.WriteByte('"')955	for _, r := range s {956		if isQtext(r) || isWSP(r) {957			b.WriteRune(r)958		} else if isVchar(r) {959			b.WriteByte('\\')960			b.WriteRune(r)961		}962	}963	b.WriteByte('"')964	return b.String()965}966967// isVchar reports whether r is an RFC 5322 VCHAR character.968func isVchar(r rune) bool {969	// Visible (printing) characters.970	return '!' <= r && r <= '~' || isMultibyte(r)971}972973// isMultibyte reports whether r is a multi-byte UTF-8 character974// as supported by RFC 6532.975func isMultibyte(r rune) bool {976	return r >= utf8.RuneSelf977}978979// isWSP reports whether r is a WSP (white space).980// WSP is a space or horizontal tab (RFC 5234 Appendix B).981func isWSP(r rune) bool {982	return r == ' ' || r == '\t'983}984985// isDtext reports whether r is an RFC 5322 dtext character.986func isDtext(r rune) bool {987	// Printable US-ASCII, excluding "[", "]", or "\".988	if r == '[' || r == ']' || r == '\\' {989		return false990	}991	return isVchar(r)992}

Code quality findings 11

Ensure errors are handled or logged
warning correctness unhandled-error
if err != nil {
Multiple appends without pre-allocation; use make() with capacity when size is known
info performance append-without-prealloc
m[key] = append(m[key], value)
Deeply nested control structures reduce readability; consider extracting to functions or using early returns
info maintainability deep-nesting
for _, dow := range dows {
Deeply nested control structures reduce readability; consider extracting to functions or using early returns
info maintainability deep-nesting
for _, day := range days {
Deeply nested control structures reduce readability; consider extracting to functions or using early returns
info maintainability deep-nesting
for _, year := range years {
Deeply nested control structures reduce readability; consider extracting to functions or using early returns
info maintainability deep-nesting
for _, second := range seconds {
Deeply nested control structures reduce readability; consider extracting to functions or using early returns
info maintainability deep-nesting
for _, zone := range zones {
Range over slice copies each element by value; use index or pointer receiver for large structs to avoid copies
info performance copy-large-struct
for i, r := range local {
Multiple appends without pre-allocation; use make() with capacity when size is known
info performance append-without-prealloc
list = append(list, addrs...)
Multiple appends without pre-allocation; use make() with capacity when size is known
info performance append-without-prealloc
group = append(group, addrs...)
Range over slice copies each element by value; use index or pointer receiver for large structs to avoid copies
info performance copy-large-struct
for idx, word := range words {

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.