// handle streaming event // this function should send only func (self nntpConnection) handleStreaming(daemon NNTPDaemon, reader bool, conn *textproto.Conn) (err error) { select { case msgid := <-self.check: log.Println(self.name, "CHECK", msgid) err = conn.PrintfLine("CHECK %s", msgid) case msgid := <-self.take: // send a file via TAKETHIS if ValidMessageID(msgid) { fname := daemon.store.GetFilename(msgid) if CheckFile(fname) { f, err := os.Open(fname) if err == nil { // time to send err = conn.PrintfLine("TAKETHIS %s", msgid) dw := conn.DotWriter() _, err = io.Copy(dw, f) err = dw.Close() f.Close() } } else { log.Println(self.name, "didn't send", msgid, "we don't have it locally") } } } return }
// handle sending 1 stream event func (self *nntpConnection) handleStreamEvent(ev nntpStreamEvent, daemon *NNTPDaemon, conn *textproto.Conn) (err error) { if ValidMessageID(ev.MessageID()) { cmd, msgid := ev.Command(), ev.MessageID() if cmd == "TAKETHIS" { // open message for reading var rc io.ReadCloser rc, err = daemon.store.OpenMessage(msgid) if err == nil { err = conn.PrintfLine("%s", ev) // time to send dw := conn.DotWriter() _, err = io.Copy(dw, rc) err = dw.Close() rc.Close() self.messageSetPendingState(msgid, "sent") } else { log.Println(self.name, "didn't send", msgid, err) self.messageSetProcessed(msgid) // ignore this error err = nil } } else if cmd == "CHECK" { conn.PrintfLine("%s", ev) } else { log.Println("invalid stream command", ev) } } return }
func (self *nntpConnection) startReader(daemon *NNTPDaemon, conn *textproto.Conn) { log.Println(self.name, "run reader mode") for { var err error select { case chnl := <-self.die: // we were asked to die // send quit conn.PrintfLine("QUIT") chnl <- true break case msgid := <-self.article: // next article to ask for log.Println(self.name, "obtaining", msgid) self.messageSetPendingState(msgid, "article") err = self.requestArticle(daemon, conn, msgid) self.messageSetProcessed(msgid) if err != nil { log.Println(self.name, "error while in reader mode:", err) break } } } // close connection conn.Close() }
func handleIHave(args []string, s *session, c *textproto.Conn) error { if !s.backend.AllowPost() { return NotWanted } // XXX: See if we have it. article, err := s.backend.GetArticle(nil, args[0]) if article != nil { return NotWanted } c.PrintfLine("335 send it") article = new(nntp.Article) article.Header, err = c.ReadMIMEHeader() if err != nil { return PostingFailed } article.Body = c.DotReader() err = s.backend.Post(article) if err != nil { return err } c.PrintfLine("235 article received OK") return nil }
// handle streaming event // this function should send only func (self *nntpConnection) handleStreaming(daemon NNTPDaemon, reader bool, conn *textproto.Conn) (err error) { for err == nil { ev := <-self.stream log.Println(self.name, ev) if ValidMessageID(ev.MessageID()) { cmd, msgid := ev.Command(), ev.MessageID() if cmd == "TAKETHIS" { fname := daemon.store.GetFilename(msgid) if CheckFile(fname) { f, err := os.Open(fname) if err == nil { err = conn.PrintfLine("%s", ev) // time to send dw := conn.DotWriter() _, err = io.Copy(dw, f) err = dw.Close() f.Close() } } else { log.Println(self.name, "didn't send", msgid, "we don't have it locally") } } else if cmd == "CHECK" { conn.PrintfLine("%s", ev) } else { log.Println("invalid stream command", ev) } } } return }
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" { dw := c.DotWriter() defer dw.Close() return handleListOverviewFmt(dw, 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 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 }
// outbound setup, check capabilities and set mode // returns (supports stream, supports reader) + error func (self nntpConnection) outboundHandshake(conn *textproto.Conn) (stream, reader bool, err error) { log.Println(self.name, "outbound handshake") var code int var line string for err == nil { code, line, err = conn.ReadCodeLine(-1) log.Println(self.name, line) if err == nil { if code == 200 { // send capabilities log.Println(self.name, "ask for capabilities") err = conn.PrintfLine("CAPABILITIES") if err == nil { // read response dr := conn.DotReader() r := bufio.NewReader(dr) for { line, err = r.ReadString('\n') if err == io.EOF { // we are at the end of the dotreader // set err back to nil and break out err = nil break } else if err == nil { // we got a line if line == "MODE-READER\n" || line == "READER\n" { log.Println(self.name, "supports READER") reader = true } else if line == "STREAMING\n" { stream = true log.Println(self.name, "supports STREAMING") } else if line == "POSTIHAVESTREAMING\n" { stream = true reader = false log.Println(self.name, "is SRNd") } } else { // we got an error log.Println("error reading capabilities", err) break } } // return after reading return } } else if code == 201 { log.Println("feed", self.name, "does not allow posting") // we don't do auth yet break } else { continue } } } return }
// ask for an article from the remote server // feed it to the daemon if we get it func (self *nntpConnection) requestArticle(daemon NNTPDaemon, conn *textproto.Conn, msgid string) (err error) { log.Println(self.name, "asking for", msgid) // send command err = conn.PrintfLine("ARTICLE %s", msgid) // read response code, line, err := conn.ReadCodeLine(-1) if code == 220 { // awwww yeh we got it var hdr textproto.MIMEHeader // read header hdr, err = conn.ReadMIMEHeader() if err == nil { // prepare to read body dr := conn.DotReader() // check header and decide if we want this reason, err := self.checkMIMEHeader(daemon, hdr) if err == nil { if len(reason) > 0 { log.Println(self.name, "discarding", msgid, reason) // we don't want it, discard io.Copy(ioutil.Discard, dr) daemon.database.BanArticle(msgid, reason) } else { // yeh we want it open up a file to store it in f := daemon.store.CreateTempFile(msgid) if f == nil { // already being loaded elsewhere } else { // write header to file writeMIMEHeader(f, hdr) // write article body to file _, _ = io.Copy(f, dr) // close file f.Close() log.Println(msgid, "obtained via reader from", self.name) // tell daemon to load article via infeed daemon.infeed_load <- msgid } } } else { // error happened while processing log.Println(self.name, "error happend while processing MIME header", err) } } else { // error happened while reading header log.Println(self.name, "error happened while reading MIME header", err) } } else if code == 430 { // they don't know it D: log.Println(msgid, "not known by", self.name) } else { // invalid response log.Println(self.name, "invald response to ARTICLE:", code, line) } return }
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 }
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 }
func GetRequest(request Request, client *textproto.Conn) error { var err error defer func() { if err != nil { if err == io.EOF { } else { log.Println("Conn died with err %s", err) } } }() requestHeaders, err := client.ReadMIMEHeader() if err != nil { return err } response := fmt.Sprintf("Hello from Baboon!\nHave I seen you before?"+ "\nI know a bit about you.\n"+ "For example, your User Agent is %s", requestHeaders["User-Agent"]) serverHeaders := map[string]string{} serverHeaders["Server"] = "Baboon/0.001" serverHeaders["Content-Type"] = "text/plain" serverHeaders["Content-Length"] = strconv.Itoa(len(response)) client.PrintfLine("HTTP/1.1 200 OK") for key, value := range serverHeaders { client.PrintfLine("%s: %s", key, value) } client.PrintfLine("") client.PrintfLine(response) return nil }
func handleListOverviewFmt(dw io.Writer, c *textproto.Conn) error { err := c.PrintfLine("215 list of newsgroups follows") if err != nil { return err } _, err = fmt.Fprintln(dw, `Subject: From: Date: Message-ID: References: :bytes :lines`) return err }
// ask for an article from the remote server // feed it to the daemon if we get it func (self *nntpConnection) requestArticle(daemon *NNTPDaemon, conn *textproto.Conn, msgid string) (err error) { // send command err = conn.PrintfLine("ARTICLE %s", msgid) // read response code, line, err := conn.ReadCodeLine(-1) if code == 220 { // awwww yeh we got it var hdr textproto.MIMEHeader // read header hdr, err = readMIMEHeader(conn.R) if err == nil { // prepare to read body dr := conn.DotReader() // check header and decide if we want this reason, ban, err := self.checkMIMEHeaderNoAuth(daemon, hdr) if err == nil { if len(reason) > 0 { log.Println(self.name, "discarding", msgid, reason) // we don't want it, discard io.Copy(ioutil.Discard, dr) if ban { daemon.database.BanArticle(msgid, reason) } } else { // yeh we want it open up a file to store it in err = self.storeMessage(daemon, hdr, dr) if err != nil { log.Println(self.name, "failed to obtain article", err) // probably an invalid signature or format daemon.database.BanArticle(msgid, err.Error()) } } } else { // error happened while processing log.Println(self.name, "error happend while processing MIME header", err) } } else { // error happened while reading header log.Println(self.name, "error happened while reading MIME header", err) } } else if code == 430 { // they don't know it D: } else { // invalid response log.Println(self.name, "invald response to ARTICLE:", code, line) } return }
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 handleGroup(args []string, s *session, c *textproto.Conn) error { if len(args) < 1 { return NoSuchGroup } 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 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 }
// switch modes func (self *nntpConnection) modeSwitch(mode string, conn *textproto.Conn) (success bool, err error) { self.access.Lock() mode = strings.ToUpper(mode) conn.PrintfLine("MODE %s", mode) var code int code, _, err = conn.ReadCodeLine(-1) if code >= 200 && code < 300 { // accepted mode change if len(self.mode) > 0 { log.Printf(self.name, "mode switch %s -> %s", self.mode, mode) } else { log.Println(self.name, "switched to mode", mode) } self.mode = mode success = len(self.mode) > 0 } self.access.Unlock() return }
// handle streaming events // this function should send only func (self *nntpConnection) handleStreaming(daemon *NNTPDaemon, conn *textproto.Conn) (err error) { for err == nil { select { case chnl := <-self.die: // someone asked us to die conn.PrintfLine("QUIT") conn.Close() chnl <- true return case msgid := <-self.check: err = self.handleStreamEvent(nntpCHECK(msgid), daemon, conn) self.messageSetPendingState(msgid, "check") case msgid := <-self.takethis: self.messageSetPendingState(msgid, "takethis") err = self.handleStreamEvent(nntpTAKETHIS(msgid), daemon, conn) } } return }
func handlePost(args []string, s *session, c *textproto.Conn) error { if !s.backend.AllowPost() { return PostingNotPermitted } c.PrintfLine("340 Go ahead") var err error var article nntp.Article article.Header, err = c.ReadMIMEHeader() if err != nil { return PostingFailed } article.Body = c.DotReader() err = s.backend.Post(&article) if err != nil { return err } c.PrintfLine("240 article received OK") return nil }
func handleAuthInfo(args []string, s *session, c *textproto.Conn) error { if len(args) < 2 { return SyntaxError } if strings.ToLower(args[0]) != "user" { return SyntaxError } 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 SyntaxError } b, err := s.backend.Authenticate(args[1], parts[2]) if err == nil { c.PrintfLine("250 authenticated") if b != nil { s.backend = b } } return err }
func handleOver(args []string, s *session, c *textproto.Conn) error { if s.group == nil { return NoGroupSelected } 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 (s *server) writeIdleResponse(p *textproto.Conn, id uint, quit chan bool, subsystems []string) { p.StartResponse(id) defer p.EndResponse(id) req := &idleRequest{ endTokenc: make(chan uint), eventc: make(chan string, 1), subsystems: subsystems, } s.idleStartc <- req token := <-req.endTokenc select { case name := <-req.eventc: p.PrintfLine("changed: %s", name) p.PrintfLine("OK") <-quit case <-quit: p.PrintfLine("OK") } s.idleEndc <- token }
func (s *server) handleConnection(p *textproto.Conn) { id := p.Next() p.StartRequest(id) p.EndRequest(id) p.StartResponse(id) p.PrintfLine("OK MPD gompd0.1") p.EndResponse(id) endIdle := make(chan bool) inIdle := false defer p.Close() for { id := p.Next() p.StartRequest(id) req, err := s.readRequest(p) if err != nil { return } // We need to do this inside request because idle response // may not have ended yet, but it will end after the following. if inIdle { endIdle <- true } p.EndRequest(id) if req.typ == idle { inIdle = true go s.writeIdleResponse(p, id, endIdle, req.args[1:]) // writeIdleResponse does it's own StartResponse/EndResponse continue } p.StartResponse(id) if inIdle { inIdle = false } switch req.typ { case noIdle: case commandListOk: var ok, closed bool ok = true for _, args := range req.cmdList { ok, closed = s.writeResponse(p, args, "list_OK") if closed { return } if !ok { break } } if ok { p.PrintfLine("OK") } case simple: if _, closed := s.writeResponse(p, req.args, "OK"); closed { return } } p.EndResponse(id) } }
// run the mainloop for this connection // stream if true means they support streaming mode // reader if true means they support reader mode func (self nntpConnection) runConnection(daemon NNTPDaemon, inbound, stream, reader bool, preferMode string, conn *textproto.Conn) { var err error var line string var success bool for err == nil { if self.mode == "" { if inbound { // no mode and inbound line, err = conn.ReadLine() log.Println(self.name, line) parts := strings.Split(line, " ") cmd := parts[0] if cmd == "CAPABILITIES" { // write capabilities conn.PrintfLine("101 i support to the following:") dw := conn.DotWriter() caps := []string{"VERSION 2", "READER", "STREAMING", "IMPLEMENTATION srndv2"} for _, cap := range caps { io.WriteString(dw, cap) io.WriteString(dw, "\n") } dw.Close() log.Println(self.name, "sent Capabilities") } else if cmd == "MODE" { if len(parts) == 2 { if parts[1] == "READER" { // set reader mode self.mode = "READER" // posting is not permitted with reader mode conn.PrintfLine("201 Posting not permitted") } else if parts[1] == "STREAM" { // set streaming mode conn.PrintfLine("203 Stream it brah") self.mode = "STREAM" log.Println(self.name, "streaming enabled") go self.startStreaming(daemon, reader, conn) } } } else { // handle a it as a command, we don't have a mode set parts := strings.Split(line, " ") var code64 int64 code64, err = strconv.ParseInt(parts[0], 10, 32) if err == nil { err = self.handleLine(daemon, int(code64), line[4:], conn) } else { err = self.handleLine(daemon, 0, line, conn) } } } else { // no mode and outbound if preferMode == "stream" { // try outbound streaming if stream { success, err = self.modeSwitch("STREAM", conn) self.mode = "STREAM" if success { // start outbound streaming in background go self.startStreaming(daemon, reader, conn) } } } else if reader { // try reader mode success, err = self.modeSwitch("READER", conn) if success { self.mode = "READER" self.startReader(daemon, conn) } } if success { log.Println(self.name, "mode set to", self.mode) } else { // bullshit // we can't do anything so we quit log.Println(self.name, "can't stream or read, wtf?") conn.PrintfLine("QUIT") conn.Close() return } } } else { // we have our mode set line, err = conn.ReadLine() if err == nil { parts := strings.Split(line, " ") var code64 int64 code64, err = strconv.ParseInt(parts[0], 10, 32) if err == nil { err = self.handleLine(daemon, int(code64), line[4:], conn) } else { err = self.handleLine(daemon, 0, line, conn) } } } } log.Println(self.name, "got error", err) if !inbound { // send quit on outbound conn.PrintfLine("QUIT") } conn.Close() }
// send a banner for inbound connections func (self nntpConnection) inboundHandshake(conn *textproto.Conn) (err error) { err = conn.PrintfLine("200 Posting Allowed") return err }
func (s *server) writeResponse(p *textproto.Conn, args []string, okLine string) (cmdOk, closed bool) { if len(args) < 1 { p.PrintfLine("No command given") return } ack := func(format string, a ...interface{}) error { return p.PrintfLine("ACK {"+args[0]+"} "+format, a...) } switch args[0] { case "close": closed = true return case "list": if len(args) < 2 { ack("too few arguments") return } if args[1] == "file" { for _, a := range s.database { p.PrintfLine("file: %s", a["file"]) } } case "lsinfo": for _, a := range s.database { p.PrintfLine("file: %s", a["file"]) p.PrintfLine("Last-Modified: 2014-07-02T12:32:26Z") p.PrintfLine("Artist: Newcleus") p.PrintfLine("Title: Jam On It") p.PrintfLine("Track: 02") } for _, a := range []string{ "music/Buck 65 - Dirtbike 1", "music/Howlin' Wolf - Moanin' in the Moonlight", } { p.PrintfLine("directory: %s", a) } p.PrintfLine("playlist: BBC 6 Music.m3u") case "listplaylists": for k := range s.playlists { p.PrintfLine("playlist: %s", k) } case "playlistinfo": var rng []string var start int end := s.currentPlaylist.Len() if len(args) >= 2 { rng = strings.Split(args[1], ":") } if len(rng) == 1 { // Requesting a single song from the playlist at position i. i, err := strconv.Atoi(rng[0]) if err != nil { ack("invalid song position") return } start = i end = i + 1 } else if len(rng) == 2 { // Requesting a range of the playlist from specified start/end positions. var err error start, err = strconv.Atoi(rng[0]) if err != nil { ack("Integer or range expected") return } end, err := strconv.Atoi(rng[1]) if err != nil { ack("Integer or range expected") return } if start < 0 || end < 0 { ack("Number is negative") return } } for i := start; i < end; i++ { p.PrintfLine("file: %s", s.database[s.currentPlaylist.At(i)]["file"]) } case "listplaylistinfo": if len(args) < 2 { ack("too few arguments") return } pl, ok := s.playlists[args[1]] if !ok { ack("no such playlist") return } for i := 0; i < pl.Len(); i++ { p.PrintfLine("file: %s", s.database[pl.At(i)]["file"]) } case "playlistadd": if len(args) != 3 { ack("wrong number of arguments") return } name, uri := args[1], args[2] i, ok := s.index[uri] if !ok { ack("URI not found") return } if s.playlists[name] == nil { s.playlists[name] = newPlaylist() } s.playlists[name].Add(i) case "playlistdelete": if len(args) != 3 { ack("wrong number of arguments") return } name := args[1] pos, err := strconv.Atoi(args[2]) if err != nil { ack("invalid position number") return } pl, ok := s.playlists[name] if !ok { ack("playlist not found") return } if pos < 0 || pos >= pl.Len() { ack("invalid song position") return } pl.Delete(pos) case "playlistclear": if len(args) != 2 { ack("wrong number of arguments") return } pl, ok := s.playlists[args[1]] if !ok { ack("playlist not found") return } pl.Clear() case "rm": if len(args) != 2 { ack("wrong number of arguments") return } _, ok := s.playlists[args[1]] if !ok { ack("playlist not found") return } delete(s.playlists, args[1]) case "rename": if len(args) != 3 { ack("wrong number of arguments") return } old, new := args[1], args[2] _, ok := s.playlists[old] if !ok { ack("playlist %s does not exist", old) return } _, ok = s.playlists[new] if ok { ack("playlist %s already exists", new) return } s.playlists[new] = s.playlists[old] delete(s.playlists, old) case "load": if len(args) != 2 { ack("wrong number of arguments") return } pl, ok := s.playlists[args[1]] if !ok { ack("playlist %s does not exist", args[1]) return } s.currentPlaylist.Append(pl) case "clear": s.currentPlaylist.Clear() case "add": if len(args) != 2 { ack("wrong number of arguments") return } i, ok := s.index[args[1]] if !ok { ack("URI not found") return } s.currentPlaylist.Add(i) case "delete": if len(args) != 2 { ack("wrong number of arguments") return } i, err := strconv.Atoi(args[1]) if err != nil { ack("invalid song position") return } s.idleEventc <- "playlist" if i < 0 || i >= s.currentPlaylist.Len() { ack("invalid song position") return } s.currentPlaylist.Delete(i) case "save": if len(args) != 2 { ack("wrong number of arguments") return } name := args[1] _, ok := s.playlists[name] if ok { ack("playlist %s already exists", name) return } s.playlists[name] = newPlaylist() s.playlists[name].Append(s.currentPlaylist) case "play", "stop": s.idleEventc <- "player" s.state = args[0] case "next": s.idleEventc <- "player" if s.pos < 0 || s.pos >= s.currentPlaylist.Len() { s.pos = 0 break } s.pos = (s.pos + 1) % s.currentPlaylist.Len() case "previous": s.idleEventc <- "player" if s.pos < 0 || s.pos >= s.currentPlaylist.Len() { s.pos = 0 break } if s.pos == 0 { s.pos = s.currentPlaylist.Len() - 1 break } s.pos-- case "pause": if s.state != "stop" { s.state = args[0] } case "status": state := s.state p.PrintfLine("state: %s", state) case "update": p.PrintfLine("updating_db: 1") case "ping": case "currentsong": if s.currentPlaylist.Len() == 0 { break } if s.pos >= s.currentPlaylist.Len() { s.pos = 0 } p.PrintfLine("file: %s", s.database[s.currentPlaylist.At(s.pos)]["file"]) case "outputs": p.PrintfLine("outputid: 0") p.PrintfLine("outputenabled: 1") p.PrintfLine("outputname: downstairs") p.PrintfLine("outputid: 1") p.PrintfLine("outputenabled: 0") p.PrintfLine("outputname: upstairs") case "disableoutput", "enableoutput": default: p.PrintfLine("ACK {} unknown command %q", args[0]) log.Printf("unknown command: %s\n", args[0]) return } cmdOk = true p.PrintfLine(okLine) return }
func childError(t *testing.T, conn *textproto.Conn, err error) { conn.PrintfLine("E") t.Error(err) os.Exit(1) }
func handleNewGroups(args []string, s *session, c *textproto.Conn) error { c.PrintfLine("231 list of newsgroups follows") c.PrintfLine(".") return nil }
func handleQuit(args []string, s *session, c *textproto.Conn) error { c.PrintfLine("205 bye") return io.EOF }