From 2d62a3bc7f17708610579cffe7a96e3797d0e125 Mon Sep 17 00:00:00 2001 From: cathugger Date: Sat, 8 Dec 2018 19:44:05 +0000 Subject: [PATCH 1/7] srnd: error while reading message isn't valid thing --- contrib/backends/srndv2/src/srnd/store.go | 8 ++++---- contrib/backends/srndv2/src/srnd/util.go | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/contrib/backends/srndv2/src/srnd/store.go b/contrib/backends/srndv2/src/srnd/store.go index d421e6c..b205a2d 100644 --- a/contrib/backends/srndv2/src/srnd/store.go +++ b/contrib/backends/srndv2/src/srnd/store.go @@ -547,8 +547,8 @@ func (self *articleStore) ProcessMessage(wr io.Writer, msg io.Reader, spamfilter if e != nil { log.Println("failed to read entire message", e) } - pw_in.Close() - pr_in.Close() + pw_in.CloseWithError(e) + pr_in.CloseWithError(e) }() r := bufio.NewReader(pr_out) m, e := readMIMEHeader(r) @@ -579,7 +579,7 @@ func (self *articleStore) ProcessMessage(wr io.Writer, msg io.Reader, spamfilter return } writeMIMEHeader(wr, m.Header) - read_message_body(m.Body, m.Header, self, wr, false, process) + err = read_message_body(m.Body, m.Header, self, wr, false, process) } return } @@ -632,7 +632,7 @@ func read_message_body(body io.Reader, hdr map[string][]string, store ArticleSto body = io.TeeReader(body, wr) } boundary, ok := params["boundary"] - if ok || content_type == "multipart/mixed" { + if content_type == "multipart/mixed" && ok { partReader := multipart.NewReader(body, boundary) for { part, err := partReader.NextPart() diff --git a/contrib/backends/srndv2/src/srnd/util.go b/contrib/backends/srndv2/src/srnd/util.go index efa9428..70db80d 100644 --- a/contrib/backends/srndv2/src/srnd/util.go +++ b/contrib/backends/srndv2/src/srnd/util.go @@ -801,8 +801,8 @@ func storeMessage(daemon *NNTPDaemon, hdr textproto.MIMEHeader, body io.Reader) go func() { var buff [65536]byte writeMIMEHeader(pw, hdr) - io.CopyBuffer(pw, body, buff[:]) - pw.Close() + _, e := io.CopyBuffer(pw, body, buff[:]) + pw.CloseWithError(e) }() err = daemon.store.ProcessMessage(f, pr, daemon.CheckText, hdr.Get("Newsgroups")) pr.Close() From b5ff2dc4a2ae84f93dbb2c163c585becacf143d9 Mon Sep 17 00:00:00 2001 From: cathugger Date: Sat, 8 Dec 2018 20:59:46 +0000 Subject: [PATCH 2/7] srnd: fix multipart message parsing --- contrib/backends/srndv2/src/srnd/store.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/backends/srndv2/src/srnd/store.go b/contrib/backends/srndv2/src/srnd/store.go index b205a2d..8bd1c3d 100644 --- a/contrib/backends/srndv2/src/srnd/store.go +++ b/contrib/backends/srndv2/src/srnd/store.go @@ -632,7 +632,7 @@ func read_message_body(body io.Reader, hdr map[string][]string, store ArticleSto body = io.TeeReader(body, wr) } boundary, ok := params["boundary"] - if content_type == "multipart/mixed" && ok { + if strings.HasPrefix(media_type, "multipart/") && ok { partReader := multipart.NewReader(body, boundary) for { part, err := partReader.NextPart() From fa511c275e66d4aa9944dce910921ec1c75d0462 Mon Sep 17 00:00:00 2001 From: cathugger Date: Sun, 9 Dec 2018 19:50:27 +0000 Subject: [PATCH 3/7] srnd: ensure clean XOVER output --- contrib/backends/srndv2/src/srnd/nntp.go | 27 +++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/contrib/backends/srndv2/src/srnd/nntp.go b/contrib/backends/srndv2/src/srnd/nntp.go index 0d9327d..19a0e7f 100644 --- a/contrib/backends/srndv2/src/srnd/nntp.go +++ b/contrib/backends/srndv2/src/srnd/nntp.go @@ -550,6 +550,13 @@ func (self *nntpConnection) checkMIMEHeaderNoAuth(daemon *NNTPDaemon, hdr textpr return } +var overReplacer = strings.NewReplacer("\t", " ", "\n", " ", "\r", "", "\000", "") + +// safeOver cleans string for OVER/XOVER output +func safeOver(s string) string { + return strings.TrimSpace(overReplacer.Replace(s)) +} + func (self *nntpConnection) handleLine(daemon *NNTPDaemon, code int, line string, conn *textproto.Conn) (err error) { parts := strings.Split(line, " ") var msgid string @@ -914,7 +921,25 @@ func (self *nntpConnection) handleLine(daemon *NNTPDaemon, code int, line string for _, model := range models { if model != nil { if err == nil { - io.WriteString(dw, fmt.Sprintf("%.6d\t%s\t\"%s\" <%s@%s>\t%s\t%s\t%s\r\n", model.NNTPID(), model.Subject(), model.Name(), model.Name(), model.Frontend(), model.Date(), model.MessageID(), model.Reference())) + /* + The first 8 fields MUST be the following, in order: + "0" or article number (see below) + Subject header content + From header content + Date header content + Message-ID header content + References header content + :bytes metadata item + :lines metadata item + */ + fmt.Fprintf(dw, + "%.6d\t%s\t\"%s\" <%s@%s>\t%s\t%s\t%s\r\n", + model.NNTPID(), + safeOver(model.Subject()), + safeOver(model.Name()), safeOver(model.Name()), safeOver(model.Frontend()), + safeOver(model.Date()), + safeOver(model.MessageID()), + safeOver(model.Reference())) } } } From 7e6f143108cea1aa43f30c963c44f78bf56e6837 Mon Sep 17 00:00:00 2001 From: cathugger Date: Tue, 11 Dec 2018 22:56:29 +0000 Subject: [PATCH 4/7] generate compliant From headers, more tolerance to non-compliant From headers, other fixups --- .../backends/srndv2/src/srnd/frontend_http.go | 6 +++- contrib/backends/srndv2/src/srnd/message.go | 23 +++++++++--- contrib/backends/srndv2/src/srnd/nntp.go | 35 ++++++++----------- contrib/backends/srndv2/src/srnd/util.go | 13 +++++++ 4 files changed, 51 insertions(+), 26 deletions(-) diff --git a/contrib/backends/srndv2/src/srnd/frontend_http.go b/contrib/backends/srndv2/src/srnd/frontend_http.go index dd37673..c87798d 100644 --- a/contrib/backends/srndv2/src/srnd/frontend_http.go +++ b/contrib/backends/srndv2/src/srnd/frontend_http.go @@ -19,6 +19,7 @@ import ( "log" "mime" "net/http" + "net/mail" "strings" "time" ) @@ -829,7 +830,10 @@ func (self *httpFrontend) handle_postRequest(pr *postRequest, b bannedFunc, e er msgid = genMessageID(pr.Frontend) } - nntp.headers.Set("From", nntpSanitize(fmt.Sprintf("%s ", name, pr.Frontend))) + nntp.headers.Set("From", (&mail.Address{ + Name: name, + Address: "poster@" + pr.Frontend, + }).String()) 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 e12cebe..a0a8922 100644 --- a/contrib/backends/srndv2/src/srnd/message.go +++ b/contrib/backends/srndv2/src/srnd/message.go @@ -135,7 +135,7 @@ func (self *nntpArticle) Reset() { self.boundary = "" self.message = "" if self.attachments != nil { - for idx, _ := range self.attachments { + for idx := range self.attachments { self.attachments[idx].Reset() self.attachments[idx] = nil } @@ -156,7 +156,10 @@ func newPlaintextArticle(message, email, subject, name, instance, message_id, ne nntp := &nntpArticle{ headers: make(ArticleHeaders), } - nntp.headers.Set("From", fmt.Sprintf("%s <%s>", name, email)) + nntp.headers.Set("From", (&mail.Address{ + Name: name, + Address: email, + }).String()) nntp.headers.Set("Subject", subject) if isSage(subject) { nntp.headers.Set("X-Sage", "1") @@ -296,18 +299,30 @@ func (self *nntpArticle) Newsgroup() string { func (self *nntpArticle) Name() string { const defname = "Anonymous" + from := strings.TrimSpace(self.headers.Get("From", "")) if from == "" { return defname } + a, e := mail.ParseAddress(from) + var name string if e != nil { - return fmt.Sprintf("[Invalid From header: %v]", e) + // try older method - some nodes generate non-compliant stuff + if i := strings.IndexByte(from, '<'); i > 1 { + name = from[:i] + } else { + return "[Invalid From header]" + } + } else { + name = a.Name } - name := strings.TrimSpace(a.Name) + + name = safeHeader(name) if name == "" { return defname } + return name } diff --git a/contrib/backends/srndv2/src/srnd/nntp.go b/contrib/backends/srndv2/src/srnd/nntp.go index 19a0e7f..2de8ca1 100644 --- a/contrib/backends/srndv2/src/srnd/nntp.go +++ b/contrib/backends/srndv2/src/srnd/nntp.go @@ -550,13 +550,6 @@ func (self *nntpConnection) checkMIMEHeaderNoAuth(daemon *NNTPDaemon, hdr textpr return } -var overReplacer = strings.NewReplacer("\t", " ", "\n", " ", "\r", "", "\000", "") - -// safeOver cleans string for OVER/XOVER output -func safeOver(s string) string { - return strings.TrimSpace(overReplacer.Replace(s)) -} - func (self *nntpConnection) handleLine(daemon *NNTPDaemon, code int, line string, conn *textproto.Conn) (err error) { parts := strings.Split(line, " ") var msgid string @@ -922,24 +915,24 @@ func (self *nntpConnection) handleLine(daemon *NNTPDaemon, code int, line string if model != nil { if err == nil { /* - The first 8 fields MUST be the following, in order: - "0" or article number (see below) - Subject header content - From header content - Date header content - Message-ID header content - References header content - :bytes metadata item - :lines metadata item + The first 8 fields MUST be the following, in order: + "0" or article number (see below) + Subject header content + From header content + Date header content + Message-ID header content + References header content + :bytes metadata item + :lines metadata item */ fmt.Fprintf(dw, "%.6d\t%s\t\"%s\" <%s@%s>\t%s\t%s\t%s\r\n", model.NNTPID(), - safeOver(model.Subject()), - safeOver(model.Name()), safeOver(model.Name()), safeOver(model.Frontend()), - safeOver(model.Date()), - safeOver(model.MessageID()), - safeOver(model.Reference())) + safeHeader(model.Subject()), + safeHeader(model.Name()), safeHeader(model.Name()), safeHeader(model.Frontend()), + safeHeader(model.Date()), + safeHeader(model.MessageID()), + safeHeader(model.Reference())) } } } diff --git a/contrib/backends/srndv2/src/srnd/util.go b/contrib/backends/srndv2/src/srnd/util.go index 70db80d..e3c6706 100644 --- a/contrib/backends/srndv2/src/srnd/util.go +++ b/contrib/backends/srndv2/src/srnd/util.go @@ -26,6 +26,7 @@ import ( "strconv" "strings" "time" + "unicode" ) func DelFile(fname string) { @@ -161,6 +162,18 @@ func nntpSanitize(data string) (ret string) { return ret } +var safeHeaderReplacer = strings.NewReplacer( + "\t", " ", + "\n", string(unicode.ReplacementChar), + "\r", string(unicode.ReplacementChar), + "\000", string(unicode.ReplacementChar)) + +// safeHeader replaces dangerous stuff from header, +// also replaces space with tab for XOVER/OVER output +func safeHeader(s string) string { + return strings.TrimSpace(safeHeaderReplacer.Replace(s)) +} + type int64Sorter []int64 func (self int64Sorter) Len() int { From 40e4ae1fc4b5ff868ad4c3e92db77bec1af55dbd Mon Sep 17 00:00:00 2001 From: cathugger Date: Wed, 12 Dec 2018 18:38:58 +0200 Subject: [PATCH 5/7] 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. --- .../backends/srndv2/src/srnd/frontend_http.go | 34 ++--- contrib/backends/srndv2/src/srnd/message.go | 5 +- contrib/backends/srndv2/src/srnd/util.go | 117 ++++++++++++++++++ 3 files changed, 129 insertions(+), 27 deletions(-) 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 { From af161968c8c9be6875d1268861a8cf3563eb7fd8 Mon Sep 17 00:00:00 2001 From: cathugger Date: Wed, 12 Dec 2018 20:54:16 +0200 Subject: [PATCH 6/7] srnd: avoid quoting in some cases --- contrib/backends/srndv2/src/srnd/util.go | 27 ++++++++++++------------ 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/contrib/backends/srndv2/src/srnd/util.go b/contrib/backends/srndv2/src/srnd/util.go index a7efb67..5cdcf44 100644 --- a/contrib/backends/srndv2/src/srnd/util.go +++ b/contrib/backends/srndv2/src/srnd/util.go @@ -28,7 +28,6 @@ import ( "strings" "time" "unicode" - "unicode/utf8" ) func DelFile(fname string) { @@ -201,21 +200,15 @@ func isQtext(r rune) bool { } 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]) - } + for _, r := range s { + if isQtext(r) || isWSP(r) { + b.WriteRune(r) + } else { b.WriteByte('\\') b.WriteRune(r) - last = i + utf8.RuneLen(r) } } - if last < len(s) { - b.WriteString(s[last:]) - } b.WriteByte('"') } @@ -227,14 +220,20 @@ func formatAddress(name, email string) string { if name != "" { needsEncoding := false needsQuoting := false - for _, r := range name { + for i, r := range name { if r >= 0x80 || (!isWSP(r) && !isVchar(r)) { needsEncoding = true break } - if !isAtext(r) { - needsQuoting = true + if isAtext(r) { + continue } + if r == ' ' && i > 0 && name[i-1] != ' ' && i < len(name)-1 { + // allow spaces but only surrounded by non-spaces + // otherwise they will be removed by receiver + continue + } + needsQuoting = true } if needsEncoding { From b54e1d84da06d584bfdaebf3049e05db1832d31e Mon Sep 17 00:00:00 2001 From: cathugger Date: Sat, 15 Dec 2018 02:05:00 +0200 Subject: [PATCH 7/7] use correct header, reserve some msgids, tweaks --- .../backends/srndv2/src/srnd/frontend_http.go | 22 +++++++++++++++---- contrib/backends/srndv2/src/srnd/util.go | 10 ++++++--- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/contrib/backends/srndv2/src/srnd/frontend_http.go b/contrib/backends/srndv2/src/srnd/frontend_http.go index 9f2e661..0dafd1a 100644 --- a/contrib/backends/srndv2/src/srnd/frontend_http.go +++ b/contrib/backends/srndv2/src/srnd/frontend_http.go @@ -743,7 +743,7 @@ func (self *httpFrontend) handle_postRequest(pr *postRequest, b bannedFunc, e er } ref := pr.Reference - if len(ref) > 0 { + if ref != "" { if ValidMessageID(ref) { if self.daemon.database.HasArticleLocal(ref) { nntp.headers.Set("References", ref) @@ -796,7 +796,7 @@ func (self *httpFrontend) handle_postRequest(pr *postRequest, b bannedFunc, e er } nntp.headers.Set("Subject", safeHeader(subject)) - if isSage(subject) { + if isSage(subject) && ref != "" { nntp.headers.Set("X-Sage", "1") } @@ -821,7 +821,7 @@ func (self *httpFrontend) handle_postRequest(pr *postRequest, b bannedFunc, e er msgid = genMessageID(pr.Frontend) } - nntp.headers.Set("From", formatAddress(safeHeader(name), "poster@" + pr.Frontend)) + nntp.headers.Set("From", formatAddress(safeHeader(name), "poster@"+pr.Frontend)) nntp.headers.Set("Message-ID", msgid) // set message @@ -834,7 +834,21 @@ func (self *httpFrontend) handle_postRequest(pr *postRequest, b bannedFunc, e er } if len(cites) > 0 { - nntp.headers.Set("Reply-To", strings.Join(cites, " ")) + if ref == "" && len(cites) == 1 { + /* + this is workaround for: + + {RFC 5322} + If the parent message does not contain + a "References:" field but does have an "In-Reply-To:" field + containing a single message identifier, then the "References:" field + will contain the contents of the parent's "In-Reply-To:" field + followed by the contents of the parent's "Message-ID:" field (if + any). + */ + cites = append(cites, "<0>") + } + nntp.headers.Set("In-Reply-To", strings.Join(cites, " ")) } // set date diff --git a/contrib/backends/srndv2/src/srnd/util.go b/contrib/backends/srndv2/src/srnd/util.go index 5cdcf44..cd0f8eb 100644 --- a/contrib/backends/srndv2/src/srnd/util.go +++ b/contrib/backends/srndv2/src/srnd/util.go @@ -92,6 +92,10 @@ func ValidMessageID(id string) bool { strings.IndexAny(id[1:len(id)-1], "/\\") < 0 } +func ReservedMessageID(id string) bool { + return id == "<0>" || id == "" +} + // message id hash func HashMessageID(msgid string) string { return fmt.Sprintf("%x", sha1.Sum([]byte(msgid))) @@ -898,7 +902,7 @@ func storeMessage(daemon *NNTPDaemon, hdr textproto.MIMEHeader, body io.Reader) log.Println("dropping message with invalid mime header, no message-id") _, err = io.Copy(Discard, body) return - } else if ValidMessageID(msgid) { + } else if ValidMessageID(msgid) && !ReservedMessageID(msgid) { f = daemon.store.CreateFile(msgid) } else { // invalid message-id @@ -914,9 +918,9 @@ func storeMessage(daemon *NNTPDaemon, hdr textproto.MIMEHeader, body io.Reader) } // ask for replies - replyTos := strings.Split(hdr.Get("Reply-To"), " ") + replyTos := strings.Split(hdr.Get("In-Reply-To"), " ") for _, reply := range replyTos { - if ValidMessageID(reply) { + if ValidMessageID(reply) && !ReservedMessageID(reply) { if !daemon.store.HasArticle(reply) { go daemon.askForArticle(reply) }