Merge branch 'main' into feature/168_improve-error-handling

This commit is contained in:
Winni Neessen 2024-09-06 11:15:50 +02:00
commit a747f5f74c
Signed by: wneessen
GPG key ID: 385AC9889632126E
21 changed files with 248 additions and 82 deletions

View file

@ -6,9 +6,9 @@ freebsd_task:
name: FreeBSD
matrix:
- name: FreeBSD 13.2
- name: FreeBSD 13.3
freebsd_instance:
image_family: freebsd-13-2
image_family: freebsd-13-3
- name: FreeBSD 14.0
freebsd_instance:
image_family: freebsd-14-0

View file

@ -36,28 +36,28 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
go: [1.18, 1.19, '1.20', '1.21', '1.22']
go: ['1.20', '1.21', '1.22', '1.23']
steps:
- name: Harden Runner
uses: step-security/harden-runner@17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6 # v2.8.1
uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
with:
egress-policy: audit
- name: Checkout Code
uses: actions/checkout@61b9e3751b92087fd0b06925ba6dd6314e06f089 # master
- name: Setup go
uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1
uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2
with:
go-version: ${{ matrix.go }}
- name: Install sendmail
if: matrix.go == '1.22' && matrix.os == 'ubuntu-latest'
if: matrix.go == '1.23' && matrix.os == 'ubuntu-latest'
run: |
sudo apt-get -y install sendmail; which sendmail
- name: Run Tests
run: |
go test -v -race --coverprofile=coverage.coverprofile --covermode=atomic ./...
- name: Upload coverage to Codecov
if: success() && matrix.go == '1.22' && matrix.os == 'ubuntu-latest'
if: success() && matrix.go == '1.23' && matrix.os == 'ubuntu-latest'
uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4.5.0
with:
token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos

View file

@ -45,7 +45,7 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6 # v2.8.1
uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
with:
egress-policy: audit
@ -54,7 +54,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@b611370bb5703a7efb587f9d136a52ea24c5c38c # v3.25.11
uses: github/codeql-action/init@4dd16135b69a43b6c8efb853346f8437d92d3c93 # v3.26.6
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@ -65,7 +65,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@b611370bb5703a7efb587f9d136a52ea24c5c38c # v3.25.11
uses: github/codeql-action/autobuild@4dd16135b69a43b6c8efb853346f8437d92d3c93 # v3.26.6
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@ -79,4 +79,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@b611370bb5703a7efb587f9d136a52ea24c5c38c # v3.25.11
uses: github/codeql-action/analyze@4dd16135b69a43b6c8efb853346f8437d92d3c93 # v3.26.6

View file

@ -21,11 +21,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6 # v2.8.1
uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
with:
egress-policy: audit
- name: 'Checkout Repository'
uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
- name: 'Dependency Review'
uses: actions/dependency-review-action@72eb03d02c7872a771aacd928f3123ac62ad6d3a # v4.3.3
uses: actions/dependency-review-action@5a2ce3f5b92ee19cbb1541a4984c76d921601d7c # v4.3.4

View file

@ -20,16 +20,16 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6 # v2.8.1
uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
with:
egress-policy: audit
- uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1
- uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2
with:
go-version: '1.22'
go-version: '1.23'
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
- name: golangci-lint
uses: golangci/golangci-lint-action@a4f60bb28d35aeee14e6880718e0c85ff1882e64 # v6.0.1
uses: golangci/golangci-lint-action@aaa42aa0628b4ae2578232a66b541047968fac86 # v6.1.0
with:
# Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version
version: latest

View file

@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6 # v2.8.1
uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
with:
egress-policy: audit
- name: Run govulncheck

View file

@ -14,10 +14,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6 # v2.8.1
uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
with:
egress-policy: audit
- uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2.7.0
- name: REUSE Compliance Check
uses: fsfe/reuse-action@a46482ca367aef4454a87620aa37c2be4b2f8106 # v3.0.0
uses: fsfe/reuse-action@3ae3c6bdf1257ab19397fab11fd3312144692083 # v4.0.0

View file

@ -35,7 +35,7 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6 # v2.8.1
uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
with:
egress-policy: audit
@ -45,7 +45,7 @@ jobs:
persist-credentials: false
- name: "Run analysis"
uses: ossf/scorecard-action@dc50aa9510b46c811795eb24b2f1ba02a914e534 # v2.3.3
uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # v2.4.0
with:
results_file: results.sarif
results_format: sarif
@ -67,7 +67,7 @@ jobs:
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
# format to the repository Actions tab.
- name: "Upload artifact"
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: SARIF file
path: results.sarif
@ -75,6 +75,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@b611370bb5703a7efb587f9d136a52ea24c5c38c # v3.25.11
uses: github/codeql-action/upload-sarif@4dd16135b69a43b6c8efb853346f8437d92d3c93 # v3.26.6
with:
sarif_file: results.sarif

View file

@ -27,7 +27,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6 # v2.8.1
uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
with:
egress-policy: audit
@ -36,20 +36,20 @@ jobs:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1
uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2
with:
go-version: '1.22.x'
go-version: '1.23.x'
- name: Run unit Tests
run: |
go test -v -race --coverprofile=./cov.out ./...
- uses: sonarsource/sonarqube-scan-action@540792c588b5c2740ad2bb4667db5cd46ae678f2 # master
- uses: sonarsource/sonarqube-scan-action@0c0f3958d90fc466625f1d1af1f47bddd4cc6bd1 # master
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
- uses: sonarsource/sonarqube-quality-gate-action@72f24ebf1f81eda168a979ce14b8203273b7c3ad # master
- uses: sonarsource/sonarqube-quality-gate-action@dc2f7b0dd95544cd550de3028f89193576e958b9 # master
timeout-minutes: 5
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

View file

@ -3,8 +3,9 @@
## SPDX-License-Identifier: MIT
[run]
go = "1.22"
go = "1.23"
tests = true
exclude-dirs = ["examples"]
[linters]
enable = ["stylecheck", "whitespace", "containedctx", "contextcheck", "decorder",

View file

@ -5,7 +5,7 @@
package mail
import (
"fmt"
"errors"
"io"
)
@ -26,7 +26,7 @@ var newlineBytes = []byte(SingleNewLine)
// line length is reached
func (l *Base64LineBreaker) Write(data []byte) (numBytes int, err error) {
if l.out == nil {
err = fmt.Errorf(ErrNoOutWriter)
err = errors.New(ErrNoOutWriter)
return
}
if l.used+len(data) < MaxBodyLength {

View file

@ -683,7 +683,7 @@ func (c *Client) DialAndSendWithContext(ctx context.Context, messages ...*Msg) e
return fmt.Errorf("send failed: %w", err)
}
if err := c.Close(); err != nil {
return fmt.Errorf("failed to close connction: %w", err)
return fmt.Errorf("failed to close connection: %w", err)
}
return nil
}

2
doc.go
View file

@ -6,4 +6,4 @@
package mail
// VERSION is used in the default user agent string
const VERSION = "0.4.2"
const VERSION = "0.4.4"

28
eml.go
View file

@ -180,7 +180,15 @@ func parseEMLBodyParts(parsedMsg *netmail.Message, bodybuf *bytes.Buffer, msg *M
// Extract the transfer encoding of the body
mediatype, params, err := mime.ParseMediaType(parsedMsg.Header.Get(HeaderContentType.String()))
if err != nil {
return fmt.Errorf("failed to extract content type: %w", err)
switch {
// If no Content-Type header is found, we assume that this is a plain text, 7bit, US-ASCII mail
case strings.EqualFold(err.Error(), "mime: no media type"):
mediatype = TypeTextPlain.String()
params = make(map[string]string)
params["charset"] = CharsetASCII.String()
default:
return fmt.Errorf("failed to extract content type: %w", err)
}
}
if value, ok := params["charset"]; ok {
msg.SetCharset(Charset(value))
@ -207,6 +215,12 @@ func parseEMLBodyParts(parsedMsg *netmail.Message, bodybuf *bytes.Buffer, msg *M
// parseEMLBodyPlain parses the mail body of plain type mails
func parseEMLBodyPlain(mediatype string, parsedMsg *netmail.Message, bodybuf *bytes.Buffer, msg *Msg) error {
contentTransferEnc := parsedMsg.Header.Get(HeaderContentTransferEnc.String())
// According to RFC2045, if no Content-Transfer-Encoding is set, we can imply 7bit US-ASCII encoding
if contentTransferEnc == "" || strings.EqualFold(contentTransferEnc, EncodingUSASCII.String()) {
msg.SetEncoding(EncodingUSASCII)
msg.SetBodyString(ContentType(mediatype), bodybuf.String())
return nil
}
if strings.EqualFold(contentTransferEnc, NoEncoding.String()) {
msg.SetEncoding(NoEncoding)
msg.SetBodyString(ContentType(mediatype), bodybuf.String())
@ -308,14 +322,22 @@ ReadNextPart:
}
switch {
case strings.EqualFold(mutliPartTransferEnc[0], EncodingUSASCII.String()):
part.SetEncoding(EncodingUSASCII)
part.SetContent(string(multiPartData))
case strings.EqualFold(mutliPartTransferEnc[0], NoEncoding.String()):
part.SetEncoding(NoEncoding)
part.SetContent(string(multiPartData))
case strings.EqualFold(mutliPartTransferEnc[0], EncodingB64.String()):
if err := handleEMLMultiPartBase64Encoding(multiPartData, part); err != nil {
part.SetEncoding(EncodingB64)
if err = handleEMLMultiPartBase64Encoding(multiPartData, part); err != nil {
return fmt.Errorf("failed to handle multipart base64 transfer-encoding: %w", err)
}
case strings.EqualFold(mutliPartTransferEnc[0], EncodingQP.String()):
part.SetEncoding(EncodingQP)
part.SetContent(string(multiPartData))
default:
return fmt.Errorf("unsupported Content-Transfer-Encoding")
return fmt.Errorf("unsupported Content-Transfer-Encoding: %s", mutliPartTransferEnc[0])
}
msg.parts = append(msg.parts, part)

View file

@ -14,6 +14,16 @@ import (
)
const (
// RFC 5322 example mail
// See: https://datatracker.ietf.org/doc/html/rfc5322#appendix-A.1.1
exampleMailRFC5322A11 = `From: John Doe <jdoe@machine.example>
To: Mary Smith <mary@example.net>
Subject: Saying Hello
Date: Fri, 21 Nov 1997 09:55:06 -0600
Message-ID: <1234@local.machine.example>
This is a message just to say hello.
So, "Hello".`
exampleMailPlainNoEnc = `Date: Wed, 01 Nov 2023 00:00:00 +0000
MIME-Version: 1.0
Message-ID: <1305604950.683004066175.AAAAAAAAaaaaaaaaB@go-mail.dev>
@ -32,6 +42,29 @@ This is a test mail. Please do not reply to this. Also this line is very long so
should be wrapped.
Thank your for your business!
The go-mail team
--
This is a signature`
exampleMailPlain7Bit = `Date: Wed, 01 Nov 2023 00:00:00 +0000
MIME-Version: 1.0
Message-ID: <1305604950.683004066175.AAAAAAAAaaaaaaaaB@go-mail.dev>
Subject: Example mail // plain text without encoding
User-Agent: go-mail v0.4.0 // https://github.com/wneessen/go-mail
X-Mailer: go-mail v0.4.0 // https://github.com/wneessen/go-mail
From: "Toni Tester" <go-mail@go-mail.dev>
To: <go-mail+test@go-mail.dev>
Cc: <go-mail+cc@go-mail.dev>
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 7bit
Dear Customer,
This is a test mail. Please do not reply to this. Also this line is very long so it
should be wrapped.
Thank your for your business!
The go-mail team
@ -49,18 +82,6 @@ Cc: <go-mail+cc@go-mail.dev>
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: base64
This plain text body should not be parsed as Base64.
`
exampleMailPlainNoContentType = `Date: Wed, 01 Nov 2023 00:00:00 +0000
MIME-Version: 1.0
Message-ID: <1305604950.683004066175.AAAAAAAAaaaaaaaaB@go-mail.dev>
Subject: Example mail // plain text without encoding
User-Agent: go-mail v0.4.0 // https://github.com/wneessen/go-mail
X-Mailer: go-mail v0.4.0 // https://github.com/wneessen/go-mail
From: "Toni Tester" <go-mail@go-mail.dev>
To: <go-mail+test@go-mail.dev>
Cc: <go-mail+cc@go-mail.dev>
This plain text body should not be parsed as Base64.
`
exampleMailPlainUnknownContentType = `Date: Wed, 01 Nov 2023 00:00:00 +0000
@ -525,6 +546,72 @@ hw22iFHl7YlpOmedZvtMTfQffXeXnvI+rTKNxguyvDKvB7U4qQAAAAlwSFlzAAALEwAACxMBAJqc
GAAAABFJREFUCJljnMoAA0wMNGcCAEQrAKk9oHKhAAAAAElFTkSuQmCC
--fe785e0384e2607697cc2ecb17cce003003bb7ca9112104f3e8ce727edb5--`
exampleMultiPart7BitBase64 = `Date: Wed, 01 Nov 2023 00:00:00 +0000
MIME-Version: 1.0
Message-ID: <1305604950.683004066175.AAAAAAAAaaaaaaaaB@go-mail.dev>
Subject: Example mail // 7bit with base64 attachment
User-Agent: go-mail v0.4.1 // https://github.com/wneessen/go-mail
X-Mailer: go-mail v0.4.1 // https://github.com/wneessen/go-mail
From: "Toni Tester" <go-mail@go-mail.dev>
To: <go-mail+test@go-mail.dev>
Cc: <go-mail+cc@go-mail.dev>
Content-Type: multipart/mixed;
boundary="------------26A45336F6C6196BD8BBA2A2"
This is a multi-part message in MIME format.
--------------26A45336F6C6196BD8BBA2A2
Content-Type: text/plain; charset=US-ASCII; format=flowed
Content-Transfer-Encoding: 7bit
testtest
testtest
testtest
testtest
testtest
testtest
--------------26A45336F6C6196BD8BBA2A2
Content-Type: text/plain; charset=UTF-8;
name="testfile.txt"
Content-Transfer-Encoding: base64
Content-Disposition: attachment;
filename="testfile.txt"
VGhpcyBpcyBhIHRlc3QgaW4gQmFzZTY0
--------------26A45336F6C6196BD8BBA2A2--`
exampleMultiPart8BitBase64 = `Date: Wed, 01 Nov 2023 00:00:00 +0000
MIME-Version: 1.0
Message-ID: <1305604950.683004066175.AAAAAAAAaaaaaaaaB@go-mail.dev>
Subject: Example mail // 8bit with base64 attachment
User-Agent: go-mail v0.4.1 // https://github.com/wneessen/go-mail
X-Mailer: go-mail v0.4.1 // https://github.com/wneessen/go-mail
From: "Toni Tester" <go-mail@go-mail.dev>
To: <go-mail+test@go-mail.dev>
Cc: <go-mail+cc@go-mail.dev>
Content-Type: multipart/mixed;
boundary="------------26A45336F6C6196BD8BBA2A2"
This is a multi-part message in MIME format.
--------------26A45336F6C6196BD8BBA2A2
Content-Type: text/plain; charset=US-ASCII; format=flowed
Content-Transfer-Encoding: 8bit
testtest
testtest
testtest
testtest
testtest
testtest
--------------26A45336F6C6196BD8BBA2A2
Content-Type: text/plain; charset=UTF-8;
name="testfile.txt"
Content-Transfer-Encoding: base64
Content-Disposition: attachment;
filename="testfile.txt"
VGhpcyBpcyBhIHRlc3QgaW4gQmFzZTY0
--------------26A45336F6C6196BD8BBA2A2--`
)
func TestEMLToMsgFromString(t *testing.T) {
@ -534,6 +621,14 @@ func TestEMLToMsgFromString(t *testing.T) {
enc string
sub string
}{
{
"RFC5322 A1.1", exampleMailRFC5322A11, "7bit",
"Saying Hello",
},
{
"Plain text no encoding (7bit)", exampleMailPlain7Bit, "7bit",
"Example mail // plain text without encoding",
},
{
"Plain text no encoding", exampleMailPlainNoEnc, "8bit",
"Example mail // plain text without encoding",
@ -638,12 +733,6 @@ func TestEMLToMsgFromReaderFailing(t *testing.T) {
t.Error("EML from Reader with unknown content type was supposed to fail, but didn't")
}
mailbuf.Reset()
mailbuf.WriteString(exampleMailPlainNoContentType)
_, err = EMLToMsgFromReader(mailbuf)
if err == nil {
t.Error("EML from Reader with no content type was supposed to fail, but didn't")
}
mailbuf.Reset()
mailbuf.WriteString(exampleMailPlainUnsupportedTransferEnc)
_, err = EMLToMsgFromReader(mailbuf)
if err == nil {
@ -707,17 +796,6 @@ func TestEMLToMsgFromFileFailing(t *testing.T) {
if err = os.RemoveAll(tempDir); err != nil {
t.Error("failed to remove temp dir:", err)
}
tempDir, tempFile, err = stringToTempFile(exampleMailPlainNoContentType, "testmail")
if err != nil {
t.Errorf("failed to write EML string to temp file: %s", err)
}
_, err = EMLToMsgFromFile(tempFile)
if err == nil {
t.Error("EML from Reader with no content type was supposed to fail, but didn't")
}
if err = os.RemoveAll(tempDir); err != nil {
t.Error("failed to remove temp dir:", err)
}
tempDir, tempFile, err = stringToTempFile(exampleMailPlainUnsupportedTransferEnc, "testmail")
if err != nil {
t.Errorf("failed to write EML string to temp file: %s", err)
@ -866,6 +944,58 @@ func TestEMLToMsgFromStringMultipartMixedAlternativeRelated(t *testing.T) {
}
}
func TestEMLToMsgFromStringMultipartMixedWith7Bit(t *testing.T) {
wantSubject := "Example mail // 7bit with base64 attachment"
msg, err := EMLToMsgFromString(exampleMultiPart7BitBase64)
if err != nil {
t.Errorf("EML multipart mixed with 7bit: %s", err)
}
if subject := msg.GetGenHeader(HeaderSubject); len(subject) > 0 && !strings.EqualFold(subject[0], wantSubject) {
t.Errorf("EMLToMsgFromString of EML multipart mixed with 7bit: expected subject: %s,"+
" but got: %s", wantSubject, subject[0])
}
if len(msg.parts) != 1 {
t.Errorf("EMLToMsgFromString of EML multipart mixed with 7bit failed: expected 1 part, got: %d",
len(msg.parts))
return
}
if !strings.EqualFold(msg.parts[0].GetEncoding().String(), EncodingUSASCII.String()) {
t.Errorf("EMLToMsgFromString of EML multipart mixed with 7bit failed: expected encoding: %s, got %s",
EncodingUSASCII.String(), msg.parts[0].GetEncoding().String())
}
if len(msg.attachments) != 1 {
t.Errorf("EMLToMsgFromString of EML multipart mixed with 7bit failed: expected 1 attachment, got: %d",
len(msg.attachments))
return
}
}
func TestEMLToMsgFromStringMultipartMixedWith8Bit(t *testing.T) {
wantSubject := "Example mail // 8bit with base64 attachment"
msg, err := EMLToMsgFromString(exampleMultiPart8BitBase64)
if err != nil {
t.Errorf("EML multipart mixed with 8bit: %s", err)
}
if subject := msg.GetGenHeader(HeaderSubject); len(subject) > 0 && !strings.EqualFold(subject[0], wantSubject) {
t.Errorf("EMLToMsgFromString of EML multipart mixed with 8bit: expected subject: %s,"+
" but got: %s", wantSubject, subject[0])
}
if len(msg.parts) != 1 {
t.Errorf("EMLToMsgFromString of EML multipart mixed with 8bit failed: expected 1 part, got: %d",
len(msg.parts))
return
}
if !strings.EqualFold(msg.parts[0].GetEncoding().String(), NoEncoding.String()) {
t.Errorf("EMLToMsgFromString of EML multipart mixed with 8bit failed: expected encoding: %s, got %s",
NoEncoding.String(), msg.parts[0].GetEncoding().String())
}
if len(msg.attachments) != 1 {
t.Errorf("EMLToMsgFromString of EML multipart mixed with 8bit failed: expected 1 attachment, got: %d",
len(msg.attachments))
return
}
}
// stringToTempFile is a helper method that will create a temporary file form a give data string
func stringToTempFile(data, name string) (string, string, error) {
tempDir, err := os.MkdirTemp("", fmt.Sprintf("*-%s", name))

View file

@ -27,6 +27,9 @@ const (
// EncodingQP represents the "quoted-printable" encoding as specified in RFC 2045.
EncodingQP Encoding = "quoted-printable"
// EncodingUSASCII represents encoding with only US-ASCII characters (aka 7Bit)
EncodingUSASCII Encoding = "7bit"
// NoEncoding avoids any character encoding (except of the mail headers)
NoEncoding Encoding = "8bit"
)

View file

@ -16,6 +16,7 @@ func TestEncoding_String(t *testing.T) {
{"Encoding: Base64", EncodingB64, "base64"},
{"Encoding: QP", EncodingQP, "quoted-printable"},
{"Encoding: None/8bit", NoEncoding, "8bit"},
{"Encoding: US-ASCII/7bit", EncodingUSASCII, "7bit"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

23
msg.go
View file

@ -684,11 +684,18 @@ func (m *Msg) GetBoundary() string {
return m.boundary
}
// SetAttachements sets the attachements of the message.
func (m *Msg) SetAttachements(files []*File) {
// SetAttachments sets the attachments of the message.
func (m *Msg) SetAttachments(files []*File) {
m.attachments = files
}
// SetAttachements sets the attachments of the message.
//
// Deprecated: use SetAttachments instead.
func (m *Msg) SetAttachements(files []*File) {
m.SetAttachments(files)
}
// UnsetAllAttachments unset the attachments of the message.
func (m *Msg) UnsetAllAttachments() {
m.attachments = nil
@ -736,7 +743,7 @@ func (m *Msg) SetBodyWriter(
// The content type will be set to text/html automatically
func (m *Msg) SetBodyHTMLTemplate(tpl *ht.Template, data interface{}, opts ...PartOption) error {
if tpl == nil {
return fmt.Errorf(errTplPointerNil)
return errors.New(errTplPointerNil)
}
buffer := bytes.Buffer{}
if err := tpl.Execute(&buffer, data); err != nil {
@ -751,7 +758,7 @@ func (m *Msg) SetBodyHTMLTemplate(tpl *ht.Template, data interface{}, opts ...Pa
// The content type will be set to text/plain automatically
func (m *Msg) SetBodyTextTemplate(tpl *tt.Template, data interface{}, opts ...PartOption) error {
if tpl == nil {
return fmt.Errorf(errTplPointerNil)
return errors.New(errTplPointerNil)
}
buf := bytes.Buffer{}
if err := tpl.Execute(&buf, data); err != nil {
@ -783,7 +790,7 @@ func (m *Msg) AddAlternativeWriter(
// The content type will be set to text/html automatically
func (m *Msg) AddAlternativeHTMLTemplate(tpl *ht.Template, data interface{}, opts ...PartOption) error {
if tpl == nil {
return fmt.Errorf(errTplPointerNil)
return errors.New(errTplPointerNil)
}
buffer := bytes.Buffer{}
if err := tpl.Execute(&buffer, data); err != nil {
@ -798,7 +805,7 @@ func (m *Msg) AddAlternativeHTMLTemplate(tpl *ht.Template, data interface{}, opt
// The content type will be set to text/plain automatically
func (m *Msg) AddAlternativeTextTemplate(tpl *tt.Template, data interface{}, opts ...PartOption) error {
if tpl == nil {
return fmt.Errorf(errTplPointerNil)
return errors.New(errTplPointerNil)
}
buffer := bytes.Buffer{}
if err := tpl.Execute(&buffer, data); err != nil {
@ -1307,7 +1314,7 @@ func fileFromReadSeeker(name string, reader io.ReadSeeker) *File {
// fileFromHTMLTemplate returns a File pointer form a given html/template.Template
func fileFromHTMLTemplate(name string, tpl *ht.Template, data interface{}) (*File, error) {
if tpl == nil {
return nil, fmt.Errorf(errTplPointerNil)
return nil, errors.New(errTplPointerNil)
}
buffer := bytes.Buffer{}
if err := tpl.Execute(&buffer, data); err != nil {
@ -1319,7 +1326,7 @@ func fileFromHTMLTemplate(name string, tpl *ht.Template, data interface{}) (*Fil
// fileFromTextTemplate returns a File pointer form a given text/template.Template
func fileFromTextTemplate(name string, tpl *tt.Template, data interface{}) (*File, error) {
if tpl == nil {
return nil, fmt.Errorf(errTplPointerNil)
return nil, errors.New(errTplPointerNil)
}
buffer := bytes.Buffer{}
if err := tpl.Execute(&buffer, data); err != nil {

View file

@ -1413,7 +1413,7 @@ func TestMsg_SetAttachments(t *testing.T) {
for _, f := range tt.files {
files = append(files, &File{Name: f})
}
m.SetAttachements(files)
m.SetAttachments(files)
if len(m.attachments) != len(files) {
t.Errorf("SetAttachements() failed. Number of attachments expected: %d, got: %d", len(files),
len(m.attachments))
@ -1448,7 +1448,7 @@ func TestMsg_UnsetAllAttachments(t *testing.T) {
for _, f := range tt.attachments {
files = append(files, &File{Name: f})
}
m.SetAttachements(files)
m.SetAttachments(files)
if len(m.attachments) != len(files) {
t.Errorf("SetAttachements() failed. Number of attachments expected: %d, got: %d", len(files),
@ -1610,7 +1610,7 @@ func TestMsg_UnsetAllParts(t *testing.T) {
for _, f := range tt.attachments {
attachments = append(attachments, &File{Name: f})
}
m.SetAttachements(attachments)
m.SetAttachments(attachments)
if len(m.attachments) != len(attachments) {
t.Errorf("SetAttachements() failed. Number of attachments files expected: %d, got: %d",
len(attachments), len(m.attachments))

View file

@ -122,7 +122,7 @@ func TestMsgWriter_writeMsg(t *testing.T) {
em += fmt.Sprintf("* incorrect %q field", ea[e])
}
em += fmt.Sprintf("\n\nFull message:\n%s", ms)
t.Errorf(em)
t.Error(em)
}
}

View file

@ -20,13 +20,15 @@ const (
// extension.
//
// See: https://learn.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-xlogin/.
LoginXUsernameChallenge = "Username:"
LoginXUsernameChallenge = "Username:"
LoginXUsernameLowerChallenge = "username:"
// LoginXPasswordChallenge represents the Password Challenge response sent by the SMTP server per the AUTH LOGIN
// extension.
//
// See: https://learn.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-xlogin/.
LoginXPasswordChallenge = "Password:"
LoginXPasswordChallenge = "Password:"
LoginXPasswordLowerChallenge = "password:"
// LoginXDraftUsernameChallenge represents the Username Challenge response sent by the SMTP server per the IETF
// draft AUTH LOGIN extension. It should be noted this extension is an expired draft which was never formally
@ -76,9 +78,9 @@ func (a *loginAuth) Start(server *ServerInfo) (string, []byte, error) {
func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
if more {
switch string(fromServer) {
case LoginXUsernameChallenge, LoginXDraftUsernameChallenge:
case LoginXUsernameChallenge, LoginXUsernameLowerChallenge, LoginXDraftUsernameChallenge:
return []byte(a.username), nil
case LoginXPasswordChallenge, LoginXDraftPasswordChallenge:
case LoginXPasswordChallenge, LoginXPasswordLowerChallenge, LoginXDraftPasswordChallenge:
return []byte(a.password), nil
default:
return nil, fmt.Errorf("unexpected server response: %s", string(fromServer))