vendored
This commit is contained in:
1 parent
ffa5cd9cd7
commit
f380dfe58d
238 files changed
+151817
-41
No files matched your search
+27
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user