summaryrefslogtreecommitdiff
path: root/src/pkg/nntp
diff options
context:
space:
mode:
authorConrad Meyer <cemeyer@cs.washington.edu>2010-04-04 23:23:48 -0700
committerConrad Meyer <cemeyer@cs.washington.edu>2010-04-04 23:23:48 -0700
commit26775452ec17aeb592a113396d0227af25da9e1b (patch)
treecd6aaed9361a8f5ef25dfc58b88ac13085ac6c3c /src/pkg/nntp
parenta8583264ac09dbf3f376cfd2371814ccb329c635 (diff)
downloadgolang-26775452ec17aeb592a113396d0227af25da9e1b.tar.gz
nntp: new package, NNTP client
R=rsc, rsc1 CC=golang-dev http://codereview.appspot.com/808041 Committer: Russ Cox <rsc@golang.org>
Diffstat (limited to 'src/pkg/nntp')
-rw-r--r--src/pkg/nntp/Makefile11
-rw-r--r--src/pkg/nntp/nntp.go709
-rw-r--r--src/pkg/nntp/nntp_test.go241
3 files changed, 961 insertions, 0 deletions
diff --git a/src/pkg/nntp/Makefile b/src/pkg/nntp/Makefile
new file mode 100644
index 000000000..3cf3164ce
--- /dev/null
+++ b/src/pkg/nntp/Makefile
@@ -0,0 +1,11 @@
+# Copyright 2009 The Go Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+
+include ../../Make.$(GOARCH)
+
+TARG=nntp
+GOFILES=\
+ nntp.go
+
+include ../../Make.pkg
diff --git a/src/pkg/nntp/nntp.go b/src/pkg/nntp/nntp.go
new file mode 100644
index 000000000..e78b036f5
--- /dev/null
+++ b/src/pkg/nntp/nntp.go
@@ -0,0 +1,709 @@
+// Copyright 2009 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// The nntp package implements a client for the news protocol NNTP,
+// as defined in RFC 3977.
+package nntp
+
+import (
+ "bufio"
+ "bytes"
+ "container/vector"
+ "fmt"
+ "http"
+ "io"
+ "io/ioutil"
+ "os"
+ "net"
+ "sort"
+ "strconv"
+ "strings"
+ "time"
+)
+
+// timeFormatNew is the NNTP time format string for NEWNEWS / NEWGROUPS
+const timeFormatNew = "20060102 150405"
+
+// timeFormatDate is the NNTP time format string for responses to the DATE command
+const timeFormatDate = "20060102150405"
+
+// An Error represents an error response from an NNTP server.
+type Error struct {
+ Code uint
+ Msg string
+}
+
+// A ProtocolError represents responses from an NNTP server
+// that seem incorrect for NNTP.
+type ProtocolError string
+
+// A Conn represents a connection to an NNTP server. The connection with
+// an NNTP server is stateful; it keeps track of what group you have
+// selected, if any, and (if you have a group selected) which article is
+// current, next, or previous.
+//
+// Some methods that return information about a specific message take
+// either a message-id, which is global across all NNTP servers, groups,
+// and messages, or a message-number, which is an integer number that is
+// local to the NNTP session and currently selected group.
+//
+// For all methods that return an io.Reader (or an *Article, which contains
+// an io.Reader), that io.Reader is only valid until the next call to a
+// method of Conn.
+type Conn struct {
+ conn io.WriteCloser
+ r *bufio.Reader
+ br *bodyReader
+ close bool
+}
+
+// A Group gives information about a single news group on the server.
+type Group struct {
+ Name string
+ // High and low message-numbers
+ High, Low int
+ // Status indicates if general posting is allowed --
+ // typical values are "y", "n", or "m".
+ Status string
+}
+
+// An Article represents an NNTP article.
+type Article struct {
+ Header map[string][]string
+ Body io.Reader
+}
+
+// A bodyReader satisfies reads by reading from the connection
+// until it finds a line containing just .
+type bodyReader struct {
+ c *Conn
+ eof bool
+ buf *bytes.Buffer
+}
+
+var dotnl = []byte(".\n")
+var dotdot = []byte("..")
+
+func (r *bodyReader) Read(p []byte) (n int, err os.Error) {
+ if r.eof {
+ return 0, os.EOF
+ }
+ if r.buf == nil {
+ r.buf = &bytes.Buffer{}
+ }
+ if r.buf.Len() == 0 {
+ b, err := r.c.r.ReadBytes('\n')
+ if err != nil {
+ return 0, err
+ }
+ // canonicalize newlines
+ if b[len(b)-2] == '\r' { // crlf->lf
+ b = b[0 : len(b)-1]
+ b[len(b)-1] = '\n'
+ }
+ // stop on .
+ if bytes.Equal(b, dotnl) {
+ r.eof = true
+ return 0, os.EOF
+ }
+ // unescape leading ..
+ if bytes.HasPrefix(b, dotdot) {
+ b = b[1:]
+ }
+ r.buf.Write(b)
+ }
+ n, _ = r.buf.Read(p)
+ return
+}
+
+func (r *bodyReader) discard() os.Error {
+ _, err := ioutil.ReadAll(r)
+ return err
+}
+
+// articleReader satisfies reads by dumping out an article's headers
+// and body.
+type articleReader struct {
+ a *Article
+ headerdone bool
+ headerbuf *bytes.Buffer
+}
+
+func (r *articleReader) Read(p []byte) (n int, err os.Error) {
+ if r.headerbuf == nil {
+ buf := new(bytes.Buffer)
+ for k, fv := range r.a.Header {
+ for _, v := range fv {
+ fmt.Fprintf(buf, "%s: %s\n", k, v)
+ }
+ }
+ if r.a.Body != nil {
+ fmt.Fprintf(buf, "\n")
+ }
+ r.headerbuf = buf
+ }
+ if !r.headerdone {
+ n, err = r.headerbuf.Read(p)
+ if err == os.EOF {
+ err = nil
+ r.headerdone = true
+ }
+ if n > 0 {
+ return
+ }
+ }
+ if r.a.Body != nil {
+ n, err = r.a.Body.Read(p)
+ if err == os.EOF {
+ r.a.Body = nil
+ }
+ return
+ }
+ return 0, os.EOF
+}
+
+func (a *Article) String() string {
+ id, ok := a.Header["Message-Id"]
+ if !ok {
+ return "[NNTP article]"
+ }
+ return fmt.Sprintf("[NNTP article %s]", id[0])
+}
+
+func (a *Article) WriteTo(w io.Writer) (int64, os.Error) {
+ return io.Copy(w, &articleReader{a: a})
+}
+
+func (p ProtocolError) String() string {
+ return string(p)
+}
+
+func (e Error) String() string {
+ return fmt.Sprintf("%03d %s", e.Code, e.Msg)
+}
+
+func maybeId(cmd, id string) string {
+ if len(id) > 0 {
+ return cmd + " " + id
+ }
+ return cmd
+}
+
+// Dial connects to an NNTP server.
+// The network and addr are passed to net.Dial to
+// make the connection.
+//
+// Example:
+// conn, err := nntp.Dial("tcp", "my.news:nntp")
+//
+func Dial(network, addr string) (*Conn, os.Error) {
+ res := new(Conn)
+ c, err := net.Dial(network, "", addr)
+ if err != nil {
+ return nil, err
+ }
+
+ res.conn = c
+ if res.r, err = bufio.NewReaderSize(c, 4096); err != nil {
+ return nil, err
+ }
+
+ _, err = res.r.ReadString('\n')
+ if err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
+
+func (c *Conn) body() io.Reader {
+ c.br = &bodyReader{c: c}
+ return c.br
+}
+
+// readStrings reads a list of strings from the NNTP connection,
+// stopping at a line containing only a . (Convenience method for
+// LIST, etc.)
+func (c *Conn) readStrings() ([]string, os.Error) {
+ var sv vector.StringVector
+ for {
+ line, err := c.r.ReadString('\n')
+ if err != nil {
+ return nil, err
+ }
+ if strings.HasSuffix(line, "\r\n") {
+ line = line[0 : len(line)-2]
+ } else if strings.HasSuffix(line, "\n") {
+ line = line[0 : len(line)-1]
+ }
+ if line == "." {
+ break
+ }
+ sv.Push(line)
+ }
+ return []string(sv), nil
+}
+
+// Authenticate logs in to the NNTP server.
+// It only sends the password if the server requires one.
+func (c *Conn) Authenticate(username, password string) os.Error {
+ code, _, err := c.cmd(2, "AUTHINFO USER %s", username)
+ if code/100 == 3 {
+ _, _, err = c.cmd(2, "AUTHINFO PASS %s", password)
+ }
+ return err
+}
+
+// cmd executes an NNTP command:
+// It sends the command given by the format and arguments, and then
+// reads the response line. If expectCode > 0, the status code on the
+// response line must match it. 1 digit expectCodes only check the first
+// digit of the status code, etc.
+func (c *Conn) cmd(expectCode uint, format string, args ...interface{}) (code uint, line string, err os.Error) {
+ if c.close {
+ return 0, "", ProtocolError("connection closed")
+ }
+ if c.br != nil {
+ if err := c.br.discard(); err != nil {
+ return 0, "", err
+ }
+ c.br = nil
+ }
+ if _, err := fmt.Fprintf(c.conn, format+"\r\n", args); err != nil {
+ return 0, "", err
+ }
+ line, err = c.r.ReadString('\n')
+ if err != nil {
+ return 0, "", err
+ }
+ line = strings.TrimSpace(line)
+ if len(line) < 4 || line[3] != ' ' {
+ return 0, "", ProtocolError("short response: " + line)
+ }
+ code, err = strconv.Atoui(line[0:3])
+ if err != nil {
+ return 0, "", ProtocolError("invalid response code: " + line)
+ }
+ line = line[4:]
+ if 1 <= expectCode && expectCode < 10 && code/100 != expectCode ||
+ 10 <= expectCode && expectCode < 100 && code/10 != expectCode ||
+ 100 <= expectCode && expectCode < 1000 && code != expectCode {
+ err = Error{code, line}
+ }
+ return
+}
+
+// ModeReader switches the NNTP server to "reader" mode, if it
+// is a mode-switching server.
+func (c *Conn) ModeReader() os.Error {
+ _, _, err := c.cmd(20, "MODE READER")
+ return err
+}
+
+// NewGroups returns a list of groups added since the given time.
+func (c *Conn) NewGroups(since *time.Time) ([]Group, os.Error) {
+ if _, _, err := c.cmd(231, "NEWGROUPS %s GMT", since.Format(timeFormatNew)); err != nil {
+ return nil, err
+ }
+ return c.readGroups()
+}
+
+func (c *Conn) readGroups() ([]Group, os.Error) {
+ lines, err := c.readStrings()
+ if err != nil {
+ return nil, err
+ }
+ return parseGroups(lines)
+}
+
+// NewNews returns a list of the IDs of articles posted
+// to the given group since the given time.
+func (c *Conn) NewNews(group string, since *time.Time) ([]string, os.Error) {
+ if _, _, err := c.cmd(230, "NEWNEWS %s %s GMT", group, since.Format(timeFormatNew)); err != nil {
+ return nil, err
+ }
+
+ id, err := c.readStrings()
+ if err != nil {
+ return nil, err
+ }
+
+ sort.SortStrings(id)
+ w := 0
+ for r, s := range id {
+ if r == 0 || id[r-1] != s {
+ id[w] = s
+ w++
+ }
+ }
+ id = id[0:w]
+
+ return id, nil
+}
+
+// parseGroups is used to parse a list of group states.
+func parseGroups(lines []string) ([]Group, os.Error) {
+ var res vector.Vector
+ for _, line := range lines {
+ ss := strings.Split(strings.TrimSpace(line), " ", 4)
+ if len(ss) < 4 {
+ return nil, ProtocolError("short group info line: " + line)
+ }
+ high, err := strconv.Atoi(ss[1])
+ if err != nil {
+ return nil, ProtocolError("bad number in line: " + line)
+ }
+ low, err := strconv.Atoi(ss[2])
+ if err != nil {
+ return nil, ProtocolError("bad number in line: " + line)
+ }
+ res.Push(&Group{ss[0], high, low, ss[3]})
+ }
+ realres := make([]Group, res.Len())
+ i := 0
+ for v := range res.Iter() {
+ realres[i] = *v.(*Group)
+ i++
+ }
+ return realres, nil
+}
+
+// Capabilities returns a list of features this server performs.
+// Not all servers support capabilities.
+func (c *Conn) Capabilities() ([]string, os.Error) {
+ if _, _, err := c.cmd(101, "CAPABILITIES"); err != nil {
+ return nil, err
+ }
+ return c.readStrings()
+}
+
+// Date returns the current time on the server.
+// Typically the time is later passed to NewGroups or NewNews.
+func (c *Conn) Date() (*time.Time, os.Error) {
+ _, line, err := c.cmd(111, "DATE")
+ if err != nil {
+ return nil, err
+ }
+ t, err := time.Parse(timeFormatDate, line)
+ if err != nil {
+ return nil, ProtocolError("invalid time: " + line)
+ }
+ return t, nil
+}
+
+// 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
+//
+func (c *Conn) List(a ...string) ([]string, os.Error) {
+ if len(a) > 2 {
+ return nil, ProtocolError("List only takes up to 2 arguments")
+ }
+ cmd := "LIST"
+ if len(a) > 0 {
+ cmd += " " + a[0]
+ if len(a) > 1 {
+ cmd += " " + a[1]
+ }
+ }
+ if _, _, err := c.cmd(215, cmd); err != nil {
+ return nil, err
+ }
+ return c.readStrings()
+}
+
+// Group changes the current group.
+func (c *Conn) Group(group string) (number, low, high int, err os.Error) {
+ _, line, err := c.cmd(211, "GROUP %s", group)
+ if err != nil {
+ return
+ }
+
+ ss := strings.Split(line, " ", 4) // intentional -- we ignore optional message
+ if len(ss) < 3 {
+ err = ProtocolError("bad group response: " + line)
+ return
+ }
+
+ var n [3]int
+ for i, _ := range n {
+ c, err := strconv.Atoi(ss[i])
+ if err != nil {
+ err = ProtocolError("bad group response: " + line)
+ return
+ }
+ n[i] = c
+ }
+ number, low, high = n[0], n[1], n[2]
+ return
+}
+
+// Help returns the server's help text.
+func (c *Conn) Help() (io.Reader, os.Error) {
+ if _, _, err := c.cmd(100, "HELP"); err != nil {
+ return nil, err
+ }
+ return c.body(), nil
+}
+
+// nextLastStat performs the work for NEXT, LAST, and STAT.
+func (c *Conn) nextLastStat(cmd, id string) (string, string, os.Error) {
+ _, line, err := c.cmd(223, maybeId(cmd, id))
+ if err != nil {
+ return "", "", err
+ }
+ ss := strings.Split(line, " ", 3) // optional comment ignored
+ if len(ss) < 2 {
+ return "", "", ProtocolError("Bad response to " + cmd + ": " + line)
+ }
+ return ss[0], ss[1], nil
+}
+
+// Stat looks up the message with the given id and returns its
+// message number in the current group, and vice versa.
+// The returned message number can be "0" if the current group
+// isn't one of the groups the message was posted to.
+func (c *Conn) Stat(id string) (number, msgid string, err os.Error) {
+ return c.nextLastStat("STAT", id)
+}
+
+// Last selects the previous article, returning its message number and id.
+func (c *Conn) Last() (number, msgid string, err os.Error) {
+ return c.nextLastStat("LAST", "")
+}
+
+// Next selects the next article, returning its message number and id.
+func (c *Conn) Next() (number, msgid string, err os.Error) {
+ return c.nextLastStat("NEXT", "")
+}
+
+// ArticleText returns the article named by id as an io.Reader.
+// The article is in plain text format, not NNTP wire format.
+func (c *Conn) ArticleText(id string) (io.Reader, os.Error) {
+ if _, _, err := c.cmd(220, maybeId("ARTICLE", id)); err != nil {
+ return nil, err
+ }
+ return c.body(), nil
+}
+
+// Article returns the article named by id as an *Article.
+func (c *Conn) Article(id string) (*Article, os.Error) {
+ if _, _, err := c.cmd(220, maybeId("ARTICLE", id)); err != nil {
+ return nil, err
+ }
+ r := bufio.NewReader(c.body())
+ res, err := c.readHeader(r)
+ if err != nil {
+ return nil, err
+ }
+ res.Body = r
+ return res, nil
+}
+
+// HeadText returns the header for the article named by id as an io.Reader.
+// The article is in plain text format, not NNTP wire format.
+func (c *Conn) HeadText(id string) (io.Reader, os.Error) {
+ if _, _, err := c.cmd(221, maybeId("HEAD", id)); err != nil {
+ return nil, err
+ }
+ return c.body(), nil
+}
+
+// Head returns the header for the article named by id as an *Article.
+// The Body field in the Article is nil.
+func (c *Conn) Head(id string) (*Article, os.Error) {
+ if _, _, err := c.cmd(221, maybeId("HEAD", id)); err != nil {
+ return nil, err
+ }
+ return c.readHeader(bufio.NewReader(c.body()))
+}
+
+// Body returns the body for the article named by id as an io.Reader.
+func (c *Conn) Body(id string) (io.Reader, os.Error) {
+ if _, _, err := c.cmd(222, maybeId("BODY", id)); err != nil {
+ return nil, err
+ }
+ 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) os.Error {
+ if _, _, err := c.cmd(3, "POST"); err != nil {
+ return err
+ }
+ br := bufio.NewReader(r)
+ eof := false
+ for {
+ line, err := br.ReadString('\n')
+ if err == os.EOF {
+ eof = true
+ } else if err != nil {
+ return err
+ }
+ if eof && len(line) == 0 {
+ break
+ }
+ if strings.HasSuffix(line, "\n") {
+ line = line[0 : len(line)-1]
+ }
+ var prefix string
+ if strings.HasPrefix(line, ".") {
+ prefix = "."
+ }
+ _, err = fmt.Fprintf(c.conn, "%s%s\r\n", prefix, line)
+ if err != nil {
+ return err
+ }
+ if eof {
+ break
+ }
+ }
+
+ if _, _, err := c.cmd(240, "."); err != nil {
+ return err
+ }
+ return nil
+}
+
+// Post posts an article to the server.
+func (c *Conn) Post(a *Article) os.Error {
+ return c.RawPost(&articleReader{a: a})
+}
+
+// Quit sends the QUIT command and closes the connection to the server.
+func (c *Conn) Quit() os.Error {
+ _, _, err := c.cmd(0, "QUIT")
+ c.conn.Close()
+ c.close = true
+ return err
+}
+
+// Functions after this point are mostly copy-pasted from http
+// (though with some modifications). They should be factored out to
+// a common library.
+
+// Read a line of bytes (up to \n) from b.
+// Give up if the line exceeds maxLineLength.
+// The returned bytes are a pointer into storage in
+// the bufio, so they are only valid until the next bufio read.
+func readLineBytes(b *bufio.Reader) (p []byte, err os.Error) {
+ if p, err = b.ReadSlice('\n'); err != nil {
+ // We always know when EOF is coming.
+ // If the caller asked for a line, there should be a line.
+ if err == os.EOF {
+ err = io.ErrUnexpectedEOF
+ }
+ return nil, err
+ }
+
+ // Chop off trailing white space.
+ var i int
+ for i = len(p); i > 0; i-- {
+ if c := p[i-1]; c != ' ' && c != '\r' && c != '\t' && c != '\n' {
+ break
+ }
+ }
+ return p[0:i], nil
+}
+
+var colon = []byte{':'}
+
+// Read a key/value pair from b.
+// A key/value has the form Key: Value\r\n
+// and the Value can continue on multiple lines if each continuation line
+// starts with a space/tab.
+func readKeyValue(b *bufio.Reader) (key, value string, err os.Error) {
+ line, e := readLineBytes(b)
+ if e == io.ErrUnexpectedEOF {
+ return "", "", nil
+ } else if e != nil {
+ return "", "", e
+ }
+ if len(line) == 0 {
+ return "", "", nil
+ }
+
+ // Scan first line for colon.
+ i := bytes.Index(line, colon)
+ if i < 0 {
+ goto Malformed
+ }
+
+ key = string(line[0:i])
+ if strings.Index(key, " ") >= 0 {
+ // Key field has space - no good.
+ goto Malformed
+ }
+
+ // Skip initial space before value.
+ for i++; i < len(line); i++ {
+ if line[i] != ' ' && line[i] != '\t' {
+ break
+ }
+ }
+ value = string(line[i:])
+
+ // Look for extension lines, which must begin with space.
+ for {
+ c, e := b.ReadByte()
+ if c != ' ' && c != '\t' {
+ if e != os.EOF {
+ b.UnreadByte()
+ }
+ break
+ }
+
+ // Eat leading space.
+ for c == ' ' || c == '\t' {
+ if c, e = b.ReadByte(); e != nil {
+ if e == os.EOF {
+ e = io.ErrUnexpectedEOF
+ }
+ return "", "", e
+ }
+ }
+ b.UnreadByte()
+
+ // Read the rest of the line and add to value.
+ if line, e = readLineBytes(b); e != nil {
+ return "", "", e
+ }
+ value += " " + string(line)
+ }
+ return key, value, nil
+
+Malformed:
+ return "", "", ProtocolError("malformed header line: " + string(line))
+}
+
+// Internal. Parses headers in NNTP articles. Most of this is stolen from the http package,
+// and it should probably be split out into a generic RFC822 header-parsing package.
+func (c *Conn) readHeader(r *bufio.Reader) (res *Article, err os.Error) {
+ res = new(Article)
+ res.Header = make(map[string][]string)
+ for {
+ var key, value string
+ if key, value, err = readKeyValue(r); err != nil {
+ return nil, err
+ }
+ if key == "" {
+ break
+ }
+ key = http.CanonicalHeaderKey(key)
+ // RFC 3977 says nothing about duplicate keys' values being equivalent to
+ // a single key joined with commas, so we keep all values seperate.
+ oldvalue, present := res.Header[key]
+ if present {
+ sv := vector.StringVector(oldvalue)
+ sv.Push(value)
+ res.Header[key] = []string(sv)
+ } else {
+ res.Header[key] = []string{value}
+ }
+ }
+ return res, nil
+}
diff --git a/src/pkg/nntp/nntp_test.go b/src/pkg/nntp/nntp_test.go
new file mode 100644
index 000000000..9bd7bd6b6
--- /dev/null
+++ b/src/pkg/nntp/nntp_test.go
@@ -0,0 +1,241 @@
+// Copyright 2009 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package nntp
+
+import (
+ "bufio"
+ "bytes"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "os"
+ "strings"
+ "testing"
+ "time"
+)
+
+func TestSanityChecks(t *testing.T) {
+ if _, err := Dial("", ""); err == nil {
+ t.Fatal("Dial should require at least a destination address.")
+ }
+}
+
+type faker struct {
+ io.Writer
+}
+
+func (f faker) Close() os.Error {
+ return nil
+}
+
+func TestBasic(t *testing.T) {
+ basicServer = strings.Join(strings.Split(basicServer, "\n", 0), "\r\n")
+ basicClient = strings.Join(strings.Split(basicClient, "\n", 0), "\r\n")
+
+ var cmdbuf bytes.Buffer
+ var fake faker
+ fake.Writer = &cmdbuf
+
+ conn := &Conn{conn: fake, r: bufio.NewReader(strings.NewReader(basicServer))}
+
+ // Test some global commands that don't take arguments
+ if _, err := conn.Capabilities(); err != nil {
+ t.Fatal("should be able to request CAPABILITIES after connecting: " + err.String())
+ }
+
+ _, err := conn.Date()
+ if err != nil {
+ t.Fatal("should be able to send DATE: " + err.String())
+ }
+
+ /*
+ Test broken until time.Parse adds this format.
+ cdate := time.UTC()
+ if sdate.Year != cdate.Year || sdate.Month != cdate.Month || sdate.Day != cdate.Day {
+ t.Fatal("DATE seems off, probably erroneous: " + sdate.String())
+ }
+ */
+
+ // Test LIST (implicit ACTIVE)
+ if _, err = conn.List(); err != nil {
+ t.Fatal("LIST should work: " + err.String())
+ }
+
+ tt := new(time.Time)
+ tt.Year = 2010
+ tt.Month = 3
+ tt.Day = 1
+
+ const grp = "gmane.comp.lang.go.general"
+ _, l, h, err := conn.Group(grp)
+ if err != nil {
+ t.Fatal("Group shouldn't error: " + err.String())
+ }
+
+ // test STAT, NEXT, and LAST
+ if _, _, err = conn.Stat(""); err != nil {
+ t.Fatal("should be able to STAT after selecting a group: " + err.String())
+ }
+ if _, _, err = conn.Next(); err != nil {
+ t.Fatal("should be able to NEXT after selecting a group: " + err.String())
+ }
+ if _, _, err = conn.Last(); err != nil {
+ t.Fatal("should be able to LAST after a NEXT selecting a group: " + err.String())
+ }
+
+ // Can we grab articles?
+ a, err := conn.Article(fmt.Sprintf("%d", l))
+ if err != nil {
+ t.Fatal("should be able to fetch the low article: " + err.String())
+ }
+ body, err := ioutil.ReadAll(a.Body)
+ if err != nil {
+ t.Fatal("error reading reader: " + err.String())
+ }
+
+ // Test that the article body doesn't get mangled.
+ expectedbody := `Blah, blah.
+.A single leading .
+Fin.
+`
+ if !bytes.Equal([]byte(expectedbody), body) {
+ t.Fatalf("article body read incorrectly; got:\n%s\nExpected:\n%s", body, expectedbody)
+ }
+
+ // Test articleReader
+ expectedart := `Message-Id: <b@c.d>
+
+Body.
+`
+ a, err = conn.Article(fmt.Sprintf("%d", l+1))
+ if err != nil {
+ t.Fatal("shouldn't error reading article low+1: " + err.String())
+ }
+ var abuf bytes.Buffer
+ _, err = a.WriteTo(&abuf)
+ if err != nil {
+ t.Fatal("shouldn't error writing out article: " + err.String())
+ }
+ actualart := abuf.String()
+ if actualart != expectedart {
+ t.Fatalf("articleReader broke; got:\n%s\nExpected\n%s", actualart, expectedart)
+ }
+
+ // Just headers?
+ if _, err = conn.Head(fmt.Sprintf("%d", h)); err != nil {
+ t.Fatal("should be able to fetch the high article: " + err.String())
+ }
+
+ // Without an id?
+ if _, err = conn.Head(""); err != nil {
+ t.Fatal("should be able to fetch the selected article without specifying an id: " + err.String())
+ }
+
+ // How about bad articles? Do they error?
+ if _, err = conn.Head(fmt.Sprintf("%d", l-1)); err == nil {
+ t.Fatal("shouldn't be able to fetch articles lower than low")
+ }
+ if _, err = conn.Head(fmt.Sprintf("%d", h+1)); err == nil {
+ t.Fatal("shouldn't be able to fetch articles higher than high")
+ }
+
+ // Just the body?
+ r, err := conn.Body(fmt.Sprintf("%d", l))
+ if err != nil {
+ t.Fatal("should be able to fetch the low article body\n" + err.String())
+ }
+ if _, err = ioutil.ReadAll(r); err != nil {
+ t.Fatal("error reading reader: " + err.String())
+ }
+
+ if _, err = conn.NewNews(grp, tt); err != nil {
+ t.Fatal("newnews should work: " + err.String())
+ }
+
+
+ // NewGroups
+ if _, err = conn.NewGroups(tt); err != nil {
+ t.Fatal("newgroups shouldn't error " + err.String())
+ }
+
+ if err = conn.Quit(); err != nil {
+ t.Fatal("Quit shouldn't error: " + err.String())
+ }
+
+ actualcmds := cmdbuf.String()
+ if basicClient != actualcmds {
+ t.Fatalf("Got:\n%s\nExpected\n%s", actualcmds, basicClient)
+ }
+}
+
+var basicServer = `101 Capability list:
+VERSION 2
+.
+111 20100329034158
+215 Blah blah
+foo 7 3 y
+bar 000008 02 m
+.
+211 100 1 100 gmane.comp.lang.go.general
+223 1 <a@b.c> status
+223 2 <b@c.d> Article retrieved
+223 1 <a@b.c> Article retrieved
+220 1 <a@b.c> article
+Path: fake!not-for-mail
+From: Someone
+Newsgroups: gmane.comp.lang.go.general
+Subject: [go-nuts] What about base members?
+Message-ID: <a@b.c>
+
+Blah, blah.
+..A single leading .
+Fin.
+.
+220 2 <b@c.d> article
+Message-ID: <b@c.d>
+
+Body.
+.
+221 100 <c@d.e> head
+Path: fake!not-for-mail
+Message-ID: <c@d.e>
+.
+221 100 <c@d.e> head
+Path: fake!not-for-mail
+Message-ID: <c@d.e>
+.
+423 Bad article number
+423 Bad article number
+222 1 <a@b.c> body
+Blah, blah.
+..A single leading .
+Fin.
+.
+230 list of new articles by message-id follows
+<d@e.c>
+.
+231 New newsgroups follow
+.
+205 Bye!
+`
+
+var basicClient = `CAPABILITIES
+DATE
+LIST
+GROUP gmane.comp.lang.go.general
+STAT
+NEXT
+LAST
+ARTICLE 1
+ARTICLE 2
+HEAD 100
+HEAD
+HEAD 0
+HEAD 101
+BODY 1
+NEWNEWS gmane.comp.lang.go.general 20100301 000000 GMT
+NEWGROUPS 20100301 000000 GMT
+QUIT
+`