commit - 29ce0ee40d7b9db89104d99db4f54d2075d2e08b
commit + 807f644f282ede31199892fc79be5a46c0bd3814
blob - 3f5fbb77ebe31a1bb210cdb48e37043595f04711 (mode 644)
blob + /dev/null
--- server/server.go
+++ /dev/null
-// 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
-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
+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
+}