From 0d6777ab39877f07519758f4c328bbc0e95bc507 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 10 Jan 2023 00:38:42 +0100 Subject: [PATCH 1/9] Fork net/smpt into go-mail As part of #97 we are going to fork the official `net/smtp` package into go-mail to provide us with more flexibility. This commit fulfills the first big step of importing the package into smtp/. Also go-mail's own LoginAuth has been moved from auth/ into smtp/ to be consistent with the stdlib. There are still a couple of open issues (i. e. license adjustments and making golangci-lint happy) but so far all tests already work, which is a good start. --- auth/auth_test.go | 104 --- client.go | 5 +- client_test.go | 7 +- smtp/auth.go | 31 + smtp/auth_cram_md5.go | 34 + auth/login.go => smtp/auth_login.go | 12 +- smtp/auth_plain.go | 47 ++ smtp/example_test.go | 84 ++ smtp/smtp.go | 438 ++++++++++ smtp/smtp_test.go | 1194 +++++++++++++++++++++++++++ 10 files changed, 1837 insertions(+), 119 deletions(-) delete mode 100644 auth/auth_test.go create mode 100644 smtp/auth.go create mode 100644 smtp/auth_cram_md5.go rename auth/login.go => smtp/auth_login.go (88%) create mode 100644 smtp/auth_plain.go create mode 100644 smtp/example_test.go create mode 100644 smtp/smtp.go create mode 100644 smtp/smtp_test.go diff --git a/auth/auth_test.go b/auth/auth_test.go deleted file mode 100644 index 4df16ce..0000000 --- a/auth/auth_test.go +++ /dev/null @@ -1,104 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Winni Neessen -// -// 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) - } - } -} diff --git a/client.go b/client.go index fd0e7f7..2bee9f2 100644 --- a/client.go +++ b/client.go @@ -10,12 +10,11 @@ import ( "errors" "fmt" "net" - "net/smtp" "os" "strings" "time" - "github.com/wneessen/go-mail/auth" + "github.com/wneessen/go-mail/smtp" ) // Defaults @@ -593,7 +592,7 @@ func (c *Client) auth() error { if !strings.Contains(sat, string(SMTPAuthLogin)) { return ErrLoginAuthNotSupported } - c.sa = auth.LoginAuth(c.user, c.pass, c.host) + c.sa = smtp.LoginAuth(c.user, c.pass, c.host) case SMTPAuthCramMD5: if !strings.Contains(sat, string(SMTPAuthCramMD5)) { return ErrCramMD5AuthNotSupported diff --git a/client_test.go b/client_test.go index b3bed52..c85a91c 100644 --- a/client_test.go +++ b/client_test.go @@ -9,14 +9,13 @@ import ( "crypto/tls" "errors" "fmt" - "net/smtp" "os" "strconv" "strings" "testing" "time" - "github.com/wneessen/go-mail/auth" + "github.com/wneessen/go-mail/smtp" ) // 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: CRAM-MD5", smtp.CRAMMD5Auth("", ""), "CRAM-MD5", false}, - {"SMTPAuth: LOGIN", auth.LoginAuth("", "", ""), "LOGIN", false}, + {"SMTPAuth: LOGIN", smtp.LoginAuth("", "", ""), "LOGIN", false}, } si := smtp.ServerInfo{TLS: true} for _, tt := range tests { @@ -584,7 +583,7 @@ func TestClient_DialWithContextInvalidAuth(t *testing.T) { } c.user = "invalid" c.pass = "invalid" - c.SetSMTPAuthCustom(auth.LoginAuth("invalid", "invalid", "invalid")) + c.SetSMTPAuthCustom(smtp.LoginAuth("invalid", "invalid", "invalid")) ctx := context.Background() if err := c.DialWithContext(ctx); err == nil { t.Errorf("dial succeeded but was supposed to fail") diff --git a/smtp/auth.go b/smtp/auth.go new file mode 100644 index 0000000..70cf7aa --- /dev/null +++ b/smtp/auth.go @@ -0,0 +1,31 @@ +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" +} diff --git a/smtp/auth_cram_md5.go b/smtp/auth_cram_md5.go new file mode 100644 index 0000000..2f91289 --- /dev/null +++ b/smtp/auth_cram_md5.go @@ -0,0 +1,34 @@ +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(server *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 +} diff --git a/auth/login.go b/smtp/auth_login.go similarity index 88% rename from auth/login.go rename to smtp/auth_login.go index 51f9179..6f92bab 100644 --- a/auth/login.go +++ b/smtp/auth_login.go @@ -3,14 +3,14 @@ // SPDX-License-Identifier: MIT // Package auth implements the LOGIN and MD5-DIGEST smtp authentication mechanisms -package auth +package smtp import ( "errors" "fmt" - "net/smtp" ) +// loginAuth is the type that satisfies the Auth interface for the "SMTP LOGIN" auth type loginAuth struct { username, password string host string @@ -35,15 +35,11 @@ const ( // 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) smtp.Auth { +func LoginAuth(username, password, host string) Auth { return &loginAuth{username, password, host} } -func isLocalhost(name string) bool { - return name == "localhost" || name == "127.0.0.1" || name == "::1" -} - -func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) { +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. diff --git a/smtp/auth_plain.go b/smtp/auth_plain.go new file mode 100644 index 0000000..035699b --- /dev/null +++ b/smtp/auth_plain.go @@ -0,0 +1,47 @@ +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(fromServer []byte, more bool) ([]byte, error) { + if more { + // We've already sent everything. + return nil, errors.New("unexpected server challenge") + } + return nil, nil +} diff --git a/smtp/example_test.go b/smtp/example_test.go new file mode 100644 index 0000000..452f085 --- /dev/null +++ b/smtp/example_test.go @@ -0,0 +1,84 @@ +// Copyright 2013 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +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) + } +} diff --git a/smtp/smtp.go b/smtp/smtp.go new file mode 100644 index 0000000..1fef69c --- /dev/null +++ b/smtp/smtp.go @@ -0,0 +1,438 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// 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 +} + +// 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 +} + +// 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 +} diff --git a/smtp/smtp_test.go b/smtp/smtp_test.go new file mode 100644 index 0000000..90b5306 --- /dev/null +++ b/smtp/smtp_test.go @@ -0,0 +1,1194 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package smtp + +import ( + "bufio" + "bytes" + "crypto/tls" + "crypto/x509" + "fmt" + "io" + "net" + "net/textproto" + "strings" + "testing" + "time" +) + +type authTest struct { + auth Auth + challenges []string + name string + responses []string +} + +var authTests = []authTest{ + {PlainAuth("", "user", "pass", "testserver"), []string{}, "PLAIN", []string{"\x00user\x00pass"}}, + {PlainAuth("foo", "bar", "baz", "testserver"), []string{}, "PLAIN", []string{"foo\x00bar\x00baz"}}, + {CRAMMD5Auth("user", "pass"), []string{"<123456.1322876914@testserver>"}, "CRAM-MD5", []string{"", "user 287eb355114cf5c471c26a875f1ca4ae"}}, + { + LoginAuth("user", "pass", "testserver"), + []string{"Username:", "Password:", "Invalid:"}, + "LOGIN", + []string{"", "user", "pass", ""}, + }, +} + +func TestAuth(t *testing.T) { +testLoop: + for i, test := range authTests { + name, resp, err := test.auth.Start(&ServerInfo{"testserver", true, 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 { + 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 TestAuthPlain(t *testing.T) { + tests := []struct { + authName string + server *ServerInfo + err string + }{ + { + authName: "servername", + server: &ServerInfo{Name: "servername", TLS: true}, + }, + { + // OK to use PlainAuth on localhost without TLS + authName: "localhost", + server: &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: &ServerInfo{Name: "servername", Auth: []string{"PLAIN"}}, + err: "unencrypted connection", + }, + { + authName: "servername", + server: &ServerInfo{Name: "servername", Auth: []string{"CRAM-MD5"}}, + err: "unencrypted connection", + }, + { + authName: "servername", + server: &ServerInfo{Name: "attacker", TLS: true}, + err: "wrong host name", + }, + } + for i, tt := range tests { + auth := PlainAuth("foo", "bar", "baz", 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) + } + } +} + +func TestAuthLogin(t *testing.T) { + tests := []struct { + authName string + server *ServerInfo + err string + }{ + { + authName: "servername", + server: &ServerInfo{Name: "servername", TLS: true}, + }, + { + // OK to use LoginAuth on localhost without TLS + authName: "localhost", + server: &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: &ServerInfo{Name: "servername", Auth: []string{"PLAIN"}}, + err: "unencrypted connection", + }, + { + authName: "servername", + server: &ServerInfo{Name: "servername", Auth: []string{"CRAM-MD5"}}, + err: "unencrypted connection", + }, + { + authName: "servername", + server: &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) + } + } +} + +// Issue 17794: don't send a trailing space on AUTH command when there's no password. +func TestClientAuthTrimSpace(t *testing.T) { + server := "220 hello world\r\n" + + "200 some more" + var wrote strings.Builder + var fake faker + fake.ReadWriter = struct { + io.Reader + io.Writer + }{ + strings.NewReader(server), + &wrote, + } + c, err := NewClient(fake, "fake.host") + if err != nil { + t.Fatalf("NewClient: %v", err) + } + c.tls = true + c.didHello = true + c.Auth(toServerEmptyAuth{}) + c.Close() + if got, want := wrote.String(), "AUTH FOOAUTH\r\n*\r\nQUIT\r\n"; got != want { + t.Errorf("wrote %q; want %q", got, want) + } +} + +// toServerEmptyAuth is an implementation of Auth that only implements +// the Start method, and returns "FOOAUTH", nil, nil. Notably, it returns +// zero bytes for "toServer" so we can test that we don't send spaces at +// the end of the line. See TestClientAuthTrimSpace. +type toServerEmptyAuth struct{} + +func (toServerEmptyAuth) Start(server *ServerInfo) (proto string, toServer []byte, err error) { + return "FOOAUTH", nil, nil +} + +func (toServerEmptyAuth) Next(fromServer []byte, more bool) (toServer []byte, err error) { + panic("unexpected call") +} + +type faker struct { + io.ReadWriter +} + +func (f faker) Close() error { return nil } +func (f faker) LocalAddr() net.Addr { return nil } +func (f faker) RemoteAddr() net.Addr { return nil } +func (f faker) SetDeadline(time.Time) error { return nil } +func (f faker) SetReadDeadline(time.Time) error { return nil } +func (f faker) SetWriteDeadline(time.Time) error { return nil } + +func TestBasic(t *testing.T) { + server := strings.Join(strings.Split(basicServer, "\n"), "\r\n") + client := strings.Join(strings.Split(basicClient, "\n"), "\r\n") + + var cmdbuf strings.Builder + bcmdbuf := bufio.NewWriter(&cmdbuf) + var fake faker + fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(server)), bcmdbuf) + c := &Client{Text: textproto.NewConn(fake), localName: "localhost"} + + if err := c.helo(); err != nil { + t.Fatalf("HELO failed: %s", err) + } + if err := c.ehlo(); err == nil { + t.Fatalf("Expected first EHLO to fail") + } + if err := c.ehlo(); err != nil { + t.Fatalf("Second EHLO failed: %s", err) + } + + c.didHello = true + if ok, args := c.Extension("aUtH"); !ok || args != "LOGIN PLAIN" { + t.Fatalf("Expected AUTH supported") + } + if ok, _ := c.Extension("DSN"); ok { + t.Fatalf("Shouldn't support DSN") + } + + if err := c.Mail("user@gmail.com"); err == nil { + t.Fatalf("MAIL should require authentication") + } + + if err := c.Verify("user1@gmail.com"); err == nil { + t.Fatalf("First VRFY: expected no verification") + } + if err := c.Verify("user2@gmail.com>\r\nDATA\r\nAnother injected message body\r\n.\r\nQUIT\r\n"); err == nil { + t.Fatalf("VRFY should have failed due to a message injection attempt") + } + if err := c.Verify("user2@gmail.com"); err != nil { + t.Fatalf("Second VRFY: expected verification, got %s", err) + } + + // fake TLS so authentication won't complain + c.tls = true + c.serverName = "smtp.google.com" + if err := c.Auth(PlainAuth("", "user", "pass", "smtp.google.com")); err != nil { + t.Fatalf("AUTH failed: %s", err) + } + + if err := c.Rcpt("golang-nuts@googlegroups.com>\r\nDATA\r\nInjected message body\r\n.\r\nQUIT\r\n"); err == nil { + t.Fatalf("RCPT should have failed due to a message injection attempt") + } + if err := c.Mail("user@gmail.com>\r\nDATA\r\nAnother injected message body\r\n.\r\nQUIT\r\n"); err == nil { + t.Fatalf("MAIL should have failed due to a message injection attempt") + } + if err := c.Mail("user@gmail.com"); err != nil { + t.Fatalf("MAIL failed: %s", err) + } + if err := c.Rcpt("golang-nuts@googlegroups.com"); err != nil { + t.Fatalf("RCPT failed: %s", err) + } + msg := `From: user@gmail.com +To: golang-nuts@googlegroups.com +Subject: Hooray for Go + +Line 1 +.Leading dot line . +Goodbye.` + w, err := c.Data() + if err != nil { + t.Fatalf("DATA failed: %s", err) + } + if _, err := w.Write([]byte(msg)); err != nil { + t.Fatalf("Data write failed: %s", err) + } + if err := w.Close(); err != nil { + t.Fatalf("Bad data response: %s", err) + } + + if err := c.Quit(); err != nil { + t.Fatalf("QUIT failed: %s", err) + } + + bcmdbuf.Flush() + actualcmds := cmdbuf.String() + if client != actualcmds { + t.Fatalf("Got:\n%s\nExpected:\n%s", actualcmds, client) + } +} + +var basicServer = `250 mx.google.com at your service +502 Unrecognized command. +250-mx.google.com at your service +250-SIZE 35651584 +250-AUTH LOGIN PLAIN +250 8BITMIME +530 Authentication required +252 Send some mail, I'll try my best +250 User is valid +235 Accepted +250 Sender OK +250 Receiver OK +354 Go ahead +250 Data OK +221 OK +` + +var basicClient = `HELO localhost +EHLO localhost +EHLO localhost +MAIL FROM: BODY=8BITMIME +VRFY user1@gmail.com +VRFY user2@gmail.com +AUTH PLAIN AHVzZXIAcGFzcw== +MAIL FROM: BODY=8BITMIME +RCPT TO: +DATA +From: user@gmail.com +To: golang-nuts@googlegroups.com +Subject: Hooray for Go + +Line 1 +..Leading dot line . +Goodbye. +. +QUIT +` + +func TestExtensions(t *testing.T) { + fake := func(server string) (c *Client, bcmdbuf *bufio.Writer, cmdbuf *strings.Builder) { + server = strings.Join(strings.Split(server, "\n"), "\r\n") + + cmdbuf = &strings.Builder{} + bcmdbuf = bufio.NewWriter(cmdbuf) + var fake faker + fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(server)), bcmdbuf) + c = &Client{Text: textproto.NewConn(fake), localName: "localhost"} + + return c, bcmdbuf, cmdbuf + } + + t.Run("helo", func(t *testing.T) { + const ( + basicServer = `250 mx.google.com at your service +250 Sender OK +221 Goodbye +` + + basicClient = `HELO localhost +MAIL FROM: +QUIT +` + ) + + c, bcmdbuf, cmdbuf := fake(basicServer) + + if err := c.helo(); err != nil { + t.Fatalf("HELO failed: %s", err) + } + c.didHello = true + if err := c.Mail("user@gmail.com"); err != nil { + t.Fatalf("MAIL FROM failed: %s", err) + } + if err := c.Quit(); err != nil { + t.Fatalf("QUIT failed: %s", err) + } + + bcmdbuf.Flush() + actualcmds := cmdbuf.String() + client := strings.Join(strings.Split(basicClient, "\n"), "\r\n") + if client != actualcmds { + t.Fatalf("Got:\n%s\nExpected:\n%s", actualcmds, client) + } + }) + + t.Run("ehlo", func(t *testing.T) { + const ( + basicServer = `250-mx.google.com at your service +250 SIZE 35651584 +250 Sender OK +221 Goodbye +` + + basicClient = `EHLO localhost +MAIL FROM: +QUIT +` + ) + + c, bcmdbuf, cmdbuf := fake(basicServer) + + if err := c.Hello("localhost"); err != nil { + t.Fatalf("EHLO failed: %s", err) + } + if ok, _ := c.Extension("8BITMIME"); ok { + t.Fatalf("Shouldn't support 8BITMIME") + } + if ok, _ := c.Extension("SMTPUTF8"); ok { + t.Fatalf("Shouldn't support SMTPUTF8") + } + if err := c.Mail("user@gmail.com"); err != nil { + t.Fatalf("MAIL FROM failed: %s", err) + } + if err := c.Quit(); err != nil { + t.Fatalf("QUIT failed: %s", err) + } + + bcmdbuf.Flush() + actualcmds := cmdbuf.String() + client := strings.Join(strings.Split(basicClient, "\n"), "\r\n") + if client != actualcmds { + t.Fatalf("Got:\n%s\nExpected:\n%s", actualcmds, client) + } + }) + + t.Run("ehlo 8bitmime", func(t *testing.T) { + const ( + basicServer = `250-mx.google.com at your service +250-SIZE 35651584 +250 8BITMIME +250 Sender OK +221 Goodbye +` + + basicClient = `EHLO localhost +MAIL FROM: BODY=8BITMIME +QUIT +` + ) + + c, bcmdbuf, cmdbuf := fake(basicServer) + + if err := c.Hello("localhost"); err != nil { + t.Fatalf("EHLO failed: %s", err) + } + if ok, _ := c.Extension("8BITMIME"); !ok { + t.Fatalf("Should support 8BITMIME") + } + if ok, _ := c.Extension("SMTPUTF8"); ok { + t.Fatalf("Shouldn't support SMTPUTF8") + } + if err := c.Mail("user@gmail.com"); err != nil { + t.Fatalf("MAIL FROM failed: %s", err) + } + if err := c.Quit(); err != nil { + t.Fatalf("QUIT failed: %s", err) + } + + bcmdbuf.Flush() + actualcmds := cmdbuf.String() + client := strings.Join(strings.Split(basicClient, "\n"), "\r\n") + if client != actualcmds { + t.Fatalf("Got:\n%s\nExpected:\n%s", actualcmds, client) + } + }) + + t.Run("ehlo smtputf8", func(t *testing.T) { + const ( + basicServer = `250-mx.google.com at your service +250-SIZE 35651584 +250 SMTPUTF8 +250 Sender OK +221 Goodbye +` + + basicClient = `EHLO localhost +MAIL FROM: SMTPUTF8 +QUIT +` + ) + + c, bcmdbuf, cmdbuf := fake(basicServer) + + if err := c.Hello("localhost"); err != nil { + t.Fatalf("EHLO failed: %s", err) + } + if ok, _ := c.Extension("8BITMIME"); ok { + t.Fatalf("Shouldn't support 8BITMIME") + } + if ok, _ := c.Extension("SMTPUTF8"); !ok { + t.Fatalf("Should support SMTPUTF8") + } + if err := c.Mail("user+📧@gmail.com"); err != nil { + t.Fatalf("MAIL FROM failed: %s", err) + } + if err := c.Quit(); err != nil { + t.Fatalf("QUIT failed: %s", err) + } + + bcmdbuf.Flush() + actualcmds := cmdbuf.String() + client := strings.Join(strings.Split(basicClient, "\n"), "\r\n") + if client != actualcmds { + t.Fatalf("Got:\n%s\nExpected:\n%s", actualcmds, client) + } + }) + + t.Run("ehlo 8bitmime smtputf8", func(t *testing.T) { + const ( + basicServer = `250-mx.google.com at your service +250-SIZE 35651584 +250-8BITMIME +250 SMTPUTF8 +250 Sender OK +221 Goodbye + ` + + basicClient = `EHLO localhost +MAIL FROM: BODY=8BITMIME SMTPUTF8 +QUIT +` + ) + + c, bcmdbuf, cmdbuf := fake(basicServer) + + if err := c.Hello("localhost"); err != nil { + t.Fatalf("EHLO failed: %s", err) + } + c.didHello = true + if ok, _ := c.Extension("8BITMIME"); !ok { + t.Fatalf("Should support 8BITMIME") + } + if ok, _ := c.Extension("SMTPUTF8"); !ok { + t.Fatalf("Should support SMTPUTF8") + } + if err := c.Mail("user+📧@gmail.com"); err != nil { + t.Fatalf("MAIL FROM failed: %s", err) + } + if err := c.Quit(); err != nil { + t.Fatalf("QUIT failed: %s", err) + } + + bcmdbuf.Flush() + actualcmds := cmdbuf.String() + client := strings.Join(strings.Split(basicClient, "\n"), "\r\n") + if client != actualcmds { + t.Fatalf("Got:\n%s\nExpected:\n%s", actualcmds, client) + } + }) +} + +func TestNewClient(t *testing.T) { + server := strings.Join(strings.Split(newClientServer, "\n"), "\r\n") + client := strings.Join(strings.Split(newClientClient, "\n"), "\r\n") + + var cmdbuf strings.Builder + bcmdbuf := bufio.NewWriter(&cmdbuf) + out := func() string { + bcmdbuf.Flush() + return cmdbuf.String() + } + var fake faker + fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(server)), bcmdbuf) + c, err := NewClient(fake, "fake.host") + if err != nil { + t.Fatalf("NewClient: %v\n(after %v)", err, out()) + } + defer c.Close() + if ok, args := c.Extension("aUtH"); !ok || args != "LOGIN PLAIN" { + t.Fatalf("Expected AUTH supported") + } + if ok, _ := c.Extension("DSN"); ok { + t.Fatalf("Shouldn't support DSN") + } + if err := c.Quit(); err != nil { + t.Fatalf("QUIT failed: %s", err) + } + + actualcmds := out() + if client != actualcmds { + t.Fatalf("Got:\n%s\nExpected:\n%s", actualcmds, client) + } +} + +var newClientServer = `220 hello world +250-mx.google.com at your service +250-SIZE 35651584 +250-AUTH LOGIN PLAIN +250 8BITMIME +221 OK +` + +var newClientClient = `EHLO localhost +QUIT +` + +func TestNewClient2(t *testing.T) { + server := strings.Join(strings.Split(newClient2Server, "\n"), "\r\n") + client := strings.Join(strings.Split(newClient2Client, "\n"), "\r\n") + + var cmdbuf strings.Builder + bcmdbuf := bufio.NewWriter(&cmdbuf) + var fake faker + fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(server)), bcmdbuf) + c, err := NewClient(fake, "fake.host") + if err != nil { + t.Fatalf("NewClient: %v", err) + } + defer c.Close() + if ok, _ := c.Extension("DSN"); ok { + t.Fatalf("Shouldn't support DSN") + } + if err := c.Quit(); err != nil { + t.Fatalf("QUIT failed: %s", err) + } + + bcmdbuf.Flush() + actualcmds := cmdbuf.String() + if client != actualcmds { + t.Fatalf("Got:\n%s\nExpected:\n%s", actualcmds, client) + } +} + +var newClient2Server = `220 hello world +502 EH? +250-mx.google.com at your service +250-SIZE 35651584 +250-AUTH LOGIN PLAIN +250 8BITMIME +221 OK +` + +var newClient2Client = `EHLO localhost +HELO localhost +QUIT +` + +func TestNewClientWithTLS(t *testing.T) { + cert, err := tls.X509KeyPair(localhostCert, localhostKey) + if err != nil { + t.Fatalf("loadcert: %v", err) + } + + config := tls.Config{Certificates: []tls.Certificate{cert}} + + ln, err := tls.Listen("tcp", "127.0.0.1:0", &config) + if err != nil { + ln, err = tls.Listen("tcp", "[::1]:0", &config) + if err != nil { + t.Fatalf("server: listen: %v", err) + } + } + + go func() { + conn, err := ln.Accept() + if err != nil { + t.Errorf("server: accept: %v", err) + return + } + defer conn.Close() + + _, err = conn.Write([]byte("220 SIGNS\r\n")) + if err != nil { + t.Errorf("server: write: %v", err) + return + } + }() + + config.InsecureSkipVerify = true + conn, err := tls.Dial("tcp", ln.Addr().String(), &config) + if err != nil { + t.Fatalf("client: dial: %v", err) + } + defer conn.Close() + + client, err := NewClient(conn, ln.Addr().String()) + if err != nil { + t.Fatalf("smtp: newclient: %v", err) + } + if !client.tls { + t.Errorf("client.tls Got: %t Expected: %t", client.tls, true) + } +} + +func TestHello(t *testing.T) { + if len(helloServer) != len(helloClient) { + t.Fatalf("Hello server and client size mismatch") + } + + for i := 0; i < len(helloServer); i++ { + server := strings.Join(strings.Split(baseHelloServer+helloServer[i], "\n"), "\r\n") + client := strings.Join(strings.Split(baseHelloClient+helloClient[i], "\n"), "\r\n") + var cmdbuf strings.Builder + bcmdbuf := bufio.NewWriter(&cmdbuf) + var fake faker + fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(server)), bcmdbuf) + c, err := NewClient(fake, "fake.host") + if err != nil { + t.Fatalf("NewClient: %v", err) + } + defer c.Close() + c.localName = "customhost" + err = nil + + switch i { + case 0: + err = c.Hello("hostinjection>\n\rDATA\r\nInjected message body\r\n.\r\nQUIT\r\n") + if err == nil { + t.Errorf("Expected Hello to be rejected due to a message injection attempt") + } + err = c.Hello("customhost") + case 1: + err = c.StartTLS(nil) + if err.Error() == "502 Not implemented" { + err = nil + } + case 2: + err = c.Verify("test@example.com") + case 3: + c.tls = true + c.serverName = "smtp.google.com" + err = c.Auth(PlainAuth("", "user", "pass", "smtp.google.com")) + case 4: + err = c.Mail("test@example.com") + case 5: + ok, _ := c.Extension("feature") + if ok { + t.Errorf("Expected FEATURE not to be supported") + } + case 6: + err = c.Reset() + case 7: + err = c.Quit() + case 8: + err = c.Verify("test@example.com") + if err != nil { + err = c.Hello("customhost") + if err != nil { + t.Errorf("Want error, got none") + } + } + case 9: + err = c.Noop() + default: + t.Fatalf("Unhandled command") + } + + if err != nil { + t.Errorf("Command %d failed: %v", i, err) + } + + bcmdbuf.Flush() + actualcmds := cmdbuf.String() + if client != actualcmds { + t.Errorf("Got:\n%s\nExpected:\n%s", actualcmds, client) + } + } +} + +var baseHelloServer = `220 hello world +502 EH? +250-mx.google.com at your service +250 FEATURE +` + +var helloServer = []string{ + "", + "502 Not implemented\n", + "250 User is valid\n", + "235 Accepted\n", + "250 Sender ok\n", + "", + "250 Reset ok\n", + "221 Goodbye\n", + "250 Sender ok\n", + "250 ok\n", +} + +var baseHelloClient = `EHLO customhost +HELO customhost +` + +var helloClient = []string{ + "", + "STARTTLS\n", + "VRFY test@example.com\n", + "AUTH PLAIN AHVzZXIAcGFzcw==\n", + "MAIL FROM:\n", + "", + "RSET\n", + "QUIT\n", + "VRFY test@example.com\n", + "NOOP\n", +} + +func TestSendMail(t *testing.T) { + server := strings.Join(strings.Split(sendMailServer, "\n"), "\r\n") + client := strings.Join(strings.Split(sendMailClient, "\n"), "\r\n") + var cmdbuf strings.Builder + bcmdbuf := bufio.NewWriter(&cmdbuf) + l, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("Unable to create listener: %v", err) + } + defer l.Close() + + // prevent data race on bcmdbuf + done := make(chan struct{}) + go func(data []string) { + defer close(done) + + conn, err := l.Accept() + if err != nil { + t.Errorf("Accept error: %v", err) + return + } + defer conn.Close() + + tc := textproto.NewConn(conn) + for i := 0; i < len(data) && data[i] != ""; i++ { + tc.PrintfLine(data[i]) + for len(data[i]) >= 4 && data[i][3] == '-' { + i++ + tc.PrintfLine(data[i]) + } + if data[i] == "221 Goodbye" { + return + } + read := false + for !read || data[i] == "354 Go ahead" { + msg, err := tc.ReadLine() + bcmdbuf.Write([]byte(msg + "\r\n")) + read = true + if err != nil { + t.Errorf("Read error: %v", err) + return + } + if data[i] == "354 Go ahead" && msg == "." { + break + } + } + } + }(strings.Split(server, "\r\n")) + + err = SendMail(l.Addr().String(), nil, "test@example.com", []string{"other@example.com>\n\rDATA\r\nInjected message body\r\n.\r\nQUIT\r\n"}, []byte(strings.Replace(`From: test@example.com +To: other@example.com +Subject: SendMail test + +SendMail is working for me. +`, "\n", "\r\n", -1))) + if err == nil { + t.Errorf("Expected SendMail to be rejected due to a message injection attempt") + } + + err = SendMail(l.Addr().String(), nil, "test@example.com", []string{"other@example.com"}, []byte(strings.Replace(`From: test@example.com +To: other@example.com +Subject: SendMail test + +SendMail is working for me. +`, "\n", "\r\n", -1))) + + if err != nil { + t.Errorf("%v", err) + } + + <-done + bcmdbuf.Flush() + actualcmds := cmdbuf.String() + if client != actualcmds { + t.Errorf("Got:\n%s\nExpected:\n%s", actualcmds, client) + } +} + +var sendMailServer = `220 hello world +502 EH? +250 mx.google.com at your service +250 Sender ok +250 Receiver ok +354 Go ahead +250 Data ok +221 Goodbye +` + +var sendMailClient = `EHLO localhost +HELO localhost +MAIL FROM: +RCPT TO: +DATA +From: test@example.com +To: other@example.com +Subject: SendMail test + +SendMail is working for me. +. +QUIT +` + +func TestSendMailWithAuth(t *testing.T) { + l, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("Unable to create listener: %v", err) + } + defer l.Close() + + errCh := make(chan error) + go func() { + defer close(errCh) + conn, err := l.Accept() + if err != nil { + errCh <- fmt.Errorf("Accept: %v", err) + return + } + defer conn.Close() + + tc := textproto.NewConn(conn) + tc.PrintfLine("220 hello world") + msg, err := tc.ReadLine() + if err != nil { + errCh <- fmt.Errorf("ReadLine error: %v", err) + return + } + const wantMsg = "EHLO localhost" + if msg != wantMsg { + errCh <- fmt.Errorf("unexpected response %q; want %q", msg, wantMsg) + return + } + err = tc.PrintfLine("250 mx.google.com at your service") + if err != nil { + errCh <- fmt.Errorf("PrintfLine: %v", err) + return + } + }() + + err = SendMail(l.Addr().String(), PlainAuth("", "user", "pass", "smtp.google.com"), "test@example.com", []string{"other@example.com"}, []byte(strings.Replace(`From: test@example.com +To: other@example.com +Subject: SendMail test + +SendMail is working for me. +`, "\n", "\r\n", -1))) + if err == nil { + t.Error("SendMail: Server doesn't support AUTH, expected to get an error, but got none ") + } + if err.Error() != "smtp: server doesn't support AUTH" { + t.Errorf("Expected: smtp: server doesn't support AUTH, got: %s", err) + } + err = <-errCh + if err != nil { + t.Fatalf("server error: %v", err) + } +} + +func TestAuthFailed(t *testing.T) { + server := strings.Join(strings.Split(authFailedServer, "\n"), "\r\n") + client := strings.Join(strings.Split(authFailedClient, "\n"), "\r\n") + var cmdbuf strings.Builder + bcmdbuf := bufio.NewWriter(&cmdbuf) + var fake faker + fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(server)), bcmdbuf) + c, err := NewClient(fake, "fake.host") + if err != nil { + t.Fatalf("NewClient: %v", err) + } + defer c.Close() + + c.tls = true + c.serverName = "smtp.google.com" + err = c.Auth(PlainAuth("", "user", "pass", "smtp.google.com")) + + if err == nil { + t.Error("Auth: expected error; got none") + } else if err.Error() != "535 Invalid credentials\nplease see www.example.com" { + t.Errorf("Auth: got error: %v, want: %s", err, "535 Invalid credentials\nplease see www.example.com") + } + + bcmdbuf.Flush() + actualcmds := cmdbuf.String() + if client != actualcmds { + t.Errorf("Got:\n%s\nExpected:\n%s", actualcmds, client) + } +} + +var authFailedServer = `220 hello world +250-mx.google.com at your service +250 AUTH LOGIN PLAIN +535-Invalid credentials +535 please see www.example.com +221 Goodbye +` + +var authFailedClient = `EHLO localhost +AUTH PLAIN AHVzZXIAcGFzcw== +* +QUIT +` + +func TestTLSClient(t *testing.T) { + /* + TODO: Check if we need this + if runtime.GOOS == "freebsd" || runtime.GOOS == "js" { + testenv.SkipFlaky(t, 19229) + } + */ + ln := newLocalListener(t) + defer ln.Close() + errc := make(chan error) + go func() { + errc <- sendMail(ln.Addr().String()) + }() + conn, err := ln.Accept() + if err != nil { + t.Fatalf("failed to accept connection: %v", err) + } + defer conn.Close() + if err := serverHandle(conn, t); err != nil { + t.Fatalf("failed to handle connection: %v", err) + } + if err := <-errc; err != nil { + t.Fatalf("client error: %v", err) + } +} + +func TestTLSConnState(t *testing.T) { + ln := newLocalListener(t) + defer ln.Close() + clientDone := make(chan bool) + serverDone := make(chan bool) + go func() { + defer close(serverDone) + c, err := ln.Accept() + if err != nil { + t.Errorf("Server accept: %v", err) + return + } + defer c.Close() + if err := serverHandle(c, t); err != nil { + t.Errorf("server error: %v", err) + } + }() + go func() { + defer close(clientDone) + c, err := Dial(ln.Addr().String()) + if err != nil { + t.Errorf("Client dial: %v", err) + return + } + defer c.Quit() + cfg := &tls.Config{ServerName: "example.com"} + testHookStartTLS(cfg) // set the RootCAs + if err := c.StartTLS(cfg); err != nil { + t.Errorf("StartTLS: %v", err) + return + } + cs, ok := c.TLSConnectionState() + if !ok { + t.Errorf("TLSConnectionState returned ok == false; want true") + return + } + if cs.Version == 0 || !cs.HandshakeComplete { + t.Errorf("ConnectionState = %#v; expect non-zero Version and HandshakeComplete", cs) + } + }() + <-clientDone + <-serverDone +} + +func newLocalListener(t *testing.T) net.Listener { + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + ln, err = net.Listen("tcp6", "[::1]:0") + } + if err != nil { + t.Fatal(err) + } + return ln +} + +type smtpSender struct { + w io.Writer +} + +func (s smtpSender) send(f string) { + s.w.Write([]byte(f + "\r\n")) +} + +// smtp server, finely tailored to deal with our own client only! +func serverHandle(c net.Conn, t *testing.T) error { + send := smtpSender{c}.send + send("220 127.0.0.1 ESMTP service ready") + s := bufio.NewScanner(c) + for s.Scan() { + switch s.Text() { + case "EHLO localhost": + send("250-127.0.0.1 ESMTP offers a warm hug of welcome") + send("250-STARTTLS") + send("250 Ok") + case "STARTTLS": + send("220 Go ahead") + keypair, err := tls.X509KeyPair(localhostCert, localhostKey) + if err != nil { + return err + } + config := &tls.Config{Certificates: []tls.Certificate{keypair}} + c = tls.Server(c, config) + defer c.Close() + return serverHandleTLS(c, t) + default: + t.Fatalf("unrecognized command: %q", s.Text()) + } + } + return s.Err() +} + +func serverHandleTLS(c net.Conn, t *testing.T) error { + send := smtpSender{c}.send + s := bufio.NewScanner(c) + for s.Scan() { + switch s.Text() { + case "EHLO localhost": + send("250 Ok") + case "MAIL FROM:": + send("250 Ok") + case "RCPT TO:": + send("250 Ok") + case "DATA": + send("354 send the mail data, end with .") + send("250 Ok") + case "Subject: test": + case "": + case "howdy!": + case ".": + case "QUIT": + send("221 127.0.0.1 Service closing transmission channel") + return nil + default: + t.Fatalf("unrecognized command during TLS: %q", s.Text()) + } + } + return s.Err() +} + +func init() { + testRootCAs := x509.NewCertPool() + testRootCAs.AppendCertsFromPEM(localhostCert) + testHookStartTLS = func(config *tls.Config) { + config.RootCAs = testRootCAs + } +} + +func sendMail(hostPort string) error { + from := "joe1@example.com" + to := []string{"joe2@example.com"} + return SendMail(hostPort, nil, from, to, []byte("Subject: test\n\nhowdy!")) +} + +// localhostCert is a PEM-encoded TLS cert generated from src/crypto/tls: +// +// go run generate_cert.go --rsa-bits 1024 --host 127.0.0.1,::1,example.com \ +// --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h +var localhostCert = []byte(` +-----BEGIN CERTIFICATE----- +MIICFDCCAX2gAwIBAgIRAK0xjnaPuNDSreeXb+z+0u4wDQYJKoZIhvcNAQELBQAw +EjEQMA4GA1UEChMHQWNtZSBDbzAgFw03MDAxMDEwMDAwMDBaGA8yMDg0MDEyOTE2 +MDAwMFowEjEQMA4GA1UEChMHQWNtZSBDbzCBnzANBgkqhkiG9w0BAQEFAAOBjQAw +gYkCgYEA0nFbQQuOWsjbGtejcpWz153OlziZM4bVjJ9jYruNw5n2Ry6uYQAffhqa +JOInCmmcVe2siJglsyH9aRh6vKiobBbIUXXUU1ABd56ebAzlt0LobLlx7pZEMy30 +LqIi9E6zmL3YvdGzpYlkFRnRrqwEtWYbGBf3znO250S56CCWH2UCAwEAAaNoMGYw +DgYDVR0PAQH/BAQDAgKkMBMGA1UdJQQMMAoGCCsGAQUFBwMBMA8GA1UdEwEB/wQF +MAMBAf8wLgYDVR0RBCcwJYILZXhhbXBsZS5jb22HBH8AAAGHEAAAAAAAAAAAAAAA +AAAAAAEwDQYJKoZIhvcNAQELBQADgYEAbZtDS2dVuBYvb+MnolWnCNqvw1w5Gtgi +NmvQQPOMgM3m+oQSCPRTNGSg25e1Qbo7bgQDv8ZTnq8FgOJ/rbkyERw2JckkHpD4 +n4qcK27WkEDBtQFlPihIM8hLIuzWoi/9wygiElTy/tVL3y7fGCvY2/k1KBthtZGF +tN8URjVmyEo= +-----END CERTIFICATE-----`) + +// localhostKey is the private key for localhostCert. +var localhostKey = []byte(testingKey(` +-----BEGIN RSA TESTING KEY----- +MIICXgIBAAKBgQDScVtBC45ayNsa16NylbPXnc6XOJkzhtWMn2Niu43DmfZHLq5h +AB9+Gpok4icKaZxV7ayImCWzIf1pGHq8qKhsFshRddRTUAF3np5sDOW3QuhsuXHu +lkQzLfQuoiL0TrOYvdi90bOliWQVGdGurAS1ZhsYF/fOc7bnRLnoIJYfZQIDAQAB +AoGBAMst7OgpKyFV6c3JwyI/jWqxDySL3caU+RuTTBaodKAUx2ZEmNJIlx9eudLA +kucHvoxsM/eRxlxkhdFxdBcwU6J+zqooTnhu/FE3jhrT1lPrbhfGhyKnUrB0KKMM +VY3IQZyiehpxaeXAwoAou6TbWoTpl9t8ImAqAMY8hlULCUqlAkEA+9+Ry5FSYK/m +542LujIcCaIGoG1/Te6Sxr3hsPagKC2rH20rDLqXwEedSFOpSS0vpzlPAzy/6Rbb +PHTJUhNdwwJBANXkA+TkMdbJI5do9/mn//U0LfrCR9NkcoYohxfKz8JuhgRQxzF2 +6jpo3q7CdTuuRixLWVfeJzcrAyNrVcBq87cCQFkTCtOMNC7fZnCTPUv+9q1tcJyB +vNjJu3yvoEZeIeuzouX9TJE21/33FaeDdsXbRhQEj23cqR38qFHsF1qAYNMCQQDP +QXLEiJoClkR2orAmqjPLVhR3t2oB3INcnEjLNSq8LHyQEfXyaFfu4U9l5+fRPL2i +jiC0k/9L5dHUsF0XZothAkEA23ddgRs+Id/HxtojqqUT27B8MT/IGNrYsp4DvS/c +qgkeluku4GjxRlDMBuXk94xOBEinUs+p/hwP1Alll80Tpg== +-----END RSA TESTING KEY-----`)) + +func testingKey(s string) string { return strings.ReplaceAll(s, "TESTING KEY", "PRIVATE KEY") } From a804e4a101eabc360fca1396054190b7f460106c Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 10 Jan 2023 10:09:45 +0100 Subject: [PATCH 2/9] Fix license SPDX headers for forked net/smtp code - Also import the original BSD-3-Clause.txt license from the Go team into the LICENSES directory - Further on, license headers should hold "The go-mail Authors" instead of my name. Did this already for the MIT license. --- LICENSES/BSD-3-Clause.txt | 27 +++++++++++++++++++++++++++ LICENSES/MIT.txt | 17 +++++++++++++---- smtp/auth.go | 13 +++++++++++++ smtp/auth_cram_md5.go | 13 +++++++++++++ smtp/auth_login.go | 1 - smtp/auth_plain.go | 13 +++++++++++++ smtp/example_test.go | 13 +++++++++++-- smtp/smtp.go | 13 +++++++++++-- smtp/smtp_test.go | 13 +++++++++++-- 9 files changed, 112 insertions(+), 11 deletions(-) create mode 100644 LICENSES/BSD-3-Clause.txt diff --git a/LICENSES/BSD-3-Clause.txt b/LICENSES/BSD-3-Clause.txt new file mode 100644 index 0000000..ea5ea89 --- /dev/null +++ b/LICENSES/BSD-3-Clause.txt @@ -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. \ No newline at end of file diff --git a/LICENSES/MIT.txt b/LICENSES/MIT.txt index 2071b23..00ec3d1 100644 --- a/LICENSES/MIT.txt +++ b/LICENSES/MIT.txt @@ -1,9 +1,18 @@ MIT License -Copyright (c) +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. diff --git a/smtp/auth.go b/smtp/auth.go index 70cf7aa..7ec3ec9 100644 --- a/smtp/auth.go +++ b/smtp/auth.go @@ -1,3 +1,16 @@ +// 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 that can be found in the [PROJECT ROOT]/LICENSES 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. diff --git a/smtp/auth_cram_md5.go b/smtp/auth_cram_md5.go index 2f91289..f3a20d9 100644 --- a/smtp/auth_cram_md5.go +++ b/smtp/auth_cram_md5.go @@ -1,3 +1,16 @@ +// 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 that can be found in the [PROJECT ROOT]/LICENSES 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 ( diff --git a/smtp/auth_login.go b/smtp/auth_login.go index 6f92bab..0e63be5 100644 --- a/smtp/auth_login.go +++ b/smtp/auth_login.go @@ -2,7 +2,6 @@ // // SPDX-License-Identifier: MIT -// Package auth implements the LOGIN and MD5-DIGEST smtp authentication mechanisms package smtp import ( diff --git a/smtp/auth_plain.go b/smtp/auth_plain.go index 035699b..50e98cf 100644 --- a/smtp/auth_plain.go +++ b/smtp/auth_plain.go @@ -1,3 +1,16 @@ +// 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 that can be found in the [PROJECT ROOT]/LICENSES 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 ( diff --git a/smtp/example_test.go b/smtp/example_test.go index 452f085..30b5ae5 100644 --- a/smtp/example_test.go +++ b/smtp/example_test.go @@ -1,6 +1,15 @@ -// Copyright 2013 The Go Authors. All rights reserved. +// 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 that can be found in the LICENSE file. +// license that can be found in the [PROJECT ROOT]/LICENSES 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 diff --git a/smtp/smtp.go b/smtp/smtp.go index 1fef69c..39e2c77 100644 --- a/smtp/smtp.go +++ b/smtp/smtp.go @@ -1,6 +1,15 @@ -// Copyright 2010 The Go Authors. All rights reserved. +// 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 that can be found in the LICENSE file. +// license that can be found in the [PROJECT ROOT]/LICENSES 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: diff --git a/smtp/smtp_test.go b/smtp/smtp_test.go index 90b5306..681e207 100644 --- a/smtp/smtp_test.go +++ b/smtp/smtp_test.go @@ -1,6 +1,15 @@ -// Copyright 2010 The Go Authors. All rights reserved. +// 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 that can be found in the LICENSE file. +// license that can be found in the [PROJECT ROOT]/LICENSES 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 From 1836f6c49d537854b37546cdc37c7625361f38a4 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 10 Jan 2023 16:55:47 +0100 Subject: [PATCH 3/9] Start fixing golangci-lint findings in test suite --- smtp/smtp_test.go | 57 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 43 insertions(+), 14 deletions(-) diff --git a/smtp/smtp_test.go b/smtp/smtp_test.go index 681e207..da68bfd 100644 --- a/smtp/smtp_test.go +++ b/smtp/smtp_test.go @@ -35,15 +35,30 @@ type authTest struct { } var authTests = []authTest{ - {PlainAuth("", "user", "pass", "testserver"), []string{}, "PLAIN", []string{"\x00user\x00pass"}}, - {PlainAuth("foo", "bar", "baz", "testserver"), []string{}, "PLAIN", []string{"foo\x00bar\x00baz"}}, - {CRAMMD5Auth("user", "pass"), []string{"<123456.1322876914@testserver>"}, "CRAM-MD5", []string{"", "user 287eb355114cf5c471c26a875f1ca4ae"}}, + { + PlainAuth("", "user", "pass", "testserver"), + []string{}, + "PLAIN", + []string{"\x00user\x00pass"}, + }, + { + PlainAuth("foo", "bar", "baz", "testserver"), + []string{}, + "PLAIN", + []string{"foo\x00bar\x00baz"}, + }, { LoginAuth("user", "pass", "testserver"), - []string{"Username:", "Password:", "Invalid:"}, + []string{"Username:", "Password:"}, "LOGIN", []string{"", "user", "pass", ""}, }, + { + CRAMMD5Auth("user", "pass"), + []string{"<123456.1322876914@testserver>"}, + "CRAM-MD5", + []string{"", "user 287eb355114cf5c471c26a875f1ca4ae"}, + }, } func TestAuth(t *testing.T) { @@ -186,8 +201,10 @@ func TestClientAuthTrimSpace(t *testing.T) { } c.tls = true c.didHello = true - c.Auth(toServerEmptyAuth{}) - c.Close() + _ = c.Auth(toServerEmptyAuth{}) + if err := c.Close(); err != nil { + t.Errorf("close failed: %s", err) + } if got, want := wrote.String(), "AUTH FOOAUTH\r\n*\r\nQUIT\r\n"; got != want { t.Errorf("wrote %q; want %q", got, want) } @@ -199,11 +216,11 @@ func TestClientAuthTrimSpace(t *testing.T) { // the end of the line. See TestClientAuthTrimSpace. type toServerEmptyAuth struct{} -func (toServerEmptyAuth) Start(server *ServerInfo) (proto string, toServer []byte, err error) { +func (toServerEmptyAuth) Start(_ *ServerInfo) (proto string, toServer []byte, err error) { return "FOOAUTH", nil, nil } -func (toServerEmptyAuth) Next(fromServer []byte, more bool) (toServer []byte, err error) { +func (toServerEmptyAuth) Next(_ []byte, _ bool) (toServer []byte, err error) { panic("unexpected call") } @@ -301,7 +318,9 @@ Goodbye.` t.Fatalf("QUIT failed: %s", err) } - bcmdbuf.Flush() + if err := bcmdbuf.Flush(); err != nil { + t.Errorf("flush failed: %s", err) + } actualcmds := cmdbuf.String() if client != actualcmds { t.Fatalf("Got:\n%s\nExpected:\n%s", actualcmds, client) @@ -385,7 +404,9 @@ QUIT t.Fatalf("QUIT failed: %s", err) } - bcmdbuf.Flush() + if err := bcmdbuf.Flush(); err != nil { + t.Errorf("flush failed: %s", err) + } actualcmds := cmdbuf.String() client := strings.Join(strings.Split(basicClient, "\n"), "\r\n") if client != actualcmds { @@ -425,7 +446,9 @@ QUIT t.Fatalf("QUIT failed: %s", err) } - bcmdbuf.Flush() + if err := bcmdbuf.Flush(); err != nil { + t.Errorf("flush failed: %s", err) + } actualcmds := cmdbuf.String() client := strings.Join(strings.Split(basicClient, "\n"), "\r\n") if client != actualcmds { @@ -824,14 +847,20 @@ func TestSendMail(t *testing.T) { t.Errorf("Accept error: %v", err) return } - defer conn.Close() + defer func() { + _ = conn.Close() + }() tc := textproto.NewConn(conn) for i := 0; i < len(data) && data[i] != ""; i++ { - tc.PrintfLine(data[i]) + if err := tc.PrintfLine(data[i]); err != nil { + t.Errorf("printing to textproto failed: %s", err) + } for len(data[i]) >= 4 && data[i][3] == '-' { i++ - tc.PrintfLine(data[i]) + if err := tc.PrintfLine(data[i]); err != nil { + t.Errorf("printing to textproto failed: %s", err) + } } if data[i] == "221 Goodbye" { return From 8e807c2569d9fad79e17d1d3d1067f6bbdab85f8 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Wed, 11 Jan 2023 15:28:33 +0100 Subject: [PATCH 4/9] Fork net/smpt into go-mail Fixed open issues in smtp_test.go --- smtp/smtp_test.go | 108 +++++++++++++++++++++++++++++++++------------- 1 file changed, 79 insertions(+), 29 deletions(-) diff --git a/smtp/smtp_test.go b/smtp/smtp_test.go index da68bfd..8e4c866 100644 --- a/smtp/smtp_test.go +++ b/smtp/smtp_test.go @@ -489,7 +489,9 @@ QUIT t.Fatalf("QUIT failed: %s", err) } - bcmdbuf.Flush() + if err := bcmdbuf.Flush(); err != nil { + t.Errorf("failed to flush: %s", err) + } actualcmds := cmdbuf.String() client := strings.Join(strings.Split(basicClient, "\n"), "\r\n") if client != actualcmds { @@ -530,7 +532,9 @@ QUIT t.Fatalf("QUIT failed: %s", err) } - bcmdbuf.Flush() + if err := bcmdbuf.Flush(); err != nil { + t.Errorf("failed to flush: %s", err) + } actualcmds := cmdbuf.String() client := strings.Join(strings.Split(basicClient, "\n"), "\r\n") if client != actualcmds { @@ -573,7 +577,9 @@ QUIT t.Fatalf("QUIT failed: %s", err) } - bcmdbuf.Flush() + if err := bcmdbuf.Flush(); err != nil { + t.Errorf("failed to flush: %s", err) + } actualcmds := cmdbuf.String() client := strings.Join(strings.Split(basicClient, "\n"), "\r\n") if client != actualcmds { @@ -589,7 +595,9 @@ func TestNewClient(t *testing.T) { var cmdbuf strings.Builder bcmdbuf := bufio.NewWriter(&cmdbuf) out := func() string { - bcmdbuf.Flush() + if err := bcmdbuf.Flush(); err != nil { + t.Errorf("failed to flush: %s", err) + } return cmdbuf.String() } var fake faker @@ -598,7 +606,9 @@ func TestNewClient(t *testing.T) { if err != nil { t.Fatalf("NewClient: %v\n(after %v)", err, out()) } - defer c.Close() + defer func() { + _ = c.Close() + }() if ok, args := c.Extension("aUtH"); !ok || args != "LOGIN PLAIN" { t.Fatalf("Expected AUTH supported") } @@ -639,7 +649,9 @@ func TestNewClient2(t *testing.T) { if err != nil { t.Fatalf("NewClient: %v", err) } - defer c.Close() + defer func() { + _ = c.Close() + }() if ok, _ := c.Extension("DSN"); ok { t.Fatalf("Shouldn't support DSN") } @@ -647,7 +659,9 @@ func TestNewClient2(t *testing.T) { t.Fatalf("QUIT failed: %s", err) } - bcmdbuf.Flush() + if err := bcmdbuf.Flush(); err != nil { + t.Errorf("flush failed: %s", err) + } actualcmds := cmdbuf.String() if client != actualcmds { t.Fatalf("Got:\n%s\nExpected:\n%s", actualcmds, client) @@ -690,7 +704,9 @@ func TestNewClientWithTLS(t *testing.T) { t.Errorf("server: accept: %v", err) return } - defer conn.Close() + defer func() { + _ = conn.Close() + }() _, err = conn.Write([]byte("220 SIGNS\r\n")) if err != nil { @@ -704,7 +720,9 @@ func TestNewClientWithTLS(t *testing.T) { if err != nil { t.Fatalf("client: dial: %v", err) } - defer conn.Close() + defer func() { + _ = conn.Close() + }() client, err := NewClient(conn, ln.Addr().String()) if err != nil { @@ -731,7 +749,9 @@ func TestHello(t *testing.T) { if err != nil { t.Fatalf("NewClient: %v", err) } - defer c.Close() + defer func() { + _ = c.Close() + }() c.localName = "customhost" err = nil @@ -782,7 +802,9 @@ func TestHello(t *testing.T) { t.Errorf("Command %d failed: %v", i, err) } - bcmdbuf.Flush() + if err := bcmdbuf.Flush(); err != nil { + t.Errorf("flush failed: %s", err) + } actualcmds := cmdbuf.String() if client != actualcmds { t.Errorf("Got:\n%s\nExpected:\n%s", actualcmds, client) @@ -835,7 +857,9 @@ func TestSendMail(t *testing.T) { if err != nil { t.Fatalf("Unable to create listener: %v", err) } - defer l.Close() + defer func() { + _ = l.Close() + }() // prevent data race on bcmdbuf done := make(chan struct{}) @@ -868,7 +892,9 @@ func TestSendMail(t *testing.T) { read := false for !read || data[i] == "354 Go ahead" { msg, err := tc.ReadLine() - bcmdbuf.Write([]byte(msg + "\r\n")) + if _, err := bcmdbuf.Write([]byte(msg + "\r\n")); err != nil { + t.Errorf("write failed: %s", err) + } read = true if err != nil { t.Errorf("Read error: %v", err) @@ -903,7 +929,9 @@ SendMail is working for me. } <-done - bcmdbuf.Flush() + if err := bcmdbuf.Flush(); err != nil { + t.Errorf("flush failed: %s", err) + } actualcmds := cmdbuf.String() if client != actualcmds { t.Errorf("Got:\n%s\nExpected:\n%s", actualcmds, client) @@ -939,23 +967,29 @@ func TestSendMailWithAuth(t *testing.T) { if err != nil { t.Fatalf("Unable to create listener: %v", err) } - defer l.Close() + defer func() { + _ = l.Close() + }() errCh := make(chan error) go func() { defer close(errCh) conn, err := l.Accept() if err != nil { - errCh <- fmt.Errorf("Accept: %v", err) + errCh <- fmt.Errorf("listener Accept: %w", err) return } - defer conn.Close() + defer func() { + _ = conn.Close() + }() tc := textproto.NewConn(conn) - tc.PrintfLine("220 hello world") + if err := tc.PrintfLine("220 hello world"); err != nil { + t.Errorf("textproto connetion print failed: %s", err) + } msg, err := tc.ReadLine() if err != nil { - errCh <- fmt.Errorf("ReadLine error: %v", err) + errCh <- fmt.Errorf("textproto connection ReadLine error: %w", err) return } const wantMsg = "EHLO localhost" @@ -965,7 +999,7 @@ func TestSendMailWithAuth(t *testing.T) { } err = tc.PrintfLine("250 mx.google.com at your service") if err != nil { - errCh <- fmt.Errorf("PrintfLine: %v", err) + errCh <- fmt.Errorf("textproto connection PrintfLine: %w", err) return } }() @@ -999,7 +1033,9 @@ func TestAuthFailed(t *testing.T) { if err != nil { t.Fatalf("NewClient: %v", err) } - defer c.Close() + defer func() { + _ = c.Close() + }() c.tls = true c.serverName = "smtp.google.com" @@ -1011,7 +1047,9 @@ func TestAuthFailed(t *testing.T) { t.Errorf("Auth: got error: %v, want: %s", err, "535 Invalid credentials\nplease see www.example.com") } - bcmdbuf.Flush() + if err := bcmdbuf.Flush(); err != nil { + t.Errorf("flush failed: %s", err) + } actualcmds := cmdbuf.String() if client != actualcmds { t.Errorf("Got:\n%s\nExpected:\n%s", actualcmds, client) @@ -1040,7 +1078,9 @@ func TestTLSClient(t *testing.T) { } */ ln := newLocalListener(t) - defer ln.Close() + defer func() { + _ = ln.Close() + }() errc := make(chan error) go func() { errc <- sendMail(ln.Addr().String()) @@ -1049,7 +1089,9 @@ func TestTLSClient(t *testing.T) { if err != nil { t.Fatalf("failed to accept connection: %v", err) } - defer conn.Close() + defer func() { + _ = conn.Close() + }() if err := serverHandle(conn, t); err != nil { t.Fatalf("failed to handle connection: %v", err) } @@ -1060,7 +1102,9 @@ func TestTLSClient(t *testing.T) { func TestTLSConnState(t *testing.T) { ln := newLocalListener(t) - defer ln.Close() + defer func() { + _ = ln.Close() + }() clientDone := make(chan bool) serverDone := make(chan bool) go func() { @@ -1070,7 +1114,9 @@ func TestTLSConnState(t *testing.T) { t.Errorf("Server accept: %v", err) return } - defer c.Close() + defer func() { + _ = c.Close() + }() if err := serverHandle(c, t); err != nil { t.Errorf("server error: %v", err) } @@ -1082,7 +1128,9 @@ func TestTLSConnState(t *testing.T) { t.Errorf("Client dial: %v", err) return } - defer c.Quit() + defer func() { + _ = c.Quit() + }() cfg := &tls.Config{ServerName: "example.com"} testHookStartTLS(cfg) // set the RootCAs if err := c.StartTLS(cfg); err != nil { @@ -1118,7 +1166,7 @@ type smtpSender struct { } func (s smtpSender) send(f string) { - s.w.Write([]byte(f + "\r\n")) + _, _ = s.w.Write([]byte(f + "\r\n")) } // smtp server, finely tailored to deal with our own client only! @@ -1140,7 +1188,9 @@ func serverHandle(c net.Conn, t *testing.T) error { } config := &tls.Config{Certificates: []tls.Certificate{keypair}} c = tls.Server(c, config) - defer c.Close() + defer func() { + _ = c.Close() + }() return serverHandleTLS(c, t) default: t.Fatalf("unrecognized command: %q", s.Text()) From df7bb9b742e4783323bd194f114759f92d0b463d Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Wed, 11 Jan 2023 20:07:03 +0100 Subject: [PATCH 5/9] Fork net/smpt into go-mail Fix open TODO in smtp_test.go and fork `testenv.SkipFlaky()` from `internal/testenv` to implement flaky test skipping for FBSD --- smtp/smtp_test.go | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/smtp/smtp_test.go b/smtp/smtp_test.go index 8e4c866..ecdfcd1 100644 --- a/smtp/smtp_test.go +++ b/smtp/smtp_test.go @@ -18,10 +18,12 @@ import ( "bytes" "crypto/tls" "crypto/x509" + "flag" "fmt" "io" "net" "net/textproto" + "runtime" "strings" "testing" "time" @@ -1071,12 +1073,9 @@ QUIT ` func TestTLSClient(t *testing.T) { - /* - TODO: Check if we need this - if runtime.GOOS == "freebsd" || runtime.GOOS == "js" { - testenv.SkipFlaky(t, 19229) - } - */ + if runtime.GOOS == "freebsd" || runtime.GOOS == "js" { + SkipFlaky(t, 19229) + } ln := newLocalListener(t) defer func() { _ = ln.Close() @@ -1280,3 +1279,12 @@ qgkeluku4GjxRlDMBuXk94xOBEinUs+p/hwP1Alll80Tpg== -----END RSA TESTING KEY-----`)) func testingKey(s string) string { return strings.ReplaceAll(s, "TESTING KEY", "PRIVATE KEY") } + +var flaky = flag.Bool("flaky", false, "run known-flaky tests too") + +func SkipFlaky(t testing.TB, issue int) { + t.Helper() + if !*flaky { + t.Skipf("skipping known flaky test without the -flaky flag; see golang.org/issue/%d", issue) + } +} From 820d1c25d82a145dead93470677bcda4cf33f9a4 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Wed, 11 Jan 2023 20:35:22 +0100 Subject: [PATCH 6/9] Fork net/smpt into go-mail Implemented negative check for AuthLogin Auth method --- smtp/smtp_test.go | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/smtp/smtp_test.go b/smtp/smtp_test.go index ecdfcd1..f8866a7 100644 --- a/smtp/smtp_test.go +++ b/smtp/smtp_test.go @@ -34,6 +34,7 @@ type authTest struct { challenges []string name string responses []string + sf []bool } var authTests = []authTest{ @@ -42,24 +43,28 @@ var authTests = []authTest{ []string{}, "PLAIN", []string{"\x00user\x00pass"}, + []bool{false, false}, }, { PlainAuth("foo", "bar", "baz", "testserver"), []string{}, "PLAIN", []string{"foo\x00bar\x00baz"}, + []bool{false, false}, }, { LoginAuth("user", "pass", "testserver"), - []string{"Username:", "Password:"}, + []string{"Username:", "Password:", "Invalid:"}, "LOGIN", []string{"", "user", "pass", ""}, + []bool{false, false, true}, }, { CRAMMD5Auth("user", "pass"), []string{"<123456.1322876914@testserver>"}, "CRAM-MD5", []string{"", "user 287eb355114cf5c471c26a875f1ca4ae"}, + []bool{false, false}, }, } @@ -79,8 +84,9 @@ testLoop: for j := range test.challenges { challenge := []byte(test.challenges[j]) expected := []byte(test.responses[j+1]) + sf := test.sf[j] resp, err := test.auth.Next(challenge, true) - if err != nil { + if err != nil && !sf { t.Errorf("#%d error: %s", i, err) continue testLoop } @@ -157,7 +163,7 @@ func TestAuthLogin(t *testing.T) { // 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: &ServerInfo{Name: "servername", Auth: []string{"PLAIN"}}, + server: &ServerInfo{Name: "servername", Auth: []string{"LOGIN"}}, err: "unencrypted connection", }, { From 7cb34856a314639f1ab61b68c7c5a87d19465452 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 13 Jan 2023 16:54:55 +0100 Subject: [PATCH 7/9] Adjusting license headers and including original LICENSE file from the Go project into the smtp/ directory as suggested in https://github.com/wneessen/go-mail/issues/97#issuecomment-1381046444 --- smtp/LICENSE | 27 +++++++++++++++++++++++++++ smtp/auth.go | 2 +- smtp/auth_cram_md5.go | 4 ++-- smtp/auth_login.go | 2 +- smtp/auth_plain.go | 4 ++-- smtp/example_test.go | 2 +- smtp/smtp.go | 2 +- smtp/smtp_test.go | 2 +- 8 files changed, 36 insertions(+), 9 deletions(-) create mode 100644 smtp/LICENSE diff --git a/smtp/LICENSE b/smtp/LICENSE new file mode 100644 index 0000000..ea5ea89 --- /dev/null +++ b/smtp/LICENSE @@ -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. \ No newline at end of file diff --git a/smtp/auth.go b/smtp/auth.go index 7ec3ec9..30948e1 100644 --- a/smtp/auth.go +++ b/smtp/auth.go @@ -3,7 +3,7 @@ // // Original net/smtp code from the Go stdlib by the Go Authors. // Use of this source code is governed by a BSD-style -// license that can be found in the [PROJECT ROOT]/LICENSES directory. +// LICENSE file that can be found in this directory. // // go-mail specific modifications by the go-mail Authors. // Licensed under the MIT License. diff --git a/smtp/auth_cram_md5.go b/smtp/auth_cram_md5.go index f3a20d9..5357032 100644 --- a/smtp/auth_cram_md5.go +++ b/smtp/auth_cram_md5.go @@ -3,7 +3,7 @@ // // Original net/smtp code from the Go stdlib by the Go Authors. // Use of this source code is governed by a BSD-style -// license that can be found in the [PROJECT ROOT]/LICENSES directory. +// LICENSE file that can be found in this directory. // // go-mail specific modifications by the go-mail Authors. // Licensed under the MIT License. @@ -32,7 +32,7 @@ func CRAMMD5Auth(username, secret string) Auth { return &cramMD5Auth{username, secret} } -func (a *cramMD5Auth) Start(server *ServerInfo) (string, []byte, error) { +func (a *cramMD5Auth) Start(_ *ServerInfo) (string, []byte, error) { return "CRAM-MD5", nil, nil } diff --git a/smtp/auth_login.go b/smtp/auth_login.go index 0e63be5..6811c71 100644 --- a/smtp/auth_login.go +++ b/smtp/auth_login.go @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2022 Winni Neessen +// SPDX-FileCopyrightText: 2022-2023 The go-mail Authors // // SPDX-License-Identifier: MIT diff --git a/smtp/auth_plain.go b/smtp/auth_plain.go index 50e98cf..49c537b 100644 --- a/smtp/auth_plain.go +++ b/smtp/auth_plain.go @@ -3,7 +3,7 @@ // // Original net/smtp code from the Go stdlib by the Go Authors. // Use of this source code is governed by a BSD-style -// license that can be found in the [PROJECT ROOT]/LICENSES directory. +// LICENSE file that can be found in this directory. // // go-mail specific modifications by the go-mail Authors. // Licensed under the MIT License. @@ -51,7 +51,7 @@ func (a *plainAuth) Start(server *ServerInfo) (string, []byte, error) { return "PLAIN", resp, nil } -func (a *plainAuth) Next(fromServer []byte, more bool) ([]byte, error) { +func (a *plainAuth) Next(_ []byte, more bool) ([]byte, error) { if more { // We've already sent everything. return nil, errors.New("unexpected server challenge") diff --git a/smtp/example_test.go b/smtp/example_test.go index 30b5ae5..0445dfc 100644 --- a/smtp/example_test.go +++ b/smtp/example_test.go @@ -3,7 +3,7 @@ // // Original net/smtp code from the Go stdlib by the Go Authors. // Use of this source code is governed by a BSD-style -// license that can be found in the [PROJECT ROOT]/LICENSES directory. +// LICENSE file that can be found in this directory. // // go-mail specific modifications by the go-mail Authors. // Licensed under the MIT License. diff --git a/smtp/smtp.go b/smtp/smtp.go index 39e2c77..4b23170 100644 --- a/smtp/smtp.go +++ b/smtp/smtp.go @@ -3,7 +3,7 @@ // // Original net/smtp code from the Go stdlib by the Go Authors. // Use of this source code is governed by a BSD-style -// license that can be found in the [PROJECT ROOT]/LICENSES directory. +// LICENSE file that can be found in this directory. // // go-mail specific modifications by the go-mail Authors. // Licensed under the MIT License. diff --git a/smtp/smtp_test.go b/smtp/smtp_test.go index f8866a7..7423100 100644 --- a/smtp/smtp_test.go +++ b/smtp/smtp_test.go @@ -3,7 +3,7 @@ // // Original net/smtp code from the Go stdlib by the Go Authors. // Use of this source code is governed by a BSD-style -// license that can be found in the [PROJECT ROOT]/LICENSES directory. +// LICENSE file that can be found in this directory. // // go-mail specific modifications by the go-mail Authors. // Licensed under the MIT License. From 8559e8c3015998818cd6b9d692444e73a30d18e5 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 13 Jan 2023 17:34:41 +0100 Subject: [PATCH 8/9] Re-introduce backwards compatibility with Go 1.17 and Go 1.18 The following changes make use of methods that are not available in Go 1.17/Go 1.18. To guarantee at least 4 versions of backwards compatibility, versioned copies of those changes have been back-ported: - https://github.com/golang/go/commit/4d8db00641cc9ff4f44de7df9b8c4f4a4f9416ee#diff-4f6f6bdb9891d4dd271f9f31430420a2e44018fe4ee539576faf458bebb3cee4 - https://github.com/golang/go/commit/58158e990f272774e615c9abd8662bf0198c29aa#diff-772fc9f5d0c86f26e35158fb3e7a71a4967d18b4ec23a5dbb60781ab0babf426 --- smtp/auth_cram_md5.go | 3 +++ smtp/auth_cram_md5_118.go | 50 +++++++++++++++++++++++++++++++++++++++ smtp/smtp.go | 23 ------------------ smtp/smtp_ehlo.go | 42 ++++++++++++++++++++++++++++++++ smtp/smtp_ehlo_117.go | 46 +++++++++++++++++++++++++++++++++++ 5 files changed, 141 insertions(+), 23 deletions(-) create mode 100644 smtp/auth_cram_md5_118.go create mode 100644 smtp/smtp_ehlo.go create mode 100644 smtp/smtp_ehlo_117.go diff --git a/smtp/auth_cram_md5.go b/smtp/auth_cram_md5.go index 5357032..a02e3f1 100644 --- a/smtp/auth_cram_md5.go +++ b/smtp/auth_cram_md5.go @@ -11,6 +11,9 @@ // // SPDX-License-Identifier: BSD-3-Clause AND MIT +//go:build go1.19 +// +build go1.19 + package smtp import ( diff --git a/smtp/auth_cram_md5_118.go b/smtp/auth_cram_md5_118.go new file mode 100644 index 0000000..c9ff86f --- /dev/null +++ b/smtp/auth_cram_md5_118.go @@ -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 []byte(fmt.Sprintf("%s %x", a.username, d.Sum(s))), nil + } + return nil, nil +} diff --git a/smtp/smtp.go b/smtp/smtp.go index 4b23170..9d8c029 100644 --- a/smtp/smtp.go +++ b/smtp/smtp.go @@ -137,29 +137,6 @@ func (c *Client) helo() error { return err } -// 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 -} - // 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 { diff --git a/smtp/smtp_ehlo.go b/smtp/smtp_ehlo.go new file mode 100644 index 0000000..ae80a62 --- /dev/null +++ b/smtp/smtp_ehlo.go @@ -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 +} diff --git a/smtp/smtp_ehlo_117.go b/smtp/smtp_ehlo_117.go new file mode 100644 index 0000000..6e52858 --- /dev/null +++ b/smtp/smtp_ehlo_117.go @@ -0,0 +1,46 @@ +// 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 { + 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 +} From 2950f222cf64bed33d251d5ea149daceb2ddaf29 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 13 Jan 2023 17:42:05 +0100 Subject: [PATCH 9/9] Added backport comments as reference --- smtp/auth_cram_md5_118.go | 2 ++ smtp/smtp_ehlo_117.go | 3 +++ 2 files changed, 5 insertions(+) diff --git a/smtp/auth_cram_md5_118.go b/smtp/auth_cram_md5_118.go index c9ff86f..71233f2 100644 --- a/smtp/auth_cram_md5_118.go +++ b/smtp/auth_cram_md5_118.go @@ -39,6 +39,8 @@ 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)) diff --git a/smtp/smtp_ehlo_117.go b/smtp/smtp_ehlo_117.go index 6e52858..a77d937 100644 --- a/smtp/smtp_ehlo_117.go +++ b/smtp/smtp_ehlo_117.go @@ -20,6 +20,9 @@ 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 {