Documentation
¶
Overview ¶
Package netsmtp is a pure-Go (no cgo) reimplementation of the deterministic, interpreter-independent core of Ruby's Net::SMTP: the SMTP command grammar, the DATA message dot-stuffing, the reply-line parser, the EHLO capability parse, and the SASL auth encodings (PLAIN, LOGIN, CRAM-MD5). It mirrors MRI 4.0.5's net-smtp 0.5.1 byte-for-byte on every string it produces and every reply it parses, without any Ruby runtime.
What it is — and isn't ¶
Building a request line ("MAIL FROM:<a@b>"), dot-stuffing a DATA payload, splitting a "250-…/250 …" reply into a Response, decoding an EHLO capability list, and encoding an AUTH PLAIN/LOGIN/CRAM-MD5 exchange are all pure compute, so they live here. The TCP connection, the TLS upgrade (STARTTLS), and the blocking read/write of those bytes are the host's job: this package never opens a socket. The host (go-embedded-ruby's rbgo) supplies a Conn, and the Session driver below sequences the codec against it so the same exchange MRI performs over a real socket is reproduced here with the I/O factored out as a seam.
The socket / TLS seam ¶
A host wires in transport by implementing Conn:
type Conn interface {
WriteLine(line string) error // send a request line + CRLF
ReadLine() (string, error) // read one reply line, CRLF stripped
WriteRaw(b string) error // send a DATA payload verbatim
StartTLS() error // upgrade the connection in place
}
The codec functions (HELO, MailFrom, WriteMessage, ParseResponse, AuthPlain, …) are usable directly without a Conn; Session is the optional convenience driver.
Index ¶
- Constants
- func AuthCramMD5Initial() string
- func AuthCramMD5Response(user, secret string, challenge []byte) string
- func AuthLoginInitial() string
- func AuthLoginSecret(secret string) string
- func AuthLoginUser(user string) string
- func AuthPlain(user, secret string) string
- func DATA() (string, error)
- func EHLO(domain string) (string, error)
- func HELO(domain string) (string, error)
- func MailFrom(from string, params ...string) (string, error)
- func QUIT() (string, error)
- func RSET() (string, error)
- func RcptTo(to string, params ...string) (string, error)
- func STARTTLS() (string, error)
- func ValidateLine(line string) error
- func Wire(reqline string) string
- func WriteMessage(src string) string
- type Address
- type Conn
- type ErrBareCRLF
- type ErrorKind
- type Response
- type SMTPError
- type Session
- func (s *Session) AuthCramMD5(user, secret string) (*Response, error)
- func (s *Session) AuthLogin(user, secret string) (*Response, error)
- func (s *Session) AuthPlain(user, secret string) (*Response, error)
- func (s *Session) Capabilities() map[string][]string
- func (s *Session) Data(msg string) (*Response, error)
- func (s *Session) Ehlo(domain string) (*Response, error)
- func (s *Session) Helo(domain string) (*Response, error)
- func (s *Session) MailFrom(from string, params ...string) (*Response, error)
- func (s *Session) Quit() (*Response, error)
- func (s *Session) RcptTo(to string, params ...string) (*Response, error)
- func (s *Session) RecvResponse() (*Response, error)
- func (s *Session) Rset() (*Response, error)
- func (s *Session) StartTLS() (*Response, error)
Constants ¶
const CRLF = "\r\n"
CRLF is the wire line terminator (Net writeline appends "\r\n").
Variables ¶
This section is empty.
Functions ¶
func AuthCramMD5Initial ¶
func AuthCramMD5Initial() string
AuthCramMD5Initial is the first CRAM-MD5 line (AuthCramMD5#auth: continue('AUTH CRAM-MD5')), which prompts the server for its challenge.
func AuthCramMD5Response ¶
AuthCramMD5Response builds the base64 "user hexdigest" response to a decoded CRAM-MD5 challenge, mirroring AuthCramMD5#auth's final finish(...). The caller supplies the already-base64-decoded challenge bytes (the host's seam reads the server's challenge line; Response.CRAMMD5Challenge decodes it).
func AuthLoginInitial ¶
func AuthLoginInitial() string
AuthLoginInitial is the first AUTH LOGIN line (AuthLogin#auth: continue('AUTH LOGIN')).
func AuthLoginSecret ¶
AuthLoginSecret is the base64-encoded secret sent after the password prompt (AuthLogin#auth: finish(base64_encode(secret))).
func AuthLoginUser ¶
AuthLoginUser is the base64-encoded user name sent after the username prompt (AuthLogin#auth: continue(base64_encode(user))).
func AuthPlain ¶
AuthPlain builds the single AUTH PLAIN request line, mirroring AuthPlain#auth: finish('AUTH PLAIN ' + base64_encode("\0user\0secret")).
func MailFrom ¶
MailFrom builds the "MAIL FROM:<addr>" request line plus any ESMTP parameters, mirroring Net::SMTP#mailfrom: (["MAIL FROM:<addr>"] + params).join(' ').
func RcptTo ¶
RcptTo builds the "RCPT TO:<addr>" request line plus any ESMTP parameters, mirroring Net::SMTP#rcptto: (["RCPT TO:<addr>"] + params).join(' ').
func ValidateLine ¶
ValidateLine mirrors Net::SMTP#validate_line: a request line must not contain a bare CR or LF (RFC 5321). Any "\r" or "\n" is rejected.
func Wire ¶
Wire renders a request line as it goes on the wire: the line followed by CRLF (Net::BufferedIO#writeline appends "\r\n" to the reqline). It does not validate; the command builders already did.
func WriteMessage ¶
WriteMessage renders a DATA payload exactly as Net::InternetMessageIO#write_message puts it on the wire: every line normalised to CRLF, dot-stuffed (a leading "." doubled), an unterminated final line CRLF-terminated, an empty source emitted as a single CRLF, and the whole closed by the "." + CRLF terminator.
WriteMessage(".hi\r\n") == "..hi\r\n.\r\n"
WriteMessage("") == "\r\n.\r\n"
Types ¶
type Address ¶
Address mirrors Net::SMTP::Address: an envelope address plus its ESMTP parameters. Parameters are deduplicated preserving first-seen order, each rendered "k=v" (or bare "k") just as the Ruby constructor does.
func NewAddress ¶
NewAddress builds an Address from a mail address and zero or more "k=v" (or bare) parameter strings, deduplicating while preserving order (Ruby's #uniq).
type Conn ¶
type Conn interface {
// WriteLine sends a request line; the implementation appends CRLF.
WriteLine(line string) error
// ReadLine reads one reply line and returns it with the trailing CRLF
// removed (Ruby readline = readuntil("\n").chop).
ReadLine() (string, error)
// WriteRaw writes the given bytes to the connection unchanged (used for the
// already dot-stuffed, terminated DATA payload).
WriteRaw(b string) error
// StartTLS upgrades the connection to TLS in place.
StartTLS() error
}
Conn is the transport seam the host supplies. It abstracts the TCP/TLS socket Net::SMTP holds in @socket; the codec drives it but never creates it. WriteLine sends a request line (the impl appends CRLF, or accepts the pre-terminated form from the command builders — implementations append CRLF themselves to mirror BufferedIO#writeline). ReadLine returns one reply line with its CRLF removed (Net::BufferedIO#readline). WriteRaw writes a DATA payload verbatim. StartTLS upgrades the connection in place.
type ErrBareCRLF ¶
type ErrBareCRLF struct{}
ErrBareCRLF is returned by ValidateLine and the command builders when a request line contains a bare CR or LF, mirroring Net::SMTP#validate_line which raises ArgumentError, "A line must not contain CR or LF".
func (ErrBareCRLF) Error ¶
func (ErrBareCRLF) Error() string
type ErrorKind ¶
type ErrorKind int
ErrorKind names a member of the Net::SMTPError family. The host (rbgo) maps each kind to the matching Ruby exception class; the codec selects the kind purely from the reply code so the mapping needs no interpreter.
const ( // KindUnknown is Net::SMTPUnknownError (code outside 4xx/5xx, or a 5xx not // covered by the more specific kinds). KindUnknown ErrorKind = iota // KindServerBusy is Net::SMTPServerBusy (4xx). KindServerBusy // KindSyntax is Net::SMTPSyntaxError (50x). KindSyntax // KindAuthentication is Net::SMTPAuthenticationError (53x). KindAuthentication // KindFatal is Net::SMTPFatalError (other 5xx). KindFatal )
type Response ¶
type Response struct {
// Status is the first three bytes of the reply (the numeric code). MRI does
// not validate that they are digits — it is literally str[0,3].
Status string
// String is the human-readable reply text: the whole multi-line reply, each
// line CRLF-chopped and "\n"-terminated.
String string
}
Response is a server reply, mirroring Net::SMTP::Response. It holds the raw three-digit Status and the full reply String (every line, terminated by a bare "\n"), exactly as MRI's recv_response assembles it: each wire line has its trailing CRLF chopped and a single "\n" re-appended.
res := ParseResponse("250-PIPELINING\n250 HELP\n")
res.Status // "250"
res.Success() // true
func ParseResponse ¶
ParseResponse mirrors Net::SMTP::Response.parse(str): the status is the first three bytes (str[0,3]) and the string is the whole reply verbatim. A string shorter than three bytes yields a status equal to whatever bytes exist, just as Ruby's str[0,3] returns the available prefix.
func (*Response) CRAMMD5Challenge ¶
CRAMMD5Challenge decodes the base64 CRAM-MD5 challenge carried in the reply, mirroring Response#cram_md5_challenge: @string.split(/ /)[1].unpack1('m'). The reply is split on single spaces and the second field base64-decoded.
func (*Response) Capabilities ¶
Capabilities parses the EHLO capability map, mirroring Response#capabilities. It returns an empty map unless the reply is multi-line (String[3,1] == "-"); then each continuation line (drop the first) is split on spaces after its "NNN-" / "NNN " prefix, keying the verb to its arguments. A duplicate verb keeps the last line's arguments, matching Ruby's hash assignment.
func (*Response) Continue ¶
Continue reports a 3xx Positive Intermediate reply (Response#continue?).
func (*Response) ExceptionClass ¶
ExceptionClass selects the error type for a non-success reply, mirroring Response#exception_class's case on @status: /\A4/→ServerBusy, /\A50/→Syntax, /\A53/→Authentication, /\A5/→Fatal, else→Unknown. The ordering matters: 50x is tested before the general 5xx, and 53x before it too (53 matches /\A50/? no — 53 is tested by /\A53/ which comes after /\A50/, and 530 does not start "50").
type SMTPError ¶
SMTPError carries the kind, the offending Response, and the message. It mirrors Net::SMTPError instances, whose default message is the reply's first line (ProtocolError.new(response).message), so a host can surface the same text.
type Session ¶
type Session struct {
// contains filtered or unexported fields
}
Session drives the codec against a Conn, sequencing commands and replies the way Net::SMTP does: validate_line, writeline, recv_response (multi-line aware), then check_response. It is a thin convenience over the pure functions; a host that wants finer control can call those directly.
func (*Session) AuthCramMD5 ¶
AuthCramMD5 runs the AUTH CRAM-MD5 exchange (AuthCramMD5#auth): the initial line yields the challenge (3xx continue), whose payload is decoded and answered.
func (*Session) AuthLogin ¶
AuthLogin runs the AUTH LOGIN exchange (AuthLogin#auth): the initial line and the user name each require a 3xx continue, then the secret requires success.
func (*Session) AuthPlain ¶
AuthPlain runs the AUTH PLAIN exchange (AuthPlain#auth): one line, success required.
func (*Session) Capabilities ¶
Capabilities returns the capability map from the last successful Ehlo, or nil.
func (*Session) Data ¶
Data performs the DATA exchange (Net::SMTP#data): send DATA, require a 3xx continue reply (else SMTPUnknownError "could not get 3xx"), write the dot-stuffed message, then read and check the final reply.
func (*Session) Ehlo ¶
Ehlo sends EHLO, returns the reply, and caches its capabilities for later AUTH/SMTPUTF8 decisions (Net::SMTP#ehlo).
func (*Session) MailFrom ¶
MailFrom sends MAIL FROM with optional ESMTP parameters (Net::SMTP#mailfrom).
func (*Session) RecvResponse ¶
RecvResponse reads a full reply, mirroring Net::SMTP#recv_response: it reads lines until one whose 4th byte is not "-", assembling them (CRLF-stripped, each re-terminated with "\n") into the Response string, then parses it.
