This commit is contained in:
vfs committed 2026-04-08 21:34:29 +02:00
1 parent ffa5cd9cd7
commit f380dfe58d
238 files changed
+151817 -41

No files matched your search

+27
View File
@@ -0,0 +1,27 @@
Copyright (c) 2009 The Go Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+57
View File
@@ -0,0 +1,57 @@
// SPDX-FileCopyrightText: Copyright 2010 The Go Authors. All rights reserved.
// SPDX-FileCopyrightText: Copyright (c) The go-mail Authors
//
// Original net/smtp code from the Go stdlib by the Go Authors.
// Use of this source code is governed by a BSD-style
// LICENSE file that can be found in this directory.
//
// go-mail specific modifications by the go-mail Authors.
// Licensed under the MIT License.
// See [PROJECT ROOT]/LICENSES directory for more information.
//
// SPDX-License-Identifier: BSD-3-Clause AND MIT
package smtp
import "errors"
var (
// ErrUnencrypted is an error indicating that the connection is not encrypted.
ErrUnencrypted = errors.New("unencrypted connection")
// ErrUnexpectedServerChallange is an error indicating that the server issued an unexpected challenge.
ErrUnexpectedServerChallange = errors.New("unexpected server challenge")
// ErrUnexpectedServerResponse is an error indicating that the server issued an unexpected response.
ErrUnexpectedServerResponse = errors.New("unexpected server response")
// ErrWrongHostname is an error indicating that the provided hostname does not match the expected value.
ErrWrongHostname = errors.New("wrong host name")
)
// Auth is implemented by an SMTP authentication mechanism.
type Auth interface {
// Start begins an authentication with a server.
// It returns the name of the authentication protocol
// and optionally data to include in the initial AUTH message
// sent to the server.
// If it returns a non-nil error, the SMTP client aborts
// the authentication attempt and closes the connection.
Start(server *ServerInfo) (proto string, toServer []byte, err error)
// Next continues the authentication. The server has just sent
// the fromServer data. If more is true, the server expects a
// response, which Next should return as toServer; otherwise
// Next should return toServer == nil.
// If Next returns a non-nil error, the SMTP client aborts
// the authentication attempt and closes the connection.
Next(fromServer []byte, more bool) (toServer []byte, err error)
}
// ServerInfo records information about an SMTP server.
type ServerInfo struct {
Name string // SMTP server name
TLS bool // using TLS, with valid certificate for Name
Auth []string // advertised authentication mechanisms
}
func isLocalhost(name string) bool {
return name == "localhost" || name == "127.0.0.1" || name == "::1"
}
+47
View File
@@ -0,0 +1,47 @@
// SPDX-FileCopyrightText: Copyright 2010 The Go Authors. All rights reserved.
// SPDX-FileCopyrightText: Copyright (c) The go-mail Authors
//
// Original net/smtp code from the Go stdlib by the Go Authors.
// Use of this source code is governed by a BSD-style
// LICENSE file that can be found in this directory.
//
// go-mail specific modifications by the go-mail Authors.
// Licensed under the MIT License.
// See [PROJECT ROOT]/LICENSES directory for more information.
//
// SPDX-License-Identifier: BSD-3-Clause AND MIT
package smtp
import (
"crypto/hmac"
"crypto/md5"
"fmt"
)
// cramMD5Auth is the type that satisfies the Auth interface for the "SMTP CRAM_MD5" auth
type cramMD5Auth struct {
username, secret string
}
// CRAMMD5Auth returns an [Auth] that implements the CRAM-MD5 authentication
// mechanism as defined in RFC 2195.
// The returned Auth uses the given username and secret to authenticate
// to the server using the challenge-response mechanism.
func CRAMMD5Auth(username, secret string) Auth {
return &cramMD5Auth{username, secret}
}
func (a *cramMD5Auth) Start(_ *ServerInfo) (string, []byte, error) {
return "CRAM-MD5", nil, nil
}
func (a *cramMD5Auth) Next(fromServer []byte, more bool) ([]byte, error) {
if more {
d := hmac.New(md5.New, []byte(a.secret))
d.Write(fromServer)
s := make([]byte, 0, d.Size())
return fmt.Appendf(nil, "%s %x", a.username, d.Sum(s)), nil
}
return nil, nil
}
+77
View File
@@ -0,0 +1,77 @@
// SPDX-FileCopyrightText: Copyright (c) The go-mail Authors
//
// SPDX-License-Identifier: MIT
package smtp
import (
"fmt"
)
// loginAuth is the type that satisfies the Auth interface for the "SMTP LOGIN" auth
type loginAuth struct {
username, password string
host string
respStep uint8
allowUnencryptedAuth bool
}
// LoginAuth returns an [Auth] that implements the LOGIN authentication
// mechanism as it is used by MS Outlook. The Auth works similar to PLAIN
// but instead of sending all in one response, the login is handled within
// 3 steps:
// - Sending AUTH LOGIN (server might responds with "Username:")
// - Sending the username (server might responds with "Password:")
// - Sending the password (server authenticates)
// This is the common approach as specified by Microsoft in their MS-XLOGIN spec.
// See: https://msopenspecs.azureedge.net/files/MS-XLOGIN/%5bMS-XLOGIN%5d.pdf
// Yet, there is also an old IETF draft for SMTP AUTH LOGIN that states for clients:
// "The contents of both challenges SHOULD be ignored.".
// See: https://datatracker.ietf.org/doc/html/draft-murchison-sasl-login-00
// Since there is no official standard RFC and we've seen different implementations
// of this mechanism (sending "Username:", "Username", "username", "User name", etc.)
// we follow the IETF-Draft and ignore any server challenge to allow compatibility
// with most mail servers/providers.
//
// LoginAuth will only send the credentials if the connection is using TLS
// or is connected to localhost. Otherwise authentication will fail with an
// error, without sending the credentials.
func LoginAuth(username, password, host string, allowUnenc bool) Auth {
return &loginAuth{username, password, host, 0, allowUnenc}
}
// Start begins the SMTP authentication process by validating server's TLS status and hostname.
// Returns "LOGIN" on success.
func (a *loginAuth) Start(server *ServerInfo) (string, []byte, error) {
// Must have TLS, or else localhost server.
// Note: If TLS is not true, then we can't trust ANYTHING in ServerInfo.
// In particular, it doesn't matter if the server advertises LOGIN auth.
// That might just be the attacker saying
// "it's ok, you can trust me with your password."
if !a.allowUnencryptedAuth && !server.TLS && !isLocalhost(server.Name) {
return "", nil, ErrUnencrypted
}
if server.Name != a.host {
return "", nil, ErrWrongHostname
}
a.respStep = 0
return "LOGIN", nil, nil
}
// Next processes responses from the server during the SMTP authentication exchange, sending the
// username and password.
func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
if more {
switch a.respStep {
case 0:
a.respStep++
return []byte(a.username), nil
case 1:
a.respStep++
return []byte(a.password), nil
default:
return nil, fmt.Errorf("%w: %s", ErrUnexpectedServerResponse, string(fromServer))
}
}
return nil, nil
}
+57
View File
@@ -0,0 +1,57 @@
// SPDX-FileCopyrightText: Copyright 2010 The Go Authors. All rights reserved.
// SPDX-FileCopyrightText: Copyright (c) The go-mail Authors
//
// Original net/smtp code from the Go stdlib by the Go Authors.
// Use of this source code is governed by a BSD-style
// LICENSE file that can be found in this directory.
//
// go-mail specific modifications by the go-mail Authors.
// Licensed under the MIT License.
// See [PROJECT ROOT]/LICENSES directory for more information.
//
// SPDX-License-Identifier: BSD-3-Clause AND MIT
package smtp
// plainAuth is the type that satisfies the Auth interface for the "SMTP PLAIN" auth
type plainAuth struct {
identity, username, password string
host string
allowUnencryptedAuth bool
}
// PlainAuth returns an [Auth] that implements the PLAIN authentication
// mechanism as defined in RFC 4616. The returned Auth uses the given
// username and password to authenticate to host and act as identity.
// Usually identity should be the empty string, to act as username.
//
// PlainAuth will only send the credentials if the connection is using TLS
// or is connected to localhost. Otherwise authentication will fail with an
// error, without sending the credentials.
func PlainAuth(identity, username, password, host string, allowUnenc bool) Auth {
return &plainAuth{identity, username, password, host, allowUnenc}
}
func (a *plainAuth) Start(server *ServerInfo) (string, []byte, error) {
// Must have TLS, or else localhost server.
// Note: If TLS is not true, then we can't trust ANYTHING in ServerInfo.
// In particular, it doesn't matter if the server advertises PLAIN auth.
// That might just be the attacker saying
// "it's ok, you can trust me with your password."
if !a.allowUnencryptedAuth && !server.TLS && !isLocalhost(server.Name) {
return "", nil, ErrUnencrypted
}
if server.Name != a.host {
return "", nil, ErrWrongHostname
}
resp := []byte(a.identity + "\x00" + a.username + "\x00" + a.password)
return "PLAIN", resp, nil
}
func (a *plainAuth) Next(_ []byte, more bool) ([]byte, error) {
if more {
// We've already sent everything.
return nil, ErrUnexpectedServerChallange
}
return nil, nil
}
+317
View File
@@ -0,0 +1,317 @@
// SPDX-FileCopyrightText: Copyright (c) The go-mail Authors
//
// SPDX-License-Identifier: MIT
package smtp
import (
"bytes"
"crypto/hmac"
"crypto/pbkdf2"
"crypto/rand"
"crypto/sha1"
"crypto/sha256"
"crypto/tls"
"encoding/base64"
"errors"
"fmt"
"hash"
"io"
"strconv"
"strings"
"golang.org/x/text/secure/precis"
)
// scramAuth represents a SCRAM (Salted Challenge Response Authentication Mechanism) client and
// satisfies the smtp.Auth interface.
type scramAuth struct {
username, password, algorithm string
firstBareMsg, nonce, saltedPwd, authMessage []byte
iterations int
h func() hash.Hash
isPlus bool
tlsConnState *tls.ConnectionState
bindData []byte
}
// ScramSHA1Auth creates and returns a new SCRAM-SHA-1 authentication mechanism with the given
// username and password.
func ScramSHA1Auth(username, password string) Auth {
return &scramAuth{
username: username,
password: password,
algorithm: "SCRAM-SHA-1",
h: sha1.New,
}
}
// ScramSHA256Auth creates and returns a new SCRAM-SHA-256 authentication mechanism with the given
// username and password.
func ScramSHA256Auth(username, password string) Auth {
return &scramAuth{
username: username,
password: password,
algorithm: "SCRAM-SHA-256",
h: sha256.New,
}
}
// ScramSHA1PlusAuth returns an Auth instance configured for SCRAM-SHA-1-PLUS authentication with
// the provided username, password, and TLS connection state.
func ScramSHA1PlusAuth(username, password string, tlsConnState *tls.ConnectionState) Auth {
return &scramAuth{
username: username,
password: password,
algorithm: "SCRAM-SHA-1-PLUS",
h: sha1.New,
isPlus: true,
tlsConnState: tlsConnState,
}
}
// ScramSHA256PlusAuth returns an Auth instance configured for SCRAM-SHA-256-PLUS authentication with
// the provided username, password, and TLS connection state.
func ScramSHA256PlusAuth(username, password string, tlsConnState *tls.ConnectionState) Auth {
return &scramAuth{
username: username,
password: password,
algorithm: "SCRAM-SHA-256-PLUS",
h: sha256.New,
isPlus: true,
tlsConnState: tlsConnState,
}
}
// Start initializes the SCRAM authentication process and returns the selected algorithm, nil data, and no error.
func (a *scramAuth) Start(_ *ServerInfo) (string, []byte, error) {
return a.algorithm, nil, nil
}
// Next processes the server's challenge and returns the client's response for SCRAM authentication.
func (a *scramAuth) Next(fromServer []byte, more bool) ([]byte, error) {
if more {
if len(fromServer) == 0 {
a.reset()
return a.initialClientMessage()
}
switch {
case bytes.HasPrefix(fromServer, []byte("r=")):
resp, err := a.handleServerFirstResponse(fromServer)
if err != nil {
a.reset()
return nil, err
}
return resp, nil
case bytes.HasPrefix(fromServer, []byte("v=")):
resp, err := a.handleServerValidationMessage(fromServer)
if err != nil {
a.reset()
return nil, err
}
return resp, nil
default:
a.reset()
return nil, fmt.Errorf("%w: %s", ErrUnexpectedServerResponse, string(fromServer))
}
}
return nil, nil
}
// reset clears all authentication-related properties in the scramAuth instance, effectively resetting its state.
func (a *scramAuth) reset() {
a.nonce = nil
a.firstBareMsg = nil
a.saltedPwd = nil
a.authMessage = nil
a.iterations = 0
}
// initialClientMessage generates the initial message for SCRAM authentication, including a nonce and
// optional channel binding.
func (a *scramAuth) initialClientMessage() ([]byte, error) {
username, err := a.normalizeUsername()
if err != nil {
return nil, fmt.Errorf("username normalization failed: %w", err)
}
nonceBuffer := make([]byte, 24)
if _, err := io.ReadFull(rand.Reader, nonceBuffer); err != nil {
return nil, fmt.Errorf("unable to generate client secret: %w", err)
}
a.nonce = make([]byte, base64.StdEncoding.EncodedLen(len(nonceBuffer)))
base64.StdEncoding.Encode(a.nonce, nonceBuffer)
a.firstBareMsg = []byte("n=" + username + ",r=" + string(a.nonce))
returnBytes := []byte("n,," + string(a.firstBareMsg))
// SCRAM-SHA-X-PLUS auth requires channel binding
if a.isPlus {
if a.tlsConnState == nil {
return nil, errors.New("tls connection state is required for SCRAM-SHA-X-PLUS")
}
bindType := "tls-unique"
connState := a.tlsConnState
bindData := connState.TLSUnique
// crypto/tls: no tls-unique channel binding value for this tls connection, possibly due to missing
// extended master key support and/or resumed connection
// RFC9266:122 tls-unique not defined for tls 1.3 and later
if bindData == nil || connState.Version >= tls.VersionTLS13 {
bindType = "tls-exporter"
bindData, err = connState.ExportKeyingMaterial("EXPORTER-Channel-Binding", []byte{}, 32)
if err != nil {
return nil, fmt.Errorf("unable to export keying material: %w", err)
}
}
bindData = []byte("p=" + bindType + ",," + string(bindData))
a.bindData = make([]byte, base64.StdEncoding.EncodedLen(len(bindData)))
base64.StdEncoding.Encode(a.bindData, bindData)
returnBytes = []byte("p=" + bindType + ",," + string(a.firstBareMsg))
}
return returnBytes, nil
}
// handleServerFirstResponse processes the first response from the server in SCRAM authentication.
func (a *scramAuth) handleServerFirstResponse(fromServer []byte) ([]byte, error) {
parts := bytes.Split(fromServer, []byte(","))
if len(parts) < 3 {
return nil, errors.New("not enough fields in the first server response")
}
if !bytes.HasPrefix(parts[0], []byte("r=")) {
return nil, errors.New("first part of the server response does not start with r=")
}
if !bytes.HasPrefix(parts[1], []byte("s=")) {
return nil, errors.New("second part of the server response does not start with s=")
}
if !bytes.HasPrefix(parts[2], []byte("i=")) {
return nil, errors.New("third part of the server response does not start with i=")
}
combinedNonce := parts[0][2:]
if len(a.nonce) == 0 || !bytes.HasPrefix(combinedNonce, a.nonce) {
return nil, errors.New("server nonce does not start with our nonce")
}
a.nonce = combinedNonce
encodedSalt := parts[1][2:]
salt := make([]byte, base64.StdEncoding.DecodedLen(len(encodedSalt)))
n, err := base64.StdEncoding.Decode(salt, encodedSalt)
if err != nil {
return nil, fmt.Errorf("invalid encoded salt: %w", err)
}
salt = salt[:n]
iterations, err := strconv.Atoi(string(parts[2][2:]))
if err != nil {
return nil, fmt.Errorf("invalid iterations: %w", err)
}
a.iterations = iterations
password, err := a.normalizeString(a.password)
if err != nil {
return nil, fmt.Errorf("unable to normalize password: %w", err)
}
a.saltedPwd, err = pbkdf2.Key(a.h, password, salt, a.iterations, a.h().Size())
if err != nil {
return nil, fmt.Errorf("unable to derive key: %w", err)
}
msgWithoutProof := []byte("c=biws,r=" + string(a.nonce))
// A PLUS authentication requires the channel binding data
if a.isPlus {
msgWithoutProof = []byte("c=" + string(a.bindData) + ",r=" + string(a.nonce))
}
a.authMessage = []byte(string(a.firstBareMsg) + "," + string(fromServer) + "," + string(msgWithoutProof))
clientProof := a.computeClientProof()
return []byte(string(msgWithoutProof) + ",p=" + string(clientProof)), nil
}
// handleServerValidationMessage verifies the server's signature during the SCRAM authentication process.
func (a *scramAuth) handleServerValidationMessage(fromServer []byte) ([]byte, error) {
serverSignature := fromServer[2:]
computedServerSignature := a.computeServerSignature()
if !hmac.Equal(serverSignature, computedServerSignature) {
return nil, errors.New("invalid server signature")
}
return []byte(""), nil
}
// computeHMAC generates a Hash-based Message Authentication Code (HMAC) using the specified key and message.
func (a *scramAuth) computeHMAC(key, msg []byte) []byte {
mac := hmac.New(a.h, key)
mac.Write(msg)
return mac.Sum(nil)
}
// computeHash generates a hash of the given key using the configured hashing algorithm.
func (a *scramAuth) computeHash(key []byte) []byte {
hasher := a.h()
hasher.Write(key)
return hasher.Sum(nil)
}
// computeClientProof generates the client proof as part of the SCRAM authentication process.
func (a *scramAuth) computeClientProof() []byte {
clientKey := a.computeHMAC(a.saltedPwd, []byte("Client Key"))
storedKey := a.computeHash(clientKey)
clientSignature := a.computeHMAC(storedKey[:], a.authMessage)
clientProof := make([]byte, len(clientSignature))
for i := 0; i < len(clientSignature); i++ {
clientProof[i] = clientKey[i] ^ clientSignature[i]
}
buf := make([]byte, base64.StdEncoding.EncodedLen(len(clientProof)))
base64.StdEncoding.Encode(buf, clientProof)
return buf
}
// computeServerSignature returns the computed base64-encoded server signature in the SCRAM
// authentication process.
func (a *scramAuth) computeServerSignature() []byte {
serverKey := a.computeHMAC(a.saltedPwd, []byte("Server Key"))
serverSignature := a.computeHMAC(serverKey, a.authMessage)
buf := make([]byte, base64.StdEncoding.EncodedLen(len(serverSignature)))
base64.StdEncoding.Encode(buf, serverSignature)
return buf
}
// normalizeUsername replaces special characters in the username for SCRAM authentication
// and prepares it using the SASLprep profile as per RFC 8265, returning the normalized
// username or an error.
func (a *scramAuth) normalizeUsername() (string, error) {
// RFC 5802 section 5.1: the characters ',' or '=' in usernames are
// sent as '=2C' and '=3D' respectively.
replacer := strings.NewReplacer("=", "=3D", ",", "=2C")
username := replacer.Replace(a.username)
// RFC 5802 section 5.1: before sending the username to the server,
// the client SHOULD prepare the username using the "SASLprep"
// profile [RFC4013] of the "stringprep" algorithm [RFC3454]
// treating it as a query string (i.e., unassigned Unicode code
// points are allowed). If the preparation of the username fails or
// results in an empty string, the client SHOULD abort the
// authentication exchange.
//
// Since RFC 8265 obsoletes RFC 4013 we use it instead.
username, err := a.normalizeString(username)
if err != nil {
return "", fmt.Errorf("unable to normalize username: %w", err)
}
return username, nil
}
// normalizeString normalizes the input string according to the OpaqueString profile of the
// precis framework. It returns the normalized string or an error if normalization fails or
// results in an empty string.
func (a *scramAuth) normalizeString(s string) (string, error) {
s, err := precis.OpaqueString.String(s)
if err != nil {
return "", fmt.Errorf("failed to normalize string: %w", err)
}
return s, nil
}
+29
View File
@@ -0,0 +1,29 @@
// SPDX-FileCopyrightText: Copyright (c) The go-mail Authors
//
// SPDX-License-Identifier: MIT
package smtp
type xoauth2Auth struct {
username, token string
}
// XOAuth2Auth returns an [Auth] that implements the XOAuth2 authentication
// mechanism as defined in the following specs:
//
// https://developers.google.com/gmail/imap/xoauth2-protocol
// https://learn.microsoft.com/en-us/exchange/client-developer/legacy-protocols/how-to-authenticate-an-imap-pop-smtp-application-by-using-oauth
func XOAuth2Auth(username, token string) Auth {
return &xoauth2Auth{username, token}
}
func (a *xoauth2Auth) Start(_ *ServerInfo) (string, []byte, error) {
return "XOAUTH2", []byte("user=" + a.username + "\x01" + "auth=Bearer " + a.token + "\x01\x01"), nil
}
func (a *xoauth2Auth) Next(_ []byte, more bool) ([]byte, error) {
if more {
return []byte(""), nil
}
return nil, nil
}
+128
View File
@@ -0,0 +1,128 @@
// SPDX-FileCopyrightText: Copyright (c) The go-mail Authors
//
// SPDX-License-Identifier: MIT
package smtp
import (
"net/textproto"
"strings"
"sync"
)
// ResponseErrorHandler provides custom handling for SMTP response errors.
//
// This interface defines a method for handling SMTP responses that do not comply with expected
// formats or behaviors. It is useful for implementing retry logic, logging, or protocol-specific
// error handling logic. It injects itself into the smtp.Client and is called whenever a server
// response does fail.
//
// Parameters:
// - host: The hostname of the SMTP server that this ResponseErrorHandler should handle
// - command: The SMTP command that triggered the error.
// - conn: The textproto.Conn to the SMTP server.
// - err: The error received from the SMTP server.
//
// Returns:
// - An error indicating the outcome of the error handling logic.
type ResponseErrorHandler interface {
HandleError(host, command string, conn *textproto.Conn, err error) error
}
// DefaultErrorHandler implements ResponseErrorHandler by returning the original error.
//
// This handler provides a default implementation of the ResponseErrorHandler interface,
// where no custom error processing is performed. It simply returns the original error
// received from the SMTP server.
//
// Returns:
// - The original error passed to the handler.
type DefaultErrorHandler struct{}
// HandleError satisfies the ResponseErrorHandler interface for the DefaultErrorHandler type
func (d *DefaultErrorHandler) HandleError(_, _ string, _ *textproto.Conn, err error) error {
return err
}
// HandlerKey uniquely identifies a host-command pair for handler mapping.
//
// This struct is used to associate a specific SMTP host and command combination with
// a ResponseErrorHandler. It enables mapping and retrieval of handlers based on
// the source of the error.
type HandlerKey struct {
Host string
Command string
}
// ErrorHandlerRegistry manages custom error handlers for SMTP host-command pairs.
//
// This struct stores mappings between HandlerKey values and corresponding
// ResponseErrorHandler implementations. It supports concurrent access and provides
// a fallback default handler when no specific match is found.
type ErrorHandlerRegistry struct {
mu sync.RWMutex
handlers map[HandlerKey]ResponseErrorHandler
defaultHandler ResponseErrorHandler
}
// NewErrorHandlerRegistry creates a new ErrorHandlerRegistry instance.
//
// This function initializes an ErrorHandlerRegistry with an empty handler map and
// assigns a DefaultErrorHandler as the fallback handler.
//
// Returns:
// - A pointer to the newly constructed ErrorHandlerRegistry.
func NewErrorHandlerRegistry() *ErrorHandlerRegistry {
return &ErrorHandlerRegistry{
handlers: make(map[HandlerKey]ResponseErrorHandler),
defaultHandler: &DefaultErrorHandler{}, // Set the default handler
}
}
// RegisterHandler associates a custom handler with a specific host and command.
//
// This method registers a ResponseErrorHandler for the given SMTP host and command
// combination. It ensures thread-safe access to the internal handler map.
//
// Parameters:
// - host: The SMTP server hostname.
// - command: The SMTP command to associate with the handler.
// - handler: The ResponseErrorHandler to register.
func (r *ErrorHandlerRegistry) RegisterHandler(host, command string, handler ResponseErrorHandler) {
r.mu.Lock()
defer r.mu.Unlock()
r.handlers[HandlerKey{Host: strings.ToLower(host), Command: strings.ToLower(command)}] = handler
}
// GetHandler retrieves the handler for a given host and command.
//
// This method returns the ResponseErrorHandler registered for the specified SMTP host
// and command. If no handler is found, it returns the default handler.
//
// Parameters:
// - host: The SMTP server hostname.
// - command: The SMTP command to look up.
//
// Returns:
// - The corresponding ResponseErrorHandler, or the default handler if none is registered.
func (r *ErrorHandlerRegistry) GetHandler(host, command string) ResponseErrorHandler {
r.mu.RLock()
defer r.mu.RUnlock()
if handler, ok := r.handlers[HandlerKey{Host: strings.ToLower(host), Command: strings.ToLower(command)}]; ok {
return handler
}
return r.defaultHandler
}
// SetDefaultHandler overrides the default ResponseErrorHandler.
//
// This method sets a new default handler to be used when no specific handler is
// registered for a host and command combination. It ensures thread-safe access.
//
// Parameters:
// - handler: The new default ResponseErrorHandler to assign.
func (r *ErrorHandlerRegistry) SetDefaultHandler(handler ResponseErrorHandler) {
r.mu.Lock()
defer r.mu.Unlock()
r.defaultHandler = handler
}
+702
View File
@@ -0,0 +1,702 @@
// SPDX-FileCopyrightText: Copyright 2010 The Go Authors. All rights reserved.
// SPDX-FileCopyrightText: Copyright (c) The go-mail Authors
//
// Original net/smtp code from the Go stdlib by the Go Authors.
// Use of this source code is governed by a BSD-style
// LICENSE file that can be found in this directory.
//
// go-mail specific modifications by the go-mail Authors.
// Licensed under the MIT License.
// See [PROJECT ROOT]/LICENSES directory for more information.
//
// SPDX-License-Identifier: BSD-3-Clause AND MIT
// Package smtp implements the Simple Mail Transfer Protocol as defined in RFC 5321.
// It also implements the following extensions:
//
// 8BITMIME RFC 1652
// AUTH RFC 2554
// STARTTLS RFC 3207
// DSN RFC 1891
package smtp
import (
"crypto/tls"
"encoding/base64"
"errors"
"fmt"
"io"
"net"
"net/textproto"
"os"
"strings"
"sync"
"time"
"github.com/wneessen/go-mail/log"
)
var (
// ErrNonTLSConnection is returned when an attempt is made to retrieve TLS state on a non-TLS connection.
ErrNonTLSConnection = errors.New("connection is not using TLS")
// ErrNoConnection is returned when attempting to perform an operation that requires an established
// connection but none exists.
ErrNoConnection = errors.New("connection is not established")
)
// 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
// ErrorHandlerRegistry manages custom error handlers for SMTP host-command pairs.
ErrorHandlerRegistry *ErrorHandlerRegistry
// auth supported auth mechanisms
auth []string
// authIsActive indicates that the Client is currently during SMTP authentication
authIsActive bool
// keep a reference to the connection so it can be used to create a TLS connection later
conn net.Conn
// debug logging is enabled
debug bool
// didHello indicates whether we've said HELO/EHLO
didHello bool
// dsnmrtype defines the mail return option in case DSN is enabled
dsnmrtype string
// dsnrntype defines the recipient notify option in case DSN is enabled
dsnrntype string
// ext is a map of supported extensions
ext map[string]string
// helloError is the error from the hello
helloError error
// isConnected indicates if the Client has an active connection
isConnected bool
// logAuthData indicates if the Client should include SMTP authentication data in the logs
logAuthData bool
// localName is the name to use in HELO/EHLO
localName string // the name to use in HELO/EHLO
// logger will be used for debug logging
logger log.Logger
// mutex is used to synchronize access to shared resources, ensuring that only one goroutine can access
// the resource at a time.
mutex sync.RWMutex
// tls indicates whether the Client is using TLS
tls bool
// serverName denotes the name of the server to which the application will connect. Used for
// identification and routing.
serverName string
}
// Dial returns a new [Client] connected to an SMTP server at addr.
// The addr must include a port, as in "mail.example.com:smtp".
func Dial(addr string) (*Client, error) {
conn, err := net.Dial("tcp", addr)
if err != nil {
return nil, err
}
host, _, _ := net.SplitHostPort(addr)
return NewClient(conn, host)
}
// 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) {
text := textproto.NewConn(conn)
_, _, err := text.ReadResponse(220)
if err != nil {
if cerr := text.Close(); cerr != nil {
// Since we are being Go <1.20 compatible, we can't combine errorrs and
// duplicate %w vers are not suppored. Therefore let's ignore this linting
// error for now
// nolint:errorlint
return nil, fmt.Errorf("%w, %s", err, cerr)
}
return nil, err
}
c := &Client{Text: text, conn: conn, serverName: host, localName: "localhost"}
_, c.tls = conn.(*tls.Conn)
c.isConnected = true
c.ErrorHandlerRegistry = NewErrorHandlerRegistry()
return c, nil
}
// Close closes the connection.
func (c *Client) Close() error {
c.mutex.Lock()
err := c.Text.Close()
c.isConnected = false
c.mutex.Unlock()
return err
}
// hello runs a hello exchange if needed.
func (c *Client) hello() error {
if !c.didHello {
c.didHello = true
err := c.ehlo()
if err != nil {
c.helloError = c.helo()
}
}
return c.helloError
}
// Hello sends a HELO or EHLO to the server as the given host name.
// Calling this method is only necessary if the client needs control
// over the host name used. The client will introduce itself as "localhost"
// automatically otherwise. If Hello is called, it must be called before
// any of the other methods.
func (c *Client) Hello(localName string) error {
if err := validateLine(localName); err != nil {
return err
}
if c.didHello {
return errors.New("smtp: Hello called after other methods")
}
c.mutex.Lock()
c.localName = localName
c.mutex.Unlock()
return c.hello()
}
// cmd is a convenience function that sends a command and returns the response
func (c *Client) cmd(expectCode int, format string, args ...interface{}) (int, string, error) {
c.mutex.Lock()
var logMsg []interface{}
logMsg = args
logFmt := format
if c.authIsActive {
logMsg = []interface{}{"<SMTP auth data redacted>"}
logFmt = "%s"
}
c.debugLog(log.DirClientToServer, logFmt, logMsg...)
id, err := c.Text.Cmd(format, args...)
if err != nil {
c.mutex.Unlock()
return 0, "", err
}
c.Text.StartResponse(id)
defer c.Text.EndResponse(id)
code, msg, err := c.Text.ReadResponse(expectCode)
if err != nil {
fmtValues := strings.Split(format, " ")
currentCmd := strings.ToLower(fmtValues[0])
handler := c.ErrorHandlerRegistry.GetHandler(c.serverName, currentCmd)
handledErr := handler.HandleError(c.serverName, currentCmd, c.Text, err)
if handledErr != nil {
c.mutex.Unlock()
return 0, "", handledErr
}
// If the handler successfully recovered, we try reading the response again
// This assumes the handler has consumed the problematic data beforehand.
code, msg, err = c.Text.ReadResponse(expectCode)
}
logMsg = []interface{}{code, msg}
if c.authIsActive && code >= 300 && code <= 400 {
logMsg = []interface{}{code, "<SMTP auth data redacted>"}
}
c.debugLog(log.DirServerToClient, "%d %s", logMsg...)
c.mutex.Unlock()
return code, msg, err
}
// helo sends the HELO greeting to the server. It should be used only when the
// server does not support ehlo.
func (c *Client) helo() error {
c.mutex.Lock()
c.ext = nil
c.mutex.Unlock()
_, _, err := c.cmd(250, "HELO %s", c.localName)
return err
}
// StartTLS sends the STARTTLS command and encrypts all further communication.
// Only servers that advertise the STARTTLS extension support this function.
func (c *Client) StartTLS(config *tls.Config) error {
if err := c.hello(); err != nil {
return err
}
_, _, err := c.cmd(220, "STARTTLS")
if err != nil {
return err
}
c.mutex.Lock()
c.conn = tls.Client(c.conn, config)
c.Text = textproto.NewConn(c.conn)
c.tls = true
c.mutex.Unlock()
return c.ehlo()
}
// TLSConnectionState returns the client's TLS connection state.
// The return values are their zero values if [Client.StartTLS] did
// not succeed.
func (c *Client) TLSConnectionState() (state tls.ConnectionState, ok bool) {
c.mutex.RLock()
defer c.mutex.RUnlock()
tc, ok := c.conn.(*tls.Conn)
if !ok {
return state, ok
}
state, ok = tc.ConnectionState(), true
return state, ok
}
// Verify checks the validity of an email address on the server.
// If Verify returns nil, the address is valid. A non-nil return
// does not necessarily indicate an invalid address. Many servers
// will not verify addresses for security reasons.
func (c *Client) Verify(addr string) error {
if err := validateLine(addr); err != nil {
return err
}
if err := c.hello(); err != nil {
return err
}
_, _, err := c.cmd(250, "VRFY %s", addr)
return err
}
// Auth authenticates a client using the provided authentication mechanism.
// A failed authentication closes the connection.
// Only servers that advertise the AUTH extension support this function.
func (c *Client) Auth(a Auth) error {
if err := c.hello(); err != nil {
return err
}
c.mutex.Lock()
if !c.logAuthData {
c.authIsActive = true
}
c.mutex.Unlock()
defer func() {
c.mutex.Lock()
if !c.logAuthData {
c.authIsActive = false
}
c.mutex.Unlock()
}()
encoding := base64.StdEncoding
mech, resp, err := a.Start(&ServerInfo{c.serverName, c.tls, c.auth})
if err != nil {
if qerr := c.Quit(); qerr != nil {
// Since we are being Go <1.20 compatible, we can't combine errorrs and
// duplicate %w vers are not suppored. Therefore let's ignore this linting
// error for now
// nolint:errorlint
return fmt.Errorf("%w, %s", err, qerr)
}
return err
}
resp64 := make([]byte, encoding.EncodedLen(len(resp)))
encoding.Encode(resp64, resp)
code, msg64, err := c.cmd(0, "%s", strings.TrimSpace(fmt.Sprintf("AUTH %s %s", mech,
resp64)))
for err == nil {
var msg []byte
switch code {
case 334:
msg, err = encoding.DecodeString(msg64)
case 235:
// the last message isn't base64 because it isn't a challenge
msg = []byte(msg64)
default:
err = &textproto.Error{Code: code, Msg: msg64}
}
if err == nil {
resp, err = a.Next(msg, code == 334)
}
if err != nil {
if mech != "XOAUTH2" {
// abort the AUTH. Not required for XOAUTH2
_, _, _ = c.cmd(501, "*")
}
_ = c.Quit()
break
}
if resp == nil {
break
}
resp64 = make([]byte, encoding.EncodedLen(len(resp)))
encoding.Encode(resp64, resp)
code, msg64, err = c.cmd(0, "%s", resp64)
}
return err
}
// Mail issues a MAIL command to the server using the provided email address.
// If the server supports the 8BITMIME extension, Mail adds the BODY=8BITMIME
// parameter. If the server supports the SMTPUTF8 extension, Mail adds the
// SMTPUTF8 parameter.
// This initiates a mail transaction and is followed by one or more [Client.Rcpt] calls.
func (c *Client) Mail(from string) error {
if err := validateLine(from); err != nil {
return err
}
if err := c.hello(); err != nil {
return err
}
cmdStr := "MAIL FROM:%s"
c.mutex.RLock()
if c.ext != nil {
if _, ok := c.ext["8BITMIME"]; ok {
cmdStr += " BODY=8BITMIME"
}
if _, ok := c.ext["SMTPUTF8"]; ok {
cmdStr += " SMTPUTF8"
}
_, ok := c.ext["DSN"]
if ok && c.dsnmrtype != "" {
cmdStr += fmt.Sprintf(" RET=%s", c.dsnmrtype)
}
}
c.mutex.RUnlock()
_, _, err := c.cmd(250, cmdStr, from)
return err
}
// Rcpt issues a RCPT command to the server using the provided email address.
// A call to Rcpt must be preceded by a call to [Client.Mail] and may be followed by
// a [Client.Data] call or another Rcpt call.
func (c *Client) Rcpt(to string) error {
if err := validateLine(to); err != nil {
return err
}
c.mutex.RLock()
_, ok := c.ext["DSN"]
c.mutex.RUnlock()
if ok && c.dsnrntype != "" {
_, _, err := c.cmd(25, "RCPT TO:%s NOTIFY=%s", to, c.dsnrntype)
return err
}
_, _, err := c.cmd(25, "RCPT TO:%s", to)
return err
}
type DataCloser struct {
c *Client
done bool
io.WriteCloser
response string
}
// Close releases the lock, closes the WriteCloser, waits for a response, and then returns any error encountered.
func (d *DataCloser) Close() error {
d.c.mutex.Lock()
_ = d.WriteCloser.Close()
_, resp, err := d.c.Text.ReadResponse(250)
d.response = resp
d.done = true
d.c.mutex.Unlock()
return err
}
// Write writes data to the underlying WriteCloser while ensuring thread-safety by locking and unlocking a mutex.
func (d *DataCloser) Write(p []byte) (n int, err error) {
d.c.mutex.Lock()
n, err = d.WriteCloser.Write(p)
d.c.mutex.Unlock()
return n, err
}
// ServerResponse returns the response that was returned by the server after the DataCloser has
// been closed. If the DataCloser has not been closed yet, it will return an empty string.
func (d *DataCloser) ServerResponse() string {
if !d.done {
return ""
}
return d.response
}
// Data issues a DATA command to the server and returns a writer that
// can be used to write the mail headers and body. The caller should
// close the writer before calling any more methods on c. A call to
// Data must be preceded by one or more calls to [Client.Rcpt].
func (c *Client) Data() (io.WriteCloser, error) {
_, _, err := c.cmd(354, "DATA")
if err != nil {
return nil, err
}
datacloser := &DataCloser{}
c.mutex.Lock()
datacloser.c = c
datacloser.WriteCloser = c.Text.DotWriter()
c.mutex.Unlock()
return datacloser, nil
}
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 msg.
// The addr must include a port, as in "mail.example.com:smtp".
//
// The addresses in the to parameter are the SMTP RCPT addresses.
//
// The msg parameter should be an RFC 822-style email with headers
// first, a blank line, and then the message body. The lines of msg
// should be CRLF terminated. The msg 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 msg headers.
//
// The SendMail function and the net/smtp package are low-level
// mechanisms and provide no support for DKIM signing, MIME
// attachments (see the mime/multipart package), or other mail
// functionality. Higher-level packages exist outside of the standard
// library.
func SendMail(addr string, a Auth, from string, to []string, msg []byte) 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 func() {
_ = c.Close()
}()
if err = c.hello(); err != nil {
return err
}
if ok, _ := c.Extension("STARTTLS"); ok {
config := &tls.Config{ServerName: c.serverName}
if testHookStartTLS != nil {
testHookStartTLS(config)
}
if err = c.StartTLS(config); err != nil {
return err
}
}
if a != nil && c.ext != nil {
if _, ok := c.ext["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); err != nil {
return err
}
for _, addr := range to {
if err = c.Rcpt(addr); err != nil {
return err
}
}
w, err := c.Data()
if err != nil {
return err
}
_, err = w.Write(msg)
if err != nil {
return err
}
err = w.Close()
if err != nil {
return err
}
return c.Quit()
}
// Extension reports whether an extension is support by the server.
// The extension name is case-insensitive. If the extension is supported,
// Extension also returns a string that contains any parameters the
// server specifies for the extension.
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)
c.mutex.RLock()
param, ok := c.ext[ext]
c.mutex.RUnlock()
return ok, param
}
// Reset sends the RSET command to the server, aborting the current mail
// transaction.
func (c *Client) Reset() error {
if err := c.hello(); err != nil {
return err
}
_, _, err := c.cmd(250, "RSET")
return err
}
// Noop sends the NOOP command to the server. It does nothing but check
// that the connection to the server is okay.
func (c *Client) Noop() error {
if err := c.hello(); err != nil {
return err
}
_, _, err := c.cmd(250, "NOOP")
return err
}
// Quit sends the QUIT command and closes the connection to the server.
func (c *Client) Quit() error {
// See https://github.com/golang/go/issues/70011
_ = c.hello() // ignore error; we're quitting anyhow
_, _, err := c.cmd(221, "QUIT")
if err != nil {
return err
}
c.mutex.Lock()
err = c.Text.Close()
c.isConnected = false
c.mutex.Unlock()
return err
}
// SetDebugLog enables the debug logging for incoming and outgoing SMTP messages
func (c *Client) SetDebugLog(v bool) {
c.debug = v
if v {
if c.logger == nil {
c.logger = log.New(os.Stderr, log.LevelDebug)
}
return
}
c.logger = nil
}
// SetLogger overrides the default log.Stdlog for the debug logging with a logger that
// satisfies the log.Logger interface
func (c *Client) SetLogger(l log.Logger) {
if l == nil {
return
}
c.mutex.Lock()
c.logger = l
c.mutex.Unlock()
}
// SetLogAuthData enables logging of authentication data in the Client.
func (c *Client) SetLogAuthData() {
c.mutex.Lock()
c.logAuthData = true
c.mutex.Unlock()
}
// SetDSNMailReturnOption sets the DSN mail return option for the Mail method
func (c *Client) SetDSNMailReturnOption(d string) {
c.mutex.Lock()
c.dsnmrtype = d
c.mutex.Unlock()
}
// SetDSNRcptNotifyOption sets the DSN recipient notify option for the Mail method
func (c *Client) SetDSNRcptNotifyOption(d string) {
c.mutex.Lock()
c.dsnrntype = d
c.mutex.Unlock()
}
// HasConnection checks if the client has an active connection.
// Returns true if the `conn` field is not nil, indicating an active connection.
func (c *Client) HasConnection() bool {
c.mutex.RLock()
isConn := c.isConnected
c.mutex.RUnlock()
return isConn
}
// UpdateDeadline sets a new deadline on the SMTP connection with the specified timeout duration.
func (c *Client) UpdateDeadline(timeout time.Duration) error {
c.mutex.Lock()
defer c.mutex.Unlock()
if c.conn == nil {
return errors.New("smtp: client has no connection")
}
if err := c.conn.SetDeadline(time.Now().Add(timeout)); err != nil {
return fmt.Errorf("smtp: failed to update deadline: %w", err)
}
return nil
}
// GetTLSConnectionState retrieves the TLS connection state of the client's current connection.
// Returns an error if the connection is not using TLS or if the connection is not established.
func (c *Client) GetTLSConnectionState() (*tls.ConnectionState, error) {
c.mutex.RLock()
defer c.mutex.RUnlock()
if !c.isConnected {
return nil, ErrNoConnection
}
if !c.tls {
return nil, ErrNonTLSConnection
}
if conn, ok := c.conn.(*tls.Conn); ok {
cstate := conn.ConnectionState()
return &cstate, nil
}
return nil, errors.New("unable to retrieve TLS connection state")
}
// debugLog checks if the debug flag is set and if so logs the provided message to
// the log.Logger interface
func (c *Client) debugLog(d log.Direction, f string, a ...interface{}) {
if c.debug {
c.logger.Debugf(log.Log{Direction: d, Format: f, Messages: a})
}
}
// 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
}
+42
View File
@@ -0,0 +1,42 @@
// SPDX-FileCopyrightText: Copyright 2010 The Go Authors. All rights reserved.
// SPDX-FileCopyrightText: Copyright (c) The go-mail Authors
//
// Original net/smtp code from the Go stdlib by the Go Authors.
// Use of this source code is governed by a BSD-style
// LICENSE file that can be found in this directory.
//
// go-mail specific modifications by the go-mail Authors.
// Licensed under the MIT License.
// See [PROJECT ROOT]/LICENSES directory for more information.
//
// SPDX-License-Identifier: BSD-3-Clause AND MIT
package smtp
import "strings"
// ehlo sends the EHLO (extended hello) greeting to the server. It
// should be the preferred greeting for servers that support it.
func (c *Client) ehlo() error {
_, msg, err := c.cmd(250, "EHLO %s", c.localName)
if err != nil {
return err
}
c.mutex.Lock()
defer c.mutex.Unlock()
ext := make(map[string]string)
extList := strings.Split(msg, "\n")
if len(extList) > 1 {
extList = extList[1:]
for _, line := range extList {
k, v, _ := strings.Cut(line, " ")
ext[k] = v
}
}
if mechs, ok := ext["AUTH"]; ok {
c.auth = strings.Split(mechs, " ")
}
c.ext = ext
return err
}