Ensure errors are handled or logged
if err != nil {
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}
Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.