mirror of
https://github.com/wneessen/go-mail.git
synced 2024-11-23 06:10:48 +01:00
Compare commits
54 commits
1e3f4fe757
...
bf9d55cdd1
Author | SHA1 | Date | |
---|---|---|---|
|
bf9d55cdd1 | ||
|
4b60557518 | ||
|
9bdb741e05 | ||
|
2368113872 | ||
|
12edb2724b | ||
077c85bea0 | |||
909e699b99 | |||
|
b97073db19 | ||
|
d75d990124 | ||
0676d99f20 | |||
|
d6725b2d63 | ||
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 |
29 changed files with 1468 additions and 347 deletions
4
.github/workflows/codecov.yml
vendored
4
.github/workflows/codecov.yml
vendored
|
@ -36,10 +36,10 @@ jobs:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||||
go: ['1.20', '1.21', '1.22', '1.23']
|
go: ['1.19', '1.20', '1.23']
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
|
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
|
8
.github/workflows/codeql-analysis.yml
vendored
8
.github/workflows/codeql-analysis.yml
vendored
|
@ -45,7 +45,7 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
|
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
@ -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@4dd16135b69a43b6c8efb853346f8437d92d3c93 # v3.26.6
|
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@4dd16135b69a43b6c8efb853346f8437d92d3c93 # v3.26.6
|
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@4dd16135b69a43b6c8efb853346f8437d92d3c93 # v3.26.6
|
uses: github/codeql-action/analyze@461ef6c76dfe95d5c364de2f431ddbd31a417628 # v3.26.9
|
||||||
|
|
2
.github/workflows/dependency-review.yml
vendored
2
.github/workflows/dependency-review.yml
vendored
|
@ -21,7 +21,7 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
|
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
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
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
|
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
|
2
.github/workflows/govulncheck.yml
vendored
2
.github/workflows/govulncheck.yml
vendored
|
@ -14,7 +14,7 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
|
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
- name: Run govulncheck
|
- name: Run govulncheck
|
||||||
|
|
2
.github/workflows/reuse.yml
vendored
2
.github/workflows/reuse.yml
vendored
|
@ -14,7 +14,7 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
|
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
|
4
.github/workflows/scorecards.yml
vendored
4
.github/workflows/scorecards.yml
vendored
|
@ -35,7 +35,7 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
|
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
@ -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@4dd16135b69a43b6c8efb853346f8437d92d3c93 # v3.26.6
|
uses: github/codeql-action/upload-sarif@461ef6c76dfe95d5c364de2f431ddbd31a417628 # v3.26.9
|
||||||
with:
|
with:
|
||||||
sarif_file: results.sarif
|
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
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
|
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
@ -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 }}
|
||||||
|
|
97
client.go
97
client.go
|
@ -787,3 +787,100 @@ func (c *Client) auth() error {
|
||||||
}
|
}
|
||||||
return nil
|
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 {
|
||||||
|
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
|
package mail
|
||||||
|
|
||||||
import "strings"
|
import "errors"
|
||||||
|
|
||||||
// Send sends out the mail message
|
// Send sends out the mail message
|
||||||
func (c *Client) Send(messages ...*Msg) error {
|
func (c *Client) Send(messages ...*Msg) error {
|
||||||
if cerr := c.checkConn(); cerr != nil {
|
if err := c.checkConn(); err != nil {
|
||||||
return &SendError{Reason: ErrConnCheck, errlist: []error{cerr}, isTemp: isTempError(cerr)}
|
return &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)}
|
||||||
}
|
}
|
||||||
var errs []*SendError
|
var errs []*SendError
|
||||||
for _, message := range messages {
|
for id, message := range messages {
|
||||||
message.sendError = nil
|
if sendErr := c.sendSingleMsg(message); sendErr != nil {
|
||||||
if message.encoding == NoEncoding {
|
messages[id].sendError = sendErr
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.dsn {
|
var msgSendErr *SendError
|
||||||
if c.dsnmrtype != "" {
|
if errors.As(sendErr, &msgSendErr) {
|
||||||
c.smtpClient.SetDSNMailReturnOption(string(c.dsnmrtype))
|
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 {
|
if len(errs) > 0 {
|
||||||
|
|
|
@ -9,7 +9,6 @@ package mail
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Send sends out the mail message
|
// 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)}
|
returnErr = &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)}
|
||||||
return
|
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 {
|
var errs []error
|
||||||
if c.dsnmrtype != "" {
|
defer func() {
|
||||||
c.smtpClient.SetDSNMailReturnOption(string(c.dsnmrtype))
|
returnErr = errors.Join(errs...)
|
||||||
}
|
}()
|
||||||
}
|
|
||||||
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
|
|
||||||
|
|
||||||
if err = writer.Close(); err != nil {
|
for id, message := range messages {
|
||||||
message.sendError = &SendError{Reason: ErrSMTPDataClose, errlist: []error{err}, isTemp: isTempError(err)}
|
if sendErr := c.sendSingleMsg(message); sendErr != nil {
|
||||||
returnErr = errors.Join(returnErr, message.sendError)
|
messages[id].sendError = sendErr
|
||||||
continue
|
errs = append(errs, sendErr)
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
694
client_test.go
694
client_test.go
|
@ -5,6 +5,7 @@
|
||||||
package mail
|
package mail
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"errors"
|
"errors"
|
||||||
|
@ -21,11 +22,18 @@ import (
|
||||||
"github.com/wneessen/go-mail/smtp"
|
"github.com/wneessen/go-mail/smtp"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DefaultHost is used as default hostname for the Client
|
const (
|
||||||
const DefaultHost = "localhost"
|
// DefaultHost is used as default hostname for the Client
|
||||||
|
DefaultHost = "localhost"
|
||||||
// TestRcpt
|
// TestRcpt is a trash mail address to send test mails to
|
||||||
const TestRcpt = "go-mail@mytrashmailer.com"
|
TestRcpt = "go-mail@mytrashmailer.com"
|
||||||
|
// TestServerProto is the protocol used for the simple SMTP test server
|
||||||
|
TestServerProto = "tcp"
|
||||||
|
// TestServerAddr is the address the simple SMTP test server listens on
|
||||||
|
TestServerAddr = "127.0.0.1"
|
||||||
|
// TestServerPortBase is the base port for the simple SMTP test server
|
||||||
|
TestServerPortBase = 2025
|
||||||
|
)
|
||||||
|
|
||||||
// TestNewClient tests the NewClient() method with its default options
|
// TestNewClient tests the NewClient() method with its default options
|
||||||
func TestNewClient(t *testing.T) {
|
func TestNewClient(t *testing.T) {
|
||||||
|
@ -629,7 +637,7 @@ func TestClient_DialWithContext(t *testing.T) {
|
||||||
// TestClient_DialWithContext_Fallback tests the Client.DialWithContext method with the fallback
|
// TestClient_DialWithContext_Fallback tests the Client.DialWithContext method with the fallback
|
||||||
// port functionality
|
// port functionality
|
||||||
func TestClient_DialWithContext_Fallback(t *testing.T) {
|
func TestClient_DialWithContext_Fallback(t *testing.T) {
|
||||||
c, err := getTestConnection(true)
|
c, err := getTestConnectionNoTestPort(true)
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
@ -1251,6 +1259,475 @@ func TestClient_DialAndSendWithContext_withSendError(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestClient_SendErrorNoEncoding(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
featureSet := "250-AUTH PLAIN\r\n250-DSN\r\n250 SMTPUTF8"
|
||||||
|
serverPort := TestServerPortBase + 1
|
||||||
|
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 * 300)
|
||||||
|
|
||||||
|
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")
|
||||||
|
message.SetEncoding(NoEncoding)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
if err = client.DialWithContext(context.Background()); err != nil {
|
||||||
|
t.Errorf("failed to dial to test server: %s", err)
|
||||||
|
}
|
||||||
|
if err = client.Send(message); err == nil {
|
||||||
|
t.Error("expected Send() to fail but didn't")
|
||||||
|
}
|
||||||
|
|
||||||
|
var sendErr *SendError
|
||||||
|
if !errors.As(err, &sendErr) {
|
||||||
|
t.Errorf("expected *SendError type as returned error, but got %T", sendErr)
|
||||||
|
}
|
||||||
|
if errors.As(err, &sendErr) {
|
||||||
|
if sendErr.IsTemp() {
|
||||||
|
t.Errorf("expected permanent error but IsTemp() returned true")
|
||||||
|
}
|
||||||
|
if sendErr.Reason != ErrNoUnencoded {
|
||||||
|
t.Errorf("expected ErrNoUnencoded error, but got %s", sendErr.Reason)
|
||||||
|
}
|
||||||
|
if !strings.EqualFold(sendErr.MessageID(), "<this.is.a.message.id>") {
|
||||||
|
t.Errorf("expected message ID: %q, but got %q", "<this.is.a.message.id>",
|
||||||
|
sendErr.MessageID())
|
||||||
|
}
|
||||||
|
if sendErr.Msg() == nil {
|
||||||
|
t.Errorf("expected message to be set, but got nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = client.Close(); err != nil {
|
||||||
|
t.Errorf("failed to close server connection: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_SendErrorMailFrom(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
serverPort := TestServerPortBase + 2
|
||||||
|
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 * 300)
|
||||||
|
|
||||||
|
message := NewMsg()
|
||||||
|
if err := message.From("invalid-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")
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
if err = client.DialWithContext(context.Background()); err != nil {
|
||||||
|
t.Errorf("failed to dial to test server: %s", err)
|
||||||
|
}
|
||||||
|
if err = client.Send(message); err == nil {
|
||||||
|
t.Error("expected Send() to fail but didn't")
|
||||||
|
}
|
||||||
|
|
||||||
|
var sendErr *SendError
|
||||||
|
if !errors.As(err, &sendErr) {
|
||||||
|
t.Errorf("expected *SendError type as returned error, but got %T", sendErr)
|
||||||
|
}
|
||||||
|
if errors.As(err, &sendErr) {
|
||||||
|
if sendErr.IsTemp() {
|
||||||
|
t.Errorf("expected permanent error but IsTemp() returned true")
|
||||||
|
}
|
||||||
|
if sendErr.Reason != ErrSMTPMailFrom {
|
||||||
|
t.Errorf("expected ErrSMTPMailFrom error, but got %s", sendErr.Reason)
|
||||||
|
}
|
||||||
|
if !strings.EqualFold(sendErr.MessageID(), "<this.is.a.message.id>") {
|
||||||
|
t.Errorf("expected message ID: %q, but got %q", "<this.is.a.message.id>",
|
||||||
|
sendErr.MessageID())
|
||||||
|
}
|
||||||
|
if sendErr.Msg() == nil {
|
||||||
|
t.Errorf("expected message to be set, but got nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = client.Close(); err != nil {
|
||||||
|
t.Errorf("failed to close server connection: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_SendErrorMailFromReset(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
serverPort := TestServerPortBase + 3
|
||||||
|
featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"
|
||||||
|
go func() {
|
||||||
|
if err := simpleSMTPServer(ctx, featureSet, true, serverPort); err != nil {
|
||||||
|
t.Errorf("failed to start test server: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
time.Sleep(time.Millisecond * 300)
|
||||||
|
|
||||||
|
message := NewMsg()
|
||||||
|
if err := message.From("invalid-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")
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
if err = client.DialWithContext(context.Background()); err != nil {
|
||||||
|
t.Errorf("failed to dial to test server: %s", err)
|
||||||
|
}
|
||||||
|
if err = client.Send(message); err == nil {
|
||||||
|
t.Error("expected Send() to fail but didn't")
|
||||||
|
}
|
||||||
|
|
||||||
|
var sendErr *SendError
|
||||||
|
if !errors.As(err, &sendErr) {
|
||||||
|
t.Errorf("expected *SendError type as returned error, but got %T", sendErr)
|
||||||
|
}
|
||||||
|
if errors.As(err, &sendErr) {
|
||||||
|
if sendErr.IsTemp() {
|
||||||
|
t.Errorf("expected permanent error but IsTemp() returned true")
|
||||||
|
}
|
||||||
|
if sendErr.Reason != ErrSMTPMailFrom {
|
||||||
|
t.Errorf("expected ErrSMTPMailFrom error, but got %s", sendErr.Reason)
|
||||||
|
}
|
||||||
|
if !strings.EqualFold(sendErr.MessageID(), "<this.is.a.message.id>") {
|
||||||
|
t.Errorf("expected message ID: %q, but got %q", "<this.is.a.message.id>",
|
||||||
|
sendErr.MessageID())
|
||||||
|
}
|
||||||
|
if len(sendErr.errlist) != 2 {
|
||||||
|
t.Errorf("expected 2 errors, but got %d", len(sendErr.errlist))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !strings.EqualFold(sendErr.errlist[0].Error(), "503 5.1.2 Invalid from: <invalid-from@domain.tld>") {
|
||||||
|
t.Errorf("expected error: %q, but got %q",
|
||||||
|
"503 5.1.2 Invalid from: <invalid-from@domain.tld>", sendErr.errlist[0].Error())
|
||||||
|
}
|
||||||
|
if !strings.EqualFold(sendErr.errlist[1].Error(), "500 5.1.2 Error: reset failed") {
|
||||||
|
t.Errorf("expected error: %q, but got %q",
|
||||||
|
"500 5.1.2 Error: reset failed", sendErr.errlist[1].Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = client.Close(); err != nil {
|
||||||
|
t.Errorf("failed to close server connection: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_SendErrorToReset(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
serverPort := TestServerPortBase + 4
|
||||||
|
featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"
|
||||||
|
go func() {
|
||||||
|
if err := simpleSMTPServer(ctx, featureSet, true, serverPort); err != nil {
|
||||||
|
t.Errorf("failed to start test server: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
time.Sleep(time.Millisecond * 300)
|
||||||
|
|
||||||
|
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("invalid-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")
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
if err = client.DialWithContext(context.Background()); err != nil {
|
||||||
|
t.Errorf("failed to dial to test server: %s", err)
|
||||||
|
}
|
||||||
|
if err = client.Send(message); err == nil {
|
||||||
|
t.Error("expected Send() to fail but didn't")
|
||||||
|
}
|
||||||
|
|
||||||
|
var sendErr *SendError
|
||||||
|
if !errors.As(err, &sendErr) {
|
||||||
|
t.Errorf("expected *SendError type as returned error, but got %T", sendErr)
|
||||||
|
}
|
||||||
|
if errors.As(err, &sendErr) {
|
||||||
|
if sendErr.IsTemp() {
|
||||||
|
t.Errorf("expected permanent error but IsTemp() returned true")
|
||||||
|
}
|
||||||
|
if sendErr.Reason != ErrSMTPRcptTo {
|
||||||
|
t.Errorf("expected ErrSMTPRcptTo error, but got %s", sendErr.Reason)
|
||||||
|
}
|
||||||
|
if !strings.EqualFold(sendErr.MessageID(), "<this.is.a.message.id>") {
|
||||||
|
t.Errorf("expected message ID: %q, but got %q", "<this.is.a.message.id>",
|
||||||
|
sendErr.MessageID())
|
||||||
|
}
|
||||||
|
if len(sendErr.errlist) != 2 {
|
||||||
|
t.Errorf("expected 2 errors, but got %d", len(sendErr.errlist))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !strings.EqualFold(sendErr.errlist[0].Error(), "500 5.1.2 Invalid to: <invalid-to@domain.tld>") {
|
||||||
|
t.Errorf("expected error: %q, but got %q",
|
||||||
|
"500 5.1.2 Invalid to: <invalid-to@domain.tld>", sendErr.errlist[0].Error())
|
||||||
|
}
|
||||||
|
if !strings.EqualFold(sendErr.errlist[1].Error(), "500 5.1.2 Error: reset failed") {
|
||||||
|
t.Errorf("expected error: %q, but got %q",
|
||||||
|
"500 5.1.2 Error: reset failed", sendErr.errlist[1].Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = client.Close(); err != nil {
|
||||||
|
t.Errorf("failed to close server connection: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_SendErrorDataClose(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
serverPort := TestServerPortBase + 5
|
||||||
|
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 * 300)
|
||||||
|
|
||||||
|
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, "DATA close should fail")
|
||||||
|
message.SetMessageIDWithValue("this.is.a.message.id")
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
if err = client.DialWithContext(context.Background()); err != nil {
|
||||||
|
t.Errorf("failed to dial to test server: %s", err)
|
||||||
|
}
|
||||||
|
if err = client.Send(message); err == nil {
|
||||||
|
t.Error("expected Send() to fail but didn't")
|
||||||
|
}
|
||||||
|
|
||||||
|
var sendErr *SendError
|
||||||
|
if !errors.As(err, &sendErr) {
|
||||||
|
t.Errorf("expected *SendError type as returned error, but got %T", sendErr)
|
||||||
|
}
|
||||||
|
if errors.As(err, &sendErr) {
|
||||||
|
if sendErr.IsTemp() {
|
||||||
|
t.Errorf("expected permanent error but IsTemp() returned true")
|
||||||
|
}
|
||||||
|
if sendErr.Reason != ErrSMTPDataClose {
|
||||||
|
t.Errorf("expected ErrSMTPDataClose error, but got %s", sendErr.Reason)
|
||||||
|
}
|
||||||
|
if !strings.EqualFold(sendErr.MessageID(), "<this.is.a.message.id>") {
|
||||||
|
t.Errorf("expected message ID: %q, but got %q", "<this.is.a.message.id>",
|
||||||
|
sendErr.MessageID())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = client.Close(); err != nil {
|
||||||
|
t.Errorf("failed to close server connection: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_SendErrorDataWrite(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
serverPort := TestServerPortBase + 6
|
||||||
|
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 * 300)
|
||||||
|
|
||||||
|
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, "DATA write should fail")
|
||||||
|
message.SetMessageIDWithValue("this.is.a.message.id")
|
||||||
|
message.SetGenHeader("X-Test-Header", "DATA write should fail")
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
if err = client.DialWithContext(context.Background()); err != nil {
|
||||||
|
t.Errorf("failed to dial to test server: %s", err)
|
||||||
|
}
|
||||||
|
if err = client.Send(message); err == nil {
|
||||||
|
t.Error("expected Send() to fail but didn't")
|
||||||
|
}
|
||||||
|
|
||||||
|
var sendErr *SendError
|
||||||
|
if !errors.As(err, &sendErr) {
|
||||||
|
t.Errorf("expected *SendError type as returned error, but got %T", sendErr)
|
||||||
|
}
|
||||||
|
if errors.As(err, &sendErr) {
|
||||||
|
if sendErr.IsTemp() {
|
||||||
|
t.Errorf("expected permanent error but IsTemp() returned true")
|
||||||
|
}
|
||||||
|
if sendErr.Reason != ErrSMTPDataClose {
|
||||||
|
t.Errorf("expected ErrSMTPDataClose error, but got %s", sendErr.Reason)
|
||||||
|
}
|
||||||
|
if !strings.EqualFold(sendErr.MessageID(), "<this.is.a.message.id>") {
|
||||||
|
t.Errorf("expected message ID: %q, but got %q", "<this.is.a.message.id>",
|
||||||
|
sendErr.MessageID())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_SendErrorReset(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
serverPort := TestServerPortBase + 7
|
||||||
|
featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"
|
||||||
|
go func() {
|
||||||
|
if err := simpleSMTPServer(ctx, featureSet, true, serverPort); err != nil {
|
||||||
|
t.Errorf("failed to start test server: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
time.Sleep(time.Millisecond * 300)
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
if err = client.DialWithContext(context.Background()); err != nil {
|
||||||
|
t.Errorf("failed to dial to test server: %s", err)
|
||||||
|
}
|
||||||
|
if err = client.Send(message); err == nil {
|
||||||
|
t.Error("expected Send() to fail but didn't")
|
||||||
|
}
|
||||||
|
|
||||||
|
var sendErr *SendError
|
||||||
|
if !errors.As(err, &sendErr) {
|
||||||
|
t.Errorf("expected *SendError type as returned error, but got %T", sendErr)
|
||||||
|
}
|
||||||
|
if errors.As(err, &sendErr) {
|
||||||
|
if sendErr.IsTemp() {
|
||||||
|
t.Errorf("expected permanent error but IsTemp() returned true")
|
||||||
|
}
|
||||||
|
if sendErr.Reason != ErrSMTPReset {
|
||||||
|
t.Errorf("expected ErrSMTPReset error, but got %s", sendErr.Reason)
|
||||||
|
}
|
||||||
|
if !strings.EqualFold(sendErr.MessageID(), "<this.is.a.message.id>") {
|
||||||
|
t.Errorf("expected message ID: %q, but got %q", "<this.is.a.message.id>",
|
||||||
|
sendErr.MessageID())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
@ -1302,6 +1779,50 @@ func getTestConnection(auth bool) (*Client, error) {
|
||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getTestConnectionNoTestPort takes environment variables (except the port) to establish a
|
||||||
|
// connection to a real SMTP server to test all functionality that requires a connection
|
||||||
|
func getTestConnectionNoTestPort(auth bool) (*Client, error) {
|
||||||
|
if os.Getenv("TEST_SKIP_ONLINE") != "" {
|
||||||
|
return nil, fmt.Errorf("env variable TEST_SKIP_ONLINE is set. Skipping online tests")
|
||||||
|
}
|
||||||
|
th := os.Getenv("TEST_HOST")
|
||||||
|
if th == "" {
|
||||||
|
return nil, fmt.Errorf("no TEST_HOST set")
|
||||||
|
}
|
||||||
|
sv := false
|
||||||
|
if sve := os.Getenv("TEST_TLS_SKIP_VERIFY"); sve != "" {
|
||||||
|
sv = true
|
||||||
|
}
|
||||||
|
c, err := NewClient(th)
|
||||||
|
if err != nil {
|
||||||
|
return c, err
|
||||||
|
}
|
||||||
|
c.tlsconfig.InsecureSkipVerify = sv
|
||||||
|
if auth {
|
||||||
|
st := os.Getenv("TEST_SMTPAUTH_TYPE")
|
||||||
|
if st != "" {
|
||||||
|
c.SetSMTPAuth(SMTPAuthType(st))
|
||||||
|
}
|
||||||
|
u := os.Getenv("TEST_SMTPAUTH_USER")
|
||||||
|
if u != "" {
|
||||||
|
c.SetUsername(u)
|
||||||
|
}
|
||||||
|
p := os.Getenv("TEST_SMTPAUTH_PASS")
|
||||||
|
if p != "" {
|
||||||
|
c.SetPassword(p)
|
||||||
|
}
|
||||||
|
// We don't want to log authentication data in tests
|
||||||
|
c.SetDebugLog(false)
|
||||||
|
}
|
||||||
|
if err := c.DialWithContext(context.Background()); err != nil {
|
||||||
|
return c, fmt.Errorf("connection to test server failed: %w", err)
|
||||||
|
}
|
||||||
|
if err := c.Close(); err != nil {
|
||||||
|
return c, fmt.Errorf("disconnect from test server failed: %w", err)
|
||||||
|
}
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
// getTestClient takes environment variables to establish a client without connecting
|
// getTestClient takes environment variables to establish a client without connecting
|
||||||
// to the SMTP server
|
// to the SMTP server
|
||||||
func getTestClient(auth bool) (*Client, error) {
|
func getTestClient(auth bool) (*Client, error) {
|
||||||
|
@ -1357,7 +1878,14 @@ func getTestConnectionWithDSN(auth bool) (*Client, error) {
|
||||||
if th == "" {
|
if th == "" {
|
||||||
return nil, fmt.Errorf("no TEST_HOST set")
|
return nil, fmt.Errorf("no TEST_HOST set")
|
||||||
}
|
}
|
||||||
c, err := NewClient(th, WithDSN())
|
tp := 25
|
||||||
|
if tps := os.Getenv("TEST_PORT"); tps != "" {
|
||||||
|
tpi, err := strconv.Atoi(tps)
|
||||||
|
if err == nil {
|
||||||
|
tp = tpi
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c, err := NewClient(th, WithDSN(), WithPort(tp))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c, err
|
return c, err
|
||||||
}
|
}
|
||||||
|
@ -1494,3 +2022,155 @@ func (f faker) RemoteAddr() net.Addr { return nil }
|
||||||
func (f faker) SetDeadline(time.Time) error { return nil }
|
func (f faker) SetDeadline(time.Time) error { return nil }
|
||||||
func (f faker) SetReadDeadline(time.Time) error { return nil }
|
func (f faker) SetReadDeadline(time.Time) error { return nil }
|
||||||
func (f faker) SetWriteDeadline(time.Time) error { return nil }
|
func (f faker) SetWriteDeadline(time.Time) error { return nil }
|
||||||
|
|
||||||
|
// simpleSMTPServer starts a simple TCP server that resonds to SMTP commands.
|
||||||
|
// The provided featureSet represents in what the server responds to EHLO command
|
||||||
|
// failReset controls if a RSET succeeds
|
||||||
|
func simpleSMTPServer(ctx context.Context, featureSet string, failReset bool, port int) error {
|
||||||
|
listener, err := net.Listen(TestServerProto, fmt.Sprintf("%s:%d", TestServerAddr, port))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to listen on %s://%s: %w", TestServerProto, TestServerAddr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err := listener.Close(); err != nil {
|
||||||
|
fmt.Printf("unable to close listener: %s\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
connection, err := listener.Accept()
|
||||||
|
var opErr *net.OpError
|
||||||
|
if err != nil {
|
||||||
|
if errors.As(err, &opErr) && opErr.Temporary() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return fmt.Errorf("unable to accept connection: %w", err)
|
||||||
|
}
|
||||||
|
handleTestServerConnection(connection, featureSet, failReset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleTestServerConnection(connection net.Conn, featureSet string, failReset bool) {
|
||||||
|
defer func() {
|
||||||
|
if err := connection.Close(); err != nil {
|
||||||
|
fmt.Printf("unable to close connection: %s\n", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
reader := bufio.NewReader(connection)
|
||||||
|
writer := bufio.NewWriter(connection)
|
||||||
|
|
||||||
|
writeLine := func(data string) error {
|
||||||
|
_, err := writer.WriteString(data + "\r\n")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to write line: %w", err)
|
||||||
|
}
|
||||||
|
return writer.Flush()
|
||||||
|
}
|
||||||
|
writeOK := func() {
|
||||||
|
_ = writeLine("250 2.0.0 OK")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := writeLine("220 go-mail test server ready ESMTP"); err != nil {
|
||||||
|
fmt.Printf("unable to write to client: %s\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := reader.ReadString('\n')
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("unable to read from connection: %s\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(data, "EHLO") && !strings.HasPrefix(data, "HELO") {
|
||||||
|
fmt.Printf("expected EHLO, got %q", data)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = writeLine("250-localhost.localdomain\r\n" + featureSet); err != nil {
|
||||||
|
fmt.Printf("unable to write to connection: %s\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
data, err = reader.ReadString('\n')
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, io.EOF) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
fmt.Println("Error reading data:", err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
var datastring string
|
||||||
|
data = strings.TrimSpace(data)
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(data, "MAIL FROM:"):
|
||||||
|
from := strings.TrimPrefix(data, "MAIL FROM:")
|
||||||
|
from = strings.ReplaceAll(from, "BODY=8BITMIME", "")
|
||||||
|
from = strings.ReplaceAll(from, "SMTPUTF8", "")
|
||||||
|
from = strings.TrimSpace(from)
|
||||||
|
if !strings.EqualFold(from, "<valid-from@domain.tld>") {
|
||||||
|
_ = writeLine(fmt.Sprintf("503 5.1.2 Invalid from: %s", from))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
writeOK()
|
||||||
|
case strings.HasPrefix(data, "RCPT TO:"):
|
||||||
|
to := strings.TrimPrefix(data, "RCPT TO:")
|
||||||
|
to = strings.TrimSpace(to)
|
||||||
|
if !strings.EqualFold(to, "<valid-to@domain.tld>") {
|
||||||
|
_ = writeLine(fmt.Sprintf("500 5.1.2 Invalid to: %s", to))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
writeOK()
|
||||||
|
case strings.HasPrefix(data, "AUTH PLAIN"):
|
||||||
|
auth := strings.TrimPrefix(data, "AUTH PLAIN ")
|
||||||
|
if !strings.EqualFold(auth, "AHRvbmlAdGVzdGVyLmNvbQBWM3J5UzNjcjN0Kw==") {
|
||||||
|
_ = writeLine("535 5.7.8 Error: authentication failed")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
_ = writeLine("235 2.7.0 Authentication successful")
|
||||||
|
case strings.EqualFold(data, "DATA"):
|
||||||
|
_ = writeLine("354 End data with <CR><LF>.<CR><LF>")
|
||||||
|
for {
|
||||||
|
ddata, derr := reader.ReadString('\n')
|
||||||
|
if derr != nil {
|
||||||
|
fmt.Printf("failed to read DATA data from connection: %s\n", derr)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
ddata = strings.TrimSpace(ddata)
|
||||||
|
if strings.EqualFold(ddata, "DATA write should fail") {
|
||||||
|
_ = writeLine("500 5.0.0 Error during DATA transmission")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if ddata == "." {
|
||||||
|
if strings.Contains(datastring, "DATA close should fail") {
|
||||||
|
_ = writeLine("500 5.0.0 Error during DATA closing")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
_ = writeLine("250 2.0.0 Ok: queued as 1234567890")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
datastring += ddata + "\n"
|
||||||
|
}
|
||||||
|
case strings.EqualFold(data, "noop"),
|
||||||
|
strings.EqualFold(data, "vrfy"):
|
||||||
|
writeOK()
|
||||||
|
case strings.EqualFold(data, "rset"):
|
||||||
|
if failReset {
|
||||||
|
_ = writeLine("500 5.1.2 Error: reset failed")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
writeOK()
|
||||||
|
case strings.EqualFold(data, "quit"):
|
||||||
|
_ = writeLine("221 2.0.0 Bye")
|
||||||
|
default:
|
||||||
|
_ = writeLine("500 5.5.2 Error: bad syntax")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
62
dummy-chain-cert.pem
Normal file
62
dummy-chain-cert.pem
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
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
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
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
|
||||||
|
-----END CERTIFICATE-----
|
52
dummy-child-key.pem
Normal file
52
dummy-child-key.pem
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
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
|
||||||
|
S++xGnGHEUSEl7cWnO8k0RyJzzc=
|
||||||
|
-----END PRIVATE KEY-----
|
59
msg.go
59
msg.go
|
@ -7,8 +7,7 @@ package mail
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"crypto/rsa"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
|
||||||
"embed"
|
"embed"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
@ -208,8 +207,8 @@ func WithNoDefaultUserAgent() MsgOption {
|
||||||
}
|
}
|
||||||
|
|
||||||
// SignWithSMime configures the Msg to be signed with S/MIME
|
// SignWithSMime configures the Msg to be signed with S/MIME
|
||||||
func (m *Msg) SignWithSMime(privateKey *rsa.PrivateKey, certificate *x509.Certificate) error {
|
func (m *Msg) SignWithSMime(keyPair *tls.Certificate) error {
|
||||||
sMime, err := NewSMime(privateKey, certificate)
|
sMime, err := newSMime(keyPair)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -482,8 +481,8 @@ func (m *Msg) SetMessageID() {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
hostname = "localhost.localdomain"
|
hostname = "localhost.localdomain"
|
||||||
}
|
}
|
||||||
randNumPrimary, _ := randNum(100000000)
|
randNumPrimary := randNum(100000000)
|
||||||
randNumSecondary, _ := randNum(10000)
|
randNumSecondary := randNum(10000)
|
||||||
randString, _ := randomStringSecure(17)
|
randString, _ := randomStringSecure(17)
|
||||||
procID := os.Getpid() * randNumSecondary
|
procID := os.Getpid() * randNumSecondary
|
||||||
messageID := fmt.Sprintf("%d.%d%d.%s@%s", procID, randNumPrimary, randNumSecondary,
|
messageID := fmt.Sprintf("%d.%d%d.%s@%s", procID, randNumPrimary, randNumSecondary,
|
||||||
|
@ -491,6 +490,17 @@ func (m *Msg) SetMessageID() {
|
||||||
m.SetMessageIDWithValue(messageID)
|
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
|
// SetMessageIDWithValue sets the message id for the mail
|
||||||
func (m *Msg) SetMessageIDWithValue(messageID string) {
|
func (m *Msg) SetMessageIDWithValue(messageID string) {
|
||||||
m.SetGenHeader(HeaderMessageID, fmt.Sprintf("<%s>", messageID))
|
m.SetGenHeader(HeaderMessageID, fmt.Sprintf("<%s>", messageID))
|
||||||
|
@ -985,28 +995,31 @@ func (m *Msg) applyMiddlewares(msg *Msg) *Msg {
|
||||||
|
|
||||||
// signMessage sign the Msg with S/MIME
|
// signMessage sign the Msg with S/MIME
|
||||||
func (m *Msg) signMessage(msg *Msg) (*Msg, error) {
|
func (m *Msg) signMessage(msg *Msg) (*Msg, error) {
|
||||||
currentPart := m.GetParts()[0]
|
signedPart := msg.GetParts()[0]
|
||||||
currentPart.SetEncoding(EncodingUSASCII)
|
body, err := signedPart.GetContent()
|
||||||
currentPart.SetContentType(TypeTextPlain)
|
|
||||||
content, err := currentPart.GetContent()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.New("failed to extract content from part")
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
signedContent, err := m.sMime.Sign(content)
|
signaturePart, err := m.createSignaturePart(signedPart.GetEncoding(), signedPart.GetContentType(), signedPart.GetCharset(), body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.New("failed to sign message")
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
signedPart := msg.newPart(
|
m.parts = append(m.parts, signaturePart)
|
||||||
typeSMimeSigned,
|
|
||||||
WithPartEncoding(EncodingB64),
|
|
||||||
WithContentDisposition(DispositionSMime),
|
|
||||||
)
|
|
||||||
signedPart.SetContent(*signedContent)
|
|
||||||
msg.parts = append(msg.parts, signedPart)
|
|
||||||
|
|
||||||
return msg, nil
|
return m, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Msg) createSignaturePart(encoding Encoding, contentType ContentType, charSet Charset, body []byte) (*Part, error) {
|
||||||
|
message := m.sMime.createMessage(encoding, contentType, charSet, body)
|
||||||
|
signaturePart := m.newPart(typeSMimeSigned, WithPartEncoding(EncodingB64), WithSMimeSinging())
|
||||||
|
|
||||||
|
if err := m.sMime.sign(signaturePart, message); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return signaturePart, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteTo writes the formated Msg into a give io.Writer and satisfies the io.WriteTo interface
|
// WriteTo writes the formated Msg into a give io.Writer and satisfies the io.WriteTo interface
|
||||||
|
@ -1014,7 +1027,7 @@ func (m *Msg) WriteTo(writer io.Writer) (int64, error) {
|
||||||
mw := &msgWriter{writer: writer, charset: m.charset, encoder: m.encoder}
|
mw := &msgWriter{writer: writer, charset: m.charset, encoder: m.encoder}
|
||||||
msg := m.applyMiddlewares(m)
|
msg := m.applyMiddlewares(m)
|
||||||
|
|
||||||
if m.sMime != nil {
|
if m.hasSMime() {
|
||||||
signedMsg, err := m.signMessage(msg)
|
signedMsg, err := m.signMessage(msg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
|
@ -1210,7 +1223,7 @@ func (m *Msg) hasAlt() bool {
|
||||||
count++
|
count++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return count > 1 && m.pgptype == 0
|
return count > 1 && m.pgptype == 0 && !m.hasSMime()
|
||||||
}
|
}
|
||||||
|
|
||||||
// hasMixed returns true if the Msg has mixed parts
|
// hasMixed returns true if the Msg has mixed parts
|
||||||
|
|
|
@ -61,3 +61,25 @@ func TestMsg_WriteToSendmail(t *testing.T) {
|
||||||
t.Errorf("WriteToSendmail failed: %s", err)
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
154
msg_test.go
154
msg_test.go
|
@ -786,13 +786,11 @@ func TestMsg_SetMessageIDWithValue(t *testing.T) {
|
||||||
// TestMsg_SetMessageIDRandomness tests the randomness of Msg.SetMessageID methods
|
// TestMsg_SetMessageIDRandomness tests the randomness of Msg.SetMessageID methods
|
||||||
func TestMsg_SetMessageIDRandomness(t *testing.T) {
|
func TestMsg_SetMessageIDRandomness(t *testing.T) {
|
||||||
var mids []string
|
var mids []string
|
||||||
for i := 0; i < 100; i++ {
|
for i := 0; i < 50_000; i++ {
|
||||||
m := NewMsg()
|
m := NewMsg()
|
||||||
m.SetMessageID()
|
m.SetMessageID()
|
||||||
mid := m.GetGenHeader(HeaderMessageID)
|
mid := m.GetMessageID()
|
||||||
if len(mid) > 0 {
|
mids = append(mids, mid)
|
||||||
mids = append(mids, mid[0])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
c := make(map[string]int)
|
c := make(map[string]int)
|
||||||
for i := range mids {
|
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
|
// TestMsg_FromFormat tests the FromFormat and EnvelopeFrom methods for the Msg object
|
||||||
func TestMsg_FromFormat(t *testing.T) {
|
func TestMsg_FromFormat(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
|
@ -1894,6 +1907,23 @@ func TestMsg_hasAlt(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestMsg_hasAlt tests the hasAlt() method of the Msg with active S/MIME
|
||||||
|
func TestMsg_hasAltWithSMime(t *testing.T) {
|
||||||
|
keyPair, err := getDummyCertificate()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to load dummy certificate. Cause: %v", err)
|
||||||
|
}
|
||||||
|
m := NewMsg()
|
||||||
|
m.SetBodyString(TypeTextPlain, "Plain")
|
||||||
|
m.AddAlternativeString(TypeTextHTML, "<b>HTML</b>")
|
||||||
|
if err := m.SignWithSMime(keyPair); err != nil {
|
||||||
|
t.Errorf("set of certificate was not successful")
|
||||||
|
}
|
||||||
|
if m.hasAlt() {
|
||||||
|
t.Errorf("mail has alternative parts and S/MIME is active, but hasAlt() returned true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestMsg_hasRelated tests the hasRelated() method of the Msg
|
// TestMsg_hasRelated tests the hasRelated() method of the Msg
|
||||||
func TestMsg_hasRelated(t *testing.T) {
|
func TestMsg_hasRelated(t *testing.T) {
|
||||||
m := NewMsg()
|
m := NewMsg()
|
||||||
|
@ -3233,32 +3263,33 @@ func TestNewMsgWithNoDefaultUserAgent(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestWithSMimeSinging_ValidPrivateKey tests WithSMimeSinging with given privateKey
|
// TestSignWithSMime_ValidKeyPair tests WithSMimeSinging with given key pair
|
||||||
func TestWithSMimeSinging_ValidPrivateKey(t *testing.T) {
|
func TestSignWithSMime_ValidKeyPair(t *testing.T) {
|
||||||
privateKey, err := getDummyPrivateKey()
|
keyPair, err := getDummyCertificate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("failed to load dummy private key: %s", err)
|
t.Errorf("failed to load dummy certificate. Cause: %v", err)
|
||||||
}
|
}
|
||||||
certificate, err := getDummyCertificate(privateKey)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("failed to load dummy certificate: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
m := NewMsg()
|
m := NewMsg()
|
||||||
if err := m.SignWithSMime(privateKey, certificate); err != nil {
|
if err := m.SignWithSMime(keyPair); err != nil {
|
||||||
t.Errorf("failed to set sMime. Cause: %v", err)
|
t.Errorf("failed to set sMime. Cause: %v", err)
|
||||||
}
|
}
|
||||||
if m.sMime.privateKey != privateKey {
|
if m.sMime.privateKey == nil {
|
||||||
t.Errorf("WithSMimeSinging. Expected %v, got: %v", privateKey, m.sMime.privateKey)
|
t.Errorf("WithSMimeSinging() - no private key is given")
|
||||||
|
}
|
||||||
|
if m.sMime.certificate == nil {
|
||||||
|
t.Errorf("WithSMimeSinging() - no certificate is given")
|
||||||
|
}
|
||||||
|
if len(m.sMime.parentCertificates) != len(keyPair.Certificate[:1]) {
|
||||||
|
t.Errorf("WithSMimeSinging() - no certificate is given")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestWithSMimeSinging_InvalidPrivateKey tests WithSMimeSinging with given invalid privateKey
|
// TestSignWithSMime_InvalidKeyPair tests WithSMimeSinging with given invalid key pair
|
||||||
func TestWithSMimeSinging_InvalidPrivateKey(t *testing.T) {
|
func TestSignWithSMime_InvalidKeyPair(t *testing.T) {
|
||||||
m := NewMsg()
|
m := NewMsg()
|
||||||
|
|
||||||
err := m.SignWithSMime(nil, nil)
|
err := m.SignWithSMime(nil)
|
||||||
if !errors.Is(err, ErrInvalidPrivateKey) {
|
if !errors.Is(err, ErrInvalidKeyPair) {
|
||||||
t.Errorf("failed to check sMimeAuthConfig values correctly: %s", err)
|
t.Errorf("failed to check sMimeAuthConfig values correctly: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3290,3 +3321,84 @@ func FuzzMsg_From(f *testing.F) {
|
||||||
m.Reset()
|
m.Reset()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestMsg_createSignaturePart tests the Msg.createSignaturePart method
|
||||||
|
func TestMsg_createSignaturePart(t *testing.T) {
|
||||||
|
keyPair, err := getDummyCertificate()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to load dummy certificate. Cause: %v", err)
|
||||||
|
}
|
||||||
|
m := NewMsg()
|
||||||
|
if err := m.SignWithSMime(keyPair); err != nil {
|
||||||
|
t.Errorf("set of certificate was not successful")
|
||||||
|
}
|
||||||
|
body := []byte("This is the body")
|
||||||
|
part, err := m.createSignaturePart(EncodingQP, TypeTextPlain, CharsetUTF7, body)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("createSignaturePart() method failed.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if part.GetEncoding() != EncodingB64 {
|
||||||
|
t.Errorf("createSignaturePart() method failed. Expected encoding: %s, got: %s", EncodingB64, part.GetEncoding())
|
||||||
|
}
|
||||||
|
if part.GetContentType() != typeSMimeSigned {
|
||||||
|
t.Errorf("createSignaturePart() method failed. Expected content type: %s, got: %s", typeSMimeSigned, part.GetContentType())
|
||||||
|
}
|
||||||
|
if part.GetCharset() != CharsetUTF8 {
|
||||||
|
t.Errorf("createSignaturePart() method failed. Expected charset: %s, got: %s", CharsetUTF8, part.GetCharset())
|
||||||
|
}
|
||||||
|
if content, err := part.GetContent(); err != nil || len(content) == len(body) {
|
||||||
|
t.Errorf("createSignaturePart() method failed. Expected content should not be equal: %s, got: %s", body, part.GetEncoding())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestMsg_signMessage tests the Msg.signMessage method
|
||||||
|
func TestMsg_signMessage(t *testing.T) {
|
||||||
|
keyPair, err := getDummyCertificate()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to load dummy certificate. Cause: %v", err)
|
||||||
|
}
|
||||||
|
body := []byte("This is the body")
|
||||||
|
m := NewMsg()
|
||||||
|
m.SetBodyString(TypeTextPlain, string(body))
|
||||||
|
if err := m.SignWithSMime(keyPair); err != nil {
|
||||||
|
t.Errorf("set of certificate was not successful")
|
||||||
|
}
|
||||||
|
msg, err := m.signMessage(m)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("createSignaturePart() method failed.")
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := msg.GetParts()
|
||||||
|
if len(parts) != 2 {
|
||||||
|
t.Errorf("createSignaturePart() method failed. Expected 2 parts, got: %d", len(parts))
|
||||||
|
}
|
||||||
|
|
||||||
|
signedPart := parts[0]
|
||||||
|
if signedPart.GetEncoding() != EncodingQP {
|
||||||
|
t.Errorf("createSignaturePart() method failed. Expected encoding: %s, got: %s", EncodingB64, signedPart.GetEncoding())
|
||||||
|
}
|
||||||
|
if signedPart.GetContentType() != TypeTextPlain {
|
||||||
|
t.Errorf("createSignaturePart() method failed. Expected content type: %s, got: %s", typeSMimeSigned, signedPart.GetContentType())
|
||||||
|
}
|
||||||
|
if signedPart.GetCharset() != CharsetUTF8 {
|
||||||
|
t.Errorf("createSignaturePart() method failed. Expected charset: %s, got: %s", CharsetUTF8, signedPart.GetCharset())
|
||||||
|
}
|
||||||
|
if content, err := signedPart.GetContent(); err != nil || len(content) != len(body) {
|
||||||
|
t.Errorf("createSignaturePart() method failed. Expected content should be equal: %s, got: %s", body, content)
|
||||||
|
}
|
||||||
|
|
||||||
|
signaturePart := parts[1]
|
||||||
|
if signaturePart.GetEncoding() != EncodingB64 {
|
||||||
|
t.Errorf("createSignaturePart() method failed. Expected encoding: %s, got: %s", EncodingB64, signaturePart.GetEncoding())
|
||||||
|
}
|
||||||
|
if signaturePart.GetContentType() != typeSMimeSigned {
|
||||||
|
t.Errorf("createSignaturePart() method failed. Expected content type: %s, got: %s", typeSMimeSigned, signaturePart.GetContentType())
|
||||||
|
}
|
||||||
|
if signaturePart.GetCharset() != CharsetUTF8 {
|
||||||
|
t.Errorf("createSignaturePart() method failed. Expected charset: %s, got: %s", CharsetUTF8, signaturePart.GetCharset())
|
||||||
|
}
|
||||||
|
if content, err := signaturePart.GetContent(); err != nil || len(content) == len(body) {
|
||||||
|
t.Errorf("createSignaturePart() method failed. Expected content should not be equal: %s, got: %s", body, signaturePart.GetEncoding())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
17
msgwriter.go
17
msgwriter.go
|
@ -100,7 +100,7 @@ func (mw *msgWriter) writeMsg(msg *Msg) {
|
||||||
mw.startMP(MIMERelated, msg.boundary)
|
mw.startMP(MIMERelated, msg.boundary)
|
||||||
mw.writeString(DoubleNewLine)
|
mw.writeString(DoubleNewLine)
|
||||||
}
|
}
|
||||||
if msg.hasAlt() && !msg.hasSMime() {
|
if msg.hasAlt() {
|
||||||
mw.startMP(MIMEAlternative, msg.boundary)
|
mw.startMP(MIMEAlternative, msg.boundary)
|
||||||
mw.writeString(DoubleNewLine)
|
mw.writeString(DoubleNewLine)
|
||||||
}
|
}
|
||||||
|
@ -241,7 +241,7 @@ func (mw *msgWriter) addFiles(files []*File, isAttachment bool) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if mw.err == nil {
|
if mw.err == nil {
|
||||||
mw.writeBody(file.Writer, encoding)
|
mw.writeBody(file.Writer, encoding, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -273,7 +273,7 @@ func (mw *msgWriter) writePart(part *Part, charset Charset) {
|
||||||
mimeHeader.Add(string(HeaderContentTransferEnc), contentTransferEnc)
|
mimeHeader.Add(string(HeaderContentTransferEnc), contentTransferEnc)
|
||||||
mw.newPart(mimeHeader)
|
mw.newPart(mimeHeader)
|
||||||
}
|
}
|
||||||
mw.writeBody(part.writeFunc, part.encoding)
|
mw.writeBody(part.writeFunc, part.encoding, part.smime)
|
||||||
}
|
}
|
||||||
|
|
||||||
// writeString writes a string into the msgWriter's io.Writer interface
|
// writeString writes a string into the msgWriter's io.Writer interface
|
||||||
|
@ -322,7 +322,7 @@ func (mw *msgWriter) writeHeader(key Header, values ...string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// writeBody writes an io.Reader into an io.Writer using provided Encoding
|
// writeBody writes an io.Reader into an io.Writer using provided Encoding
|
||||||
func (mw *msgWriter) writeBody(writeFunc func(io.Writer) (int64, error), encoding Encoding) {
|
func (mw *msgWriter) writeBody(writeFunc func(io.Writer) (int64, error), encoding Encoding, singingWithSMime bool) {
|
||||||
var writer io.Writer
|
var writer io.Writer
|
||||||
var encodedWriter io.WriteCloser
|
var encodedWriter io.WriteCloser
|
||||||
var n int64
|
var n int64
|
||||||
|
@ -337,12 +337,11 @@ func (mw *msgWriter) writeBody(writeFunc func(io.Writer) (int64, error), encodin
|
||||||
lineBreaker := Base64LineBreaker{}
|
lineBreaker := Base64LineBreaker{}
|
||||||
lineBreaker.out = &writeBuffer
|
lineBreaker.out = &writeBuffer
|
||||||
|
|
||||||
switch encoding {
|
if encoding == EncodingQP {
|
||||||
case EncodingQP:
|
|
||||||
encodedWriter = quotedprintable.NewWriter(&writeBuffer)
|
encodedWriter = quotedprintable.NewWriter(&writeBuffer)
|
||||||
case EncodingB64:
|
} else if encoding == EncodingB64 && !singingWithSMime {
|
||||||
encodedWriter = base64.NewEncoder(base64.StdEncoding, &lineBreaker)
|
encodedWriter = base64.NewEncoder(base64.StdEncoding, &lineBreaker)
|
||||||
case NoEncoding:
|
} else if encoding == NoEncoding {
|
||||||
_, err = writeFunc(&writeBuffer)
|
_, err = writeFunc(&writeBuffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mw.err = fmt.Errorf("bodyWriter function: %w", err)
|
mw.err = fmt.Errorf("bodyWriter function: %w", err)
|
||||||
|
@ -355,7 +354,7 @@ func (mw *msgWriter) writeBody(writeFunc func(io.Writer) (int64, error), encodin
|
||||||
mw.bytesWritten += n
|
mw.bytesWritten += n
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
default:
|
} else {
|
||||||
encodedWriter = quotedprintable.NewWriter(writer)
|
encodedWriter = quotedprintable.NewWriter(writer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -154,3 +154,26 @@ func TestMsgWriter_writeMsg_PGP(t *testing.T) {
|
||||||
t.Errorf("writeMsg failed. Expected PGP encoding header but didn't find it in message output")
|
t.Errorf("writeMsg failed. Expected PGP encoding header but didn't find it in message output")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestMsgWriter_writeMsg_SMime tests the writeMsg method of the msgWriter with S/MIME types set
|
||||||
|
func TestMsgWriter_writeMsg_SMime(t *testing.T) {
|
||||||
|
keyPair, err := getDummyCertificate()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to load dummy certificate. Cause: %v", err)
|
||||||
|
}
|
||||||
|
m := NewMsg()
|
||||||
|
if err := m.SignWithSMime(keyPair); err != nil {
|
||||||
|
t.Errorf("set of certificate was not successful")
|
||||||
|
}
|
||||||
|
_ = m.From(`"Toni Tester" <test@example.com>`)
|
||||||
|
_ = m.To(`"Toni Receiver" <receiver@example.com>`)
|
||||||
|
m.Subject("This is a subject")
|
||||||
|
m.SetBodyString(TypeTextPlain, "This is the body")
|
||||||
|
buf := bytes.Buffer{}
|
||||||
|
mw := &msgWriter{writer: &buf, charset: CharsetUTF8, encoder: mime.QEncoding}
|
||||||
|
mw.writeMsg(m)
|
||||||
|
ms := buf.String()
|
||||||
|
if !strings.Contains(ms, `multipart/signed; protocol="application/pkcs7-signature"; micalg=sha256;`) {
|
||||||
|
t.Errorf("writeMsg failed. Expected PGP encoding header but didn't find it in message output")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -275,6 +275,7 @@ func TestPart_IsSMimeSigned(t *testing.T) {
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
m := NewMsg()
|
m := NewMsg()
|
||||||
|
m.SetBodyString(TypeTextPlain, "This is a body!")
|
||||||
pl, err := getPartList(m)
|
pl, err := getPartList(m)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("failed: %s", err)
|
t.Errorf("failed: %s", err)
|
||||||
|
|
22
random.go
22
random.go
|
@ -7,8 +7,6 @@ package mail
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"fmt"
|
|
||||||
"math/big"
|
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -52,23 +50,3 @@ func randomStringSecure(length int) (string, error) {
|
||||||
|
|
||||||
return randString.String(), nil
|
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.19 && !go1.20
|
||||||
|
// +build go1.19,!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 {
|
for _, tc := range tt {
|
||||||
t.Run(tc.testName, func(t *testing.T) {
|
t.Run(tc.testName, func(t *testing.T) {
|
||||||
rn, err := randNum(tc.max)
|
rn := 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)
|
|
||||||
}
|
|
||||||
if rn > tc.max {
|
if rn > tc.max {
|
||||||
t.Errorf("random number generation failed: %d is bigger than given value %d", 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
|
// SendError is an error wrapper for delivery errors of the Msg
|
||||||
type SendError struct {
|
type SendError struct {
|
||||||
Reason SendErrReason
|
affectedMsg *Msg
|
||||||
isTemp bool
|
|
||||||
errlist []error
|
errlist []error
|
||||||
|
isTemp bool
|
||||||
rcpt []string
|
rcpt []string
|
||||||
|
Reason SendErrReason
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendErrReason represents a comparable reason on why the delivery failed
|
// 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()
|
return errMessage.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,6 +118,23 @@ func (e *SendError) IsTemp() bool {
|
||||||
return e.isTemp
|
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
|
// String implements the Stringer interface for the SendErrReason
|
||||||
func (r SendErrReason) String() string {
|
func (r SendErrReason) String() string {
|
||||||
switch r {
|
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
|
// returnSendError is a helper method to retunr a SendError with a specific reason
|
||||||
func returnSendError(r SendErrReason, t bool) error {
|
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}
|
||||||
}
|
}
|
||||||
|
|
137
sime.go
137
sime.go
|
@ -1,18 +1,20 @@
|
||||||
package mail
|
package mail
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"go.mozilla.org/pkcs7"
|
"go.mozilla.org/pkcs7"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// ErrInvalidPrivateKey should be used if private key is invalid
|
// ErrInvalidKeyPair should be used if key pair is invalid
|
||||||
ErrInvalidPrivateKey = errors.New("invalid private key")
|
ErrInvalidKeyPair = errors.New("invalid key pair")
|
||||||
|
|
||||||
// ErrInvalidCertificate should be used if certificate is invalid
|
|
||||||
ErrInvalidCertificate = errors.New("invalid certificate")
|
|
||||||
|
|
||||||
// ErrCouldNotInitialize should be used if the signed data could not initialize
|
// ErrCouldNotInitialize should be used if the signed data could not initialize
|
||||||
ErrCouldNotInitialize = errors.New("could not initialize signed data")
|
ErrCouldNotInitialize = errors.New("could not initialize signed data")
|
||||||
|
@ -22,49 +24,136 @@ var (
|
||||||
|
|
||||||
// ErrCouldNotFinishSigning should be used if the signing could not be finished
|
// ErrCouldNotFinishSigning should be used if the signing could not be finished
|
||||||
ErrCouldNotFinishSigning = errors.New("could not finish signing")
|
ErrCouldNotFinishSigning = errors.New("could not finish signing")
|
||||||
|
|
||||||
|
// ErrCouldNoEncodeToPEM should be used if the signature could not be encoded to PEM
|
||||||
|
ErrCouldNoEncodeToPEM = errors.New("could not encode to PEM")
|
||||||
)
|
)
|
||||||
|
|
||||||
// SMime is used to sign messages with S/MIME
|
// SMime is used to sign messages with S/MIME
|
||||||
type SMime struct {
|
type SMime struct {
|
||||||
privateKey *rsa.PrivateKey
|
privateKey *rsa.PrivateKey
|
||||||
certificate *x509.Certificate
|
certificate *x509.Certificate
|
||||||
|
parentCertificates []*x509.Certificate
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewSMime construct a new instance of SMime with a provided *rsa.PrivateKey
|
// NewSMime construct a new instance of SMime with a provided *tls.Certificate
|
||||||
func NewSMime(privateKey *rsa.PrivateKey, certificate *x509.Certificate) (*SMime, error) {
|
func newSMime(keyPair *tls.Certificate) (*SMime, error) {
|
||||||
if privateKey == nil {
|
if keyPair == nil {
|
||||||
return nil, ErrInvalidPrivateKey
|
return nil, ErrInvalidKeyPair
|
||||||
}
|
}
|
||||||
|
|
||||||
if certificate == nil {
|
parentCertificates := make([]*x509.Certificate, 0)
|
||||||
return nil, ErrInvalidCertificate
|
for _, cert := range keyPair.Certificate[1:] {
|
||||||
|
c, err := x509.ParseCertificate(cert)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
parentCertificates = append(parentCertificates, c)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &SMime{
|
return &SMime{
|
||||||
privateKey: privateKey,
|
privateKey: keyPair.PrivateKey.(*rsa.PrivateKey),
|
||||||
certificate: certificate,
|
certificate: keyPair.Leaf,
|
||||||
|
parentCertificates: parentCertificates,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sign the content with the given privateKey of the method NewSMime
|
// sign with the S/MIME method the message of the actual *Part
|
||||||
func (sm *SMime) Sign(content []byte) (*string, error) {
|
func (sm *SMime) sign(signaturePart *Part, message string) error {
|
||||||
toBeSigned, err := pkcs7.NewSignedData(content)
|
lines := parseLines([]byte(message))
|
||||||
|
toBeSigned := lines.bytesFromLines([]byte("\r\n"))
|
||||||
|
|
||||||
toBeSigned.SetDigestAlgorithm(pkcs7.OIDDigestAlgorithmSHA256)
|
tmp, err := pkcs7.NewSignedData(toBeSigned)
|
||||||
|
tmp.SetDigestAlgorithm(pkcs7.OIDDigestAlgorithmSHA256)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, ErrCouldNotInitialize
|
return ErrCouldNotInitialize
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = toBeSigned.AddSigner(sm.certificate, sm.privateKey, pkcs7.SignerInfoConfig{}); err != nil {
|
if err = tmp.AddSignerChain(sm.certificate, sm.privateKey, sm.parentCertificates, pkcs7.SignerInfoConfig{}); err != nil {
|
||||||
return nil, ErrCouldNotAddSigner
|
return ErrCouldNotAddSigner
|
||||||
}
|
}
|
||||||
|
|
||||||
signed, err := toBeSigned.Finish()
|
signatureDER, err := tmp.Finish()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, ErrCouldNotFinishSigning
|
return ErrCouldNotFinishSigning
|
||||||
}
|
}
|
||||||
|
|
||||||
signedData := string(signed)
|
pemMsg, err := encodeToPEM(signatureDER)
|
||||||
|
if err != nil {
|
||||||
|
return ErrCouldNoEncodeToPEM
|
||||||
|
}
|
||||||
|
signaturePart.SetContent(*pemMsg)
|
||||||
|
|
||||||
return &signedData, nil
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// createMessage prepares the message that will be used for the sign method later
|
||||||
|
func (sm *SMime) createMessage(encoding Encoding, contentType ContentType, charset Charset, body []byte) string {
|
||||||
|
return fmt.Sprintf("Content-Transfer-Encoding: %v\r\nContent-Type: %v; charset=%v\r\n\r\n%v", encoding, contentType, charset, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
// encodeToPEM uses the method pem.Encode from the standard library but cuts the typical PEM preamble
|
||||||
|
func encodeToPEM(msg []byte) (*string, error) {
|
||||||
|
block := &pem.Block{Bytes: msg}
|
||||||
|
|
||||||
|
var arrayBuffer bytes.Buffer
|
||||||
|
if err := pem.Encode(&arrayBuffer, block); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
r := arrayBuffer.String()
|
||||||
|
r = strings.ReplaceAll(r, "-----BEGIN -----\n", "")
|
||||||
|
r = strings.ReplaceAll(r, "-----END -----\n", "")
|
||||||
|
|
||||||
|
return &r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// line is the representation of one line of the message that will be used for signing purposes
|
||||||
|
type line struct {
|
||||||
|
line []byte
|
||||||
|
endOfLine []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// lines is the representation of a message that will be used for signing purposes
|
||||||
|
type lines []line
|
||||||
|
|
||||||
|
// bytesFromLines creates the line representation with the given endOfLine char
|
||||||
|
func (ls lines) bytesFromLines(sep []byte) []byte {
|
||||||
|
var raw []byte
|
||||||
|
for i := range ls {
|
||||||
|
raw = append(raw, ls[i].line...)
|
||||||
|
if len(ls[i].endOfLine) != 0 && sep != nil {
|
||||||
|
raw = append(raw, sep...)
|
||||||
|
} else {
|
||||||
|
raw = append(raw, ls[i].endOfLine...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return raw
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseLines constructs the lines representation of a given message
|
||||||
|
func parseLines(raw []byte) lines {
|
||||||
|
oneLine := line{raw, nil}
|
||||||
|
lines := lines{oneLine}
|
||||||
|
lines = lines.splitLine([]byte("\r\n"))
|
||||||
|
lines = lines.splitLine([]byte("\r"))
|
||||||
|
lines = lines.splitLine([]byte("\n"))
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
|
||||||
|
// splitLine uses the given endOfLine to split the given line
|
||||||
|
func (ls lines) splitLine(sep []byte) lines {
|
||||||
|
nl := lines{}
|
||||||
|
for _, l := range ls {
|
||||||
|
split := bytes.Split(l.line, sep)
|
||||||
|
if len(split) > 1 {
|
||||||
|
for i := 0; i < len(split)-1; i++ {
|
||||||
|
nl = append(nl, line{split[i], sep})
|
||||||
|
}
|
||||||
|
nl = append(nl, line{split[len(split)-1], l.endOfLine})
|
||||||
|
} else {
|
||||||
|
nl = append(nl, l)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nl
|
||||||
}
|
}
|
||||||
|
|
43
util_test.go
43
util_test.go
|
@ -1,41 +1,22 @@
|
||||||
package mail
|
package mail
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"crypto/tls"
|
||||||
"crypto/rsa"
|
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"crypto/x509/pkix"
|
|
||||||
"math/big"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func getDummyPrivateKey() (*rsa.PrivateKey, error) {
|
const (
|
||||||
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
certFilePath = "dummy-chain-cert.pem"
|
||||||
|
keyFilePath = "dummy-child-key.pem"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getDummyCertificate() (*tls.Certificate, error) {
|
||||||
|
keyPair, err := tls.LoadX509KeyPair(certFilePath, keyFilePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return privateKey, nil
|
|
||||||
}
|
keyPair.Leaf, err = x509.ParseCertificate(keyPair.Certificate[0])
|
||||||
|
|
||||||
func getDummyCertificate(privateKey *rsa.PrivateKey) (*x509.Certificate, error) {
|
return &keyPair, nil
|
||||||
template := &x509.Certificate{
|
|
||||||
SerialNumber: big.NewInt(1234),
|
|
||||||
Subject: pkix.Name{Organization: []string{"My Organization"}},
|
|
||||||
NotBefore: time.Now(),
|
|
||||||
NotAfter: time.Now().AddDate(1, 0, 0),
|
|
||||||
KeyUsage: x509.KeyUsageDigitalSignature,
|
|
||||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
|
||||||
}
|
|
||||||
|
|
||||||
certDER, err := x509.CreateCertificate(rand.Reader, template, template, &privateKey.PublicKey, privateKey)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
cert, err := x509.ParseCertificate(certDER)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return cert, nil
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue