mirror of
https://github.com/wneessen/go-mail.git
synced 2024-11-23 06:10:48 +01:00
Compare commits
17 commits
3871b2be44
...
2d98c40cb6
Author | SHA1 | Date | |
---|---|---|---|
2d98c40cb6 | |||
253d065c83 | |||
6bd9a9c735 | |||
f59aa23ed8 | |||
2234f0c5bc | |||
fdb80ad9dd | |||
fec2f2075a | |||
2084526c77 | |||
23c71d608f | |||
371b950bc7 | |||
b2c4b533d7 | |||
077c85bea0 | |||
909e699b99 | |||
|
b97073db19 | ||
|
d75d990124 | ||
0676d99f20 | |||
|
d6725b2d63 |
10 changed files with 295 additions and 94 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@294a9d92911152fe08befb9ec03e240add280cb3 # v3.26.8
|
uses: github/codeql-action/init@461ef6c76dfe95d5c364de2f431ddbd31a417628 # v3.26.9
|
||||||
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@294a9d92911152fe08befb9ec03e240add280cb3 # v3.26.8
|
uses: github/codeql-action/autobuild@461ef6c76dfe95d5c364de2f431ddbd31a417628 # v3.26.9
|
||||||
|
|
||||||
# ℹ️ 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@294a9d92911152fe08befb9ec03e240add280cb3 # v3.26.8
|
uses: github/codeql-action/analyze@461ef6c76dfe95d5c364de2f431ddbd31a417628 # v3.26.9
|
||||||
|
|
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@294a9d92911152fe08befb9ec03e240add280cb3 # v3.26.8
|
uses: github/codeql-action/upload-sarif@461ef6c76dfe95d5c364de2f431ddbd31a417628 # v3.26.9
|
||||||
with:
|
with:
|
||||||
sarif_file: results.sarif
|
sarif_file: results.sarif
|
||||||
|
|
2
.github/workflows/sonarqube.yml
vendored
2
.github/workflows/sonarqube.yml
vendored
|
@ -44,7 +44,7 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
go test -v -race --coverprofile=./cov.out ./...
|
go test -v -race --coverprofile=./cov.out ./...
|
||||||
|
|
||||||
- uses: sonarsource/sonarqube-scan-action@0c0f3958d90fc466625f1d1af1f47bddd4cc6bd1 # master
|
- uses: sonarsource/sonarqube-scan-action@884b79409bbd464b2a59edc326a4b77dc56b2195 # master
|
||||||
env:
|
env:
|
||||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||||
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
|
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
|
||||||
|
|
53
client.go
53
client.go
|
@ -88,13 +88,12 @@ type DialContextFunc func(ctx context.Context, network, address string) (net.Con
|
||||||
|
|
||||||
// Client is the SMTP client struct
|
// Client is the SMTP client struct
|
||||||
type Client struct {
|
type Client struct {
|
||||||
mutex sync.RWMutex
|
|
||||||
// connection is the net.Conn that the smtp.Client is based on
|
|
||||||
connection net.Conn
|
|
||||||
|
|
||||||
// Timeout for the SMTP server connection
|
// Timeout for the SMTP server connection
|
||||||
connTimeout time.Duration
|
connTimeout time.Duration
|
||||||
|
|
||||||
|
// dialContextFunc is a custom DialContext function to dial target SMTP server
|
||||||
|
dialContextFunc DialContextFunc
|
||||||
|
|
||||||
// dsn indicates that we want to use DSN for the Client
|
// dsn indicates that we want to use DSN for the Client
|
||||||
dsn bool
|
dsn bool
|
||||||
|
|
||||||
|
@ -104,11 +103,9 @@ type Client struct {
|
||||||
// dsnrntype defines the DSNRcptNotifyOption in case DSN is enabled
|
// dsnrntype defines the DSNRcptNotifyOption in case DSN is enabled
|
||||||
dsnrntype []string
|
dsnrntype []string
|
||||||
|
|
||||||
// isEncrypted indicates if a Client connection is encrypted or not
|
// fallbackPort is used as an alternative port number in case the primary port is unavailable or
|
||||||
isEncrypted bool
|
// fails to bind.
|
||||||
|
fallbackPort int
|
||||||
// noNoop indicates the Noop is to be skipped
|
|
||||||
noNoop bool
|
|
||||||
|
|
||||||
// HELO/EHLO string for the greeting the target SMTP server
|
// HELO/EHLO string for the greeting the target SMTP server
|
||||||
helo string
|
helo string
|
||||||
|
@ -116,12 +113,24 @@ type Client struct {
|
||||||
// Hostname of the target SMTP server to connect to
|
// Hostname of the target SMTP server to connect to
|
||||||
host string
|
host string
|
||||||
|
|
||||||
|
// isEncrypted indicates if a Client connection is encrypted or not
|
||||||
|
isEncrypted bool
|
||||||
|
|
||||||
|
// logger is a logger that implements the log.Logger interface
|
||||||
|
logger log.Logger
|
||||||
|
|
||||||
|
// mutex is used to synchronize access to shared resources, ensuring that only one goroutine can
|
||||||
|
// modify them at a time.
|
||||||
|
mutex sync.RWMutex
|
||||||
|
|
||||||
|
// noNoop indicates the Noop is to be skipped
|
||||||
|
noNoop bool
|
||||||
|
|
||||||
// pass is the corresponding SMTP AUTH password
|
// pass is the corresponding SMTP AUTH password
|
||||||
pass string
|
pass string
|
||||||
|
|
||||||
// Port of the SMTP server to connect to
|
// port specifies the network port number on which the server listens for incoming connections.
|
||||||
port int
|
port int
|
||||||
fallbackPort int
|
|
||||||
|
|
||||||
// smtpAuth is a pointer to smtp.Auth
|
// smtpAuth is a pointer to smtp.Auth
|
||||||
smtpAuth smtp.Auth
|
smtpAuth smtp.Auth
|
||||||
|
@ -132,26 +141,20 @@ type Client struct {
|
||||||
// smtpClient is the smtp.Client that is set up when using the Dial*() methods
|
// smtpClient is the smtp.Client that is set up when using the Dial*() methods
|
||||||
smtpClient *smtp.Client
|
smtpClient *smtp.Client
|
||||||
|
|
||||||
// Use SSL for the connection
|
|
||||||
useSSL bool
|
|
||||||
|
|
||||||
// tlspolicy sets the client to use the provided TLSPolicy for the STARTTLS protocol
|
// tlspolicy sets the client to use the provided TLSPolicy for the STARTTLS protocol
|
||||||
tlspolicy TLSPolicy
|
tlspolicy TLSPolicy
|
||||||
|
|
||||||
// tlsconfig represents the tls.Config setting for the STARTTLS connection
|
// tlsconfig represents the tls.Config setting for the STARTTLS connection
|
||||||
tlsconfig *tls.Config
|
tlsconfig *tls.Config
|
||||||
|
|
||||||
// user is the SMTP AUTH username
|
|
||||||
user string
|
|
||||||
|
|
||||||
// useDebugLog enables the debug logging on the SMTP client
|
// useDebugLog enables the debug logging on the SMTP client
|
||||||
useDebugLog bool
|
useDebugLog bool
|
||||||
|
|
||||||
// logger is a logger that implements the log.Logger interface
|
// user is the SMTP AUTH username
|
||||||
logger log.Logger
|
user string
|
||||||
|
|
||||||
// dialContextFunc is a custom DialContext function to dial target SMTP server
|
// Use SSL for the connection
|
||||||
dialContextFunc DialContextFunc
|
useSSL bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Option returns a function that can be used for grouping Client options
|
// Option returns a function that can be used for grouping Client options
|
||||||
|
@ -552,6 +555,9 @@ func (c *Client) SetLogger(logger log.Logger) {
|
||||||
|
|
||||||
// SetTLSConfig overrides the current *tls.Config with the given *tls.Config value
|
// SetTLSConfig overrides the current *tls.Config with the given *tls.Config value
|
||||||
func (c *Client) SetTLSConfig(tlsconfig *tls.Config) error {
|
func (c *Client) SetTLSConfig(tlsconfig *tls.Config) error {
|
||||||
|
c.mutex.Lock()
|
||||||
|
defer c.mutex.Unlock()
|
||||||
|
|
||||||
if tlsconfig == nil {
|
if tlsconfig == nil {
|
||||||
return ErrInvalidTLSConfig
|
return ErrInvalidTLSConfig
|
||||||
}
|
}
|
||||||
|
@ -795,6 +801,9 @@ func (c *Client) auth() error {
|
||||||
// sendSingleMsg sends out a single message and returns an error if the transmission/delivery fails.
|
// sendSingleMsg sends out a single message and returns an error if the transmission/delivery fails.
|
||||||
// It is invoked by the public Send methods
|
// It is invoked by the public Send methods
|
||||||
func (c *Client) sendSingleMsg(message *Msg) error {
|
func (c *Client) sendSingleMsg(message *Msg) error {
|
||||||
|
c.mutex.Lock()
|
||||||
|
defer c.mutex.Unlock()
|
||||||
|
|
||||||
if message.encoding == NoEncoding {
|
if message.encoding == NoEncoding {
|
||||||
if ok, _ := c.smtpClient.Extension("8BITMIME"); !ok {
|
if ok, _ := c.smtpClient.Extension("8BITMIME"); !ok {
|
||||||
return &SendError{Reason: ErrNoUnencoded, isTemp: false, affectedMsg: message}
|
return &SendError{Reason: ErrNoUnencoded, isTemp: false, affectedMsg: message}
|
||||||
|
|
|
@ -13,8 +13,6 @@ import (
|
||||||
|
|
||||||
// Send sends out the mail message
|
// Send sends out the mail message
|
||||||
func (c *Client) Send(messages ...*Msg) (returnErr error) {
|
func (c *Client) Send(messages ...*Msg) (returnErr error) {
|
||||||
c.mutex.Lock()
|
|
||||||
defer c.mutex.Unlock()
|
|
||||||
if err := c.checkConn(); err != nil {
|
if err := c.checkConn(); err != nil {
|
||||||
returnErr = &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)}
|
returnErr = &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)}
|
||||||
return
|
return
|
||||||
|
|
187
client_test.go
187
client_test.go
|
@ -15,6 +15,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -623,11 +624,12 @@ func TestClient_DialWithContext(t *testing.T) {
|
||||||
t.Errorf("failed to dial with context: %s", err)
|
t.Errorf("failed to dial with context: %s", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if c.connection == nil {
|
|
||||||
t.Errorf("DialWithContext didn't fail but no connection found.")
|
|
||||||
}
|
|
||||||
if c.smtpClient == nil {
|
if c.smtpClient == nil {
|
||||||
t.Errorf("DialWithContext didn't fail but no SMTP client found.")
|
t.Errorf("DialWithContext didn't fail but no SMTP client found.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !c.smtpClient.HasConnection() {
|
||||||
|
t.Errorf("DialWithContext didn't fail but no connection found.")
|
||||||
}
|
}
|
||||||
if err := c.Close(); err != nil {
|
if err := c.Close(); err != nil {
|
||||||
t.Errorf("failed to close connection: %s", err)
|
t.Errorf("failed to close connection: %s", err)
|
||||||
|
@ -644,17 +646,18 @@ func TestClient_DialWithContext_Fallback(t *testing.T) {
|
||||||
c.SetTLSPortPolicy(TLSOpportunistic)
|
c.SetTLSPortPolicy(TLSOpportunistic)
|
||||||
c.port = 999
|
c.port = 999
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
if err := c.DialWithContext(ctx); err != nil {
|
if err = c.DialWithContext(ctx); err != nil {
|
||||||
t.Errorf("failed to dial with context: %s", err)
|
t.Errorf("failed to dial with context: %s", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if c.connection == nil {
|
|
||||||
t.Errorf("DialWithContext didn't fail but no connection found.")
|
|
||||||
}
|
|
||||||
if c.smtpClient == nil {
|
if c.smtpClient == nil {
|
||||||
t.Errorf("DialWithContext didn't fail but no SMTP client found.")
|
t.Errorf("DialWithContext didn't fail but no SMTP client found.")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
if err := c.Close(); err != nil {
|
if !c.smtpClient.HasConnection() {
|
||||||
|
t.Errorf("DialWithContext didn't fail but no connection found.")
|
||||||
|
}
|
||||||
|
if err = c.Close(); err != nil {
|
||||||
t.Errorf("failed to close connection: %s", err)
|
t.Errorf("failed to close connection: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -674,18 +677,19 @@ func TestClient_DialWithContext_Debug(t *testing.T) {
|
||||||
t.Skipf("failed to create test client: %s. Skipping tests", err)
|
t.Skipf("failed to create test client: %s. Skipping tests", err)
|
||||||
}
|
}
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
if err := c.DialWithContext(ctx); err != nil {
|
if err = c.DialWithContext(ctx); err != nil {
|
||||||
t.Errorf("failed to dial with context: %s", err)
|
t.Errorf("failed to dial with context: %s", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if c.connection == nil {
|
|
||||||
t.Errorf("DialWithContext didn't fail but no connection found.")
|
|
||||||
}
|
|
||||||
if c.smtpClient == nil {
|
if c.smtpClient == nil {
|
||||||
t.Errorf("DialWithContext didn't fail but no SMTP client found.")
|
t.Errorf("DialWithContext didn't fail but no SMTP client found.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !c.smtpClient.HasConnection() {
|
||||||
|
t.Errorf("DialWithContext didn't fail but no connection found.")
|
||||||
}
|
}
|
||||||
c.SetDebugLog(true)
|
c.SetDebugLog(true)
|
||||||
if err := c.Close(); err != nil {
|
if err = c.Close(); err != nil {
|
||||||
t.Errorf("failed to close connection: %s", err)
|
t.Errorf("failed to close connection: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -698,19 +702,20 @@ func TestClient_DialWithContext_Debug_custom(t *testing.T) {
|
||||||
t.Skipf("failed to create test client: %s. Skipping tests", err)
|
t.Skipf("failed to create test client: %s. Skipping tests", err)
|
||||||
}
|
}
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
if err := c.DialWithContext(ctx); err != nil {
|
if err = c.DialWithContext(ctx); err != nil {
|
||||||
t.Errorf("failed to dial with context: %s", err)
|
t.Errorf("failed to dial with context: %s", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if c.connection == nil {
|
|
||||||
t.Errorf("DialWithContext didn't fail but no connection found.")
|
|
||||||
}
|
|
||||||
if c.smtpClient == nil {
|
if c.smtpClient == nil {
|
||||||
t.Errorf("DialWithContext didn't fail but no SMTP client found.")
|
t.Errorf("DialWithContext didn't fail but no SMTP client found.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !c.smtpClient.HasConnection() {
|
||||||
|
t.Errorf("DialWithContext didn't fail but no connection found.")
|
||||||
}
|
}
|
||||||
c.SetDebugLog(true)
|
c.SetDebugLog(true)
|
||||||
c.SetLogger(log.New(os.Stderr, log.LevelDebug))
|
c.SetLogger(log.New(os.Stderr, log.LevelDebug))
|
||||||
if err := c.Close(); err != nil {
|
if err = c.Close(); err != nil {
|
||||||
t.Errorf("failed to close connection: %s", err)
|
t.Errorf("failed to close connection: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -722,10 +727,9 @@ func TestClient_DialWithContextInvalidHost(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Skipf("failed to create test client: %s. Skipping tests", err)
|
t.Skipf("failed to create test client: %s. Skipping tests", err)
|
||||||
}
|
}
|
||||||
c.connection = nil
|
|
||||||
c.host = "invalid.addr"
|
c.host = "invalid.addr"
|
||||||
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")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -738,10 +742,9 @@ func TestClient_DialWithContextInvalidHELO(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Skipf("failed to create test client: %s. Skipping tests", err)
|
t.Skipf("failed to create test client: %s. Skipping tests", err)
|
||||||
}
|
}
|
||||||
c.connection = nil
|
|
||||||
c.helo = ""
|
c.helo = ""
|
||||||
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")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -758,7 +761,7 @@ func TestClient_DialWithContextInvalidAuth(t *testing.T) {
|
||||||
c.pass = "invalid"
|
c.pass = "invalid"
|
||||||
c.SetSMTPAuthCustom(smtp.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")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -770,8 +773,7 @@ func TestClient_checkConn(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Skipf("failed to create test client: %s. Skipping tests", err)
|
t.Skipf("failed to create test client: %s. Skipping tests", err)
|
||||||
}
|
}
|
||||||
c.connection = nil
|
if err = c.checkConn(); err == nil {
|
||||||
if err := c.checkConn(); err == nil {
|
|
||||||
t.Errorf("connCheck() should fail but succeeded")
|
t.Errorf("connCheck() should fail but succeeded")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -802,21 +804,23 @@ func TestClient_DialWithContextOptions(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
if err := c.DialWithContext(ctx); err != nil && !tt.sf {
|
if err = c.DialWithContext(ctx); err != nil && !tt.sf {
|
||||||
t.Errorf("failed to dial with context: %s", err)
|
t.Errorf("failed to dial with context: %s", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !tt.sf {
|
if !tt.sf {
|
||||||
if c.connection == nil && !tt.sf {
|
|
||||||
t.Errorf("DialWithContext didn't fail but no connection found.")
|
|
||||||
}
|
|
||||||
if c.smtpClient == nil && !tt.sf {
|
if c.smtpClient == nil && !tt.sf {
|
||||||
t.Errorf("DialWithContext didn't fail but no SMTP client found.")
|
t.Errorf("DialWithContext didn't fail but no SMTP client found.")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
if err := c.Reset(); err != nil {
|
if !c.smtpClient.HasConnection() && !tt.sf {
|
||||||
|
t.Errorf("DialWithContext didn't fail but no connection found.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = c.Reset(); err != nil {
|
||||||
t.Errorf("failed to reset connection: %s", err)
|
t.Errorf("failed to reset connection: %s", err)
|
||||||
}
|
}
|
||||||
if err := c.Close(); err != nil {
|
if err = c.Close(); err != nil {
|
||||||
t.Errorf("failed to close connection: %s", err)
|
t.Errorf("failed to close connection: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1011,17 +1015,15 @@ func TestClient_DialSendCloseBroken(t *testing.T) {
|
||||||
}
|
}
|
||||||
if tt.closestart {
|
if tt.closestart {
|
||||||
_ = c.smtpClient.Close()
|
_ = c.smtpClient.Close()
|
||||||
_ = c.connection.Close()
|
|
||||||
}
|
}
|
||||||
if err := c.Send(m); err != nil && !tt.sf {
|
if err = c.Send(m); err != nil && !tt.sf {
|
||||||
t.Errorf("Send() failed: %s", err)
|
t.Errorf("Send() failed: %s", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if tt.closeearly {
|
if tt.closeearly {
|
||||||
_ = c.smtpClient.Close()
|
_ = c.smtpClient.Close()
|
||||||
_ = c.connection.Close()
|
|
||||||
}
|
}
|
||||||
if err := c.Close(); err != nil && !tt.sf {
|
if err = c.Close(); err != nil && !tt.sf {
|
||||||
t.Errorf("Close() failed: %s", err)
|
t.Errorf("Close() failed: %s", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -1071,17 +1073,15 @@ func TestClient_DialSendCloseBrokenWithDSN(t *testing.T) {
|
||||||
}
|
}
|
||||||
if tt.closestart {
|
if tt.closestart {
|
||||||
_ = c.smtpClient.Close()
|
_ = c.smtpClient.Close()
|
||||||
_ = c.connection.Close()
|
|
||||||
}
|
}
|
||||||
if err := c.Send(m); err != nil && !tt.sf {
|
if err = c.Send(m); err != nil && !tt.sf {
|
||||||
t.Errorf("Send() failed: %s", err)
|
t.Errorf("Send() failed: %s", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if tt.closeearly {
|
if tt.closeearly {
|
||||||
_ = c.smtpClient.Close()
|
_ = c.smtpClient.Close()
|
||||||
_ = c.connection.Close()
|
|
||||||
}
|
}
|
||||||
if err := c.Close(); err != nil && !tt.sf {
|
if err = c.Close(); err != nil && !tt.sf {
|
||||||
t.Errorf("Close() failed: %s", err)
|
t.Errorf("Close() failed: %s", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -1728,6 +1728,114 @@ func TestClient_SendErrorReset(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestClient_DialSendConcurrent_online(t *testing.T) {
|
||||||
|
if os.Getenv("TEST_ALLOW_SEND") == "" {
|
||||||
|
t.Skipf("TEST_ALLOW_SEND is not set. Skipping mail sending test")
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := getTestConnection(true)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unable to create new client: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var messages []*Msg
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
message := NewMsg()
|
||||||
|
if err := message.FromFormat("go-mail Test Mailer", os.Getenv("TEST_FROM")); err != nil {
|
||||||
|
t.Errorf("failed to set FROM address: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := message.To(TestRcpt); err != nil {
|
||||||
|
t.Errorf("failed to set TO address: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
message.Subject(fmt.Sprintf("Test subject for mail %d", i))
|
||||||
|
message.SetBodyString(TypeTextPlain, fmt.Sprintf("This is the test body of the mail no. %d", i))
|
||||||
|
message.SetMessageID()
|
||||||
|
messages = append(messages, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = client.DialWithContext(context.Background()); err != nil {
|
||||||
|
t.Errorf("failed to dial to test server: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
for id, message := range messages {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(curMsg *Msg, curID int) {
|
||||||
|
defer wg.Done()
|
||||||
|
if goroutineErr := client.Send(curMsg); err != nil {
|
||||||
|
t.Errorf("failed to send message with ID %d: %s", curID, goroutineErr)
|
||||||
|
}
|
||||||
|
}(message, id)
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
if err = client.Close(); err != nil {
|
||||||
|
t.Errorf("failed to close server connection: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_DialSendConcurrent_local(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
serverPort := TestServerPortBase + 20
|
||||||
|
featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"
|
||||||
|
go func() {
|
||||||
|
if err := simpleSMTPServer(ctx, featureSet, false, serverPort); err != nil {
|
||||||
|
t.Errorf("failed to start test server: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
time.Sleep(time.Millisecond * 500)
|
||||||
|
|
||||||
|
client, err := NewClient(TestServerAddr, WithPort(serverPort),
|
||||||
|
WithTLSPortPolicy(NoTLS), WithSMTPAuth(SMTPAuthPlain),
|
||||||
|
WithUsername("toni@tester.com"),
|
||||||
|
WithPassword("V3ryS3cr3t+"))
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unable to create new client: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var messages []*Msg
|
||||||
|
for i := 0; i < 50; i++ {
|
||||||
|
message := NewMsg()
|
||||||
|
if err := message.From("valid-from@domain.tld"); err != nil {
|
||||||
|
t.Errorf("failed to set FROM address: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := message.To("valid-to@domain.tld"); err != nil {
|
||||||
|
t.Errorf("failed to set TO address: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
message.Subject("Test subject")
|
||||||
|
message.SetBodyString(TypeTextPlain, "Test body")
|
||||||
|
message.SetMessageIDWithValue("this.is.a.message.id")
|
||||||
|
messages = append(messages, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = client.DialWithContext(context.Background()); err != nil {
|
||||||
|
t.Errorf("failed to dial to test server: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
for id, message := range messages {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(curMsg *Msg, curID int) {
|
||||||
|
defer wg.Done()
|
||||||
|
if goroutineErr := client.Send(curMsg); err != nil {
|
||||||
|
t.Errorf("failed to send message with ID %d: %s", curID, goroutineErr)
|
||||||
|
}
|
||||||
|
}(message, id)
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
if err = client.Close(); err != nil {
|
||||||
|
t.Errorf("failed to close server connection: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// getTestConnection takes environment variables to establish a connection to a real
|
// getTestConnection takes environment variables to establish a connection to a real
|
||||||
// SMTP server to test all functionality that requires a connection
|
// SMTP server to test all functionality that requires a connection
|
||||||
func getTestConnection(auth bool) (*Client, error) {
|
func getTestConnection(auth bool) (*Client, error) {
|
||||||
|
@ -2100,6 +2208,7 @@ func handleTestServerConnection(connection net.Conn, featureSet string, failRese
|
||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
time.Sleep(time.Millisecond)
|
||||||
|
|
||||||
var datastring string
|
var datastring string
|
||||||
data = strings.TrimSpace(data)
|
data = strings.TrimSpace(data)
|
||||||
|
|
|
@ -2,8 +2,8 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
//go:build go1.19 && !go1.20
|
//go:build !go1.20
|
||||||
// +build go1.19,!go1.20
|
// +build !go1.20
|
||||||
|
|
||||||
package mail
|
package mail
|
||||||
|
|
||||||
|
|
127
smtp/smtp.go
127
smtp/smtp.go
|
@ -30,6 +30,7 @@ import (
|
||||||
"net/textproto"
|
"net/textproto"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/wneessen/go-mail/log"
|
"github.com/wneessen/go-mail/log"
|
||||||
|
@ -37,28 +38,49 @@ import (
|
||||||
|
|
||||||
// A Client represents a client connection to an SMTP server.
|
// A Client represents a client connection to an SMTP server.
|
||||||
type Client struct {
|
type Client struct {
|
||||||
// Text is the textproto.Conn used by the Client. It is exported to allow for
|
// Text is the textproto.Conn used by the Client. It is exported to allow for clients to add extensions.
|
||||||
// clients to add extensions.
|
|
||||||
Text *textproto.Conn
|
Text *textproto.Conn
|
||||||
// keep a reference to the connection so it can be used to create a TLS
|
|
||||||
// connection later
|
// auth supported auth mechanisms
|
||||||
|
auth []string
|
||||||
|
|
||||||
|
// keep a reference to the connection so it can be used to create a TLS connection later
|
||||||
conn net.Conn
|
conn net.Conn
|
||||||
// whether the Client is using TLS
|
|
||||||
tls bool
|
// debug logging is enabled
|
||||||
serverName string
|
debug bool
|
||||||
// map of supported extensions
|
|
||||||
|
// didHello indicates whether we've said HELO/EHLO
|
||||||
|
didHello bool
|
||||||
|
|
||||||
|
// dsnmrtype defines the mail return option in case DSN is enabled
|
||||||
|
dsnmrtype string
|
||||||
|
|
||||||
|
// dsnrntype defines the recipient notify option in case DSN is enabled
|
||||||
|
dsnrntype string
|
||||||
|
|
||||||
|
// ext is a map of supported extensions
|
||||||
ext map[string]string
|
ext map[string]string
|
||||||
// supported auth mechanisms
|
|
||||||
auth []string
|
// helloError is the error from the hello
|
||||||
localName string // the name to use in HELO/EHLO
|
helloError error
|
||||||
didHello bool // whether we've said HELO/EHLO
|
|
||||||
helloError error // the error from the hello
|
// localName is the name to use in HELO/EHLO
|
||||||
// debug logging
|
localName string // the name to use in HELO/EHLO
|
||||||
debug bool // debug logging is enabled
|
|
||||||
logger log.Logger // logger will be used for debug logging
|
// logger will be used for debug logging
|
||||||
// DSN support
|
logger log.Logger
|
||||||
dsnmrtype string // dsnmrtype defines the mail return option in case DSN is enabled
|
|
||||||
dsnrntype string // dsnrntype defines the recipient notify option in case DSN is enabled
|
// mutex is used to synchronize access to shared resources, ensuring that only one goroutine can access
|
||||||
|
// the resource at a time.
|
||||||
|
mutex sync.RWMutex
|
||||||
|
|
||||||
|
// tls indicates whether the Client is using TLS
|
||||||
|
tls bool
|
||||||
|
|
||||||
|
// serverName denotes the name of the server to which the application will connect. Used for
|
||||||
|
// identification and routing.
|
||||||
|
serverName string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dial returns a new [Client] connected to an SMTP server at addr.
|
// Dial returns a new [Client] connected to an SMTP server at addr.
|
||||||
|
@ -95,7 +117,10 @@ func NewClient(conn net.Conn, host string) (*Client, error) {
|
||||||
|
|
||||||
// Close closes the connection.
|
// Close closes the connection.
|
||||||
func (c *Client) Close() error {
|
func (c *Client) Close() error {
|
||||||
return c.Text.Close()
|
c.mutex.Lock()
|
||||||
|
err := c.Text.Close()
|
||||||
|
c.mutex.Unlock()
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// hello runs a hello exchange if needed.
|
// hello runs a hello exchange if needed.
|
||||||
|
@ -122,28 +147,39 @@ func (c *Client) Hello(localName string) error {
|
||||||
if c.didHello {
|
if c.didHello {
|
||||||
return errors.New("smtp: Hello called after other methods")
|
return errors.New("smtp: Hello called after other methods")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c.mutex.Lock()
|
||||||
c.localName = localName
|
c.localName = localName
|
||||||
|
c.mutex.Unlock()
|
||||||
|
|
||||||
return c.hello()
|
return c.hello()
|
||||||
}
|
}
|
||||||
|
|
||||||
// cmd is a convenience function that sends a command and returns the response
|
// 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) {
|
func (c *Client) cmd(expectCode int, format string, args ...interface{}) (int, string, error) {
|
||||||
|
c.mutex.Lock()
|
||||||
|
|
||||||
c.debugLog(log.DirClientToServer, format, args...)
|
c.debugLog(log.DirClientToServer, format, args...)
|
||||||
id, err := c.Text.Cmd(format, args...)
|
id, err := c.Text.Cmd(format, args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
c.mutex.Unlock()
|
||||||
return 0, "", err
|
return 0, "", err
|
||||||
}
|
}
|
||||||
c.Text.StartResponse(id)
|
c.Text.StartResponse(id)
|
||||||
defer c.Text.EndResponse(id)
|
|
||||||
code, msg, err := c.Text.ReadResponse(expectCode)
|
code, msg, err := c.Text.ReadResponse(expectCode)
|
||||||
c.debugLog(log.DirServerToClient, "%d %s", code, msg)
|
c.debugLog(log.DirServerToClient, "%d %s", code, msg)
|
||||||
|
c.Text.EndResponse(id)
|
||||||
|
c.mutex.Unlock()
|
||||||
return code, msg, err
|
return code, msg, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// helo sends the HELO greeting to the server. It should be used only when the
|
// helo sends the HELO greeting to the server. It should be used only when the
|
||||||
// server does not support ehlo.
|
// server does not support ehlo.
|
||||||
func (c *Client) helo() error {
|
func (c *Client) helo() error {
|
||||||
|
c.mutex.Lock()
|
||||||
c.ext = nil
|
c.ext = nil
|
||||||
|
c.mutex.Unlock()
|
||||||
|
|
||||||
_, _, err := c.cmd(250, "HELO %s", c.localName)
|
_, _, err := c.cmd(250, "HELO %s", c.localName)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -158,9 +194,13 @@ func (c *Client) StartTLS(config *tls.Config) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c.mutex.Lock()
|
||||||
c.conn = tls.Client(c.conn, config)
|
c.conn = tls.Client(c.conn, config)
|
||||||
c.Text = textproto.NewConn(c.conn)
|
c.Text = textproto.NewConn(c.conn)
|
||||||
c.tls = true
|
c.tls = true
|
||||||
|
c.mutex.Unlock()
|
||||||
|
|
||||||
return c.ehlo()
|
return c.ehlo()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -168,11 +208,15 @@ func (c *Client) StartTLS(config *tls.Config) error {
|
||||||
// The return values are their zero values if [Client.StartTLS] did
|
// The return values are their zero values if [Client.StartTLS] did
|
||||||
// not succeed.
|
// not succeed.
|
||||||
func (c *Client) TLSConnectionState() (state tls.ConnectionState, ok bool) {
|
func (c *Client) TLSConnectionState() (state tls.ConnectionState, ok bool) {
|
||||||
|
c.mutex.RLock()
|
||||||
|
|
||||||
tc, ok := c.conn.(*tls.Conn)
|
tc, ok := c.conn.(*tls.Conn)
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
return tc.ConnectionState(), true
|
state, ok = tc.ConnectionState(), true
|
||||||
|
c.mutex.RUnlock()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify checks the validity of an email address on the server.
|
// Verify checks the validity of an email address on the server.
|
||||||
|
@ -258,6 +302,8 @@ func (c *Client) Mail(from string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
cmdStr := "MAIL FROM:<%s>"
|
cmdStr := "MAIL FROM:<%s>"
|
||||||
|
|
||||||
|
c.mutex.RLock()
|
||||||
if c.ext != nil {
|
if c.ext != nil {
|
||||||
if _, ok := c.ext["8BITMIME"]; ok {
|
if _, ok := c.ext["8BITMIME"]; ok {
|
||||||
cmdStr += " BODY=8BITMIME"
|
cmdStr += " BODY=8BITMIME"
|
||||||
|
@ -270,6 +316,8 @@ func (c *Client) Mail(from string) error {
|
||||||
cmdStr += fmt.Sprintf(" RET=%s", c.dsnmrtype)
|
cmdStr += fmt.Sprintf(" RET=%s", c.dsnmrtype)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
c.mutex.RUnlock()
|
||||||
|
|
||||||
_, _, err := c.cmd(250, cmdStr, from)
|
_, _, err := c.cmd(250, cmdStr, from)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -281,7 +329,11 @@ func (c *Client) Rcpt(to string) error {
|
||||||
if err := validateLine(to); err != nil {
|
if err := validateLine(to); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c.mutex.RLock()
|
||||||
_, ok := c.ext["DSN"]
|
_, ok := c.ext["DSN"]
|
||||||
|
c.mutex.RUnlock()
|
||||||
|
|
||||||
if ok && c.dsnrntype != "" {
|
if ok && c.dsnrntype != "" {
|
||||||
_, _, err := c.cmd(25, "RCPT TO:<%s> NOTIFY=%s", to, c.dsnrntype)
|
_, _, err := c.cmd(25, "RCPT TO:<%s> NOTIFY=%s", to, c.dsnrntype)
|
||||||
return err
|
return err
|
||||||
|
@ -295,12 +347,23 @@ type dataCloser struct {
|
||||||
io.WriteCloser
|
io.WriteCloser
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Close releases the lock, closes the WriteCloser, waits for a response, and then returns any error encountered.
|
||||||
func (d *dataCloser) Close() error {
|
func (d *dataCloser) Close() error {
|
||||||
|
d.c.mutex.Lock()
|
||||||
_ = d.WriteCloser.Close()
|
_ = d.WriteCloser.Close()
|
||||||
_, _, err := d.c.Text.ReadResponse(250)
|
_, _, err := d.c.Text.ReadResponse(250)
|
||||||
|
d.c.mutex.Unlock()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Write writes data to the underlying WriteCloser while ensuring thread-safety by locking and unlocking a mutex.
|
||||||
|
func (d *dataCloser) Write(p []byte) (n int, err error) {
|
||||||
|
d.c.mutex.Lock()
|
||||||
|
n, err = d.WriteCloser.Write(p)
|
||||||
|
d.c.mutex.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Data issues a DATA command to the server and returns a writer that
|
// 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
|
// 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
|
// close the writer before calling any more methods on c. A call to
|
||||||
|
@ -310,7 +373,14 @@ func (c *Client) Data() (io.WriteCloser, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &dataCloser{c, c.Text.DotWriter()}, nil
|
datacloser := &dataCloser{}
|
||||||
|
|
||||||
|
c.mutex.Lock()
|
||||||
|
datacloser.c = c
|
||||||
|
datacloser.WriteCloser = c.Text.DotWriter()
|
||||||
|
c.mutex.Unlock()
|
||||||
|
|
||||||
|
return datacloser, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var testHookStartTLS func(*tls.Config) // nil, except for tests
|
var testHookStartTLS func(*tls.Config) // nil, except for tests
|
||||||
|
@ -406,7 +476,10 @@ func (c *Client) Extension(ext string) (bool, string) {
|
||||||
return false, ""
|
return false, ""
|
||||||
}
|
}
|
||||||
ext = strings.ToUpper(ext)
|
ext = strings.ToUpper(ext)
|
||||||
|
|
||||||
|
c.mutex.RLock()
|
||||||
param, ok := c.ext[ext]
|
param, ok := c.ext[ext]
|
||||||
|
c.mutex.RUnlock()
|
||||||
return ok, param
|
return ok, param
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -439,7 +512,11 @@ func (c *Client) Quit() error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return c.Text.Close()
|
c.mutex.Lock()
|
||||||
|
err = c.Text.Close()
|
||||||
|
c.mutex.Unlock()
|
||||||
|
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetDebugLog enables the debug logging for incoming and outgoing SMTP messages
|
// SetDebugLog enables the debug logging for incoming and outgoing SMTP messages
|
||||||
|
@ -480,9 +557,11 @@ func (c *Client) HasConnection() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) UpdateDeadline(timeout time.Duration) error {
|
func (c *Client) UpdateDeadline(timeout time.Duration) error {
|
||||||
|
c.mutex.Lock()
|
||||||
if err := c.conn.SetDeadline(time.Now().Add(timeout)); err != nil {
|
if err := c.conn.SetDeadline(time.Now().Add(timeout)); err != nil {
|
||||||
return fmt.Errorf("smtp: failed to update deadline: %w", err)
|
return fmt.Errorf("smtp: failed to update deadline: %w", err)
|
||||||
}
|
}
|
||||||
|
c.mutex.Unlock()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,9 @@ func (c *Client) ehlo() error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c.mutex.Lock()
|
||||||
|
defer c.mutex.Unlock()
|
||||||
ext := make(map[string]string)
|
ext := make(map[string]string)
|
||||||
extList := strings.Split(msg, "\n")
|
extList := strings.Split(msg, "\n")
|
||||||
if len(extList) > 1 {
|
if len(extList) > 1 {
|
||||||
|
|
|
@ -28,6 +28,9 @@ func (c *Client) ehlo() error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c.mutex.Lock()
|
||||||
|
defer c.mutex.Unlock()
|
||||||
ext := make(map[string]string)
|
ext := make(map[string]string)
|
||||||
extList := strings.Split(msg, "\n")
|
extList := strings.Split(msg, "\n")
|
||||||
if len(extList) > 1 {
|
if len(extList) > 1 {
|
||||||
|
|
Loading…
Reference in a new issue