Documentation
¶
Overview ¶
Package netpop is a pure-Go (CGO=0) reimplementation of the POP3 protocol codec at the heart of Ruby's Net::POP3 — MRI's net-pop gem (net/pop.rb on top of net/protocol.rb).
It builds the POP3 command bytes (USER/PASS/APOP/STAT/LIST/UIDL/RETR/TOP/DELE/ RSET/NOOP/QUIT/STLS), computes the APOP MD5 digest, parses the "+OK"/"-ERR" status replies, and decodes a multiline response — the ".\r\n" terminator plus the leading-dot un-stuffing — into the Net::POPMail model (number / length / unique-id). It matches MRI byte-for-byte on the wire bytes and on the parsed values, without any Ruby runtime.
The connect / read / write / TLS is the host's job, injected as a small Transport interface (Writeline + Readline). rbgo wires a real TCP/TLS socket; tests use an in-memory transport, so the whole suite is deterministic and Ruby-free — exactly mirroring how MRI layers Net::POP3Command over the Net::InternetMessageIO socket.
Index ¶
- func ApopCommand(account, stamp, password string) string
- func ApopDigest(stamp, password string) string
- func ApopStamp(greeting string) (string, bool)
- func CheckResponse(res string) error
- func CheckResponseAuth(res string) error
- func DeleCommand(num int) string
- func IsOK(res string) bool
- func IsTerminator(rawLine string) bool
- func ListCommand() string
- func NoopCommand() string
- func ParseListItem(line string) (number, length int, err error)
- func ParseStat(res string) (count, size int, err error)
- func ParseUidlItem(line string) (number int, uid string, ok bool)
- func ParseUidlSingle(res string) string
- func PassCommand(password string) string
- func QuitCommand() string
- func RetrCommand(num int) string
- func RsetCommand() string
- func SetAllUIDs(mails []*POPMail, table map[int]string)
- func StatCommand() string
- func StlsCommand() string
- func TopCommand(num, lines int) string
- func UidlCommand() string
- func UidlNumCommand(num int) string
- func Unstuff(line string) string
- func UserCommand(account string) string
- func Writeline(line string) string
- type Conn
- func (c *Conn) Apop(account, password string) error
- func (c *Conn) ApopStamp() (string, bool)
- func (c *Conn) Auth(account, password string) error
- func (c *Conn) Dele(num int) error
- func (c *Conn) Greet() error
- func (c *Conn) List() ([]*POPMail, error)
- func (c *Conn) Noop() error
- func (c *Conn) Quit() error
- func (c *Conn) Retr(num int) (string, error)
- func (c *Conn) Rset() error
- func (c *Conn) Stat() (count, size int, err error)
- func (c *Conn) Stls() error
- func (c *Conn) Top(num, lines int) (string, error)
- func (c *Conn) UIDL() (map[int]string, error)
- func (c *Conn) UIDLNum(num int) (string, error)
- type POPAuthenticationError
- type POPBadResponse
- type POPError
- type POPMail
- type Transport
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func ApopCommand ¶
ApopCommand builds the APOP command line: "APOP <account> <digest>", where digest is the lowercase hex MD5 of (stamp + password). This is the exact line MRI's POP3Command#apop writes.
func ApopDigest ¶
ApopDigest computes the APOP authentication digest exactly as MRI does: Digest::MD5.hexdigest(stamp + password), where stamp is the server-greeting timestamp token (including its surrounding angle brackets) and the result is lowercase hex. See RFC 1939 §7.
func ApopStamp ¶
ApopStamp extracts the APOP timestamp from a server greeting line, matching MRI's POP3Command#initialize: res.slice(/<[!-~]+@[!-~]+>/). It returns the "<...@...>" token (brackets included) and ok==true, or ("", false) when the greeting carries no APOP stamp.
The Ruby regexp /<[!-~]+@[!-~]+>/ matches the *first* "<", one-or-more printable ASCII (0x21..0x7e) characters, an "@", one-or-more printable ASCII, and a ">". Because "@" and ">" are themselves in [!-~], the "+" quantifiers backtrack to the last "@" / first ">" that still satisfies the whole pattern; we reproduce that leftmost-longest-with-required-tail behaviour directly.
func CheckResponse ¶
CheckResponse returns a *POPError when res is not a "+OK" reply, matching MRI's POP3Command#check_response. The error carries res verbatim (MRI raises POPError with the response string).
func CheckResponseAuth ¶
CheckResponseAuth returns a *POPAuthenticationError when res is not a "+OK" reply, matching MRI's POP3Command#check_response_auth.
func DeleCommand ¶
DeleCommand builds the DELE command line: "DELE <num>".
func IsOK ¶
IsOK reports whether a status reply indicates success, matching MRI's check_response: /\A\+OK/i — a case-insensitive "+OK" anchored at the start.
func IsTerminator ¶
IsTerminator reports whether a raw read line (still bearing its CRLF) is the "." -on-its-own-line multiline terminator ".\r\n", matching MRI's loop guard `(line = readuntil("\r\n")) != ".\r\n"`.
func ListCommand ¶
func ListCommand() string
ListCommand builds the multiline-LIST command line: "LIST".
func ParseListItem ¶
ParseListItem parses one line of a multiline LIST response into (number, length), matching MRI's POP3Command#list: /\A(\d+)[ \t]+(\d+)/ over a line that has already had its CRLF chopped. On mismatch it returns a *POPBadResponse (MRI raises POPBadResponse, "bad response: #{line}").
func ParseStat ¶
ParseStat parses a STAT "+OK" reply into the (count, size) pair, matching MRI's POP3Command#stat: /\A\+OK\s+(\d+)\s+(\d+)/. The reply must begin with "+OK" (any case — MRI's regexp uses a literal "+OK", but the reply has already been validated by check_response which is case-insensitive; we accept either case here for the literal "+OK", then the two whitespace-separated decimals). On a shape mismatch it returns a *POPBadResponse carrying res, exactly as MRI raises "wrong response format: <res>".
Note: MRI raises POPBadResponse with the *message* "wrong response format: #{res}". We surface res itself in the typed error and leave the human prefix to the caller's formatting, so the parsed-value comparison stays byte-exact.
func ParseUidlItem ¶
ParseUidlItem parses one line of a multiline UIDL response into (number, uid), matching MRI's POP3Command#uidl: num, uid = line.split(' '). Ruby's String#split with a single ASCII-space argument splits on runs of *any* whitespace and discards leading whitespace, so " 3 abc " yields ["3", "abc"]. We reproduce that. A line with no fields yields ok==false (MRI would leave num/uid nil; the caller skips such a degenerate line — it never reaches here for a well-formed terminator).
func ParseUidlSingle ¶
ParseUidlSingle parses a single-message "UIDL <num>" "+OK" reply into the uid, matching MRI's POP3Command#uidl(num): res.split(/ /)[1]. Ruby's split(/ /) on a single-space *regexp* (not the special " " string) does NOT collapse runs and keeps leading empty fields, so the second field is res.split(/ /)[1]. We reproduce that exact indexing.
func PassCommand ¶
PassCommand builds the PASS command line: "PASS <password>".
func RetrCommand ¶
RetrCommand builds the RETR command line: "RETR <num>".
func SetAllUIDs ¶
SetAllUIDs fills the UID of each mail from a number->uid table (as produced by Conn.UIDL), mirroring POP3#set_all_uids: m.uid = uidl[m.number]. Mails whose number is absent from the table get an empty UID.
func StlsCommand ¶
func StlsCommand() string
StlsCommand builds the STLS command line: "STLS" (RFC 2595 STARTTLS for POP3).
func TopCommand ¶
TopCommand builds the TOP command line: "TOP <num> <lines>". Equivalent to sprintf('TOP %d %d', num, lines).
func UidlCommand ¶
func UidlCommand() string
UidlCommand builds the multiline-UIDL command line: "UIDL".
func UidlNumCommand ¶
UidlNumCommand builds the single-message UIDL command line: "UIDL <num>". Equivalent to sprintf('UIDL %d', num).
func Unstuff ¶
Unstuff removes one leading "." from a multiline message chunk, matching MRI's each_message_chunk: line.delete_prefix('.'). Only a single leading dot is stripped (so a line that was ".." on the wire becomes "."), and a line with no leading dot is returned unchanged.
func UserCommand ¶
UserCommand builds the USER command line (without the trailing CRLF): "USER <account>". Equivalent to sprintf('USER %s', account).
Types ¶
type Conn ¶
type Conn struct {
// contains filtered or unexported fields
}
Conn drives a POP3 session over a Transport. It is the Go analogue of MRI's Net::POP3Command: it issues the commands, validates the "+OK"/"-ERR" status replies, parses STAT/LIST/UIDL, and decodes the multiline RETR/TOP bodies — all without owning the socket.
func NewConn ¶
NewConn returns a Conn driving the given Transport. The greeting is not read here; call Greet (mirroring how MRI's POP3Command#initialize reads the banner) before authenticating.
func (*Conn) Apop ¶
Apop performs APOP authentication, mirroring POP3Command#apop. It requires that the greeting carried an APOP stamp (else MRI raises POPAuthenticationError, "not APOP server; cannot login"). A non-"+OK" reply yields a *POPAuthenticationError.
func (*Conn) ApopStamp ¶
ApopStamp returns the APOP timestamp captured from the greeting, and whether one was present. Valid only after Greet.
func (*Conn) Auth ¶
Auth performs USER/PASS authentication, mirroring POP3Command#auth: send USER, require "+OK", send PASS, require "+OK". A non-"+OK" reply at either step yields a *POPAuthenticationError.
func (*Conn) Dele ¶
Dele issues "DELE <num>", mirroring POP3Command#dele. A non-"+OK" reply yields a *POPError.
func (*Conn) Greet ¶
Greet reads and validates the server greeting, capturing the APOP stamp, mirroring MRI's POP3Command#initialize:
res = check_response(critical { recv_response() })
@apop_stamp = res.slice(/<[!-~]+@[!-~]+>/)
It returns a *POPError when the greeting is not "+OK".
func (*Conn) List ¶
List issues a multiline LIST and returns one POPMail per message (number + length; uid empty until set via SetAllUIDs), mirroring POP3Command#list mapped through POP3#mails.
func (*Conn) Noop ¶
Noop issues NOOP and requires a "+OK" reply. NOOP is not used by MRI's net-pop, but is a standard no-argument POP3 keepalive (RFC 1939); we validate the reply the same way the other single-line commands do.
func (*Conn) Retr ¶
Retr issues "RETR <num>" and returns the full message bytes, mirroring POP3Command#retr feeding each_message_chunk. Each multiline chunk is dot-unstuffed; the ".\r\n" terminator ends the body. The returned bytes are the concatenation of the (CRLF-bearing) un-stuffed chunks — exactly what MRI's POPMail#pop accumulates.
func (*Conn) Stls ¶
Stls issues STLS (RFC 2595 STARTTLS for POP3) and requires a "+OK" reply. After a "+OK" the host is expected to upgrade the underlying Transport to TLS; that upgrade is the socket seam's concern, not this codec's.
func (*Conn) Top ¶
Top issues "TOP <num> <lines>" and returns the header plus the first `lines` body lines, mirroring POP3Command#top.
type POPAuthenticationError ¶
type POPAuthenticationError struct {
Response string
}
POPAuthenticationError is raised when authentication (USER/PASS or APOP) fails. It corresponds to Net::POPAuthenticationError.
func (*POPAuthenticationError) Error ¶
func (e *POPAuthenticationError) Error() string
type POPBadResponse ¶
type POPBadResponse struct {
Response string
}
POPBadResponse is raised when the server returns a reply that does not match the expected shape for STAT/LIST/UIDL. It corresponds to Net::POPBadResponse.
func (*POPBadResponse) Error ¶
func (e *POPBadResponse) Error() string
type POPError ¶
type POPError struct {
// Response is the raw server line that triggered the error, with its CRLF
// terminator already stripped — exactly the String MRI passes to raise.
Response string
}
POPError is the base error: an ordinary non-authentication "-ERR" reply (or any reply that does not begin with "+OK"). It corresponds to Net::POPError.
type POPMail ¶
type POPMail struct {
// Number is the message's sequence number on the server (Net::POPMail#number).
Number int
// Length is the message size in octets (Net::POPMail#length / #size).
Length int
// UID is the message's unique-id (Net::POPMail#unique_id / #uidl), empty until
// populated from a UIDL response.
UID string
// Deleted records whether the message has been marked for deletion this
// session (Net::POPMail#deleted?). DELE on the server defers actual removal to
// session end; RSET clears the mark.
Deleted bool
}
POPMail is the Go analogue of Net::POPMail: a message that exists on the POP server, identified by its sequence Number, with its Length in octets and, optionally, its unique-id (UID). MRI constructs these from the LIST reply and fills the UID lazily from a UIDL reply.
Unlike MRI's POPMail, this struct holds no back-reference to a live session and performs no I/O — it is the parsed data model. The session-driving (pop / top / delete / unique_id) lives on Conn, which owns the Transport seam.
type Transport ¶
type Transport interface {
// Writeline writes one command line, appending CRLF.
Writeline(line string) error
// Readline reads one response line and returns it without the trailing CRLF.
Readline() (string, error)
// ReadRawLine reads one line of a multiline body and returns it WITH its
// trailing CRLF intact, mirroring Net::BufferedIO#readuntil("\r\n"). The
// terminator detection ((line) != ".\r\n") and the dot-unstuffing depend on
// the CRLF being present, so multiline reads use this rather than Readline.
ReadRawLine() (string, error)
}
Transport is the socket seam. It is the pure-compute boundary of this package: everything above it (command bytes, APOP digest, response parsing, multiline dot-unstuffing, the POPMail model) is deterministic and Ruby-free; everything below it (TCP connect, TLS handshake, timeouts, the actual read/write syscalls) is the host's job.
It mirrors the two methods MRI's POP3Command leans on from Net::InternetMessageIO:
- Writeline(line) corresponds to writeline(str): the implementation MUST append a single CRLF — exactly what Net::InternetMessageIO#writeline does. Callers therefore pass the bare command line (e.g. "STAT", "RETR 3").
- Readline() corresponds to readline: it returns one response line WITHOUT its trailing CRLF (Net::BufferedIO#readline strips it). The status-line parsers here expect that stripped form.
rbgo wires a real *net.Conn / *tls.Conn behind this; tests use an in-memory transport, so the whole suite is deterministic.
