upgrade deps; rewrite smtp session

This commit is contained in:
Aine
2024-02-19 22:55:14 +02:00
parent 10213cc7d7
commit a01720da00
277 changed files with 106832 additions and 7641 deletions

View File

@@ -2,12 +2,9 @@ package dkim
import (
"io"
"regexp"
"strings"
)
var rxReduceWS = regexp.MustCompile(`[ \t\r\n]+`)
// Canonicalization is a canonicalization algorithm.
type Canonicalization string
@@ -113,17 +110,15 @@ func (c *simpleCanonicalizer) CanonicalizeBody(w io.Writer) io.WriteCloser {
type relaxedCanonicalizer struct{}
func (c *relaxedCanonicalizer) CanonicalizeHeader(s string) string {
kv := strings.SplitN(s, ":", 2)
k := strings.TrimSpace(strings.ToLower(kv[0]))
var v string
if len(kv) > 1 {
v = rxReduceWS.ReplaceAllString(kv[1], " ")
v = strings.TrimSpace(v)
k, v, ok := strings.Cut(s, ":")
if !ok {
return strings.TrimSpace(strings.ToLower(s)) + ":" + crlf
}
k = strings.TrimSpace(strings.ToLower(k))
v = strings.Join(strings.FieldsFunc(v, func(r rune) bool {
return r == ' ' || r == '\t' || r == '\n' || r == '\r'
}), " ")
return k + ":" + v + crlf
}

View File

@@ -1,4 +1,17 @@
// Package dkim creates and verifies DKIM signatures, as specified in RFC 6376.
//
// # FAQ
//
// Why can't I verify a [net/mail.Message] directly? A [net/mail.Message]
// header is already parsed, and whitespace characters (especially continuation
// lines) are removed. Thus, the signature computed from the parsed header is
// not the same as the one computed from the raw header.
//
// How can I publish my public key? You have to add a TXT record to your DNS
// zone. See [RFC 6376 appendix C]. You can use the dkim-keygen tool included
// in go-msgauth to generate the key and the TXT record.
//
// [RFC 6376 appendix C]: https://tools.ietf.org/html/rfc6376#appendix-C
package dkim
import (

View File

@@ -66,28 +66,24 @@ func foldHeaderField(kv string) string {
return fold.String() + crlf
}
func parseHeaderField(s string) (k string, v string) {
kv := strings.SplitN(s, ":", 2)
k = strings.TrimSpace(kv[0])
if len(kv) > 1 {
v = strings.TrimSpace(kv[1])
}
return
func parseHeaderField(s string) (string, string) {
key, value, _ := strings.Cut(s, ":")
return strings.TrimSpace(key), strings.TrimSpace(value)
}
func parseHeaderParams(s string) (map[string]string, error) {
pairs := strings.Split(s, ";")
params := make(map[string]string)
for _, s := range pairs {
kv := strings.SplitN(s, "=", 2)
if len(kv) != 2 {
key, value, ok := strings.Cut(s, "=")
if !ok {
if strings.TrimSpace(s) == "" {
continue
}
return params, errors.New("dkim: malformed header params")
}
params[strings.TrimSpace(kv[0])] = strings.TrimSpace(kv[1])
params[strings.TrimSpace(key)] = strings.TrimSpace(value)
}
return params, nil
}
@@ -149,6 +145,8 @@ func newHeaderPicker(h header) *headerPicker {
}
func (p *headerPicker) Pick(key string) string {
key = strings.ToLower(key)
at := p.picked[key]
for i := len(p.h) - 1; i >= 0; i-- {
kv := p.h[i]

View File

@@ -70,24 +70,31 @@ var queryMethods = map[QueryMethod]queryFunc{
}
func queryDNSTXT(domain, selector string, txtLookup txtLookupFunc) (*queryResult, error) {
var txts []string
var err error
if txtLookup != nil {
txts, err = txtLookup(selector + "._domainkey." + domain)
} else {
txts, err = net.LookupTXT(selector + "._domainkey." + domain)
if txtLookup == nil {
txtLookup = net.LookupTXT
}
txts, err := txtLookup(selector + "._domainkey." + domain)
if netErr, ok := err.(net.Error); ok && netErr.Temporary() {
return nil, tempFailError("key unavailable: " + err.Error())
} else if err != nil {
return nil, permFailError("no key for signature: " + err.Error())
}
// Long keys are split in multiple parts
txt := strings.Join(txts, "")
return parsePublicKey(txt)
// net.LookupTXT will concatenate strings contained in a single TXT record.
// In other words, net.LookupTXT returns one entry per TXT record, even if
// a record contains multiple strings.
//
// RFC 6376 section 3.6.2.2 says multiple TXT records lead to undefined
// behavior, so reject that.
switch len(txts) {
case 0:
return nil, permFailError("no valid key found")
case 1:
return parsePublicKey(txts[0])
default:
return nil, permFailError("multiple TXT records found for key")
}
}
func parsePublicKey(s string) (*queryResult, error) {

View File

@@ -74,7 +74,7 @@ type SignOptions struct {
//
// The whole message header and body must be written to the Signer. Close should
// always be called (either after the whole message has been written, or after
// an error occured and the signer won't be used anymore). Close may return an
// an error occurred and the signer won't be used anymore). Close may return an
// error in case signing fails.
//
// After a successful Close, Signature can be called to retrieve the

View File

@@ -293,12 +293,10 @@ func verify(h header, r io.Reader, sigField, sigValue string, options *VerifyOpt
}
// Parse algos
algos := strings.SplitN(stripWhitespace(params["a"]), "-", 2)
if len(algos) != 2 {
keyAlgo, hashAlgo, ok := strings.Cut(stripWhitespace(params["a"]), "-")
if !ok {
return verif, permFailError("malformed algorithm name")
}
keyAlgo := algos[0]
hashAlgo := algos[1]
// Check hash algo
if res.HashAlgos != nil {
@@ -457,6 +455,8 @@ func stripWhitespace(s string) string {
}, s)
}
var sigRegex = regexp.MustCompile(`(b\s*=)[^;]+`)
func removeSignature(s string) string {
return regexp.MustCompile(`(b\s*=)[^;]+`).ReplaceAllString(s, "$1")
return sigRegex.ReplaceAllString(s, "$1")
}

View File

@@ -1,11 +1,12 @@
# go-sasl
[![GoDoc](https://godoc.org/github.com/emersion/go-sasl?status.svg)](https://godoc.org/github.com/emersion/go-sasl)
[![godocs.io](https://godocs.io/github.com/emersion/go-sasl?status.svg)](https://godocs.io/github.com/emersion/go-sasl)
[![Build Status](https://travis-ci.org/emersion/go-sasl.svg?branch=master)](https://travis-ci.org/emersion/go-sasl)
A [SASL](https://tools.ietf.org/html/rfc4422) library written in Go.
Implemented mechanisms:
* [ANONYMOUS](https://tools.ietf.org/html/rfc4505)
* [EXTERNAL](https://tools.ietf.org/html/rfc4422#appendix-A)
* [LOGIN](https://tools.ietf.org/html/draft-murchison-sasl-login-00) (obsolete, use PLAIN instead)

View File

@@ -27,7 +27,7 @@ func NewAnonymousClient(trace string) Client {
type AnonymousAuthenticator func(trace string) error
type anonymousServer struct {
done bool
done bool
authenticate AnonymousAuthenticator
}

View File

@@ -1,5 +1,10 @@
package sasl
import (
"bytes"
"errors"
)
// The EXTERNAL mechanism name.
const External = "EXTERNAL"
@@ -24,3 +29,39 @@ func (a *externalClient) Next(challenge []byte) (response []byte, err error) {
func NewExternalClient(identity string) Client {
return &externalClient{identity}
}
// ExternalAuthenticator authenticates users with the EXTERNAL mechanism. If
// the identity is left blank, it indicates that it is the same as the one used
// in the external credentials. If identity is not empty and the server doesn't
// support it, an error must be returned.
type ExternalAuthenticator func(identity string) error
type externalServer struct {
done bool
authenticate ExternalAuthenticator
}
func (a *externalServer) Next(response []byte) (challenge []byte, done bool, err error) {
if a.done {
return nil, false, ErrUnexpectedClientResponse
}
// No initial response, send an empty challenge
if response == nil {
return []byte{}, false, nil
}
a.done = true
if bytes.Contains(response, []byte("\x00")) {
return nil, false, errors.New("sasl: identity contains a NUL character")
}
return nil, true, a.authenticate(string(response))
}
// NewExternalServer creates a server implementation of the EXTERNAL
// authentication mechanism, as described in RFC 4422.
func NewExternalServer(authenticator ExternalAuthenticator) Server {
return &externalServer{authenticate: authenticator}
}

View File

@@ -35,8 +35,11 @@ type oauthBearerClient struct {
}
func (a *oauthBearerClient) Start() (mech string, ir []byte, err error) {
mech = OAuthBearer
var str = "n,a=" + a.Username + ","
var authzid string
if a.Username != "" {
authzid = "a=" + a.Username
}
str := "n," + authzid + ","
if a.Host != "" {
str += "\x01host=" + a.Host
@@ -47,7 +50,7 @@ func (a *oauthBearerClient) Start() (mech string, ir []byte, err error) {
}
str += "\x01auth=Bearer " + a.Token + "\x01\x01"
ir = []byte(str)
return
return OAuthBearer, ir, nil
}
func (a *oauthBearerClient) Next(challenge []byte) ([]byte, error) {
@@ -81,7 +84,7 @@ func (a *oauthBearerServer) fail(descr string) ([]byte, bool, error) {
if err != nil {
panic(err) // wtf
}
a.failErr = errors.New(descr)
a.failErr = errors.New("sasl: client error: " + descr)
return blob, false, nil
}
@@ -95,7 +98,7 @@ func (a *oauthBearerServer) Next(response []byte) (challenge []byte, done bool,
// indirectly OAUTHBEARER) defines a protocol-independent way to do so
// using 0x01.
if len(response) != 1 && response[0] != 0x01 {
return nil, true, errors.New("unexpected response")
return nil, true, errors.New("sasl: invalid response")
}
return nil, true, a.failErr
}
@@ -121,14 +124,18 @@ func (a *oauthBearerServer) Next(response []byte) (challenge []byte, done bool,
if len(parts) != 3 {
return a.fail("Invalid response")
}
if !bytes.Equal(parts[0], []byte{'n'}) {
return a.fail("Invalid response, missing 'n'")
flag := parts[0]
authzid := parts[1]
if !bytes.Equal(flag, []byte{'n'}) {
return a.fail("Invalid response, missing 'n' in gs2-cb-flag")
}
opts := OAuthBearerOptions{}
if !bytes.HasPrefix(parts[1], []byte("a=")) {
return a.fail("Invalid response, missing 'a'")
if len(authzid) > 0 {
if !bytes.HasPrefix(authzid, []byte("a=")) {
return a.fail("Invalid response, missing 'a=' in gs2-authzid")
}
opts.Username = string(bytes.TrimPrefix(authzid, []byte("a=")))
}
opts.Username = string(bytes.TrimPrefix(parts[1], []byte("a=")))
// Cut \x01host=...\x01auth=...\x01\x01
// into

View File

@@ -38,7 +38,7 @@ func NewPlainClient(identity, username, password string) Client {
type PlainAuthenticator func(identity, username, password string) error
type plainServer struct {
done bool
done bool
authenticate PlainAuthenticator
}
@@ -57,7 +57,7 @@ func (a *plainServer) Next(response []byte) (challenge []byte, done bool, err er
parts := bytes.Split(response, []byte("\x00"))
if len(parts) != 3 {
err = errors.New("Invalid response")
err = errors.New("sasl: invalid response")
return
}

View File

@@ -12,7 +12,7 @@ import (
// Common SASL errors.
var (
ErrUnexpectedClientResponse = errors.New("sasl: unexpected client response")
ErrUnexpectedClientResponse = errors.New("sasl: unexpected client response")
ErrUnexpectedServerChallenge = errors.New("sasl: unexpected server challenge")
)

View File

@@ -1,11 +1,10 @@
image: alpine/edge
packages:
- go
# Required by codecov
- bash
- findutils
sources:
- https://github.com/emersion/go-smtp
artifacts:
- coverage.html
tasks:
- build: |
cd go-smtp
@@ -13,7 +12,6 @@ tasks:
- test: |
cd go-smtp
go test -coverprofile=coverage.txt -covermode=atomic ./...
- upload-coverage: |
- coverage: |
cd go-smtp
export CODECOV_TOKEN=3f257f71-a128-4834-8f68-2b534e9f4cb1
curl -s https://codecov.io/bash | bash
go tool cover -html=coverage.txt -o ~/coverage.html

View File

@@ -1,144 +1,16 @@
# go-smtp
[![godocs.io](https://godocs.io/github.com/emersion/go-smtp?status.svg)](https://godocs.io/github.com/emersion/go-smtp)
[![Go Reference](https://pkg.go.dev/badge/github.com/emersion/go-smtp.svg)](https://pkg.go.dev/github.com/emersion/go-smtp)
[![builds.sr.ht status](https://builds.sr.ht/~emersion/go-smtp/commits.svg)](https://builds.sr.ht/~emersion/go-smtp/commits?)
[![codecov](https://codecov.io/gh/emersion/go-smtp/branch/master/graph/badge.svg)](https://codecov.io/gh/emersion/go-smtp)
An ESMTP client and server library written in Go.
## Features
* ESMTP client & server implementing [RFC 5321](https://tools.ietf.org/html/rfc5321)
* Support for SMTP [AUTH](https://tools.ietf.org/html/rfc4954) and [PIPELINING](https://tools.ietf.org/html/rfc2920)
* ESMTP client & server implementing [RFC 5321]
* Support for additional SMTP extensions such as [AUTH] and [PIPELINING]
* UTF-8 support for subject and message
* [LMTP](https://tools.ietf.org/html/rfc2033) support
## Usage
### Client
```go
package main
import (
"log"
"strings"
"github.com/emersion/go-sasl"
"github.com/emersion/go-smtp"
)
func main() {
// Set up authentication information.
auth := sasl.NewPlainClient("", "user@example.com", "password")
// Connect to the server, authenticate, set the sender and recipient,
// and send the email all in one step.
to := []string{"recipient@example.net"}
msg := strings.NewReader("To: recipient@example.net\r\n" +
"Subject: discount Gophers!\r\n" +
"\r\n" +
"This is the email body.\r\n")
err := smtp.SendMail("mail.example.com:25", auth, "sender@example.org", to, msg)
if err != nil {
log.Fatal(err)
}
}
```
If you need more control, you can use `Client` instead.
### Server
```go
package main
import (
"errors"
"io"
"io/ioutil"
"log"
"time"
"github.com/emersion/go-smtp"
)
// The Backend implements SMTP server methods.
type Backend struct{}
// Login handles a login command with username and password.
func (bkd *Backend) Login(state *smtp.ConnectionState, username, password string) (smtp.Session, error) {
if username != "username" || password != "password" {
return nil, errors.New("Invalid username or password")
}
return &Session{}, nil
}
// AnonymousLogin requires clients to authenticate using SMTP AUTH before sending emails
func (bkd *Backend) AnonymousLogin(state *smtp.ConnectionState) (smtp.Session, error) {
return nil, smtp.ErrAuthRequired
}
// A Session is returned after successful login.
type Session struct{}
func (s *Session) Mail(from string, opts smtp.MailOptions) error {
log.Println("Mail from:", from)
return nil
}
func (s *Session) Rcpt(to string) error {
log.Println("Rcpt to:", to)
return nil
}
func (s *Session) Data(r io.Reader) error {
if b, err := ioutil.ReadAll(r); err != nil {
return err
} else {
log.Println("Data:", string(b))
}
return nil
}
func (s *Session) Reset() {}
func (s *Session) Logout() error {
return nil
}
func main() {
be := &Backend{}
s := smtp.NewServer(be)
s.Addr = ":1025"
s.Domain = "localhost"
s.ReadTimeout = 10 * time.Second
s.WriteTimeout = 10 * time.Second
s.MaxMessageBytes = 1024 * 1024
s.MaxRecipients = 50
s.AllowInsecureAuth = true
log.Println("Starting server at", s.Addr)
if err := s.ListenAndServe(); err != nil {
log.Fatal(err)
}
}
```
You can use the server manually with `telnet`:
```
$ telnet localhost 1025
EHLO localhost
AUTH PLAIN
AHVzZXJuYW1lAHBhc3N3b3Jk
MAIL FROM:<root@nsa.gov>
RCPT TO:<root@gchq.gov.uk>
DATA
Hey <3
.
```
* [LMTP] support
## Relationship with net/smtp
@@ -149,3 +21,8 @@ provides a server implementation and a number of client improvements.
## Licence
MIT
[RFC 5321]: https://tools.ietf.org/html/rfc5321
[AUTH]: https://tools.ietf.org/html/rfc4954
[PIPELINING]: https://tools.ietf.org/html/rfc2920
[LMTP]: https://tools.ietf.org/html/rfc2033

View File

@@ -1,61 +1,30 @@
package smtp
import (
"errors"
"io"
)
var (
ErrAuthRequired = errors.New("Please authenticate first")
ErrAuthUnsupported = errors.New("Authentication not supported")
ErrAuthFailed = &SMTPError{
Code: 535,
EnhancedCode: EnhancedCode{5, 7, 8},
Message: "Authentication failed",
}
ErrAuthRequired = &SMTPError{
Code: 502,
EnhancedCode: EnhancedCode{5, 7, 0},
Message: "Please authenticate first",
}
ErrAuthUnsupported = &SMTPError{
Code: 502,
EnhancedCode: EnhancedCode{5, 7, 0},
Message: "Authentication not supported",
}
)
// A SMTP server backend.
type Backend interface {
// Authenticate a user. Return smtp.ErrAuthUnsupported if you don't want to
// support this.
Login(state *ConnectionState, username, password string) (Session, error)
// Called if the client attempts to send mail without logging in first.
// Return smtp.ErrAuthRequired if you don't want to support this.
AnonymousLogin(state *ConnectionState) (Session, error)
}
type BodyType string
const (
Body7Bit BodyType = "7BIT"
Body8BitMIME BodyType = "8BITMIME"
BodyBinaryMIME BodyType = "BINARYMIME"
)
// MailOptions contains custom arguments that were
// passed as an argument to the MAIL command.
type MailOptions struct {
// Value of BODY= argument, 7BIT, 8BITMIME or BINARYMIME.
Body BodyType
// Size of the body. Can be 0 if not specified by client.
Size int
// TLS is required for the message transmission.
//
// The message should be rejected if it can't be transmitted
// with TLS.
RequireTLS bool
// The message envelope or message header contains UTF-8-encoded strings.
// This flag is set by SMTPUTF8-aware (RFC 6531) client.
UTF8 bool
// The authorization identity asserted by the message sender in decoded
// form with angle brackets stripped.
//
// nil value indicates missing AUTH, non-nil empty string indicates
// AUTH=<>.
//
// Defined in RFC 4954.
Auth *string
NewSession(c *Conn) (Session, error)
}
// Session is used by servers to respond to an SMTP client.
@@ -68,17 +37,24 @@ type Session interface {
// Free all resources associated with session.
Logout() error
// Authenticate the user using SASL PLAIN.
AuthPlain(username, password string) error
// Set return path for currently processed message.
Mail(from string, opts MailOptions) error
Mail(from string, opts *MailOptions) error
// Add recipient for currently processed message.
Rcpt(to string) error
Rcpt(to string, opts *RcptOptions) error
// Set currently processed message contents and send it.
//
// r must be consumed before Data returns.
Data(r io.Reader) error
}
// LMTPSession is an add-on interface for Session. It can be implemented by
// LMTP servers to provide extra functionality.
type LMTPSession interface {
Session
// LMTPData is the LMTP-specific version of Data method.
// It can be optionally implemented by the backend to provide
// per-recipient status information when it is used over LMTP

View File

@@ -21,15 +21,10 @@ import (
// A Client represents a client connection to an SMTP server.
type Client struct {
// Text is the textproto.Conn used by the Client. It is exported to allow for
// clients to add extensions.
Text *textproto.Conn
// keep a reference to the connection so it can be used to create a TLS
// connection later
conn net.Conn
// whether the Client is using TLS
tls bool
conn net.Conn
text *textproto.Conn
serverName string
lmtp bool
// map of supported extensions
@@ -50,15 +45,24 @@ type Client struct {
DebugWriter io.Writer
}
// Dial returns a new Client connected to an SMTP server at addr.
// The addr must include a port, as in "mail.example.com:smtp".
// 30 seconds was chosen as it's the same duration as http.DefaultTransport's
// timeout.
const defaultTimeout = 30 * time.Second
var defaultDialer = net.Dialer{Timeout: defaultTimeout}
// Dial returns a new Client connected to an SMTP server at addr. The addr must
// include a port, as in "mail.example.com:smtp".
//
// This function returns a plaintext connection. To enable TLS, use StartTLS.
func Dial(addr string) (*Client, error) {
conn, err := net.Dial("tcp", addr)
conn, err := defaultDialer.Dial("tcp", addr)
if err != nil {
return nil, err
}
host, _, _ := net.SplitHostPort(addr)
return NewClient(conn, host)
client := NewClient(conn)
client.serverName, _, _ = net.SplitHostPort(addr)
return client, nil
}
// DialTLS returns a new Client connected to an SMTP server via TLS at addr.
@@ -66,20 +70,24 @@ func Dial(addr string) (*Client, error) {
//
// A nil tlsConfig is equivalent to a zero tls.Config.
func DialTLS(addr string, tlsConfig *tls.Config) (*Client, error) {
conn, err := tls.Dial("tcp", addr, tlsConfig)
tlsDialer := tls.Dialer{
NetDialer: &defaultDialer,
Config: tlsConfig,
}
conn, err := tlsDialer.Dial("tcp", addr)
if err != nil {
return nil, err
}
host, _, _ := net.SplitHostPort(addr)
return NewClient(conn, host)
client := NewClient(conn)
client.serverName, _, _ = net.SplitHostPort(addr)
return client, nil
}
// NewClient returns a new Client using an existing connection and host as a
// server name to be used when authenticating.
func NewClient(conn net.Conn, host string) (*Client, error) {
func NewClient(conn net.Conn) *Client {
c := &Client{
serverName: host,
localName: "localhost",
localName: "localhost",
// As recommended by RFC 5321. For DATA command reply (3xx one) RFC
// recommends a slightly shorter timeout but we do not bother
// differentiating these.
@@ -91,27 +99,15 @@ func NewClient(conn net.Conn, host string) (*Client, error) {
c.setConn(conn)
_, _, err := c.Text.ReadResponse(220)
if err != nil {
c.Text.Close()
if protoErr, ok := err.(*textproto.Error); ok {
return nil, toSMTPErr(protoErr)
}
return nil, err
}
return c, nil
return c
}
// NewClientLMTP returns a new LMTP Client (as defined in RFC 2033) using an
// existing connector and host as a server name to be used when authenticating.
func NewClientLMTP(conn net.Conn, host string) (*Client, error) {
c, err := NewClient(conn, host)
if err != nil {
return nil, err
}
// existing connection and host as a server name to be used when authenticating.
func NewClientLMTP(conn net.Conn) *Client {
c := NewClient(conn)
c.lmtp = true
return c, nil
return c
}
// setConn sets the underlying network connection for the client.
@@ -139,23 +135,38 @@ func (c *Client) setConn(conn net.Conn) {
Writer: w,
Closer: conn,
}
c.Text = textproto.NewConn(rwc)
_, isTLS := conn.(*tls.Conn)
c.tls = isTLS
c.text = textproto.NewConn(rwc)
}
// Close closes the connection.
func (c *Client) Close() error {
return c.Text.Close()
return c.text.Close()
}
func (c *Client) greet() error {
// Initial greeting timeout. RFC 5321 recommends 5 minutes.
c.conn.SetDeadline(time.Now().Add(c.CommandTimeout))
defer c.conn.SetDeadline(time.Time{})
_, _, err := c.text.ReadResponse(220)
if err != nil {
c.text.Close()
if protoErr, ok := err.(*textproto.Error); ok {
return toSMTPErr(protoErr)
}
return err
}
return nil
}
// hello runs a hello exchange if needed.
func (c *Client) hello() error {
if !c.didHello {
c.didHello = true
err := c.ehlo()
if err != nil {
if err := c.greet(); err != nil {
c.helloError = err
} else if err := c.ehlo(); err != nil {
c.helloError = c.helo()
}
}
@@ -181,18 +192,18 @@ func (c *Client) Hello(localName string) error {
}
// cmd is a convenience function that sends a command and returns the response
// textproto.Error returned by c.Text.ReadResponse is converted into SMTPError.
// textproto.Error returned by c.text.ReadResponse is converted into SMTPError.
func (c *Client) cmd(expectCode int, format string, args ...interface{}) (int, string, error) {
c.conn.SetDeadline(time.Now().Add(c.CommandTimeout))
defer c.conn.SetDeadline(time.Time{})
id, err := c.Text.Cmd(format, args...)
id, err := c.text.Cmd(format, args...)
if err != nil {
return 0, "", err
}
c.Text.StartResponse(id)
defer c.Text.EndResponse(id)
code, msg, err := c.Text.ReadResponse(expectCode)
c.text.StartResponse(id)
defer c.text.EndResponse(id)
code, msg, err := c.text.ReadResponse(expectCode)
if err != nil {
if protoErr, ok := err.(*textproto.Error); ok {
smtpErr := toSMTPErr(protoErr)
@@ -260,7 +271,7 @@ func (c *Client) StartTLS(config *tls.Config) error {
if config == nil {
config = &tls.Config{}
}
if config.ServerName == "" {
if config.ServerName == "" && c.serverName != "" {
// Make a copy to avoid polluting argument
config = config.Clone()
config.ServerName = c.serverName
@@ -365,34 +376,54 @@ func (c *Client) Mail(from string, opts *MailOptions) error {
if err := c.hello(); err != nil {
return err
}
cmdStr := "MAIL FROM:<%s>"
var sb strings.Builder
// A high enough power of 2 than 510+14+26+11+9+9+39+500
sb.Grow(2048)
fmt.Fprintf(&sb, "MAIL FROM:<%s>", from)
if _, ok := c.ext["8BITMIME"]; ok {
cmdStr += " BODY=8BITMIME"
sb.WriteString(" BODY=8BITMIME")
}
if _, ok := c.ext["SIZE"]; ok && opts != nil && opts.Size != 0 {
cmdStr += " SIZE=" + strconv.Itoa(opts.Size)
fmt.Fprintf(&sb, " SIZE=%v", opts.Size)
}
if opts != nil && opts.RequireTLS {
if _, ok := c.ext["REQUIRETLS"]; ok {
cmdStr += " REQUIRETLS"
sb.WriteString(" REQUIRETLS")
} else {
return errors.New("smtp: server does not support REQUIRETLS")
}
}
if opts != nil && opts.UTF8 {
if _, ok := c.ext["SMTPUTF8"]; ok {
cmdStr += " SMTPUTF8"
sb.WriteString(" SMTPUTF8")
} else {
return errors.New("smtp: server does not support SMTPUTF8")
}
}
if _, ok := c.ext["DSN"]; ok && opts != nil {
switch opts.Return {
case DSNReturnFull, DSNReturnHeaders:
fmt.Fprintf(&sb, " RET=%s", string(opts.Return))
case "":
// This space is intentionally left blank
default:
return errors.New("smtp: Unknown RET parameter value")
}
if opts.EnvelopeID != "" {
if !isPrintableASCII(opts.EnvelopeID) {
return errors.New("smtp: Malformed ENVID parameter value")
}
fmt.Fprintf(&sb, " ENVID=%s", encodeXtext(opts.EnvelopeID))
}
}
if opts != nil && opts.Auth != nil {
if _, ok := c.ext["AUTH"]; ok {
cmdStr += " AUTH=" + encodeXtext(*opts.Auth)
fmt.Fprintf(&sb, " AUTH=%s", encodeXtext(*opts.Auth))
}
// We can safely discard parameter if server does not support AUTH.
}
_, _, err := c.cmd(250, cmdStr, from)
_, _, err := c.cmd(250, "%s", sb.String())
return err
}
@@ -400,12 +431,53 @@ func (c *Client) Mail(from string, opts *MailOptions) error {
// A call to Rcpt must be preceded by a call to Mail and may be followed by
// a Data call or another Rcpt call.
//
// If opts is not nil, RCPT arguments provided in the structure will be added
// to the command. Handling of unsupported options depends on the extension.
//
// If server returns an error, it will be of type *SMTPError.
func (c *Client) Rcpt(to string) error {
func (c *Client) Rcpt(to string, opts *RcptOptions) error {
if err := validateLine(to); err != nil {
return err
}
if _, _, err := c.cmd(25, "RCPT TO:<%s>", to); err != nil {
var sb strings.Builder
// A high enough power of 2 than 510+29+501
sb.Grow(2048)
fmt.Fprintf(&sb, "RCPT TO:<%s>", to)
if _, ok := c.ext["DSN"]; ok && opts != nil {
if opts.Notify != nil && len(opts.Notify) != 0 {
sb.WriteString(" NOTIFY=")
if err := checkNotifySet(opts.Notify); err != nil {
return errors.New("smtp: Malformed NOTIFY parameter value")
}
for i, v := range opts.Notify {
if i != 0 {
sb.WriteString(",")
}
sb.WriteString(string(v))
}
}
if opts.OriginalRecipient != "" {
var enc string
switch opts.OriginalRecipientType {
case DSNAddressTypeRFC822:
if !isPrintableASCII(opts.OriginalRecipient) {
return errors.New("smtp: Illegal address")
}
enc = encodeXtext(opts.OriginalRecipient)
case DSNAddressTypeUTF8:
if _, ok := c.ext["SMTPUTF8"]; ok {
enc = encodeUTF8AddrUnitext(opts.OriginalRecipient)
} else {
enc = encodeUTF8AddrXtext(opts.OriginalRecipient)
}
default:
return errors.New("smtp: Unknown address type")
}
fmt.Fprintf(&sb, " ORCPT=%s;%s", string(opts.OriginalRecipientType), enc)
}
}
if _, _, err := c.cmd(25, "%s", sb.String()); err != nil {
return err
}
c.rcpts = append(c.rcpts, to)
@@ -416,10 +488,17 @@ type dataCloser struct {
c *Client
io.WriteCloser
statusCb func(rcpt string, status *SMTPError)
closed bool
}
func (d *dataCloser) Close() error {
d.WriteCloser.Close()
if d.closed {
return fmt.Errorf("smtp: data writer closed twice")
}
if err := d.WriteCloser.Close(); err != nil {
return err
}
d.c.conn.SetDeadline(time.Now().Add(d.c.SubmissionTimeout))
defer d.c.conn.SetDeadline(time.Time{})
@@ -428,7 +507,7 @@ func (d *dataCloser) Close() error {
if d.c.lmtp {
for expectedResponses > 0 {
rcpt := d.c.rcpts[len(d.c.rcpts)-expectedResponses]
if _, _, err := d.c.Text.ReadResponse(250); err != nil {
if _, _, err := d.c.text.ReadResponse(250); err != nil {
if protoErr, ok := err.(*textproto.Error); ok {
if d.statusCb != nil {
d.statusCb(rcpt, toSMTPErr(protoErr))
@@ -441,17 +520,18 @@ func (d *dataCloser) Close() error {
}
expectedResponses--
}
return nil
} else {
_, _, err := d.c.Text.ReadResponse(250)
_, _, err := d.c.text.ReadResponse(250)
if err != nil {
if protoErr, ok := err.(*textproto.Error); ok {
return toSMTPErr(protoErr)
}
return err
}
return nil
}
d.closed = true
return nil
}
// Data issues a DATA command to the server and returns a writer that
@@ -465,7 +545,7 @@ func (c *Client) Data() (io.WriteCloser, error) {
if err != nil {
return nil, err
}
return &dataCloser{c, c.Text.DotWriter(), nil}, nil
return &dataCloser{c: c, WriteCloser: c.text.DotWriter()}, nil
}
// LMTPData is the LMTP-specific version of the Data method. It accepts a callback
@@ -485,16 +565,51 @@ func (c *Client) LMTPData(statusCb func(rcpt string, status *SMTPError)) (io.Wri
if err != nil {
return nil, err
}
return &dataCloser{c, c.Text.DotWriter(), statusCb}, nil
return &dataCloser{c: c, WriteCloser: c.text.DotWriter(), statusCb: statusCb}, nil
}
// SendMail will use an existing connection to send an email from
// address from, to addresses to, with message r.
//
// This function does not start TLS, nor does it perform authentication. Use
// StartTLS and Auth before-hand if desirable.
//
// The addresses in the to parameter are the SMTP RCPT addresses.
//
// The r parameter should be an RFC 822-style email with headers
// first, a blank line, and then the message body. The lines of r
// should be CRLF terminated. The r headers should usually include
// fields such as "From", "To", "Subject", and "Cc". Sending "Bcc"
// messages is accomplished by including an email address in the to
// parameter but not including it in the r headers.
func (c *Client) SendMail(from string, to []string, r io.Reader) error {
var err error
if err = c.Mail(from, nil); err != nil {
return err
}
for _, addr := range to {
if err = c.Rcpt(addr, nil); err != nil {
return err
}
}
w, err := c.Data()
if err != nil {
return err
}
_, err = io.Copy(w, r)
if err != nil {
return err
}
return w.Close()
}
var testHookStartTLS func(*tls.Config) // nil, except for tests
// SendMail connects to the server at addr, switches to TLS if
// possible, authenticates with the optional mechanism a if possible,
// and then sends an email from address from, to addresses to, with
// message r.
// The addr must include a port, as in "mail.example.com:smtp".
// SendMail connects to the server at addr, switches to TLS, authenticates with
// the optional SASL client, and then sends an email from address from, to
// addresses to, with message r. The addr must include a port, as in
// "mail.example.com:smtp".
//
// The addresses in the to parameter are the SMTP RCPT addresses.
//
@@ -521,45 +636,65 @@ func SendMail(addr string, a sasl.Client, from string, to []string, r io.Reader)
return err
}
}
c, err := Dial(addr)
if err != nil {
return err
}
defer c.Close()
if err = c.hello(); err != nil {
return err
}
if ok, _ := c.Extension("STARTTLS"); ok {
if err = c.StartTLS(nil); err != nil {
return err
}
if ok, _ := c.Extension("STARTTLS"); !ok {
return errors.New("smtp: server doesn't support STARTTLS")
}
if a != nil && c.ext != nil {
if _, ok := c.ext["AUTH"]; !ok {
if err = c.StartTLS(nil); err != nil {
return err
}
if a != nil {
if ok, _ := c.Extension("AUTH"); !ok {
return errors.New("smtp: server doesn't support AUTH")
}
if err = c.Auth(a); err != nil {
return err
}
}
if err = c.Mail(from, nil); err != nil {
if err := c.SendMail(from, to, r); err != nil {
return err
}
for _, addr := range to {
if err = c.Rcpt(addr); err != nil {
return c.Quit()
}
// SendMailTLS works like SendMail, but with implicit TLS.
func SendMailTLS(addr string, a sasl.Client, from string, to []string, r io.Reader) error {
if err := validateLine(from); err != nil {
return err
}
for _, recp := range to {
if err := validateLine(recp); err != nil {
return err
}
}
w, err := c.Data()
c, err := DialTLS(addr, nil)
if err != nil {
return err
}
_, err = io.Copy(w, r)
if err != nil {
defer c.Close()
if err = c.hello(); err != nil {
return err
}
err = w.Close()
if err != nil {
if a != nil {
if ok, _ := c.Extension("AUTH"); !ok {
return errors.New("smtp: server doesn't support AUTH")
}
if err = c.Auth(a); err != nil {
return err
}
}
if err := c.SendMail(from, to, r); err != nil {
return err
}
return c.Quit()
@@ -616,7 +751,7 @@ func (c *Client) Quit() error {
if err != nil {
return err
}
return c.Text.Close()
return c.Close()
}
func parseEnhancedCode(s string) (EnhancedCode, error) {
@@ -639,9 +774,6 @@ func parseEnhancedCode(s string) (EnhancedCode, error) {
// toSMTPErr converts textproto.Error into SMTPError, parsing
// enhanced status code if it is present.
func toSMTPErr(protoErr *textproto.Error) *SMTPError {
if protoErr == nil {
return nil
}
smtpErr := &SMTPError{
Code: protoErr.Code,
Message: protoErr.Msg,
@@ -677,3 +809,11 @@ func (cdw clientDebugWriter) Write(b []byte) (int, error) {
}
return cdw.c.DebugWriter.Write(b)
}
// validateLine checks to see if a line has CR or LF.
func validateLine(line string) error {
if strings.ContainsAny(line, "\n\r") {
return errors.New("smtp: a line must not contain CR or LF")
}
return nil
}

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,7 @@ package smtp
import (
"bufio"
"fmt"
"io"
)
@@ -29,7 +30,11 @@ var NoEnhancedCode = EnhancedCode{-1, -1, -1}
var EnhancedCodeNotSet = EnhancedCode{0, 0, 0}
func (err *SMTPError) Error() string {
return err.Message
s := fmt.Sprintf("SMTP error %03d", err.Code)
if err.Message != "" {
s += ": " + err.Message
}
return s
}
func (err *SMTPError) Temporary() bool {
@@ -77,7 +82,7 @@ func (r *dataReader) Read(b []byte) (n int, err error) {
// not rewrite CRLF -> LF.
// Run data through a simple state machine to
// elide leading dots and detect ending .\r\n line.
// elide leading dots and detect End-of-Data (<CR><LF>.<CR><LF>) line.
const (
stateBeginLine = iota // beginning of line; initial state; must be zero
stateDot // read . at beginning of line
@@ -101,17 +106,16 @@ func (r *dataReader) Read(b []byte) (n int, err error) {
r.state = stateDot
continue
}
if c == '\r' {
r.state = stateCR
break
}
r.state = stateData
case stateDot:
if c == '\r' {
r.state = stateDotCR
continue
}
if c == '\n' {
r.state = stateEOF
continue
}
r.state = stateData
case stateDotCR:
if c == '\n' {
@@ -129,9 +133,6 @@ func (r *dataReader) Read(b []byte) (n int, err error) {
if c == '\r' {
r.state = stateCR
}
if c == '\n' {
r.state = stateBeginLine
}
}
b[n] = c
n++

View File

@@ -5,7 +5,7 @@ import (
"io"
)
var ErrTooLongLine = errors.New("smtp: too longer line in input stream")
var ErrTooLongLine = errors.New("smtp: too long a line in input stream")
// lineLimitReader reads from the underlying Reader but restricts
// line length of lines in input stream to a certain length.

View File

@@ -5,6 +5,14 @@ import (
"strings"
)
// cutPrefixFold is a version of strings.CutPrefix which is case-insensitive.
func cutPrefixFold(s, prefix string) (string, bool) {
if len(s) < len(prefix) || !strings.EqualFold(s[:len(prefix)], prefix) {
return "", false
}
return s[len(prefix):], true
}
func parseCmd(line string) (cmd string, arg string, err error) {
line = strings.TrimRight(line, "\r\n")
@@ -15,36 +23,33 @@ func parseCmd(line string) (cmd string, arg string, err error) {
case l == 0:
return "", "", nil
case l < 4:
return "", "", fmt.Errorf("Command too short: %q", line)
return "", "", fmt.Errorf("command too short: %q", line)
case l == 4:
return strings.ToUpper(line), "", nil
case l == 5:
// Too long to be only command, too short to have args
return "", "", fmt.Errorf("Mangled command: %q", line)
return "", "", fmt.Errorf("mangled command: %q", line)
}
// If we made it here, command is long enough to have args
if line[4] != ' ' {
// There wasn't a space after the command?
return "", "", fmt.Errorf("Mangled command: %q", line)
return "", "", fmt.Errorf("mangled command: %q", line)
}
// I'm not sure if we should trim the args or not, but we will for now
//return strings.ToUpper(line[0:4]), strings.Trim(line[5:], " "), nil
return strings.ToUpper(line[0:4]), strings.Trim(line[5:], " \n\r"), nil
return strings.ToUpper(line[0:4]), strings.TrimSpace(line[5:]), nil
}
// Takes the arguments proceeding a command and files them
// into a map[string]string after uppercasing each key. Sample arg
// string:
// " BODY=8BITMIME SIZE=1024 SMTPUTF8"
//
// " BODY=8BITMIME SIZE=1024 SMTPUTF8"
//
// The leading space is mandatory.
func parseArgs(args []string) (map[string]string, error) {
func parseArgs(s string) (map[string]string, error) {
argMap := map[string]string{}
for _, arg := range args {
if arg == "" {
continue
}
for _, arg := range strings.Fields(s) {
m := strings.Split(arg, "=")
switch len(m) {
case 2:
@@ -52,7 +57,7 @@ func parseArgs(args []string) (map[string]string, error) {
case 1:
argMap[strings.ToUpper(m[0])] = ""
default:
return nil, fmt.Errorf("Failed to parse arg string: %q", arg)
return nil, fmt.Errorf("failed to parse arg string: %q", arg)
}
}
return argMap, nil
@@ -64,7 +69,146 @@ func parseHelloArgument(arg string) (string, error) {
domain = arg[:idx]
}
if domain == "" {
return "", fmt.Errorf("Invalid domain")
return "", fmt.Errorf("invalid domain")
}
return domain, nil
}
// parser parses command arguments defined in RFC 5321 section 4.1.2.
type parser struct {
s string
}
func (p *parser) peekByte() (byte, bool) {
if len(p.s) == 0 {
return 0, false
}
return p.s[0], true
}
func (p *parser) readByte() (byte, bool) {
ch, ok := p.peekByte()
if ok {
p.s = p.s[1:]
}
return ch, ok
}
func (p *parser) acceptByte(ch byte) bool {
got, ok := p.peekByte()
if !ok || got != ch {
return false
}
p.readByte()
return true
}
func (p *parser) expectByte(ch byte) error {
if !p.acceptByte(ch) {
if len(p.s) == 0 {
return fmt.Errorf("expected '%v', got EOF", string(ch))
} else {
return fmt.Errorf("expected '%v', got '%v'", string(ch), string(p.s[0]))
}
}
return nil
}
func (p *parser) parseReversePath() (string, error) {
if strings.HasPrefix(p.s, "<>") {
p.s = strings.TrimPrefix(p.s, "<>")
return "", nil
}
return p.parsePath()
}
func (p *parser) parsePath() (string, error) {
hasBracket := p.acceptByte('<')
if p.acceptByte('@') {
i := strings.IndexByte(p.s, ':')
if i < 0 {
return "", fmt.Errorf("malformed a-d-l")
}
p.s = p.s[i+1:]
}
mbox, err := p.parseMailbox()
if err != nil {
return "", fmt.Errorf("in mailbox: %v", err)
}
if hasBracket {
if err := p.expectByte('>'); err != nil {
return "", err
}
}
return mbox, nil
}
func (p *parser) parseMailbox() (string, error) {
localPart, err := p.parseLocalPart()
if err != nil {
return "", fmt.Errorf("in local-part: %v", err)
} else if localPart == "" {
return "", fmt.Errorf("local-part is empty")
}
if err := p.expectByte('@'); err != nil {
return "", err
}
var sb strings.Builder
sb.WriteString(localPart)
sb.WriteByte('@')
for {
ch, ok := p.peekByte()
if !ok {
break
}
if ch == ' ' || ch == '\t' || ch == '>' {
break
}
p.readByte()
sb.WriteByte(ch)
}
if strings.HasSuffix(sb.String(), "@") {
return "", fmt.Errorf("domain is empty")
}
return sb.String(), nil
}
func (p *parser) parseLocalPart() (string, error) {
var sb strings.Builder
if p.acceptByte('"') { // quoted-string
for {
ch, ok := p.readByte()
switch ch {
case '\\':
ch, ok = p.readByte()
case '"':
return sb.String(), nil
}
if !ok {
return "", fmt.Errorf("malformed quoted-string")
}
sb.WriteByte(ch)
}
} else { // dot-string
for {
ch, ok := p.peekByte()
if !ok {
return sb.String(), nil
}
switch ch {
case '@':
return sb.String(), nil
case '(', ')', '<', '>', '[', ']', ':', ';', '\\', ',', '"', ' ', '\t':
return "", fmt.Errorf("malformed dot-string")
}
p.readByte()
sb.WriteByte(ch)
}
}
}

View File

@@ -1,6 +1,7 @@
package smtp
import (
"context"
"crypto/tls"
"errors"
"io"
@@ -13,7 +14,9 @@ import (
"github.com/emersion/go-sasl"
)
var errTCPAndLMTP = errors.New("smtp: cannot start LMTP server listening on a TCP socket")
var (
ErrServerClosed = errors.New("smtp: server already closed")
)
// A function that creates SASL servers.
type SaslServerFactory func(conn *Conn) sasl.Server
@@ -26,20 +29,20 @@ type Logger interface {
// A SMTP server.
type Server struct {
// The type of network, "tcp" or "unix".
Network string
// TCP or Unix address to listen on.
Addr string
// The server TLS configuration.
TLSConfig *tls.Config
// Enable LMTP mode, as defined in RFC 2033. LMTP mode cannot be used with a
// TCP listener.
// Enable LMTP mode, as defined in RFC 2033.
LMTP bool
Domain string
MaxRecipients int
MaxMessageBytes int
MaxMessageBytes int64
MaxLineLength int
AllowInsecureAuth bool
Strict bool
Debug io.Writer
ErrorLog Logger
ReadTimeout time.Duration
@@ -57,6 +60,10 @@ type Server struct {
// Should be used only if backend supports it.
EnableBINARYMIME bool
// Advertise DSN (RFC 3461) capability.
// Should be used only if backend supports it.
EnableDSN bool
// If set, the AUTH command will not be advertised and authentication
// attempts will be rejected. This setting overrides AllowInsecureAuth.
AuthDisabled bool
@@ -64,6 +71,8 @@ type Server struct {
// The server backend.
Backend Backend
wg sync.WaitGroup
caps []string
auths map[string]SaslServerFactory
done chan struct{}
@@ -87,17 +96,15 @@ func NewServer(be Backend) *Server {
sasl.Plain: func(conn *Conn) sasl.Server {
return sasl.NewPlainServer(func(identity, username, password string) error {
if identity != "" && identity != username {
return errors.New("Identities not supported")
return errors.New("identities not supported")
}
state := conn.State()
session, err := be.Login(&state, username, password)
if err != nil {
return err
sess := conn.Session()
if sess == nil {
panic("No session when AUTH is called")
}
conn.SetSession(session)
return nil
return sess.AuthPlain(username, password)
})
},
},
@@ -111,6 +118,8 @@ func (s *Server) Serve(l net.Listener) error {
s.listeners = append(s.listeners, l)
s.locker.Unlock()
var tempDelay time.Duration // how long to sleep on accept failure
for {
c, err := l.Accept()
if err != nil {
@@ -119,11 +128,32 @@ func (s *Server) Serve(l net.Listener) error {
// we called Close()
return nil
default:
return err
}
if ne, ok := err.(net.Error); ok && ne.Temporary() {
if tempDelay == 0 {
tempDelay = 5 * time.Millisecond
} else {
tempDelay *= 2
}
if max := 1 * time.Second; tempDelay > max {
tempDelay = max
}
s.ErrorLog.Printf("accept error: %s; retrying in %s", err, tempDelay)
time.Sleep(tempDelay)
continue
}
return err
}
go s.handleConn(newConn(c, s))
s.wg.Add(1)
go func() {
defer s.wg.Done()
err := s.handleConn(newConn(c, s))
if err != nil {
s.ErrorLog.Printf("handler error: %s", err)
}
}()
}
}
@@ -140,10 +170,22 @@ func (s *Server) handleConn(c *Conn) error {
s.locker.Unlock()
}()
if tlsConn, ok := c.conn.(*tls.Conn); ok {
if d := s.ReadTimeout; d != 0 {
c.conn.SetReadDeadline(time.Now().Add(d))
}
if d := s.WriteTimeout; d != 0 {
c.conn.SetWriteDeadline(time.Now().Add(d))
}
if err := tlsConn.Handshake(); err != nil {
return err
}
}
c.greet()
for {
line, err := c.ReadLine()
line, err := c.readLine()
if err == nil {
cmd, arg, err := parseCmd(line)
if err != nil {
@@ -153,34 +195,41 @@ func (s *Server) handleConn(c *Conn) error {
c.handle(cmd, arg)
} else {
if err == io.EOF {
if err == io.EOF || errors.Is(err, net.ErrClosed) {
return nil
}
if err == ErrTooLongLine {
c.WriteResponse(500, EnhancedCode{5, 4, 0}, "Too long line, closing connection")
c.writeResponse(500, EnhancedCode{5, 4, 0}, "Too long line, closing connection")
return nil
}
if neterr, ok := err.(net.Error); ok && neterr.Timeout() {
c.WriteResponse(221, EnhancedCode{2, 4, 2}, "Idle timeout, bye bye")
c.writeResponse(421, EnhancedCode{4, 4, 2}, "Idle timeout, bye bye")
return nil
}
c.WriteResponse(221, EnhancedCode{2, 4, 0}, "Connection error, sorry")
c.writeResponse(421, EnhancedCode{4, 4, 0}, "Connection error, sorry")
return err
}
}
}
func (s *Server) network() string {
if s.Network != "" {
return s.Network
}
if s.LMTP {
return "unix"
}
return "tcp"
}
// ListenAndServe listens on the network address s.Addr and then calls Serve
// to handle requests on incoming connections.
//
// If s.Addr is blank and LMTP is disabled, ":smtp" is used.
func (s *Server) ListenAndServe() error {
network := "tcp"
if s.LMTP {
network = "unix"
}
network := s.network()
addr := s.Addr
if !s.LMTP && addr == "" {
@@ -198,18 +247,16 @@ func (s *Server) ListenAndServe() error {
// ListenAndServeTLS listens on the TCP network address s.Addr and then calls
// Serve to handle requests on incoming TLS connections.
//
// If s.Addr is blank, ":smtps" is used.
// If s.Addr is blank and LMTP is disabled, ":smtps" is used.
func (s *Server) ListenAndServeTLS() error {
if s.LMTP {
return errTCPAndLMTP
}
network := s.network()
addr := s.Addr
if addr == "" {
if !s.LMTP && addr == "" {
addr = ":smtps"
}
l, err := tls.Listen("tcp", addr, s.TLSConfig)
l, err := tls.Listen(network, addr, s.TLSConfig)
if err != nil {
return err
}
@@ -224,19 +271,19 @@ func (s *Server) ListenAndServeTLS() error {
func (s *Server) Close() error {
select {
case <-s.done:
return errors.New("smtp: server already closed")
return ErrServerClosed
default:
close(s.done)
}
var err error
s.locker.Lock()
for _, l := range s.listeners {
if lerr := l.Close(); lerr != nil && err == nil {
err = lerr
}
}
s.locker.Lock()
for conn := range s.conns {
conn.Close()
}
@@ -245,6 +292,44 @@ func (s *Server) Close() error {
return err
}
// Shutdown gracefully shuts down the server without interrupting any
// active connections. Shutdown works by first closing all open
// listeners and then waiting indefinitely for connections to return to
// idle and then shut down.
// If the provided context expires before the shutdown is complete,
// Shutdown returns the context's error, otherwise it returns any
// error returned from closing the Server's underlying Listener(s).
func (s *Server) Shutdown(ctx context.Context) error {
select {
case <-s.done:
return ErrServerClosed
default:
close(s.done)
}
var err error
s.locker.Lock()
for _, l := range s.listeners {
if lerr := l.Close(); lerr != nil && err == nil {
err = lerr
}
}
s.locker.Unlock()
connDone := make(chan struct{})
go func() {
defer close(connDone)
s.wg.Wait()
}()
select {
case <-ctx.Done():
return ctx.Err()
case <-connDone:
return err
}
}
// EnableAuth enables an authentication mechanism on this server.
//
// This function should not be called directly, it must only be used by
@@ -252,12 +337,3 @@ func (s *Server) Close() error {
func (s *Server) EnableAuth(name string, f SaslServerFactory) {
s.auths[name] = f
}
// ForEachConn iterates through all opened connections.
func (s *Server) ForEachConn(f func(*Conn)) {
s.locker.Lock()
defer s.locker.Unlock()
for conn := range s.conns {
f(conn)
}
}

View File

@@ -2,29 +2,93 @@
//
// It also implements the following extensions:
//
// 8BITMIME: RFC 1652
// AUTH: RFC 2554
// STARTTLS: RFC 3207
// ENHANCEDSTATUSCODES: RFC 2034
// SMTPUTF8: RFC 6531
// REQUIRETLS: RFC 8689
// CHUNKING: RFC 3030
// BINARYMIME: RFC 3030
// - 8BITMIME (RFC 1652)
// - AUTH (RFC 2554)
// - STARTTLS (RFC 3207)
// - ENHANCEDSTATUSCODES (RFC 2034)
// - SMTPUTF8 (RFC 6531)
// - REQUIRETLS (RFC 8689)
// - CHUNKING (RFC 3030)
// - BINARYMIME (RFC 3030)
// - DSN (RFC 3461, RFC 6533)
//
// LMTP (RFC 2033) is also supported.
//
// Additional extensions may be handled by other packages.
package smtp
import (
"errors"
"strings"
type BodyType string
const (
Body7Bit BodyType = "7BIT"
Body8BitMIME BodyType = "8BITMIME"
BodyBinaryMIME BodyType = "BINARYMIME"
)
// validateLine checks to see if a line has CR or LF as per RFC 5321
func validateLine(line string) error {
if strings.ContainsAny(line, "\n\r") {
return errors.New("smtp: A line must not contain CR or LF")
}
return nil
type DSNReturn string
const (
DSNReturnFull DSNReturn = "FULL"
DSNReturnHeaders DSNReturn = "HDRS"
)
// MailOptions contains parameters for the MAIL command.
type MailOptions struct {
// Value of BODY= argument, 7BIT, 8BITMIME or BINARYMIME.
Body BodyType
// Size of the body. Can be 0 if not specified by client.
Size int64
// TLS is required for the message transmission.
//
// The message should be rejected if it can't be transmitted
// with TLS.
RequireTLS bool
// The message envelope or message header contains UTF-8-encoded strings.
// This flag is set by SMTPUTF8-aware (RFC 6531) client.
UTF8 bool
// Value of RET= argument, FULL or HDRS.
Return DSNReturn
// Envelope identifier set by the client.
EnvelopeID string
// The authorization identity asserted by the message sender in decoded
// form with angle brackets stripped.
//
// nil value indicates missing AUTH, non-nil empty string indicates
// AUTH=<>.
//
// Defined in RFC 4954.
Auth *string
}
type DSNNotify string
const (
DSNNotifyNever DSNNotify = "NEVER"
DSNNotifyDelayed DSNNotify = "DELAY"
DSNNotifyFailure DSNNotify = "FAILURE"
DSNNotifySuccess DSNNotify = "SUCCESS"
)
type DSNAddressType string
const (
DSNAddressTypeRFC822 DSNAddressType = "RFC822"
DSNAddressTypeUTF8 DSNAddressType = "UTF-8"
)
// RcptOptions contains parameters for the RCPT command.
type RcptOptions struct {
// Value of NOTIFY= argument, NEVER or a combination of either of
// DELAY, FAILURE, SUCCESS.
Notify []DSNNotify
// Original recipient set by client.
OriginalRecipientType DSNAddressType
OriginalRecipient string
}