diff --git a/.cirrus.yml b/.cirrus.yml index 48c69f6..f240ff5 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -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 diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index 622ab66..41a6c27 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -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 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 9bee2c3..caab6ba 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -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 diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 4d6ba02..13ad6e7 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -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 diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 2eeeeba..8604469 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -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 diff --git a/.github/workflows/govulncheck.yml b/.github/workflows/govulncheck.yml index 121dcd8..8b1693d 100644 --- a/.github/workflows/govulncheck.yml +++ b/.github/workflows/govulncheck.yml @@ -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 diff --git a/.github/workflows/reuse.yml b/.github/workflows/reuse.yml index 657df03..1897833 100644 --- a/.github/workflows/reuse.yml +++ b/.github/workflows/reuse.yml @@ -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 diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 4c3e0b8..2e0f045 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -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 diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml index 279ecd1..1c77858 100644 --- a/.github/workflows/sonarqube.yml +++ b/.github/workflows/sonarqube.yml @@ -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 }} diff --git a/.golangci.toml b/.golangci.toml index 2501c6a..223dc0b 100644 --- a/.golangci.toml +++ b/.golangci.toml @@ -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", diff --git a/b64linebreaker.go b/b64linebreaker.go index f5fb967..088b38e 100644 --- a/b64linebreaker.go +++ b/b64linebreaker.go @@ -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 { diff --git a/client.go b/client.go index 1ca4814..1d175d1 100644 --- a/client.go +++ b/client.go @@ -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 } diff --git a/doc.go b/doc.go index 5f41bc9..831a57c 100644 --- a/doc.go +++ b/doc.go @@ -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" diff --git a/eml.go b/eml.go index ed20d0d..7e705f6 100644 --- a/eml.go +++ b/eml.go @@ -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) diff --git a/eml_test.go b/eml_test.go index 8823eec..44bfb54 100644 --- a/eml_test.go +++ b/eml_test.go @@ -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 +To: Mary Smith +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" +To: +Cc: +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: 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" -To: -Cc: - 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" +To: +Cc: +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" +To: +Cc: +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)) diff --git a/encoding.go b/encoding.go index 2187e5f..47213da 100644 --- a/encoding.go +++ b/encoding.go @@ -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" ) diff --git a/encoding_test.go b/encoding_test.go index 86f686a..14711b7 100644 --- a/encoding_test.go +++ b/encoding_test.go @@ -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) { diff --git a/msg.go b/msg.go index e018492..a909d04 100644 --- a/msg.go +++ b/msg.go @@ -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 { diff --git a/msg_test.go b/msg_test.go index b2f0920..16cd196 100644 --- a/msg_test.go +++ b/msg_test.go @@ -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)) diff --git a/msgwriter_test.go b/msgwriter_test.go index e6582fb..a41e5d3 100644 --- a/msgwriter_test.go +++ b/msgwriter_test.go @@ -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) } } diff --git a/smtp/auth_login.go b/smtp/auth_login.go index 7cd5c5d..aa80223 100644 --- a/smtp/auth_login.go +++ b/smtp/auth_login.go @@ -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))