Commit Diff


commit - 29ce0ee40d7b9db89104d99db4f54d2075d2e08b
commit + 807f644f282ede31199892fc79be5a46c0bd3814
blob - 3f5fbb77ebe31a1bb210cdb48e37043595f04711 (mode 644)
blob + /dev/null
--- server/server.go
+++ /dev/null
@@ -1,513 +0,0 @@
-// Package nntpserver provides everything you need for your own NNTP server.
-package nntpserver
-
-import (
-	"fmt"
-	"io"
-	"log"
-	"math"
-	"net"
-	"net/textproto"
-	"strconv"
-	"strings"
-
-	"github.com/dustin/go-nntp"
-)
-
-// Handler is a low-level protocol handler
-type Handler func(args []string, s *session, c *textproto.Conn) error
-
-// A NumberedArticle provides local sequence nubers to articles When
-// listing articles in a group.
-type NumberedArticle struct {
-	Num     int64
-	Article *nntp.Article
-}
-
-// The Backend that provides the things and does the stuff.
-type Backend interface {
-	ListGroups(max int) ([]*nntp.Group, error)
-	GetGroup(name string) (*nntp.Group, error)
-	GetArticle(group *nntp.Group, id string) (*nntp.Article, error)
-	GetArticles(group *nntp.Group, from, to int64) ([]NumberedArticle, error)
-	Authorized() bool
-	// Authenticate and optionally swap out the backend for this session.
-	// You may return nil to continue using the same backend.
-	Authenticate(user, pass string) (Backend, error)
-	AllowPost() bool
-	Post(article *nntp.Article) error
-}
-
-type session struct {
-	server  *Server
-	backend Backend
-	group   *nntp.Group
-}
-
-// The Server handle.
-type Server struct {
-	// Handlers are dispatched by command name.
-	Handlers map[string]Handler
-	// The backend (your code) that provides data
-	Backend Backend
-	// The currently selected group.
-	group *nntp.Group
-}
-
-// NewServer builds a new server handle request to a backend.
-func NewServer(backend Backend) *Server {
-	rv := Server{
-		Handlers: make(map[string]Handler),
-		Backend:  backend,
-	}
-	rv.Handlers[""] = handleDefault
-	rv.Handlers["quit"] = handleQuit
-	rv.Handlers["group"] = handleGroup
-	rv.Handlers["list"] = handleList
-	rv.Handlers["head"] = handleHead
-	rv.Handlers["body"] = handleBody
-	rv.Handlers["article"] = handleArticle
-	rv.Handlers["post"] = handlePost
-	rv.Handlers["ihave"] = handleIHave
-	rv.Handlers["capabilities"] = handleCap
-	rv.Handlers["mode"] = handleMode
-	rv.Handlers["authinfo"] = handleAuthInfo
-	rv.Handlers["newgroups"] = handleNewGroups
-	rv.Handlers["over"] = handleOver
-	rv.Handlers["xover"] = handleOver
-	return &rv
-}
-
-func (s *session) dispatchCommand(cmd string, args []string,
-	c *textproto.Conn) (err error) {
-
-	handler, found := s.server.Handlers[strings.ToLower(cmd)]
-	if !found {
-		handler, found = s.server.Handlers[""]
-		if !found {
-			panic("No default handler.")
-		}
-	}
-	return handler(args, s, c)
-}
-
-// Process an NNTP session.
-func (s *Server) Process(nc net.Conn) {
-	defer nc.Close()
-	c := textproto.NewConn(nc)
-
-	sess := &session{
-		server:  s,
-		backend: s.Backend,
-		group:   nil,
-	}
-
-	c.PrintfLine("200 Hello!")
-	for {
-		l, err := c.ReadLine()
-		if err != nil {
-			log.Printf("nntp.Error reading from client, dropping conn: %v", err)
-			return
-		}
-		cmd := strings.Split(l, " ")
-		log.Printf("Got cmd:  %+v", cmd)
-		args := []string{}
-		if len(cmd) > 1 {
-			args = cmd[1:]
-		}
-		err = sess.dispatchCommand(cmd[0], args, c)
-		if err != nil {
-			_, ok := err.(*nntp.Error)
-			switch {
-			case err == io.EOF:
-				// Drop this connection silently. They hung up
-				return
-			case ok:
-				c.PrintfLine(err.Error())
-			default:
-				log.Printf("nntp.Error dispatching command, dropping conn: %v", err)
-				return
-			}
-		}
-	}
-}
-
-func parseRange(spec string) (low, high int64) {
-	if spec == "" {
-		return 0, math.MaxInt64
-	}
-	parts := strings.Split(spec, "-")
-	if len(parts) == 1 {
-		h, err := strconv.ParseInt(parts[0], 10, 64)
-		if err != nil {
-			h = math.MaxInt64
-		}
-		return 0, h
-	}
-	l, _ := strconv.ParseInt(parts[0], 10, 64)
-	h, err := strconv.ParseInt(parts[1], 10, 64)
-	if err != nil {
-		h = math.MaxInt64
-	}
-	return l, h
-}
-
-/*
-   "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
-*/
-
-func handleOver(args []string, s *session, c *textproto.Conn) error {
-	if s.group == nil {
-		return nntp.ErrNoGroupSelected
-	}
-	from, to := parseRange(args[0])
-	articles, err := s.backend.GetArticles(s.group, from, to)
-	if err != nil {
-		return err
-	}
-	c.PrintfLine("224 here it comes")
-	dw := c.DotWriter()
-	defer dw.Close()
-	for _, a := range articles {
-		fmt.Fprintf(dw, "%d\t%s\t%s\t%s\t%s\t%s\t%d\t%d\n", a.Num,
-			a.Article.Header.Get("Subject"),
-			a.Article.Header.Get("From"),
-			a.Article.Header.Get("Date"),
-			a.Article.Header.Get("Message-Id"),
-			a.Article.Header.Get("References"),
-			a.Article.Bytes, a.Article.Lines)
-	}
-	return nil
-}
-
-func handleListOverviewFmt(c *textproto.Conn) error {
-	err := c.PrintfLine("215 Order of fields in overview database.")
-	if err != nil {
-		return err
-	}
-	dw := c.DotWriter()
-	defer dw.Close()
-	_, err = fmt.Fprintln(dw, `Subject:
-From:
-Date:
-Message-ID:
-References:
-:bytes
-:lines`)
-	return err
-}
-
-func handleList(args []string, s *session, c *textproto.Conn) error {
-	ltype := "active"
-	if len(args) > 0 {
-		ltype = strings.ToLower(args[0])
-	}
-
-	if ltype == "overview.fmt" {
-		return handleListOverviewFmt(c)
-	}
-
-	groups, err := s.backend.ListGroups(-1)
-	if err != nil {
-		return err
-	}
-	c.PrintfLine("215 list of newsgroups follows")
-	dw := c.DotWriter()
-	defer dw.Close()
-	for _, g := range groups {
-		switch ltype {
-		case "active":
-			fmt.Fprintf(dw, "%s %d %d %v\r\n",
-				g.Name, g.High, g.Low, g.Posting)
-		case "newsgroups":
-			fmt.Fprintf(dw, "%s %s\r\n", g.Name, g.Description)
-		}
-	}
-
-	return nil
-}
-
-func handleNewGroups(args []string, s *session, c *textproto.Conn) error {
-	c.PrintfLine("231 list of newsgroups follows")
-	c.PrintfLine(".")
-	return nil
-}
-
-func handleDefault(args []string, s *session, c *textproto.Conn) error {
-	return nntp.ErrUnknownCommand
-}
-
-func handleQuit(args []string, s *session, c *textproto.Conn) error {
-	c.PrintfLine("205 bye")
-	return io.EOF
-}
-
-func handleGroup(args []string, s *session, c *textproto.Conn) error {
-	if len(args) < 1 {
-		return nntp.ErrNoSuchGroup
-	}
-
-	group, err := s.backend.GetGroup(args[0])
-	if err != nil {
-		return err
-	}
-
-	s.group = group
-
-	c.PrintfLine("211 %d %d %d %s",
-		group.Count, group.Low, group.High, group.Name)
-	return nil
-}
-
-func (s *session) getArticle(args []string) (*nntp.Article, error) {
-	if s.group == nil {
-		return nil, nntp.ErrNoGroupSelected
-	}
-	return s.backend.GetArticle(s.group, args[0])
-}
-
-/*
-   Syntax
-     HEAD message-id
-     HEAD number
-     HEAD
-
-
-   First form (message-id specified)
-     221 0|n message-id    Headers follow (multi-line)
-     430                   No article with that message-id
-
-   Second form (article number specified)
-     221 n message-id      Headers follow (multi-line)
-     412                   No newsgroup selected
-     423                   No article with that number
-
-   Third form (current article number used)
-     221 n message-id      Headers follow (multi-line)
-     412                   No newsgroup selected
-     420                   Current article number is invalid
-*/
-
-func handleHead(args []string, s *session, c *textproto.Conn) error {
-	article, err := s.getArticle(args)
-	if err != nil {
-		return err
-	}
-	c.PrintfLine("221 1 %s", article.MessageID())
-	dw := c.DotWriter()
-	defer dw.Close()
-	for k, v := range article.Header {
-		fmt.Fprintf(dw, "%s: %s\r\n", k, v[0])
-	}
-	return nil
-}
-
-/*
-   Syntax
-     BODY message-id
-     BODY number
-     BODY
-
-   Responses
-
-   First form (message-id specified)
-     222 0|n message-id    Body follows (multi-line)
-     430                   No article with that message-id
-
-   Second form (article number specified)
-     222 n message-id      Body follows (multi-line)
-     412                   No newsgroup selected
-     423                   No article with that number
-
-   Third form (current article number used)
-     222 n message-id      Body follows (multi-line)
-     412                   No newsgroup selected
-     420                   Current article number is invalid
-
-   Parameters
-     number        Requested article number
-     n             Returned article number
-     message-id    Article message-id
-*/
-
-func handleBody(args []string, s *session, c *textproto.Conn) error {
-	article, err := s.getArticle(args)
-	if err != nil {
-		return err
-	}
-	c.PrintfLine("222 1 %s", article.MessageID())
-	dw := c.DotWriter()
-	defer dw.Close()
-	_, err = io.Copy(dw, article.Body)
-	return err
-}
-
-/*
-   Syntax
-     ARTICLE message-id
-     ARTICLE number
-     ARTICLE
-
-   Responses
-
-   First form (message-id specified)
-     220 0|n message-id    Article follows (multi-line)
-     430                   No article with that message-id
-
-   Second form (article number specified)
-     220 n message-id      Article follows (multi-line)
-     412                   No newsgroup selected
-     423                   No article with that number
-
-   Third form (current article number used)
-     220 n message-id      Article follows (multi-line)
-     412                   No newsgroup selected
-     420                   Current article number is invalid
-
-   Parameters
-     number        Requested article number
-     n             Returned article number
-     message-id    Article message-id
-*/
-
-func handleArticle(args []string, s *session, c *textproto.Conn) error {
-	article, err := s.getArticle(args)
-	if err != nil {
-		return err
-	}
-	c.PrintfLine("220 1 %s", article.MessageID())
-	dw := c.DotWriter()
-	defer dw.Close()
-
-	for k, v := range article.Header {
-		fmt.Fprintf(dw, "%s: %s\r\n", k, v[0])
-	}
-
-	fmt.Fprintln(dw, "")
-
-	_, err = io.Copy(dw, article.Body)
-	return err
-}
-
-/*
-   Syntax
-     POST
-
-   Responses
-
-   Initial responses
-     340    Send article to be posted
-     440    Posting not permitted
-
-   Subsequent responses
-     240    Article received OK
-     441    Posting failed
-*/
-
-func handlePost(args []string, s *session, c *textproto.Conn) error {
-	if !s.backend.AllowPost() {
-		return nntp.ErrPostingNotPermitted
-	}
-
-	c.PrintfLine("340 Go ahead")
-	var err error
-	var article nntp.Article
-	article.Header, err = c.ReadMIMEHeader()
-	if err != nil {
-		return nntp.ErrPostingFailed
-	}
-	article.Body = c.DotReader()
-	err = s.backend.Post(&article)
-	if err != nil {
-		return err
-	}
-	c.PrintfLine("240 article received OK")
-	return nil
-}
-
-func handleIHave(args []string, s *session, c *textproto.Conn) error {
-	if !s.backend.AllowPost() {
-		return nntp.ErrNotWanted
-	}
-
-	// XXX:  See if we have it.
-	article, err := s.backend.GetArticle(nil, args[0])
-	if article != nil {
-		return nntp.ErrNotWanted
-	}
-
-	c.PrintfLine("335 send it")
-	article = &nntp.Article{}
-	article.Header, err = c.ReadMIMEHeader()
-	if err != nil {
-		return nntp.ErrPostingFailed
-	}
-	article.Body = c.DotReader()
-	err = s.backend.Post(article)
-	if err != nil {
-		return err
-	}
-	c.PrintfLine("235 article received OK")
-	return nil
-}
-
-func handleCap(args []string, s *session, c *textproto.Conn) error {
-	c.PrintfLine("101 Capability list:")
-	dw := c.DotWriter()
-	defer dw.Close()
-
-	fmt.Fprintf(dw, "VERSION 2\n")
-	fmt.Fprintf(dw, "READER\n")
-	if s.backend.AllowPost() {
-		fmt.Fprintf(dw, "POST\n")
-		fmt.Fprintf(dw, "IHAVE\n")
-	}
-	fmt.Fprintf(dw, "OVER\n")
-	fmt.Fprintf(dw, "XOVER\n")
-	fmt.Fprintf(dw, "LIST ACTIVE NEWSGROUPS OVERVIEW.FMT\n")
-	return nil
-}
-
-func handleMode(args []string, s *session, c *textproto.Conn) error {
-	if s.backend.AllowPost() {
-		c.PrintfLine("200 Posting allowed")
-	} else {
-		c.PrintfLine("201 Posting prohibited")
-	}
-	return nil
-}
-
-func handleAuthInfo(args []string, s *session, c *textproto.Conn) error {
-	if len(args) < 2 {
-		return nntp.ErrSyntax
-	}
-	if strings.ToLower(args[0]) != "user" {
-		return nntp.ErrSyntax
-	}
-
-	if s.backend.Authorized() {
-		return c.PrintfLine("250 authenticated")
-	}
-
-	c.PrintfLine("350 Continue")
-	a, err := c.ReadLine()
-	parts := strings.SplitN(a, " ", 3)
-	if strings.ToLower(parts[0]) != "authinfo" || strings.ToLower(parts[1]) != "pass" {
-		return nntp.ErrSyntax
-	}
-	b, err := s.backend.Authenticate(args[1], parts[2])
-	if err == nil {
-		c.PrintfLine("250 authenticated")
-		if b != nil {
-			s.backend = b
-		}
-	}
-	return err
-}
blob - 0ac3f95b4f57e79a6fe7e366c8d9e77bca803a3d (mode 644)
blob + /dev/null
--- server/server_test.go
+++ /dev/null
@@ -1,32 +0,0 @@
-package nntpserver
-
-import (
-	"math"
-	"testing"
-)
-
-type rangeExpectation struct {
-	input string
-	low   int64
-	high  int64
-}
-
-var rangeExpectations = []rangeExpectation{
-	rangeExpectation{"", 0, math.MaxInt64},
-	rangeExpectation{"73-", 73, math.MaxInt64},
-	rangeExpectation{"73-1845", 73, 1845},
-}
-
-func TestRangeEmpty(t *testing.T) {
-	for _, e := range rangeExpectations {
-		l, h := parseRange(e.input)
-		if l != e.low {
-			t.Fatalf("Error parsing %q, got low=%v, wanted %v",
-				e.input, l, e.low)
-		}
-		if h != e.high {
-			t.Fatalf("Error parsing %q, got high=%v, wanted %v",
-				e.input, h, e.high)
-		}
-	}
-}
blob - /dev/null
blob + f386cf59c1ea5644e6e1bb4837985c097dd8be6d (mode 644)
--- /dev/null
+++ server.go
@@ -0,0 +1,510 @@
+package nntp
+
+import (
+	"fmt"
+	"io"
+	"log"
+	"math"
+	"net"
+	"net/textproto"
+	"strconv"
+	"strings"
+)
+
+// Handler is a low-level protocol handler
+type Handler func(args []string, s *session, c *textproto.Conn) error
+
+// A NumberedArticle provides local sequence nubers to articles When
+// listing articles in a group.
+type NumberedArticle struct {
+	Num     int64
+	Article *Article
+}
+
+// The Backend that provides the things and does the stuff.
+type Backend interface {
+	ListGroups(max int) ([]*Group, error)
+	GetGroup(name string) (*Group, error)
+	GetArticle(g *Group, id string) (*Article, error)
+	GetArticles(g *Group, from, to int64) ([]NumberedArticle, error)
+	Authorized() bool
+	// Authenticate and optionally swap out the backend for this session.
+	// You may return nil to continue using the same backend.
+	Authenticate(user, pass string) (Backend, error)
+	AllowPost() bool
+	Post(article *Article) error
+}
+
+type session struct {
+	server  *Server
+	backend Backend
+	group   *Group
+}
+
+// The Server handle.
+type Server struct {
+	// Handlers are dispatched by command name.
+	Handlers map[string]Handler
+	// The backend (your code) that provides data
+	Backend Backend
+	// The currently selected group.
+	group *Group
+}
+
+// NewServer builds a new server handle request to a backend.
+func NewServer(backend Backend) *Server {
+	rv := Server{
+		Handlers: make(map[string]Handler),
+		Backend:  backend,
+	}
+	rv.Handlers[""] = handleDefault
+	rv.Handlers["quit"] = handleQuit
+	rv.Handlers["group"] = handleGroup
+	rv.Handlers["list"] = handleList
+	rv.Handlers["head"] = handleHead
+	rv.Handlers["body"] = handleBody
+	rv.Handlers["article"] = handleArticle
+	rv.Handlers["post"] = handlePost
+	rv.Handlers["ihave"] = handleIHave
+	rv.Handlers["capabilities"] = handleCap
+	rv.Handlers["mode"] = handleMode
+	rv.Handlers["authinfo"] = handleAuthInfo
+	rv.Handlers["newgroups"] = handleNewGroups
+	rv.Handlers["over"] = handleOver
+	rv.Handlers["xover"] = handleOver
+	return &rv
+}
+
+func (s *session) dispatchCommand(cmd string, args []string,
+	c *textproto.Conn) (err error) {
+
+	handler, found := s.server.Handlers[strings.ToLower(cmd)]
+	if !found {
+		handler, found = s.server.Handlers[""]
+		if !found {
+			panic("No default handler.")
+		}
+	}
+	return handler(args, s, c)
+}
+
+// Process an NNTP session.
+func (s *Server) Process(nc net.Conn) {
+	defer nc.Close()
+	c := textproto.NewConn(nc)
+
+	sess := &session{
+		server:  s,
+		backend: s.Backend,
+		group:   nil,
+	}
+
+	c.PrintfLine("200 Hello!")
+	for {
+		l, err := c.ReadLine()
+		if err != nil {
+			log.Printf("Error reading from client, dropping conn: %v", err)
+			return
+		}
+		cmd := strings.Split(l, " ")
+		log.Printf("Got cmd:  %+v", cmd)
+		args := []string{}
+		if len(cmd) > 1 {
+			args = cmd[1:]
+		}
+		err = sess.dispatchCommand(cmd[0], args, c)
+		if err != nil {
+			_, ok := err.(*Error)
+			switch {
+			case err == io.EOF:
+				// Drop this connection silently. They hung up
+				return
+			case ok:
+				c.PrintfLine(err.Error())
+			default:
+				log.Printf("Error dispatching command, dropping conn: %v", err)
+				return
+			}
+		}
+	}
+}
+
+func parseRange(spec string) (low, high int64) {
+	if spec == "" {
+		return 0, math.MaxInt64
+	}
+	parts := strings.Split(spec, "-")
+	if len(parts) == 1 {
+		h, err := strconv.ParseInt(parts[0], 10, 64)
+		if err != nil {
+			h = math.MaxInt64
+		}
+		return 0, h
+	}
+	l, _ := strconv.ParseInt(parts[0], 10, 64)
+	h, err := strconv.ParseInt(parts[1], 10, 64)
+	if err != nil {
+		h = math.MaxInt64
+	}
+	return l, h
+}
+
+/*
+   "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
+*/
+
+func handleOver(args []string, s *session, c *textproto.Conn) error {
+	if s.group == nil {
+		return ErrNoGroupSelected
+	}
+	from, to := parseRange(args[0])
+	articles, err := s.backend.GetArticles(s.group, from, to)
+	if err != nil {
+		return err
+	}
+	c.PrintfLine("224 here it comes")
+	dw := c.DotWriter()
+	defer dw.Close()
+	for _, a := range articles {
+		fmt.Fprintf(dw, "%d\t%s\t%s\t%s\t%s\t%s\t%d\t%d\n", a.Num,
+			a.Article.Header.Get("Subject"),
+			a.Article.Header.Get("From"),
+			a.Article.Header.Get("Date"),
+			a.Article.Header.Get("Message-Id"),
+			a.Article.Header.Get("References"),
+			a.Article.Bytes, a.Article.Lines)
+	}
+	return nil
+}
+
+func handleListOverviewFmt(c *textproto.Conn) error {
+	err := c.PrintfLine("215 Order of fields in overview database.")
+	if err != nil {
+		return err
+	}
+	dw := c.DotWriter()
+	defer dw.Close()
+	_, err = fmt.Fprintln(dw, `Subject:
+From:
+Date:
+Message-ID:
+References:
+:bytes
+:lines`)
+	return err
+}
+
+func handleList(args []string, s *session, c *textproto.Conn) error {
+	ltype := "active"
+	if len(args) > 0 {
+		ltype = strings.ToLower(args[0])
+	}
+
+	if ltype == "overview.fmt" {
+		return handleListOverviewFmt(c)
+	}
+
+	groups, err := s.backend.ListGroups(-1)
+	if err != nil {
+		return err
+	}
+	c.PrintfLine("215 list of newsgroups follows")
+	dw := c.DotWriter()
+	defer dw.Close()
+	for _, g := range groups {
+		switch ltype {
+		case "active":
+			fmt.Fprintf(dw, "%s %d %d %v\r\n",
+				g.Name, g.High, g.Low, g.Posting)
+		case "newsgroups":
+			fmt.Fprintf(dw, "%s %s\r\n", g.Name, g.Description)
+		}
+	}
+
+	return nil
+}
+
+func handleNewGroups(args []string, s *session, c *textproto.Conn) error {
+	c.PrintfLine("231 list of newsgroups follows")
+	c.PrintfLine(".")
+	return nil
+}
+
+func handleDefault(args []string, s *session, c *textproto.Conn) error {
+	return ErrUnknownCommand
+}
+
+func handleQuit(args []string, s *session, c *textproto.Conn) error {
+	c.PrintfLine("205 bye")
+	return io.EOF
+}
+
+func handleGroup(args []string, s *session, c *textproto.Conn) error {
+	if len(args) < 1 {
+		return ErrNoSuchGroup
+	}
+
+	group, err := s.backend.GetGroup(args[0])
+	if err != nil {
+		return err
+	}
+
+	s.group = group
+
+	c.PrintfLine("211 %d %d %d %s",
+		group.Count, group.Low, group.High, group.Name)
+	return nil
+}
+
+func (s *session) getArticle(args []string) (*Article, error) {
+	if s.group == nil {
+		return nil, ErrNoGroupSelected
+	}
+	return s.backend.GetArticle(s.group, args[0])
+}
+
+/*
+   Syntax
+     HEAD message-id
+     HEAD number
+     HEAD
+
+
+   First form (message-id specified)
+     221 0|n message-id    Headers follow (multi-line)
+     430                   No article with that message-id
+
+   Second form (article number specified)
+     221 n message-id      Headers follow (multi-line)
+     412                   No newsgroup selected
+     423                   No article with that number
+
+   Third form (current article number used)
+     221 n message-id      Headers follow (multi-line)
+     412                   No newsgroup selected
+     420                   Current article number is invalid
+*/
+
+func handleHead(args []string, s *session, c *textproto.Conn) error {
+	article, err := s.getArticle(args)
+	if err != nil {
+		return err
+	}
+	c.PrintfLine("221 1 %s", article.MessageID())
+	dw := c.DotWriter()
+	defer dw.Close()
+	for k, v := range article.Header {
+		fmt.Fprintf(dw, "%s: %s\r\n", k, v[0])
+	}
+	return nil
+}
+
+/*
+   Syntax
+     BODY message-id
+     BODY number
+     BODY
+
+   Responses
+
+   First form (message-id specified)
+     222 0|n message-id    Body follows (multi-line)
+     430                   No article with that message-id
+
+   Second form (article number specified)
+     222 n message-id      Body follows (multi-line)
+     412                   No newsgroup selected
+     423                   No article with that number
+
+   Third form (current article number used)
+     222 n message-id      Body follows (multi-line)
+     412                   No newsgroup selected
+     420                   Current article number is invalid
+
+   Parameters
+     number        Requested article number
+     n             Returned article number
+     message-id    Article message-id
+*/
+
+func handleBody(args []string, s *session, c *textproto.Conn) error {
+	article, err := s.getArticle(args)
+	if err != nil {
+		return err
+	}
+	c.PrintfLine("222 1 %s", article.MessageID())
+	dw := c.DotWriter()
+	defer dw.Close()
+	_, err = io.Copy(dw, article.Body)
+	return err
+}
+
+/*
+   Syntax
+     ARTICLE message-id
+     ARTICLE number
+     ARTICLE
+
+   Responses
+
+   First form (message-id specified)
+     220 0|n message-id    Article follows (multi-line)
+     430                   No article with that message-id
+
+   Second form (article number specified)
+     220 n message-id      Article follows (multi-line)
+     412                   No newsgroup selected
+     423                   No article with that number
+
+   Third form (current article number used)
+     220 n message-id      Article follows (multi-line)
+     412                   No newsgroup selected
+     420                   Current article number is invalid
+
+   Parameters
+     number        Requested article number
+     n             Returned article number
+     message-id    Article message-id
+*/
+
+func handleArticle(args []string, s *session, c *textproto.Conn) error {
+	article, err := s.getArticle(args)
+	if err != nil {
+		return err
+	}
+	c.PrintfLine("220 1 %s", article.MessageID())
+	dw := c.DotWriter()
+	defer dw.Close()
+
+	for k, v := range article.Header {
+		fmt.Fprintf(dw, "%s: %s\r\n", k, v[0])
+	}
+
+	fmt.Fprintln(dw, "")
+
+	_, err = io.Copy(dw, article.Body)
+	return err
+}
+
+/*
+   Syntax
+     POST
+
+   Responses
+
+   Initial responses
+     340    Send article to be posted
+     440    Posting not permitted
+
+   Subsequent responses
+     240    Article received OK
+     441    Posting failed
+*/
+
+func handlePost(args []string, s *session, c *textproto.Conn) error {
+	if !s.backend.AllowPost() {
+		return ErrPostingNotPermitted
+	}
+
+	c.PrintfLine("340 Go ahead")
+	var err error
+	var article Article
+	article.Header, err = c.ReadMIMEHeader()
+	if err != nil {
+		return ErrPostingFailed
+	}
+	article.Body = c.DotReader()
+	err = s.backend.Post(&article)
+	if err != nil {
+		return err
+	}
+	c.PrintfLine("240 article received OK")
+	return nil
+}
+
+func handleIHave(args []string, s *session, c *textproto.Conn) error {
+	if !s.backend.AllowPost() {
+		return ErrNotWanted
+	}
+
+	// XXX:  See if we have it.
+	article, err := s.backend.GetArticle(nil, args[0])
+	if article != nil {
+		return ErrNotWanted
+	}
+
+	c.PrintfLine("335 send it")
+	article = &Article{}
+	article.Header, err = c.ReadMIMEHeader()
+	if err != nil {
+		return ErrPostingFailed
+	}
+	article.Body = c.DotReader()
+	err = s.backend.Post(article)
+	if err != nil {
+		return err
+	}
+	c.PrintfLine("235 article received OK")
+	return nil
+}
+
+func handleCap(args []string, s *session, c *textproto.Conn) error {
+	c.PrintfLine("101 Capability list:")
+	dw := c.DotWriter()
+	defer dw.Close()
+
+	fmt.Fprintf(dw, "VERSION 2\n")
+	fmt.Fprintf(dw, "READER\n")
+	if s.backend.AllowPost() {
+		fmt.Fprintf(dw, "POST\n")
+		fmt.Fprintf(dw, "IHAVE\n")
+	}
+	fmt.Fprintf(dw, "OVER\n")
+	fmt.Fprintf(dw, "XOVER\n")
+	fmt.Fprintf(dw, "LIST ACTIVE NEWSGROUPS OVERVIEW.FMT\n")
+	return nil
+}
+
+func handleMode(args []string, s *session, c *textproto.Conn) error {
+	if s.backend.AllowPost() {
+		c.PrintfLine("200 Posting allowed")
+	} else {
+		c.PrintfLine("201 Posting prohibited")
+	}
+	return nil
+}
+
+func handleAuthInfo(args []string, s *session, c *textproto.Conn) error {
+	if len(args) < 2 {
+		return ErrSyntax
+	}
+	if strings.ToLower(args[0]) != "user" {
+		return ErrSyntax
+	}
+
+	if s.backend.Authorized() {
+		return c.PrintfLine("250 authenticated")
+	}
+
+	c.PrintfLine("350 Continue")
+	a, err := c.ReadLine()
+	parts := strings.SplitN(a, " ", 3)
+	if strings.ToLower(parts[0]) != "authinfo" || strings.ToLower(parts[1]) != "pass" {
+		return ErrSyntax
+	}
+	b, err := s.backend.Authenticate(args[1], parts[2])
+	if err == nil {
+		c.PrintfLine("250 authenticated")
+		if b != nil {
+			s.backend = b
+		}
+	}
+	return err
+}