Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
-------

Expand Down
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/Tensai75/nntp

go 1.16
147 changes: 107 additions & 40 deletions nntp.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import (
"crypto/tls"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"sort"
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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()
Expand All @@ -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.
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand All @@ -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 = "."
Expand All @@ -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
}
Expand All @@ -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
Expand Down Expand Up @@ -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
}
Expand Down
5 changes: 2 additions & 3 deletions nntp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
"bytes"
"fmt"
"io"
"io/ioutil"
"strings"
"testing"
"time"
Expand Down Expand Up @@ -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())
}
Expand Down Expand Up @@ -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())
}

Expand Down