mirror of
https://github.com/wneessen/go-mail.git
synced 2024-11-22 22:00:49 +01:00
Merge pull request #100 from wneessen/feature/97_fork-the-netsmtp-package-from-stdlib-into-go-mail
Fork the net/smtp package from Go's stdlib into go-mail
This commit is contained in:
commit
813020f02d
16 changed files with 2187 additions and 125 deletions
27
LICENSES/BSD-3-Clause.txt
Normal file
27
LICENSES/BSD-3-Clause.txt
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
Copyright (c) 2009 The Go Authors. All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are
|
||||||
|
met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above
|
||||||
|
copyright notice, this list of conditions and the following disclaimer
|
||||||
|
in the documentation and/or other materials provided with the
|
||||||
|
distribution.
|
||||||
|
* Neither the name of Google Inc. nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from
|
||||||
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
|
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@ -1,9 +1,18 @@
|
||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) <year> <copyright holders>
|
Copyright (c) 2022-2023 The go-mail Authors
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
|
||||||
|
documentation files (the "Software"), to deal in the Software without restriction, including without
|
||||||
|
limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
the Software, and to permit persons to whom the Software is furnished to do so, subject to the
|
||||||
|
following conditions:
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
The above copyright notice and this permission notice shall be included in all copies or substantial
|
||||||
|
portions of the Software.
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||||
|
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
||||||
|
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
DEALINGS IN THE SOFTWARE.
|
||||||
|
|
|
@ -1,104 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2022 Winni Neessen <winni@neessen.dev>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
package auth
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"net/smtp"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestAuth(t *testing.T) {
|
|
||||||
type authTest struct {
|
|
||||||
auth smtp.Auth
|
|
||||||
challenges []string
|
|
||||||
name string
|
|
||||||
responses []string
|
|
||||||
shouldfail []bool
|
|
||||||
}
|
|
||||||
|
|
||||||
authTests := []authTest{
|
|
||||||
{
|
|
||||||
LoginAuth("user", "pass", "testserver"),
|
|
||||||
[]string{"Username:", "Password:", "Invalid:"},
|
|
||||||
"LOGIN",
|
|
||||||
[]string{"", "user", "pass", ""},
|
|
||||||
[]bool{false, false, true},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
testLoop:
|
|
||||||
for i, test := range authTests {
|
|
||||||
name, resp, err := test.auth.Start(&smtp.ServerInfo{Name: "testserver", TLS: true, Auth: nil})
|
|
||||||
if name != test.name {
|
|
||||||
t.Errorf("#%d got name %s, expected %s", i, name, test.name)
|
|
||||||
}
|
|
||||||
if !bytes.Equal(resp, []byte(test.responses[0])) {
|
|
||||||
t.Errorf("#%d got response %s, expected %s", i, resp, test.responses[0])
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("#%d error: %s", i, err)
|
|
||||||
}
|
|
||||||
for j := range test.challenges {
|
|
||||||
challenge := []byte(test.challenges[j])
|
|
||||||
expected := []byte(test.responses[j+1])
|
|
||||||
resp, err := test.auth.Next(challenge, true)
|
|
||||||
if err != nil && !test.shouldfail[j] {
|
|
||||||
t.Errorf("#%d error: %s", i, err)
|
|
||||||
continue testLoop
|
|
||||||
}
|
|
||||||
if !bytes.Equal(resp, expected) {
|
|
||||||
t.Errorf("#%d got %s, expected %s", i, resp, expected)
|
|
||||||
continue testLoop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAuthLogin(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
authName string
|
|
||||||
server *smtp.ServerInfo
|
|
||||||
err string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
authName: "servername",
|
|
||||||
server: &smtp.ServerInfo{Name: "servername", TLS: true},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// OK to use LoginAuth on localhost without TLS
|
|
||||||
authName: "localhost",
|
|
||||||
server: &smtp.ServerInfo{Name: "localhost", TLS: false},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// NOT OK on non-localhost, even if server says PLAIN is OK.
|
|
||||||
// (We don't know that the server is the real server.)
|
|
||||||
authName: "servername",
|
|
||||||
server: &smtp.ServerInfo{Name: "servername", Auth: []string{"PLAIN"}},
|
|
||||||
err: "unencrypted connection",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
authName: "servername",
|
|
||||||
server: &smtp.ServerInfo{Name: "servername", Auth: []string{"CRAM-MD5"}},
|
|
||||||
err: "unencrypted connection",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
authName: "servername",
|
|
||||||
server: &smtp.ServerInfo{Name: "attacker", TLS: true},
|
|
||||||
err: "wrong host name",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for i, tt := range tests {
|
|
||||||
auth := LoginAuth("foo", "bar", tt.authName)
|
|
||||||
_, _, err := auth.Start(tt.server)
|
|
||||||
got := ""
|
|
||||||
if err != nil {
|
|
||||||
got = err.Error()
|
|
||||||
}
|
|
||||||
if got != tt.err {
|
|
||||||
t.Errorf("%d. got error = %q; want %q", i, got, tt.err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -10,12 +10,11 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/smtp"
|
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/wneessen/go-mail/auth"
|
"github.com/wneessen/go-mail/smtp"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Defaults
|
// Defaults
|
||||||
|
@ -593,7 +592,7 @@ func (c *Client) auth() error {
|
||||||
if !strings.Contains(sat, string(SMTPAuthLogin)) {
|
if !strings.Contains(sat, string(SMTPAuthLogin)) {
|
||||||
return ErrLoginAuthNotSupported
|
return ErrLoginAuthNotSupported
|
||||||
}
|
}
|
||||||
c.sa = auth.LoginAuth(c.user, c.pass, c.host)
|
c.sa = smtp.LoginAuth(c.user, c.pass, c.host)
|
||||||
case SMTPAuthCramMD5:
|
case SMTPAuthCramMD5:
|
||||||
if !strings.Contains(sat, string(SMTPAuthCramMD5)) {
|
if !strings.Contains(sat, string(SMTPAuthCramMD5)) {
|
||||||
return ErrCramMD5AuthNotSupported
|
return ErrCramMD5AuthNotSupported
|
||||||
|
|
|
@ -9,14 +9,13 @@ import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/smtp"
|
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/wneessen/go-mail/auth"
|
"github.com/wneessen/go-mail/smtp"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DefaultHost is used as default hostname for the Client
|
// DefaultHost is used as default hostname for the Client
|
||||||
|
@ -496,7 +495,7 @@ func TestSetSMTPAuthCustom(t *testing.T) {
|
||||||
}{
|
}{
|
||||||
{"SMTPAuth: PLAIN", smtp.PlainAuth("", "", "", ""), "PLAIN", false},
|
{"SMTPAuth: PLAIN", smtp.PlainAuth("", "", "", ""), "PLAIN", false},
|
||||||
{"SMTPAuth: CRAM-MD5", smtp.CRAMMD5Auth("", ""), "CRAM-MD5", false},
|
{"SMTPAuth: CRAM-MD5", smtp.CRAMMD5Auth("", ""), "CRAM-MD5", false},
|
||||||
{"SMTPAuth: LOGIN", auth.LoginAuth("", "", ""), "LOGIN", false},
|
{"SMTPAuth: LOGIN", smtp.LoginAuth("", "", ""), "LOGIN", false},
|
||||||
}
|
}
|
||||||
si := smtp.ServerInfo{TLS: true}
|
si := smtp.ServerInfo{TLS: true}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
@ -584,7 +583,7 @@ func TestClient_DialWithContextInvalidAuth(t *testing.T) {
|
||||||
}
|
}
|
||||||
c.user = "invalid"
|
c.user = "invalid"
|
||||||
c.pass = "invalid"
|
c.pass = "invalid"
|
||||||
c.SetSMTPAuthCustom(auth.LoginAuth("invalid", "invalid", "invalid"))
|
c.SetSMTPAuthCustom(smtp.LoginAuth("invalid", "invalid", "invalid"))
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
if err := c.DialWithContext(ctx); err == nil {
|
if err := c.DialWithContext(ctx); err == nil {
|
||||||
t.Errorf("dial succeeded but was supposed to fail")
|
t.Errorf("dial succeeded but was supposed to fail")
|
||||||
|
|
27
smtp/LICENSE
Normal file
27
smtp/LICENSE
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
Copyright (c) 2009 The Go Authors. All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are
|
||||||
|
met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above
|
||||||
|
copyright notice, this list of conditions and the following disclaimer
|
||||||
|
in the documentation and/or other materials provided with the
|
||||||
|
distribution.
|
||||||
|
* Neither the name of Google Inc. nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from
|
||||||
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
|
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
44
smtp/auth.go
Normal file
44
smtp/auth.go
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2010 The Go Authors. All rights reserved.
|
||||||
|
// SPDX-FileCopyrightText: Copyright (c) 2022-2023 The go-mail Authors
|
||||||
|
//
|
||||||
|
// Original net/smtp code from the Go stdlib by the Go Authors.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// LICENSE file that can be found in this directory.
|
||||||
|
//
|
||||||
|
// go-mail specific modifications by the go-mail Authors.
|
||||||
|
// Licensed under the MIT License.
|
||||||
|
// See [PROJECT ROOT]/LICENSES directory for more information.
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause AND MIT
|
||||||
|
|
||||||
|
package smtp
|
||||||
|
|
||||||
|
// Auth is implemented by an SMTP authentication mechanism.
|
||||||
|
type Auth interface {
|
||||||
|
// Start begins an authentication with a server.
|
||||||
|
// It returns the name of the authentication protocol
|
||||||
|
// and optionally data to include in the initial AUTH message
|
||||||
|
// sent to the server.
|
||||||
|
// If it returns a non-nil error, the SMTP client aborts
|
||||||
|
// the authentication attempt and closes the connection.
|
||||||
|
Start(server *ServerInfo) (proto string, toServer []byte, err error)
|
||||||
|
|
||||||
|
// Next continues the authentication. The server has just sent
|
||||||
|
// the fromServer data. If more is true, the server expects a
|
||||||
|
// response, which Next should return as toServer; otherwise
|
||||||
|
// Next should return toServer == nil.
|
||||||
|
// If Next returns a non-nil error, the SMTP client aborts
|
||||||
|
// the authentication attempt and closes the connection.
|
||||||
|
Next(fromServer []byte, more bool) (toServer []byte, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerInfo records information about an SMTP server.
|
||||||
|
type ServerInfo struct {
|
||||||
|
Name string // SMTP server name
|
||||||
|
TLS bool // using TLS, with valid certificate for Name
|
||||||
|
Auth []string // advertised authentication mechanisms
|
||||||
|
}
|
||||||
|
|
||||||
|
func isLocalhost(name string) bool {
|
||||||
|
return name == "localhost" || name == "127.0.0.1" || name == "::1"
|
||||||
|
}
|
50
smtp/auth_cram_md5.go
Normal file
50
smtp/auth_cram_md5.go
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2010 The Go Authors. All rights reserved.
|
||||||
|
// SPDX-FileCopyrightText: Copyright (c) 2022-2023 The go-mail Authors
|
||||||
|
//
|
||||||
|
// Original net/smtp code from the Go stdlib by the Go Authors.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// LICENSE file that can be found in this directory.
|
||||||
|
//
|
||||||
|
// go-mail specific modifications by the go-mail Authors.
|
||||||
|
// Licensed under the MIT License.
|
||||||
|
// See [PROJECT ROOT]/LICENSES directory for more information.
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause AND MIT
|
||||||
|
|
||||||
|
//go:build go1.19
|
||||||
|
// +build go1.19
|
||||||
|
|
||||||
|
package smtp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/md5"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// cramMD5Auth is the type that satisfies the Auth interface for the "SMTP CRAM_MD5" auth
|
||||||
|
type cramMD5Auth struct {
|
||||||
|
username, secret string
|
||||||
|
}
|
||||||
|
|
||||||
|
// CRAMMD5Auth returns an Auth that implements the CRAM-MD5 authentication
|
||||||
|
// mechanism as defined in RFC 2195.
|
||||||
|
// The returned Auth uses the given username and secret to authenticate
|
||||||
|
// to the server using the challenge-response mechanism.
|
||||||
|
func CRAMMD5Auth(username, secret string) Auth {
|
||||||
|
return &cramMD5Auth{username, secret}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *cramMD5Auth) Start(_ *ServerInfo) (string, []byte, error) {
|
||||||
|
return "CRAM-MD5", nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *cramMD5Auth) Next(fromServer []byte, more bool) ([]byte, error) {
|
||||||
|
if more {
|
||||||
|
d := hmac.New(md5.New, []byte(a.secret))
|
||||||
|
d.Write(fromServer)
|
||||||
|
s := make([]byte, 0, d.Size())
|
||||||
|
return fmt.Appendf(nil, "%s %x", a.username, d.Sum(s)), nil
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
52
smtp/auth_cram_md5_118.go
Normal file
52
smtp/auth_cram_md5_118.go
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2010 The Go Authors. All rights reserved.
|
||||||
|
// SPDX-FileCopyrightText: Copyright (c) 2022-2023 The go-mail Authors
|
||||||
|
//
|
||||||
|
// Original net/smtp code from the Go stdlib by the Go Authors.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// LICENSE file that can be found in this directory.
|
||||||
|
//
|
||||||
|
// go-mail specific modifications by the go-mail Authors.
|
||||||
|
// Licensed under the MIT License.
|
||||||
|
// See [PROJECT ROOT]/LICENSES directory for more information.
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause AND MIT
|
||||||
|
|
||||||
|
//go:build !go1.19
|
||||||
|
// +build !go1.19
|
||||||
|
|
||||||
|
package smtp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/md5"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// cramMD5Auth is the type that satisfies the Auth interface for the "SMTP CRAM_MD5" auth
|
||||||
|
type cramMD5Auth struct {
|
||||||
|
username, secret string
|
||||||
|
}
|
||||||
|
|
||||||
|
// CRAMMD5Auth returns an Auth that implements the CRAM-MD5 authentication
|
||||||
|
// mechanism as defined in RFC 2195.
|
||||||
|
// The returned Auth uses the given username and secret to authenticate
|
||||||
|
// to the server using the challenge-response mechanism.
|
||||||
|
func CRAMMD5Auth(username, secret string) Auth {
|
||||||
|
return &cramMD5Auth{username, secret}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *cramMD5Auth) Start(_ *ServerInfo) (string, []byte, error) {
|
||||||
|
return "CRAM-MD5", nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backport of: https://github.com/golang/go/commit/58158e990f272774e615c9abd8662bf0198c29aa#diff-772fc9f5d0c86f26e35158fb3e7a71a4967d18b4ec23a5dbb60781ab0babf426
|
||||||
|
// to guarantee backwards compatiblity with Go 1.16-1.18
|
||||||
|
func (a *cramMD5Auth) Next(fromServer []byte, more bool) ([]byte, error) {
|
||||||
|
if more {
|
||||||
|
d := hmac.New(md5.New, []byte(a.secret))
|
||||||
|
d.Write(fromServer)
|
||||||
|
s := make([]byte, 0, d.Size())
|
||||||
|
return []byte(fmt.Sprintf("%s %x", a.username, d.Sum(s))), nil
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
|
@ -1,16 +1,15 @@
|
||||||
// SPDX-FileCopyrightText: 2022 Winni Neessen <winni@neessen.dev>
|
// SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
// Package auth implements the LOGIN and MD5-DIGEST smtp authentication mechanisms
|
package smtp
|
||||||
package auth
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/smtp"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// loginAuth is the type that satisfies the Auth interface for the "SMTP LOGIN" auth
|
||||||
type loginAuth struct {
|
type loginAuth struct {
|
||||||
username, password string
|
username, password string
|
||||||
host string
|
host string
|
||||||
|
@ -35,15 +34,11 @@ const (
|
||||||
// LoginAuth will only send the credentials if the connection is using TLS
|
// LoginAuth will only send the credentials if the connection is using TLS
|
||||||
// or is connected to localhost. Otherwise authentication will fail with an
|
// or is connected to localhost. Otherwise authentication will fail with an
|
||||||
// error, without sending the credentials.
|
// error, without sending the credentials.
|
||||||
func LoginAuth(username, password, host string) smtp.Auth {
|
func LoginAuth(username, password, host string) Auth {
|
||||||
return &loginAuth{username, password, host}
|
return &loginAuth{username, password, host}
|
||||||
}
|
}
|
||||||
|
|
||||||
func isLocalhost(name string) bool {
|
func (a *loginAuth) Start(server *ServerInfo) (string, []byte, error) {
|
||||||
return name == "localhost" || name == "127.0.0.1" || name == "::1"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
|
|
||||||
// Must have TLS, or else localhost server.
|
// Must have TLS, or else localhost server.
|
||||||
// Note: If TLS is not true, then we can't trust ANYTHING in ServerInfo.
|
// Note: If TLS is not true, then we can't trust ANYTHING in ServerInfo.
|
||||||
// In particular, it doesn't matter if the server advertises LOGIN auth.
|
// In particular, it doesn't matter if the server advertises LOGIN auth.
|
60
smtp/auth_plain.go
Normal file
60
smtp/auth_plain.go
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2010 The Go Authors. All rights reserved.
|
||||||
|
// SPDX-FileCopyrightText: Copyright (c) 2022-2023 The go-mail Authors
|
||||||
|
//
|
||||||
|
// Original net/smtp code from the Go stdlib by the Go Authors.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// LICENSE file that can be found in this directory.
|
||||||
|
//
|
||||||
|
// go-mail specific modifications by the go-mail Authors.
|
||||||
|
// Licensed under the MIT License.
|
||||||
|
// See [PROJECT ROOT]/LICENSES directory for more information.
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause AND MIT
|
||||||
|
|
||||||
|
package smtp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// plainAuth is the type that satisfies the Auth interface for the "SMTP PLAIN" auth
|
||||||
|
type plainAuth struct {
|
||||||
|
identity, username, password string
|
||||||
|
host string
|
||||||
|
}
|
||||||
|
|
||||||
|
// PlainAuth returns an Auth that implements the PLAIN authentication
|
||||||
|
// mechanism as defined in RFC 4616. The returned Auth uses the given
|
||||||
|
// username and password to authenticate to host and act as identity.
|
||||||
|
// Usually identity should be the empty string, to act as username.
|
||||||
|
//
|
||||||
|
// PlainAuth will only send the credentials if the connection is using TLS
|
||||||
|
// or is connected to localhost. Otherwise authentication will fail with an
|
||||||
|
// error, without sending the credentials.
|
||||||
|
func PlainAuth(identity, username, password, host string) Auth {
|
||||||
|
return &plainAuth{identity, username, password, host}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *plainAuth) Start(server *ServerInfo) (string, []byte, error) {
|
||||||
|
// Must have TLS, or else localhost server.
|
||||||
|
// Note: If TLS is not true, then we can't trust ANYTHING in ServerInfo.
|
||||||
|
// In particular, it doesn't matter if the server advertises PLAIN auth.
|
||||||
|
// That might just be the attacker saying
|
||||||
|
// "it's ok, you can trust me with your password."
|
||||||
|
if !server.TLS && !isLocalhost(server.Name) {
|
||||||
|
return "", nil, errors.New("unencrypted connection")
|
||||||
|
}
|
||||||
|
if server.Name != a.host {
|
||||||
|
return "", nil, errors.New("wrong host name")
|
||||||
|
}
|
||||||
|
resp := []byte(a.identity + "\x00" + a.username + "\x00" + a.password)
|
||||||
|
return "PLAIN", resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *plainAuth) Next(_ []byte, more bool) ([]byte, error) {
|
||||||
|
if more {
|
||||||
|
// We've already sent everything.
|
||||||
|
return nil, errors.New("unexpected server challenge")
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
93
smtp/example_test.go
Normal file
93
smtp/example_test.go
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2010 The Go Authors. All rights reserved.
|
||||||
|
// SPDX-FileCopyrightText: Copyright (c) 2022-2023 The go-mail Authors
|
||||||
|
//
|
||||||
|
// Original net/smtp code from the Go stdlib by the Go Authors.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// LICENSE file that can be found in this directory.
|
||||||
|
//
|
||||||
|
// go-mail specific modifications by the go-mail Authors.
|
||||||
|
// Licensed under the MIT License.
|
||||||
|
// See [PROJECT ROOT]/LICENSES directory for more information.
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause AND MIT
|
||||||
|
|
||||||
|
package smtp_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/wneessen/go-mail/smtp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Example() {
|
||||||
|
// Connect to the remote SMTP server.
|
||||||
|
c, err := smtp.Dial("mail.example.com:25")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the sender and recipient first
|
||||||
|
if err := c.Mail("sender@example.org"); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := c.Rcpt("recipient@example.net"); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the email body.
|
||||||
|
wc, err := c.Data()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
_, err = fmt.Fprintf(wc, "This is the email body")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
err = wc.Close()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the QUIT command and close the connection.
|
||||||
|
err = c.Quit()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// variables to make ExamplePlainAuth compile, without adding
|
||||||
|
// unnecessary noise there.
|
||||||
|
var (
|
||||||
|
from = "gopher@example.net"
|
||||||
|
msg = []byte("dummy message")
|
||||||
|
recipients = []string{"foo@example.com"}
|
||||||
|
)
|
||||||
|
|
||||||
|
func ExamplePlainAuth() {
|
||||||
|
// hostname is used by PlainAuth to validate the TLS certificate.
|
||||||
|
hostname := "mail.example.com"
|
||||||
|
auth := smtp.PlainAuth("", "user@example.com", "password", hostname)
|
||||||
|
|
||||||
|
err := smtp.SendMail(hostname+":25", auth, from, recipients, msg)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleSendMail() {
|
||||||
|
// Set up authentication information.
|
||||||
|
auth := smtp.PlainAuth("", "user@example.com", "password", "mail.example.com")
|
||||||
|
|
||||||
|
// Connect to the server, authenticate, set the sender and recipient,
|
||||||
|
// and send the email all in one step.
|
||||||
|
to := []string{"recipient@example.net"}
|
||||||
|
msg := []byte("To: recipient@example.net\r\n" +
|
||||||
|
"Subject: discount Gophers!\r\n" +
|
||||||
|
"\r\n" +
|
||||||
|
"This is the email body.\r\n")
|
||||||
|
err := smtp.SendMail("mail.example.com:25", auth, "sender@example.org", to, msg)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
424
smtp/smtp.go
Normal file
424
smtp/smtp.go
Normal file
|
@ -0,0 +1,424 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2010 The Go Authors. All rights reserved.
|
||||||
|
// SPDX-FileCopyrightText: Copyright (c) 2022-2023 The go-mail Authors
|
||||||
|
//
|
||||||
|
// Original net/smtp code from the Go stdlib by the Go Authors.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// LICENSE file that can be found in this directory.
|
||||||
|
//
|
||||||
|
// go-mail specific modifications by the go-mail Authors.
|
||||||
|
// Licensed under the MIT License.
|
||||||
|
// See [PROJECT ROOT]/LICENSES directory for more information.
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause AND MIT
|
||||||
|
|
||||||
|
// Package smtp implements the Simple Mail Transfer Protocol as defined in RFC 5321.
|
||||||
|
// It also implements the following extensions:
|
||||||
|
//
|
||||||
|
// 8BITMIME RFC 1652
|
||||||
|
// AUTH RFC 2554
|
||||||
|
// STARTTLS RFC 3207
|
||||||
|
//
|
||||||
|
// Additional extensions may be handled by clients.
|
||||||
|
//
|
||||||
|
// The smtp package is frozen and is not accepting new features.
|
||||||
|
// Some external packages provide more functionality. See:
|
||||||
|
//
|
||||||
|
// https://godoc.org/?q=smtp
|
||||||
|
package smtp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/textproto"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A Client represents a client connection to an SMTP server.
|
||||||
|
type Client struct {
|
||||||
|
// Text is the textproto.Conn used by the Client. It is exported to allow for
|
||||||
|
// clients to add extensions.
|
||||||
|
Text *textproto.Conn
|
||||||
|
// keep a reference to the connection so it can be used to create a TLS
|
||||||
|
// connection later
|
||||||
|
conn net.Conn
|
||||||
|
// whether the Client is using TLS
|
||||||
|
tls bool
|
||||||
|
serverName string
|
||||||
|
// map of supported extensions
|
||||||
|
ext map[string]string
|
||||||
|
// supported auth mechanisms
|
||||||
|
auth []string
|
||||||
|
localName string // the name to use in HELO/EHLO
|
||||||
|
didHello bool // whether we've said HELO/EHLO
|
||||||
|
helloError error // the error from the hello
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dial returns a new Client connected to an SMTP server at addr.
|
||||||
|
// The addr must include a port, as in "mail.example.com:smtp".
|
||||||
|
func Dial(addr string) (*Client, error) {
|
||||||
|
conn, err := net.Dial("tcp", addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
host, _, _ := net.SplitHostPort(addr)
|
||||||
|
return NewClient(conn, host)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClient returns a new Client using an existing connection and host as a
|
||||||
|
// server name to be used when authenticating.
|
||||||
|
func NewClient(conn net.Conn, host string) (*Client, error) {
|
||||||
|
text := textproto.NewConn(conn)
|
||||||
|
_, _, err := text.ReadResponse(220)
|
||||||
|
if err != nil {
|
||||||
|
if cerr := text.Close(); cerr != nil {
|
||||||
|
return nil, fmt.Errorf("%w, %s", err, cerr)
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
c := &Client{Text: text, conn: conn, serverName: host, localName: "localhost"}
|
||||||
|
_, c.tls = conn.(*tls.Conn)
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the connection.
|
||||||
|
func (c *Client) Close() error {
|
||||||
|
return c.Text.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// hello runs a hello exchange if needed.
|
||||||
|
func (c *Client) hello() error {
|
||||||
|
if !c.didHello {
|
||||||
|
c.didHello = true
|
||||||
|
err := c.ehlo()
|
||||||
|
if err != nil {
|
||||||
|
c.helloError = c.helo()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return c.helloError
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hello sends a HELO or EHLO to the server as the given host name.
|
||||||
|
// Calling this method is only necessary if the client needs control
|
||||||
|
// over the host name used. The client will introduce itself as "localhost"
|
||||||
|
// automatically otherwise. If Hello is called, it must be called before
|
||||||
|
// any of the other methods.
|
||||||
|
func (c *Client) Hello(localName string) error {
|
||||||
|
if err := validateLine(localName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if c.didHello {
|
||||||
|
return errors.New("smtp: Hello called after other methods")
|
||||||
|
}
|
||||||
|
c.localName = localName
|
||||||
|
return c.hello()
|
||||||
|
}
|
||||||
|
|
||||||
|
// cmd is a convenience function that sends a command and returns the response
|
||||||
|
func (c *Client) cmd(expectCode int, format string, args ...interface{}) (int, string, error) {
|
||||||
|
id, err := c.Text.Cmd(format, args...)
|
||||||
|
if err != nil {
|
||||||
|
return 0, "", err
|
||||||
|
}
|
||||||
|
c.Text.StartResponse(id)
|
||||||
|
defer c.Text.EndResponse(id)
|
||||||
|
code, msg, err := c.Text.ReadResponse(expectCode)
|
||||||
|
return code, msg, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// helo sends the HELO greeting to the server. It should be used only when the
|
||||||
|
// server does not support ehlo.
|
||||||
|
func (c *Client) helo() error {
|
||||||
|
c.ext = nil
|
||||||
|
_, _, err := c.cmd(250, "HELO %s", c.localName)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartTLS sends the STARTTLS command and encrypts all further communication.
|
||||||
|
// Only servers that advertise the STARTTLS extension support this function.
|
||||||
|
func (c *Client) StartTLS(config *tls.Config) error {
|
||||||
|
if err := c.hello(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, _, err := c.cmd(220, "STARTTLS")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.conn = tls.Client(c.conn, config)
|
||||||
|
c.Text = textproto.NewConn(c.conn)
|
||||||
|
c.tls = true
|
||||||
|
return c.ehlo()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TLSConnectionState returns the client's TLS connection state.
|
||||||
|
// The return values are their zero values if StartTLS did
|
||||||
|
// not succeed.
|
||||||
|
func (c *Client) TLSConnectionState() (state tls.ConnectionState, ok bool) {
|
||||||
|
tc, ok := c.conn.(*tls.Conn)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return tc.ConnectionState(), true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify checks the validity of an email address on the server.
|
||||||
|
// If Verify returns nil, the address is valid. A non-nil return
|
||||||
|
// does not necessarily indicate an invalid address. Many servers
|
||||||
|
// will not verify addresses for security reasons.
|
||||||
|
func (c *Client) Verify(addr string) error {
|
||||||
|
if err := validateLine(addr); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := c.hello(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, _, err := c.cmd(250, "VRFY %s", addr)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auth authenticates a client using the provided authentication mechanism.
|
||||||
|
// A failed authentication closes the connection.
|
||||||
|
// Only servers that advertise the AUTH extension support this function.
|
||||||
|
func (c *Client) Auth(a Auth) error {
|
||||||
|
if err := c.hello(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
encoding := base64.StdEncoding
|
||||||
|
mech, resp, err := a.Start(&ServerInfo{c.serverName, c.tls, c.auth})
|
||||||
|
if err != nil {
|
||||||
|
if qerr := c.Quit(); qerr != nil {
|
||||||
|
return fmt.Errorf("%w, %s", err, qerr)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
resp64 := make([]byte, encoding.EncodedLen(len(resp)))
|
||||||
|
encoding.Encode(resp64, resp)
|
||||||
|
code, msg64, err := c.cmd(0, strings.TrimSpace(fmt.Sprintf("AUTH %s %s", mech, resp64)))
|
||||||
|
for err == nil {
|
||||||
|
var msg []byte
|
||||||
|
switch code {
|
||||||
|
case 334:
|
||||||
|
msg, err = encoding.DecodeString(msg64)
|
||||||
|
case 235:
|
||||||
|
// the last message isn't base64 because it isn't a challenge
|
||||||
|
msg = []byte(msg64)
|
||||||
|
default:
|
||||||
|
err = &textproto.Error{Code: code, Msg: msg64}
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
resp, err = a.Next(msg, code == 334)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
// abort the AUTH
|
||||||
|
_, _, _ = c.cmd(501, "*")
|
||||||
|
_ = c.Quit()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if resp == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
resp64 = make([]byte, encoding.EncodedLen(len(resp)))
|
||||||
|
encoding.Encode(resp64, resp)
|
||||||
|
code, msg64, err = c.cmd(0, string(resp64))
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mail issues a MAIL command to the server using the provided email address.
|
||||||
|
// If the server supports the 8BITMIME extension, Mail adds the BODY=8BITMIME
|
||||||
|
// parameter. If the server supports the SMTPUTF8 extension, Mail adds the
|
||||||
|
// SMTPUTF8 parameter.
|
||||||
|
// This initiates a mail transaction and is followed by one or more Rcpt calls.
|
||||||
|
func (c *Client) Mail(from string) error {
|
||||||
|
if err := validateLine(from); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := c.hello(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cmdStr := "MAIL FROM:<%s>"
|
||||||
|
if c.ext != nil {
|
||||||
|
if _, ok := c.ext["8BITMIME"]; ok {
|
||||||
|
cmdStr += " BODY=8BITMIME"
|
||||||
|
}
|
||||||
|
if _, ok := c.ext["SMTPUTF8"]; ok {
|
||||||
|
cmdStr += " SMTPUTF8"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, _, err := c.cmd(250, cmdStr, from)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rcpt issues a RCPT command to the server using the provided email address.
|
||||||
|
// A call to Rcpt must be preceded by a call to Mail and may be followed by
|
||||||
|
// a Data call or another Rcpt call.
|
||||||
|
func (c *Client) Rcpt(to string) error {
|
||||||
|
if err := validateLine(to); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, _, err := c.cmd(25, "RCPT TO:<%s>", to)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type dataCloser struct {
|
||||||
|
c *Client
|
||||||
|
io.WriteCloser
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dataCloser) Close() error {
|
||||||
|
_ = d.WriteCloser.Close()
|
||||||
|
_, _, err := d.c.Text.ReadResponse(250)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data issues a DATA command to the server and returns a writer that
|
||||||
|
// can be used to write the mail headers and body. The caller should
|
||||||
|
// close the writer before calling any more methods on c. A call to
|
||||||
|
// Data must be preceded by one or more calls to Rcpt.
|
||||||
|
func (c *Client) Data() (io.WriteCloser, error) {
|
||||||
|
_, _, err := c.cmd(354, "DATA")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &dataCloser{c, c.Text.DotWriter()}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var testHookStartTLS func(*tls.Config) // nil, except for tests
|
||||||
|
|
||||||
|
// SendMail connects to the server at addr, switches to TLS if
|
||||||
|
// possible, authenticates with the optional mechanism a if possible,
|
||||||
|
// and then sends an email from address from, to addresses to, with
|
||||||
|
// message msg.
|
||||||
|
// The addr must include a port, as in "mail.example.com:smtp".
|
||||||
|
//
|
||||||
|
// The addresses in the to parameter are the SMTP RCPT addresses.
|
||||||
|
//
|
||||||
|
// The msg parameter should be an RFC 822-style email with headers
|
||||||
|
// first, a blank line, and then the message body. The lines of msg
|
||||||
|
// should be CRLF terminated. The msg headers should usually include
|
||||||
|
// fields such as "From", "To", "Subject", and "Cc". Sending "Bcc"
|
||||||
|
// messages is accomplished by including an email address in the to
|
||||||
|
// parameter but not including it in the msg headers.
|
||||||
|
//
|
||||||
|
// The SendMail function and the net/smtp package are low-level
|
||||||
|
// mechanisms and provide no support for DKIM signing, MIME
|
||||||
|
// attachments (see the mime/multipart package), or other mail
|
||||||
|
// functionality. Higher-level packages exist outside of the standard
|
||||||
|
// library.
|
||||||
|
func SendMail(addr string, a Auth, from string, to []string, msg []byte) error {
|
||||||
|
if err := validateLine(from); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, recp := range to {
|
||||||
|
if err := validateLine(recp); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c, err := Dial(addr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
_ = c.Close()
|
||||||
|
}()
|
||||||
|
if err = c.hello(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if ok, _ := c.Extension("STARTTLS"); ok {
|
||||||
|
config := &tls.Config{ServerName: c.serverName}
|
||||||
|
if testHookStartTLS != nil {
|
||||||
|
testHookStartTLS(config)
|
||||||
|
}
|
||||||
|
if err = c.StartTLS(config); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if a != nil && c.ext != nil {
|
||||||
|
if _, ok := c.ext["AUTH"]; !ok {
|
||||||
|
return errors.New("smtp: server doesn't support AUTH")
|
||||||
|
}
|
||||||
|
if err = c.Auth(a); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err = c.Mail(from); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, addr := range to {
|
||||||
|
if err = c.Rcpt(addr); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w, err := c.Data()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = w.Write(msg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = w.Close()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return c.Quit()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extension reports whether an extension is support by the server.
|
||||||
|
// The extension name is case-insensitive. If the extension is supported,
|
||||||
|
// Extension also returns a string that contains any parameters the
|
||||||
|
// server specifies for the extension.
|
||||||
|
func (c *Client) Extension(ext string) (bool, string) {
|
||||||
|
if err := c.hello(); err != nil {
|
||||||
|
return false, ""
|
||||||
|
}
|
||||||
|
if c.ext == nil {
|
||||||
|
return false, ""
|
||||||
|
}
|
||||||
|
ext = strings.ToUpper(ext)
|
||||||
|
param, ok := c.ext[ext]
|
||||||
|
return ok, param
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset sends the RSET command to the server, aborting the current mail
|
||||||
|
// transaction.
|
||||||
|
func (c *Client) Reset() error {
|
||||||
|
if err := c.hello(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, _, err := c.cmd(250, "RSET")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Noop sends the NOOP command to the server. It does nothing but check
|
||||||
|
// that the connection to the server is okay.
|
||||||
|
func (c *Client) Noop() error {
|
||||||
|
if err := c.hello(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, _, err := c.cmd(250, "NOOP")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quit sends the QUIT command and closes the connection to the server.
|
||||||
|
func (c *Client) Quit() error {
|
||||||
|
if err := c.hello(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, _, err := c.cmd(221, "QUIT")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return c.Text.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateLine checks to see if a line has CR or LF as per RFC 5321.
|
||||||
|
func validateLine(line string) error {
|
||||||
|
if strings.ContainsAny(line, "\n\r") {
|
||||||
|
return errors.New("smtp: A line must not contain CR or LF")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
42
smtp/smtp_ehlo.go
Normal file
42
smtp/smtp_ehlo.go
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2010 The Go Authors. All rights reserved.
|
||||||
|
// SPDX-FileCopyrightText: Copyright (c) 2022-2023 The go-mail Authors
|
||||||
|
//
|
||||||
|
// Original net/smtp code from the Go stdlib by the Go Authors.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// LICENSE file that can be found in this directory.
|
||||||
|
//
|
||||||
|
// go-mail specific modifications by the go-mail Authors.
|
||||||
|
// Licensed under the MIT License.
|
||||||
|
// See [PROJECT ROOT]/LICENSES directory for more information.
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause AND MIT
|
||||||
|
|
||||||
|
//go:build go1.18
|
||||||
|
// +build go1.18
|
||||||
|
|
||||||
|
package smtp
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
// ehlo sends the EHLO (extended hello) greeting to the server. It
|
||||||
|
// should be the preferred greeting for servers that support it.
|
||||||
|
func (c *Client) ehlo() error {
|
||||||
|
_, msg, err := c.cmd(250, "EHLO %s", c.localName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ext := make(map[string]string)
|
||||||
|
extList := strings.Split(msg, "\n")
|
||||||
|
if len(extList) > 1 {
|
||||||
|
extList = extList[1:]
|
||||||
|
for _, line := range extList {
|
||||||
|
k, v, _ := strings.Cut(line, " ")
|
||||||
|
ext[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if mechs, ok := ext["AUTH"]; ok {
|
||||||
|
c.auth = strings.Split(mechs, " ")
|
||||||
|
}
|
||||||
|
c.ext = ext
|
||||||
|
return err
|
||||||
|
}
|
49
smtp/smtp_ehlo_117.go
Normal file
49
smtp/smtp_ehlo_117.go
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2010 The Go Authors. All rights reserved.
|
||||||
|
// SPDX-FileCopyrightText: Copyright (c) 2022-2023 The go-mail Authors
|
||||||
|
//
|
||||||
|
// Original net/smtp code from the Go stdlib by the Go Authors.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// LICENSE file that can be found in this directory.
|
||||||
|
//
|
||||||
|
// go-mail specific modifications by the go-mail Authors.
|
||||||
|
// Licensed under the MIT License.
|
||||||
|
// See [PROJECT ROOT]/LICENSES directory for more information.
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause AND MIT
|
||||||
|
|
||||||
|
//go:build !go1.18
|
||||||
|
// +build !go1.18
|
||||||
|
|
||||||
|
package smtp
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
// ehlo sends the EHLO (extended hello) greeting to the server. It
|
||||||
|
// should be the preferred greeting for servers that support it.
|
||||||
|
//
|
||||||
|
// Backport of: https://github.com/golang/go/commit/4d8db00641cc9ff4f44de7df9b8c4f4a4f9416ee#diff-4f6f6bdb9891d4dd271f9f31430420a2e44018fe4ee539576faf458bebb3cee4
|
||||||
|
// to guarantee backwards compatiblity with Go 1.16/1.17:w
|
||||||
|
func (c *Client) ehlo() error {
|
||||||
|
_, msg, err := c.cmd(250, "EHLO %s", c.localName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ext := make(map[string]string)
|
||||||
|
extList := strings.Split(msg, "\n")
|
||||||
|
if len(extList) > 1 {
|
||||||
|
extList = extList[1:]
|
||||||
|
for _, line := range extList {
|
||||||
|
args := strings.SplitN(line, " ", 2)
|
||||||
|
if len(args) > 1 {
|
||||||
|
ext[args[0]] = args[1]
|
||||||
|
} else {
|
||||||
|
ext[args[0]] = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if mechs, ok := ext["AUTH"]; ok {
|
||||||
|
c.auth = strings.Split(mechs, " ")
|
||||||
|
}
|
||||||
|
c.ext = ext
|
||||||
|
return err
|
||||||
|
}
|
1296
smtp/smtp_test.go
Normal file
1296
smtp/smtp_test.go
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue