| // Copyright 2010 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| // Package textproto implements generic support for text-based request/response |
| // protocols in the style of HTTP, NNTP, and SMTP. |
| // |
| // The package provides: |
| // |
| // [Error], which represents a numeric error response from |
| // a server. |
| // |
| // [Pipeline], to manage pipelined requests and responses |
| // in a client. |
| // |
| // [Reader], to read numeric response code lines, |
| // key: value headers, lines wrapped with leading spaces |
| // on continuation lines, and whole text blocks ending |
| // with a dot on a line by itself. |
| // |
| // [Writer], to write dot-encoded text blocks. |
| // |
| // [Conn], a convenient packaging of [Reader], [Writer], and [Pipeline] for use |
| // with a single network connection. |
| package textproto |
| |
| import ( |
| "bufio" |
| "fmt" |
| "io" |
| "net" |
| ) |
| |
| // An Error represents a numeric error response from a server. |
| type Error struct { |
| Code int |
| Msg string |
| } |
| |
| func (e *Error) Error() string { |
| return fmt.Sprintf("%03d %s", e.Code, e.Msg) |
| } |
| |
| // A ProtocolError describes a protocol violation such |
| // as an invalid response or a hung-up connection. |
| type ProtocolError string |
| |
| func (p ProtocolError) Error() string { |
| return string(p) |
| } |
| |
| // A Conn represents a textual network protocol connection. |
| // It consists of a [Reader] and [Writer] to manage I/O |
| // and a [Pipeline] to sequence concurrent requests on the connection. |
| // These embedded types carry methods with them; |
| // see the documentation of those types for details. |
| type Conn struct { |
| Reader |
| Writer |
| Pipeline |
| conn io.ReadWriteCloser |
| } |
| |
| // NewConn returns a new [Conn] using conn for I/O. |
| func NewConn(conn io.ReadWriteCloser) *Conn { |
| return &Conn{ |
| Reader: Reader{R: bufio.NewReader(conn)}, |
| Writer: Writer{W: bufio.NewWriter(conn)}, |
| conn: conn, |
| } |
| } |
| |
| // Close closes the connection. |
| func (c *Conn) Close() error { |
| return c.conn.Close() |
| } |
| |
| // Dial connects to the given address on the given network using [net.Dial] |
| // and then returns a new [Conn] for the connection. |
| func Dial(network, addr string) (*Conn, error) { |
| c, err := net.Dial(network, addr) |
| if err != nil { |
| return nil, err |
| } |
| return NewConn(c), nil |
| } |
| |
| // Cmd is a convenience method that sends a command after |
| // waiting its turn in the pipeline. The command text is the |
| // result of formatting format with args and appending \r\n. |
| // Cmd returns the id of the command, for use with StartResponse and EndResponse. |
| // |
| // For example, a client might run a HELP command that returns a dot-body |
| // by using: |
| // |
| // id, err := c.Cmd("HELP") |
| // if err != nil { |
| // return nil, err |
| // } |
| // |
| // c.StartResponse(id) |
| // defer c.EndResponse(id) |
| // |
| // if _, _, err = c.ReadCodeLine(110); err != nil { |
| // return nil, err |
| // } |
| // text, err := c.ReadDotBytes() |
| // if err != nil { |
| // return nil, err |
| // } |
| // return c.ReadCodeLine(250) |
| func (c *Conn) Cmd(format string, args ...any) (id uint, err error) { |
| id = c.Next() |
| c.StartRequest(id) |
| err = c.PrintfLine(format, args...) |
| c.EndRequest(id) |
| if err != nil { |
| return 0, err |
| } |
| return id, nil |
| } |
| |
| // TrimString returns s without leading and trailing ASCII space. |
| func TrimString(s string) string { |
| for len(s) > 0 && isASCIISpace(s[0]) { |
| s = s[1:] |
| } |
| for len(s) > 0 && isASCIISpace(s[len(s)-1]) { |
| s = s[:len(s)-1] |
| } |
| return s |
| } |
| |
| // TrimBytes returns b without leading and trailing ASCII space. |
| func TrimBytes(b []byte) []byte { |
| for len(b) > 0 && isASCIISpace(b[0]) { |
| b = b[1:] |
| } |
| for len(b) > 0 && isASCIISpace(b[len(b)-1]) { |
| b = b[:len(b)-1] |
| } |
| return b |
| } |
| |
| func isASCIISpace(b byte) bool { |
| return b == ' ' || b == '\t' || b == '\n' || b == '\r' |
| } |
| |
| func isASCIILetter(b byte) bool { |
| b |= 0x20 // make lower case |
| return 'a' <= b && b <= 'z' |
| } |