smtp

package
v0.53.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 2, 2024 License: BSD-3-Clause Imports: 24 Imported by: 1

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

Examples

Constants

View Source
const (
	CommandZERO CommandKind = 0
	CommandHELO             = 1 << iota
	CommandEHLO
	CommandAUTH
	CommandMAIL
	CommandRCPT
	CommandDATA
	CommandRSET
	CommandVRFY
	CommandEXPN
	CommandHELP
	CommandNOOP
	CommandQUIT
)

List of SMTP commands.

View Source
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.
	StatusMailboxUnavailable       = 450
	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

View Source
var (
	ErrInvalidCredential = &errors.E{
		Code:    StatusInvalidCredential,
		Message: "5.7.8 Authentication credentials invalid",
	}
)

List of errors.

Functions

func ParseMailbox

func ParseMailbox(data []byte) (mailbox []byte)

ParseMailbox parse the mailbox, remove comment or any escaped characters insided quoted-string.

func ParsePath

func ParsePath(path []byte) (mailbox []byte, err error)

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
package main

import (
	"fmt"

	"github.com/shuLhan/share/lib/smtp"
)

func main() {
	var mb []byte

	mb, _ = smtp.ParsePath([]byte(`<@domain.com,@domain.net:local.part@domain.com>`))
	fmt.Printf("%s\n", mb)
	mb, _ = smtp.ParsePath([]byte(`<local.part@domain.com>`))
	fmt.Printf("%s\n", mb)
	mb, _ = smtp.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

func NewAccount(name, local, domain, pass string) (acc *Account, err error)

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

func (acc *Account) Authenticate(pass string) (err error)

Authenticate a user using plain text password. It will return an error if password does not match.

func (*Account) Short

func (acc *Account) Short() (out string)

Short return the account email address without Name, "local@domain".

func (*Account) String

func (acc *Account) String() (out string)

String representation of account in the format of "Name <local@domain>" if Name is not empty, or "local@domain" is Name is empty.

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) Expand

func (cl *Client) Expand(mlist string) (res *Response, err error)

Expand get members of mailing-list.

func (*Client) Help

func (cl *Client) Help(cmdName string) (res *Response, err error)

Help get information on specific command from server.

func (*Client) MailTx

func (cl *Client) MailTx(mail *MailTx) (res *Response, err error)

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

func (cl *Client) Noop(msg string) (res *Response, err error)

Noop send the NOOP command to server with optional message.

On success, it will return response with Code 250, StatusOK.

func (*Client) Quit

func (cl *Client) Quit() (res *Response, err error)

Quit signal the server that the client will close the connection.

func (*Client) Reset added in v0.31.0

func (cl *Client) Reset() (res *Response, err error)

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

func (cl *Client) SendCommand(cmd []byte) (res *Response, err error)

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 sending email. This method automatically create MailTx for passing it to method Client.MailTx.

func (*Client) StartTLS added in v0.5.0

func (cl *Client) StartTLS() (res *Response, err error)

StartTLS upgrade the underlying connection to TLS. This method only works if client connected to remote URL using scheme "smtp+starttls" or on port 587, and on server that support STARTTLS extension.

func (*Client) Verify

func (cl *Client) Verify(mailbox string) (res *Response, err error)

Verify send the VRFY command to server to check if mailbox is exist.

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 //revive:disable-line

	// 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 CommandKind

type CommandKind int

CommandKind represent the numeric value of SMTP command.

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.

type LocalHandler

type LocalHandler struct {
	// contains filtered or unexported fields
}

LocalHandler is an handler using local environment.

func NewLocalHandler

func NewLocalHandler(env *Environment) (local *LocalHandler)

NewLocalHandler create 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(_ *MailTx) (res *Response, err error)

ServeBounce handle email transaction with unknown or invalid recipent.

func (*LocalHandler) ServeExpand

func (lh *LocalHandler) ServeExpand(_ string) (res *Response, err error)

ServeExpand handle SMTP EXPN command.

TODO: The group feature currently is not supported.

func (*LocalHandler) ServeMailTx

func (lh *LocalHandler) ServeMailTx(_ *MailTx) (res *Response, err error)

ServeMailTx handle processing the final delivery of incoming mail. TODO: implement it.

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 NewLocalStorage

func NewLocalStorage(dir string) (storage *LocalStorage, err error)

NewLocalStorage create and initialize new file storage. If directory is empty, the default storage is located at "/var/spool/smtp/".

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.MailTx.
	Received time.Time

	// ID of message.
	// This field is ignored in Client.MailTx.
	ID string

	// From contains originator address.
	// This field is required in Client.MailTx.
	From string

	// Recipients contains list of the destination address.
	// This field is required in Client.MailTx.
	Recipients []string

	// Data contains content of message.
	// This field is optional in Client.MailTx.
	Data []byte

	Retry int
}

MailTx define a mail transaction.

func NewMailTx

func NewMailTx(from string, to []string, data []byte) (mail *MailTx)

NewMailTx create and return new mail object.

Example
package main

import (
	"bytes"
	"fmt"
	"log"
	"regexp"
	"time"

	"github.com/shuLhan/share/lib/email"
	"github.com/shuLhan/share/lib/smtp"
)

func main() {
	// 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     *smtp.MailTx
		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 = smtp.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.

	data = bytes.ReplaceAll(mailtx.Data, []byte("\r\n"), []byte("\n"))

	var (
		reDate = regexp.MustCompile(`^date: Wed(.*) \+....`)
	)
	data = reDate.ReplaceAll(data, []byte(`date: `+dateNowUtc))

	var (
		msgID   = msg.Header.ID()
		fixedID = `1645600000.QoqDPQfz@hostname`
	)
	data = bytes.Replace(data, []byte(msgID), []byte(fixedID), 1)

	var (
		msgBoundary   = msg.Header.Boundary()
		fixedBoundary = `QoqDPQfzDVkv5R49vrA78GmqPmlfmBHf`
	)
	data = bytes.ReplaceAll(data, []byte(msgBoundary), []byte(fixedBoundary))

	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--

func (*MailTx) Reset

func (mail *MailTx) Reset()

Reset all mail attributes to their zero value.

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

type Response struct {
	Message string
	Body    []string
	Code    int
}

Response represent a generic single or multilines response from server.

func NewResponse

func NewResponse(raw []byte) (res *Response, err error)

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

func (srv *Server) LoadCertificate(certFile, keyFile string) (err error)

LoadCertificate load TLS certificate and its private key from file.

func (*Server) Start

func (srv *Server) Start() (err error)

Start listening for SMTP connections. Each client connection will be handled in a single routine.

func (*Server) Stop

func (srv *Server) Stop()

Stop the server.

type ServerInfo

type ServerInfo struct {
	Exts   map[string][]string
	Domain string
	Info   string
}

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).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL