1// Copyright 2010 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5// Package textproto implements generic support for text-based request/response
6// protocols in the style of HTTP, NNTP, and SMTP.
7//
8// The package provides:
9//
10// [Error], which represents a numeric error response from
11// a server.
12//
13// [Pipeline], to manage pipelined requests and responses
14// in a client.
15//
16// [Reader], to read numeric response code lines,
17// key: value headers, lines wrapped with leading spaces
18// on continuation lines, and whole text blocks ending
19// with a dot on a line by itself.
20//
21// [Writer], to write dot-encoded text blocks.
22//
23// [Conn], a convenient packaging of [Reader], [Writer], and [Pipeline] for use
24// with a single network connection.
25package textproto
26
27import (
28	"bufio"
29	"fmt"
30	"io"
31	"net"
32)
33
34// An Error represents a numeric error response from a server.
35type Error struct {
36	Code int
37	Msg  string
38}
39
40func (e *Error) Error() string {
41	return fmt.Sprintf("%03d %s", e.Code, e.Msg)
42}
43
44// A ProtocolError describes a protocol violation such
45// as an invalid response or a hung-up connection.
46type ProtocolError string
47
48func (p ProtocolError) Error() string {
49	return string(p)
50}
51
52// A Conn represents a textual network protocol connection.
53// It consists of a [Reader] and [Writer] to manage I/O
54// and a [Pipeline] to sequence concurrent requests on the connection.
55// These embedded types carry methods with them;
56// see the documentation of those types for details.
57type Conn struct {
58	Reader
59	Writer
60	Pipeline
61	conn io.ReadWriteCloser
62}
63
64// NewConn returns a new [Conn] using conn for I/O.
65func NewConn(conn io.ReadWriteCloser) *Conn {
66	return &Conn{
67		Reader: Reader{R: bufio.NewReader(conn)},
68		Writer: Writer{W: bufio.NewWriter(conn)},
69		conn:   conn,
70	}
71}
72
73// Close closes the connection.
74func (c *Conn) Close() error {
75	return c.conn.Close()
76}
77
78// Dial connects to the given address on the given network using [net.Dial]
79// and then returns a new [Conn] for the connection.
80func Dial(network, addr string) (*Conn, error) {
81	c, err := net.Dial(network, addr)
82	if err != nil {
83		return nil, err
84	}
85	return NewConn(c), nil
86}
87
88// Cmd is a convenience method that sends a command after
89// waiting its turn in the pipeline. The command text is the
90// result of formatting format with args and appending \r\n.
91// Cmd returns the id of the command, for use with StartResponse and EndResponse.
92//
93// For example, a client might run a HELP command that returns a dot-body
94// by using:
95//
96//	id, err := c.Cmd("HELP")
97//	if err != nil {
98//		return nil, err
99//	}
100//
101//	c.StartResponse(id)
102//	defer c.EndResponse(id)
103//
104//	if _, _, err = c.ReadCodeLine(110); err != nil {
105//		return nil, err
106//	}
107//	text, err := c.ReadDotBytes()
108//	if err != nil {
109//		return nil, err
110//	}
111//	return c.ReadCodeLine(250)
112func (c *Conn) Cmd(format string, args ...any) (id uint, err error) {
113	id = c.Next()
114	c.StartRequest(id)
115	err = c.PrintfLine(format, args...)
116	c.EndRequest(id)
117	if err != nil {
118		return 0, err
119	}
120	return id, nil
121}
122
123// TrimString returns s without leading and trailing ASCII space.
124func TrimString(s string) string {
125	for len(s) > 0 && isASCIISpace(s[0]) {
126		s = s[1:]
127	}
128	for len(s) > 0 && isASCIISpace(s[len(s)-1]) {
129		s = s[:len(s)-1]
130	}
131	return s
132}
133
134// TrimBytes returns b without leading and trailing ASCII space.
135func TrimBytes(b []byte) []byte {
136	for len(b) > 0 && isASCIISpace(b[0]) {
137		b = b[1:]
138	}
139	for len(b) > 0 && isASCIISpace(b[len(b)-1]) {
140		b = b[:len(b)-1]
141	}
142	return b
143}
144
145func isASCIISpace(b byte) bool {
146	return b == ' ' || b == '\t' || b == '\n' || b == '\r'
147}
148
149func isASCIILetter(b byte) bool {
150	b |= 0x20 // make lower case
151	return 'a' <= b && b <= 'z'
152}
153