Documentation ¶
Overview ¶
Package smtp provide a library for building SMTP server and client.
Server ¶
By default, server will listen on port 25 and 465.
Port 25 is only used to receive message relay from other mail server. Any command that require authentication will be rejected by this port.
Port 465 is used to receive message submission from SMTP accounts with authentication.
Server Environment ¶
The server require one primary domain with one primary account called "postmaster". Domain can have two or more accounts. Domain can have their own DKIM certificate.
Limitations ¶
The server favor implicit TLS over STARTTLS (RFC 8314) on port 465 for message submission.
Index ¶
- Constants
- Variables
- func ParseMailbox(data []byte) (mailbox []byte)
- func ParsePath(path []byte) (mailbox []byte, err error)
- type Account
- type Client
- func (cl *Client) Authenticate(mech SaslMechanism, username, password string) (res *Response, err error)
- func (cl *Client) Expand(mlist string) (res *Response, err error)
- func (cl *Client) Help(cmdName string) (res *Response, err error)
- func (cl *Client) MailTx(mail *MailTx) (res *Response, err error)
- func (cl *Client) Noop(msg string) (res *Response, err error)
- func (cl *Client) Quit() (res *Response, err error)
- func (cl *Client) Reset() (res *Response, err error)
- func (cl *Client) SendCommand(cmd []byte) (res *Response, err error)
- func (cl *Client) SendEmail(from string, to []string, subject, bodyText, bodyHtml []byte) (err error)
- func (cl *Client) StartTLS() (res *Response, err error)
- func (cl *Client) Verify(mailbox string) (res *Response, err error)
- type ClientOptions
- type Command
- type CommandKind
- type DKIMOptions
- type Domain
- type Environment
- type Extension
- type Handler
- type LocalHandler
- func (lh *LocalHandler) ServeAuth(username, password string) (res *Response, err error)
- func (lh *LocalHandler) ServeBounce(mail *MailTx) (res *Response, err error)
- func (lh *LocalHandler) ServeExpand(mailingList string) (res *Response, err error)
- func (lh *LocalHandler) ServeMailTx(mail *MailTx) (res *Response, err error)
- func (lh *LocalHandler) ServeVerify(username string) (res *Response, err error)
- type LocalStorage
- func (fs *LocalStorage) MailBounce(id string) error
- func (fs *LocalStorage) MailDelete(id string) (err error)
- func (fs *LocalStorage) MailLoad(id string) (mail *MailTx, err error)
- func (fs *LocalStorage) MailLoadAll() (mails []*MailTx, err error)
- func (fs *LocalStorage) MailSave(mail *MailTx) (err error)
- type MailTx
- type Mailbox
- type Response
- type SaslMechanism
- type Server
- type ServerInfo
- type Storage
Examples ¶
Constants ¶
const ( CommandZERO CommandKind = 0 CommandHELO = 1 << iota CommandEHLO CommandAUTH CommandMAIL CommandRCPT CommandDATA CommandRSET CommandVRFY CommandEXPN CommandHELP CommandNOOP CommandQUIT )
List of SMTP commands.
const ( // // 2yz Positive Completion reply // // The requested action has been successfully completed. A new // request may be initiated. // StatusSystem = 211 StatusHelp = 214 StatusReady = 220 StatusClosing = 221 StatusAuthenticated = 235 // RFC 4954 StatusOK = 250 StatusAddressChange = 251 // RFC 5321, section 3.4. StatusVerifyFailed = 252 // RFC 5321, section 3.5.3. // // 3xx Positive Intermediate reply. // // The command has been accepted, but the requested action is being // held in abeyance, pending receipt of further information. The // SMTP client should send another command specifying this // information. This reply is used in command DATA. // StatusAuthReady = 334 StatusDataReady = 354 // // 4xx Transient Negative Completion reply // // The command was not accepted, and the requested action did not // occur. However, the error condition is temporary, and the action // may be requested again. The sender should return to the beginning // of the command sequence (if any). It is difficult to assign a // meaning to "transient" when two different sites (receiver- and // sender-SMTP agents) must agree on the interpretation. Each reply // in this category might have a different time value, but the SMTP // client SHOULD try again. A rule of thumb to determine whether a // reply fits into the 4yz or the 5yz category (see below) is that // replies are 4yz if they can be successful if repeated without any // change in command form or in properties of the sender or receiver // (that is, the command is repeated identically and the receiver // does not put up a new implementation). // StatusShuttingDown = 421 StatusPasswordTransitionNeeded = 432 // RFC 4954 section 4.7.12. StatusLocalError = 451 StatusNoStorage = 452 StatusTemporaryAuthFailure = 454 // RFC 4954 section 4.7.0. StatusParameterUnprocessable = 455 // // 5xx indicate permanent failure. // // The command was not accepted and the requested action did not // occur. The SMTP client SHOULD NOT repeat the exact request (in // the same sequence). Even some "permanent" error conditions can be // corrected, so the human user may want to direct the SMTP client to // reinitiate the command sequence by direct action at some point in // the future (e.g., after the spelling has been changed, or the user // has altered the account status). // StatusCmdUnknown = 500 // RFC 5321 section 4.2.4. StatusCmdTooLong = 500 // RFC 5321 section 4.3.2, RFC 4954 section 5.5.6. StatusCmdSyntaxError = 501 StatusCmdNotImplemented = 502 // RFC 5321 section 4.2.4. StatusCmdBadSequence = 503 StatusParamUnimplemented = 504 StatusNotAuthenticated = 530 StatusAuthMechanismTooWeak = 534 // RFC 4954 section 5.7.9. StatusInvalidCredential = 535 // RFC 4954 section 5.7.8. StatusMailboxNotFound = 550 StatusAddressChangeAborted = 551 // RFC 5321 section 3.4. StatusMailNoStorage = 552 StatusMailboxIncorrect = 553 StatusTransactionFailed = 554 StatusMailRcptParamUnknown = 555 )
List of SMTP status codes.
Variables ¶
var ( ErrInvalidCredential = &errors.E{ Code: StatusInvalidCredential, Message: "5.7.8 Authentication credentials invalid", } )
List of errors.
Functions ¶
func ParseMailbox ¶
ParseMailbox parse the mailbox, remove comment or any escaped characters insided quoted-string.
func ParsePath ¶
ParsePath parse the Reverse-path or Forward-path as in argument of MAIL and RCPT commands. This function ignore the source route and only return the mailbox. Empty mailbox without an error is equal to Null Reverse-Path "<>".
Example ¶
mb, _ := ParsePath([]byte(`<@domain.com,@domain.net:local.part@domain.com>`)) fmt.Printf("%s\n", mb) mb, _ = ParsePath([]byte(`<local.part@domain.com>`)) fmt.Printf("%s\n", mb) mb, _ = ParsePath([]byte(`<local>`)) fmt.Printf("%s\n", mb)
Output: local.part@domain.com local.part@domain.com local
Types ¶
type Account ¶
type Account struct { Mailbox // HashPass user password that has been hashed using bcrypt. HashPass string }
Account represent an SMTP account in the server that can send and receive email.
func NewAccount ¶
NewAccount create new account. Password will be hashed using bcrypt. An account with empty password is system account, which mean it will not allowed in SMTP AUTH.
func (*Account) Authenticate ¶
Authenticate a user using plain text password. It will return an error if password does not match.
type Client ¶
type Client struct { // ServerInfo contains the server information, from the response of // EHLO command. ServerInfo *ServerInfo // contains filtered or unexported fields }
Client for SMTP.
func NewClient ¶
func NewClient(opts ClientOptions) (cl *Client, err error)
NewClient create and initialize connection to remote SMTP server.
When connected, the client send implicit EHLO command issued to server immediately. If scheme is "smtp+starttls", the connection automatically upgraded to TLS after EHLO command success.
If both AuthUser and AuthPass in the ClientOptions is not empty, the client will try to authenticate to remote server.
On fail, it will return nil client with an error.
func (*Client) Authenticate ¶
func (cl *Client) Authenticate(mech SaslMechanism, username, password string) ( res *Response, err error, )
Authenticate to server using one of SASL mechanism. Currently, the only mechanism available is PLAIN.
func (*Client) MailTx ¶
MailTx send the mail to server. This function is implementation of mail transaction (MAIL, RCPT, and DATA commands as described in RFC 5321, section 3.3). The MailTx.Data must be internet message format which contains headers and content as defined by RFC 5322.
On success, it will return the last response, which is the success status of data transaction (250).
On fail, it will return response from the failed command with error is string combination of command, response code and message.
func (*Client) Noop ¶ added in v0.31.0
Noop send the NOOP command to server with optional message.
On success, it will return response with Code 250, StatusOK.
func (*Client) Reset ¶ added in v0.31.0
Reset send the RSET command to server. This command clear the current buffer on MAIL, RCPT, and DATA, but not the EHLO/HELO buffer.
On success, it will return response with Code 250, StatusOK.
func (*Client) SendCommand ¶
SendCommand send any custom command to server.
func (*Client) SendEmail ¶ added in v0.35.0
func (cl *Client) SendEmail(from string, to []string, subject, bodyText, bodyHtml []byte) (err error)
SendEmail is the wrapper that simplify MailTx.
type ClientOptions ¶ added in v0.35.0
type ClientOptions struct { // LocalName define the client domain address, used when issuing EHLO // command to server. // If its empty, it will set to current operating system's // hostname. // The LocalName only has effect when client is connecting from // server-to-server. LocalName string // ServerUrl use the following format, // // ServerUrl = [ scheme "://" ](domain | IP-address)[":" port] // scheme = "smtp" / "smtps" / "smtp+starttls" // // If scheme is "smtp" and no port is given, client will connect to // remote address at port 25. // If scheme is "smtps" and no port is given, client will connect to // remote address at port 465 (implicit TLS). // If scheme is "smtp+starttls" and no port is given, client will // connect to remote address at port 587. ServerUrl string // The user name to authenticate to remote server. // // AuthUser and AuthPass enable automatic authentication when creating // new Client, as long as one is not empty. AuthUser string // The user password to authenticate to remote server. AuthPass string // The SASL mechanism used for authentication. AuthMechanism SaslMechanism // Insecure if set to true it will disable verifying remote certificate when // connecting with TLS or STARTTLS. Insecure bool }
ClientOptions contains all options to create new client.
type Command ¶
type Command struct { Params map[string]string Arg string Param string Kind CommandKind }
Command represent a single SMTP command with its parsed argument and parameters.
type DKIMOptions ¶ added in v0.6.0
type DKIMOptions struct { Signature *dkim.Signature PrivateKey *rsa.PrivateKey }
DKIMOptions contains the DKIM signature fields and private key to sign the incoming message.
type Domain ¶
type Domain struct { Accounts map[string]*Account Name string // contains filtered or unexported fields }
Domain contains a host name and list of accounts in domain, with optional DKIM feature.
func NewDomain ¶
func NewDomain(name string, dkimOpts *DKIMOptions) (domain *Domain)
NewDomain create new domain with single main user, "postmaster".
type Environment ¶
type Environment struct { // PrimaryDomain of the SMTP server. // This field is required. PrimaryDomain *Domain // VirtualDomains contains list of virtual domain handled by server. // This field is optional. VirtualDomains map[string]*Domain }
Environment contains SMTP server environment.
type Extension ¶
type Extension interface { // // Name return the SMTP extension name to be used on reply of EHLO. // Name() string // // Params return the SMTP extension parameters. // Params() string // // ValidateCommand validate the command parameters, if the extension // provide custom parameters. // ValidateCommand(cmd *Command) error }
Extension is an interface to implement extension for SMTP server.
type Handler ¶
type Handler interface { // ServeAuth handle SMTP AUTH parameter username and password. ServeAuth(username, password string) (*Response, error) // ServeBounce handle email transaction that with unknown or invalid // recipent. ServeBounce(mail *MailTx) (*Response, error) // ServeExpand handle SMTP EXPN command. ServeExpand(mailingList string) (*Response, error) // ServeMailTx handle termination on email transaction. ServeMailTx(mail *MailTx) (*Response, error) // ServeVerify handle SMTP VRFY command. ServeVerify(username string) (*Response, error) }
Handler define an interface to handle bouncing and incoming mail message, and handling EXPN and VRFY commands.
func NewLocalHandler ¶
func NewLocalHandler(env *Environment) Handler
NewLocalHandler create an handler using local environment.
type LocalHandler ¶
type LocalHandler struct {
// contains filtered or unexported fields
}
LocalHandler is an handler using local environment.
func (*LocalHandler) ServeAuth ¶
func (lh *LocalHandler) ServeAuth(username, password string) ( res *Response, err error, )
ServeAuth handle SMTP AUTH parameter username and password.
func (*LocalHandler) ServeBounce ¶
func (lh *LocalHandler) ServeBounce(mail *MailTx) (res *Response, err error)
ServeBounce handle email transaction with unknown or invalid recipent.
func (*LocalHandler) ServeExpand ¶
func (lh *LocalHandler) ServeExpand(mailingList string) (res *Response, err error)
ServeExpand handle SMTP EXPN command.
BUG: The group feature currently is not supported.
func (*LocalHandler) ServeMailTx ¶
func (lh *LocalHandler) ServeMailTx(mail *MailTx) (res *Response, err error)
ServeMailTx handle processing the final delivery of incoming mail.
func (*LocalHandler) ServeVerify ¶
func (lh *LocalHandler) ServeVerify(username string) (res *Response, err error)
ServeVerify handle SMTP VRFY command. The username must be in the format of mailbox, "local@domain".
type LocalStorage ¶
type LocalStorage struct {
// contains filtered or unexported fields
}
LocalStorage implement the Storage interface where mail object is save and retrieved in file system inside a directory.
func (*LocalStorage) MailBounce ¶
func (fs *LocalStorage) MailBounce(id string) error
MailBounce move the incoming mail to bounced state. In this storage service, the mail file is moved to "{dir}/bounce".
func (*LocalStorage) MailDelete ¶
func (fs *LocalStorage) MailDelete(id string) (err error)
MailDelete the mail object on file system by ID.
func (*LocalStorage) MailLoad ¶
func (fs *LocalStorage) MailLoad(id string) (mail *MailTx, err error)
MailLoad read the mail object from file system by ID.
func (*LocalStorage) MailLoadAll ¶
func (fs *LocalStorage) MailLoadAll() (mails []*MailTx, err error)
MailLoadAll mail objects from file system.
func (*LocalStorage) MailSave ¶
func (fs *LocalStorage) MailSave(mail *MailTx) (err error)
MailSave save the mail object into file system.
type MailTx ¶
type MailTx struct { Postpone time.Time // Received contains the time when the message arrived on server. // This field is ignored in Client.Send(). Received time.Time // ID of message. // This field is ignored in Client.Send(). ID string // From contains originator address. // This field is required in Client.Send(). From string // Recipients contains list of the destination address. // This field is required in Client.Send(). Recipients []string // Data contains content of message. // This field is optional in Client.Send(). Data []byte Retry int }
MailTx define a mail transaction.
func NewMailTx ¶
NewMailTx create and return new mail object.
Example ¶
// Example on how to create MailTx Data using email package [1]. // // [1] github.com/shuLhan/share/lib/email // Overwrite the email.Epoch to make the example works. email.Epoch = func() int64 { return 1645600000 } var ( txFrom = "Postmaster <postmaster@mail.example.com>" fromAddress = []byte("Noreply <noreply@example.com>") toAddresses = []byte("John <john@example.com>, Jane <jane@example.com>") subject = []byte(`Example subject`) bodyText = []byte(`Email body as plain text`) bodyHtml = []byte(`Email body as <b>HTML</b>`) timeNowUtc = time.Unix(email.Epoch(), 0).UTC() dateNowUtc = timeNowUtc.Format(email.DateFormat) recipients []string mboxes []*email.Mailbox msg *email.Message mailtx *MailTx reDate *regexp.Regexp hostname string data []byte err error ) mboxes, err = email.ParseMailboxes(toAddresses) if err != nil { log.Fatal(err) } for _, mbox := range mboxes { recipients = append(recipients, mbox.Address) } msg, err = email.NewMultipart( fromAddress, toAddresses, subject, bodyText, bodyHtml, ) if err != nil { log.Fatal(err) } // The From parameter is not necessary equal to the fromAddress. // The From in MailTx define the account that authorize or allowed // sending the email on behalf of fromAddress domain, while the // fromAddress define the address that viewed by recipients. data, _ = msg.Pack() mailtx = NewMailTx(txFrom, recipients, data) fmt.Printf("Tx From: %s\n", mailtx.From) fmt.Printf("Tx Recipients: %s\n", mailtx.Recipients) // In order to make the example Output works, we need to replace all // CRLF with LF, "date:" with the system timezone, and message-id // hostname with fixed "hostname". data = bytes.ReplaceAll(mailtx.Data, []byte("\r\n"), []byte("\n")) //fmt.Printf("timeNowUtc: %s\n", timeNowUtc) //fmt.Printf("dateNowUtc: %s\n", dateNowUtc) reDate = regexp.MustCompile(`^date: Wed(.*) \+....`) data = reDate.ReplaceAll(data, []byte(`date: `+dateNowUtc)) hostname, err = os.Hostname() if err != nil { log.Fatal(err) } data = bytes.Replace(data, []byte(hostname), []byte("hostname"), 1) fmt.Printf("Tx Data:\n%s", data)
Output: Tx From: Postmaster <postmaster@mail.example.com> Tx Recipients: [john@example.com jane@example.com] Tx Data: date: Wed, 23 Feb 2022 07:06:40 +0000 from: Noreply <noreply@example.com> to: John <john@example.com>, Jane <jane@example.com> subject: Example subject mime-version: 1.0 content-type: multipart/alternative; boundary=QoqDPQfzDVkv5R49vrA78GmqPmlfmBHf message-id: <1645600000.QoqDPQfz@hostname> --QoqDPQfzDVkv5R49vrA78GmqPmlfmBHf mime-version: 1.0 content-type: text/plain; charset="utf-8" content-transfer-encoding: quoted-printable Email body as plain text --QoqDPQfzDVkv5R49vrA78GmqPmlfmBHf mime-version: 1.0 content-type: text/html; charset="utf-8" content-transfer-encoding: quoted-printable Email body as <b>HTML</b> --QoqDPQfzDVkv5R49vrA78GmqPmlfmBHf--
type Mailbox ¶
type Mailbox struct { Name string // Name of user in system. Local string // Local part. Domain string // Domain part. }
Mailbox represent a mailbox format.
type Response ¶
Response represent a generic single or multilines response from server.
func NewResponse ¶
NewResponse create and initialize new Response from parsing the raw response text.
type SaslMechanism ¶ added in v0.35.0
type SaslMechanism int
SaslMechanism represent Simple Authentication and Security Layer (SASL) mechanism (RFC 4422).
const (
SaslMechanismPlain SaslMechanism = 1
)
List of available SASL mechanism.
type Server ¶
type Server struct { // Env contains server environment. Env *Environment // // Handler define an interface that will process the bouncing email, // incoming email, EXPN command, and VRFY command. // This field is optional, if not set, it will default to // LocalHandler. // Handler Handler // TLSCert the server certificate for TLS or nil if no certificate. // This field is optional, if its non nil, the server will also listen // on address defined in TLSAddress. TLSCert *tls.Certificate // // Exts define list of custom extensions that the server will provide. // Exts []Extension // contains filtered or unexported fields }
Server defines parameters for running an SMTP server.
func (*Server) LoadCertificate ¶
LoadCertificate load TLS certificate and its private key from file.
type ServerInfo ¶
ServerInfo provide information about server from response of EHLO or HELO command.
func NewServerInfo ¶
func NewServerInfo(res *Response) (srvInfo *ServerInfo)
NewServerInfo create and initialize ServerInfo from EHLO/HELO response.
type Storage ¶
type Storage interface { MailBounce(id string) error MailDelete(id string) error MailLoad(id string) (mail *MailTx, err error) MailLoadAll() (mail []*MailTx, err error) MailSave(mail *MailTx) error }
Storage define an interface for storing and retrieving mail object into permanent storage (for example, file system or database).
func NewLocalStorage ¶
NewLocalStorage create and initialize new file storage. If directory is empty, the default storage is located at "/var/spool/smtp/".