diff --git a/src/pkg/Makefile b/src/pkg/Makefile index 3371fe82d0..404deb05b9 100644 --- a/src/pkg/Makefile +++ b/src/pkg/Makefile @@ -96,7 +96,6 @@ DIRS=\ net/dict\ net/textproto\ netchan\ - nntp\ os\ os/signal\ patch\ diff --git a/src/pkg/nntp/Makefile b/src/pkg/nntp/Makefile deleted file mode 100644 index ce5e447555..0000000000 --- a/src/pkg/nntp/Makefile +++ /dev/null @@ -1,11 +0,0 @@ -# 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.inc - -TARG=nntp -GOFILES=\ - nntp.go - -include ../../Make.pkg diff --git a/src/pkg/nntp/nntp.go b/src/pkg/nntp/nntp.go deleted file mode 100644 index ce7a2ccd2d..0000000000 --- a/src/pkg/nntp/nntp.go +++ /dev/null @@ -1,707 +0,0 @@ -// 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()) - for i, v := range res { - realres[i] = *v.(*Group) - } - 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 deleted file mode 100644 index 0944efff34..0000000000 --- a/src/pkg/nntp/nntp_test.go +++ /dev/null @@ -1,240 +0,0 @@ -// 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", -1), "\r\n") - basicClient = strings.Join(strings.Split(basicClient, "\n", -1), "\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: - -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" + 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 status -223 2 Article retrieved -223 1 Article retrieved -220 1 article -Path: fake!not-for-mail -From: Someone -Newsgroups: gmane.comp.lang.go.general -Subject: [go-nuts] What about base members? -Message-ID: - -Blah, blah. -..A single leading . -Fin. -. -220 2 article -Message-ID: - -Body. -. -221 100 head -Path: fake!not-for-mail -Message-ID: -. -221 100 head -Path: fake!not-for-mail -Message-ID: -. -423 Bad article number -423 Bad article number -222 1 body -Blah, blah. -..A single leading . -Fin. -. -230 list of new articles by message-id follows - -. -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 -`