updated deps; updated healthchecks.io integration

This commit is contained in:
Aine
2024-04-07 14:42:12 +03:00
parent 271a4a0e31
commit 15d61f174e
122 changed files with 3432 additions and 4613 deletions

View File

@@ -2,6 +2,8 @@ package smtp
import (
"io"
"github.com/emersion/go-sasl"
)
var (
@@ -20,6 +22,11 @@ var (
EnhancedCode: EnhancedCode{5, 7, 0},
Message: "Authentication not supported",
}
ErrAuthUnknownMechanism = &SMTPError{
Code: 504,
EnhancedCode: EnhancedCode{5, 7, 4},
Message: "Unsupported authentication mechanism",
}
)
// A SMTP server backend.
@@ -27,6 +34,17 @@ type Backend interface {
NewSession(c *Conn) (Session, error)
}
// BackendFunc is an adapter to allow the use of an ordinary function as a
// Backend.
type BackendFunc func(c *Conn) (Session, error)
var _ Backend = (BackendFunc)(nil)
// NewSession calls f(c).
func (f BackendFunc) NewSession(c *Conn) (Session, error) {
return f(c)
}
// Session is used by servers to respond to an SMTP client.
//
// The methods are called when the remote client issues the matching command.
@@ -37,9 +55,6 @@ 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
// Add recipient for currently processed message.
@@ -76,3 +91,12 @@ type LMTPSession interface {
type StatusCollector interface {
SetStatus(rcptTo string, err error)
}
// AuthSession is an add-on interface for Session. It provides support for the
// AUTH extension.
type AuthSession interface {
Session
AuthMechanisms() []string
Auth(mech string) (sasl.Server, error)
}

View File

@@ -27,14 +27,11 @@ type Client struct {
text *textproto.Conn
serverName string
lmtp bool
// map of supported extensions
ext map[string]string
// supported auth mechanisms
auth []string
localName string // the name to use in HELO/EHLO/LHLO
didHello bool // whether we've said HELO/EHLO/LHLO
helloError error // the error from the hello
rcpts []string // recipients accumulated for the current session
ext map[string]string // supported extensions
localName string // the name to use in HELO/EHLO/LHLO
didHello bool // whether we've said HELO/EHLO/LHLO
helloError error // the error from the hello
rcpts []string // recipients accumulated for the current session
// Time to wait for command responses (this includes 3xx reply to DATA).
CommandTimeout time.Duration
@@ -54,7 +51,8 @@ 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.
// This function returns a plaintext connection. To enable TLS, use
// DialStartTLS.
func Dial(addr string) (*Client, error) {
conn, err := defaultDialer.Dial("tcp", addr)
if err != nil {
@@ -83,6 +81,22 @@ func DialTLS(addr string, tlsConfig *tls.Config) (*Client, error) {
return client, nil
}
// DialStartTLS retruns a new Client connected to an SMTP server via STARTTLS
// at addr. The addr must include a port, as in "mail.example.com:smtp".
//
// A nil tlsConfig is equivalent to a zero tls.Config.
func DialStartTLS(addr string, tlsConfig *tls.Config) (*Client, error) {
c, err := Dial(addr)
if err != nil {
return nil, err
}
if err := initStartTLS(c, tlsConfig); err != nil {
c.Close()
return nil, err
}
return c, 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) *Client {
@@ -102,6 +116,29 @@ func NewClient(conn net.Conn) *Client {
return c
}
// NewClientStartTLS creates a new Client and performs a STARTTLS command.
func NewClientStartTLS(conn net.Conn, tlsConfig *tls.Config) (*Client, error) {
c := NewClient(conn)
if err := initStartTLS(c, tlsConfig); err != nil {
c.Close()
return nil, err
}
return c, nil
}
func initStartTLS(c *Client, tlsConfig *tls.Config) error {
if err := c.hello(); err != nil {
return err
}
if ok, _ := c.Extension("STARTTLS"); !ok {
return errors.New("smtp: server doesn't support STARTTLS")
}
if err := c.startTLS(tlsConfig); err != nil {
return err
}
return nil
}
// NewClientLMTP returns a new LMTP Client (as defined in RFC 2033) using an
// existing connection and host as a server name to be used when authenticating.
func NewClientLMTP(conn net.Conn) *Client {
@@ -247,20 +284,17 @@ func (c *Client) ehlo() error {
}
}
}
if mechs, ok := ext["AUTH"]; ok {
c.auth = strings.Split(mechs, " ")
}
c.ext = ext
return err
}
// StartTLS sends the STARTTLS command and encrypts all further communication.
// startTLS sends the STARTTLS command and encrypts all further communication.
// Only servers that advertise the STARTTLS extension support this function.
//
// A nil config is equivalent to a zero tls.Config.
//
// If server returns an error, it will be of type *SMTPError.
func (c *Client) StartTLS(config *tls.Config) error {
func (c *Client) startTLS(config *tls.Config) error {
if err := c.hello(); err != nil {
return err
}
@@ -284,7 +318,7 @@ func (c *Client) StartTLS(config *tls.Config) error {
}
// TLSConnectionState returns the client's TLS connection state.
// The return values are their zero values if StartTLS did
// The return values are their zero values if STARTTLS did
// not succeed.
func (c *Client) TLSConnectionState() (state tls.ConnectionState, ok bool) {
tc, ok := c.conn.(*tls.Conn)
@@ -572,7 +606,7 @@ func (c *Client) LMTPData(statusCb func(rcpt string, status *SMTPError)) (io.Wri
// 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.
// DialStartTLS and Auth before-hand if desirable.
//
// The addresses in the to parameter are the SMTP RCPT addresses.
//
@@ -606,6 +640,46 @@ func (c *Client) SendMail(from string, to []string, r io.Reader) error {
var testHookStartTLS func(*tls.Config) // nil, except for tests
func sendMail(addr string, implicitTLS bool, 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
}
}
var (
c *Client
err error
)
if implicitTLS {
c, err = DialTLS(addr, nil)
} else {
c, err = DialStartTLS(addr, nil)
}
if err != nil {
return err
}
defer c.Close()
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()
}
// 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
@@ -628,76 +702,12 @@ var testHookStartTLS func(*tls.Config) // nil, except for tests
// attachments (see the mime/multipart package or the go-message package), or
// other mail functionality.
func SendMail(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
}
}
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 {
return errors.New("smtp: server doesn't support STARTTLS")
}
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.SendMail(from, to, r); err != nil {
return err
}
return c.Quit()
return sendMail(addr, false, a, from, to, r)
}
// 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
}
}
c, err := DialTLS(addr, nil)
if err != nil {
return err
}
defer c.Close()
if err = c.hello(); 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.SendMail(from, to, r); err != nil {
return err
}
return c.Quit()
return sendMail(addr, true, a, from, to, r)
}
// Extension reports whether an extension is support by the server.
@@ -708,14 +718,47 @@ func (c *Client) Extension(ext string) (bool, string) {
if err := c.hello(); err != nil {
return false, ""
}
if c.ext == nil {
return false, ""
}
ext = strings.ToUpper(ext)
param, ok := c.ext[ext]
return ok, param
}
// SupportsAuth checks whether an authentication mechanism is supported.
func (c *Client) SupportsAuth(mech string) bool {
if err := c.hello(); err != nil {
return false
}
mechs, ok := c.ext["AUTH"]
if !ok {
return false
}
for _, m := range strings.Split(mechs, " ") {
if strings.EqualFold(m, mech) {
return true
}
}
return false
}
// MaxMessageSize returns the maximum message size accepted by the server.
// 0 means unlimited.
//
// If the server doesn't convey this information, ok = false is returned.
func (c *Client) MaxMessageSize() (size int, ok bool) {
if err := c.hello(); err != nil {
return 0, false
}
v := c.ext["SIZE"]
if v == "" {
return 0, false
}
size, err := strconv.Atoi(v)
if err != nil || size < 0 {
return 0, false
}
return size, true
}
// Reset sends the RSET command to the server, aborting the current mail
// transaction.
func (c *Client) Reset() error {

View File

@@ -15,6 +15,8 @@ import (
"strings"
"sync"
"time"
"github.com/emersion/go-sasl"
)
// Number of errors we'll tolerate per connection before closing. Defaults to 3.
@@ -139,11 +141,7 @@ func (c *Conn) handle(cmd string, arg string) {
c.writeResponse(221, EnhancedCode{2, 0, 0}, "Bye")
c.Close()
case "AUTH":
if c.server.AuthDisabled {
c.protocolError(500, EnhancedCode{5, 5, 2}, "Syntax error, AUTH command unrecognized")
} else {
c.handleAuth(arg)
}
c.handleAuth(arg)
case "STARTTLS":
c.handleStartTLS()
default:
@@ -205,7 +203,7 @@ func (c *Conn) Conn() net.Conn {
func (c *Conn) authAllowed() bool {
_, isTLS := c.TLSConnectionState()
return !c.server.AuthDisabled && (isTLS || c.server.AllowInsecureAuth)
return isTLS || c.server.AllowInsecureAuth
}
// protocolError writes errors responses and closes the connection once too many
@@ -250,18 +248,26 @@ func (c *Conn) handleGreet(enhanced bool, arg string) {
return
}
caps := []string{}
caps = append(caps, c.server.caps...)
caps := []string{
"PIPELINING",
"8BITMIME",
"ENHANCEDSTATUSCODES",
"CHUNKING",
}
if _, isTLS := c.TLSConnectionState(); c.server.TLSConfig != nil && !isTLS {
caps = append(caps, "STARTTLS")
}
if c.authAllowed() {
mechs := c.authMechanisms()
authCap := "AUTH"
for name := range c.server.auths {
for _, name := range mechs {
authCap += " " + name
}
caps = append(caps, authCap)
if len(mechs) > 0 {
caps = append(caps, authCap)
}
}
if c.server.EnableSMTPUTF8 {
caps = append(caps, "SMTPUTF8")
@@ -280,6 +286,9 @@ func (c *Conn) handleGreet(enhanced bool, arg string) {
} else {
caps = append(caps, "SIZE")
}
if c.server.MaxRecipients > 0 {
caps = append(caps, fmt.Sprintf("LIMITS RCPTMAX=%v", c.server.MaxRecipients))
}
args := []string{"Hello " + domain}
args = append(args, caps...)
@@ -348,16 +357,18 @@ func (c *Conn) handleMail(arg string) {
}
opts.RequireTLS = true
case "BODY":
switch value {
case "BINARYMIME":
value = strings.ToUpper(value)
switch BodyType(value) {
case BodyBinaryMIME:
if !c.server.EnableBINARYMIME {
c.writeResponse(504, EnhancedCode{5, 5, 4}, "BINARYMIME is not implemented")
return
}
c.binarymime = true
case "7BIT", "8BITMIME":
case Body7Bit, Body8BitMIME:
// This space is intentionally left blank
default:
c.writeResponse(500, EnhancedCode{5, 5, 4}, "Unknown BODY value")
c.writeResponse(501, EnhancedCode{5, 5, 4}, "Unknown BODY value")
return
}
opts.Body = BodyType(value)
@@ -765,7 +776,7 @@ func (c *Conn) handleAuth(arg string) {
return
}
if _, isTLS := c.TLSConnectionState(); !isTLS && !c.server.AllowInsecureAuth {
if !c.authAllowed() {
c.writeResponse(523, EnhancedCode{5, 7, 10}, "TLS is required")
return
}
@@ -778,18 +789,21 @@ func (c *Conn) handleAuth(arg string) {
var err error
ir, err = base64.StdEncoding.DecodeString(parts[1])
if err != nil {
c.writeResponse(454, EnhancedCode{4, 7, 0}, "Invalid base64 data")
return
}
}
newSasl, ok := c.server.auths[mechanism]
if !ok {
c.writeResponse(504, EnhancedCode{5, 7, 4}, "Unsupported authentication mechanism")
sasl, err := c.auth(mechanism)
if err != nil {
if smtpErr, ok := err.(*SMTPError); ok {
c.writeResponse(smtpErr.Code, smtpErr.EnhancedCode, smtpErr.Message)
} else {
c.writeResponse(454, EnhancedCode{4, 7, 0}, err.Error())
}
return
}
sasl := newSasl(c)
response := ir
for {
challenge, done, err := sasl.Next(response)
@@ -834,6 +848,20 @@ func (c *Conn) handleAuth(arg string) {
c.didAuth = true
}
func (c *Conn) authMechanisms() []string {
if authSession, ok := c.Session().(AuthSession); ok {
return authSession.AuthMechanisms()
}
return nil
}
func (c *Conn) auth(mech string) (sasl.Server, error) {
if authSession, ok := c.Session().(AuthSession); ok {
return authSession.Auth(mech)
}
return nil, ErrAuthUnknownMechanism
}
func (c *Conn) handleStartTLS() {
if _, isTLS := c.TLSConnectionState(); isTLS {
c.writeResponse(502, EnhancedCode{5, 5, 1}, "Already running in TLS")

View File

@@ -10,17 +10,12 @@ import (
"os"
"sync"
"time"
"github.com/emersion/go-sasl"
)
var (
ErrServerClosed = errors.New("smtp: server already closed")
)
// A function that creates SASL servers.
type SaslServerFactory func(conn *Conn) sasl.Server
// Logger interface is used by Server to report unexpected internal errors.
type Logger interface {
Printf(format string, v ...interface{})
@@ -64,18 +59,11 @@ type Server struct {
// 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
// The server backend.
Backend Backend
wg sync.WaitGroup
caps []string
auths map[string]SaslServerFactory
done chan struct{}
wg sync.WaitGroup
done chan struct{}
locker sync.Mutex
listeners []net.Listener
@@ -91,24 +79,7 @@ func NewServer(be Backend) *Server {
Backend: be,
done: make(chan struct{}, 1),
ErrorLog: log.New(os.Stderr, "smtp/server ", log.LstdFlags),
caps: []string{"PIPELINING", "8BITMIME", "ENHANCEDSTATUSCODES", "CHUNKING"},
auths: map[string]SaslServerFactory{
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")
}
sess := conn.Session()
if sess == nil {
panic("No session when AUTH is called")
}
return sess.AuthPlain(username, password)
})
},
},
conns: make(map[*Conn]struct{}),
conns: make(map[*Conn]struct{}),
}
}
@@ -329,11 +300,3 @@ func (s *Server) Shutdown(ctx context.Context) error {
return err
}
}
// EnableAuth enables an authentication mechanism on this server.
//
// This function should not be called directly, it must only be used by
// libraries implementing extensions of the SMTP protocol.
func (s *Server) EnableAuth(name string, f SaslServerFactory) {
s.auths[name] = f
}