panel: show the subject as text, not as its MIME encoding
A non-Latin subject arrives as RFC 2047 encoded-words, which the send-log printed verbatim: unreadable, and one unbreakable run wide enough to push the Status column out of its card. Decode at journal time (UTF-8/ASCII; exotic charsets keep the raw header) and cap at 200 characters, then clip the column to one line with the full text in the tooltip so no subject can widen the row. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,7 @@ package milter
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"mime"
|
||||
"net"
|
||||
"net/textproto"
|
||||
"strings"
|
||||
@@ -92,11 +93,35 @@ func (s *session) RcptTo(rcpt string, m *milter.Modifier) (milter.Response, erro
|
||||
// Header captures the Subject. Only the first Subject header is kept.
|
||||
func (s *session) Header(name, value string, m *milter.Modifier) (milter.Response, error) {
|
||||
if s.subject == "" && textproto.CanonicalMIMEHeaderKey(name) == "Subject" {
|
||||
s.subject = value
|
||||
s.subject = decodeSubject(value)
|
||||
}
|
||||
return milter.RespContinue, nil
|
||||
}
|
||||
|
||||
// subjectMaxRunes caps what the journal keeps of a subject. A Subject header
|
||||
// may legally run to hundreds of characters; the log only needs enough to
|
||||
// recognise the message, and the panel shows one row per recipient.
|
||||
const subjectMaxRunes = 200
|
||||
|
||||
// decodeSubject turns the raw Subject header into display text. Anything
|
||||
// non-ASCII arrives as RFC 2047 encoded-words (=?utf-8?Q?=D0=9F…?=), which the
|
||||
// panel would otherwise show verbatim: unreadable, and — being one unbreakable
|
||||
// run — wide enough to push the send-log table out of its card. Go's decoder
|
||||
// covers the UTF-8 and ASCII charsets senders use in practice; for anything
|
||||
// else (windows-1251, koi8-r) it fails and the raw header is kept, which is no
|
||||
// worse than before. Truncation is applied after decoding so the cap counts
|
||||
// characters of the subject, not bytes of its encoding.
|
||||
func decodeSubject(v string) string {
|
||||
if dec, err := (&mime.WordDecoder{}).DecodeHeader(v); err == nil {
|
||||
v = dec
|
||||
}
|
||||
v = strings.TrimSpace(v)
|
||||
if r := []rune(v); len(r) > subjectMaxRunes {
|
||||
v = string(r[:subjectMaxRunes]) + "…"
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// Body fires at end-of-message, when the queue-id macro {i} is set and the
|
||||
// message is about to be committed to the queue. This is where the "queued"
|
||||
// rows are written. We accept (this milter is done) without ever rejecting.
|
||||
|
||||
Reference in New Issue
Block a user