diff --git a/contrib/backends/srndv2/src/srnd/frontend_http.go b/contrib/backends/srndv2/src/srnd/frontend_http.go index c87798d..9f2e661 100644 --- a/contrib/backends/srndv2/src/srnd/frontend_http.go +++ b/contrib/backends/srndv2/src/srnd/frontend_http.go @@ -19,7 +19,6 @@ import ( "log" "mime" "net/http" - "net/mail" "strings" "time" ) @@ -785,10 +784,10 @@ func (self *httpFrontend) handle_postRequest(pr *postRequest, b bannedFunc, e er return } - subject := pr.Subject + subject := strings.TrimSpace(pr.Subject) // set subject - if len(subject) == 0 { + if subject == "" { subject = "None" } else if len(subject) > 256 { // subject too big @@ -796,28 +795,20 @@ func (self *httpFrontend) handle_postRequest(pr *postRequest, b bannedFunc, e er return } - nntp.headers.Set("Subject", subject) + nntp.headers.Set("Subject", safeHeader(subject)) if isSage(subject) { nntp.headers.Set("X-Sage", "1") } - name := pr.Name - + name := strings.TrimSpace(pr.Name) var tripcode_privkey []byte - - // set name - if len(name) == 0 { + // tripcode + if idx := strings.IndexByte(name, '#'); idx >= 0 { + tripcode_privkey = parseTripcodeSecret(name[idx+1:]) + name = strings.TrimSpace(name[:idx]) + } + if name == "" { name = "Anonymous" - } else { - idx := strings.Index(name, "#") - // tripcode - if idx >= 0 { - tripcode_privkey = parseTripcodeSecret(name[idx+1:]) - name = strings.Trim(name[:idx], "\t ") - if name == "" { - name = "Anonymous" - } - } } if len(name) > 128 { // name too long @@ -830,10 +821,7 @@ func (self *httpFrontend) handle_postRequest(pr *postRequest, b bannedFunc, e er msgid = genMessageID(pr.Frontend) } - nntp.headers.Set("From", (&mail.Address{ - Name: name, - Address: "poster@" + pr.Frontend, - }).String()) + nntp.headers.Set("From", formatAddress(safeHeader(name), "poster@" + pr.Frontend)) nntp.headers.Set("Message-ID", msgid) // set message diff --git a/contrib/backends/srndv2/src/srnd/message.go b/contrib/backends/srndv2/src/srnd/message.go index a0a8922..cc4706b 100644 --- a/contrib/backends/srndv2/src/srnd/message.go +++ b/contrib/backends/srndv2/src/srnd/message.go @@ -156,10 +156,7 @@ func newPlaintextArticle(message, email, subject, name, instance, message_id, ne nntp := &nntpArticle{ headers: make(ArticleHeaders), } - nntp.headers.Set("From", (&mail.Address{ - Name: name, - Address: email, - }).String()) + nntp.headers.Set("From", formatAddress(name, email)) nntp.headers.Set("Subject", subject) if isSage(subject) { nntp.headers.Set("X-Sage", "1") diff --git a/contrib/backends/srndv2/src/srnd/util.go b/contrib/backends/srndv2/src/srnd/util.go index e3c6706..a7efb67 100644 --- a/contrib/backends/srndv2/src/srnd/util.go +++ b/contrib/backends/srndv2/src/srnd/util.go @@ -15,6 +15,7 @@ import ( "golang.org/x/crypto/ed25519" "io" "log" + "mime" "net" "net/http" "net/mail" @@ -27,6 +28,7 @@ import ( "strings" "time" "unicode" + "unicode/utf8" ) func DelFile(fname string) { @@ -174,6 +176,121 @@ func safeHeader(s string) string { return strings.TrimSpace(safeHeaderReplacer.Replace(s)) } +func isVchar(r rune) bool { + // RFC 5234 B.1: VCHAR = %x21-7E ; visible (printing) characters + // RFC 6532 3.2: VCHAR =/ UTF8-non-ascii + return (r >= 0x21 && r <= 0x7E) || r >= 0x80 +} + +func isAtext(r rune) bool { + // RFC 5322: Printable US-ASCII characters not including specials. Used for atoms. + switch r { + case '(', ')', '<', '>', '[', ']', ':', ';', '@', '\\', ',', '.', '"': + return false + } + return isVchar(r) +} + +func isWSP(r rune) bool { return r == ' ' || r == '\t' } + +func isQtext(r rune) bool { + if r == '\\' || r == '"' { + return false + } + return isVchar(r) +} + +func writeQuoted(b *strings.Builder, s string) { + last := 0 + b.WriteByte('"') + for i, r := range s { + if !isQtext(r) && !isWSP(r) { + if i > last { + b.WriteString(s[last:i]) + } + b.WriteByte('\\') + b.WriteRune(r) + last = i + utf8.RuneLen(r) + } + } + if last < len(s) { + b.WriteString(s[last:]) + } + b.WriteByte('"') +} + +func formatAddress(name, email string) string { + // somewhat based on stdlib' mail.Address.String() + + b := &strings.Builder{} + + if name != "" { + needsEncoding := false + needsQuoting := false + for _, r := range name { + if r >= 0x80 || (!isWSP(r) && !isVchar(r)) { + needsEncoding = true + break + } + if !isAtext(r) { + needsQuoting = true + } + } + + if needsEncoding { + // Text in an encoded-word in a display-name must not contain certain + // characters like quotes or parentheses (see RFC 2047 section 5.3). + // When this is the case encode the name using base64 encoding. + if strings.ContainsAny(name, "\"#$%&'(),.:;<>@[]^`{|}~") { + b.WriteString(mime.BEncoding.Encode("utf-8", name)) + } else { + b.WriteString(mime.QEncoding.Encode("utf-8", name)) + } + } else if needsQuoting { + writeQuoted(b, name) + } else { + b.WriteString(name) + } + + b.WriteByte(' ') + } + + at := strings.LastIndex(email, "@") + var local, domain string + if at >= 0 { + local, domain = email[:at], email[at+1:] + } else { + local = email + } + + quoteLocal := false + for i, r := range local { + if isAtext(r) { + // if atom then okay + continue + } + if r == '.' && r > 0 && local[i-1] != '.' && i < len(local)-1 { + // dots are okay but only if surrounded by non-dots + continue + } + quoteLocal = true + break + } + + b.WriteByte('<') + if !quoteLocal { + b.WriteString(local) + } else { + writeQuoted(b, local) + } + b.WriteByte('@') + b.WriteString(domain) + b.WriteByte('>') + + return b.String() +} + + type int64Sorter []int64 func (self int64Sorter) Len() int {