Merge pull request #100 from wneessen/feature/97_fork-the-netsmtp-package-from-stdlib-into-go-mail

Fork the net/smtp package from Go's stdlib into go-mail
This commit is contained in:
Winni Neessen 2023-01-13 17:51:16 +01:00 committed by GitHub
commit 813020f02d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 2187 additions and 125 deletions

27
LICENSES/BSD-3-Clause.txt Normal file
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.

View file

@ -1,9 +1,18 @@
MIT License MIT License
Copyright (c) <year> <copyright holders> Copyright (c) 2022-2023 The go-mail Authors
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the Software without restriction, including without
limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so, subject to the
following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.

View file

@ -1,104 +0,0 @@
// SPDX-FileCopyrightText: 2022 Winni Neessen <winni@neessen.dev>
//
// SPDX-License-Identifier: MIT
package auth
import (
"bytes"
"net/smtp"
"testing"
)
func TestAuth(t *testing.T) {
type authTest struct {
auth smtp.Auth
challenges []string
name string
responses []string
shouldfail []bool
}
authTests := []authTest{
{
LoginAuth("user", "pass", "testserver"),
[]string{"Username:", "Password:", "Invalid:"},
"LOGIN",
[]string{"", "user", "pass", ""},
[]bool{false, false, true},
},
}
testLoop:
for i, test := range authTests {
name, resp, err := test.auth.Start(&smtp.ServerInfo{Name: "testserver", TLS: true, Auth: nil})
if name != test.name {
t.Errorf("#%d got name %s, expected %s", i, name, test.name)
}
if !bytes.Equal(resp, []byte(test.responses[0])) {
t.Errorf("#%d got response %s, expected %s", i, resp, test.responses[0])
}
if err != nil {
t.Errorf("#%d error: %s", i, err)
}
for j := range test.challenges {
challenge := []byte(test.challenges[j])
expected := []byte(test.responses[j+1])
resp, err := test.auth.Next(challenge, true)
if err != nil && !test.shouldfail[j] {
t.Errorf("#%d error: %s", i, err)
continue testLoop
}
if !bytes.Equal(resp, expected) {
t.Errorf("#%d got %s, expected %s", i, resp, expected)
continue testLoop
}
}
}
}
func TestAuthLogin(t *testing.T) {
tests := []struct {
authName string
server *smtp.ServerInfo
err string
}{
{
authName: "servername",
server: &smtp.ServerInfo{Name: "servername", TLS: true},
},
{
// OK to use LoginAuth on localhost without TLS
authName: "localhost",
server: &smtp.ServerInfo{Name: "localhost", TLS: false},
},
{
// NOT OK on non-localhost, even if server says PLAIN is OK.
// (We don't know that the server is the real server.)
authName: "servername",
server: &smtp.ServerInfo{Name: "servername", Auth: []string{"PLAIN"}},
err: "unencrypted connection",
},
{
authName: "servername",
server: &smtp.ServerInfo{Name: "servername", Auth: []string{"CRAM-MD5"}},
err: "unencrypted connection",
},
{
authName: "servername",
server: &smtp.ServerInfo{Name: "attacker", TLS: true},
err: "wrong host name",
},
}
for i, tt := range tests {
auth := LoginAuth("foo", "bar", tt.authName)
_, _, err := auth.Start(tt.server)
got := ""
if err != nil {
got = err.Error()
}
if got != tt.err {
t.Errorf("%d. got error = %q; want %q", i, got, tt.err)
}
}
}

View file

@ -10,12 +10,11 @@ import (
"errors" "errors"
"fmt" "fmt"
"net" "net"
"net/smtp"
"os" "os"
"strings" "strings"
"time" "time"
"github.com/wneessen/go-mail/auth" "github.com/wneessen/go-mail/smtp"
) )
// Defaults // Defaults
@ -593,7 +592,7 @@ func (c *Client) auth() error {
if !strings.Contains(sat, string(SMTPAuthLogin)) { if !strings.Contains(sat, string(SMTPAuthLogin)) {
return ErrLoginAuthNotSupported return ErrLoginAuthNotSupported
} }
c.sa = auth.LoginAuth(c.user, c.pass, c.host) c.sa = smtp.LoginAuth(c.user, c.pass, c.host)
case SMTPAuthCramMD5: case SMTPAuthCramMD5:
if !strings.Contains(sat, string(SMTPAuthCramMD5)) { if !strings.Contains(sat, string(SMTPAuthCramMD5)) {
return ErrCramMD5AuthNotSupported return ErrCramMD5AuthNotSupported

View file

@ -9,14 +9,13 @@ import (
"crypto/tls" "crypto/tls"
"errors" "errors"
"fmt" "fmt"
"net/smtp"
"os" "os"
"strconv" "strconv"
"strings" "strings"
"testing" "testing"
"time" "time"
"github.com/wneessen/go-mail/auth" "github.com/wneessen/go-mail/smtp"
) )
// DefaultHost is used as default hostname for the Client // DefaultHost is used as default hostname for the Client
@ -496,7 +495,7 @@ func TestSetSMTPAuthCustom(t *testing.T) {
}{ }{
{"SMTPAuth: PLAIN", smtp.PlainAuth("", "", "", ""), "PLAIN", false}, {"SMTPAuth: PLAIN", smtp.PlainAuth("", "", "", ""), "PLAIN", false},
{"SMTPAuth: CRAM-MD5", smtp.CRAMMD5Auth("", ""), "CRAM-MD5", false}, {"SMTPAuth: CRAM-MD5", smtp.CRAMMD5Auth("", ""), "CRAM-MD5", false},
{"SMTPAuth: LOGIN", auth.LoginAuth("", "", ""), "LOGIN", false}, {"SMTPAuth: LOGIN", smtp.LoginAuth("", "", ""), "LOGIN", false},
} }
si := smtp.ServerInfo{TLS: true} si := smtp.ServerInfo{TLS: true}
for _, tt := range tests { for _, tt := range tests {
@ -584,7 +583,7 @@ func TestClient_DialWithContextInvalidAuth(t *testing.T) {
} }
c.user = "invalid" c.user = "invalid"
c.pass = "invalid" c.pass = "invalid"
c.SetSMTPAuthCustom(auth.LoginAuth("invalid", "invalid", "invalid")) c.SetSMTPAuthCustom(smtp.LoginAuth("invalid", "invalid", "invalid"))
ctx := context.Background() ctx := context.Background()
if err := c.DialWithContext(ctx); err == nil { if err := c.DialWithContext(ctx); err == nil {
t.Errorf("dial succeeded but was supposed to fail") t.Errorf("dial succeeded but was supposed to fail")

27
smtp/LICENSE Normal file
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.

44
smtp/auth.go Normal file
View file

@ -0,0 +1,44 @@
// SPDX-FileCopyrightText: Copyright 2010 The Go Authors. All rights reserved.
// SPDX-FileCopyrightText: Copyright (c) 2022-2023 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
// 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"
}

50
smtp/auth_cram_md5.go Normal file
View file

@ -0,0 +1,50 @@
// SPDX-FileCopyrightText: Copyright 2010 The Go Authors. All rights reserved.
// SPDX-FileCopyrightText: Copyright (c) 2022-2023 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
//go:build go1.19
// +build go1.19
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
}

52
smtp/auth_cram_md5_118.go Normal file
View file

@ -0,0 +1,52 @@
// SPDX-FileCopyrightText: Copyright 2010 The Go Authors. All rights reserved.
// SPDX-FileCopyrightText: Copyright (c) 2022-2023 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
//go:build !go1.19
// +build !go1.19
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
}
// Backport of: https://github.com/golang/go/commit/58158e990f272774e615c9abd8662bf0198c29aa#diff-772fc9f5d0c86f26e35158fb3e7a71a4967d18b4ec23a5dbb60781ab0babf426
// to guarantee backwards compatiblity with Go 1.16-1.18
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 []byte(fmt.Sprintf("%s %x", a.username, d.Sum(s))), nil
}
return nil, nil
}

View file

@ -1,16 +1,15 @@
// SPDX-FileCopyrightText: 2022 Winni Neessen <winni@neessen.dev> // SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Package auth implements the LOGIN and MD5-DIGEST smtp authentication mechanisms package smtp
package auth
import ( import (
"errors" "errors"
"fmt" "fmt"
"net/smtp"
) )
// loginAuth is the type that satisfies the Auth interface for the "SMTP LOGIN" auth
type loginAuth struct { type loginAuth struct {
username, password string username, password string
host string host string
@ -35,15 +34,11 @@ const (
// LoginAuth will only send the credentials if the connection is using TLS // LoginAuth will only send the credentials if the connection is using TLS
// or is connected to localhost. Otherwise authentication will fail with an // or is connected to localhost. Otherwise authentication will fail with an
// error, without sending the credentials. // error, without sending the credentials.
func LoginAuth(username, password, host string) smtp.Auth { func LoginAuth(username, password, host string) Auth {
return &loginAuth{username, password, host} return &loginAuth{username, password, host}
} }
func isLocalhost(name string) bool { func (a *loginAuth) Start(server *ServerInfo) (string, []byte, error) {
return name == "localhost" || name == "127.0.0.1" || name == "::1"
}
func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
// Must have TLS, or else localhost server. // Must have TLS, or else localhost server.
// Note: If TLS is not true, then we can't trust ANYTHING in ServerInfo. // 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. // In particular, it doesn't matter if the server advertises LOGIN auth.

60
smtp/auth_plain.go Normal file
View file

@ -0,0 +1,60 @@
// SPDX-FileCopyrightText: Copyright 2010 The Go Authors. All rights reserved.
// SPDX-FileCopyrightText: Copyright (c) 2022-2023 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"
)
// plainAuth is the type that satisfies the Auth interface for the "SMTP PLAIN" auth
type plainAuth struct {
identity, username, password string
host string
}
// 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) Auth {
return &plainAuth{identity, username, password, host}
}
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 !server.TLS && !isLocalhost(server.Name) {
return "", nil, errors.New("unencrypted connection")
}
if server.Name != a.host {
return "", nil, errors.New("wrong host name")
}
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, errors.New("unexpected server challenge")
}
return nil, nil
}

93
smtp/example_test.go Normal file
View file

@ -0,0 +1,93 @@
// SPDX-FileCopyrightText: Copyright 2010 The Go Authors. All rights reserved.
// SPDX-FileCopyrightText: Copyright (c) 2022-2023 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_test
import (
"fmt"
"log"
"github.com/wneessen/go-mail/smtp"
)
func Example() {
// Connect to the remote SMTP server.
c, err := smtp.Dial("mail.example.com:25")
if err != nil {
log.Fatal(err)
}
// Set the sender and recipient first
if err := c.Mail("sender@example.org"); err != nil {
log.Fatal(err)
}
if err := c.Rcpt("recipient@example.net"); err != nil {
log.Fatal(err)
}
// Send the email body.
wc, err := c.Data()
if err != nil {
log.Fatal(err)
}
_, err = fmt.Fprintf(wc, "This is the email body")
if err != nil {
log.Fatal(err)
}
err = wc.Close()
if err != nil {
log.Fatal(err)
}
// Send the QUIT command and close the connection.
err = c.Quit()
if err != nil {
log.Fatal(err)
}
}
// variables to make ExamplePlainAuth compile, without adding
// unnecessary noise there.
var (
from = "gopher@example.net"
msg = []byte("dummy message")
recipients = []string{"foo@example.com"}
)
func ExamplePlainAuth() {
// hostname is used by PlainAuth to validate the TLS certificate.
hostname := "mail.example.com"
auth := smtp.PlainAuth("", "user@example.com", "password", hostname)
err := smtp.SendMail(hostname+":25", auth, from, recipients, msg)
if err != nil {
log.Fatal(err)
}
}
func ExampleSendMail() {
// Set up authentication information.
auth := smtp.PlainAuth("", "user@example.com", "password", "mail.example.com")
// Connect to the server, authenticate, set the sender and recipient,
// and send the email all in one step.
to := []string{"recipient@example.net"}
msg := []byte("To: recipient@example.net\r\n" +
"Subject: discount Gophers!\r\n" +
"\r\n" +
"This is the email body.\r\n")
err := smtp.SendMail("mail.example.com:25", auth, "sender@example.org", to, msg)
if err != nil {
log.Fatal(err)
}
}

424
smtp/smtp.go Normal file
View file

@ -0,0 +1,424 @@
// SPDX-FileCopyrightText: Copyright 2010 The Go Authors. All rights reserved.
// SPDX-FileCopyrightText: Copyright (c) 2022-2023 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
//
// Additional extensions may be handled by clients.
//
// The smtp package is frozen and is not accepting new features.
// Some external packages provide more functionality. See:
//
// https://godoc.org/?q=smtp
package smtp
import (
"crypto/tls"
"encoding/base64"
"errors"
"fmt"
"io"
"net"
"net/textproto"
"strings"
)
// A Client represents a client connection to an SMTP server.
type Client struct {
// Text is the textproto.Conn used by the Client. It is exported to allow for
// clients to add extensions.
Text *textproto.Conn
// keep a reference to the connection so it can be used to create a TLS
// connection later
conn net.Conn
// whether the Client is using TLS
tls bool
serverName string
// map of supported extensions
ext map[string]string
// supported auth mechanisms
auth []string
localName string // the name to use in HELO/EHLO
didHello bool // whether we've said HELO/EHLO
helloError error // the error from the hello
}
// 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 {
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)
return c, nil
}
// Close closes the connection.
func (c *Client) Close() error {
return c.Text.Close()
}
// 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.localName = localName
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) {
id, err := c.Text.Cmd(format, args...)
if err != nil {
return 0, "", err
}
c.Text.StartResponse(id)
defer c.Text.EndResponse(id)
code, msg, err := c.Text.ReadResponse(expectCode)
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.ext = nil
_, _, 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.conn = tls.Client(c.conn, config)
c.Text = textproto.NewConn(c.conn)
c.tls = true
return c.ehlo()
}
// TLSConnectionState returns the client's TLS connection state.
// The return values are their zero values if StartTLS did
// not succeed.
func (c *Client) TLSConnectionState() (state tls.ConnectionState, ok bool) {
tc, ok := c.conn.(*tls.Conn)
if !ok {
return
}
return tc.ConnectionState(), true
}
// 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
}
encoding := base64.StdEncoding
mech, resp, err := a.Start(&ServerInfo{c.serverName, c.tls, c.auth})
if err != nil {
if qerr := c.Quit(); qerr != nil {
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, 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 {
// abort the AUTH
_, _, _ = 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, string(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 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>"
if c.ext != nil {
if _, ok := c.ext["8BITMIME"]; ok {
cmdStr += " BODY=8BITMIME"
}
if _, ok := c.ext["SMTPUTF8"]; ok {
cmdStr += " SMTPUTF8"
}
}
_, _, 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 Mail and may be followed by
// a Data call or another Rcpt call.
func (c *Client) Rcpt(to string) error {
if err := validateLine(to); err != nil {
return err
}
_, _, err := c.cmd(25, "RCPT TO:<%s>", to)
return err
}
type dataCloser struct {
c *Client
io.WriteCloser
}
func (d *dataCloser) Close() error {
_ = d.WriteCloser.Close()
_, _, err := d.c.Text.ReadResponse(250)
return err
}
// 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 Rcpt.
func (c *Client) Data() (io.WriteCloser, error) {
_, _, err := c.cmd(354, "DATA")
if err != nil {
return nil, err
}
return &dataCloser{c, c.Text.DotWriter()}, 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)
param, ok := c.ext[ext]
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 {
if err := c.hello(); err != nil {
return err
}
_, _, err := c.cmd(221, "QUIT")
if err != nil {
return err
}
return c.Text.Close()
}
// 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
smtp/smtp_ehlo.go Normal file
View file

@ -0,0 +1,42 @@
// SPDX-FileCopyrightText: Copyright 2010 The Go Authors. All rights reserved.
// SPDX-FileCopyrightText: Copyright (c) 2022-2023 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
//go:build go1.18
// +build go1.18
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
}
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
}

49
smtp/smtp_ehlo_117.go Normal file
View file

@ -0,0 +1,49 @@
// SPDX-FileCopyrightText: Copyright 2010 The Go Authors. All rights reserved.
// SPDX-FileCopyrightText: Copyright (c) 2022-2023 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
//go:build !go1.18
// +build !go1.18
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.
//
// Backport of: https://github.com/golang/go/commit/4d8db00641cc9ff4f44de7df9b8c4f4a4f9416ee#diff-4f6f6bdb9891d4dd271f9f31430420a2e44018fe4ee539576faf458bebb3cee4
// to guarantee backwards compatiblity with Go 1.16/1.17:w
func (c *Client) ehlo() error {
_, msg, err := c.cmd(250, "EHLO %s", c.localName)
if err != nil {
return err
}
ext := make(map[string]string)
extList := strings.Split(msg, "\n")
if len(extList) > 1 {
extList = extList[1:]
for _, line := range extList {
args := strings.SplitN(line, " ", 2)
if len(args) > 1 {
ext[args[0]] = args[1]
} else {
ext[args[0]] = ""
}
}
}
if mechs, ok := ext["AUTH"]; ok {
c.auth = strings.Split(mechs, " ")
}
c.ext = ext
return err
}

1296
smtp/smtp_test.go Normal file

File diff suppressed because it is too large Load diff