6c81df17ec
The Gmail category rule matched X-GM-LABELS and X-Gmail-Labels against the parsed header block. Neither is a header. X-GM-LABELS is a Gmail FETCH data item and never appears in the message source, and X-Gmail-Labels only exists in a Takeout export, so the branch could not fire against a real mailbox while its doc comment promised a Promotions filter. Its test built the header by hand and therefore asserted the matcher rather than the plumbing. The rule is removed and the comment says what bringing it back would take. multipartText folded a nested multipart's answer into one string, so HTML derived text landed in the plain bucket and a real text/plain sibling later in the message was discarded by the guard on plain being set. The two buckets now stay separate through the recursion. windows-1251 returned an unsupported-charset error and the message degraded to subject only. That is the charset older Russian senders still use, so those mails could never produce a task candidate. It is decoded from a 128 entry table here rather than by vendoring x/text, for the body and for encoded words in the subject. Every other unknown charset still degrades to subject only. Found in review of #63. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TrVSBKe3RFDF4fGYKWYQnX
49 lines
2.2 KiB
Go
49 lines
2.2 KiB
Go
package email
|
|
|
|
// windows-1251 (and its ASCII-compatible low half) is decoded here rather than
|
|
// pulled in from x/text.
|
|
//
|
|
// The alternative was returning an error for the charset, which ParseMessage
|
|
// turns into a subject-only message. That is a live gap and not a small one:
|
|
// cp1251 is still what older Russian senders emit, and subject-only means those
|
|
// mails can never produce a task candidate. The whole of x/text/encoding is a
|
|
// large dependency for the most privacy-sensitive path in the tree, and
|
|
// windows-1251 is a 128-entry table.
|
|
//
|
|
// Only cp1251 is added. Guessing at an unknown charset stays forbidden: mojibake
|
|
// is worse than nothing, because the model extracts a task from it happily.
|
|
|
|
// cp1251High — the 0x80..0xFF half of windows-1251. The low half is ASCII.
|
|
var cp1251High = [128]rune{
|
|
0x0402, 0x0403, 0x201A, 0x0453, 0x201E, 0x2026, 0x2020, 0x2021,
|
|
0x20AC, 0x2030, 0x0409, 0x2039, 0x040A, 0x040C, 0x040B, 0x040F,
|
|
0x0452, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014,
|
|
0xFFFD, 0x2122, 0x0459, 0x203A, 0x045A, 0x045C, 0x045B, 0x045F,
|
|
0x00A0, 0x040E, 0x045E, 0x0408, 0x00A4, 0x0490, 0x00A6, 0x00A7,
|
|
0x0401, 0x00A9, 0x0404, 0x00AB, 0x00AC, 0x00AD, 0x00AE, 0x0407,
|
|
0x00B0, 0x00B1, 0x0406, 0x0456, 0x0491, 0x00B5, 0x00B6, 0x00B7,
|
|
0x0451, 0x2116, 0x0454, 0x00BB, 0x0458, 0x0405, 0x0455, 0x0457,
|
|
0x0410, 0x0411, 0x0412, 0x0413, 0x0414, 0x0415, 0x0416, 0x0417,
|
|
0x0418, 0x0419, 0x041A, 0x041B, 0x041C, 0x041D, 0x041E, 0x041F,
|
|
0x0420, 0x0421, 0x0422, 0x0423, 0x0424, 0x0425, 0x0426, 0x0427,
|
|
0x0428, 0x0429, 0x042A, 0x042B, 0x042C, 0x042D, 0x042E, 0x042F,
|
|
0x0430, 0x0431, 0x0432, 0x0433, 0x0434, 0x0435, 0x0436, 0x0437,
|
|
0x0438, 0x0439, 0x043A, 0x043B, 0x043C, 0x043D, 0x043E, 0x043F,
|
|
0x0440, 0x0441, 0x0442, 0x0443, 0x0444, 0x0445, 0x0446, 0x0447,
|
|
0x0448, 0x0449, 0x044A, 0x044B, 0x044C, 0x044D, 0x044E, 0x044F,
|
|
}
|
|
|
|
// decodeCP1251 maps each byte through the table. Every byte has a defined
|
|
// meaning in this charset, so decoding cannot fail.
|
|
func decodeCP1251(b []byte) string {
|
|
out := make([]rune, 0, len(b))
|
|
for _, c := range b {
|
|
if c < 0x80 {
|
|
out = append(out, rune(c))
|
|
continue
|
|
}
|
|
out = append(out, cp1251High[c-0x80])
|
|
}
|
|
return string(out)
|
|
}
|