Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / internal/platform/imapclient/parse.go |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 720d47fae476b0d38477ae4401697d372b1ea2bc |
| Branch | feature/opportunity-to-outcome |
| Path or archive member | internal/platform/imapclient/parse.go |
| Original SHA-256 | c7ad7ce16e6f76a1eb6d82f5514e0e973adb6a7bf31a4204039c5fe822c3a0bc |
| Git blob SHA | a53c4101fd011b6590ca3f2d28f512472cf5f5de |
| Reading-copy SHA-256 | c7ad7ce16e6f76a1eb6d82f5514e0e973adb6a7bf31a4204039c5fe822c3a0bc |
Recovered source transcript
Download complete source text ↓218 lines · 6.6 KB
package imapclient
// HEADER PARSING. This is the only part of this package that touches text a stranger controls, because
// anybody can send a recruiter an email and choose exactly what these headers say.
//
// EVERY FUNCTION HERE IS TOTAL. There is no input that panics, no input that loops forever, and no input that
// allocates without bound. A malformed header yields an empty field and the message is skipped; it never
// yields an error that stops the sync, because one hostile message must not be able to stop a recruiter's
// entire mailbox from logging.
import (
"strings"
"time"
)
// parseFetch turns raw FETCH response lines into headers. Untagged responses interleave, continuation lines
// are folded, and literals are announced with a byte count that this deliberately does not trust: the parser
// works on the lines it was given and stops at the next response boundary.
func parseFetch(lines []string) []Header {
var out []Header
var cur []string
inMsg := false
flush := func() {
if len(cur) > 0 {
if h, ok := parseHeaderBlock(cur); ok {
out = append(out, h)
}
cur = nil
}
}
for _, l := range lines {
// A new FETCH response begins a new message.
if strings.HasPrefix(l, "* ") && strings.Contains(l, "FETCH") {
flush()
inMsg = true
continue
}
if !inMsg {
continue
}
// A bare closing paren ends the literal for this message.
if strings.TrimSpace(l) == ")" {
flush()
inMsg = false
continue
}
cur = append(cur, l)
// A single message's header block is bounded. A server claiming a million header lines for one
// message is either broken or hostile, and either way the answer is to stop reading it.
if len(cur) > 400 {
flush()
inMsg = false
}
}
flush()
return out
}
// parseHeaderBlock reads one RFC 5322 header block. It returns ok=false when there is no Message-ID, because
// a message with no identifier cannot be deduplicated and logging it would mean logging it again on every
// subsequent sync.
func parseHeaderBlock(lines []string) (Header, bool) {
var h Header
fields := map[string]string{}
key := ""
for _, raw := range lines {
if strings.TrimSpace(raw) == "" {
continue
}
// A continuation line begins with whitespace and belongs to the previous field. This is header
// folding, and getting it wrong truncates long recipient lists at the first wrap.
if raw[0] == ' ' || raw[0] == '\t' {
if key != "" {
fields[key] += " " + strings.TrimSpace(raw)
}
continue
}
i := strings.IndexByte(raw, ':')
if i <= 0 {
continue
}
key = strings.ToLower(strings.TrimSpace(raw[:i]))
fields[key] = strings.TrimSpace(raw[i+1:])
}
h.MessageID = strings.Trim(strings.TrimSpace(fields["message-id"]), "<>")
if h.MessageID == "" || len(h.MessageID) > 400 {
return h, false
}
h.Subject = clip(decodeIfEncoded(fields["subject"]), 300)
h.From = firstAddress(fields["from"])
h.To = addresses(fields["to"])
h.Cc = addresses(fields["cc"])
h.Date = parseDate(fields["date"])
return h, true
}
// addresses extracts the bare addresses from a header value, discarding display names. Bounded, because a
// header can legitimately list hundreds of recipients and a hostile one can list a great many more.
func addresses(v string) []string {
if v == "" {
return nil
}
out := make([]string, 0, 8)
seen := map[string]bool{}
for _, part := range splitAddressList(v) {
a := bareAddress(part)
if a == "" || seen[a] {
continue
}
seen[a] = true
out = append(out, a)
if len(out) == 64 {
break
}
}
return out
}
func firstAddress(v string) string {
for _, part := range splitAddressList(v) {
if a := bareAddress(part); a != "" {
return a
}
}
return ""
}
// splitAddressList splits on commas that are NOT inside a quoted display name. A recruiter's contact list is
// full of names like "Smith, Jane" <jane@x.test>, and splitting naively on every comma turns one recipient
// into two, one of which is garbage.
func splitAddressList(v string) []string {
var parts []string
var b strings.Builder
inQuote := false
for i := 0; i < len(v); i++ {
switch c := v[i]; {
case c == '"':
inQuote = !inQuote
b.WriteByte(c)
case c == ',' && !inQuote:
parts = append(parts, b.String())
b.Reset()
if len(parts) > 200 {
return parts
}
default:
b.WriteByte(c)
}
}
if b.Len() > 0 {
parts = append(parts, b.String())
}
return parts
}
// bareAddress pulls the address out of one list entry, in either form: angle bracketed after a display name,
// or bare. It lowercases, because address comparison is how a message is matched to a person and case is not
// meaningful for the domain and is conventionally ignored for the local part.
func bareAddress(s string) string {
s = strings.TrimSpace(s)
if i := strings.LastIndexByte(s, '<'); i >= 0 {
if j := strings.IndexByte(s[i:], '>'); j > 0 {
s = s[i+1 : i+j]
}
}
s = strings.TrimSpace(strings.Trim(s, "<>"))
s = strings.ToLower(s)
at := strings.IndexByte(s, '@')
if at <= 0 || at == len(s)-1 || len(s) > 320 || strings.ContainsAny(s, " \t\"") {
return ""
}
return s
}
// parseDate accepts the RFC 5322 forms that appear in the wild. A date that will not parse yields the zero
// time rather than an error, and the caller falls back to the time of the sync, because a message with an
// unreadable Date is still a message that happened.
func parseDate(v string) time.Time {
v = strings.TrimSpace(v)
if v == "" {
return time.Time{}
}
// A trailing parenthesised zone comment, "(UTC)", is common and breaks strict parsing.
if i := strings.IndexByte(v, '('); i > 0 {
v = strings.TrimSpace(v[:i])
}
for _, layout := range []string{
time.RFC1123Z, time.RFC1123,
"2 Jan 2006 15:04:05 -0700", "Mon, 2 Jan 2006 15:04:05 -0700",
"Mon, 2 Jan 2006 15:04:05 MST", "2 Jan 2006 15:04:05 MST",
"Mon, 2 Jan 2006 15:04 -0700",
} {
if t, err := time.Parse(layout, v); err == nil {
return t
}
}
return time.Time{}
}
// decodeIfEncoded handles the one encoded word case common enough to matter, and otherwise returns the text
// unchanged. It deliberately does NOT implement RFC 2047 fully: a subject line is a label on an activity row,
// and a partially decoded subject is a cosmetic problem while a decoder with a bug in it is a parser bug in
// the one place stranger controlled text arrives.
func decodeIfEncoded(s string) string {
s = strings.TrimSpace(s)
if !strings.HasPrefix(s, "=?") {
return s
}
// Strip the encoding wrapper and return the readable remainder rather than attempting a decode.
if i := strings.LastIndex(s, "?="); i > 0 {
inner := s[:i]
if j := strings.LastIndexByte(inner, '?'); j > 0 {
return strings.TrimSpace(strings.ReplaceAll(inner[j+1:], "_", " "))
}
}
return s
}