srnd: custom email address formatter, some tweaks
This adds custom email address formatter, which, unlike stdlib one, doesn't needlessly quote names. Quoted names can be a bit of issue with older nodes which parse addresses in simpler way, and end up not removing quote characters. This also ensures that newlines cannot be inserted in in From and Subject headers, which effectively allowed insertion of new headers in message being posted, and generating invalid messages.
This commit is contained in:
parent
2ac773cc64
commit
40e4ae1fc4
@ -19,7 +19,6 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"mime"
|
"mime"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/mail"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@ -785,10 +784,10 @@ func (self *httpFrontend) handle_postRequest(pr *postRequest, b bannedFunc, e er
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
subject := pr.Subject
|
subject := strings.TrimSpace(pr.Subject)
|
||||||
|
|
||||||
// set subject
|
// set subject
|
||||||
if len(subject) == 0 {
|
if subject == "" {
|
||||||
subject = "None"
|
subject = "None"
|
||||||
} else if len(subject) > 256 {
|
} else if len(subject) > 256 {
|
||||||
// subject too big
|
// subject too big
|
||||||
@ -796,28 +795,20 @@ func (self *httpFrontend) handle_postRequest(pr *postRequest, b bannedFunc, e er
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
nntp.headers.Set("Subject", subject)
|
nntp.headers.Set("Subject", safeHeader(subject))
|
||||||
if isSage(subject) {
|
if isSage(subject) {
|
||||||
nntp.headers.Set("X-Sage", "1")
|
nntp.headers.Set("X-Sage", "1")
|
||||||
}
|
}
|
||||||
|
|
||||||
name := pr.Name
|
name := strings.TrimSpace(pr.Name)
|
||||||
|
|
||||||
var tripcode_privkey []byte
|
var tripcode_privkey []byte
|
||||||
|
// tripcode
|
||||||
// set name
|
if idx := strings.IndexByte(name, '#'); idx >= 0 {
|
||||||
if len(name) == 0 {
|
tripcode_privkey = parseTripcodeSecret(name[idx+1:])
|
||||||
|
name = strings.TrimSpace(name[:idx])
|
||||||
|
}
|
||||||
|
if name == "" {
|
||||||
name = "Anonymous"
|
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 {
|
if len(name) > 128 {
|
||||||
// name too long
|
// name too long
|
||||||
@ -830,10 +821,7 @@ func (self *httpFrontend) handle_postRequest(pr *postRequest, b bannedFunc, e er
|
|||||||
msgid = genMessageID(pr.Frontend)
|
msgid = genMessageID(pr.Frontend)
|
||||||
}
|
}
|
||||||
|
|
||||||
nntp.headers.Set("From", (&mail.Address{
|
nntp.headers.Set("From", formatAddress(safeHeader(name), "poster@" + pr.Frontend))
|
||||||
Name: name,
|
|
||||||
Address: "poster@" + pr.Frontend,
|
|
||||||
}).String())
|
|
||||||
nntp.headers.Set("Message-ID", msgid)
|
nntp.headers.Set("Message-ID", msgid)
|
||||||
|
|
||||||
// set message
|
// set message
|
||||||
|
@ -156,10 +156,7 @@ func newPlaintextArticle(message, email, subject, name, instance, message_id, ne
|
|||||||
nntp := &nntpArticle{
|
nntp := &nntpArticle{
|
||||||
headers: make(ArticleHeaders),
|
headers: make(ArticleHeaders),
|
||||||
}
|
}
|
||||||
nntp.headers.Set("From", (&mail.Address{
|
nntp.headers.Set("From", formatAddress(name, email))
|
||||||
Name: name,
|
|
||||||
Address: email,
|
|
||||||
}).String())
|
|
||||||
nntp.headers.Set("Subject", subject)
|
nntp.headers.Set("Subject", subject)
|
||||||
if isSage(subject) {
|
if isSage(subject) {
|
||||||
nntp.headers.Set("X-Sage", "1")
|
nntp.headers.Set("X-Sage", "1")
|
||||||
|
@ -15,6 +15,7 @@ import (
|
|||||||
"golang.org/x/crypto/ed25519"
|
"golang.org/x/crypto/ed25519"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
|
"mime"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/mail"
|
"net/mail"
|
||||||
@ -27,6 +28,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
"unicode"
|
"unicode"
|
||||||
|
"unicode/utf8"
|
||||||
)
|
)
|
||||||
|
|
||||||
func DelFile(fname string) {
|
func DelFile(fname string) {
|
||||||
@ -174,6 +176,121 @@ func safeHeader(s string) string {
|
|||||||
return strings.TrimSpace(safeHeaderReplacer.Replace(s))
|
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
|
type int64Sorter []int64
|
||||||
|
|
||||||
func (self int64Sorter) Len() int {
|
func (self int64Sorter) Len() int {
|
||||||
|
Reference in New Issue
Block a user