mirror of
https://github.com/wneessen/go-mail.git
synced 2024-11-23 06:10:48 +01:00
Compare commits
84 commits
4b01abd8a9
...
7da89ac3e1
Author | SHA1 | Date | |
---|---|---|---|
|
7da89ac3e1 | ||
627216425f | |||
|
012082978d | ||
65a91a2711 | |||
c1f6ef07d4 | |||
6e98d7e47d | |||
8791ce5a33 | |||
2d98c40cb6 | |||
253d065c83 | |||
6bd9a9c735 | |||
f59aa23ed8 | |||
2234f0c5bc | |||
fdb80ad9dd | |||
fec2f2075a | |||
2084526c77 | |||
23c71d608f | |||
371b950bc7 | |||
b2c4b533d7 | |||
3871b2be44 | |||
8683917c3d | |||
077c85bea0 | |||
909e699b99 | |||
|
b97073db19 | ||
|
d75d990124 | ||
0676d99f20 | |||
|
d6725b2d63 | ||
fd115d5173 | |||
4f6224131e | |||
c8684886ed | |||
07e7b17ae8 | |||
2abdee743d | |||
5503be8451 | |||
774925078a | |||
f1188bdad7 | |||
2cbd0c4aef | |||
9a9e0c936d | |||
33d4eb5b21 | |||
d6e5034bba | |||
1394f1fc20 | |||
26ff177fb0 | |||
5ec8a5a5fe | |||
580d0b0e4e | |||
3f0ac027e2 | |||
0b9a215e7d | |||
4053457020 | |||
f3633e1913 | |||
52061f97c6 | |||
77920be1a1 | |||
f5d4cdafea | |||
19330fc108 | |||
af9915e4e7 | |||
6f869e4efd | |||
44df830348 | |||
4ee11e8406 | |||
bd5a8a40b9 | |||
8dfb121aec | |||
d5437f6b7a | |||
157c138142 | |||
482194b4b3 | |||
b8f0462ce3 | |||
6af6a28f78 | |||
0aa24c6f3a | |||
fbebcf96d8 | |||
619318ba0d | |||
d400379e2f | |||
fcbd202595 | |||
db2ec99cd6 | |||
|
664b7299e6 | ||
508a2f2a6c | |||
f469ba977d | |||
0239318d94 | |||
69e211682c | |||
3bdb6f7cca | |||
277ae9be19 | |||
2e7156182a | |||
8ee37abca2 | |||
ee726487f1 | |||
1dd73778d4 | |||
d3bea90761 | |||
|
68109ed40d | ||
94d5f3f7a1 | |||
|
6d9829776a | ||
a747f5f74c | |||
0ea9631855 |
25 changed files with 1438 additions and 337 deletions
4
.github/workflows/codecov.yml
vendored
4
.github/workflows/codecov.yml
vendored
|
@ -36,10 +36,10 @@ jobs:
|
|||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
go: ['1.20', '1.21', '1.22', '1.23']
|
||||
go: ['1.19', '1.20', '1.23']
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
|
||||
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
|
8
.github/workflows/codeql-analysis.yml
vendored
8
.github/workflows/codeql-analysis.yml
vendored
|
@ -45,7 +45,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
|
||||
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
@ -54,7 +54,7 @@ jobs:
|
|||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@4dd16135b69a43b6c8efb853346f8437d92d3c93 # v3.26.6
|
||||
uses: github/codeql-action/init@e2b3eafc8d227b0241d48be5f425d47c2d750a13 # v3.26.10
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# 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).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@4dd16135b69a43b6c8efb853346f8437d92d3c93 # v3.26.6
|
||||
uses: github/codeql-action/autobuild@e2b3eafc8d227b0241d48be5f425d47c2d750a13 # v3.26.10
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
@ -79,4 +79,4 @@ jobs:
|
|||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@4dd16135b69a43b6c8efb853346f8437d92d3c93 # v3.26.6
|
||||
uses: github/codeql-action/analyze@e2b3eafc8d227b0241d48be5f425d47c2d750a13 # v3.26.10
|
||||
|
|
2
.github/workflows/dependency-review.yml
vendored
2
.github/workflows/dependency-review.yml
vendored
|
@ -21,7 +21,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
|
||||
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
|
2
.github/workflows/golangci-lint.yml
vendored
2
.github/workflows/golangci-lint.yml
vendored
|
@ -20,7 +20,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
|
||||
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
|
2
.github/workflows/govulncheck.yml
vendored
2
.github/workflows/govulncheck.yml
vendored
|
@ -14,7 +14,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
|
||||
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: Run govulncheck
|
||||
|
|
2
.github/workflows/reuse.yml
vendored
2
.github/workflows/reuse.yml
vendored
|
@ -14,7 +14,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
|
||||
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
|
4
.github/workflows/scorecards.yml
vendored
4
.github/workflows/scorecards.yml
vendored
|
@ -35,7 +35,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
|
||||
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
@ -75,6 +75,6 @@ jobs:
|
|||
|
||||
# Upload the results to GitHub's code scanning dashboard.
|
||||
- name: "Upload to code-scanning"
|
||||
uses: github/codeql-action/upload-sarif@4dd16135b69a43b6c8efb853346f8437d92d3c93 # v3.26.6
|
||||
uses: github/codeql-action/upload-sarif@e2b3eafc8d227b0241d48be5f425d47c2d750a13 # v3.26.10
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
|
4
.github/workflows/sonarqube.yml
vendored
4
.github/workflows/sonarqube.yml
vendored
|
@ -27,7 +27,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
|
||||
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
@ -44,7 +44,7 @@ jobs:
|
|||
run: |
|
||||
go test -v -race --coverprofile=./cov.out ./...
|
||||
|
||||
- uses: sonarsource/sonarqube-scan-action@0c0f3958d90fc466625f1d1af1f47bddd4cc6bd1 # master
|
||||
- uses: sonarsource/sonarqube-scan-action@884b79409bbd464b2a59edc326a4b77dc56b2195 # master
|
||||
env:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
|
||||
|
|
164
client.go
164
client.go
|
@ -12,6 +12,7 @@ import (
|
|||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/wneessen/go-mail/log"
|
||||
|
@ -87,12 +88,12 @@ type DialContextFunc func(ctx context.Context, network, address string) (net.Con
|
|||
|
||||
// Client is the SMTP client struct
|
||||
type Client struct {
|
||||
// connection is the net.Conn that the smtp.Client is based on
|
||||
connection net.Conn
|
||||
|
||||
// Timeout for the SMTP server connection
|
||||
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 bool
|
||||
|
||||
|
@ -102,11 +103,9 @@ type Client struct {
|
|||
// dsnrntype defines the DSNRcptNotifyOption in case DSN is enabled
|
||||
dsnrntype []string
|
||||
|
||||
// isEncrypted indicates if a Client connection is encrypted or not
|
||||
isEncrypted bool
|
||||
|
||||
// noNoop indicates the Noop is to be skipped
|
||||
noNoop bool
|
||||
// fallbackPort is used as an alternative port number in case the primary port is unavailable or
|
||||
// fails to bind.
|
||||
fallbackPort int
|
||||
|
||||
// HELO/EHLO string for the greeting the target SMTP server
|
||||
helo string
|
||||
|
@ -114,12 +113,24 @@ type Client struct {
|
|||
// Hostname of the target SMTP server to connect to
|
||||
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 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
|
||||
fallbackPort int
|
||||
|
||||
// smtpAuth is a pointer to smtp.Auth
|
||||
smtpAuth smtp.Auth
|
||||
|
@ -130,26 +141,20 @@ type Client struct {
|
|||
// smtpClient is the smtp.Client that is set up when using the Dial*() methods
|
||||
smtpClient *smtp.Client
|
||||
|
||||
// Use SSL for the connection
|
||||
useSSL bool
|
||||
|
||||
// tlspolicy sets the client to use the provided TLSPolicy for the STARTTLS protocol
|
||||
tlspolicy TLSPolicy
|
||||
|
||||
// tlsconfig represents the tls.Config setting for the STARTTLS connection
|
||||
tlsconfig *tls.Config
|
||||
|
||||
// user is the SMTP AUTH username
|
||||
user string
|
||||
|
||||
// useDebugLog enables the debug logging on the SMTP client
|
||||
useDebugLog bool
|
||||
|
||||
// logger is a logger that implements the log.Logger interface
|
||||
logger log.Logger
|
||||
// user is the SMTP AUTH username
|
||||
user string
|
||||
|
||||
// dialContextFunc is a custom DialContext function to dial target SMTP server
|
||||
dialContextFunc DialContextFunc
|
||||
// Use SSL for the connection
|
||||
useSSL bool
|
||||
}
|
||||
|
||||
// Option returns a function that can be used for grouping Client options
|
||||
|
@ -550,6 +555,9 @@ func (c *Client) SetLogger(logger log.Logger) {
|
|||
|
||||
// SetTLSConfig overrides the current *tls.Config with the given *tls.Config value
|
||||
func (c *Client) SetTLSConfig(tlsconfig *tls.Config) error {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
|
||||
if tlsconfig == nil {
|
||||
return ErrInvalidTLSConfig
|
||||
}
|
||||
|
@ -589,6 +597,9 @@ func (c *Client) setDefaultHelo() error {
|
|||
|
||||
// DialWithContext establishes a connection to the SMTP server with a given context.Context
|
||||
func (c *Client) DialWithContext(dialCtx context.Context) error {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
|
||||
ctx, cancel := context.WithDeadline(dialCtx, time.Now().Add(c.connTimeout))
|
||||
defer cancel()
|
||||
|
||||
|
@ -602,17 +613,16 @@ func (c *Client) DialWithContext(dialCtx context.Context) error {
|
|||
c.dialContextFunc = tlsDialer.DialContext
|
||||
}
|
||||
}
|
||||
var err error
|
||||
c.connection, err = c.dialContextFunc(ctx, "tcp", c.ServerAddr())
|
||||
connection, err := c.dialContextFunc(ctx, "tcp", c.ServerAddr())
|
||||
if err != nil && c.fallbackPort != 0 {
|
||||
// TODO: should we somehow log or append the previous error?
|
||||
c.connection, err = c.dialContextFunc(ctx, "tcp", c.serverFallbackAddr())
|
||||
connection, err = c.dialContextFunc(ctx, "tcp", c.serverFallbackAddr())
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client, err := smtp.NewClient(c.connection, c.host)
|
||||
client, err := smtp.NewClient(connection, c.host)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -691,7 +701,7 @@ func (c *Client) DialAndSendWithContext(ctx context.Context, messages ...*Msg) e
|
|||
// checkConn makes sure that a required server connection is available and extends the
|
||||
// connection deadline
|
||||
func (c *Client) checkConn() error {
|
||||
if c.connection == nil {
|
||||
if !c.smtpClient.HasConnection() {
|
||||
return ErrNoActiveConnection
|
||||
}
|
||||
|
||||
|
@ -701,7 +711,7 @@ func (c *Client) checkConn() error {
|
|||
}
|
||||
}
|
||||
|
||||
if err := c.connection.SetDeadline(time.Now().Add(c.connTimeout)); err != nil {
|
||||
if err := c.smtpClient.UpdateDeadline(c.connTimeout); err != nil {
|
||||
return ErrDeadlineExtendFailed
|
||||
}
|
||||
return nil
|
||||
|
@ -715,7 +725,7 @@ func (c *Client) serverFallbackAddr() string {
|
|||
|
||||
// tls tries to make sure that the STARTTLS requirements are satisfied
|
||||
func (c *Client) tls() error {
|
||||
if c.connection == nil {
|
||||
if !c.smtpClient.HasConnection() {
|
||||
return ErrNoActiveConnection
|
||||
}
|
||||
if !c.useSSL && c.tlspolicy != NoTLS {
|
||||
|
@ -787,3 +797,103 @@ func (c *Client) auth() error {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// sendSingleMsg sends out a single message and returns an error if the transmission/delivery fails.
|
||||
// It is invoked by the public Send methods
|
||||
func (c *Client) sendSingleMsg(message *Msg) error {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
|
||||
if message.encoding == NoEncoding {
|
||||
if ok, _ := c.smtpClient.Extension("8BITMIME"); !ok {
|
||||
return &SendError{Reason: ErrNoUnencoded, isTemp: false, affectedMsg: message}
|
||||
}
|
||||
}
|
||||
from, err := message.GetSender(false)
|
||||
if err != nil {
|
||||
return &SendError{
|
||||
Reason: ErrGetSender, errlist: []error{err}, isTemp: isTempError(err),
|
||||
affectedMsg: message,
|
||||
}
|
||||
}
|
||||
rcpts, err := message.GetRecipients()
|
||||
if err != nil {
|
||||
return &SendError{
|
||||
Reason: ErrGetRcpts, errlist: []error{err}, isTemp: isTempError(err),
|
||||
affectedMsg: message,
|
||||
}
|
||||
}
|
||||
|
||||
if c.dsn {
|
||||
if c.dsnmrtype != "" {
|
||||
c.smtpClient.SetDSNMailReturnOption(string(c.dsnmrtype))
|
||||
}
|
||||
}
|
||||
if err = c.smtpClient.Mail(from); err != nil {
|
||||
retError := &SendError{
|
||||
Reason: ErrSMTPMailFrom, errlist: []error{err}, isTemp: isTempError(err),
|
||||
affectedMsg: message,
|
||||
}
|
||||
if resetSendErr := c.smtpClient.Reset(); resetSendErr != nil {
|
||||
retError.errlist = append(retError.errlist, resetSendErr)
|
||||
}
|
||||
return retError
|
||||
}
|
||||
hasError := false
|
||||
rcptSendErr := &SendError{affectedMsg: message}
|
||||
rcptSendErr.errlist = make([]error, 0)
|
||||
rcptSendErr.rcpt = make([]string, 0)
|
||||
rcptNotifyOpt := strings.Join(c.dsnrntype, ",")
|
||||
c.smtpClient.SetDSNRcptNotifyOption(rcptNotifyOpt)
|
||||
for _, rcpt := range rcpts {
|
||||
if err = c.smtpClient.Rcpt(rcpt); err != nil {
|
||||
rcptSendErr.Reason = ErrSMTPRcptTo
|
||||
rcptSendErr.errlist = append(rcptSendErr.errlist, err)
|
||||
rcptSendErr.rcpt = append(rcptSendErr.rcpt, rcpt)
|
||||
rcptSendErr.isTemp = isTempError(err)
|
||||
hasError = true
|
||||
}
|
||||
}
|
||||
if hasError {
|
||||
if resetSendErr := c.smtpClient.Reset(); resetSendErr != nil {
|
||||
rcptSendErr.errlist = append(rcptSendErr.errlist, resetSendErr)
|
||||
}
|
||||
return rcptSendErr
|
||||
}
|
||||
writer, err := c.smtpClient.Data()
|
||||
if err != nil {
|
||||
return &SendError{
|
||||
Reason: ErrSMTPData, errlist: []error{err}, isTemp: isTempError(err),
|
||||
affectedMsg: message,
|
||||
}
|
||||
}
|
||||
_, err = message.WriteTo(writer)
|
||||
if err != nil {
|
||||
return &SendError{
|
||||
Reason: ErrWriteContent, errlist: []error{err}, isTemp: isTempError(err),
|
||||
affectedMsg: message,
|
||||
}
|
||||
}
|
||||
message.isDelivered = true
|
||||
|
||||
if err = writer.Close(); err != nil {
|
||||
return &SendError{
|
||||
Reason: ErrSMTPDataClose, errlist: []error{err}, isTemp: isTempError(err),
|
||||
affectedMsg: message,
|
||||
}
|
||||
}
|
||||
|
||||
if err = c.Reset(); err != nil {
|
||||
return &SendError{
|
||||
Reason: ErrSMTPReset, errlist: []error{err}, isTemp: isTempError(err),
|
||||
affectedMsg: message,
|
||||
}
|
||||
}
|
||||
if err = c.checkConn(); err != nil {
|
||||
return &SendError{
|
||||
Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err),
|
||||
affectedMsg: message,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
106
client_119.go
106
client_119.go
|
@ -7,111 +7,23 @@
|
|||
|
||||
package mail
|
||||
|
||||
import "strings"
|
||||
import "errors"
|
||||
|
||||
// Send sends out the mail message
|
||||
func (c *Client) Send(messages ...*Msg) error {
|
||||
if cerr := c.checkConn(); cerr != nil {
|
||||
return &SendError{Reason: ErrConnCheck, errlist: []error{cerr}, isTemp: isTempError(cerr)}
|
||||
if err := c.checkConn(); err != nil {
|
||||
return &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)}
|
||||
}
|
||||
var errs []*SendError
|
||||
for _, message := range messages {
|
||||
message.sendError = nil
|
||||
if message.encoding == NoEncoding {
|
||||
if ok, _ := c.smtpClient.Extension("8BITMIME"); !ok {
|
||||
sendErr := &SendError{Reason: ErrNoUnencoded, isTemp: false}
|
||||
message.sendError = sendErr
|
||||
errs = append(errs, sendErr)
|
||||
continue
|
||||
}
|
||||
}
|
||||
from, err := message.GetSender(false)
|
||||
if err != nil {
|
||||
sendErr := &SendError{Reason: ErrGetSender, errlist: []error{err}, isTemp: isTempError(err)}
|
||||
message.sendError = sendErr
|
||||
errs = append(errs, sendErr)
|
||||
continue
|
||||
}
|
||||
rcpts, err := message.GetRecipients()
|
||||
if err != nil {
|
||||
sendErr := &SendError{Reason: ErrGetRcpts, errlist: []error{err}, isTemp: isTempError(err)}
|
||||
message.sendError = sendErr
|
||||
errs = append(errs, sendErr)
|
||||
continue
|
||||
}
|
||||
for id, message := range messages {
|
||||
if sendErr := c.sendSingleMsg(message); sendErr != nil {
|
||||
messages[id].sendError = sendErr
|
||||
|
||||
if c.dsn {
|
||||
if c.dsnmrtype != "" {
|
||||
c.smtpClient.SetDSNMailReturnOption(string(c.dsnmrtype))
|
||||
var msgSendErr *SendError
|
||||
if errors.As(sendErr, &msgSendErr) {
|
||||
errs = append(errs, msgSendErr)
|
||||
}
|
||||
}
|
||||
if err = c.smtpClient.Mail(from); err != nil {
|
||||
sendErr := &SendError{Reason: ErrSMTPMailFrom, errlist: []error{err}, isTemp: isTempError(err)}
|
||||
if resetSendErr := c.smtpClient.Reset(); resetSendErr != nil {
|
||||
sendErr.errlist = append(sendErr.errlist, resetSendErr)
|
||||
}
|
||||
message.sendError = sendErr
|
||||
errs = append(errs, sendErr)
|
||||
continue
|
||||
}
|
||||
failed := false
|
||||
rcptSendErr := &SendError{}
|
||||
rcptSendErr.errlist = make([]error, 0)
|
||||
rcptSendErr.rcpt = make([]string, 0)
|
||||
rcptNotifyOpt := strings.Join(c.dsnrntype, ",")
|
||||
c.smtpClient.SetDSNRcptNotifyOption(rcptNotifyOpt)
|
||||
for _, rcpt := range rcpts {
|
||||
if err = c.smtpClient.Rcpt(rcpt); err != nil {
|
||||
rcptSendErr.Reason = ErrSMTPRcptTo
|
||||
rcptSendErr.errlist = append(rcptSendErr.errlist, err)
|
||||
rcptSendErr.rcpt = append(rcptSendErr.rcpt, rcpt)
|
||||
rcptSendErr.isTemp = isTempError(err)
|
||||
failed = true
|
||||
}
|
||||
}
|
||||
if failed {
|
||||
if resetSendErr := c.smtpClient.Reset(); resetSendErr != nil {
|
||||
rcptSendErr.errlist = append(rcptSendErr.errlist, err)
|
||||
}
|
||||
message.sendError = rcptSendErr
|
||||
errs = append(errs, rcptSendErr)
|
||||
continue
|
||||
}
|
||||
writer, err := c.smtpClient.Data()
|
||||
if err != nil {
|
||||
sendErr := &SendError{Reason: ErrSMTPData, errlist: []error{err}, isTemp: isTempError(err)}
|
||||
message.sendError = sendErr
|
||||
errs = append(errs, sendErr)
|
||||
continue
|
||||
}
|
||||
_, err = message.WriteTo(writer)
|
||||
if err != nil {
|
||||
sendErr := &SendError{Reason: ErrWriteContent, errlist: []error{err}, isTemp: isTempError(err)}
|
||||
message.sendError = sendErr
|
||||
errs = append(errs, sendErr)
|
||||
continue
|
||||
}
|
||||
message.isDelivered = true
|
||||
|
||||
if err = writer.Close(); err != nil {
|
||||
sendErr := &SendError{Reason: ErrSMTPDataClose, errlist: []error{err}, isTemp: isTempError(err)}
|
||||
message.sendError = sendErr
|
||||
errs = append(errs, sendErr)
|
||||
continue
|
||||
}
|
||||
|
||||
if err = c.Reset(); err != nil {
|
||||
sendErr := &SendError{Reason: ErrSMTPReset, errlist: []error{err}, isTemp: isTempError(err)}
|
||||
message.sendError = sendErr
|
||||
errs = append(errs, sendErr)
|
||||
continue
|
||||
}
|
||||
if err = c.checkConn(); err != nil {
|
||||
sendErr := &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)}
|
||||
message.sendError = sendErr
|
||||
errs = append(errs, sendErr)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
|
|
|
@ -9,7 +9,6 @@ package mail
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Send sends out the mail message
|
||||
|
@ -18,92 +17,16 @@ func (c *Client) Send(messages ...*Msg) (returnErr error) {
|
|||
returnErr = &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)}
|
||||
return
|
||||
}
|
||||
for _, message := range messages {
|
||||
message.sendError = nil
|
||||
if message.encoding == NoEncoding {
|
||||
if ok, _ := c.smtpClient.Extension("8BITMIME"); !ok {
|
||||
message.sendError = &SendError{Reason: ErrNoUnencoded, isTemp: false}
|
||||
returnErr = errors.Join(returnErr, message.sendError)
|
||||
continue
|
||||
}
|
||||
}
|
||||
from, err := message.GetSender(false)
|
||||
if err != nil {
|
||||
message.sendError = &SendError{Reason: ErrGetSender, errlist: []error{err}, isTemp: isTempError(err)}
|
||||
returnErr = errors.Join(returnErr, message.sendError)
|
||||
continue
|
||||
}
|
||||
rcpts, err := message.GetRecipients()
|
||||
if err != nil {
|
||||
message.sendError = &SendError{Reason: ErrGetRcpts, errlist: []error{err}, isTemp: isTempError(err)}
|
||||
returnErr = errors.Join(returnErr, message.sendError)
|
||||
continue
|
||||
}
|
||||
|
||||
if c.dsn {
|
||||
if c.dsnmrtype != "" {
|
||||
c.smtpClient.SetDSNMailReturnOption(string(c.dsnmrtype))
|
||||
}
|
||||
}
|
||||
if err = c.smtpClient.Mail(from); err != nil {
|
||||
message.sendError = &SendError{Reason: ErrSMTPMailFrom, errlist: []error{err}, isTemp: isTempError(err)}
|
||||
returnErr = errors.Join(returnErr, message.sendError)
|
||||
if resetSendErr := c.smtpClient.Reset(); resetSendErr != nil {
|
||||
returnErr = errors.Join(returnErr, resetSendErr)
|
||||
}
|
||||
continue
|
||||
}
|
||||
failed := false
|
||||
rcptSendErr := &SendError{}
|
||||
rcptSendErr.errlist = make([]error, 0)
|
||||
rcptSendErr.rcpt = make([]string, 0)
|
||||
rcptNotifyOpt := strings.Join(c.dsnrntype, ",")
|
||||
c.smtpClient.SetDSNRcptNotifyOption(rcptNotifyOpt)
|
||||
for _, rcpt := range rcpts {
|
||||
if err = c.smtpClient.Rcpt(rcpt); err != nil {
|
||||
rcptSendErr.Reason = ErrSMTPRcptTo
|
||||
rcptSendErr.errlist = append(rcptSendErr.errlist, err)
|
||||
rcptSendErr.rcpt = append(rcptSendErr.rcpt, rcpt)
|
||||
rcptSendErr.isTemp = isTempError(err)
|
||||
failed = true
|
||||
}
|
||||
}
|
||||
if failed {
|
||||
if resetSendErr := c.smtpClient.Reset(); resetSendErr != nil {
|
||||
returnErr = errors.Join(returnErr, resetSendErr)
|
||||
}
|
||||
message.sendError = rcptSendErr
|
||||
returnErr = errors.Join(returnErr, message.sendError)
|
||||
continue
|
||||
}
|
||||
writer, err := c.smtpClient.Data()
|
||||
if err != nil {
|
||||
message.sendError = &SendError{Reason: ErrSMTPData, errlist: []error{err}, isTemp: isTempError(err)}
|
||||
returnErr = errors.Join(returnErr, message.sendError)
|
||||
continue
|
||||
}
|
||||
_, err = message.WriteTo(writer)
|
||||
if err != nil {
|
||||
message.sendError = &SendError{Reason: ErrWriteContent, errlist: []error{err}, isTemp: isTempError(err)}
|
||||
returnErr = errors.Join(returnErr, message.sendError)
|
||||
continue
|
||||
}
|
||||
message.isDelivered = true
|
||||
var errs []error
|
||||
defer func() {
|
||||
returnErr = errors.Join(errs...)
|
||||
}()
|
||||
|
||||
if err = writer.Close(); err != nil {
|
||||
message.sendError = &SendError{Reason: ErrSMTPDataClose, errlist: []error{err}, isTemp: isTempError(err)}
|
||||
returnErr = errors.Join(returnErr, message.sendError)
|
||||
continue
|
||||
}
|
||||
|
||||
if err = c.Reset(); err != nil {
|
||||
message.sendError = &SendError{Reason: ErrSMTPReset, errlist: []error{err}, isTemp: isTempError(err)}
|
||||
returnErr = errors.Join(returnErr, message.sendError)
|
||||
continue
|
||||
}
|
||||
if err = c.checkConn(); err != nil {
|
||||
message.sendError = &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)}
|
||||
returnErr = errors.Join(returnErr, message.sendError)
|
||||
for id, message := range messages {
|
||||
if sendErr := c.sendSingleMsg(message); sendErr != nil {
|
||||
messages[id].sendError = sendErr
|
||||
errs = append(errs, sendErr)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
948
client_test.go
948
client_test.go
File diff suppressed because it is too large
Load diff
15
msg.go
15
msg.go
|
@ -481,8 +481,8 @@ func (m *Msg) SetMessageID() {
|
|||
if err != nil {
|
||||
hostname = "localhost.localdomain"
|
||||
}
|
||||
randNumPrimary, _ := randNum(100000000)
|
||||
randNumSecondary, _ := randNum(10000)
|
||||
randNumPrimary := randNum(100000000)
|
||||
randNumSecondary := randNum(10000)
|
||||
randString, _ := randomStringSecure(17)
|
||||
procID := os.Getpid() * randNumSecondary
|
||||
messageID := fmt.Sprintf("%d.%d%d.%s@%s", procID, randNumPrimary, randNumSecondary,
|
||||
|
@ -490,6 +490,17 @@ func (m *Msg) SetMessageID() {
|
|||
m.SetMessageIDWithValue(messageID)
|
||||
}
|
||||
|
||||
// GetMessageID returns the message ID of the Msg as string value. If no message ID
|
||||
// is set, an empty string will be returned
|
||||
func (m *Msg) GetMessageID() string {
|
||||
if msgidheader, ok := m.genHeader[HeaderMessageID]; ok {
|
||||
if len(msgidheader) > 0 {
|
||||
return msgidheader[0]
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// SetMessageIDWithValue sets the message id for the mail
|
||||
func (m *Msg) SetMessageIDWithValue(messageID string) {
|
||||
m.SetGenHeader(HeaderMessageID, fmt.Sprintf("<%s>", messageID))
|
||||
|
|
|
@ -61,3 +61,25 @@ func TestMsg_WriteToSendmail(t *testing.T) {
|
|||
t.Errorf("WriteToSendmail failed: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMsg_WriteToTempFileFailed(t *testing.T) {
|
||||
m := NewMsg()
|
||||
_ = m.From("Toni Tester <tester@example.com>")
|
||||
_ = m.To("Ellenor Tester <ellinor@example.com>")
|
||||
m.SetBodyString(TypeTextPlain, "This is a test")
|
||||
|
||||
curTmpDir := os.Getenv("TMPDIR")
|
||||
defer func() {
|
||||
if err := os.Setenv("TMPDIR", curTmpDir); err != nil {
|
||||
t.Errorf("failed to set TMPDIR environment variable: %s", err)
|
||||
}
|
||||
}()
|
||||
|
||||
if err := os.Setenv("TMPDIR", "/invalid/directory/that/does/not/exist"); err != nil {
|
||||
t.Errorf("failed to set TMPDIR environment variable: %s", err)
|
||||
}
|
||||
_, err := m.WriteToTempFile()
|
||||
if err == nil {
|
||||
t.Errorf("WriteToTempFile() did not fail as expected")
|
||||
}
|
||||
}
|
||||
|
|
23
msg_test.go
23
msg_test.go
|
@ -786,13 +786,11 @@ func TestMsg_SetMessageIDWithValue(t *testing.T) {
|
|||
// TestMsg_SetMessageIDRandomness tests the randomness of Msg.SetMessageID methods
|
||||
func TestMsg_SetMessageIDRandomness(t *testing.T) {
|
||||
var mids []string
|
||||
for i := 0; i < 100; i++ {
|
||||
for i := 0; i < 50_000; i++ {
|
||||
m := NewMsg()
|
||||
m.SetMessageID()
|
||||
mid := m.GetGenHeader(HeaderMessageID)
|
||||
if len(mid) > 0 {
|
||||
mids = append(mids, mid[0])
|
||||
}
|
||||
mid := m.GetMessageID()
|
||||
mids = append(mids, mid)
|
||||
}
|
||||
c := make(map[string]int)
|
||||
for i := range mids {
|
||||
|
@ -805,6 +803,21 @@ func TestMsg_SetMessageIDRandomness(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestMsg_GetMessageID(t *testing.T) {
|
||||
expected := "this.is.a.message.id"
|
||||
msg := NewMsg()
|
||||
msg.SetMessageIDWithValue(expected)
|
||||
val := msg.GetMessageID()
|
||||
if !strings.EqualFold(val, fmt.Sprintf("<%s>", expected)) {
|
||||
t.Errorf("GetMessageID() failed. Expected: %s, got: %s", fmt.Sprintf("<%s>", expected), val)
|
||||
}
|
||||
msg.genHeader[HeaderMessageID] = nil
|
||||
val = msg.GetMessageID()
|
||||
if val != "" {
|
||||
t.Errorf("GetMessageID() failed. Expected empty string, got: %s", val)
|
||||
}
|
||||
}
|
||||
|
||||
// TestMsg_FromFormat tests the FromFormat and EnvelopeFrom methods for the Msg object
|
||||
func TestMsg_FromFormat(t *testing.T) {
|
||||
tests := []struct {
|
||||
|
|
22
random.go
22
random.go
|
@ -7,8 +7,6 @@ package mail
|
|||
import (
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strings"
|
||||
)
|
||||
|
||||
|
@ -52,23 +50,3 @@ func randomStringSecure(length int) (string, error) {
|
|||
|
||||
return randString.String(), nil
|
||||
}
|
||||
|
||||
// randNum returns a random number with a maximum value of length
|
||||
func randNum(length int) (int, error) {
|
||||
if length <= 0 {
|
||||
return 0, fmt.Errorf("provided number is <= 0: %d", length)
|
||||
}
|
||||
length64 := big.NewInt(int64(length))
|
||||
if !length64.IsUint64() {
|
||||
return 0, fmt.Errorf("big.NewInt() generation returned negative value: %d", length64)
|
||||
}
|
||||
randNum64, err := rand.Int(rand.Reader, length64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
randomNum := int(randNum64.Int64())
|
||||
if randomNum < 0 {
|
||||
return 0, fmt.Errorf("generated random number does not fit as int64: %d", randNum64)
|
||||
}
|
||||
return randomNum, nil
|
||||
}
|
||||
|
|
22
random_119.go
Normal file
22
random_119.go
Normal file
|
@ -0,0 +1,22 @@
|
|||
// SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build !go1.20
|
||||
// +build !go1.20
|
||||
|
||||
package mail
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"time"
|
||||
)
|
||||
|
||||
// randNum returns a random number with a maximum value of length
|
||||
func randNum(maxval int) int {
|
||||
if maxval <= 0 {
|
||||
return 0
|
||||
}
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
return rand.Intn(maxval)
|
||||
}
|
20
random_121.go
Normal file
20
random_121.go
Normal file
|
@ -0,0 +1,20 @@
|
|||
// SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build go1.20 && !go1.22
|
||||
// +build go1.20,!go1.22
|
||||
|
||||
package mail
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
)
|
||||
|
||||
// randNum returns a random number with a maximum value of length
|
||||
func randNum(maxval int) int {
|
||||
if maxval <= 0 {
|
||||
return 0
|
||||
}
|
||||
return rand.Intn(maxval)
|
||||
}
|
22
random_122.go
Normal file
22
random_122.go
Normal file
|
@ -0,0 +1,22 @@
|
|||
// SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build go1.22
|
||||
// +build go1.22
|
||||
|
||||
package mail
|
||||
|
||||
import (
|
||||
"math/rand/v2"
|
||||
)
|
||||
|
||||
// randNum returns a random number with a maximum value of maxval.
|
||||
// go-mail compiled with Go 1.22+ will make use of the novel math/rand/v2 interface
|
||||
// Older versions of Go will use math/rand
|
||||
func randNum(maxval int) int {
|
||||
if maxval <= 0 {
|
||||
return 0
|
||||
}
|
||||
return rand.IntN(maxval)
|
||||
}
|
|
@ -55,16 +55,17 @@ func TestRandomNum(t *testing.T) {
|
|||
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.testName, func(t *testing.T) {
|
||||
rn, err := randNum(tc.max)
|
||||
if err != nil {
|
||||
t.Errorf("random number generation failed: %s", err)
|
||||
}
|
||||
if rn < 0 {
|
||||
t.Errorf("random number generation failed: %d is smaller than zero", rn)
|
||||
}
|
||||
rn := randNum(tc.max)
|
||||
if rn > tc.max {
|
||||
t.Errorf("random number generation failed: %d is bigger than given value %d", rn, tc.max)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRandomNumZero(t *testing.T) {
|
||||
rn := randNum(0)
|
||||
if rn != 0 {
|
||||
t.Errorf("random number generation failed: %d is not zero", rn)
|
||||
}
|
||||
}
|
||||
|
|
27
senderror.go
27
senderror.go
|
@ -56,10 +56,11 @@ const (
|
|||
|
||||
// SendError is an error wrapper for delivery errors of the Msg
|
||||
type SendError struct {
|
||||
Reason SendErrReason
|
||||
isTemp bool
|
||||
affectedMsg *Msg
|
||||
errlist []error
|
||||
isTemp bool
|
||||
rcpt []string
|
||||
Reason SendErrReason
|
||||
}
|
||||
|
||||
// SendErrReason represents a comparable reason on why the delivery failed
|
||||
|
@ -92,6 +93,11 @@ func (e *SendError) Error() string {
|
|||
}
|
||||
}
|
||||
}
|
||||
if e.affectedMsg != nil && e.affectedMsg.GetMessageID() != "" {
|
||||
errMessage.WriteString(", affected message ID: ")
|
||||
errMessage.WriteString(e.affectedMsg.GetMessageID())
|
||||
}
|
||||
|
||||
return errMessage.String()
|
||||
}
|
||||
|
||||
|
@ -112,6 +118,23 @@ func (e *SendError) IsTemp() bool {
|
|||
return e.isTemp
|
||||
}
|
||||
|
||||
// MessageID returns the message ID of the affected Msg that caused the error
|
||||
// If no message ID was set for the Msg, an empty string will be returned
|
||||
func (e *SendError) MessageID() string {
|
||||
if e == nil || e.affectedMsg == nil {
|
||||
return ""
|
||||
}
|
||||
return e.affectedMsg.GetMessageID()
|
||||
}
|
||||
|
||||
// Msg returns the pointer to the affected message that caused the error
|
||||
func (e *SendError) Msg() *Msg {
|
||||
if e == nil || e.affectedMsg == nil {
|
||||
return nil
|
||||
}
|
||||
return e.affectedMsg
|
||||
}
|
||||
|
||||
// String implements the Stringer interface for the SendErrReason
|
||||
func (r SendErrReason) String() string {
|
||||
switch r {
|
||||
|
|
|
@ -83,7 +83,96 @@ func TestSendError_IsTemp(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestSendError_IsTempNil(t *testing.T) {
|
||||
var se *SendError
|
||||
if se.IsTemp() {
|
||||
t.Error("expected false on nil-senderror")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendError_MessageID(t *testing.T) {
|
||||
var se *SendError
|
||||
err := returnSendError(ErrAmbiguous, false)
|
||||
if !errors.As(err, &se) {
|
||||
t.Errorf("error mismatch, expected error to be of type *SendError")
|
||||
return
|
||||
}
|
||||
if errors.As(err, &se) {
|
||||
if se.MessageID() == "" {
|
||||
t.Errorf("sendError expected message-id, but got empty string")
|
||||
}
|
||||
if !strings.EqualFold(se.MessageID(), "<this.is.a.message.id>") {
|
||||
t.Errorf("sendError message-id expected: %s, but got: %s", "<this.is.a.message.id>",
|
||||
se.MessageID())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendError_MessageIDNil(t *testing.T) {
|
||||
var se *SendError
|
||||
if se.MessageID() != "" {
|
||||
t.Error("expected empty string on nil-senderror")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendError_Msg(t *testing.T) {
|
||||
var se *SendError
|
||||
err := returnSendError(ErrAmbiguous, false)
|
||||
if !errors.As(err, &se) {
|
||||
t.Errorf("error mismatch, expected error to be of type *SendError")
|
||||
return
|
||||
}
|
||||
if errors.As(err, &se) {
|
||||
if se.Msg() == nil {
|
||||
t.Errorf("sendError expected msg pointer, but got nil")
|
||||
}
|
||||
from := se.Msg().GetFromString()
|
||||
if len(from) == 0 {
|
||||
t.Errorf("sendError expected msg from, but got empty string")
|
||||
return
|
||||
}
|
||||
if !strings.EqualFold(from[0], "<toni.tester@domain.tld>") {
|
||||
t.Errorf("sendError message from expected: %s, but got: %s", "<toni.tester@domain.tld>",
|
||||
from[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendError_MsgNil(t *testing.T) {
|
||||
var se *SendError
|
||||
if se.Msg() != nil {
|
||||
t.Error("expected nil on nil-senderror")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendError_IsFail(t *testing.T) {
|
||||
err1 := returnSendError(ErrAmbiguous, false)
|
||||
err2 := returnSendError(ErrSMTPMailFrom, false)
|
||||
if errors.Is(err1, err2) {
|
||||
t.Errorf("error mismatch, ErrAmbiguous should not be equal to ErrSMTPMailFrom")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendError_ErrorMulti(t *testing.T) {
|
||||
expected := `ambiguous reason, check Msg.SendError for message specific reasons, ` +
|
||||
`affected recipient(s): <email1@domain.tld>, <email2@domain.tld>`
|
||||
err := &SendError{
|
||||
Reason: ErrAmbiguous, isTemp: false, affectedMsg: nil,
|
||||
rcpt: []string{"<email1@domain.tld>", "<email2@domain.tld>"},
|
||||
}
|
||||
if err.Error() != expected {
|
||||
t.Errorf("error mismatch, expected: %s, got: %s", expected, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// returnSendError is a helper method to retunr a SendError with a specific reason
|
||||
func returnSendError(r SendErrReason, t bool) error {
|
||||
return &SendError{Reason: r, isTemp: t}
|
||||
message := NewMsg()
|
||||
_ = message.From("toni.tester@domain.tld")
|
||||
_ = message.To("tina.tester@domain.tld")
|
||||
message.Subject("This is the subject")
|
||||
message.SetBodyString(TypeTextPlain, "This is the message body")
|
||||
message.SetMessageIDWithValue("this.is.a.message.id")
|
||||
|
||||
return &SendError{Reason: r, isTemp: t, affectedMsg: message}
|
||||
}
|
||||
|
|
141
smtp/smtp.go
141
smtp/smtp.go
|
@ -30,34 +30,57 @@ import (
|
|||
"net/textproto"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/wneessen/go-mail/log"
|
||||
)
|
||||
|
||||
// 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 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 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
|
||||
|
||||
// debug logging is enabled
|
||||
debug bool
|
||||
|
||||
// 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
|
||||
|
||||
// helloError is the error from the hello
|
||||
helloError error
|
||||
|
||||
// localName is the name to use in HELO/EHLO
|
||||
localName string // the name to use in HELO/EHLO
|
||||
didHello bool // whether we've said HELO/EHLO
|
||||
helloError error // the error from the hello
|
||||
// debug logging
|
||||
debug bool // debug logging is enabled
|
||||
logger log.Logger // logger will be used for debug logging
|
||||
// DSN support
|
||||
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
|
||||
|
||||
// logger will be used for debug logging
|
||||
logger log.Logger
|
||||
|
||||
// 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.
|
||||
|
@ -94,7 +117,10 @@ func NewClient(conn net.Conn, host string) (*Client, error) {
|
|||
|
||||
// Close closes the connection.
|
||||
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.
|
||||
|
@ -121,28 +147,39 @@ func (c *Client) Hello(localName string) error {
|
|||
if c.didHello {
|
||||
return errors.New("smtp: Hello called after other methods")
|
||||
}
|
||||
|
||||
c.mutex.Lock()
|
||||
c.localName = localName
|
||||
c.mutex.Unlock()
|
||||
|
||||
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) {
|
||||
c.mutex.Lock()
|
||||
|
||||
c.debugLog(log.DirClientToServer, format, args...)
|
||||
id, err := c.Text.Cmd(format, args...)
|
||||
if err != nil {
|
||||
c.mutex.Unlock()
|
||||
return 0, "", err
|
||||
}
|
||||
c.Text.StartResponse(id)
|
||||
defer c.Text.EndResponse(id)
|
||||
code, msg, err := c.Text.ReadResponse(expectCode)
|
||||
c.debugLog(log.DirServerToClient, "%d %s", code, msg)
|
||||
c.Text.EndResponse(id)
|
||||
c.mutex.Unlock()
|
||||
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.mutex.Lock()
|
||||
c.ext = nil
|
||||
c.mutex.Unlock()
|
||||
|
||||
_, _, err := c.cmd(250, "HELO %s", c.localName)
|
||||
return err
|
||||
}
|
||||
|
@ -157,9 +194,13 @@ func (c *Client) StartTLS(config *tls.Config) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.mutex.Lock()
|
||||
c.conn = tls.Client(c.conn, config)
|
||||
c.Text = textproto.NewConn(c.conn)
|
||||
c.tls = true
|
||||
c.mutex.Unlock()
|
||||
|
||||
return c.ehlo()
|
||||
}
|
||||
|
||||
|
@ -167,11 +208,15 @@ func (c *Client) StartTLS(config *tls.Config) error {
|
|||
// The return values are their zero values if [Client.StartTLS] did
|
||||
// not succeed.
|
||||
func (c *Client) TLSConnectionState() (state tls.ConnectionState, ok bool) {
|
||||
c.mutex.RLock()
|
||||
defer c.mutex.RUnlock()
|
||||
|
||||
tc, ok := c.conn.(*tls.Conn)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
return tc.ConnectionState(), true
|
||||
state, ok = tc.ConnectionState(), true
|
||||
return
|
||||
}
|
||||
|
||||
// Verify checks the validity of an email address on the server.
|
||||
|
@ -257,6 +302,8 @@ func (c *Client) Mail(from string) error {
|
|||
return err
|
||||
}
|
||||
cmdStr := "MAIL FROM:<%s>"
|
||||
|
||||
c.mutex.RLock()
|
||||
if c.ext != nil {
|
||||
if _, ok := c.ext["8BITMIME"]; ok {
|
||||
cmdStr += " BODY=8BITMIME"
|
||||
|
@ -269,6 +316,8 @@ func (c *Client) Mail(from string) error {
|
|||
cmdStr += fmt.Sprintf(" RET=%s", c.dsnmrtype)
|
||||
}
|
||||
}
|
||||
c.mutex.RUnlock()
|
||||
|
||||
_, _, err := c.cmd(250, cmdStr, from)
|
||||
return err
|
||||
}
|
||||
|
@ -280,7 +329,11 @@ func (c *Client) Rcpt(to string) error {
|
|||
if err := validateLine(to); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.mutex.RLock()
|
||||
_, ok := c.ext["DSN"]
|
||||
c.mutex.RUnlock()
|
||||
|
||||
if ok && c.dsnrntype != "" {
|
||||
_, _, err := c.cmd(25, "RCPT TO:<%s> NOTIFY=%s", to, c.dsnrntype)
|
||||
return err
|
||||
|
@ -294,12 +347,23 @@ type dataCloser struct {
|
|||
io.WriteCloser
|
||||
}
|
||||
|
||||
// Close releases the lock, closes the WriteCloser, waits for a response, and then returns any error encountered.
|
||||
func (d *dataCloser) Close() error {
|
||||
d.c.mutex.Lock()
|
||||
_ = d.WriteCloser.Close()
|
||||
_, _, err := d.c.Text.ReadResponse(250)
|
||||
d.c.mutex.Unlock()
|
||||
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
|
||||
// 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
|
||||
|
@ -309,7 +373,14 @@ func (c *Client) Data() (io.WriteCloser, error) {
|
|||
if err != nil {
|
||||
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
|
||||
|
@ -405,7 +476,10 @@ func (c *Client) Extension(ext string) (bool, string) {
|
|||
return false, ""
|
||||
}
|
||||
ext = strings.ToUpper(ext)
|
||||
|
||||
c.mutex.RLock()
|
||||
param, ok := c.ext[ext]
|
||||
c.mutex.RUnlock()
|
||||
return ok, param
|
||||
}
|
||||
|
||||
|
@ -438,7 +512,11 @@ func (c *Client) Quit() error {
|
|||
if err != nil {
|
||||
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
|
||||
|
@ -472,6 +550,21 @@ func (c *Client) SetDSNRcptNotifyOption(d string) {
|
|||
c.dsnrntype = d
|
||||
}
|
||||
|
||||
// HasConnection checks if the client has an active connection.
|
||||
// Returns true if the `conn` field is not nil, indicating an active connection.
|
||||
func (c *Client) HasConnection() bool {
|
||||
return c.conn != nil
|
||||
}
|
||||
|
||||
func (c *Client) UpdateDeadline(timeout time.Duration) error {
|
||||
c.mutex.Lock()
|
||||
if err := c.conn.SetDeadline(time.Now().Add(timeout)); err != nil {
|
||||
return fmt.Errorf("smtp: failed to update deadline: %w", err)
|
||||
}
|
||||
c.mutex.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// debugLog checks if the debug flag is set and if so logs the provided message to
|
||||
// the log.Logger interface
|
||||
func (c *Client) debugLog(d log.Direction, f string, a ...interface{}) {
|
||||
|
|
|
@ -25,6 +25,9 @@ func (c *Client) ehlo() error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
ext := make(map[string]string)
|
||||
extList := strings.Split(msg, "\n")
|
||||
if len(extList) > 1 {
|
||||
|
|
|
@ -22,12 +22,15 @@ import "strings"
|
|||
// should be the preferred greeting for servers that support it.
|
||||
//
|
||||
// Backport of: https://github.com/golang/go/commit/4d8db00641cc9ff4f44de7df9b8c4f4a4f9416ee#diff-4f6f6bdb9891d4dd271f9f31430420a2e44018fe4ee539576faf458bebb3cee4
|
||||
// to guarantee backwards compatibility with Go 1.16/1.17:w
|
||||
// to guarantee backwards compatibility with Go 1.16/1.17
|
||||
func (c *Client) ehlo() error {
|
||||
_, msg, err := c.cmd(250, "EHLO %s", c.localName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
ext := make(map[string]string)
|
||||
extList := strings.Split(msg, "\n")
|
||||
if len(extList) > 1 {
|
||||
|
|
Loading…
Reference in a new issue