mirror of
https://github.com/wneessen/go-mail.git
synced 2024-11-22 22:00:49 +01:00
Compare commits
17 commits
0354aa1c7e
...
2d92fc0efe
Author | SHA1 | Date | |
---|---|---|---|
|
2d92fc0efe | ||
cec7e38332 | |||
|
9ad77012e3 | ||
55e5a1536e | |||
c7d0a03ddc | |||
f79c1b8ebe | |||
df1a141368 | |||
e2ed5b747a | |||
2bd950469a | |||
3c29f68cc1 | |||
f5531eae14 | |||
91caf200ec | |||
|
63e6fc882d | ||
957cd8e0ca | |||
09133ef2a4 | |||
bf44fd2ad1 | |||
7bebdda27c |
11 changed files with 291 additions and 40 deletions
6
.github/workflows/codeql-analysis.yml
vendored
6
.github/workflows/codeql-analysis.yml
vendored
|
@ -54,7 +54,7 @@ jobs:
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@f779452ac5af1c261dce0346a8f964149f49322b # v3.26.13
|
uses: github/codeql-action/init@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
|
@ -65,7 +65,7 @@ jobs:
|
||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||||
# If this step fails, then you should remove it and run the build manually (see below)
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@f779452ac5af1c261dce0346a8f964149f49322b # v3.26.13
|
uses: github/codeql-action/autobuild@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
# 📚 https://git.io/JvXDl
|
# 📚 https://git.io/JvXDl
|
||||||
|
@ -79,4 +79,4 @@ jobs:
|
||||||
# make release
|
# make release
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@f779452ac5af1c261dce0346a8f964149f49322b # v3.26.13
|
uses: github/codeql-action/analyze@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
|
||||||
|
|
2
.github/workflows/dependency-review.yml
vendored
2
.github/workflows/dependency-review.yml
vendored
|
@ -28,4 +28,4 @@ jobs:
|
||||||
- name: 'Checkout Repository'
|
- name: 'Checkout Repository'
|
||||||
uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||||
- name: 'Dependency Review'
|
- name: 'Dependency Review'
|
||||||
uses: actions/dependency-review-action@5a2ce3f5b92ee19cbb1541a4984c76d921601d7c # v4.3.4
|
uses: actions/dependency-review-action@a6993e2c61fd5dc440b409aa1d6904921c5e1894 # v4.3.5
|
||||||
|
|
2
.github/workflows/scorecards.yml
vendored
2
.github/workflows/scorecards.yml
vendored
|
@ -75,6 +75,6 @@ jobs:
|
||||||
|
|
||||||
# Upload the results to GitHub's code scanning dashboard.
|
# Upload the results to GitHub's code scanning dashboard.
|
||||||
- name: "Upload to code-scanning"
|
- name: "Upload to code-scanning"
|
||||||
uses: github/codeql-action/upload-sarif@f779452ac5af1c261dce0346a8f964149f49322b # v3.26.13
|
uses: github/codeql-action/upload-sarif@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
|
||||||
with:
|
with:
|
||||||
sarif_file: results.sarif
|
sarif_file: results.sarif
|
||||||
|
|
73
auth.go
73
auth.go
|
@ -4,7 +4,11 @@
|
||||||
|
|
||||||
package mail
|
package mail
|
||||||
|
|
||||||
import "errors"
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
// SMTPAuthType is a type wrapper for a string type. It represents the type of SMTP authentication
|
// SMTPAuthType is a type wrapper for a string type. It represents the type of SMTP authentication
|
||||||
// mechanism to be used.
|
// mechanism to be used.
|
||||||
|
@ -44,6 +48,25 @@ const (
|
||||||
// https://datatracker.ietf.org/doc/html/draft-murchison-sasl-login-00
|
// https://datatracker.ietf.org/doc/html/draft-murchison-sasl-login-00
|
||||||
SMTPAuthLogin SMTPAuthType = "LOGIN"
|
SMTPAuthLogin SMTPAuthType = "LOGIN"
|
||||||
|
|
||||||
|
// SMTPAuthLoginNoEnc is the "LOGIN" SASL authentication mechanism. This authentication mechanism
|
||||||
|
// does not have an official RFC that could be followed. There is a spec by Microsoft and an
|
||||||
|
// IETF draft. The IETF draft is more lax than the MS spec, therefore we follow the I-D, which
|
||||||
|
// automatically matches the MS spec.
|
||||||
|
//
|
||||||
|
// Since the "LOGIN" SASL authentication mechanism transmits the username and password in
|
||||||
|
// plaintext over the internet connection, by default we only allow this mechanism over
|
||||||
|
// a TLS secured connection. This authentiation mechanism overrides this default and will
|
||||||
|
// allow LOGIN authentication via an unencrypted channel. This can be useful if the
|
||||||
|
// connection has already been secured in a different way (e. g. a SSH tunnel)
|
||||||
|
//
|
||||||
|
// Note: Use this authentication method with caution. If used in the wrong way, you might
|
||||||
|
// expose your authentication information over unencrypted channels!
|
||||||
|
//
|
||||||
|
// https://msopenspecs.azureedge.net/files/MS-XLOGIN/%5bMS-XLOGIN%5d.pdf
|
||||||
|
//
|
||||||
|
// https://datatracker.ietf.org/doc/html/draft-murchison-sasl-login-00
|
||||||
|
SMTPAuthLoginNoEnc SMTPAuthType = "LOGIN-NOENC"
|
||||||
|
|
||||||
// SMTPAuthNoAuth is equivalent to performing no authentication at all. It is a convenience
|
// SMTPAuthNoAuth is equivalent to performing no authentication at all. It is a convenience
|
||||||
// option and should not be used. Instead, for mail servers that do no support/require
|
// option and should not be used. Instead, for mail servers that do no support/require
|
||||||
// authentication, the Client should not be passed the WithSMTPAuth option at all.
|
// authentication, the Client should not be passed the WithSMTPAuth option at all.
|
||||||
|
@ -58,6 +81,20 @@ const (
|
||||||
// https://datatracker.ietf.org/doc/html/rfc4616/
|
// https://datatracker.ietf.org/doc/html/rfc4616/
|
||||||
SMTPAuthPlain SMTPAuthType = "PLAIN"
|
SMTPAuthPlain SMTPAuthType = "PLAIN"
|
||||||
|
|
||||||
|
// SMTPAuthPlainNoEnc is the "PLAIN" authentication mechanism as described in RFC 4616.
|
||||||
|
//
|
||||||
|
// Since the "PLAIN" SASL authentication mechanism transmits the username and password in
|
||||||
|
// plaintext over the internet connection, by default we only allow this mechanism over
|
||||||
|
// a TLS secured connection. This authentiation mechanism overrides this default and will
|
||||||
|
// allow PLAIN authentication via an unencrypted channel. This can be useful if the
|
||||||
|
// connection has already been secured in a different way (e. g. a SSH tunnel)
|
||||||
|
//
|
||||||
|
// Note: Use this authentication method with caution. If used in the wrong way, you might
|
||||||
|
// expose your authentication information over unencrypted channels!
|
||||||
|
//
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc4616/
|
||||||
|
SMTPAuthPlainNoEnc SMTPAuthType = "PLAIN-NOENC"
|
||||||
|
|
||||||
// SMTPAuthXOAUTH2 is the "XOAUTH2" SASL authentication mechanism.
|
// SMTPAuthXOAUTH2 is the "XOAUTH2" SASL authentication mechanism.
|
||||||
// https://developers.google.com/gmail/imap/xoauth2-protocol
|
// https://developers.google.com/gmail/imap/xoauth2-protocol
|
||||||
SMTPAuthXOAUTH2 SMTPAuthType = "XOAUTH2"
|
SMTPAuthXOAUTH2 SMTPAuthType = "XOAUTH2"
|
||||||
|
@ -134,3 +171,37 @@ var (
|
||||||
// authentication type.
|
// authentication type.
|
||||||
ErrSCRAMSHA256PLUSAuthNotSupported = errors.New("server does not support SMTP AUTH type: SCRAM-SHA-256-PLUS")
|
ErrSCRAMSHA256PLUSAuthNotSupported = errors.New("server does not support SMTP AUTH type: SCRAM-SHA-256-PLUS")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// UnmarshalString satisfies the fig.StringUnmarshaler interface for the SMTPAuthType type
|
||||||
|
// https://pkg.go.dev/github.com/kkyr/fig#StringUnmarshaler
|
||||||
|
func (sa *SMTPAuthType) UnmarshalString(value string) error {
|
||||||
|
switch strings.ToLower(value) {
|
||||||
|
case "cram-md5", "crammd5", "cram":
|
||||||
|
*sa = SMTPAuthCramMD5
|
||||||
|
case "custom":
|
||||||
|
*sa = SMTPAuthCustom
|
||||||
|
case "login":
|
||||||
|
*sa = SMTPAuthLogin
|
||||||
|
case "login-noenc":
|
||||||
|
*sa = SMTPAuthLoginNoEnc
|
||||||
|
case "none", "noauth", "no":
|
||||||
|
*sa = SMTPAuthNoAuth
|
||||||
|
case "plain":
|
||||||
|
*sa = SMTPAuthPlain
|
||||||
|
case "plain-noenc":
|
||||||
|
*sa = SMTPAuthPlainNoEnc
|
||||||
|
case "scram-sha-1", "scram-sha1", "scramsha1":
|
||||||
|
*sa = SMTPAuthSCRAMSHA1
|
||||||
|
case "scram-sha-1-plus", "scram-sha1-plus", "scramsha1plus":
|
||||||
|
*sa = SMTPAuthSCRAMSHA1PLUS
|
||||||
|
case "scram-sha-256", "scram-sha256", "scramsha256":
|
||||||
|
*sa = SMTPAuthSCRAMSHA256
|
||||||
|
case "scram-sha-256-plus", "scram-sha256-plus", "scramsha256plus":
|
||||||
|
*sa = SMTPAuthSCRAMSHA256PLUS
|
||||||
|
case "xoauth2", "oauth2":
|
||||||
|
*sa = SMTPAuthXOAUTH2
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported SMTP auth type: %s", value)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
59
auth_test.go
Normal file
59
auth_test.go
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 The go-mail Authors
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package mail
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestSMTPAuthType_UnmarshalString(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
authString string
|
||||||
|
expected SMTPAuthType
|
||||||
|
}{
|
||||||
|
{"CRAM-MD5: cram-md5", "cram-md5", SMTPAuthCramMD5},
|
||||||
|
{"CRAM-MD5: crammd5", "crammd5", SMTPAuthCramMD5},
|
||||||
|
{"CRAM-MD5: cram", "cram", SMTPAuthCramMD5},
|
||||||
|
{"CUSTOM", "custom", SMTPAuthCustom},
|
||||||
|
{"LOGIN", "login", SMTPAuthLogin},
|
||||||
|
{"LOGIN-NOENC", "login-noenc", SMTPAuthLoginNoEnc},
|
||||||
|
{"NONE: none", "none", SMTPAuthNoAuth},
|
||||||
|
{"NONE: noauth", "noauth", SMTPAuthNoAuth},
|
||||||
|
{"NONE: no", "no", SMTPAuthNoAuth},
|
||||||
|
{"PLAIN", "plain", SMTPAuthPlain},
|
||||||
|
{"PLAIN-NOENC", "plain-noenc", SMTPAuthPlainNoEnc},
|
||||||
|
{"SCRAM-SHA-1: scram-sha-1", "scram-sha-1", SMTPAuthSCRAMSHA1},
|
||||||
|
{"SCRAM-SHA-1: scram-sha1", "scram-sha1", SMTPAuthSCRAMSHA1},
|
||||||
|
{"SCRAM-SHA-1: scramsha1", "scramsha1", SMTPAuthSCRAMSHA1},
|
||||||
|
{"SCRAM-SHA-1-PLUS: scram-sha-1-plus", "scram-sha-1-plus", SMTPAuthSCRAMSHA1PLUS},
|
||||||
|
{"SCRAM-SHA-1-PLUS: scram-sha1-plus", "scram-sha1-plus", SMTPAuthSCRAMSHA1PLUS},
|
||||||
|
{"SCRAM-SHA-1-PLUS: scramsha1plus", "scramsha1plus", SMTPAuthSCRAMSHA1PLUS},
|
||||||
|
{"SCRAM-SHA-256: scram-sha-256", "scram-sha-256", SMTPAuthSCRAMSHA256},
|
||||||
|
{"SCRAM-SHA-256: scram-sha256", "scram-sha256", SMTPAuthSCRAMSHA256},
|
||||||
|
{"SCRAM-SHA-256: scramsha256", "scramsha256", SMTPAuthSCRAMSHA256},
|
||||||
|
{"SCRAM-SHA-256-PLUS: scram-sha-256-plus", "scram-sha-256-plus", SMTPAuthSCRAMSHA256PLUS},
|
||||||
|
{"SCRAM-SHA-256-PLUS: scram-sha256-plus", "scram-sha256-plus", SMTPAuthSCRAMSHA256PLUS},
|
||||||
|
{"SCRAM-SHA-256-PLUS: scramsha256plus", "scramsha256plus", SMTPAuthSCRAMSHA256PLUS},
|
||||||
|
{"XOAUTH2: xoauth2", "xoauth2", SMTPAuthXOAUTH2},
|
||||||
|
{"XOAUTH2: oauth2", "oauth2", SMTPAuthXOAUTH2},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
var authType SMTPAuthType
|
||||||
|
if err := authType.UnmarshalString(tt.authString); err != nil {
|
||||||
|
t.Errorf("UnmarshalString() for type %s failed: %s", tt.authString, err)
|
||||||
|
}
|
||||||
|
if authType != tt.expected {
|
||||||
|
t.Errorf("UnmarshalString() for type %s failed: expected %s, got %s",
|
||||||
|
tt.authString, tt.expected, authType)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
t.Run("should fail", func(t *testing.T) {
|
||||||
|
var authType SMTPAuthType
|
||||||
|
if err := authType.UnmarshalString("invalid"); err == nil {
|
||||||
|
t.Error("UnmarshalString() should have failed")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
14
client.go
14
client.go
|
@ -1096,12 +1096,22 @@ func (c *Client) auth() error {
|
||||||
if !strings.Contains(smtpAuthType, string(SMTPAuthPlain)) {
|
if !strings.Contains(smtpAuthType, string(SMTPAuthPlain)) {
|
||||||
return ErrPlainAuthNotSupported
|
return ErrPlainAuthNotSupported
|
||||||
}
|
}
|
||||||
c.smtpAuth = smtp.PlainAuth("", c.user, c.pass, c.host)
|
c.smtpAuth = smtp.PlainAuth("", c.user, c.pass, c.host, false)
|
||||||
|
case SMTPAuthPlainNoEnc:
|
||||||
|
if !strings.Contains(smtpAuthType, string(SMTPAuthPlain)) {
|
||||||
|
return ErrPlainAuthNotSupported
|
||||||
|
}
|
||||||
|
c.smtpAuth = smtp.PlainAuth("", c.user, c.pass, c.host, true)
|
||||||
case SMTPAuthLogin:
|
case SMTPAuthLogin:
|
||||||
if !strings.Contains(smtpAuthType, string(SMTPAuthLogin)) {
|
if !strings.Contains(smtpAuthType, string(SMTPAuthLogin)) {
|
||||||
return ErrLoginAuthNotSupported
|
return ErrLoginAuthNotSupported
|
||||||
}
|
}
|
||||||
c.smtpAuth = smtp.LoginAuth(c.user, c.pass, c.host)
|
c.smtpAuth = smtp.LoginAuth(c.user, c.pass, c.host, false)
|
||||||
|
case SMTPAuthLoginNoEnc:
|
||||||
|
if !strings.Contains(smtpAuthType, string(SMTPAuthLogin)) {
|
||||||
|
return ErrLoginAuthNotSupported
|
||||||
|
}
|
||||||
|
c.smtpAuth = smtp.LoginAuth(c.user, c.pass, c.host, true)
|
||||||
case SMTPAuthCramMD5:
|
case SMTPAuthCramMD5:
|
||||||
if !strings.Contains(smtpAuthType, string(SMTPAuthCramMD5)) {
|
if !strings.Contains(smtpAuthType, string(SMTPAuthCramMD5)) {
|
||||||
return ErrCramMD5AuthNotSupported
|
return ErrCramMD5AuthNotSupported
|
||||||
|
|
|
@ -110,7 +110,7 @@ func TestNewClientWithOptions(t *testing.T) {
|
||||||
{"WithSMTPAuth()", WithSMTPAuth(SMTPAuthLogin), false},
|
{"WithSMTPAuth()", WithSMTPAuth(SMTPAuthLogin), false},
|
||||||
{
|
{
|
||||||
"WithSMTPAuthCustom()",
|
"WithSMTPAuthCustom()",
|
||||||
WithSMTPAuthCustom(smtp.PlainAuth("", "", "", "")),
|
WithSMTPAuthCustom(smtp.PlainAuth("", "", "", "", false)),
|
||||||
false,
|
false,
|
||||||
},
|
},
|
||||||
{"WithUsername()", WithUsername("test"), false},
|
{"WithUsername()", WithUsername("test"), false},
|
||||||
|
@ -605,8 +605,8 @@ func TestSetSMTPAuthCustom(t *testing.T) {
|
||||||
sf bool
|
sf bool
|
||||||
}{
|
}{
|
||||||
{"SMTPAuth: CRAM-MD5", smtp.CRAMMD5Auth("", ""), "CRAM-MD5", false},
|
{"SMTPAuth: CRAM-MD5", smtp.CRAMMD5Auth("", ""), "CRAM-MD5", false},
|
||||||
{"SMTPAuth: LOGIN", smtp.LoginAuth("", "", ""), "LOGIN", false},
|
{"SMTPAuth: LOGIN", smtp.LoginAuth("", "", "", false), "LOGIN", false},
|
||||||
{"SMTPAuth: PLAIN", smtp.PlainAuth("", "", "", ""), "PLAIN", false},
|
{"SMTPAuth: PLAIN", smtp.PlainAuth("", "", "", "", false), "PLAIN", false},
|
||||||
}
|
}
|
||||||
si := smtp.ServerInfo{TLS: true}
|
si := smtp.ServerInfo{TLS: true}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
@ -807,7 +807,7 @@ func TestClient_DialWithContextInvalidAuth(t *testing.T) {
|
||||||
}
|
}
|
||||||
c.user = "invalid"
|
c.user = "invalid"
|
||||||
c.pass = "invalid"
|
c.pass = "invalid"
|
||||||
c.SetSMTPAuthCustom(smtp.LoginAuth("invalid", "invalid", "invalid"))
|
c.SetSMTPAuthCustom(smtp.LoginAuth("invalid", "invalid", "invalid", false))
|
||||||
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")
|
||||||
|
@ -1227,7 +1227,7 @@ func TestClient_DialWithContext_switchAuth(t *testing.T) {
|
||||||
|
|
||||||
// We switch to CUSTOM by providing PLAIN auth as function - the server supports this
|
// We switch to CUSTOM by providing PLAIN auth as function - the server supports this
|
||||||
client.SetSMTPAuthCustom(smtp.PlainAuth("", os.Getenv("TEST_SMTPAUTH_USER"),
|
client.SetSMTPAuthCustom(smtp.PlainAuth("", os.Getenv("TEST_SMTPAUTH_USER"),
|
||||||
os.Getenv("TEST_SMTPAUTH_PASS"), os.Getenv("TEST_HOST")))
|
os.Getenv("TEST_SMTPAUTH_PASS"), os.Getenv("TEST_HOST"), false))
|
||||||
if client.smtpAuthType != SMTPAuthCustom {
|
if client.smtpAuthType != SMTPAuthCustom {
|
||||||
t.Errorf("expected auth type to be Custom, got: %s", client.smtpAuthType)
|
t.Errorf("expected auth type to be Custom, got: %s", client.smtpAuthType)
|
||||||
}
|
}
|
||||||
|
@ -1955,7 +1955,7 @@ func TestClient_DialSendConcurrent_local(t *testing.T) {
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
|
||||||
if err = client.Close(); err != nil {
|
if err = client.Close(); err != nil {
|
||||||
t.Errorf("failed to close server connection: %s", err)
|
t.Logf("failed to close server connection: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ type loginAuth struct {
|
||||||
username, password string
|
username, password string
|
||||||
host string
|
host string
|
||||||
respStep uint8
|
respStep uint8
|
||||||
|
allowUnencryptedAuth bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoginAuth returns an [Auth] that implements the LOGIN authentication
|
// LoginAuth returns an [Auth] that implements the LOGIN authentication
|
||||||
|
@ -35,8 +36,8 @@ type loginAuth struct {
|
||||||
// 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) Auth {
|
func LoginAuth(username, password, host string, allowUnEnc bool) Auth {
|
||||||
return &loginAuth{username, password, host, 0}
|
return &loginAuth{username, password, host, 0, allowUnEnc}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start begins the SMTP authentication process by validating server's TLS status and hostname.
|
// Start begins the SMTP authentication process by validating server's TLS status and hostname.
|
||||||
|
@ -47,7 +48,7 @@ func (a *loginAuth) Start(server *ServerInfo) (string, []byte, error) {
|
||||||
// In particular, it doesn't matter if the server advertises LOGIN auth.
|
// In particular, it doesn't matter if the server advertises LOGIN auth.
|
||||||
// That might just be the attacker saying
|
// That might just be the attacker saying
|
||||||
// "it's ok, you can trust me with your password."
|
// "it's ok, you can trust me with your password."
|
||||||
if !server.TLS && !isLocalhost(server.Name) {
|
if !a.allowUnencryptedAuth && !server.TLS && !isLocalhost(server.Name) {
|
||||||
return "", nil, ErrUnencrypted
|
return "", nil, ErrUnencrypted
|
||||||
}
|
}
|
||||||
if server.Name != a.host {
|
if server.Name != a.host {
|
||||||
|
|
|
@ -17,6 +17,7 @@ package smtp
|
||||||
type plainAuth struct {
|
type plainAuth struct {
|
||||||
identity, username, password string
|
identity, username, password string
|
||||||
host string
|
host string
|
||||||
|
allowUnencryptedAuth bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// PlainAuth returns an [Auth] that implements the PLAIN authentication
|
// PlainAuth returns an [Auth] that implements the PLAIN authentication
|
||||||
|
@ -27,8 +28,8 @@ type plainAuth struct {
|
||||||
// PlainAuth will only send the credentials if the connection is using TLS
|
// PlainAuth 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 PlainAuth(identity, username, password, host string) Auth {
|
func PlainAuth(identity, username, password, host string, allowUnEnc bool) Auth {
|
||||||
return &plainAuth{identity, username, password, host}
|
return &plainAuth{identity, username, password, host, allowUnEnc}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *plainAuth) Start(server *ServerInfo) (string, []byte, error) {
|
func (a *plainAuth) Start(server *ServerInfo) (string, []byte, error) {
|
||||||
|
@ -37,7 +38,7 @@ func (a *plainAuth) Start(server *ServerInfo) (string, []byte, error) {
|
||||||
// In particular, it doesn't matter if the server advertises PLAIN auth.
|
// In particular, it doesn't matter if the server advertises PLAIN auth.
|
||||||
// That might just be the attacker saying
|
// That might just be the attacker saying
|
||||||
// "it's ok, you can trust me with your password."
|
// "it's ok, you can trust me with your password."
|
||||||
if !server.TLS && !isLocalhost(server.Name) {
|
if !a.allowUnencryptedAuth && !server.TLS && !isLocalhost(server.Name) {
|
||||||
return "", nil, ErrUnencrypted
|
return "", nil, ErrUnencrypted
|
||||||
}
|
}
|
||||||
if server.Name != a.host {
|
if server.Name != a.host {
|
||||||
|
|
|
@ -67,7 +67,7 @@ var (
|
||||||
func ExamplePlainAuth() {
|
func ExamplePlainAuth() {
|
||||||
// hostname is used by PlainAuth to validate the TLS certificate.
|
// hostname is used by PlainAuth to validate the TLS certificate.
|
||||||
hostname := "mail.example.com"
|
hostname := "mail.example.com"
|
||||||
auth := smtp.PlainAuth("", "user@example.com", "password", hostname)
|
auth := smtp.PlainAuth("", "user@example.com", "password", hostname, false)
|
||||||
|
|
||||||
err := smtp.SendMail(hostname+":25", auth, from, recipients, msg)
|
err := smtp.SendMail(hostname+":25", auth, from, recipients, msg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -77,7 +77,7 @@ func ExamplePlainAuth() {
|
||||||
|
|
||||||
func ExampleSendMail() {
|
func ExampleSendMail() {
|
||||||
// Set up authentication information.
|
// Set up authentication information.
|
||||||
auth := smtp.PlainAuth("", "user@example.com", "password", "mail.example.com")
|
auth := smtp.PlainAuth("", "user@example.com", "password", "mail.example.com", false)
|
||||||
|
|
||||||
// Connect to the server, authenticate, set the sender and recipient,
|
// Connect to the server, authenticate, set the sender and recipient,
|
||||||
// and send the email all in one step.
|
// and send the email all in one step.
|
||||||
|
|
|
@ -50,7 +50,7 @@ type authTest struct {
|
||||||
|
|
||||||
var authTests = []authTest{
|
var authTests = []authTest{
|
||||||
{
|
{
|
||||||
PlainAuth("", "user", "pass", "testserver"),
|
PlainAuth("", "user", "pass", "testserver", false),
|
||||||
[]string{},
|
[]string{},
|
||||||
"PLAIN",
|
"PLAIN",
|
||||||
[]string{"\x00user\x00pass"},
|
[]string{"\x00user\x00pass"},
|
||||||
|
@ -58,7 +58,15 @@ var authTests = []authTest{
|
||||||
false,
|
false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
PlainAuth("foo", "bar", "baz", "testserver"),
|
PlainAuth("", "user", "pass", "testserver", true),
|
||||||
|
[]string{},
|
||||||
|
"PLAIN",
|
||||||
|
[]string{"\x00user\x00pass"},
|
||||||
|
[]bool{false, false},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
PlainAuth("foo", "bar", "baz", "testserver", false),
|
||||||
[]string{},
|
[]string{},
|
||||||
"PLAIN",
|
"PLAIN",
|
||||||
[]string{"foo\x00bar\x00baz"},
|
[]string{"foo\x00bar\x00baz"},
|
||||||
|
@ -66,7 +74,7 @@ var authTests = []authTest{
|
||||||
false,
|
false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
PlainAuth("foo", "bar", "baz", "testserver"),
|
PlainAuth("foo", "bar", "baz", "testserver", false),
|
||||||
[]string{"foo"},
|
[]string{"foo"},
|
||||||
"PLAIN",
|
"PLAIN",
|
||||||
[]string{"foo\x00bar\x00baz", ""},
|
[]string{"foo\x00bar\x00baz", ""},
|
||||||
|
@ -74,7 +82,7 @@ var authTests = []authTest{
|
||||||
false,
|
false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
LoginAuth("user", "pass", "testserver"),
|
LoginAuth("user", "pass", "testserver", false),
|
||||||
[]string{"Username:", "Password:"},
|
[]string{"Username:", "Password:"},
|
||||||
"LOGIN",
|
"LOGIN",
|
||||||
[]string{"", "user", "pass"},
|
[]string{"", "user", "pass"},
|
||||||
|
@ -82,7 +90,15 @@ var authTests = []authTest{
|
||||||
false,
|
false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
LoginAuth("user", "pass", "testserver"),
|
LoginAuth("user", "pass", "testserver", true),
|
||||||
|
[]string{"Username:", "Password:"},
|
||||||
|
"LOGIN",
|
||||||
|
[]string{"", "user", "pass"},
|
||||||
|
[]bool{false, false},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
LoginAuth("user", "pass", "testserver", false),
|
||||||
[]string{"User Name\x00", "Password\x00"},
|
[]string{"User Name\x00", "Password\x00"},
|
||||||
"LOGIN",
|
"LOGIN",
|
||||||
[]string{"", "user", "pass"},
|
[]string{"", "user", "pass"},
|
||||||
|
@ -90,7 +106,7 @@ var authTests = []authTest{
|
||||||
false,
|
false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
LoginAuth("user", "pass", "testserver"),
|
LoginAuth("user", "pass", "testserver", false),
|
||||||
[]string{"Invalid", "Invalid:"},
|
[]string{"Invalid", "Invalid:"},
|
||||||
"LOGIN",
|
"LOGIN",
|
||||||
[]string{"", "user", "pass"},
|
[]string{"", "user", "pass"},
|
||||||
|
@ -98,7 +114,7 @@ var authTests = []authTest{
|
||||||
false,
|
false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
LoginAuth("user", "pass", "testserver"),
|
LoginAuth("user", "pass", "testserver", false),
|
||||||
[]string{"Invalid", "Invalid:", "Too many"},
|
[]string{"Invalid", "Invalid:", "Too many"},
|
||||||
"LOGIN",
|
"LOGIN",
|
||||||
[]string{"", "user", "pass", ""},
|
[]string{"", "user", "pass", ""},
|
||||||
|
@ -237,7 +253,47 @@ func TestAuthPlain(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for i, tt := range tests {
|
for i, tt := range tests {
|
||||||
auth := PlainAuth("foo", "bar", "baz", tt.authName)
|
auth := PlainAuth("foo", "bar", "baz", tt.authName, false)
|
||||||
|
_, _, 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 TestAuthPlainNoEnc(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},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Also OK on non-TLS secured connections. The NoEnc mechanism is meant to allow
|
||||||
|
// non-encrypted connections.
|
||||||
|
authName: "servername",
|
||||||
|
server: &ServerInfo{Name: "servername", Auth: []string{"PLAIN"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
authName: "servername",
|
||||||
|
server: &ServerInfo{Name: "attacker", TLS: true},
|
||||||
|
err: "wrong host name",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for i, tt := range tests {
|
||||||
|
auth := PlainAuth("foo", "bar", "baz", tt.authName, true)
|
||||||
_, _, err := auth.Start(tt.server)
|
_, _, err := auth.Start(tt.server)
|
||||||
got := ""
|
got := ""
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -283,7 +339,51 @@ func TestAuthLogin(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for i, tt := range tests {
|
for i, tt := range tests {
|
||||||
auth := LoginAuth("foo", "bar", tt.authName)
|
auth := LoginAuth("foo", "bar", tt.authName, false)
|
||||||
|
_, _, 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 TestAuthLoginNoEnc(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},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Also OK on non-TLS secured connections. The NoEnc mechanism is meant to allow
|
||||||
|
// non-encrypted connections.
|
||||||
|
authName: "servername",
|
||||||
|
server: &ServerInfo{Name: "servername", Auth: []string{"LOGIN"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
authName: "servername",
|
||||||
|
server: &ServerInfo{Name: "servername", Auth: []string{"CRAM-MD5"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
authName: "servername",
|
||||||
|
server: &ServerInfo{Name: "attacker", TLS: true},
|
||||||
|
err: "wrong host name",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for i, tt := range tests {
|
||||||
|
auth := LoginAuth("foo", "bar", tt.authName, true)
|
||||||
_, _, err := auth.Start(tt.server)
|
_, _, err := auth.Start(tt.server)
|
||||||
got := ""
|
got := ""
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -317,7 +417,11 @@ func TestXOAuth2OK(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("NewClient: %v", err)
|
t.Fatalf("NewClient: %v", err)
|
||||||
}
|
}
|
||||||
defer c.Close()
|
defer func() {
|
||||||
|
if err := c.Close(); err != nil {
|
||||||
|
t.Errorf("failed to close client: %s", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
auth := XOAuth2Auth("user", "token")
|
auth := XOAuth2Auth("user", "token")
|
||||||
err = c.Auth(auth)
|
err = c.Auth(auth)
|
||||||
|
@ -355,7 +459,11 @@ func TestXOAuth2Error(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("NewClient: %v", err)
|
t.Fatalf("NewClient: %v", err)
|
||||||
}
|
}
|
||||||
defer c.Close()
|
defer func() {
|
||||||
|
if err := c.Close(); err != nil {
|
||||||
|
t.Errorf("failed to close client: %s", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
auth := XOAuth2Auth("user", "token")
|
auth := XOAuth2Auth("user", "token")
|
||||||
err = c.Auth(auth)
|
err = c.Auth(auth)
|
||||||
|
@ -707,7 +815,7 @@ func TestBasic(t *testing.T) {
|
||||||
// fake TLS so authentication won't complain
|
// fake TLS so authentication won't complain
|
||||||
c.tls = true
|
c.tls = true
|
||||||
c.serverName = "smtp.google.com"
|
c.serverName = "smtp.google.com"
|
||||||
if err := c.Auth(PlainAuth("", "user", "pass", "smtp.google.com")); err != nil {
|
if err := c.Auth(PlainAuth("", "user", "pass", "smtp.google.com", false)); err != nil {
|
||||||
t.Fatalf("AUTH failed: %s", err)
|
t.Fatalf("AUTH failed: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1278,7 +1386,7 @@ func TestHello(t *testing.T) {
|
||||||
case 3:
|
case 3:
|
||||||
c.tls = true
|
c.tls = true
|
||||||
c.serverName = "smtp.google.com"
|
c.serverName = "smtp.google.com"
|
||||||
err = c.Auth(PlainAuth("", "user", "pass", "smtp.google.com"))
|
err = c.Auth(PlainAuth("", "user", "pass", "smtp.google.com", false))
|
||||||
case 4:
|
case 4:
|
||||||
err = c.Mail("test@example.com")
|
err = c.Mail("test@example.com")
|
||||||
case 5:
|
case 5:
|
||||||
|
@ -1523,7 +1631,7 @@ func TestSendMailWithAuth(t *testing.T) {
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
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
|
err = SendMail(l.Addr().String(), PlainAuth("", "user", "pass", "smtp.google.com", false), "test@example.com", []string{"other@example.com"}, []byte(strings.Replace(`From: test@example.com
|
||||||
To: other@example.com
|
To: other@example.com
|
||||||
Subject: SendMail test
|
Subject: SendMail test
|
||||||
|
|
||||||
|
@ -1531,6 +1639,7 @@ SendMail is working for me.
|
||||||
`, "\n", "\r\n", -1)))
|
`, "\n", "\r\n", -1)))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Error("SendMail: Server doesn't support AUTH, expected to get an error, but got none ")
|
t.Error("SendMail: Server doesn't support AUTH, expected to get an error, but got none ")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
if err.Error() != "smtp: server doesn't support AUTH" {
|
if err.Error() != "smtp: server doesn't support AUTH" {
|
||||||
t.Errorf("Expected: smtp: server doesn't support AUTH, got: %s", err)
|
t.Errorf("Expected: smtp: server doesn't support AUTH, got: %s", err)
|
||||||
|
@ -1558,7 +1667,7 @@ func TestAuthFailed(t *testing.T) {
|
||||||
|
|
||||||
c.tls = true
|
c.tls = true
|
||||||
c.serverName = "smtp.google.com"
|
c.serverName = "smtp.google.com"
|
||||||
err = c.Auth(PlainAuth("", "user", "pass", "smtp.google.com"))
|
err = c.Auth(PlainAuth("", "user", "pass", "smtp.google.com", false))
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Error("Auth: expected error; got none")
|
t.Error("Auth: expected error; got none")
|
||||||
|
|
Loading…
Reference in a new issue