commit 807f644f282ede31199892fc79be5a46c0bd3814 from: Oliver Lowe date: Thu Mar 28 01:44:28 2024 UTC nntp: remove nntpserver package Don't think there's anything special about it to separate it. Also abandon unfinished tests 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 +}