diff --git a/README.md b/README.md index 81d7f77..a72906f 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,13 @@ nntp.go An NNTP (news) Client package for go (golang). Forked from [nntp-go](http://code.google.com/p/nntp-go/) to bring it up to date. +Updates +------- +- added IHAVE command +- use XOVER if OVER is unrecognized +- set overview.Bytes to 0 if bytes value is empty +- set overview.Lines to 0 if lines value is empty + Example ------- diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..fb0c009 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/Tensai75/nntp + +go 1.16 diff --git a/nntp.go b/nntp.go index 4df1fd6..0604867 100644 --- a/nntp.go +++ b/nntp.go @@ -12,7 +12,6 @@ import ( "crypto/tls" "fmt" "io" - "io/ioutil" "net" "net/http" "sort" @@ -117,7 +116,7 @@ func (r *bodyReader) Read(p []byte) (n int, err error) { } func (r *bodyReader) discard() error { - _, err := ioutil.ReadAll(r) + _, err := io.ReadAll(r) return err } @@ -207,8 +206,8 @@ func newConn(c net.Conn) (res *Conn, err error) { // make the connection. // // Example: -// conn, err := nntp.Dial("tcp", "my.news:nntp") // +// conn, err := nntp.Dial("tcp", "my.news:nntp") func Dial(network, addr string) (*Conn, error) { c, err := net.Dial(network, addr) if err != nil { @@ -370,8 +369,17 @@ type MessageOverview struct { // Overview returns overviews of all messages in the current group with message number between // begin and end, inclusive. func (c *Conn) Overview(begin, end int) ([]MessageOverview, error) { - if _, _, err := c.cmd(224, "OVER %d-%d", begin, end); err != nil { - return nil, err + if code, _, err := c.cmd(224, "OVER %d-%d", begin, end); err != nil { + // if error is "500 Unknown Command" (correct response according to RFC 3977), or + // if error is "400 Unrecognized command" or (wrong response sent by newshositng, eweka, tweaknews and maybe others...) + // try the XOVER command + if code == 500 || code == 400 { + if _, _, err := c.cmd(224, "XOVER %d-%d", begin, end); err != nil { + return nil, err + } + } else { + return nil, err + } } lines, err := c.readStrings() @@ -381,36 +389,71 @@ func (c *Conn) Overview(begin, end int) ([]MessageOverview, error) { result := make([]MessageOverview, 0, len(lines)) for _, line := range lines { - overview := MessageOverview{} - ss := strings.SplitN(strings.TrimSpace(line), "\t", 9) - if len(ss) < 8 { - return nil, ProtocolError("short header listing line: " + line + strconv.Itoa(len(ss))) - } - overview.MessageNumber, err = strconv.Atoi(ss[0]) - if err != nil { - return nil, ProtocolError("bad message number '" + ss[0] + "' in line: " + line) - } - overview.Subject = ss[1] - overview.From = ss[2] - overview.Date, err = parseDate(ss[3]) + overview, err := ParseOverviewLine(line) if err != nil { - // Inability to parse date is not fatal: the field in the message may be broken or missing. - overview.Date = time.Time{} + return nil, err } - overview.MessageId = ss[4] - overview.References = strings.Split(ss[5], " ") // Message-Id's contain no spaces, so this is safe. + result = append(result, overview) + } + return result, nil +} + +func ParseOverviewLine(line string) (MessageOverview, error) { + err := error(nil) + overview := MessageOverview{} + ss := strings.SplitN(strings.TrimSpace(line), "\t", 9) + if len(ss) < 8 { + return MessageOverview{}, ProtocolError("short header listing line: " + line + strconv.Itoa(len(ss))) + } + overview.MessageNumber, err = strconv.Atoi(ss[0]) + if err != nil { + return MessageOverview{}, ProtocolError("bad message number '" + ss[0] + "' in line: " + line) + } + overview.Subject = ss[1] + overview.From = ss[2] + overview.Date, err = parseDate(ss[3]) + if err != nil { + // Inability to parse date is not fatal: the field in the message may be broken or missing. + overview.Date = time.Time{} + } + overview.MessageId = ss[4] + overview.References = strings.Split(ss[5], " ") // Message-Id's contain no spaces, so this is safe. + if ss[6] == "" { + overview.Bytes = 0 + } else { overview.Bytes, err = strconv.Atoi(ss[6]) if err != nil { - return nil, ProtocolError("bad byte count '" + ss[6] + "'in line:" + line) + return MessageOverview{}, ProtocolError("bad byte count '" + ss[6] + "'in line:" + line) } + } + if ss[7] == "" { + overview.Lines = 0 + } else { overview.Lines, err = strconv.Atoi(ss[7]) if err != nil { - return nil, ProtocolError("bad line count '" + ss[7] + "'in line:" + line) + return MessageOverview{}, ProtocolError("bad line count '" + ss[7] + "'in line:" + line) } - overview.Extra = append([]string{}, ss[8:]...) - result = append(result, overview) } - return result, nil + overview.Extra = append([]string{}, ss[8:]...) + return overview, nil +} + +// Overview returns a reader for overviews of all messages in the current group with message number between +// begin and end, inclusive. +func (c *Conn) OverviewReader(begin, end int) (*bufio.Reader, error) { + if code, _, err := c.cmd(224, "OVER %d-%d", begin, end); err != nil { + // if error is "500 Unknown Command" (correct response according to RFC 3977), or + // if error is "400 Unrecognized command" or (wrong response sent by newshositng, eweka, tweaknews and maybe others...) + // try the XOVER command + if code == 500 || code == 400 { + if _, _, err := c.cmd(224, "XOVER %d-%d", begin, end); err != nil { + return nil, err + } + } else { + return nil, err + } + } + return c.r, nil } // parseGroups is used to parse a list of group states. @@ -460,10 +503,9 @@ func (c *Conn) Date() (time.Time, error) { // List returns a list of groups present on the server. // Valid forms are: // -// List() - return active groups -// List(keyword) - return different kinds of information about groups -// List(keyword, pattern) - filter groups against a glob-like pattern called a wildmat -// +// List() - return active groups +// List(keyword) - return different kinds of information about groups +// List(keyword, pattern) - filter groups against a glob-like pattern called a wildmat func (c *Conn) List(a ...string) ([]string, error) { if len(a) > 2 { return nil, ProtocolError("List only takes up to 2 arguments") @@ -495,7 +537,7 @@ func (c *Conn) Group(group string) (number, low, high int, err error) { } var n [3]int - for i, _ := range n { + for i := range n { c, e := strconv.Atoi(ss[i]) if e != nil { err = ProtocolError("bad group response: " + line) @@ -595,11 +637,8 @@ func (c *Conn) Body(id string) (io.Reader, error) { return c.body(), nil } -// RawPost reads a text-formatted article from r and posts it to the server. -func (c *Conn) RawPost(r io.Reader) error { - if _, _, err := c.cmd(3, "POST"); err != nil { - return err - } +// sendLines sends the lines of a text-formatted article from r to the server. +func (c *Conn) sendLines(r io.Reader) error { br := bufio.NewReader(r) eof := false for { @@ -612,9 +651,7 @@ func (c *Conn) RawPost(r io.Reader) error { if eof && len(line) == 0 { break } - if strings.HasSuffix(line, "\n") { - line = line[0 : len(line)-1] - } + line = strings.TrimSuffix(line, "\n") var prefix string if strings.HasPrefix(line, ".") { prefix = "." @@ -627,7 +664,17 @@ func (c *Conn) RawPost(r io.Reader) error { break } } + return nil +} +// RawPost reads a text-formatted article from r and posts it to the server. +func (c *Conn) RawPost(r io.Reader) error { + if _, _, err := c.cmd(3, "POST"); err != nil { + return err + } + if err := c.sendLines(r); err != nil { + return err + } if _, _, err := c.cmd(240, "."); err != nil { return err } @@ -639,8 +686,28 @@ func (c *Conn) Post(a *Article) error { return c.RawPost(&articleReader{a: a}) } +// RawIHave reads a text-formatted article from r and presents it to the server with the IHAVE command. +func (c *Conn) RawIHave(r io.Reader) error { + if _, _, err := c.cmd(3, "IHAVE"); err != nil { + return err + } + if err := c.sendLines(r); err != nil { + return err + } + if _, _, err := c.cmd(235, "."); err != nil { + return err + } + return nil +} + +// IHave presents an article to the server with the IHAVE command. +func (c *Conn) IHave(a *Article) error { + return c.RawIHave(&articleReader{a: a}) +} + // Quit sends the QUIT command and closes the connection to the server. func (c *Conn) Quit() error { + c.r.Discard(c.r.Buffered()) _, _, err := c.cmd(0, "QUIT") c.conn.Close() c.close = true @@ -699,7 +766,7 @@ func readKeyValue(b *bufio.Reader) (key, value string, err error) { } key = string(line[0:i]) - if strings.Index(key, " ") >= 0 { + if strings.Contains(key, " ") { // Key field has space - no good. goto Malformed } diff --git a/nntp_test.go b/nntp_test.go index cca3760..ef0dbe8 100644 --- a/nntp_test.go +++ b/nntp_test.go @@ -9,7 +9,6 @@ import ( "bytes" "fmt" "io" - "io/ioutil" "strings" "testing" "time" @@ -86,7 +85,7 @@ func TestBasics(t *testing.T) { if err != nil { t.Fatal("should be able to fetch the low article: " + err.Error()) } - body, err := ioutil.ReadAll(a.Body) + body, err := io.ReadAll(a.Body) if err != nil { t.Fatal("error reading reader: " + err.Error()) } @@ -142,7 +141,7 @@ Body. if err != nil { t.Fatal("should be able to fetch the low article body" + err.Error()) } - if _, err = ioutil.ReadAll(r); err != nil { + if _, err = io.ReadAll(r); err != nil { t.Fatal("error reading reader: " + err.Error()) }