mirror of
https://github.com/wneessen/go-mail.git
synced 2024-11-22 13:50:49 +01:00
Compare commits
No commits in common. "e56a5632866d8c2627169804a79876a0c926ae0c" and "faffc025cb8f1d37051ba28ce99a82e7d0f1b7f4" have entirely different histories.
e56a563286
...
faffc025cb
53 changed files with 7069 additions and 13308 deletions
23
.cirrus.yml
Normal file
23
.cirrus.yml
Normal file
|
@ -0,0 +1,23 @@
|
|||
# SPDX-FileCopyrightText: 2022 Winni Neessen <winni@neessen.dev>
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
freebsd_task:
|
||||
name: FreeBSD
|
||||
|
||||
matrix:
|
||||
- name: FreeBSD 13.3
|
||||
freebsd_instance:
|
||||
image_family: freebsd-13-3
|
||||
- name: FreeBSD 14.0
|
||||
freebsd_instance:
|
||||
image_family: freebsd-14-0
|
||||
|
||||
env:
|
||||
TEST_SKIP_SENDMAIL: 1
|
||||
|
||||
pkginstall_script:
|
||||
- pkg install -y go
|
||||
|
||||
test_script:
|
||||
- go test -race -cover -shuffle=on ./...
|
221
.github/workflows/ci.yml
vendored
221
.github/workflows/ci.yml
vendored
|
@ -1,221 +0,0 @@
|
|||
# SPDX-FileCopyrightText: 2024 The go-mail Authors
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
name: CI
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref_name }}
|
||||
|
||||
jobs:
|
||||
codecov:
|
||||
name: Test with Codecov coverage (${{ matrix.os }} / ${{ matrix.go }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
concurrency:
|
||||
group: ci-codecov-${{ matrix.os }}-${{ matrix.go }}
|
||||
cancel-in-progress: true
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
go: ['1.23']
|
||||
env:
|
||||
PERFORM_ONLINE_TEST: ${{ vars.PERFORM_ONLINE_TEST }}
|
||||
PERFORM_UNIX_OPEN_WRITE_TESTS: ${{ vars.PERFORM_UNIX_OPEN_WRITE_TESTS }}
|
||||
PERFORM_SENDMAIL_TESTS: ${{ vars.PERFORM_SENDMAIL_TESTS }}
|
||||
TEST_HOST: ${{ secrets.TEST_HOST }}
|
||||
TEST_USER: ${{ secrets.TEST_USER }}
|
||||
TEST_PASS: ${{ secrets.TEST_PASS }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@61b9e3751b92087fd0b06925ba6dd6314e06f089 # master
|
||||
- name: Setup go
|
||||
uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
check-latest: true
|
||||
- name: Install sendmail
|
||||
run: |
|
||||
sudo apt-get -y update >/dev/null && sudo apt-get -y upgrade >/dev/null && sudo DEBIAN_FRONTEND=noninteractive apt-get -y install nullmailer >/dev/null && which sendmail
|
||||
- name: Run go test
|
||||
if: success()
|
||||
run: |
|
||||
go test -race -shuffle=on --coverprofile=coverage.coverprofile --covermode=atomic ./...
|
||||
- name: Upload coverage to Codecov
|
||||
if: success()
|
||||
uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4.6.0
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos
|
||||
lint:
|
||||
name: golangci-lint (${{ matrix.go }})
|
||||
runs-on: ubuntu-latest
|
||||
concurrency:
|
||||
group: ci-lint-${{ matrix.go }}
|
||||
cancel-in-progress: true
|
||||
strategy:
|
||||
matrix:
|
||||
go: ['1.23']
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: Setup go
|
||||
uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
check-latest: true
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@61b9e3751b92087fd0b06925ba6dd6314e06f089 # master
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@971e284b6050e8a5849b72094c50ab08da042db8 # v6.1.1
|
||||
with:
|
||||
version: latest
|
||||
dependency-review:
|
||||
name: Dependency review
|
||||
runs-on: ubuntu-latest
|
||||
concurrency:
|
||||
group: ci-dependency-review
|
||||
cancel-in-progress: true
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@61b9e3751b92087fd0b06925ba6dd6314e06f089 # master
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@4081bf99e2866ebe428fc0477b69eb4fcda7220a # v4.4.0
|
||||
with:
|
||||
base-ref: ${{ github.event.pull_request.base.sha || 'main' }}
|
||||
head-ref: ${{ github.event.pull_request.head.sha || github.ref }}
|
||||
govulncheck:
|
||||
name: Go vulnerabilities check
|
||||
runs-on: ubuntu-latest
|
||||
concurrency:
|
||||
group: ci-govulncheck
|
||||
cancel-in-progress: true
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: Run govulncheck
|
||||
uses: golang/govulncheck-action@b625fbe08f3bccbe446d94fbf87fcc875a4f50ee # v1.0.4
|
||||
test:
|
||||
name: Test (${{ matrix.os }} / ${{ matrix.go }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
concurrency:
|
||||
group: ci-test-${{ matrix.os }}-${{ matrix.go }}
|
||||
cancel-in-progress: true
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
go: ['1.19', '1.20', '1.21', '1.22', '1.23']
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@61b9e3751b92087fd0b06925ba6dd6314e06f089 # master
|
||||
- name: Setup go
|
||||
uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
- name: Run go test
|
||||
run: |
|
||||
go test -race -shuffle=on ./...
|
||||
test-fbsd:
|
||||
name: Test on FreeBSD ${{ matrix.osver }}
|
||||
runs-on: ubuntu-latest
|
||||
concurrency:
|
||||
group: ci-test-freebsd-${{ matrix.osver }}
|
||||
cancel-in-progress: true
|
||||
strategy:
|
||||
matrix:
|
||||
osver: ['14.1', '14.0', 13.4']
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@61b9e3751b92087fd0b06925ba6dd6314e06f089 # master
|
||||
- name: Run go test on FreeBSD
|
||||
uses: vmactions/freebsd-vm@v1
|
||||
with:
|
||||
usesh: true
|
||||
copyback: false
|
||||
prepare: |
|
||||
pkg install -y go
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE;
|
||||
go test -race -shuffle=on ./...
|
||||
reuse:
|
||||
name: REUSE Compliance Check
|
||||
runs-on: ubuntu-latest
|
||||
concurrency:
|
||||
group: ci-reuse
|
||||
cancel-in-progress: true
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@61b9e3751b92087fd0b06925ba6dd6314e06f089 # master
|
||||
- name: REUSE Compliance Check
|
||||
uses: fsfe/reuse-action@3ae3c6bdf1257ab19397fab11fd3312144692083 # v4.0.0
|
||||
sonarqube:
|
||||
name: Test with SonarQube review (${{ matrix.os }} / ${{ matrix.go }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
concurrency:
|
||||
group: ci-codecov-${{ matrix.go }}
|
||||
cancel-in-progress: true
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
go: ['1.23']
|
||||
env:
|
||||
PERFORM_ONLINE_TEST: ${{ vars.PERFORM_ONLINE_TEST }}
|
||||
TEST_HOST: ${{ secrets.TEST_HOST }}
|
||||
TEST_USER: ${{ secrets.TEST_USER }}
|
||||
TEST_PASS: ${{ secrets.TEST_PASS }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@61b9e3751b92087fd0b06925ba6dd6314e06f089 # master
|
||||
- name: Setup go
|
||||
uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
check-latest: true
|
||||
- name: Run go test
|
||||
run: |
|
||||
go test -shuffle=on -race --coverprofile=./cov.out ./...
|
||||
- name: SonarQube scan
|
||||
uses: sonarsource/sonarqube-scan-action@884b79409bbd464b2a59edc326a4b77dc56b2195 # master
|
||||
if: success()
|
||||
env:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
|
||||
- name: SonarQube quality gate
|
||||
uses: sonarsource/sonarqube-quality-gate-action@8406f4f1edaffef38e9fb9c53eb292fc1d7684fa # master
|
||||
timeout-minutes: 5
|
||||
env:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
|
67
.github/workflows/codecov.yml
vendored
Normal file
67
.github/workflows/codecov.yml
vendored
Normal file
|
@ -0,0 +1,67 @@
|
|||
# SPDX-FileCopyrightText: 2022 Winni Neessen <winni@neessen.dev>
|
||||
#
|
||||
# SPDX-License-Identifier: CC0-1.0
|
||||
|
||||
name: Codecov workflow
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- '**.go'
|
||||
- 'go.*'
|
||||
- '.github/workflows/codecov.yml'
|
||||
- 'codecov.yml'
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- '**.go'
|
||||
- 'go.*'
|
||||
- '.github/workflows/codecov.yml'
|
||||
- 'codecov.yml'
|
||||
env:
|
||||
TEST_HOST: ${{ secrets.TEST_HOST }}
|
||||
TEST_FROM: ${{ secrets.TEST_USER }}
|
||||
TEST_ALLOW_SEND: "1"
|
||||
TEST_SMTPAUTH_USER: ${{ secrets.TEST_USER }}
|
||||
TEST_SMTPAUTH_PASS: ${{ secrets.TEST_PASS }}
|
||||
TEST_SMTPAUTH_TYPE: "LOGIN"
|
||||
TEST_ONLINE_SCRAM: "1"
|
||||
TEST_HOST_SCRAM: ${{ secrets.TEST_HOST_SCRAM }}
|
||||
TEST_USER_SCRAM: ${{ secrets.TEST_USER_SCRAM }}
|
||||
TEST_PASS_SCRAM: ${{ secrets.TEST_PASS_SCRAM }}
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
run:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
go: ['1.23']
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@61b9e3751b92087fd0b06925ba6dd6314e06f089 # master
|
||||
- name: Setup go
|
||||
uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
- name: Install sendmail
|
||||
if: matrix.go == '1.23' && matrix.os == 'ubuntu-latest'
|
||||
run: |
|
||||
sudo apt-get -y install sendmail; which sendmail
|
||||
- name: Run Tests
|
||||
run: |
|
||||
go test -race --coverprofile=coverage.coverprofile --covermode=atomic ./...
|
||||
- name: Upload coverage to Codecov
|
||||
if: success() && matrix.go == '1.23' && matrix.os == 'ubuntu-latest'
|
||||
uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4.6.0
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos
|
6
.github/workflows/codeql-analysis.yml
vendored
6
.github/workflows/codeql-analysis.yml
vendored
|
@ -54,7 +54,7 @@ jobs:
|
|||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
|
||||
uses: github/codeql-action/init@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.26.12
|
||||
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@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
|
||||
uses: github/codeql-action/autobuild@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.26.12
|
||||
|
||||
# ℹ️ 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@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
|
||||
uses: github/codeql-action/analyze@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.26.12
|
||||
|
|
31
.github/workflows/dependency-review.yml
vendored
Normal file
31
.github/workflows/dependency-review.yml
vendored
Normal file
|
@ -0,0 +1,31 @@
|
|||
# SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
|
||||
#
|
||||
# SPDX-License-Identifier: CC0-1.0
|
||||
|
||||
# Dependency Review Action
|
||||
#
|
||||
# This Action will scan dependency manifest files that change as part of a Pull Request,
|
||||
# surfacing known-vulnerable versions of the packages declared or updated in the PR.
|
||||
# Once installed, if the workflow run is marked as required,
|
||||
# PRs introducing known-vulnerable packages will be blocked from merging.
|
||||
#
|
||||
# Source repository: https://github.com/actions/dependency-review-action
|
||||
name: 'Dependency Review'
|
||||
on: [pull_request]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
dependency-review:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@5a2ce3f5b92ee19cbb1541a4984c76d921601d7c # v4.3.4
|
54
.github/workflows/golangci-lint.yml
vendored
Normal file
54
.github/workflows/golangci-lint.yml
vendored
Normal file
|
@ -0,0 +1,54 @@
|
|||
# SPDX-FileCopyrightText: 2022 Winni Neessen <winni@neessen.dev>
|
||||
#
|
||||
# SPDX-License-Identifier: CC0-1.0
|
||||
|
||||
name: golangci-lint
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
permissions:
|
||||
contents: read
|
||||
# Optional: allow read access to pull request. Use with `only-new-issues` option.
|
||||
# pull-requests: read
|
||||
jobs:
|
||||
golangci:
|
||||
name: lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2
|
||||
with:
|
||||
go-version: '1.23'
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@971e284b6050e8a5849b72094c50ab08da042db8 # v6.1.1
|
||||
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
|
||||
|
||||
# Optional: working directory, useful for monorepos
|
||||
# working-directory: somedir
|
||||
|
||||
# Optional: golangci-lint command line arguments.
|
||||
# args: --issues-exit-code=0
|
||||
|
||||
# Optional: show only new issues if it's a pull request. The default value is `false`.
|
||||
# only-new-issues: true
|
||||
|
||||
# Optional: if set to true then the all caching functionality will be complete disabled,
|
||||
# takes precedence over all other caching options.
|
||||
# skip-cache: true
|
||||
|
||||
# Optional: if set to true then the action don't cache or restore ~/go/pkg.
|
||||
# skip-pkg-cache: true
|
||||
|
||||
# Optional: if set to true then the action don't cache or restore ~/.cache/go-build.
|
||||
# skip-build-cache: true
|
21
.github/workflows/govulncheck.yml
vendored
Normal file
21
.github/workflows/govulncheck.yml
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
# SPDX-FileCopyrightText: 2022 Winni Neessen <winni@neessen.dev>
|
||||
#
|
||||
# SPDX-License-Identifier: CC0-1.0
|
||||
|
||||
name: Govulncheck Security Scan
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: Run govulncheck
|
||||
uses: golang/govulncheck-action@b625fbe08f3bccbe446d94fbf87fcc875a4f50ee # v1.0.4
|
45
.github/workflows/offline-tests.yml
vendored
Normal file
45
.github/workflows/offline-tests.yml
vendored
Normal file
|
@ -0,0 +1,45 @@
|
|||
# SPDX-FileCopyrightText: 2022 Winni Neessen <winni@neessen.dev>
|
||||
#
|
||||
# SPDX-License-Identifier: CC0-1.0
|
||||
|
||||
name: Offline tests workflow
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- '**.go'
|
||||
- 'go.*'
|
||||
- '.github/workflows/offline-tests.yml'
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- '**.go'
|
||||
- 'go.*'
|
||||
- '.github/workflows/offline-tests.yml'
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
run:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
go: ['1.19', '1.20', '1.21', '1.22', '1.23']
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@61b9e3751b92087fd0b06925ba6dd6314e06f089 # master
|
||||
- name: Setup go
|
||||
uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
- name: Run Tests
|
||||
run: |
|
||||
go test -race -shuffle=on ./...
|
23
.github/workflows/reuse.yml
vendored
Normal file
23
.github/workflows/reuse.yml
vendored
Normal file
|
@ -0,0 +1,23 @@
|
|||
# SPDX-FileCopyrightText: 2022 Winni Neessen <winni@neessen.dev>
|
||||
#
|
||||
# SPDX-License-Identifier: CC0-1.0
|
||||
|
||||
name: REUSE Compliance Check
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2.7.0
|
||||
- name: REUSE Compliance Check
|
||||
uses: fsfe/reuse-action@3ae3c6bdf1257ab19397fab11fd3312144692083 # v4.0.0
|
4
.github/workflows/scorecards.yml
vendored
4
.github/workflows/scorecards.yml
vendored
|
@ -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@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
uses: actions/upload-artifact@604373da6381bf24206979c74d06a550515601b9 # v4.4.1
|
||||
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@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
|
||||
uses: github/codeql-action/upload-sarif@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.26.12
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
|
56
.github/workflows/sonarqube.yml
vendored
Normal file
56
.github/workflows/sonarqube.yml
vendored
Normal file
|
@ -0,0 +1,56 @@
|
|||
# SPDX-FileCopyrightText: 2022 Winni Neessen <winni@neessen.dev>
|
||||
#
|
||||
# SPDX-License-Identifier: CC0-1.0
|
||||
|
||||
name: SonarQube
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- '**.go'
|
||||
- 'go.*'
|
||||
- '.github/workflows/sonarqube.yml'
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- '**.go'
|
||||
- 'go.*'
|
||||
- '.github/workflows/sonarqube.yml'
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2.7.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2
|
||||
with:
|
||||
go-version: '1.23'
|
||||
|
||||
- name: Run unit Tests
|
||||
run: |
|
||||
go test -shuffle=on -race --coverprofile=./cov.out ./...
|
||||
|
||||
- uses: sonarsource/sonarqube-scan-action@884b79409bbd464b2a59edc326a4b77dc56b2195 # master
|
||||
env:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
|
||||
|
||||
- uses: sonarsource/sonarqube-quality-gate-action@dc2f7b0dd95544cd550de3028f89193576e958b9 # master
|
||||
timeout-minutes: 5
|
||||
env:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -56,6 +56,4 @@ crashlytics.properties
|
|||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
## Coverage data
|
||||
coverage.coverprofile
|
||||
coverage.html
|
||||
testdata
|
83
auth.go
83
auth.go
|
@ -4,11 +4,7 @@
|
|||
|
||||
package mail
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
import "errors"
|
||||
|
||||
// SMTPAuthType is a type wrapper for a string type. It represents the type of SMTP authentication
|
||||
// mechanism to be used.
|
||||
|
@ -39,7 +35,7 @@ const (
|
|||
// IETF draft. The IETF draft is more lax than the MS spec, therefore we follow the I-D, which
|
||||
// automatically matches the MS spec.
|
||||
//
|
||||
// Since the "LOGIN" SASL authentication mechanism transmits the username and password in
|
||||
// Since the "LOGIN" SASL authentication mechansim transmits the username and password in
|
||||
// plaintext over the internet connection, we only allow this mechanism over a TLS secured
|
||||
// connection.
|
||||
//
|
||||
|
@ -48,53 +44,20 @@ const (
|
|||
// https://datatracker.ietf.org/doc/html/draft-murchison-sasl-login-00
|
||||
SMTPAuthLogin SMTPAuthType = "LOGIN"
|
||||
|
||||
// SMTPAuthLoginNoEnc is the "LOGIN" SASL authentication mechanism. This authentication mechanism
|
||||
// does not have an official RFC that could be followed. There is a spec by Microsoft and an
|
||||
// IETF draft. The IETF draft is more lax than the MS spec, therefore we follow the I-D, which
|
||||
// automatically matches the MS spec.
|
||||
//
|
||||
// Since the "LOGIN" SASL authentication mechanism transmits the username and password in
|
||||
// plaintext over the internet connection, by default we only allow this mechanism over
|
||||
// a TLS secured connection. This authentiation mechanism overrides this default and will
|
||||
// allow LOGIN authentication via an unencrypted channel. This can be useful if the
|
||||
// connection has already been secured in a different way (e. g. a SSH tunnel)
|
||||
//
|
||||
// Note: Use this authentication method with caution. If used in the wrong way, you might
|
||||
// expose your authentication information over unencrypted channels!
|
||||
//
|
||||
// https://msopenspecs.azureedge.net/files/MS-XLOGIN/%5bMS-XLOGIN%5d.pdf
|
||||
//
|
||||
// https://datatracker.ietf.org/doc/html/draft-murchison-sasl-login-00
|
||||
SMTPAuthLoginNoEnc SMTPAuthType = "LOGIN-NOENC"
|
||||
|
||||
// SMTPAuthNoAuth is equivalent to performing no authentication at all. It is a convenience
|
||||
// option and should not be used. Instead, for mail servers that do no support/require
|
||||
// authentication, the Client should not be passed the WithSMTPAuth option at all.
|
||||
SMTPAuthNoAuth SMTPAuthType = "NOAUTH"
|
||||
SMTPAuthNoAuth SMTPAuthType = ""
|
||||
|
||||
// SMTPAuthPlain is the "PLAIN" authentication mechanism as described in RFC 4616.
|
||||
//
|
||||
// Since the "PLAIN" SASL authentication mechanism transmits the username and password in
|
||||
// Since the "PLAIN" SASL authentication mechansim transmits the username and password in
|
||||
// plaintext over the internet connection, we only allow this mechanism over a TLS secured
|
||||
// connection.
|
||||
//
|
||||
// https://datatracker.ietf.org/doc/html/rfc4616/
|
||||
SMTPAuthPlain SMTPAuthType = "PLAIN"
|
||||
|
||||
// SMTPAuthPlainNoEnc is the "PLAIN" authentication mechanism as described in RFC 4616.
|
||||
//
|
||||
// Since the "PLAIN" SASL authentication mechanism transmits the username and password in
|
||||
// plaintext over the internet connection, by default we only allow this mechanism over
|
||||
// a TLS secured connection. This authentiation mechanism overrides this default and will
|
||||
// allow PLAIN authentication via an unencrypted channel. This can be useful if the
|
||||
// connection has already been secured in a different way (e. g. a SSH tunnel)
|
||||
//
|
||||
// Note: Use this authentication method with caution. If used in the wrong way, you might
|
||||
// expose your authentication information over unencrypted channels!
|
||||
//
|
||||
// https://datatracker.ietf.org/doc/html/rfc4616/
|
||||
SMTPAuthPlainNoEnc SMTPAuthType = "PLAIN-NOENC"
|
||||
|
||||
// SMTPAuthXOAUTH2 is the "XOAUTH2" SASL authentication mechanism.
|
||||
// https://developers.google.com/gmail/imap/xoauth2-protocol
|
||||
SMTPAuthXOAUTH2 SMTPAuthType = "XOAUTH2"
|
||||
|
@ -113,7 +76,7 @@ const (
|
|||
//
|
||||
// SCRAM-SHA-X-PLUS authentication require TLS channel bindings to protect against MitM attacks and
|
||||
// to guarantee that the integrity of the transport layer is preserved throughout the authentication
|
||||
// process. Therefore we only allow this mechanism over a TLS secured connection.
|
||||
// process. Therefore we only allow this mechansim over a TLS secured connection.
|
||||
//
|
||||
// SCRAM-SHA-1-PLUS is still considered secure for certain applications, particularly when used as part
|
||||
// of a challenge-response authentication mechanism (as we use it). However, it is generally
|
||||
|
@ -132,7 +95,7 @@ const (
|
|||
//
|
||||
// SCRAM-SHA-X-PLUS authentication require TLS channel bindings to protect against MitM attacks and
|
||||
// to guarantee that the integrity of the transport layer is preserved throughout the authentication
|
||||
// process. Therefore we only allow this mechanism over a TLS secured connection.
|
||||
// process. Therefore we only allow this mechansim over a TLS secured connection.
|
||||
//
|
||||
// https://datatracker.ietf.org/doc/html/rfc7677
|
||||
SMTPAuthSCRAMSHA256PLUS SMTPAuthType = "SCRAM-SHA-256-PLUS"
|
||||
|
@ -171,37 +134,3 @@ var (
|
|||
// authentication type.
|
||||
ErrSCRAMSHA256PLUSAuthNotSupported = errors.New("server does not support SMTP AUTH type: SCRAM-SHA-256-PLUS")
|
||||
)
|
||||
|
||||
// UnmarshalString satisfies the fig.StringUnmarshaler interface for the SMTPAuthType type
|
||||
// https://pkg.go.dev/github.com/kkyr/fig#StringUnmarshaler
|
||||
func (sa *SMTPAuthType) UnmarshalString(value string) error {
|
||||
switch strings.ToLower(value) {
|
||||
case "cram-md5", "crammd5", "cram":
|
||||
*sa = SMTPAuthCramMD5
|
||||
case "custom":
|
||||
*sa = SMTPAuthCustom
|
||||
case "login":
|
||||
*sa = SMTPAuthLogin
|
||||
case "login-noenc":
|
||||
*sa = SMTPAuthLoginNoEnc
|
||||
case "none", "noauth", "no":
|
||||
*sa = SMTPAuthNoAuth
|
||||
case "plain":
|
||||
*sa = SMTPAuthPlain
|
||||
case "plain-noenc":
|
||||
*sa = SMTPAuthPlainNoEnc
|
||||
case "scram-sha-1", "scram-sha1", "scramsha1":
|
||||
*sa = SMTPAuthSCRAMSHA1
|
||||
case "scram-sha-1-plus", "scram-sha1-plus", "scramsha1plus":
|
||||
*sa = SMTPAuthSCRAMSHA1PLUS
|
||||
case "scram-sha-256", "scram-sha256", "scramsha256":
|
||||
*sa = SMTPAuthSCRAMSHA256
|
||||
case "scram-sha-256-plus", "scram-sha256-plus", "scramsha256plus":
|
||||
*sa = SMTPAuthSCRAMSHA256PLUS
|
||||
case "xoauth2", "oauth2":
|
||||
*sa = SMTPAuthXOAUTH2
|
||||
default:
|
||||
return fmt.Errorf("unsupported SMTP auth type: %s", value)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
59
auth_test.go
59
auth_test.go
|
@ -1,59 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2024 The go-mail Authors
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package mail
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestSMTPAuthType_UnmarshalString(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
authString string
|
||||
expected SMTPAuthType
|
||||
}{
|
||||
{"CRAM-MD5: cram-md5", "cram-md5", SMTPAuthCramMD5},
|
||||
{"CRAM-MD5: crammd5", "crammd5", SMTPAuthCramMD5},
|
||||
{"CRAM-MD5: cram", "cram", SMTPAuthCramMD5},
|
||||
{"CUSTOM", "custom", SMTPAuthCustom},
|
||||
{"LOGIN", "login", SMTPAuthLogin},
|
||||
{"LOGIN-NOENC", "login-noenc", SMTPAuthLoginNoEnc},
|
||||
{"NONE: none", "none", SMTPAuthNoAuth},
|
||||
{"NONE: noauth", "noauth", SMTPAuthNoAuth},
|
||||
{"NONE: no", "no", SMTPAuthNoAuth},
|
||||
{"PLAIN", "plain", SMTPAuthPlain},
|
||||
{"PLAIN-NOENC", "plain-noenc", SMTPAuthPlainNoEnc},
|
||||
{"SCRAM-SHA-1: scram-sha-1", "scram-sha-1", SMTPAuthSCRAMSHA1},
|
||||
{"SCRAM-SHA-1: scram-sha1", "scram-sha1", SMTPAuthSCRAMSHA1},
|
||||
{"SCRAM-SHA-1: scramsha1", "scramsha1", SMTPAuthSCRAMSHA1},
|
||||
{"SCRAM-SHA-1-PLUS: scram-sha-1-plus", "scram-sha-1-plus", SMTPAuthSCRAMSHA1PLUS},
|
||||
{"SCRAM-SHA-1-PLUS: scram-sha1-plus", "scram-sha1-plus", SMTPAuthSCRAMSHA1PLUS},
|
||||
{"SCRAM-SHA-1-PLUS: scramsha1plus", "scramsha1plus", SMTPAuthSCRAMSHA1PLUS},
|
||||
{"SCRAM-SHA-256: scram-sha-256", "scram-sha-256", SMTPAuthSCRAMSHA256},
|
||||
{"SCRAM-SHA-256: scram-sha256", "scram-sha256", SMTPAuthSCRAMSHA256},
|
||||
{"SCRAM-SHA-256: scramsha256", "scramsha256", SMTPAuthSCRAMSHA256},
|
||||
{"SCRAM-SHA-256-PLUS: scram-sha-256-plus", "scram-sha-256-plus", SMTPAuthSCRAMSHA256PLUS},
|
||||
{"SCRAM-SHA-256-PLUS: scram-sha256-plus", "scram-sha256-plus", SMTPAuthSCRAMSHA256PLUS},
|
||||
{"SCRAM-SHA-256-PLUS: scramsha256plus", "scramsha256plus", SMTPAuthSCRAMSHA256PLUS},
|
||||
{"XOAUTH2: xoauth2", "xoauth2", SMTPAuthXOAUTH2},
|
||||
{"XOAUTH2: oauth2", "oauth2", SMTPAuthXOAUTH2},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var authType SMTPAuthType
|
||||
if err := authType.UnmarshalString(tt.authString); err != nil {
|
||||
t.Errorf("UnmarshalString() for type %s failed: %s", tt.authString, err)
|
||||
}
|
||||
if authType != tt.expected {
|
||||
t.Errorf("UnmarshalString() for type %s failed: expected %s, got %s",
|
||||
tt.authString, tt.expected, authType)
|
||||
}
|
||||
})
|
||||
}
|
||||
t.Run("should fail", func(t *testing.T) {
|
||||
var authType SMTPAuthType
|
||||
if err := authType.UnmarshalString("invalid"); err == nil {
|
||||
t.Error("UnmarshalString() should have failed")
|
||||
}
|
||||
})
|
||||
}
|
|
@ -13,8 +13,8 @@ import (
|
|||
// in encoding processes.
|
||||
var newlineBytes = []byte(SingleNewLine)
|
||||
|
||||
// ErrNoOutWriter is the error returned when no io.Writer is set for Base64LineBreaker.
|
||||
var ErrNoOutWriter = errors.New("no io.Writer set for Base64LineBreaker")
|
||||
// ErrNoOutWriter is the error message returned when no io.Writer is set for Base64LineBreaker.
|
||||
const ErrNoOutWriter = "no io.Writer set for Base64LineBreaker"
|
||||
|
||||
// Base64LineBreaker handles base64 encoding with the insertion of new lines after a certain number
|
||||
// of characters.
|
||||
|
@ -44,7 +44,7 @@ type Base64LineBreaker struct {
|
|||
// - err: An error if one occurred during the write operation.
|
||||
func (l *Base64LineBreaker) Write(data []byte) (numBytes int, err error) {
|
||||
if l.out == nil {
|
||||
err = ErrNoOutWriter
|
||||
err = errors.New(ErrNoOutWriter)
|
||||
return
|
||||
}
|
||||
if l.used+len(data) < MaxBodyLength {
|
||||
|
|
|
@ -5,165 +5,487 @@
|
|||
package mail
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const logoB64 = `PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PCFE
|
||||
T0NUWVBFIHN2ZyBQVUJMSUMgIi0vL1czQy8vRFREIFNWRyAxLjEvL0VOIiAiaHR0cDovL3d3dy53
|
||||
My5vcmcvR3JhcGhpY3MvU1ZHLzEuMS9EVEQvc3ZnMTEuZHRkIj48c3ZnIHdpZHRoPSIxMDAlIiBo
|
||||
ZWlnaHQ9IjEwMCUiIHZpZXdCb3g9IjAgMCA2MDAgNjAwIiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJo
|
||||
dHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3Jn
|
||||
LzE5OTkveGxpbmsiIHhtbDpzcGFjZT0icHJlc2VydmUiIHhtbG5zOnNlcmlmPSJodHRwOi8vd3d3
|
||||
LnNlcmlmLmNvbS8iIHN0eWxlPSJmaWxsLXJ1bGU6ZXZlbm9kZDtjbGlwLXJ1bGU6ZXZlbm9kZDtz
|
||||
dHJva2UtbGluZWNhcDpyb3VuZDtzdHJva2UtbGluZWpvaW46cm91bmQ7c3Ryb2tlLW1pdGVybGlt
|
||||
aXQ6MS41OyI+PHJlY3QgaWQ9ItCc0L7QvdGC0LDQttC90LDRjy3QvtCx0LvQsNGB0YLRjDEiIHNl
|
||||
cmlmOmlkPSLQnNC+0L3RgtCw0LbQvdCw0Y8g0L7QsdC70LDRgdGC0YwxIiB4PSIwIiB5PSIwIiB3
|
||||
aWR0aD0iNjAwIiBoZWlnaHQ9IjYwMCIgc3R5bGU9ImZpbGw6bm9uZTsiLz48Zz48cGF0aCBkPSJN
|
||||
NTg0LjcyNiw5OC4zOTVjLTAsLTkuNzM4IC03LjkwNywtMTcuNjQ1IC0xNy42NDUsLTE3LjY0NWwt
|
||||
NTEwLjAyNywwYy05LjczOSwwIC0xNy42NDUsNy45MDcgLTE3LjY0NSwxNy42NDVsLTAsMzE5Ljc5
|
||||
NGMtMCw5LjczOSA3LjkwNiwxNy42NDYgMTcuNjQ1LDE3LjY0Nmw1MTAuMDI3LC0wYzkuNzM4LC0w
|
||||
IDE3LjY0NSwtNy45MDcgMTcuNjQ1LC0xNy42NDZsLTAsLTMxOS43OTRaIiBzdHlsZT0iZmlsbDoj
|
||||
MmMyYzJjO3N0cm9rZTojMDAwO3N0cm9rZS13aWR0aDoyLjFweDsiLz48cGF0aCBkPSJNNTg1LjEy
|
||||
Niw5OC4zOTVjLTAsLTkuNzM4IC03LjkwNywtMTcuNjQ1IC0xNy42NDUsLTE3LjY0NWwtNDk0Ljgz
|
||||
OSwwYy05LjczOCwwIC0xNy42NDUsNy45MDcgLTE3LjY0NSwxNy42NDVsMCwzMTkuNzk0YzAsOS43
|
||||
MzkgNy45MDcsMTcuNjQ2IDE3LjY0NSwxNy42NDZsNDk0LjgzOSwtMGM5LjczOCwtMCAxNy42NDUs
|
||||
LTcuOTA3IDE3LjY0NSwtMTcuNjQ2bC0wLC0zMTkuNzk0WiIgc3R5bGU9ImZpbGw6IzM3MzczNztz
|
||||
dHJva2U6IzAwMDtzdHJva2Utd2lkdGg6Mi4wN3B4OyIvPjxwYXRoIGQ9Ik01NjcuMjU5LDExNC40
|
||||
NzJjLTAsLTguNzYgLTcuMTEyLC0xNS44NzIgLTE1Ljg3MiwtMTUuODcybC00NjIuNjUxLDBjLTgu
|
||||
NzYsMCAtMTUuODcyLDcuMTEyIC0xNS44NzIsMTUuODcybDAsMjg3LjY0MWMwLDguNzYgNy4xMTIs
|
||||
MTUuODcxIDE1Ljg3MiwxNS44NzFsNDYyLjY1MSwwYzguNzYsMCAxNS44NzIsLTcuMTExIDE1Ljg3
|
||||
MiwtMTUuODcxbC0wLC0yODcuNjQxWiIgc3R5bGU9ImZpbGw6I2ZmZjJlYjtzdHJva2U6IzAwMDtz
|
||||
dHJva2Utd2lkdGg6MS45cHg7Ii8+PHBhdGggZD0iTTIzNi4xNDUsNDM1Ljg3OGwtMjMuNDIsOTEu
|
||||
MDA4bDEzMC43NjUsLTBsMjIuMzY1LC05MC43NTZsLTEyOS43MSwtMC4yNTJaIiBzdHlsZT0iZmls
|
||||
bDojMmMyYzJjO3N0cm9rZTojMDAwO3N0cm9rZS13aWR0aDoyLjQ3cHg7Ii8+PHBhdGggZD0iTTI0
|
||||
NS4zOTcsNDM1Ljg3OGwtMjMuNDIxLDkxLjAwOGwxMzAuNzY2LC0wbDIyLjM2NSwtOTAuNzU2bC0x
|
||||
MjkuNzEsLTAuMjUyWiIgc3R5bGU9ImZpbGw6IzM3MzczNztzdHJva2U6IzAwMDtzdHJva2Utd2lk
|
||||
dGg6Mi40N3B4OyIvPjxwYXRoIGQ9Ik00NzIuNTg0LDUzMS4wOTNjLTAsLTIuODI3IC0yLjI5NSwt
|
||||
NS4xMjEgLTUuMTIxLC01LjEyMWwtMjkyLjU0MSwtMGMtMi44MjYsLTAgLTUuMTIxLDIuMjk0IC01
|
||||
LjEyMSw1LjEyMWwwLDEwLjI0M2MwLDIuODI2IDIuMjk1LDUuMTIxIDUuMTIxLDUuMTIxbDI5Mi41
|
||||
NDEsLTBjMi44MjYsLTAgNS4xMjEsLTIuMjk1IDUuMTIxLC01LjEyMWwtMCwtMTAuMjQzWiIgc3R5
|
||||
bGU9ImZpbGw6IzJjMmMyYztzdHJva2U6IzAwMDtzdHJva2Utd2lkdGg6Mi40N3B4OyIvPjxnPjxw
|
||||
YXRoIGQ9Ik0xOTAuMzc5LDQzMS40NTVjLTAsLTEuOTE2IC0xLjU1NiwtMy40NzIgLTMuNDcyLC0z
|
||||
LjQ3MmwtNTQuNTYzLDBjLTEuOTE3LDAgLTMuNDcyLDEuNTU2IC0zLjQ3MiwzLjQ3MmwtMCw0Ni4z
|
||||
MDljLTAsMS45MTYgMS41NTUsMy40NzIgMy40NzIsMy40NzJsNTQuNTYzLC0wYzEuOTE2LC0wIDMu
|
||||
NDcyLC0xLjU1NiAzLjQ3MiwtMy40NzJsLTAsLTQ2LjMwOVoiIHN0eWxlPSJmaWxsOiNlZWFjMDE7
|
||||
c3Ryb2tlOiMwMDA7c3Ryb2tlLXdpZHRoOjMuMXB4OyIvPjxnPjxwYXRoIGQ9Ik0xMzkuOTk2LDQ0
|
||||
Mi44NTNjLTAsMC40MiAtMC4wNzEsMC45MjIgMC4wNjMsMS4zMjNjMC41MywxLjU5MiAyLjc1LDEu
|
||||
Mzg2IDQuMDk1LDEuMzg2YzIuMzk3LC0wIDQuNjc0LC0wLjEwOCA2LjQ4OSwtMS44MjdjMC40NSwt
|
||||
MC40MjYgMC45NjYsLTAuOTQgMS4wNzIsLTEuNTc1YzAuMDYxLC0wLjM2NyAwLjEwMiwtMS4yMDgg
|
||||
LTAuMTI2LC0xLjUxMmMtMC44MDMsLTEuMDcxIC0yLjg0NCwtMC4zNzMgLTMuNjU1LDAuMjUyYy0w
|
||||
LjcxNCwwLjU1IC0wLjgyMiwxLjgyNiAtMC45NDUsMi42NDZjLTAuNDA2LDIuNzA1IDAuMzg3LDUu
|
||||
MDMgMy4xNSw1Ljg1OWMzLjQwNSwxLjAyMSA2Ljc4NCwwLjA2IDEwLjA4NiwtMC44MDljMS41OTcs
|
||||
LTAuNDIxIDMuNjYsLTAuNzcgNS4wMTcsLTEuNzJjMC40NTIsLTAuMzE2IDAuNDQ0LC0wLjI4NCAw
|
||||
LjkwMSwtMC41NThjMS4yMDgsLTAuNzI1IDMuODY5LC0xLjkyNyAyLjQ1NywtMy41OTFjLTAuMTks
|
||||
LTAuMjI0IC0xLjEzOSwtMC4yMzcgLTEuMjYsMC4xMjZjLTAuNDIsMS4yNTcgLTEuMzI2LDMuMzAy
|
||||
IDAuNTY3LDMuNzhjMS4zOTgsMC4zNTMgMi45MTksMC4xODkgNC4zNDcsMC4xODljMC4xOTMsLTAg
|
||||
MS40ODUsLTAuMjc5IDEuNzAxLC0wLjA2M2MwLjU1NSwwLjU1NSAxLjU0MywwLjgxMSAyLjMzMSwx
|
||||
LjAwOGMwLjkzMiwwLjIzMyAxLjk0NCwwLjYwNiAyLjg5OSwwLjY5M2MwLjg2OSwwLjA3OSAxLjc5
|
||||
LC0wLjA0NSAyLjY0NiwwLjEyNiIgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2U6IzAwMDtzdHJva2Ut
|
||||
d2lkdGg6MS45NnB4OyIvPjxwYXRoIGQ9Ik0xNDIuNzA1LDQ1Mi42ODFjMC4xMTksMC41NTEgMC44
|
||||
OTMsMC42NjkgMS4zMTgsMC44NDZjMS41OTgsMC42NjYgMy41MTQsMC44NzQgNS4yMzQsMC45ODJj
|
||||
Mi4zNzgsMC4xNDggNC43MDQsLTAuMTU5IDcuMDU3LC0wLjMxNWMxLjg2MSwtMC4xMjUgMy43NDYs
|
||||
MC4xMTYgNS42MDcsLTBjMi4wNTEsLTAuMTI5IDQuMzIxLC0wLjM3NyA2LjM2NCwtMC4wNjNjMS4w
|
||||
NzIsMC4xNjQgMi4wODksMC40OCAzLjE1LDAuNjkzIiBzdHlsZT0iZmlsbDpub25lO3N0cm9rZToj
|
||||
MDAwO3N0cm9rZS13aWR0aDoxLjk2cHg7Ii8+PHBhdGggZD0iTTE3Ni4zNDksNDU1LjA3NmMxLjEx
|
||||
MywtMC4wMjEgMi4yMjYsLTAuMDQyIDMuMzQsLTAuMDYzIiBzdHlsZT0iZmlsbDpub25lO3N0cm9r
|
||||
ZTojMDAwO3N0cm9rZS13aWR0aDoxLjk2cHg7Ii8+PHBhdGggZD0iTTE0NS4xNjIsNDU5Ljg2NGwy
|
||||
MS42NzQsLTAiIHN0eWxlPSJmaWxsOm5vbmU7c3Ryb2tlOiMwMDA7c3Ryb2tlLXdpZHRoOjEuOTZw
|
||||
eDsiLz48L2c+PC9nPjxnPjxnPjxwYXRoIGQ9Ik03Ni4yNzMsMTIxLjQ5NGw0ODcuNTU3LC0wLjAx
|
||||
NSIgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2U6I2Y0ZGRkMDtzdHJva2Utd2lkdGg6NC45M3B4O3N0
|
||||
cm9rZS1saW5lY2FwOnNxdWFyZTsiLz48cGF0aCBkPSJNMjMwLjk1MSwyMjQuMTc1Yy0wLjgwMiwt
|
||||
My4zMjcgLTguMDU5LC03Ljk4NCAtMTEuMDA1LC04Ljk0NWMtMzUuODQ2LC0xMS42OTkgLTIwLjc4
|
||||
MywyNy4zNzIgLTAuNjE4LDI0LjM0MiIgc3R5bGU9ImZpbGw6IzYxZTBlZTsiLz48cGF0aCBkPSJN
|
||||
MjM0Ljk0NCwyMjMuMjEzYy0wLjM3OSwtMS41NzQgLTEuNTIyLC0zLjQzNiAtMy4yNzYsLTUuMTc0
|
||||
Yy0zLjA2MSwtMy4wMzMgLTguMDE0LC01LjkyIC0xMC40NDgsLTYuNzE0Yy0xMy4xMzUsLTQuMjg3
|
||||
IC0yMC42MTgsLTEuOTk0IC0yNC4wNzEsMS44MTljLTMuNjM4LDQuMDE3IC0zLjgzNiwxMC40OTIg
|
||||
LTAuODI1LDE2LjY1MWMzLjk4MSw4LjE0MyAxMy4zMjEsMTUuMzg2IDIzLjYxNSwxMy44MzljMi4y
|
||||
NDIsLTAuMzM3IDMuNzg4LC0yLjQzIDMuNDUxLC00LjY3MmMtMC4zMzcsLTIuMjQyIC0yLjQzLC0z
|
||||
Ljc4OSAtNC42NzIsLTMuNDUyYy02LjY0NywwLjk5OSAtMTIuNDQ0LC00LjA2NSAtMTUuMDE0LC05
|
||||
LjMyM2MtMC44MDEsLTEuNjM3IC0xLjI5MywtMy4zMDggLTEuMzAyLC00Ljg0N2MtMC4wMDUsLTEu
|
||||
MDIgMC4xOTUsLTEuOTc0IDAuODM2LC0yLjY4MmMwLjgzMiwtMC45MTggMi4yMjUsLTEuMzU0IDQu
|
||||
MTMzLC0xLjQ4MWMyLjg1MiwtMC4xOTEgNi41NjcsMC40MTIgMTEuMywxLjk1N2MxLjQ1NiwwLjQ3
|
||||
NSA0LjE2LDIuMDk2IDYuMjU3LDMuODY5YzAuNjM2LDAuNTM3IDEuMjE1LDEuMDg4IDEuNjU4LDEu
|
||||
NjM2YzAuMTUyLDAuMTg4IDAuMzMxLDAuMzI3IDAuMzcyLDAuNDk4YzAuNTMxLDIuMjA0IDIuNzUx
|
||||
LDMuNTYyIDQuOTU1LDMuMDMxYzIuMjA0LC0wLjUzMSAzLjU2MiwtMi43NTIgMy4wMzEsLTQuOTU1
|
||||
WiIvPjxwYXRoIGQ9Ik0zNDkuMzI2LDEyMy41MjdjMCwwIC0xNS4zNSwtMTMuODYyIC0xOC4xMTQs
|
||||
MjIuODM0IiBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojNjFlMGVlO3N0cm9rZS13aWR0aDo2LjE2
|
||||
cHg7Ii8+PHBhdGggZD0iTTI5OS42NjIsMTIzLjQ4NWMtMCwtMCAxMi42NzEsLTE1LjI2NSAyMC4x
|
||||
MTIsMjEuODk4IiBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojNjFlMGVlO3N0cm9rZS13aWR0aDo2
|
||||
LjE2cHg7Ii8+PHBhdGggZD0iTTMzNi4xMTEsMTA4Ljc3M2MtMCwwIC0xNi44MDgsLTkuMDk5IC0x
|
||||
MC44ODIsMzIuMTI0IiBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojNjFlMGVlO3N0cm9rZS13aWR0
|
||||
aDo2LjE2cHg7Ii8+PHBhdGggZD0iTTQxNy43OTYsMTg0LjQ1M2MzLjI4MiwwLjk3IDYuNTgzLC0x
|
||||
LjI5IDguODg5LC0zLjM2MWMyOC4wNTUsLTI1LjE5NCAtMTMuMTk4LC0zMS41MzggLTIwLjY1Niwt
|
||||
MTIuNTU5IiBzdHlsZT0iZmlsbDojNjFlMGVlOyIvPjxwYXRoIGQ9Ik00MTYuNjMyLDE4OC4zOTNj
|
||||
NC42NTcsMS4zNzUgOS41MjYsLTEuMzA3IDEyLjc5NywtNC4yNDVjMTAuMjc5LC05LjIzIDEyLjA0
|
||||
NiwtMTYuODQzIDEwLjQ4OCwtMjEuNzE2Yy0xLjY0NSwtNS4xNSAtNy4xNDIsLTguNTQ0IC0xMy45
|
||||
NzgsLTguOTljLTkuMDExLC0wLjU4NyAtMTkuOTI3LDMuOTA0IC0yMy43MzMsMTMuNTg5Yy0wLjgz
|
||||
LDIuMTEgMC4yMSw0LjQ5NiAyLjMyLDUuMzI1YzIuMTEsMC44MjkgNC40OTYsLTAuMjExIDUuMzI1
|
||||
LC0yLjMyMWMyLjQ2LC02LjI1OSA5LjczLC04Ljc3NSAxNS41NTMsLTguMzk1YzEuODA3LDAuMTE3
|
||||
IDMuNDg5LDAuNTE3IDQuODE3LDEuMjY5YzAuODcxLDAuNDk0IDEuNTg2LDEuMTMgMS44NzEsMi4w
|
||||
MjNjMC4zNzUsMS4xNzIgMC4wNDksMi41ODggLTAuNzk1LDQuMjk4Yy0xLjI2NiwyLjU2MiAtMy42
|
||||
NDksNS40NzcgLTcuMzU2LDguODA2Yy0wLjg2OCwwLjc4IC0xLjkwNywxLjYxOCAtMy4wNTQsMi4x
|
||||
NDdjLTAuNjE2LDAuMjg0IC0xLjI2NSwwLjUyNyAtMS45MjgsMC4zMzFjLTIuMTc0LC0wLjY0MiAt
|
||||
NC40NjEsMC42MDIgLTUuMTAzLDIuNzc2Yy0wLjY0MiwyLjE3NCAwLjYwMiw0LjQ2MSAyLjc3Niw1
|
||||
LjEwM1oiLz48cGF0aCBkPSJNNDM2LjAxNCw0MTkuNjQ1YzMuNjAxLC0xMC45MzQgOS4wMDMsLTMx
|
||||
LjYwOSA1LjY4MiwtNDkuNTYzYy0xNC45NTMsLTgwLjg0NyA3Ljg5NSwtOTguNTQ5IC01LjAzNSwt
|
||||
MTQ3LjU5M2MtNC40OSwtMTcuMDI5IC0xMi4wNDksLTM2LjI1IC0yMy41NTQsLTQ5Ljc5MmMtMjUu
|
||||
Mjg1LC0yOS43NjIgLTcwLjczNiwtNDIuOTEzIC0xMDguNzMsLTMyLjc4NWMtMTUuMjEzLDQuMDU1
|
||||
IC0yOC42NjIsMTEuNzI3IC00MS45MjMsMjAuMDM0Yy00Ni4xMzYsMjguOSAtNTIuMjU5LDgzLjM3
|
||||
NyAtNDIuNzkxLDEyOC4yNDhjNC4xOTEsMTkuODY0IDIzLjY3Myw0OS44OTcgMjIuNTg2LDcwLjE5
|
||||
NGMtMS4zODcsMjUuOTEgLTAuNzY1LDQ0LjQyNiAzLjM3Niw2MC44ODQiIHN0eWxlPSJmaWxsOiM2
|
||||
MWUwZWU7Ii8+PGNsaXBQYXRoIGlkPSJfY2xpcDEiPjxwYXRoIGQ9Ik00MzYuMDE0LDQxOS42NDVj
|
||||
My42MDEsLTEwLjkzNCA5LjAwMywtMzEuNjA5IDUuNjgyLC00OS41NjNjLTE0Ljk1MywtODAuODQ3
|
||||
IDcuODk1LC05OC41NDkgLTUuMDM1LC0xNDcuNTkzYy00LjQ5LC0xNy4wMjkgLTEyLjA0OSwtMzYu
|
||||
MjUgLTIzLjU1NCwtNDkuNzkyYy0yNS4yODUsLTI5Ljc2MiAtNzAuNzM2LC00Mi45MTMgLTEwOC43
|
||||
MywtMzIuNzg1Yy0xNS4yMTMsNC4wNTUgLTI4LjY2MiwxMS43MjcgLTQxLjkyMywyMC4wMzRjLTQ2
|
||||
LjEzNiwyOC45IC01Mi4yNTksODMuMzc3IC00Mi43OTEsMTI4LjI0OGM0LjE5MSwxOS44NjQgMjMu
|
||||
NjczLDQ5Ljg5NyAyMi41ODYsNzAuMTk0Yy0xLjM4NywyNS45MSAtMC43NjUsNDQuNDI2IDMuMzc2
|
||||
LDYwLjg4NCIvPjwvY2xpcFBhdGg+PGcgY2xpcC1wYXRoPSJ1cmwoI19jbGlwMSkiPjxwYXRoIGQ9
|
||||
Ik0zMTEuNzc2LDIzNi44MzVjMCwwIC0zLjA2OCwyNC4xOTkgLTMxLjU5NywyNC44MzhjLTI4LjUz
|
||||
LDAuNjM5IC0zMi41OTEsLTI4LjI0NiAtMzIuNTkxLC0yOC4yNDZsNjQuMTg4LDMuNDA4WiIgc3R5
|
||||
bGU9ImZpbGw6IzRmY2JlMDsiLz48cGF0aCBkPSJNNDMyLjUwOSwyMTYuMDIzYzAsLTAgLTMuMDY3
|
||||
LDI0LjE5OCAtMzEuNTk3LDI0LjgzN2MtMjguNTMsMC42MzkgLTMyLjU5MSwtMjguMjQ2IC0zMi41
|
||||
OTEsLTI4LjI0Nmw2NC4xODgsMy40MDlaIiBzdHlsZT0iZmlsbDojNGZjYmUwOyIvPjxwYXRoIGQ9
|
||||
Ik0yODYuODQ1LDQxOC45NjFsMTI3Ljg4NiwtMGMwLjM4NiwtMS4yNjcgMC42NjYsLTIuNDMyIDAu
|
||||
ODcsLTMuNDcxYzMuODY3LC0xOS43NDMgMy43MjksLTU3LjQ2OCAtMi40MTUsLTc3LjA5NWMtMi4x
|
||||
MzMsLTYuODE2IC01Ljc5MywtMTQuNDkgLTExLjQ3NiwtMTkuODUyYy0xMi40OSwtMTEuNzg1IC02
|
||||
NC4zOTgsLTkuMTgxIC04My41NjEsLTQuODM1Yy03LjY3MywxLjc0MSAtMTQuNDg3LDQuOTIyIC0y
|
||||
MS4yMSw4LjM1OGMtMTQuOTExLDcuNjIgLTIwLjI1Myw1OC43MTIgLTEwLjA5NCw5Ni44OTVaIiBz
|
||||
dHlsZT0iZmlsbDojYjdmOGZmOyIvPjxwYXRoIGQ9Ik0yNDYuNzExLDQxOC45NjFjLTUuMTA4LC0y
|
||||
MC42MTMgLTYuNDksLTQyLjk3NSAtNS41NjIsLTYwLjg1NWMyLjEzNiwtNDEuMTMzIC0zMS4zMTQs
|
||||
LTUwLjQ1MyAtMjUuNDYxLC0xMjEuNTEzYzE0LjUzNCwtNDIuNDA5IDE1LjA4OSwtNDAuMTA3IDI3
|
||||
LjY1MywtNTUuNDQ1YzAsMCAtMTMuMTQ3LDM5Ljk2OCAtMS4xOTMsNzAuNzI4YzExLjk1NCwzMC43
|
||||
NTkgMzIuMTc0LDczLjQxOCAzMi41OSwxMTcuNDkxYzAuMTg2LDE5Ljc3MSAxLjYzNCwzNi4zMSA0
|
||||
LjEzLDQ5LjU5NGwtMzIuMTU3LC0wWiIgc3R5bGU9ImZpbGw6IzRmY2JlMDsiLz48L2c+PHBhdGgg
|
||||
ZD0iTTQzOS45MTUsNDIwLjkzYzMuNzQ4LC0xMS4zOCA5LjI3NiwtMzIuOTA4IDUuODIsLTUxLjU5
|
||||
NWMtOC44NjUsLTQ3LjkzMyAtNC4yNTksLTczLjQwNSAtMS45MzUsLTk2LjU4YzEuNjEzLC0xNi4w
|
||||
OTMgMi4xNTYsLTMxLjEyMiAtMy4xNjcsLTUxLjMxM2MtNC42MzgsLTE3LjU5MSAtMTIuNTExLC0z
|
||||
Ny40MTYgLTI0LjM5NiwtNTEuNDA1Yy0yNi4yNjEsLTMwLjkxMSAtNzMuNDU2LC00NC42MTMgLTEx
|
||||
Mi45MTcsLTM0LjA5NGMtMTUuNjE0LDQuMTYxIC0yOS40MzcsMTEuOTk2IC00My4wNDcsMjAuNTIy
|
||||
Yy00Ny43MzUsMjkuOTAyIC01NC40MjYsODYuMTUgLTQ0LjYyOSwxMzIuNTc3YzIuMTM5LDEwLjEz
|
||||
OCA4LjEzNiwyMi44ODggMTMuNTA0LDM1LjY3NmM0Ljk5NCwxMS45IDkuNTE1LDIzLjgxIDguOTk5
|
||||
LDMzLjQ1Yy0xLjQxNSwyNi40MzIgLTAuNzMsNDUuMzE3IDMuNDk1LDYyLjEwN2wxLjAwMiwzLjk4
|
||||
M2w3Ljk2NywtMi4wMDVsLTEuMDAyLC0zLjk4M2MtNC4wNTgsLTE2LjEyNyAtNC42MTgsLTM0LjI3
|
||||
NCAtMy4yNTgsLTU5LjY2M2MwLjQ2LC04LjYwOSAtMi40NiwtMTguODk1IC02LjU0NSwtMjkuNDU3
|
||||
Yy01LjY5NCwtMTQuNzI0IC0xMy42NDYsLTMwLjA1OSAtMTYuMTI0LC00MS44MDRjLTkuMTQsLTQz
|
||||
LjMxNSAtMy41ODQsLTk2LjAyMiA0MC45NTIsLTEyMy45MTljMTIuOTEyLC04LjA4OSAyNS45ODks
|
||||
LTE1LjU5OCA0MC44MDEsLTE5LjU0N2MzNi41MjYsLTkuNzM1IDgwLjIzNCwyLjg2NSAxMDQuNTQx
|
||||
LDMxLjQ3NmMxMS4xMjYsMTMuMDk2IDE4LjM3MiwzMS43MTMgMjIuNzEzLDQ4LjE4YzYuMzEsMjMu
|
||||
OTMyIDMuODIsNDAuMjE5IDEuNjQ3LDYwLjM4M2MtMi4yNTcsMjAuOTQ5IC00LjI1OSw0NS45Mjcg
|
||||
My4zMjEsODYuOTFjMy4xODYsMTcuMjIyIC0yLjA5LDM3LjA0MyAtNS41NDQsNDcuNTMxbC0xLjI4
|
||||
NSwzLjkwMmw3LjgwMywyLjU2OWwxLjI4NCwtMy45MDFaIi8+PHBhdGggZD0iTTQyNC41MzksMjQy
|
||||
LjI0N2MtMS44NjgsMC4xMzQgLTMuMjc2LDEuNzU4IC0zLjE0MiwzLjYyNmMwLjEzMywxLjg2NyAx
|
||||
Ljc1NywzLjI3NSAzLjYyNSwzLjE0MWMxLjg2NywtMC4xMzMgMy4yNzUsLTEuNzU3IDMuMTQyLC0z
|
||||
LjYyNWMtMC4xMzQsLTEuODY3IC0xLjc1OCwtMy4yNzUgLTMuNjI1LC0zLjE0MloiIHN0eWxlPSJm
|
||||
aWxsOiNiN2Y4ZmY7Ii8+PHBhdGggZD0iTTI1Ni45OTcsMjYwLjM5MWMtMS44NjgsMC4xMzMgLTMu
|
||||
Mjc1LDEuNzU4IC0zLjE0MiwzLjYyNWMwLjEzMywxLjg2OCAxLjc1OCwzLjI3NSAzLjYyNSwzLjE0
|
||||
MmMxLjg2OCwtMC4xMzMgMy4yNzUsLTEuNzU4IDMuMTQyLC0zLjYyNWMtMC4xMzMsLTEuODY4IC0x
|
||||
Ljc1OCwtMy4yNzUgLTMuNjI1LC0zLjE0MloiIHN0eWxlPSJmaWxsOiNiN2Y4ZmY7Ii8+PGc+PHBh
|
||||
dGggZD0iTTM2NC4wOSwyNjkuMjI5Yy0xLjE4Niw0Ljk2NyAtMS40NDMsMTQuMzA2IC00LjMxNSwx
|
||||
OC40Yy0xLjM5MywxLjk4NSAtNC40OTUsMi4wODYgLTYuNTE2LDIuMTUzYy0xMS42MTIsMC4zODMg
|
||||
LTExLjQ0NiwtMTEuNTEzIC0xMy4xODEsLTIxLjg1NSIgc3R5bGU9ImZpbGw6I2VkZWRlZDsiLz48
|
||||
cGF0aCBkPSJNMzYxLjA5NCwyNjguNTEzYy0wLjcxOSwzLjAwOSAtMS4xMDYsNy42MDcgLTEuOTIy
|
||||
LDExLjY5OGMtMC40NDUsMi4yMjggLTAuOTcxLDQuMjk4IC0xLjkxOSw1LjY0OWMtMC4yNjMsMC4z
|
||||
NzYgLTAuNzQ5LDAuNDU5IC0xLjIwMywwLjU2NmMtMC45OTUsMC4yMzMgLTIuMDU2LDAuMjUgLTIu
|
||||
ODkzLDAuMjc3Yy0yLjA1NSwwLjA2OCAtMy42MSwtMC4zMjYgLTQuNzc0LC0xLjE5MmMtMS4xOTMs
|
||||
LTAuODg3IC0xLjk1NCwtMi4yMTIgLTIuNTQ0LC0zLjc0M2MtMS41NjEsLTQuMDQ3IC0xLjg5LC05
|
||||
LjM5IC0yLjcyMywtMTQuMzUxYy0wLjI4MSwtMS42NzcgLTEuODcxLC0yLjgxIC0zLjU0OCwtMi41
|
||||
MjhjLTEuNjc3LDAuMjgxIC0yLjgwOSwxLjg3MSAtMi41MjgsMy41NDhjMC45MDMsNS4zODEgMS4z
|
||||
NTgsMTEuMTU4IDMuMDUxLDE1LjU0OGMxLjAzNiwyLjY4OCAyLjUyLDQuOTEyIDQuNjE1LDYuNDdj
|
||||
Mi4xMjUsMS41ODEgNC45MDMsMi41MyA4LjY1NCwyLjQwNmMxLjQ2OCwtMC4wNDggMy40LC0wLjE1
|
||||
MiA1LjA3OSwtMC43MTRjMS41NiwtMC41MjIgMi45MTksLTEuNDEgMy44NTgsLTIuNzQ5YzEuMzUs
|
||||
LTEuOTI0IDIuMjg0LC00LjgwOCAyLjkxNywtNy45ODFjMC44LC00LjAxMSAxLjE2OCwtOC41MjIg
|
||||
MS44NzMsLTExLjQ3M2MwLjM5NSwtMS42NTMgLTAuNjI3LC0zLjMxNyAtMi4yODEsLTMuNzEyYy0x
|
||||
LjY1NCwtMC4zOTUgLTMuMzE3LDAuNjI3IC0zLjcxMiwyLjI4MVoiLz48cGF0aCBkPSJNMzQ5LjAw
|
||||
MSwyMzQuMDVjLTkuMzMyLDEuMjQ2IC0xNi4yNjUsNy4wNzUgLTE1LjQ3MywxMy4wMDhjMC43OTIs
|
||||
NS45MzMgOS4wMTIsOS43MzggMTguMzQ0LDguNDkyYzkuMzMzLC0xLjI0NiAxNi4yNjYsLTcuMDc1
|
||||
IDE1LjQ3NCwtMTMuMDA4Yy0wLjc5MiwtNS45MzMgLTkuMDEyLC05LjczOCAtMTguMzQ1LC04LjQ5
|
||||
MloiLz48cGF0aCBkPSJNMzQ4LjkwMSwyNTIuNTVjMC40Niw1LjIwNCAxLjY5NCwxMC4xNDIgMi40
|
||||
NTMsMTUuMjc3YzAuMjQ4LDEuNjgyIDEuODE1LDIuODQ2IDMuNDk3LDIuNTk4YzEuNjgyLC0wLjI0
|
||||
OCAyLjg0NiwtMS44MTYgMi41OTgsLTMuNDk4Yy0wLjc0MSwtNS4wMTUgLTEuOTYxLC05LjgzNyAt
|
||||
Mi40MTEsLTE0LjkyYy0wLjE1LC0xLjY5NCAtMS42NDcsLTIuOTQ3IC0zLjM0LC0yLjc5N2MtMS42
|
||||
OTQsMC4xNSAtMi45NDcsMS42NDcgLTIuNzk3LDMuMzRaIi8+PHBhdGggZD0iTTM3NC4yNiwyNTku
|
||||
ODE0Yy04LjAyMiw1LjM1IC0xNS45MDEsNy44MjUgLTIzLjU5NCw3LjI3NGMtNy42OTQsLTAuNTUx
|
||||
IC0xNS4xNDgsLTQuMTI0IC0yMi4zNDIsLTEwLjU4OWMtMS4yNjUsLTEuMTM2IC0zLjIxNCwtMS4w
|
||||
MzIgLTQuMzUxLDAuMjMzYy0xLjEzNiwxLjI2NCAtMS4wMzIsMy4yMTQgMC4yMzMsNC4zNWM4LjM1
|
||||
Niw3LjUwOCAxNy4wODQsMTEuNTExIDI2LjAyLDEyLjE1MWM4LjkzNiwwLjY0IDE4LjEzNSwtMi4w
|
||||
NzggMjcuNDUzLC04LjI5NGMxLjQxNCwtMC45NDMgMS43OTcsLTIuODU4IDAuODUzLC00LjI3MmMt
|
||||
MC45NDMsLTEuNDE1IC0yLjg1OCwtMS43OTcgLTQuMjcyLC0wLjg1M1oiLz48cGF0aCBkPSJNMzc4
|
||||
LjYwNSwxNTIuMTU2Yy0zLjY0NywtMC44MjYgLTYuOTU2LDAuMDM4IC03LjM4NSwxLjkyOWMtMC40
|
||||
MjgsMS44OTEgMi4xODUsNC4wOTcgNS44MzIsNC45MjRjMy42NDcsMC44MjcgNi45NTYsLTAuMDM3
|
||||
IDcuMzg1LC0xLjkyOGMwLjQyOCwtMS44OTEgLTIuMTg1LC00LjA5OCAtNS44MzIsLTQuOTI1WiIg
|
||||
c3R5bGU9ImZpbGw6I2I3ZjhmZjsiLz48cGF0aCBkPSJNMjQ5LjUsMTc3LjI3Yy0yLjE5NSwzLjAy
|
||||
OCAtMi43MDIsNi40MSAtMS4xMzIsNy41NDhjMS41NywxLjEzOCA0LjYyNiwtMC4zOTYgNi44MjEs
|
||||
LTMuNDI0YzIuMTk1LC0zLjAyOCAyLjcwMiwtNi40MSAxLjEzMiwtNy41NDhjLTEuNTcsLTEuMTM4
|
||||
IC00LjYyNiwwLjM5NiAtNi44MjEsMy40MjRaIiBzdHlsZT0iZmlsbDojYjdmOGZmOyIvPjxwYXRo
|
||||
IGQ9Ik00MDMuNjQ4LDE2Mi44MjVjLTE5LjIxMywtMS4zNSAtMzUuOTA3LDEzLjE1MSAtMzcuMjU3
|
||||
LDMyLjM2NGMtMS4zNSwxOS4yMTIgMTMuMTUyLDM1LjkwNiAzMi4zNjQsMzcuMjU2YzE5LjIxMywx
|
||||
LjM1MSAzNS45MDcsLTEzLjE1MSAzNy4yNTcsLTMyLjM2NGMxLjM1LC0xOS4yMTIgLTEzLjE1Miwt
|
||||
MzUuOTA2IC0zMi4zNjQsLTM3LjI1NloiIHN0eWxlPSJmaWxsOiNmZmY7c3Ryb2tlOiMwMDA7c3Ry
|
||||
b2tlLXdpZHRoOjYuMTZweDsiLz48cGF0aCBkPSJNMjgxLjY5NiwxODEuNzhjLTE5LjIxMiwtMS4z
|
||||
NSAtMzUuOTA2LDEzLjE1MiAtMzcuMjU3LDMyLjM2NGMtMS4zNSwxOS4yMTIgMTMuMTUyLDM1Ljkw
|
||||
NyAzMi4zNjUsMzcuMjU3YzE5LjIxMiwxLjM1IDM1LjkwNiwtMTMuMTUyIDM3LjI1NiwtMzIuMzY0
|
||||
YzEuMzUsLTE5LjIxMyAtMTMuMTUyLC0zNS45MDcgLTMyLjM2NCwtMzcuMjU3WiIgc3R5bGU9ImZp
|
||||
bGw6I2ZmZjtzdHJva2U6IzAwMDtzdHJva2Utd2lkdGg6Ni4xNnB4OyIvPjxwYXRoIGQ9Ik0zOTgu
|
||||
NDk5LDE5Mi44ODFjLTYuODI0LC0wLjQ3OSAtMTIuNzU0LDQuNjcyIC0xMy4yMzMsMTEuNDk2Yy0w
|
||||
LjQ4LDYuODI0IDQuNjcxLDEyLjc1MyAxMS40OTUsMTMuMjMzYzYuODI0LDAuNDc5IDEyLjc1NCwt
|
||||
NC42NzIgMTMuMjMzLC0xMS40OTZjMC40OCwtNi44MjQgLTQuNjcxLC0xMi43NTMgLTExLjQ5NSwt
|
||||
MTMuMjMzWiIvPjxwYXRoIGQ9Ik0yNzguMTQyLDIxMC4yMjdjLTYuODI0LC0wLjQ3OSAtMTIuNzU0
|
||||
LDQuNjcxIC0xMy4yMzMsMTEuNDk2Yy0wLjQ4LDYuODI0IDQuNjcxLDEyLjc1MyAxMS40OTUsMTMu
|
||||
MjMzYzYuODI0LDAuNDc5IDEyLjc1NCwtNC42NzIgMTMuMjMzLC0xMS40OTZjMC40OCwtNi44MjQg
|
||||
LTQuNjcxLC0xMi43NTMgLTExLjQ5NSwtMTMuMjMzWiIvPjxwYXRoIGQ9Ik0zOTkuODkyLDE5Ny45
|
||||
NjFjLTEuOTE2LC0wLjEzNSAtMy41ODEsMS4zMTEgLTMuNzE2LDMuMjI3Yy0wLjEzNCwxLjkxNiAx
|
||||
LjMxMiwzLjU4MSAzLjIyOCwzLjcxNmMxLjkxNiwwLjEzNCAzLjU4MSwtMS4zMTIgMy43MTUsLTMu
|
||||
MjI4YzAuMTM1LC0xLjkxNiAtMS4zMTEsLTMuNTgxIC0zLjIyNywtMy43MTVaIiBzdHlsZT0iZmls
|
||||
bDojZmZmOyIvPjxwYXRoIGQ9Ik0yNzkuNTM1LDIxNS4zMDZjLTEuOTE2LC0wLjEzNCAtMy41ODEs
|
||||
MS4zMTIgLTMuNzE1LDMuMjI4Yy0wLjEzNSwxLjkxNiAxLjMxMSwzLjU4MSAzLjIyNywzLjcxNmMx
|
||||
LjkxNiwwLjEzNCAzLjU4MSwtMS4zMTIgMy43MTYsLTMuMjI4YzAuMTM0LC0xLjkxNiAtMS4zMTIs
|
||||
LTMuNTgxIC0zLjIyOCwtMy43MTZaIiBzdHlsZT0iZmlsbDojZmZmOyIvPjwvZz48L2c+PHBhdGgg
|
||||
ZD0iTTUxMi4yOTEsMzMzLjcyNmMwLC0wIC0yNy40OTYsMTMuNDAyIC00My4yNDUsMy43OTJjLTE1
|
||||
Ljc1LC05LjYxIC0xMC44LC0yNC43ODggMy4xMDIsLTIyLjUyYzEzLjkwMywyLjI2OCAyNy4xNjgs
|
||||
My4xOTUgMzUuOTQzLC0zLjkzN2M4Ljc3NiwtNy4xMzIgMjEuNjMzLDE0LjM3NiA0LjIsMjIuNjY1
|
||||
WiIgc3R5bGU9ImZpbGw6I2Q2YzI5NTtzdHJva2U6IzAwMDtzdHJva2Utd2lkdGg6Ni4xNnB4OyIv
|
||||
PjwvZz48Zz48cGF0aCBkPSJNNDQxLjI2OCwyOTEuMzQ5Yy0wLDAgMjguMDgsNDcuNDg4IC0xMjIu
|
||||
NTkxLDExNC4wNGMtMTEuNjI5LC0yLjAxNyAtMTQuMzMsLTguNTE1IC0xNC4zMywtOC41MTVjMCwt
|
||||
MCA0My42MTcsLTEyLjU4NiA4My44MTIsLTQ0Ljg3NmM0My41OTcsLTM1LjAyNCA1My4xMDksLTYw
|
||||
LjY0OSA1My4xMDksLTYwLjY0OVoiIHN0eWxlPSJmaWxsOiM3YjQ3MjI7c3Ryb2tlOiMwMDA7c3Ry
|
||||
b2tlLXdpZHRoOjYuNjhweDsiLz48cGF0aCBkPSJNMjMzLjk1NywzNzIuMTM4YzMuNTYsLTUuNTA2
|
||||
IDEwLjcyOSwtNy4zOTIgMTYuNTM4LC00LjM1MWMxNy43NjcsOS4yMzUgNTMuNjE0LDI4LjAzMiA2
|
||||
Ny4xNTQsMzYuMzc2YzUuOTcxLDMuNjggLTYuNDc3LDE2LjkxMyAtMTkuMjgzLDM3LjkyN2MtMy4z
|
||||
NjgsNS41NjcgLTEwLjQzNiw3LjYyOCAtMTYuMjY5LDQuNzQ2Yy0yMS42NTYsLTEwLjcxNiAtNDEu
|
||||
MDQ3LC0yMC45NTggLTU0LjIzNCwtMjguMDczYy00LjU5MiwtMi41MDEgLTcuOTM0LC02LjgwMiAt
|
||||
OS4yMjMsLTExLjg2OWMtMS4yODgsLTUuMDY4IC0wLjQwNywtMTAuNDQzIDIuNDMzLC0xNC44MzNj
|
||||
NC4zMzUsLTYuNzA4IDkuMTYyLC0xNC4xNyAxMi44ODQsLTE5LjkyM1oiIHN0eWxlPSJmaWxsOiM3
|
||||
YjQ3MjI7c3Ryb2tlOiMwMDA7c3Ryb2tlLXdpZHRoOjQuOTVweDsiLz48cGF0aCBkPSJNMjM5Ljg1
|
||||
LDM2MS4xNzFsLTI1Ljg0OCwyMy4yOTZsNzkuMTcsNDUuNjA1bDI1LjE5NiwtMjQuMjk1bC03OC41
|
||||
MTgsLTQ0LjYwNloiIHN0eWxlPSJmaWxsOiNhZDZjNGE7c3Ryb2tlOiMwMDA7c3Ryb2tlLXdpZHRo
|
||||
OjQuOTVweDsiLz48cGF0aCBkPSJNMjM4LjA4MSwzNzcuNzA0YzIuNTI2LDAuNTMgNC4xNDcsMy4w
|
||||
MTEgMy42MTgsNS41MzdjLTAuNTMsMi41MjYgLTMuMDExLDQuMTQ3IC01LjUzOCwzLjYxOGMtMi41
|
||||
MjYsLTAuNTMgLTQuMTQ3LC0zLjAxMSAtMy42MTcsLTUuNTM4YzAuNTMsLTIuNTI2IDMuMDExLC00
|
||||
LjE0NyA1LjUzNywtMy42MTdaIiBzdHlsZT0iZmlsbDojZmZiMjVjO3N0cm9rZTojMDAwO3N0cm9r
|
||||
ZS13aWR0aDo0Ljk1cHg7Ii8+PHBhdGggZD0iTTI5MC4zMyw0MDcuOWMyLjUyNywwLjUzIDQuMTQ3
|
||||
LDMuMDExIDMuNjE4LDUuNTM3Yy0wLjUzLDIuNTI3IC0zLjAxMSw0LjE0NyAtNS41MzcsMy42MThj
|
||||
LTIuNTI3LC0wLjUzIC00LjE0OCwtMy4wMTEgLTMuNjE4LC01LjUzN2MwLjUzLC0yLjUyNyAzLjAx
|
||||
MSwtNC4xNDggNS41MzcsLTMuNjE4WiIgc3R5bGU9ImZpbGw6I2ZmYjI1YztzdHJva2U6IzAwMDtz
|
||||
dHJva2Utd2lkdGg6NC45NXB4OyIvPjwvZz48Zz48cGF0aCBkPSJNMzA1LjMyMywzMzYuMjM1YzAs
|
||||
MCAyNC4yMDQsMTEyLjU3OCAzNS4zNTcsMTE0LjM1NWMxMS4xNTMsMS43NzYgMTgyLjk5OCwtNTcu
|
||||
MTYyIDE4Mi45OTgsLTU3LjE2MmMwLC0wIC0yNC4yNywtMTA1Ljc3MyAtMzQuNDU2LC0xMTQuNjAy
|
||||
Yy0xMC4xODYsLTguODMgLTE4My44OTksNTcuNDA5IC0xODMuODk5LDU3LjQwOVoiIHN0eWxlPSJm
|
||||
aWxsOiNmZmYyZWI7c3Ryb2tlOiMwMDA7c3Ryb2tlLXdpZHRoOjQuNjhweDsiLz48cGF0aCBkPSJN
|
||||
NDEzLjU2MywzNjcuNjU2bC0xMDguMjQsLTMxLjQyMWwxMDguMjQsMzEuNDIxWiIgc3R5bGU9ImZp
|
||||
bGw6bm9uZTtzdHJva2U6IzAwMDtzdHJva2Utd2lkdGg6NS40NnB4OyIvPjxwYXRoIGQ9Ik00ODgu
|
||||
NTY2LDI3OS4xNTZsLTc1LjAwMyw4OC41IiBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMDAwO3N0
|
||||
cm9rZS13aWR0aDo1LjQ2cHg7Ii8+PHBhdGggZD0iTTMzOS4xNjcsNDQ5LjEyN2w2Mi40MDEsLTgz
|
||||
LjUwNSIgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2U6IzAwMDtzdHJva2Utd2lkdGg6NS40NnB4OyIv
|
||||
PjxwYXRoIGQ9Ik00MjMuMjcsMzU1LjgzNWw5OS4xOTQsMzcuMDAxIiBzdHlsZT0iZmlsbDpub25l
|
||||
O3N0cm9rZTojMDAwO3N0cm9rZS13aWR0aDo1LjQ2cHg7Ii8+PC9nPjxwYXRoIGQ9Ik0zMDQuNDE2
|
||||
LDM3NC43ODVjLTAsMCAtMzAuNDYsLTIuOCAtMzguOTQzLC0xOS4xODVjLTguNDgyLC0xNi4zODQg
|
||||
My42MjIsLTI2Ljc5NCAxNC4zMzIsLTE3LjY0NmMxMC43MTEsOS4xNDkgMjEuNTcyLDE2LjgyMSAz
|
||||
Mi43NzQsMTUuMjc0YzExLjIwMSwtMS41NDcgMTEuMDQsMjMuNTExIC04LjE2MywyMS41NTdaIiBz
|
||||
dHlsZT0iZmlsbDojZDZjMjk1O3N0cm9rZTojMDAwO3N0cm9rZS13aWR0aDo2LjE2cHg7Ii8+PHBh
|
||||
dGggZD0iTTEwOS45NTEsMzExLjcxMmw2OC4wNzYsLTAiIHN0eWxlPSJmaWxsOm5vbmU7c3Ryb2tl
|
||||
OiMwMDA7c3Ryb2tlLXdpZHRoOjQuOTVweDsiLz48cGF0aCBkPSJNMTM5LjMwNSwzNTEuNjgzbDY4
|
||||
LjA3NiwwIiBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMDAwO3N0cm9rZS13aWR0aDo0Ljk1cHg7
|
||||
Ii8+PHBhdGggZD0iTTEzOS42MTcsMzMxLjY5OGwzOC4wOTgsLTAiIHN0eWxlPSJmaWxsOm5vbmU7
|
||||
c3Ryb2tlOiMwMDA7c3Ryb2tlLXdpZHRoOjQuOTVweDsiLz48cGF0aCBkPSJNODUuMjAxLDIxMS4y
|
||||
NTNjLTAsLTIuMjEzIC0xLjc5NywtNC4wMSAtNC4wMSwtNC4wMWwtNTMuNDg3LC0wYy0yLjIxNCwt
|
||||
MCAtNC4wMTEsMS43OTcgLTQuMDExLDQuMDFsMCw1OC44OTRjMCwyLjIxMyAxLjc5Nyw0LjAxIDQu
|
||||
MDExLDQuMDFsNTMuNDg3LDBjMi4yMTMsMCA0LjAxLC0xLjc5NyA0LjAxLC00LjAxbC0wLC01OC44
|
||||
OTRaIiBzdHlsZT0iZmlsbDojZWVhYzAxO3N0cm9rZTojMDAwO3N0cm9rZS13aWR0aDozLjQzcHg7
|
||||
Ii8+PHBhdGggZD0iTTU2NC42NDMsNDI2LjU5MWMwLC0yLjIxMyAtMS43OTYsLTQuMDEgLTQuMDEs
|
||||
LTQuMDFsLTUzLjQ4NywtMGMtMi4yMTMsLTAgLTQuMDEsMS43OTcgLTQuMDEsNC4wMWwwLDU4Ljg5
|
||||
NGMwLDIuMjEzIDEuNzk3LDQuMDEgNC4wMSw0LjAxbDUzLjQ4NywwYzIuMjE0LDAgNC4wMSwtMS43
|
||||
OTcgNC4wMSwtNC4wMWwwLC01OC44OTRaIiBzdHlsZT0iZmlsbDojZWVhYzAxO3N0cm9rZTojMDAw
|
||||
O3N0cm9rZS13aWR0aDozLjQzcHg7Ii8+PGNpcmNsZSBjeD0iNTE3LjAxNCIgY3k9IjEwOC43NzEi
|
||||
IHI9IjQuMzUyIiBzdHlsZT0iZmlsbDojZDNkMmIzOyIvPjxjaXJjbGUgY3g9IjUzMS4zMiIgY3k9
|
||||
IjEwOC43NzEiIHI9IjQuMzUyIiBzdHlsZT0iZmlsbDojZjRiMzAzOyIvPjxjaXJjbGUgY3g9IjU0
|
||||
NC43OTIiIGN5PSIxMDguNzcxIiByPSI0LjM1MiIgc3R5bGU9ImZpbGw6I2Y5NjEzZDsiLz48cGF0
|
||||
aCBkPSJNMTA1LjI1Nyw0ODQuNDNjLTAsMCAyMC4zNzQsLTI1LjIyNSA0MS43MTksLTcuNzYxYzIx
|
||||
LjM0NCwxNy40NjMgLTEyLjYxMyw1MC40NSAtNDAuNzQ5LDQyLjY4OSIgc3R5bGU9ImZpbGw6bm9u
|
||||
ZTtzdHJva2U6IzAwMDtzdHJva2Utd2lkdGg6OS45cHg7Ii8+PHBhdGggZD0iTTExMy4wMTksNDYw
|
||||
LjE0MWMtMCwtNC4yNjUgLTMuNDYzLC03LjcyNyAtNy43MjgsLTcuNzI3bC03OC42NTUsLTBjLTQu
|
||||
MjY1LC0wIC03LjcyNywzLjQ2MiAtNy43MjcsNy43MjdsLTAsNzguNjU1Yy0wLDQuMjY1IDMuNDYy
|
||||
LDcuNzI4IDcuNzI3LDcuNzI4bDc4LjY1NSwtMGM0LjI2NSwtMCA3LjcyOCwtMy40NjMgNy43Mjgs
|
||||
LTcuNzI4bC0wLC03OC42NTVaIiBzdHlsZT0iZmlsbDojNjFlMGVlO3N0cm9rZTojMDAwO3N0cm9r
|
||||
ZS13aWR0aDo0LjU1cHg7c3Ryb2tlLWxpbmVjYXA6c3F1YXJlOyIvPjxwYXRoIGQ9Ik03Ny4yNzcs
|
||||
Mzk1LjQwMmMtMi4yMzQsOC43NTUgLTYuNDk2LDE1LjM2NiAtMTEuNjQ5LDE3LjcxNGMxLjk5NSwz
|
||||
LjA1NyAzLjE2Nyw2Ljc4MSAzLjE2NywxMC43OTdjMCwxMC4zODcgLTcuODQsMTguODIgLTE3LjQ5
|
||||
OCwxOC44MmMtOS42NTcsMCAtMTcuNDk4LC04LjQzMyAtMTcuNDk4LC0xOC44MmMwLC05LjczMyA2
|
||||
Ljg4NCwtMTcuNzUxIDE1LjY5NywtMTguNzIyYy0zLjM5OSwtNS45NiAtNS41MjQsLTE0LjQyMyAt
|
||||
NS41MjQsLTIzLjgwNWMwLC0xOC4wMjQgNy44NDEsLTMyLjY1NyAxNy40OTksLTMyLjY1N2M2Ljc5
|
||||
MiwtMCAxMi42ODUsNy4yMzggMTUuNTg0LDE3LjgwMmMxLjYyOSwtMC41OTIgMy4zODcsLTAuOTE1
|
||||
IDUuMjIsLTAuOTE1YzguNDUsMCAxNS4zMTEsNi44NjEgMTUuMzExLDE1LjMxMWMtMCw4LjQ1IC02
|
||||
Ljg2MSwxNS4zMTEgLTE1LjMxMSwxNS4zMTFjLTEuNzUsMCAtMy40MzIsLTAuMjk0IC00Ljk5OCwt
|
||||
MC44MzZaIiBzdHlsZT0iZmlsbDojZmZmO2ZpbGwtb3BhY2l0eTowLjU7Ii8+PHBhdGggZD0iTTMz
|
||||
LjU4MywyMzMuOTNjMi41MjksLTAuMzM3IDQuNTEsLTEuMTc4IDYuNzE2LC0yLjQwM2MwLjk1Nywt
|
||||
MC41MzIgMS45NjksLTAuOTA0IDIuODk4LC0xLjQ4NWMwLjc4OSwtMC40OTMgMS4zNDMsLTAuOTc2
|
||||
IDEuMzQzLC0xLjk3OWMwLC0wLjIzNiAwLjIyNCwtMC42MzIgMCwtMC43MDdjLTAuOTUyLC0wLjMx
|
||||
NyAtMi4zNjQsMC40NjUgLTMuMDQsMC45OWMtMC42MjUsMC40ODYgLTEuNDk0LDAuNTc5IC0yLjEy
|
||||
LDEuMDZjLTEuMDM2LDAuNzk2IC0xLjY4MiwyLjQ2MyAtMS43NjcsMy43NDZjLTAuMDYsMC44OTYg
|
||||
LTAuMzU3LDIuODQgMC41NjUsMy4zOTNjMC4zNTcsMC4yMTQgMC45MjksMC4xNDIgMS4zNDMsMC4x
|
||||
NDJjMS4wOCwtMCAyLjEwNCwwLjAxMiAzLjE4MSwtMC4xNDJjMi4wODIsLTAuMjk3IDQuMTY1LC0x
|
||||
LjQ1NSA2LjAwOSwtMi40MDNjMi4wMTQsLTEuMDM2IDQuMDEyLC0xLjkyMSA1LjU4NCwtMy42MDVj
|
||||
MC42NDIsLTAuNjg4IDEuMDEsLTEuMjg2IDEuMjcyLC0yLjE5MWMwLjA1OSwtMC4yMDMgLTAuMDYs
|
||||
LTAuNzE4IDAuMDcxLC0wLjg0OWMwLjEwOCwtMC4xMDggMC4wOTksMC4yMjYgMC4wNzEsMC4yODNj
|
||||
LTAuMTg1LDAuMzY3IC0wLjQ2MiwwLjc0IC0wLjYzNywxLjEzMWMtMC4zMjQsMC43MjkgLTAuMzUz
|
||||
LDEuNzU5IC0wLjM1MywyLjU0NWMwLDAuMTMyIC0wLjA3LDAuNTcxIDAsMC42MzZjMC4wNTUsMC4w
|
||||
NTEgMC4zNjMsMC4xOTMgMC40MjQsMC4yMTJjMi4wMDIsMC42MzMgNC4zNDIsLTAuNTMyIDYuMDA5
|
||||
LC0xLjQ4NGMwLjY0MSwtMC4zNjcgMS4zNjMsLTAuODUgMi4wNiwtMS4xMTJjMC4wMjYsLTAuMDEg
|
||||
MC42NzgsLTAuMTk3IDAuNjk2LC0wLjE2MWMwLjI4MiwwLjU2MyAwLjExMSwxLjUxMSAwLjIxMiwy
|
||||
LjEyMWMwLjI4NSwxLjcwNyAyLjM2NywxLjYxNSAzLjY3NiwxLjQ4NGMwLjIwNywtMC4wMiAwLjM2
|
||||
MywtMC4yMTIgMC41NjYsLTAuMjEyIiBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMDAwO3N0cm9r
|
||||
ZS13aWR0aDoyLjc1cHg7Ii8+PHBhdGggZD0iTTM2LjQ4MiwyNDIuMzQyYzEuMzQ2LC0wIDIuNjgy
|
||||
LC0wLjE1NCA0LjAyOSwtMC4yMTJjNC4zMzIsLTAuMTg5IDguNjcxLC0wLjE5NiAxMy4wMDYsLTAu
|
||||
MjgzYzYuNTI1LC0wLjEzMSAxMy4wNTYsMC4wNzEgMTkuNTgxLDAuMDcxIiBzdHlsZT0iZmlsbDpu
|
||||
b25lO3N0cm9rZTojMDAwO3N0cm9rZS13aWR0aDoyLjc1cHg7Ii8+PHBhdGggZD0iTTUxNi4wOSw0
|
||||
NDIuMDMxYy0wLDAuOTM2IC0wLjUzMiwxLjg5MiAtMC4xMjYsMi44MzljMC41MjYsMS4yMjggMi4z
|
||||
NjMsMS4yNzUgMy40NjksMC45NDZjMC42MjgsLTAuMTg3IDEuMzE2LC0xLjE3NiAxLjcwMywtMS42
|
||||
NGMxLjAzOCwtMS4yNDcgMi40MzUsLTIuMzc1IDIuNzEyLC00LjAzN2MwLjA0MSwtMC4yNDggMC4x
|
||||
OTQsLTAuOTM2IDAuMDYzLC0xLjE5OGMtMC43NjMsLTEuNTI1IC0zLjA1OSwtMS40NzggLTQuMTY4
|
||||
LC0wLjM2OWMtMC4xNjQsMC4xNjQgLTAuNDQ2LDAuMjI3IC0wLjU2MywwLjQzMmMtMC45OTQsMS43
|
||||
NDUgLTAuNDM5LDQuMzQxIDAuNDQyLDUuOTkyYzAuMzA0LDAuNTY5IDAuODI5LDEuMDQ4IDEuMzI0
|
||||
LDEuNDUxYzAuMzM5LDAuMjc1IDAuNzc1LDAuNzE2IDEuMTk5LDAuOTI4YzMuMjYyLDEuNjMxIDcu
|
||||
Njg5LC0wLjEzMyAxMC41OTYsLTEuNzQ4YzEuOTUyLC0xLjA4NSA0LjEsLTIuMTUyIDQuMSwtNC42
|
||||
NjhjLTAsLTAuMzI1IDAuMDM3LC0wLjY5NCAtMC4wNjMsLTEuMDA5Yy0wLjM2MSwtMS4xMzMgLTIu
|
||||
MzA3LC0xLjA0MyAtMy4xNTQsLTAuNjk0Yy0xLjE3OSwwLjQ4NiAtMi4zNzMsMS4zOTUgLTMuMDks
|
||||
Mi40NmMtMS4xNzQsMS43NDIgLTEuMjA3LDQuMjgzIDAuNTY3LDUuNjE0YzEuMjU2LDAuOTQxIDIu
|
||||
OTU4LDAuOTIzIDQuNDQ3LDAuNzY2YzIuMjQ3LC0wLjIzNyA0LjI1MywtMS4zMTMgNi4wMjMsLTIu
|
||||
NjU5YzAuODc1LC0wLjY2NCAyLjA5NCwtMS4zMTYgMi42NDksLTIuMzMzYzAuMTczLC0wLjMxNiAw
|
||||
LjIxNSwtMC43NDUgMC4zNzksLTEuMDczYzAuMDcxLC0wLjE0MyAtMC4xNTgsMC4yODUgLTAuMTg5
|
||||
LDAuNDQyYy0wLjA1NiwwLjI3NiAtMC4wOTQsMC44NzggLTAsMS4xMzVjMC4zNzYsMS4wMjggMS4y
|
||||
MzMsMS41MDkgMi4yNywxLjcwM2MxLjM0NiwwLjI1MiAzLjAwOSwtMC4wMSA0LjM1MiwtMC4xODlj
|
||||
MC45MzksLTAuMTI1IDEuODkyLC0wLjUxNyAxLjg5MiwwLjc1NyIgc3R5bGU9ImZpbGw6bm9uZTtz
|
||||
dHJva2U6IzAwMDtzdHJva2Utd2lkdGg6Mi40NXB4OyIvPjxwYXRoIGQ9Ik01MTQuMjU5LDQ2MC4z
|
||||
NTljMC4yNjYsMC4xMzQgMC4wOCwxLjM1NCAwLjU2NywxLjM4OGMwLjQ0MiwwLjAzMSAwLjg4NSww
|
||||
LjA0IDEuMzI4LDAuMDVjMC41MTcsMC4wMTIgMS4xMDQsMC4wOTggMS42MzMsLTAuMDM0YzEuNjg4
|
||||
LC0wLjQyMiAzLjc5NiwtMS41OTggNC40ODIsLTMuMjk2YzAuMTg4LC0wLjQ2NyAwLjI0NiwtMS4w
|
||||
OTQgMC4zMTUsLTEuNTc3YzAuMDc1LC0wLjUyNCAwLjM3OCwtMS43MTggLTAuMDYzLC0yLjE0NGMt
|
||||
MC4wODgsLTAuMDg1IC0wLjYwNCwtMC4wNjMgLTAuNjk0LC0wLjA2M2MtMC42OTUsLTAgLTEuNzk2
|
||||
LDAuMzkyIC0yLjIwNywxLjAwOWMtMC4yNzMsMC40MDkgLTAuMTg5LDAuODUxIC0wLjE4OSwxLjMy
|
||||
NGMtMCwwLjYyIC0wLjAzMywxLjIyNCAwLjEyNiwxLjgzYzAuMzYyLDEuMzc5IDIuMTExLDEuOTQx
|
||||
IDMuMzQzLDIuMDE4YzIuNjk2LDAuMTY4IDUuNDQ3LC0wLjI0NSA4LjEzNiwtMGMyLjIwNywwLjIg
|
||||
NC40NTQsMC43MTcgNi42ODYsMC41MDRjMS41OCwtMC4xNSAzLjE1NCwtMC41MjUgNC43MywtMC42
|
||||
M2MzLjM5NCwtMC4yMjYgNi43OTEsLTAuNTA1IDEwLjIxOCwtMC41MDUiIHN0eWxlPSJmaWxsOm5v
|
||||
bmU7c3Ryb2tlOiMwMDA7c3Ryb2tlLXdpZHRoOjIuNDVweDsiLz48cGF0aCBkPSJNNTE5LjA1NCw0
|
||||
NzEuNjhsMCwwLjk0NmMwLDAuMTU1IC0wLDAuMjUxIDAuMDYzLDAuMzc5bDAsMC4xMjZjMC42MDIs
|
||||
LTIuNDA3IDMuNzc1LC0yLjc4NCA1LjgxLC0yLjc4NGMwLjE1LC0wIDAuMjMyLDAuNDggMC4yNDUs
|
||||
MC41NzZjMC4wNTUsMC4zODUgLTAuMTkyLDEuNDUgMC4zNzksMS42NGMwLjk4OSwwLjMzIDIuNDA2
|
||||
LC0wLjMxOCAzLjM0MywtMC41NjhjMC4yMzUsLTAuMDYyIDEuMDA5LC0wLjQ0MSAxLjE5OCwtMC4y
|
||||
NTJjMC45NTQsMC45NTQgMi40MTIsMC41NjIgMy43MjEsMC42MzFjMy4zMzIsMC4xNzUgNi42Mjks
|
||||
LTAuMTg5IDkuOTY2LC0wLjE4OWMxLjA1OCwtMCAyLjE2MiwwLjA4NyAzLjIxNywtMGMwLjA2OCwt
|
||||
MC4wMDYgMS4wMDksLTAuMDI3IDEuMDA5LC0wLjI1MyIgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2U6
|
||||
IzAwMDtzdHJva2Utd2lkdGg6Mi40NXB4OyIvPjwvZz48Zz48cGF0aCBkPSJNMzc0LjU3OCwxMTQu
|
||||
NDU1Yy0wLjYxMywtNC4yMTYgLTQuNTMzLC03LjE0MiAtOC43NDksLTYuNTNsLTk5Ljk0NSwxNC41
|
||||
MTVjLTQuMjE2LDAuNjEzIC03LjE0Miw0LjUzMyAtNi41Myw4Ljc0OWwyLjIxOSwxNS4yNzhjMC42
|
||||
MTIsNC4yMTYgNC41MzIsNy4xNDIgOC43NDksNi41M2w5OS45NDUsLTE0LjUxNWM0LjIxNiwtMC42
|
||||
MTIgNy4xNDIsLTQuNTMzIDYuNTI5LC04Ljc0OWwtMi4yMTgsLTE1LjI3OFoiIHN0eWxlPSJmaWxs
|
||||
OiM5ZGFjY2Q7c3Ryb2tlOiMwMDA7c3Ryb2tlLXdpZHRoOjUuMjFweDsiLz48cGF0aCBkPSJNMzEz
|
||||
Ljg2OSwxMTcuNDE0YzAsMCA2Ni4xMDIsOS44NzggNzcuNTgyLC0yMy43MzNjMTEuNDgsLTMzLjYx
|
||||
MSAtMTIuOTg3LC01MS44NzMgLTQxLjYzNywtMzkuOTIxYy0yOC42NSwxMS45NTIgLTU0Ljg1LDQ2
|
||||
LjE0MiAtNTQuODUsNDYuMTQyYzAsMCAtMzAuMDA1LC04LjEwOCAtNDEuMzE0LDUuMjIxYy0xMS4z
|
||||
MDksMTMuMzMgLTE3LjE2NywyNi42NDYgLTIuNDM1LDMxLjUxOWMxNC43MzIsNC44NzMgNjIuNjU0
|
||||
LC0xOS4yMjggNjIuNjU0LC0xOS4yMjhaIiBzdHlsZT0iZmlsbDojNzM1ZDkzO3N0cm9rZTojMDAw
|
||||
O3N0cm9rZS13aWR0aDo1LjkzcHg7Ii8+PHBhdGggZD0iTTM3NC4xNDgsMTM2Ljg3NmMtMCwwIDM4
|
||||
LjQ5MywtOS4zMzggMzYuNTYzLC0zLjU0OWMtMS45Myw1Ljc5IC0zNy42MzgsMjYuOTAxIC0xMTQu
|
||||
ODMxLDE1Ljk2NmM3MC4zNDUsLTEwLjczMSA3OC4yNjgsLTEyLjQxNyA3OC4yNjgsLTEyLjQxN1oi
|
||||
IHN0eWxlPSJmaWxsOiM1MDNhNzE7c3Ryb2tlOiMwMDA7c3Ryb2tlLXdpZHRoOjQuOTVweDsiLz48
|
||||
cGF0aCBkPSJNMzU3LjM0NiwxMjQuOTA2bC01NC4wMjEsOS42MTJsLTAsNS4zOTlsNTQuMDIxLC05
|
||||
LjYxMmwwLC01LjM5OVoiIHN0eWxlPSJmaWxsOiNmZmY7Ii8+PGNpcmNsZSBjeD0iMzAwLjE1IiBj
|
||||
eT0iMTM3LjE5OCIgcj0iNC42NzciIHN0eWxlPSJmaWxsOiNmZmIyNWM7c3Ryb2tlOiMwMDA7c3Ry
|
||||
b2tlLXdpZHRoOjQuOTVweDsiLz48Y2lyY2xlIGN4PSIzNjEuNDEiIGN5PSIxMjcuNTgxIiByPSI0
|
||||
LjI4MiIgc3R5bGU9ImZpbGw6I2ZmYjI1YztzdHJva2U6IzAwMDtzdHJva2Utd2lkdGg6NC45NXB4
|
||||
OyIvPjwvZz48L3N2Zz4=
|
||||
`
|
||||
|
||||
var (
|
||||
errClosedWriter = errors.New("writer is already closed")
|
||||
errMockDefault = errors.New("mock write error")
|
||||
errMockNewline = errors.New("mock newline error")
|
||||
errMockDefault = errors.New("mock write error")
|
||||
errMockNewline = errors.New("mock newline error")
|
||||
)
|
||||
|
||||
// TestBase64LineBreaker tests the Write and Close methods of the Base64LineBreaker
|
||||
func TestBase64LineBreaker(t *testing.T) {
|
||||
t.Run("write, copy and close", func(t *testing.T) {
|
||||
logoWriter := bytes.NewBuffer(nil)
|
||||
lineBreaker := &Base64LineBreaker{out: logoWriter}
|
||||
t.Cleanup(func() {
|
||||
if err := lineBreaker.Close(); err != nil {
|
||||
t.Errorf("failed to close line breaker: %s", err)
|
||||
}
|
||||
})
|
||||
if _, err := lineBreaker.Write([]byte("testdata")); err != nil {
|
||||
t.Errorf("failed to write to line breaker: %s", err)
|
||||
}
|
||||
})
|
||||
t.Run("write actual data and compare with expected results", func(t *testing.T) {
|
||||
logo, err := os.Open("testdata/logo.svg")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to open test data file: %s", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if err := logo.Close(); err != nil {
|
||||
t.Errorf("failed to close test data file: %s", err)
|
||||
}
|
||||
})
|
||||
l, err := os.Open("assets/gopher2.svg")
|
||||
if err != nil {
|
||||
t.Errorf("failed to open gopher logo asset: %s", err)
|
||||
return
|
||||
}
|
||||
defer func() { _ = l.Close() }()
|
||||
|
||||
logoWriter := bytes.NewBuffer(nil)
|
||||
lineBreaker := &Base64LineBreaker{out: logoWriter}
|
||||
t.Cleanup(func() {
|
||||
if err := lineBreaker.Close(); err != nil {
|
||||
t.Errorf("failed to close line breaker: %s", err)
|
||||
}
|
||||
})
|
||||
base64Encoder := base64.NewEncoder(base64.StdEncoding, lineBreaker)
|
||||
t.Cleanup(func() {
|
||||
if err := base64Encoder.Close(); err != nil {
|
||||
t.Errorf("failed to close base64 encoder: %s", err)
|
||||
}
|
||||
})
|
||||
copiedBytes, err := io.Copy(base64Encoder, logo)
|
||||
if err != nil {
|
||||
t.Errorf("failed to copy test data to line breaker: %s", err)
|
||||
}
|
||||
if err = base64Encoder.Close(); err != nil {
|
||||
t.Errorf("failed to close base64 encoder: %s", err)
|
||||
}
|
||||
if err = lineBreaker.Close(); err != nil {
|
||||
t.Errorf("failed to close line breaker: %s", err)
|
||||
}
|
||||
|
||||
logoStat, err := os.Stat("testdata/logo.svg")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to stat test data file: %s", err)
|
||||
}
|
||||
if logoStat.Size() != copiedBytes {
|
||||
t.Errorf("copied %d bytes, but expected %d bytes", copiedBytes, logoStat.Size())
|
||||
}
|
||||
|
||||
expectedRaw, err := os.ReadFile("testdata/logo.svg.base64")
|
||||
if err != nil {
|
||||
t.Errorf("failed to read expected base64 data from file: %s", err)
|
||||
}
|
||||
expected := removeNewLines(t, expectedRaw)
|
||||
got := removeNewLines(t, logoWriter.Bytes())
|
||||
if !bytes.EqualFold(expected, got) {
|
||||
t.Errorf("generated line breaker output differs from expected data")
|
||||
}
|
||||
})
|
||||
t.Run("fail with no writer defined", func(t *testing.T) {
|
||||
lineBreaker := &Base64LineBreaker{}
|
||||
_, err := lineBreaker.Write([]byte("testdata"))
|
||||
if err == nil {
|
||||
t.Errorf("writing to Base64LineBreaker with no output io.Writer was supposed to failed, but didn't")
|
||||
}
|
||||
if !errors.Is(err, ErrNoOutWriter) {
|
||||
t.Errorf("unexpected error while writing to empty Base64LineBreaker: %s", err)
|
||||
}
|
||||
if err := lineBreaker.Close(); err != nil {
|
||||
t.Errorf("failed to close Base64LineBreaker: %s", err)
|
||||
}
|
||||
})
|
||||
t.Run("write on an already closed output writer", func(t *testing.T) {
|
||||
logo, err := os.Open("testdata/logo.svg")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to open test data file: %s", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if err := logo.Close(); err != nil {
|
||||
t.Errorf("failed to close test data file: %s", err)
|
||||
}
|
||||
})
|
||||
|
||||
writeBuffer := &errorWriter{}
|
||||
lineBreaker := &Base64LineBreaker{out: writeBuffer}
|
||||
_, err = io.Copy(lineBreaker, logo)
|
||||
if err == nil {
|
||||
t.Errorf("writing to Base64LineBreaker with an already closed output io.Writer was " +
|
||||
"supposed to failed, but didn't")
|
||||
}
|
||||
if !errors.Is(err, errClosedWriter) {
|
||||
t.Errorf("unexpected error while writing to Base64LineBreaker: %s", err)
|
||||
}
|
||||
})
|
||||
t.Run("fail on different scenarios with mock writer", func(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
data []byte
|
||||
writer io.Writer
|
||||
}{
|
||||
{
|
||||
name: "write data within MaxBodyLength",
|
||||
data: []byte("testdata"),
|
||||
writer: &mockWriterExcess{writeError: errMockDefault},
|
||||
},
|
||||
{
|
||||
name: "write data exceeds MaxBodyLength",
|
||||
data: []byte("verylongtestdataverylongtestdataverylongtestdata" +
|
||||
"verylongtestdataverylongtestdataverylongtestdata"),
|
||||
writer: &mockWriterExcess{writeError: errMockDefault},
|
||||
},
|
||||
{
|
||||
name: "write data exceeds MaxBodyLength with newline",
|
||||
data: []byte("verylongtestdataverylongtestdataverylongtestdata" +
|
||||
"verylongtestdataverylongtestdataverylongtestdata"),
|
||||
writer: &mockWriterNewline{writeError: errMockDefault},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
lineBreaker := &Base64LineBreaker{out: tt.writer}
|
||||
|
||||
_, err := lineBreaker.Write(tt.data)
|
||||
if err != nil && !errors.Is(err, errMockDefault) && !errors.Is(err, errMockNewline) {
|
||||
t.Errorf("unexpected error while writing to mock writer: %s", err)
|
||||
}
|
||||
err = lineBreaker.Close()
|
||||
if err != nil && !errors.Is(err, errMockDefault) && !errors.Is(err, errMockNewline) {
|
||||
t.Errorf("unexpected error while closing mock writer: %s", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
var wbuf bytes.Buffer
|
||||
lb := Base64LineBreaker{out: &wbuf}
|
||||
bw := base64.NewEncoder(base64.StdEncoding, &lb)
|
||||
if _, err := io.Copy(bw, l); err != nil {
|
||||
t.Errorf("failed to write logo asset to line breaker: %s", err)
|
||||
return
|
||||
}
|
||||
if err := bw.Close(); err != nil {
|
||||
t.Errorf("failed to close b64 encoder: %s", err)
|
||||
}
|
||||
if err := lb.Close(); err != nil {
|
||||
t.Errorf("failed to close line breaker: %s", err)
|
||||
}
|
||||
ob := removeNewLines([]byte(logoB64))
|
||||
nb := removeNewLines(wbuf.Bytes())
|
||||
if string(ob) != string(nb) {
|
||||
t.Errorf("generated line breaker output differs from original data")
|
||||
}
|
||||
}
|
||||
|
||||
// removeNewLines is a test helper thatremoves all newline characters ('\r' and '\n') from the given byte slice.
|
||||
func removeNewLines(t *testing.T, data []byte) []byte {
|
||||
t.Helper()
|
||||
// TestBase64LineBreakerFailures tests the cases in which the Base64LineBreaker would fail
|
||||
func TestBase64LineBreakerFailures(t *testing.T) {
|
||||
stt := []byte("short")
|
||||
ltt := []byte(logoB64)
|
||||
|
||||
// No output writer defined
|
||||
lb := Base64LineBreaker{}
|
||||
if _, err := lb.Write(stt); err == nil {
|
||||
t.Errorf("writing to Base64LineBreaker with no output io.Writer was supposed to failed, but didn't")
|
||||
}
|
||||
if err := lb.Close(); err != nil {
|
||||
t.Errorf("failed to close Base64LineBreaker: %s", err)
|
||||
}
|
||||
|
||||
// Closed output writer
|
||||
wbuf := errorWriter{}
|
||||
fb := Base64LineBreaker{out: wbuf}
|
||||
if _, err := fb.Write(ltt); err == nil {
|
||||
t.Errorf("writing to Base64LineBreaker with errorWriter was supposed to failed, but didn't")
|
||||
}
|
||||
if err := fb.Close(); err != nil {
|
||||
t.Errorf("failed to close Base64LineBreaker: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBase64LineBreaker_WriteAndClose(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
data []byte
|
||||
writer io.Writer
|
||||
}{
|
||||
{
|
||||
name: "Write data within MaxBodyLength",
|
||||
data: []byte("testdata"),
|
||||
writer: &mockWriterExcess{writeError: errMockDefault},
|
||||
},
|
||||
{
|
||||
name: "Write data exceeds MaxBodyLength",
|
||||
data: []byte("verylongtestdataverylongtestdataverylongtestdata" +
|
||||
"verylongtestdataverylongtestdataverylongtestdata"),
|
||||
writer: &mockWriterExcess{writeError: errMockDefault},
|
||||
},
|
||||
{
|
||||
name: "Write data exceeds MaxBodyLength with newline",
|
||||
data: []byte("verylongtestdataverylongtestdataverylongtestdata" +
|
||||
"verylongtestdataverylongtestdataverylongtestdata"),
|
||||
writer: &mockWriterNewline{writeError: errMockDefault},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
blr := &Base64LineBreaker{out: tt.writer}
|
||||
|
||||
_, err := blr.Write(tt.data)
|
||||
if err != nil && !errors.Is(err, errMockDefault) && !errors.Is(err, errMockNewline) {
|
||||
t.Errorf("Unexpected error while writing: %v", err)
|
||||
}
|
||||
err = blr.Close()
|
||||
if err != nil && !errors.Is(err, errMockDefault) && !errors.Is(err, errMockNewline) {
|
||||
t.Errorf("Unexpected error while closing: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// removeNewLines removes any newline characters from the given data
|
||||
func removeNewLines(data []byte) []byte {
|
||||
result := make([]byte, len(data))
|
||||
n := 0
|
||||
|
||||
|
@ -181,11 +503,11 @@ func removeNewLines(t *testing.T, data []byte) []byte {
|
|||
type errorWriter struct{}
|
||||
|
||||
func (e errorWriter) Write([]byte) (int, error) {
|
||||
return 0, errClosedWriter
|
||||
return 0, fmt.Errorf("supposed to always fail")
|
||||
}
|
||||
|
||||
func (e errorWriter) Close() error {
|
||||
return errClosedWriter
|
||||
return fmt.Errorf("supposed to always fail")
|
||||
}
|
||||
|
||||
type mockWriterExcess struct {
|
||||
|
@ -217,49 +539,19 @@ func (w *mockWriterNewline) Write(p []byte) (n int, err error) {
|
|||
}
|
||||
}
|
||||
|
||||
func FuzzBase64LineBreaker(f *testing.F) {
|
||||
seedData := [][]byte{
|
||||
[]byte(""),
|
||||
[]byte("abc"),
|
||||
[]byte("def"),
|
||||
[]byte("Hello, World!"),
|
||||
[]byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!§$%&/()=?`{[]}\\|^~*+#-._'"),
|
||||
[]byte("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"),
|
||||
bytes.Repeat([]byte("A"), MaxBodyLength-1), // Near the line length limit
|
||||
bytes.Repeat([]byte("A"), MaxBodyLength), // Exactly the line length limit
|
||||
bytes.Repeat([]byte("A"), MaxBodyLength+1), // Slightly above the line length limit
|
||||
bytes.Repeat([]byte("A"), MaxBodyLength*3), // Tripple exceed the line length limit
|
||||
bytes.Repeat([]byte("A"), MaxBodyLength*10), // Tenfold exceed the line length limit
|
||||
{0o0, 0o1, 0o2, 30, 255},
|
||||
}
|
||||
for _, data := range seedData {
|
||||
f.Add(data)
|
||||
}
|
||||
|
||||
func FuzzBase64LineBreaker_Write(f *testing.F) {
|
||||
f.Add([]byte("abc"))
|
||||
f.Add([]byte("def"))
|
||||
f.Add([]uint8{0o0, 0o1, 0o2, 30, 255})
|
||||
buf := bytes.Buffer{}
|
||||
bw := bufio.NewWriter(&buf)
|
||||
f.Fuzz(func(t *testing.T, data []byte) {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
lineBreaker := &Base64LineBreaker{
|
||||
out: buffer,
|
||||
b := &Base64LineBreaker{out: bw}
|
||||
if _, err := b.Write(data); err != nil {
|
||||
t.Errorf("failed to write to B64LineBreaker: %s", err)
|
||||
}
|
||||
base64Encoder := base64.NewEncoder(base64.StdEncoding, lineBreaker)
|
||||
|
||||
_, err := base64Encoder.Write(data)
|
||||
if err != nil {
|
||||
t.Errorf("failed to write test data to base64 encoder: %s", err)
|
||||
}
|
||||
if err = base64Encoder.Close(); err != nil {
|
||||
t.Errorf("failed to close base64 encoder: %s", err)
|
||||
}
|
||||
if err = lineBreaker.Close(); err != nil {
|
||||
t.Errorf("failed to close base64 line breaker: %s", err)
|
||||
}
|
||||
|
||||
decode, err := base64.StdEncoding.DecodeString(buffer.String())
|
||||
if err != nil {
|
||||
t.Errorf("failed to decode base64 data: %s", err)
|
||||
}
|
||||
if !bytes.Equal(data, decode) {
|
||||
t.Error("generated line breaker output differs from original data")
|
||||
if err := b.Close(); err != nil {
|
||||
t.Errorf("failed to close B64LineBreaker: %s", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
113
client.go
113
client.go
|
@ -145,9 +145,6 @@ type (
|
|||
// isEncrypted indicates wether the Client connection is encrypted or not.
|
||||
isEncrypted bool
|
||||
|
||||
// logAuthData indicates whether authentication-related data should be logged.
|
||||
logAuthData bool
|
||||
|
||||
// logger is a logger that satisfies the log.Logger interface.
|
||||
logger log.Logger
|
||||
|
||||
|
@ -242,12 +239,6 @@ var (
|
|||
// provided as argument to the WithDSN Option.
|
||||
ErrInvalidDSNRcptNotifyCombination = errors.New("DSN rcpt notify option NEVER cannot be " +
|
||||
"combined with any of SUCCESS, FAILURE or DELAY")
|
||||
|
||||
// ErrSMTPAuthMethodIsNil indicates that the SMTP authentication method provided is nil
|
||||
ErrSMTPAuthMethodIsNil = errors.New("SMTP auth method is nil")
|
||||
|
||||
// ErrDialContextFuncIsNil indicates that a required dial context function is not provided.
|
||||
ErrDialContextFuncIsNil = errors.New("dial context function is nil")
|
||||
)
|
||||
|
||||
// NewClient creates a new Client instance with the provided host and optional configuration Option functions.
|
||||
|
@ -265,12 +256,11 @@ var (
|
|||
// - An error if any critical default values are missing or options fail to apply.
|
||||
func NewClient(host string, opts ...Option) (*Client, error) {
|
||||
c := &Client{
|
||||
smtpAuthType: SMTPAuthNoAuth,
|
||||
connTimeout: DefaultTimeout,
|
||||
host: host,
|
||||
port: DefaultPort,
|
||||
tlsconfig: &tls.Config{ServerName: host, MinVersion: DefaultTLSMinVersion},
|
||||
tlspolicy: DefaultTLSPolicy,
|
||||
connTimeout: DefaultTimeout,
|
||||
host: host,
|
||||
port: DefaultPort,
|
||||
tlsconfig: &tls.Config{ServerName: host, MinVersion: DefaultTLSMinVersion},
|
||||
tlspolicy: DefaultTLSPolicy,
|
||||
}
|
||||
|
||||
// Set default HELO/EHLO hostname
|
||||
|
@ -374,10 +364,9 @@ func WithSSLPort(fallback bool) Option {
|
|||
// WithDebugLog enables debug logging for the Client.
|
||||
//
|
||||
// This function activates debug logging, which logs incoming and outgoing communication between the
|
||||
// Client and the SMTP server to os.Stderr. By default the debug logging will redact any kind of SMTP
|
||||
// authentication data. If you need access to the actual authentication data in your logs, you can
|
||||
// enable authentication data logging with the WithLogAuthData option or by setting it with the
|
||||
// Client.SetLogAuthData method.
|
||||
// Client and the SMTP server to os.Stderr. Be cautious when using this option, as the logs may include
|
||||
// unencrypted authentication data, depending on the SMTP authentication method in use, which could
|
||||
// pose a data protection risk.
|
||||
//
|
||||
// Returns:
|
||||
// - An Option function that enables debug logging for the Client.
|
||||
|
@ -516,9 +505,6 @@ func WithSMTPAuth(authtype SMTPAuthType) Option {
|
|||
// - An Option function that sets the custom SMTP authentication for the Client.
|
||||
func WithSMTPAuthCustom(smtpAuth smtp.Auth) Option {
|
||||
return func(c *Client) error {
|
||||
if smtpAuth == nil {
|
||||
return ErrSMTPAuthMethodIsNil
|
||||
}
|
||||
c.smtpAuth = smtpAuth
|
||||
c.smtpAuthType = SMTPAuthCustom
|
||||
return nil
|
||||
|
@ -680,30 +666,11 @@ func WithoutNoop() Option {
|
|||
// - An Option function that sets the custom DialContextFunc for the Client.
|
||||
func WithDialContextFunc(dialCtxFunc DialContextFunc) Option {
|
||||
return func(c *Client) error {
|
||||
if dialCtxFunc == nil {
|
||||
return ErrDialContextFuncIsNil
|
||||
}
|
||||
c.dialContextFunc = dialCtxFunc
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithLogAuthData enables logging of authentication data.
|
||||
//
|
||||
// This function sets the logAuthData field of the Client to true, enabling the logging of authentication data.
|
||||
//
|
||||
// Be cautious when using this option, as the logs may include unencrypted authentication data, depending on
|
||||
// the SMTP authentication method in use, which could pose a data protection risk.
|
||||
//
|
||||
// Returns:
|
||||
// - An Option function that configures the Client to enable authentication data logging.
|
||||
func WithLogAuthData() Option {
|
||||
return func(c *Client) error {
|
||||
c.logAuthData = true
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// TLSPolicy returns the TLSPolicy that is currently set on the Client as a string.
|
||||
//
|
||||
// This method retrieves the current TLSPolicy configured for the Client and returns it as a string representation.
|
||||
|
@ -751,7 +718,6 @@ func (c *Client) SetTLSPolicy(policy TLSPolicy) {
|
|||
func (c *Client) SetTLSPortPolicy(policy TLSPolicy) {
|
||||
if c.port == DefaultPort {
|
||||
c.port = DefaultPortTLS
|
||||
c.fallbackPort = 0
|
||||
|
||||
if policy == TLSOpportunistic {
|
||||
c.fallbackPort = DefaultPort
|
||||
|
@ -899,19 +865,6 @@ func (c *Client) SetSMTPAuthCustom(smtpAuth smtp.Auth) {
|
|||
c.smtpAuthType = SMTPAuthCustom
|
||||
}
|
||||
|
||||
// SetLogAuthData sets or overrides the logging of SMTP authentication data for the Client.
|
||||
//
|
||||
// This function sets the logAuthData field of the Client to true, enabling the logging of authentication data.
|
||||
//
|
||||
// Be cautious when using this option, as the logs may include unencrypted authentication data, depending on
|
||||
// the SMTP authentication method in use, which could pose a data protection risk.
|
||||
//
|
||||
// Parameters:
|
||||
// - logAuth: Set wether or not to log SMTP authentication data for the Client.
|
||||
func (c *Client) SetLogAuthData(logAuth bool) {
|
||||
c.logAuthData = logAuth
|
||||
}
|
||||
|
||||
// DialWithContext establishes a connection to the server using the provided context.Context.
|
||||
//
|
||||
// This function adds a deadline based on the Client's timeout to the provided context.Context
|
||||
|
@ -968,9 +921,6 @@ func (c *Client) DialWithContext(dialCtx context.Context) error {
|
|||
if c.useDebugLog {
|
||||
c.smtpClient.SetDebugLog(true)
|
||||
}
|
||||
if c.logAuthData {
|
||||
c.smtpClient.SetLogAuthData()
|
||||
}
|
||||
if err = c.smtpClient.Hello(c.helo); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -1078,23 +1028,19 @@ func (c *Client) DialAndSendWithContext(ctx context.Context, messages ...*Msg) e
|
|||
// determines the supported authentication methods, and applies the appropriate authentication
|
||||
// type. An error is returned if authentication fails.
|
||||
//
|
||||
// By default NewClient sets the SMTP authentication type to SMTPAuthNoAuth, meaning, that no
|
||||
// SMTP authentication will be performed. If the user makes use of SetSMTPAuth or initialzes the
|
||||
// client with WithSMTPAuth, the SMTP authentication type will be set in the Client, forcing
|
||||
// this method to determine if the server supports the selected authentication method and
|
||||
// assigning the corresponding smtp.Auth function to it.
|
||||
//
|
||||
// If the user set a custom SMTP authentication function using SetSMTPAuthCustom or
|
||||
// WithSMTPAuthCustom, we will not perform any detection and assignment logic and will trust
|
||||
// the user with their provided smtp.Auth function.
|
||||
//
|
||||
// This method first verifies the connection to the SMTP server. If no custom authentication
|
||||
// mechanism is provided, it checks which authentication methods are supported by the server.
|
||||
// Based on the configured SMTPAuthType, it sets up the appropriate authentication mechanism.
|
||||
// Finally, it attempts to authenticate the client using the selected method.
|
||||
//
|
||||
// Returns:
|
||||
// - An error if the connection check fails, if no supported authentication method is found,
|
||||
// or if the authentication process fails.
|
||||
func (c *Client) auth() error {
|
||||
if c.smtpAuth == nil && c.smtpAuthType != SMTPAuthNoAuth {
|
||||
if err := c.checkConn(); err != nil {
|
||||
return fmt.Errorf("failed to authenticate: %w", err)
|
||||
}
|
||||
if c.smtpAuth == nil && c.smtpAuthType != SMTPAuthCustom {
|
||||
hasSMTPAuth, smtpAuthType := c.smtpClient.Extension("AUTH")
|
||||
if !hasSMTPAuth {
|
||||
return fmt.Errorf("server does not support SMTP AUTH")
|
||||
|
@ -1105,22 +1051,12 @@ func (c *Client) auth() error {
|
|||
if !strings.Contains(smtpAuthType, string(SMTPAuthPlain)) {
|
||||
return ErrPlainAuthNotSupported
|
||||
}
|
||||
c.smtpAuth = smtp.PlainAuth("", c.user, c.pass, c.host, false)
|
||||
case SMTPAuthPlainNoEnc:
|
||||
if !strings.Contains(smtpAuthType, string(SMTPAuthPlain)) {
|
||||
return ErrPlainAuthNotSupported
|
||||
}
|
||||
c.smtpAuth = smtp.PlainAuth("", c.user, c.pass, c.host, true)
|
||||
c.smtpAuth = smtp.PlainAuth("", c.user, c.pass, c.host)
|
||||
case SMTPAuthLogin:
|
||||
if !strings.Contains(smtpAuthType, string(SMTPAuthLogin)) {
|
||||
return ErrLoginAuthNotSupported
|
||||
}
|
||||
c.smtpAuth = smtp.LoginAuth(c.user, c.pass, c.host, false)
|
||||
case SMTPAuthLoginNoEnc:
|
||||
if !strings.Contains(smtpAuthType, string(SMTPAuthLogin)) {
|
||||
return ErrLoginAuthNotSupported
|
||||
}
|
||||
c.smtpAuth = smtp.LoginAuth(c.user, c.pass, c.host, true)
|
||||
c.smtpAuth = smtp.LoginAuth(c.user, c.pass, c.host)
|
||||
case SMTPAuthCramMD5:
|
||||
if !strings.Contains(smtpAuthType, string(SMTPAuthCramMD5)) {
|
||||
return ErrCramMD5AuthNotSupported
|
||||
|
@ -1261,13 +1197,14 @@ func (c *Client) sendSingleMsg(message *Msg) error {
|
|||
affectedMsg: message,
|
||||
}
|
||||
}
|
||||
message.isDelivered = true
|
||||
|
||||
if err = writer.Close(); err != nil {
|
||||
return &SendError{
|
||||
Reason: ErrSMTPDataClose, errlist: []error{err}, isTemp: isTempError(err),
|
||||
affectedMsg: message,
|
||||
}
|
||||
}
|
||||
message.isDelivered = true
|
||||
|
||||
if err = c.Reset(); err != nil {
|
||||
return &SendError{
|
||||
|
@ -1275,6 +1212,12 @@ func (c *Client) sendSingleMsg(message *Msg) error {
|
|||
affectedMsg: message,
|
||||
}
|
||||
}
|
||||
if err = c.checkConn(); err != nil {
|
||||
return &SendError{
|
||||
Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err),
|
||||
affectedMsg: message,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -1291,9 +1234,6 @@ func (c *Client) sendSingleMsg(message *Msg) error {
|
|||
// - An error if there is no active connection, if the NOOP command fails, or if extending
|
||||
// the deadline fails; otherwise, returns nil.
|
||||
func (c *Client) checkConn() error {
|
||||
if c.smtpClient == nil {
|
||||
return ErrNoActiveConnection
|
||||
}
|
||||
if !c.smtpClient.HasConnection() {
|
||||
return ErrNoActiveConnection
|
||||
}
|
||||
|
@ -1352,6 +1292,9 @@ func (c *Client) setDefaultHelo() error {
|
|||
// - An error if there is no active connection, if STARTTLS is required but not supported,
|
||||
// or if there are issues during the TLS handshake; otherwise, returns nil.
|
||||
func (c *Client) tls() error {
|
||||
if !c.smtpClient.HasConnection() {
|
||||
return ErrNoActiveConnection
|
||||
}
|
||||
if !c.useSSL && c.tlspolicy != NoTLS {
|
||||
hasStartTLS := false
|
||||
extension, _ := c.smtpClient.Extension("STARTTLS")
|
||||
|
|
|
@ -1,126 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2024 The go-mail Authors
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build go1.21
|
||||
// +build go1.21
|
||||
|
||||
package mail
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/wneessen/go-mail/log"
|
||||
)
|
||||
|
||||
func TestNewClientNewVersionsOnly(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
option Option
|
||||
expectFunc func(c *Client) error
|
||||
shouldfail bool
|
||||
expectErr *error
|
||||
}{
|
||||
{
|
||||
"WithLogger log.JSONlog", WithLogger(log.NewJSON(os.Stderr, log.LevelDebug)),
|
||||
func(c *Client) error {
|
||||
if c.logger == nil {
|
||||
return errors.New("failed to set logger. Want logger bug got got nil")
|
||||
}
|
||||
loggerType := reflect.TypeOf(c.logger).String()
|
||||
if loggerType != "*log.JSONlog" {
|
||||
return fmt.Errorf("failed to set logger. Want logger type: %s, got: %s",
|
||||
"*log.JSONlog", loggerType)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
false, nil,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
client, err := NewClient(DefaultHost, tt.option)
|
||||
if !tt.shouldfail && err != nil {
|
||||
t.Fatalf("failed to create new client: %s", err)
|
||||
}
|
||||
if tt.shouldfail && err == nil {
|
||||
t.Errorf("client creation was supposed to fail, but it didn't")
|
||||
}
|
||||
if tt.shouldfail && tt.expectErr != nil {
|
||||
if !errors.Is(err, *tt.expectErr) {
|
||||
t.Errorf("error for NewClient mismatch. Expected: %s, got: %s",
|
||||
*tt.expectErr, err)
|
||||
}
|
||||
}
|
||||
if tt.expectFunc != nil {
|
||||
if err = tt.expectFunc(client); err != nil {
|
||||
t.Errorf("NewClient with custom option failed: %s", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_DialWithContextNewVersionsOnly(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
PortAdder.Add(1)
|
||||
serverPort := int(TestServerPortBase + PortAdder.Load())
|
||||
featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"
|
||||
go func() {
|
||||
if err := simpleSMTPServer(ctx, t, &serverProps{FeatureSet: featureSet, ListenPort: serverPort}); err != nil {
|
||||
t.Errorf("failed to start test server: %s", err)
|
||||
return
|
||||
}
|
||||
}()
|
||||
time.Sleep(time.Millisecond * 30)
|
||||
t.Run("connect with full debug logging and auth logging", func(t *testing.T) {
|
||||
ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500)
|
||||
t.Cleanup(cancelDial)
|
||||
|
||||
logBuffer := bytes.NewBuffer(nil)
|
||||
client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS),
|
||||
WithDebugLog(), WithLogAuthData(), WithLogger(log.NewJSON(logBuffer, log.LevelDebug)),
|
||||
WithSMTPAuth(SMTPAuthPlain), WithUsername("test"), WithPassword("password"))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create new client: %s", err)
|
||||
}
|
||||
|
||||
if err = client.DialWithContext(ctxDial); err != nil {
|
||||
var netErr net.Error
|
||||
if errors.As(err, &netErr) && netErr.Timeout() {
|
||||
t.Skip("failed to connect to the test server due to timeout")
|
||||
}
|
||||
t.Fatalf("failed to connect to the test server: %s", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if err = client.Close(); err != nil {
|
||||
t.Errorf("failed to close the client: %s", err)
|
||||
}
|
||||
})
|
||||
|
||||
logs := parseJSONLog(t, logBuffer)
|
||||
if len(logs.Lines) == 0 {
|
||||
t.Errorf("failed to enable debug logging, but no logs were found")
|
||||
}
|
||||
authFound := false
|
||||
for _, logline := range logs.Lines {
|
||||
if strings.EqualFold(logline.Message, "AUTH PLAIN AHRlc3QAcGFzc3dvcmQ=") &&
|
||||
logline.Direction.From == "client" && logline.Direction.To == "server" {
|
||||
authFound = true
|
||||
}
|
||||
}
|
||||
if !authFound {
|
||||
t.Errorf("logAuthData not working, no authentication info found in logs")
|
||||
}
|
||||
})
|
||||
}
|
6112
client_test.go
6112
client_test.go
File diff suppressed because it is too large
Load diff
|
@ -6,17 +6,17 @@ coverage:
|
|||
status:
|
||||
project:
|
||||
default:
|
||||
target: 90%
|
||||
threshold: 2%
|
||||
target: 85%
|
||||
threshold: 5%
|
||||
base: auto
|
||||
if_ci_failed: error
|
||||
only_pulls: false
|
||||
patch:
|
||||
default:
|
||||
target: 90%
|
||||
target: 80%
|
||||
base: auto
|
||||
if_ci_failed: error
|
||||
threshold: 2%
|
||||
threshold: 5%
|
||||
|
||||
comment:
|
||||
require_changes: true
|
||||
|
|
2
doc.go
2
doc.go
|
@ -11,4 +11,4 @@ package mail
|
|||
|
||||
// VERSION indicates the current version of the package. It is also attached to the default user
|
||||
// agent string.
|
||||
const VERSION = "0.5.1"
|
||||
const VERSION = "0.5.0"
|
||||
|
|
14
eml.go
14
eml.go
|
@ -60,7 +60,7 @@ func EMLToMsgFromReader(reader io.Reader) (*Msg, error) {
|
|||
return msg, fmt.Errorf("failed to parse EML from reader: %w", err)
|
||||
}
|
||||
|
||||
if err = parseEML(parsedMsg, bodybuf, msg); err != nil {
|
||||
if err := parseEML(parsedMsg, bodybuf, msg); err != nil {
|
||||
return msg, fmt.Errorf("failed to parse EML contents: %w", err)
|
||||
}
|
||||
|
||||
|
@ -93,7 +93,7 @@ func EMLToMsgFromFile(filePath string) (*Msg, error) {
|
|||
return msg, fmt.Errorf("failed to parse EML file: %w", err)
|
||||
}
|
||||
|
||||
if err = parseEML(parsedMsg, bodybuf, msg); err != nil {
|
||||
if err := parseEML(parsedMsg, bodybuf, msg); err != nil {
|
||||
return msg, fmt.Errorf("failed to parse EML contents: %w", err)
|
||||
}
|
||||
|
||||
|
@ -218,9 +218,9 @@ func parseEMLHeaders(mailHeader *netmail.Header, msg *Msg) error {
|
|||
for _, addr := range parsedAddrs {
|
||||
addrStrings = append(addrStrings, addr.String())
|
||||
}
|
||||
// We can skip the error checking here since netmail.ParseAddressList already performed the
|
||||
// same address checking that the msg methods do.
|
||||
_ = addrFunc(addrStrings...)
|
||||
if err = addrFunc(addrStrings...); err != nil {
|
||||
return fmt.Errorf(`failed to parse %q header: %w`, addrHeader, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -383,7 +383,7 @@ ReadNextPart:
|
|||
return fmt.Errorf("failed to get next part of multipart message: %w", err)
|
||||
}
|
||||
for err == nil {
|
||||
// Multipart/related and Multipart/alternative parts need to be parsed separately
|
||||
// Multipart/related and Multipart/alternative parts need to be parsed seperately
|
||||
if contentTypeSlice, ok := multiPart.Header[HeaderContentType.String()]; ok && len(contentTypeSlice) == 1 {
|
||||
contentType, _ := parseMultiPartHeader(contentTypeSlice[0])
|
||||
if strings.EqualFold(contentType, TypeMultipartRelated.String()) ||
|
||||
|
@ -600,8 +600,6 @@ func parseEMLAttachmentEmbed(contentDisposition []string, multiPart *multipart.P
|
|||
if err := msg.EmbedReader(filename, dataReader); err != nil {
|
||||
return fmt.Errorf("failed to embed multipart body: %w", err)
|
||||
}
|
||||
default:
|
||||
return errors.New("unsupported content disposition type")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
588
eml_test.go
588
eml_test.go
|
@ -6,8 +6,11 @@ package mail
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -19,23 +22,6 @@ 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".`
|
||||
exampleMailRFC5322A11InvalidFrom = `From: §§§§§§§§§
|
||||
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".`
|
||||
exampleMailInvalidHeader = `From: John Doe <jdoe@machine.example>
|
||||
To: Mary Smith <mary@example.net>
|
||||
Inva@id*Header; This is a header
|
||||
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
|
||||
|
@ -56,52 +42,6 @@ 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`
|
||||
exampleMailPlainInvalidCTE = `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: invalid
|
||||
|
||||
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
|
||||
|
||||
--
|
||||
This is a signature`
|
||||
exampleMailInvalidContentType = `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; $foo; bar; --invalid--
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
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
|
||||
|
||||
|
@ -364,128 +304,6 @@ VGhpcyBpcyBhIHNpbXBsZSB0ZXN0IHRleHQgZmlsZSBhdHRhY2htZW50LgoKSXQgCiAgaGFzCiAg
|
|||
ICAgc2V2ZXJhbAogICAgICAgICAgICBuZXdsaW5lcwoJICAgICAgICAgICAgYW5kCgkgICAgc3Bh
|
||||
Y2VzCiAgICAgaW4KICBpdAouCgpBcyB3ZWxsIGFzIGFuIGVtb2ppOiDwn5mCCg==
|
||||
|
||||
--45c75ff528359022eb03679fbe91877d75343f2e1f8193e349deffa33ff7--`
|
||||
exampleMailPlainB64WithAttachmentNoContentType = `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 base64 with 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=45c75ff528359022eb03679fbe91877d75343f2e1f8193e349deffa33ff7
|
||||
|
||||
--45c75ff528359022eb03679fbe91877d75343f2e1f8193e349deffa33ff7
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
RGVhciBDdXN0b21lciwKClRoaXMgaXMgYSB0ZXN0IG1haWwuIFBsZWFzZSBkbyBub3QgcmVwbHkg
|
||||
dG8gdGhpcy4gQWxzbyB0aGlzIGxpbmUgaXMgdmVyeSBsb25nIHNvIGl0CnNob3VsZCBiZSB3cmFw
|
||||
cGVkLgoKClRoYW5rIHlvdXIgZm9yIHlvdXIgYnVzaW5lc3MhClRoZSBnby1tYWlsIHRlYW0KCi0t
|
||||
ClRoaXMgaXMgYSBzaWduYXR1cmU=
|
||||
|
||||
--45c75ff528359022eb03679fbe91877d75343f2e1f8193e349deffa33ff7
|
||||
Content-Disposition: attachment; filename="test.attachment"
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
VGhpcyBpcyBhIHNpbXBsZSB0ZXN0IHRleHQgZmlsZSBhdHRhY2htZW50LgoKSXQgCiAgaGFzCiAg
|
||||
ICAgc2V2ZXJhbAogICAgICAgICAgICBuZXdsaW5lcwoJICAgICAgICAgICAgYW5kCgkgICAgc3Bh
|
||||
Y2VzCiAgICAgaW4KICBpdAouCgpBcyB3ZWxsIGFzIGFuIGVtb2ppOiDwn5mCCg==
|
||||
|
||||
--45c75ff528359022eb03679fbe91877d75343f2e1f8193e349deffa33ff7--`
|
||||
exampleMailPlainB64WithAttachmentBrokenB64 = `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 base64 with 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=45c75ff528359022eb03679fbe91877d75343f2e1f8193e349deffa33ff7
|
||||
|
||||
--45c75ff528359022eb03679fbe91877d75343f2e1f8193e349deffa33ff7
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
|
||||
RGVhciBDdXN0b21lciwKClRoaXMgaXMgYSB0ZXN0IG1haWwuIFBsZWFzZSBkbyBub3QgcmVwbHkg
|
||||
dG8gdGhpcy4gQWxzbyB0aGl§§§§§@@@@@XMgdmVyeSBsb25nIHNvIGl0CnNob3VsZCBiZSB3cmFw
|
||||
cGVkLgoKClRoYW5rIHlvdXIgZm9yIHlvdXIgYnVzaW5lc3MhClRoZSBnby1tYWlsIHRlYW0KCi0t
|
||||
ClRoaXMgaXMgYSBzaWduYXR1cmU=
|
||||
|
||||
--45c75ff528359022eb03679fbe91877d75343f2e1f8193e349deffa33ff7
|
||||
Content-Disposition: attachment; filename="test.attachment"
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-Type: application/octet-stream; name="test.attachment"
|
||||
|
||||
VGhpcyBpcyBhIHNpbXBsZSB0ZXN0IHRleHQgZmlsZSBhdHRhY2htZW50LgoKSXQgCiAgaGFzCiAg
|
||||
ICAgc2V2ZXJhbAogICAg§§§§§@@@@@BuZXdsaW5lcwoJICAgICAgICAgICAgYW5kCgkgICAgc3Bh
|
||||
Y2VzCiAgICAgaW4KICBpdAouCgpBcyB3ZWxsIGFzIGFuIGVtb2ppOiDwn5mCCg==
|
||||
|
||||
--45c75ff528359022eb03679fbe91877d75343f2e1f8193e349deffa33ff7--`
|
||||
exampleMailPlainB64WithAttachmentInvalidCTE = `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 base64 with 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=45c75ff528359022eb03679fbe91877d75343f2e1f8193e349deffa33ff7
|
||||
|
||||
--45c75ff528359022eb03679fbe91877d75343f2e1f8193e349deffa33ff7
|
||||
Content-Transfer-Encoding: invalid
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
|
||||
RGVhciBDdXN0b21lciwKClRoaXMgaXMgYSB0ZXN0IG1haWwuIFBsZWFzZSBkbyBub3QgcmVwbHkg
|
||||
dG8gdGhpcy4gQWxzbyB0aGlzIGxpbmUgaXMgdmVyeSBsb25nIHNvIGl0CnNob3VsZCBiZSB3cmFw
|
||||
cGVkLgoKClRoYW5rIHlvdXIgZm9yIHlvdXIgYnVzaW5lc3MhClRoZSBnby1tYWlsIHRlYW0KCi0t
|
||||
ClRoaXMgaXMgYSBzaWduYXR1cmU=
|
||||
|
||||
--45c75ff528359022eb03679fbe91877d75343f2e1f8193e349deffa33ff7
|
||||
Content-Disposition: attachment; filename="test.attachment"
|
||||
Content-Transfer-Encoding: invalid
|
||||
Content-Type: application/octet-stream; name="test.attachment"
|
||||
|
||||
VGhpcyBpcyBhIHNpbXBsZSB0ZXN0IHRleHQgZmlsZSBhdHRhY2htZW50LgoKSXQgCiAgaGFzCiAg
|
||||
ICAgc2V2ZXJhbAogICAgICAgICAgICBuZXdsaW5lcwoJICAgICAgICAgICAgYW5kCgkgICAgc3Bh
|
||||
Y2VzCiAgICAgaW4KICBpdAouCgpBcyB3ZWxsIGFzIGFuIGVtb2ppOiDwn5mCCg==
|
||||
|
||||
--45c75ff528359022eb03679fbe91877d75343f2e1f8193e349deffa33ff7--`
|
||||
exampleMailPlainB64WithAttachmentInvalidContentType = `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 base64 with 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=45c75ff528359022eb03679fbe91877d75343f2e1f8193e349deffa33ff7
|
||||
|
||||
--45c75ff528359022eb03679fbe91877d75343f2e1f8193e349deffa33ff7
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
|
||||
RGVhciBDdXN0b21lciwKClRoaXMgaXMgYSB0ZXN0IG1haWwuIFBsZWFzZSBkbyBub3QgcmVwbHkg
|
||||
dG8gdGhpcy4gQWxzbyB0aGlzIGxpbmUgaXMgdmVyeSBsb25nIHNvIGl0CnNob3VsZCBiZSB3cmFw
|
||||
cGVkLgoKClRoYW5rIHlvdXIgZm9yIHlvdXIgYnVzaW5lc3MhClRoZSBnby1tYWlsIHRlYW0KCi0t
|
||||
ClRoaXMgaXMgYSBzaWduYXR1cmU=
|
||||
|
||||
--45c75ff528359022eb03679fbe91877d75343f2e1f8193e349deffa33ff7
|
||||
Content-Disposition: attachment; filename="test.attachment"
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-Type; text/plain @ charset=UTF-8; $foo; bar; --invalid--
|
||||
|
||||
VGhpcyBpcyBhIHNpbXBsZSB0ZXN0IHRleHQgZmlsZSBhdHRhY2htZW50LgoKSXQgCiAgaGFzCiAg
|
||||
ICAgc2V2ZXJhbAogICAgICAgICAgICBuZXdsaW5lcwoJICAgICAgICAgICAgYW5kCgkgICAgc3Bh
|
||||
Y2VzCiAgICAgaW4KICBpdAouCgpBcyB3ZWxsIGFzIGFuIGVtb2ppOiDwn5mCCg==
|
||||
|
||||
--45c75ff528359022eb03679fbe91877d75343f2e1f8193e349deffa33ff7--`
|
||||
exampleMailPlainB64WithAttachmentNoBoundary = `Date: Wed, 01 Nov 2023 00:00:00 +0000
|
||||
MIME-Version: 1.0
|
||||
|
@ -760,39 +578,6 @@ Content-Disposition: attachment;
|
|||
filename="testfile.txt"
|
||||
|
||||
VGhpcyBpcyBhIHRlc3QgaW4gQmFzZTY0
|
||||
--------------26A45336F6C6196BD8BBA2A2--`
|
||||
exampleMultiPart7BitBase64BrokenB64 = `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"
|
||||
|
||||
VGh@@@@§§§§hIHRlc3QgaW4gQmFzZTY0
|
||||
--------------26A45336F6C6196BD8BBA2A2--`
|
||||
exampleMultiPart8BitBase64 = `Date: Wed, 01 Nov 2023 00:00:00 +0000
|
||||
MIME-Version: 1.0
|
||||
|
@ -827,352 +612,8 @@ Content-Disposition: attachment;
|
|||
|
||||
VGhpcyBpcyBhIHRlc3QgaW4gQmFzZTY0
|
||||
--------------26A45336F6C6196BD8BBA2A2--`
|
||||
exampleMailWithInlineEmbed = `Date: Wed, 01 Nov 2023 00:00:00 +0000
|
||||
MIME-Version: 1.0
|
||||
Message-ID: <1305604950.683004066175.AAAAAAAAaaaaaaaaB@go-mail.dev>
|
||||
Subject: Example mail with inline embed
|
||||
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>
|
||||
Content-Type: multipart/related; boundary="abc123"
|
||||
|
||||
--abc123
|
||||
Content-Type: text/html; charset=UTF-8
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
<html>
|
||||
<body>
|
||||
<p>Hello,</p>
|
||||
<p>This is an example email with an inline image:</p>
|
||||
<img src="cid:12345@go-mail.dev" alt="Inline Image">
|
||||
<p>Best regards,<br>The go-mail team</p>
|
||||
</body>
|
||||
</html>
|
||||
--abc123
|
||||
Content-Type: image/png
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-ID: <12345@go-mail.dev>
|
||||
Content-Disposition: inline; filename="test.png"
|
||||
|
||||
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQIW2NgYGD4DwABBAEAwS2O
|
||||
UAAAAABJRU5ErkJggg==
|
||||
--abc123--`
|
||||
exampleMailWithInlineEmbedWrongDisposition = `Date: Wed, 01 Nov 2023 00:00:00 +0000
|
||||
MIME-Version: 1.0
|
||||
Message-ID: <1305604950.683004066175.AAAAAAAAaaaaaaaaB@go-mail.dev>
|
||||
Subject: Example mail with inline embed
|
||||
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>
|
||||
Content-Type: multipart/related; boundary="abc123"
|
||||
|
||||
--abc123
|
||||
Content-Type: text/html; charset=UTF-8
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
<html>
|
||||
<body>
|
||||
<p>Hello,</p>
|
||||
<p>This is an example email with an inline image:</p>
|
||||
<img src="cid:12345@go-mail.dev" alt="Inline Image">
|
||||
<p>Best regards,<br>The go-mail team</p>
|
||||
</body>
|
||||
</html>
|
||||
--abc123
|
||||
Content-Type: image/png
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-ID: <12345@go-mail.dev>
|
||||
Content-Disposition: broken; filename="test.png"
|
||||
|
||||
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQIW2NgYGD4DwABBAEAwS2O
|
||||
UAAAAABJRU5ErkJggg==
|
||||
--abc123--`
|
||||
)
|
||||
|
||||
func TestEMLToMsgFromReader(t *testing.T) {
|
||||
t.Run("EMLToMsgFromReader via EMLToMsgFromString, check subject and encoding", func(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
emlString string
|
||||
wantEncoding Encoding
|
||||
wantSubject string
|
||||
}{
|
||||
{
|
||||
"RFC5322 A1.1 example mail", exampleMailRFC5322A11, EncodingUSASCII,
|
||||
"Saying Hello",
|
||||
},
|
||||
{
|
||||
"Plain text no encoding (7bit)", exampleMailPlain7Bit, EncodingUSASCII,
|
||||
"Example mail // plain text without encoding",
|
||||
},
|
||||
{
|
||||
"Plain text no encoding", exampleMailPlainNoEnc, NoEncoding,
|
||||
"Example mail // plain text without encoding",
|
||||
},
|
||||
{
|
||||
"Plain text quoted-printable", exampleMailPlainQP, EncodingQP,
|
||||
"Example mail // plain text quoted-printable",
|
||||
},
|
||||
{
|
||||
"Plain text base64", exampleMailPlainB64, EncodingB64,
|
||||
"Example mail // plain text base64",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
parsed, err := EMLToMsgFromString(tt.emlString)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse EML string: %s", err)
|
||||
}
|
||||
if parsed.Encoding() != tt.wantEncoding.String() {
|
||||
t.Errorf("failed to parse EML string: want encoding %s, got %s", tt.wantEncoding,
|
||||
parsed.Encoding())
|
||||
}
|
||||
gotSubject, ok := parsed.genHeader[HeaderSubject]
|
||||
if !ok {
|
||||
t.Fatalf("failed to parse EML string. No subject header found")
|
||||
}
|
||||
if len(gotSubject) != 1 {
|
||||
t.Fatalf("failed to parse EML string, more than one subject header found")
|
||||
}
|
||||
if !strings.EqualFold(gotSubject[0], tt.wantSubject) {
|
||||
t.Errorf("failed to parse EML string: want subject %s, got %s", tt.wantSubject,
|
||||
gotSubject[0])
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
t.Run("EMLToMsgFromReader fails on reader", func(t *testing.T) {
|
||||
emlReader := bytes.NewBufferString("invalid")
|
||||
if _, err := EMLToMsgFromReader(emlReader); err == nil {
|
||||
t.Errorf("EML parsing with invalid EML string should fail")
|
||||
}
|
||||
})
|
||||
t.Run("EMLToMsgFromReader fails on parseEML", func(t *testing.T) {
|
||||
emlReader := bytes.NewBufferString(exampleMailRFC5322A11InvalidFrom)
|
||||
if _, err := EMLToMsgFromReader(emlReader); err == nil {
|
||||
t.Errorf("EML parsing with invalid EML string should fail")
|
||||
}
|
||||
})
|
||||
t.Run("EMLToMsgFromReader via EMLToMsgFromString on different examples", func(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
emlString string
|
||||
shouldFail bool
|
||||
}{
|
||||
{
|
||||
name: "Valid RFC 5322 Example",
|
||||
emlString: exampleMailRFC5322A11,
|
||||
shouldFail: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid From Header (RFC 5322)",
|
||||
emlString: exampleMailRFC5322A11InvalidFrom,
|
||||
shouldFail: true,
|
||||
},
|
||||
{
|
||||
name: "Invalid Header",
|
||||
emlString: exampleMailInvalidHeader,
|
||||
shouldFail: true,
|
||||
},
|
||||
{
|
||||
name: "Plain broken Content-Type",
|
||||
emlString: exampleMailInvalidContentType,
|
||||
shouldFail: true,
|
||||
},
|
||||
{
|
||||
name: "Plain No Encoding",
|
||||
emlString: exampleMailPlainNoEnc,
|
||||
shouldFail: false,
|
||||
},
|
||||
{
|
||||
name: "Plain invalid CTE",
|
||||
emlString: exampleMailPlainInvalidCTE,
|
||||
shouldFail: true,
|
||||
},
|
||||
{
|
||||
name: "Plain 7bit",
|
||||
emlString: exampleMailPlain7Bit,
|
||||
shouldFail: false,
|
||||
},
|
||||
{
|
||||
name: "Broken Body Base64",
|
||||
emlString: exampleMailPlainBrokenBody,
|
||||
shouldFail: true,
|
||||
},
|
||||
{
|
||||
name: "Unknown Content Type",
|
||||
emlString: exampleMailPlainUnknownContentType,
|
||||
shouldFail: true,
|
||||
},
|
||||
{
|
||||
name: "Broken Header",
|
||||
emlString: exampleMailPlainBrokenHeader,
|
||||
shouldFail: true,
|
||||
},
|
||||
{
|
||||
name: "Broken From Header",
|
||||
emlString: exampleMailPlainBrokenFrom,
|
||||
shouldFail: true,
|
||||
},
|
||||
{
|
||||
name: "Broken To Header",
|
||||
emlString: exampleMailPlainBrokenTo,
|
||||
shouldFail: true,
|
||||
},
|
||||
{
|
||||
name: "Invalid Date",
|
||||
emlString: exampleMailPlainNoEncInvalidDate,
|
||||
shouldFail: true,
|
||||
},
|
||||
{
|
||||
name: "No Date Header",
|
||||
emlString: exampleMailPlainNoEncNoDate,
|
||||
shouldFail: false,
|
||||
},
|
||||
{
|
||||
name: "Quoted Printable Encoding",
|
||||
emlString: exampleMailPlainQP,
|
||||
shouldFail: false,
|
||||
},
|
||||
{
|
||||
name: "Unsupported Transfer Encoding",
|
||||
emlString: exampleMailPlainUnsupportedTransferEnc,
|
||||
shouldFail: true,
|
||||
},
|
||||
{
|
||||
name: "Base64 Encoding",
|
||||
emlString: exampleMailPlainB64,
|
||||
shouldFail: false,
|
||||
},
|
||||
{
|
||||
name: "Base64 with Attachment",
|
||||
emlString: exampleMailPlainB64WithAttachment,
|
||||
shouldFail: false,
|
||||
},
|
||||
{
|
||||
name: "Base64 with Attachment no content types",
|
||||
emlString: exampleMailPlainB64WithAttachmentNoContentType,
|
||||
shouldFail: true,
|
||||
},
|
||||
{
|
||||
name: "Multipart Base64 with Attachment broken Base64",
|
||||
emlString: exampleMailPlainB64WithAttachmentBrokenB64,
|
||||
shouldFail: true,
|
||||
},
|
||||
{
|
||||
name: "Base64 with Attachment with invalid content type in attachment",
|
||||
emlString: exampleMailPlainB64WithAttachmentInvalidContentType,
|
||||
shouldFail: true,
|
||||
},
|
||||
{
|
||||
name: "Base64 with Attachment with invalid CTE in attachment",
|
||||
emlString: exampleMailPlainB64WithAttachmentInvalidCTE,
|
||||
shouldFail: true,
|
||||
},
|
||||
{
|
||||
name: "Base64 with Attachment No Boundary",
|
||||
emlString: exampleMailPlainB64WithAttachmentNoBoundary,
|
||||
shouldFail: true,
|
||||
},
|
||||
{
|
||||
name: "Broken Body Base64",
|
||||
emlString: exampleMailPlainB64BrokenBody,
|
||||
shouldFail: true,
|
||||
},
|
||||
{
|
||||
name: "Base64 with Embedded Image",
|
||||
emlString: exampleMailPlainB64WithEmbed,
|
||||
shouldFail: false,
|
||||
},
|
||||
{
|
||||
name: "Base64 with Embed No Content-ID",
|
||||
emlString: exampleMailPlainB64WithEmbedNoContentID,
|
||||
shouldFail: false,
|
||||
},
|
||||
{
|
||||
name: "Multipart Mixed with Attachment, Embed, and Alternative Part",
|
||||
emlString: exampleMailMultipartMixedAlternativeRelated,
|
||||
shouldFail: false,
|
||||
},
|
||||
{
|
||||
name: "Multipart 7bit Base64",
|
||||
emlString: exampleMultiPart7BitBase64,
|
||||
shouldFail: false,
|
||||
},
|
||||
{
|
||||
name: "Multipart 7bit Base64 with broken Base64",
|
||||
emlString: exampleMultiPart7BitBase64BrokenB64,
|
||||
shouldFail: true,
|
||||
},
|
||||
{
|
||||
name: "Multipart 8bit Base64",
|
||||
emlString: exampleMultiPart8BitBase64,
|
||||
shouldFail: false,
|
||||
},
|
||||
{
|
||||
name: "Multipart with inline embed",
|
||||
emlString: exampleMailWithInlineEmbed,
|
||||
shouldFail: false,
|
||||
},
|
||||
{
|
||||
name: "Multipart with inline embed disposition broken",
|
||||
emlString: exampleMailWithInlineEmbedWrongDisposition,
|
||||
shouldFail: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := EMLToMsgFromString(tt.emlString)
|
||||
if tt.shouldFail && err == nil {
|
||||
t.Errorf("parsing of EML was supposed to fail, but it did not")
|
||||
}
|
||||
if !tt.shouldFail && err != nil {
|
||||
t.Errorf("parsing of EML failed: %s", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestEMLToMsgFromFile(t *testing.T) {
|
||||
t.Run("EMLToMsgFromFile succeeds", func(t *testing.T) {
|
||||
parsed, err := EMLToMsgFromFile("testdata/RFC5322-A1-1.eml")
|
||||
if err != nil {
|
||||
t.Fatalf("EMLToMsgFromFile failed: %s ", err)
|
||||
}
|
||||
if parsed.Encoding() != EncodingUSASCII.String() {
|
||||
t.Errorf("EMLToMsgFromFile failed: want encoding %s, got %s", EncodingUSASCII,
|
||||
parsed.Encoding())
|
||||
}
|
||||
gotSubject, ok := parsed.genHeader[HeaderSubject]
|
||||
if !ok {
|
||||
t.Fatalf("failed to parse EML string. No subject header found")
|
||||
}
|
||||
if len(gotSubject) != 1 {
|
||||
t.Fatalf("failed to parse EML string, more than one subject header found")
|
||||
}
|
||||
if !strings.EqualFold(gotSubject[0], "Saying Hello") {
|
||||
t.Errorf("failed to parse EML string: want subject %s, got %s", "Saying Hello",
|
||||
gotSubject[0])
|
||||
}
|
||||
})
|
||||
t.Run("EMLToMsgFromFile fails on file not found", func(t *testing.T) {
|
||||
if _, err := EMLToMsgFromFile("testdata/not-existing.eml"); err == nil {
|
||||
t.Errorf("EMLToMsgFromFile with invalid file should fail")
|
||||
}
|
||||
})
|
||||
t.Run("EMLToMsgFromFile fails on parseEML", func(t *testing.T) {
|
||||
if _, err := EMLToMsgFromFile("testdata/RFC5322-A1-1-invalid-from.eml"); err == nil {
|
||||
t.Errorf("EMLToMsgFromFile with invalid EML message should fail")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
func TestEMLToMsgFromString(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
@ -1180,6 +621,26 @@ 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",
|
||||
},
|
||||
{
|
||||
"Plain text quoted-printable", exampleMailPlainQP, "quoted-printable",
|
||||
"Example mail // plain text quoted-printable",
|
||||
},
|
||||
{
|
||||
"Plain text base64", exampleMailPlainB64, "base64",
|
||||
"Example mail // plain text base64",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
@ -1548,6 +1009,3 @@ func stringToTempFile(data, name string) (string, string, error) {
|
|||
}
|
||||
return tempDir, filePath, nil
|
||||
}
|
||||
|
||||
|
||||
*/
|
||||
|
|
309
file_test.go
309
file_test.go
|
@ -6,183 +6,134 @@ package mail
|
|||
|
||||
import "testing"
|
||||
|
||||
func TestFile(t *testing.T) {
|
||||
t.Run("setHeader", func(t *testing.T) {
|
||||
f := File{
|
||||
Name: "testfile.txt",
|
||||
Header: make(map[string][]string),
|
||||
}
|
||||
f.setHeader(HeaderContentType, "text/plain")
|
||||
contentType, ok := f.Header[HeaderContentType.String()]
|
||||
if !ok {
|
||||
t.Fatalf("setHeader failed. Expected header %s to be set", HeaderContentType)
|
||||
}
|
||||
if len(contentType) != 1 {
|
||||
t.Fatalf("setHeader failed. Expected header %s to have one value, got: %d", HeaderContentType,
|
||||
len(contentType))
|
||||
}
|
||||
if contentType[0] != "text/plain" {
|
||||
t.Fatalf("setHeader failed. Expected header %s to have value %s, got: %s",
|
||||
HeaderContentType.String(), "text/plain", contentType[0])
|
||||
}
|
||||
})
|
||||
t.Run("getHeader", func(t *testing.T) {
|
||||
f := File{
|
||||
Name: "testfile.txt",
|
||||
Header: make(map[string][]string),
|
||||
}
|
||||
f.setHeader(HeaderContentType, "text/plain")
|
||||
contentType, ok := f.getHeader(HeaderContentType)
|
||||
if !ok {
|
||||
t.Fatalf("setHeader failed. Expected header %s to be set", HeaderContentType)
|
||||
}
|
||||
if contentType != "text/plain" {
|
||||
t.Fatalf("setHeader failed. Expected header %s to have value %s, got: %s",
|
||||
HeaderContentType.String(), "text/plain", contentType)
|
||||
}
|
||||
})
|
||||
t.Run("WithFileDescription", func(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
desc string
|
||||
}{
|
||||
{"File description: test", "test"},
|
||||
{"File description: with newline", "test\n"},
|
||||
{"File description: empty", ""},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
message := NewMsg()
|
||||
message.AttachFile("file.go", WithFileDescription(tt.desc))
|
||||
attachments := message.GetAttachments()
|
||||
if len(attachments) <= 0 {
|
||||
t.Fatalf("failed to retrieve attachments list")
|
||||
}
|
||||
firstAttachment := attachments[0]
|
||||
if firstAttachment == nil {
|
||||
t.Fatalf("failed to retrieve first attachment, got nil")
|
||||
}
|
||||
if firstAttachment.Desc != tt.desc {
|
||||
t.Errorf("WithFileDescription() failed. Expected: %s, got: %s", tt.desc,
|
||||
firstAttachment.Desc)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
t.Run("WithFileContentID", func(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
id string
|
||||
}{
|
||||
{"Content-ID: test", "test"},
|
||||
{"Content-ID: with newline", "test\n"},
|
||||
{"Content-ID: empty", ""},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
message := NewMsg()
|
||||
message.AttachFile("file.go", WithFileContentID(tt.id))
|
||||
attachments := message.GetAttachments()
|
||||
if len(attachments) <= 0 {
|
||||
t.Fatalf("failed to retrieve attachments list")
|
||||
}
|
||||
firstAttachment := attachments[0]
|
||||
if firstAttachment == nil {
|
||||
t.Fatalf("failed to retrieve first attachment, got nil")
|
||||
}
|
||||
contentID := firstAttachment.Header.Get(HeaderContentID.String())
|
||||
if contentID != tt.id {
|
||||
t.Errorf("WithFileContentID() failed. Expected: %s, got: %s", tt.id,
|
||||
contentID)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
t.Run("WithFileEncoding", func(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
encoding Encoding
|
||||
want Encoding
|
||||
}{
|
||||
{"File encoding: US-ASCII", EncodingUSASCII, EncodingUSASCII},
|
||||
{"File encoding: 8bit raw", NoEncoding, NoEncoding},
|
||||
{"File encoding: Base64", EncodingB64, EncodingB64},
|
||||
{"File encoding: quoted-printable (not allowed)", EncodingQP, ""},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
message := NewMsg()
|
||||
message.AttachFile("file.go", WithFileEncoding(tt.encoding))
|
||||
attachments := message.GetAttachments()
|
||||
if len(attachments) <= 0 {
|
||||
t.Fatalf("failed to retrieve attachments list")
|
||||
}
|
||||
firstAttachment := attachments[0]
|
||||
if firstAttachment == nil {
|
||||
t.Fatalf("failed to retrieve first attachment, got nil")
|
||||
}
|
||||
if firstAttachment.Enc != tt.want {
|
||||
t.Errorf("WithFileEncoding() failed. Expected: %s, got: %s", tt.want, firstAttachment.Enc)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
t.Run("WithFileName", func(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
fileName string
|
||||
}{
|
||||
{"File name: test", "test"},
|
||||
{"File name: with newline", "test\n"},
|
||||
{"File name: empty", ""},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
message := NewMsg()
|
||||
message.AttachFile("file.go", WithFileName(tt.fileName))
|
||||
attachments := message.GetAttachments()
|
||||
if len(attachments) <= 0 {
|
||||
t.Fatalf("failed to retrieve attachments list")
|
||||
}
|
||||
firstAttachment := attachments[0]
|
||||
if firstAttachment == nil {
|
||||
t.Fatalf("failed to retrieve first attachment, got nil")
|
||||
}
|
||||
if firstAttachment.Name != tt.fileName {
|
||||
t.Errorf("WithFileName() failed. Expected: %s, got: %s", tt.fileName,
|
||||
firstAttachment.Name)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
t.Run("WithFileContentType", func(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
contentType ContentType
|
||||
}{
|
||||
{"File content-type: text/plain", TypeTextPlain},
|
||||
{"File content-type: html/html", TypeTextHTML},
|
||||
{"File content-type: application/octet-stream", TypeAppOctetStream},
|
||||
{"File content-type: application/pgp-encrypted", TypePGPEncrypted},
|
||||
{"File content-type: application/pgp-signature", TypePGPSignature},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
message := NewMsg()
|
||||
message.AttachFile("file.go", WithFileContentType(tt.contentType))
|
||||
attachments := message.GetAttachments()
|
||||
if len(attachments) <= 0 {
|
||||
t.Fatalf("failed to retrieve attachments list")
|
||||
}
|
||||
firstAttachment := attachments[0]
|
||||
if firstAttachment == nil {
|
||||
t.Fatalf("failed to retrieve first attachment, got nil")
|
||||
}
|
||||
if firstAttachment.ContentType != tt.contentType {
|
||||
t.Errorf("WithFileContentType() failed. Expected: %s, got: %s", tt.contentType,
|
||||
firstAttachment.ContentType)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
// TestFile_SetGetHeader tests the set-/getHeader method of the File object
|
||||
func TestFile_SetGetHeader(t *testing.T) {
|
||||
f := File{
|
||||
Name: "testfile.txt",
|
||||
Header: make(map[string][]string),
|
||||
}
|
||||
f.setHeader(HeaderContentType, "text/plain")
|
||||
fi, ok := f.getHeader(HeaderContentType)
|
||||
if !ok {
|
||||
t.Errorf("getHeader method of File did not return a value")
|
||||
return
|
||||
}
|
||||
if fi != "text/plain" {
|
||||
t.Errorf("getHeader returned wrong value. Expected: %s, got: %s", "text/plain", fi)
|
||||
}
|
||||
fi, ok = f.getHeader(HeaderContentTransferEnc)
|
||||
if ok {
|
||||
t.Errorf("getHeader method of File did return a value, but wasn't supposed to")
|
||||
return
|
||||
}
|
||||
if fi != "" {
|
||||
t.Errorf("getHeader returned wrong value. Expected: %s, got: %s", "", fi)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFile_WithFileDescription tests the WithFileDescription option
|
||||
func TestFile_WithFileDescription(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
desc string
|
||||
}{
|
||||
{"File description: test", "test"},
|
||||
{"File description: empty", ""},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
m := NewMsg()
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
m.AttachFile("file.go", WithFileDescription(tt.desc))
|
||||
al := m.GetAttachments()
|
||||
if len(al) <= 0 {
|
||||
t.Errorf("AttachFile() failed. Attachment list is empty")
|
||||
}
|
||||
a := al[0]
|
||||
if a.Desc != tt.desc {
|
||||
t.Errorf("WithFileDescription() failed. Expected: %s, got: %s", tt.desc, a.Desc)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestFile_WithContentID tests the WithFileContentID option
|
||||
func TestFile_WithContentID(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
contentid string
|
||||
}{
|
||||
{"File Content-ID: test", "test"},
|
||||
{"File Content-ID: empty", ""},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
m := NewMsg()
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
m.AttachFile("file.go", WithFileContentID(tt.contentid))
|
||||
al := m.GetAttachments()
|
||||
if len(al) <= 0 {
|
||||
t.Errorf("AttachFile() failed. Attachment list is empty")
|
||||
}
|
||||
a := al[0]
|
||||
if a.Header.Get(HeaderContentID.String()) != tt.contentid {
|
||||
t.Errorf("WithFileContentID() failed. Expected: %s, got: %s", tt.contentid,
|
||||
a.Header.Get(HeaderContentID.String()))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestFile_WithFileEncoding tests the WithFileEncoding option
|
||||
func TestFile_WithFileEncoding(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
enc Encoding
|
||||
want Encoding
|
||||
}{
|
||||
{"File encoding: 8bit raw", NoEncoding, NoEncoding},
|
||||
{"File encoding: Base64", EncodingB64, EncodingB64},
|
||||
{"File encoding: quoted-printable (not allowed)", EncodingQP, ""},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
m := NewMsg()
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
m.AttachFile("file.go", WithFileEncoding(tt.enc))
|
||||
al := m.GetAttachments()
|
||||
if len(al) <= 0 {
|
||||
t.Errorf("AttachFile() failed. Attachment list is empty")
|
||||
}
|
||||
a := al[0]
|
||||
if a.Enc != tt.want {
|
||||
t.Errorf("WithFileEncoding() failed. Expected: %s, got: %s", tt.enc, a.Enc)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestFile_WithFileContentType tests the WithFileContentType option
|
||||
func TestFile_WithFileContentType(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
ct ContentType
|
||||
want string
|
||||
}{
|
||||
{"File content-type: text/plain", TypeTextPlain, "text/plain"},
|
||||
{"File content-type: html/html", TypeTextHTML, "text/html"},
|
||||
{"File content-type: application/octet-stream", TypeAppOctetStream, "application/octet-stream"},
|
||||
{"File content-type: application/pgp-encrypted", TypePGPEncrypted, "application/pgp-encrypted"},
|
||||
{"File content-type: application/pgp-signature", TypePGPSignature, "application/pgp-signature"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
m := NewMsg()
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
m.AttachFile("file.go", WithFileContentType(tt.ct))
|
||||
al := m.GetAttachments()
|
||||
if len(al) <= 0 {
|
||||
t.Errorf("AttachFile() failed. Attachment list is empty")
|
||||
}
|
||||
a := al[0]
|
||||
if a.ContentType != ContentType(tt.want) {
|
||||
t.Errorf("WithFileContentType() failed. Expected: %s, got: %s", tt.want, a.ContentType)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
147
header_test.go
147
header_test.go
|
@ -8,13 +8,69 @@ import (
|
|||
"testing"
|
||||
)
|
||||
|
||||
var (
|
||||
genHeaderTests = []struct {
|
||||
// TestImportance_StringFuncs tests the different string method of the Importance object
|
||||
func TestImportance_StringFuncs(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
header Header
|
||||
imp Importance
|
||||
wantns string
|
||||
xprio string
|
||||
want string
|
||||
}{
|
||||
{"Header: Content-Description", HeaderContentDescription, "Content-Description"},
|
||||
{"Importance: Non-Urgent", ImportanceNonUrgent, "0", "5", "non-urgent"},
|
||||
{"Importance: Low", ImportanceLow, "0", "5", "low"},
|
||||
{"Importance: Normal", ImportanceNormal, "", "", ""},
|
||||
{"Importance: High", ImportanceHigh, "1", "1", "high"},
|
||||
{"Importance: Urgent", ImportanceUrgent, "1", "1", "urgent"},
|
||||
{"Importance: Unknown", 9, "", "", ""},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.imp.NumString() != tt.wantns {
|
||||
t.Errorf("wrong number string for Importance returned. Expected: %s, got: %s",
|
||||
tt.wantns, tt.imp.NumString())
|
||||
}
|
||||
if tt.imp.XPrioString() != tt.xprio {
|
||||
t.Errorf("wrong x-prio string for Importance returned. Expected: %s, got: %s",
|
||||
tt.xprio, tt.imp.XPrioString())
|
||||
}
|
||||
if tt.imp.String() != tt.want {
|
||||
t.Errorf("wrong string for Importance returned. Expected: %s, got: %s",
|
||||
tt.want, tt.imp.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestAddrHeader_String tests the string method of the AddrHeader object
|
||||
func TestAddrHeader_String(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
ah AddrHeader
|
||||
want string
|
||||
}{
|
||||
{"Address header: From", HeaderFrom, "From"},
|
||||
{"Address header: To", HeaderTo, "To"},
|
||||
{"Address header: Cc", HeaderCc, "Cc"},
|
||||
{"Address header: Bcc", HeaderBcc, "Bcc"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.ah.String() != tt.want {
|
||||
t.Errorf("wrong string for AddrHeader returned. Expected: %s, got: %s",
|
||||
tt.want, tt.ah.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestHeader_String tests the string method of the Header object
|
||||
func TestHeader_String(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
h Header
|
||||
want string
|
||||
}{
|
||||
{"Header: Content-Disposition", HeaderContentDisposition, "Content-Disposition"},
|
||||
{"Header: Content-ID", HeaderContentID, "Content-ID"},
|
||||
{"Header: Content-Language", HeaderContentLang, "Content-Language"},
|
||||
|
@ -22,10 +78,6 @@ var (
|
|||
{"Header: Content-Transfer-Encoding", HeaderContentTransferEnc, "Content-Transfer-Encoding"},
|
||||
{"Header: Content-Type", HeaderContentType, "Content-Type"},
|
||||
{"Header: Date", HeaderDate, "Date"},
|
||||
{
|
||||
"Header: Disposition-Notification-To", HeaderDispositionNotificationTo,
|
||||
"Disposition-Notification-To",
|
||||
},
|
||||
{"Header: Importance", HeaderImportance, "Importance"},
|
||||
{"Header: In-Reply-To", HeaderInReplyTo, "In-Reply-To"},
|
||||
{"Header: List-Unsubscribe", HeaderListUnsubscribe, "List-Unsubscribe"},
|
||||
|
@ -35,90 +87,19 @@ var (
|
|||
{"Header: Organization", HeaderOrganization, "Organization"},
|
||||
{"Header: Precedence", HeaderPrecedence, "Precedence"},
|
||||
{"Header: Priority", HeaderPriority, "Priority"},
|
||||
{"Header: References", HeaderReferences, "References"},
|
||||
{"Header: HeaderReferences", HeaderReferences, "References"},
|
||||
{"Header: Reply-To", HeaderReplyTo, "Reply-To"},
|
||||
{"Header: Subject", HeaderSubject, "Subject"},
|
||||
{"Header: User-Agent", HeaderUserAgent, "User-Agent"},
|
||||
{"Header: X-Auto-Response-Suppress", HeaderXAutoResponseSuppress, "X-Auto-Response-Suppress"},
|
||||
{"Header: X-Mailer", HeaderXMailer, "X-Mailer"},
|
||||
{"Header: X-MSMail-Priority", HeaderXMSMailPriority, "X-MSMail-Priority"},
|
||||
{"Header: X-Priority", HeaderXPriority, "X-Priority"},
|
||||
}
|
||||
addrHeaderTests = []struct {
|
||||
name string
|
||||
header AddrHeader
|
||||
want string
|
||||
}{
|
||||
{"From", HeaderFrom, "From"},
|
||||
{"To", HeaderTo, "To"},
|
||||
{"Cc", HeaderCc, "Cc"},
|
||||
{"Bcc", HeaderBcc, "Bcc"},
|
||||
}
|
||||
)
|
||||
|
||||
func TestImportance_Stringer(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
imp Importance
|
||||
wantnum string
|
||||
xprio string
|
||||
want string
|
||||
}{
|
||||
{"Non-Urgent", ImportanceNonUrgent, "0", "5", "non-urgent"},
|
||||
{"Low", ImportanceLow, "0", "5", "low"},
|
||||
{"Normal", ImportanceNormal, "", "", ""},
|
||||
{"High", ImportanceHigh, "1", "1", "high"},
|
||||
{"Urgent", ImportanceUrgent, "1", "1", "urgent"},
|
||||
{"Unknown", 9, "", "", ""},
|
||||
}
|
||||
t.Run("String", func(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.imp.String() != tt.want {
|
||||
t.Errorf("wrong string for Importance returned. Expected: %s, got: %s", tt.want, tt.imp.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
t.Run("NumString", func(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.imp.NumString() != tt.wantnum {
|
||||
t.Errorf("wrong number string for Importance returned. Expected: %s, got: %s", tt.wantnum,
|
||||
tt.imp.NumString())
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
t.Run("XPrioString", func(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.imp.XPrioString() != tt.xprio {
|
||||
t.Errorf("wrong x-prio string for Importance returned. Expected: %s, got: %s", tt.xprio,
|
||||
tt.imp.XPrioString())
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestAddrHeader_Stringer(t *testing.T) {
|
||||
for _, tt := range addrHeaderTests {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.header.String() != tt.want {
|
||||
t.Errorf("wrong string for AddrHeader returned. Expected: %s, got: %s",
|
||||
tt.want, tt.header.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeader_Stringer(t *testing.T) {
|
||||
for _, tt := range genHeaderTests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.header.String() != tt.want {
|
||||
if tt.h.String() != tt.want {
|
||||
t.Errorf("wrong string for Header returned. Expected: %s, got: %s",
|
||||
tt.want, tt.header.String())
|
||||
tt.want, tt.h.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -41,48 +41,42 @@ func NewJSON(output io.Writer, level Level) *JSONlog {
|
|||
}
|
||||
}
|
||||
|
||||
// logMessage is a helper function to handle different log levels and formats.
|
||||
func logMessage(level Level, log *slog.Logger, logData Log, formatFunc func(string, ...interface{}) string) {
|
||||
lGroup := log.WithGroup(DirString).With(
|
||||
slog.String(DirFromString, logData.directionFrom()),
|
||||
slog.String(DirToString, logData.directionTo()),
|
||||
)
|
||||
switch level {
|
||||
case LevelDebug:
|
||||
lGroup.Debug(formatFunc(logData.Format, logData.Messages...))
|
||||
case LevelInfo:
|
||||
lGroup.Info(formatFunc(logData.Format, logData.Messages...))
|
||||
case LevelWarn:
|
||||
lGroup.Warn(formatFunc(logData.Format, logData.Messages...))
|
||||
case LevelError:
|
||||
lGroup.Error(formatFunc(logData.Format, logData.Messages...))
|
||||
}
|
||||
}
|
||||
|
||||
// Debugf logs a debug message via the structured JSON logger
|
||||
func (l *JSONlog) Debugf(log Log) {
|
||||
if l.level >= LevelDebug {
|
||||
logMessage(LevelDebug, l.log, log, fmt.Sprintf)
|
||||
l.log.WithGroup(DirString).With(
|
||||
slog.String(DirFromString, log.directionFrom()),
|
||||
slog.String(DirToString, log.directionTo()),
|
||||
).Debug(fmt.Sprintf(log.Format, log.Messages...))
|
||||
}
|
||||
}
|
||||
|
||||
// Infof logs a info message via the structured JSON logger
|
||||
func (l *JSONlog) Infof(log Log) {
|
||||
if l.level >= LevelInfo {
|
||||
logMessage(LevelInfo, l.log, log, fmt.Sprintf)
|
||||
l.log.WithGroup(DirString).With(
|
||||
slog.String(DirFromString, log.directionFrom()),
|
||||
slog.String(DirToString, log.directionTo()),
|
||||
).Info(fmt.Sprintf(log.Format, log.Messages...))
|
||||
}
|
||||
}
|
||||
|
||||
// Warnf logs a warn message via the structured JSON logger
|
||||
func (l *JSONlog) Warnf(log Log) {
|
||||
if l.level >= LevelWarn {
|
||||
logMessage(LevelWarn, l.log, log, fmt.Sprintf)
|
||||
l.log.WithGroup(DirString).With(
|
||||
slog.String(DirFromString, log.directionFrom()),
|
||||
slog.String(DirToString, log.directionTo()),
|
||||
).Warn(fmt.Sprintf(log.Format, log.Messages...))
|
||||
}
|
||||
}
|
||||
|
||||
// Errorf logs a warn message via the structured JSON logger
|
||||
func (l *JSONlog) Errorf(log Log) {
|
||||
if l.level >= LevelError {
|
||||
logMessage(LevelError, l.log, log, fmt.Sprintf)
|
||||
l.log.WithGroup(DirString).With(
|
||||
slog.String(DirFromString, log.directionFrom()),
|
||||
slog.String(DirToString, log.directionTo()),
|
||||
).Error(fmt.Sprintf(log.Format, log.Messages...))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,36 +35,34 @@ func New(output io.Writer, level Level) *Stdlog {
|
|||
}
|
||||
}
|
||||
|
||||
// logStdMessage is a helper function to handle different log levels and formats for Stdlog.
|
||||
func logStdMessage(logger *log.Logger, logData Log, callDepth int) {
|
||||
format := fmt.Sprintf("%s %s", logData.directionPrefix(), logData.Format)
|
||||
_ = logger.Output(callDepth, fmt.Sprintf(format, logData.Messages...))
|
||||
}
|
||||
|
||||
// Debugf performs a Printf() on the debug logger
|
||||
func (l *Stdlog) Debugf(log Log) {
|
||||
if l.level >= LevelDebug {
|
||||
logStdMessage(l.debug, log, CallDepth)
|
||||
format := fmt.Sprintf("%s %s", log.directionPrefix(), log.Format)
|
||||
_ = l.debug.Output(CallDepth, fmt.Sprintf(format, log.Messages...))
|
||||
}
|
||||
}
|
||||
|
||||
// Infof performs a Printf() on the info logger
|
||||
func (l *Stdlog) Infof(log Log) {
|
||||
if l.level >= LevelInfo {
|
||||
logStdMessage(l.info, log, CallDepth)
|
||||
format := fmt.Sprintf("%s %s", log.directionPrefix(), log.Format)
|
||||
_ = l.info.Output(CallDepth, fmt.Sprintf(format, log.Messages...))
|
||||
}
|
||||
}
|
||||
|
||||
// Warnf performs a Printf() on the warn logger
|
||||
func (l *Stdlog) Warnf(log Log) {
|
||||
if l.level >= LevelWarn {
|
||||
logStdMessage(l.warn, log, CallDepth)
|
||||
format := fmt.Sprintf("%s %s", log.directionPrefix(), log.Format)
|
||||
_ = l.warn.Output(CallDepth, fmt.Sprintf(format, log.Messages...))
|
||||
}
|
||||
}
|
||||
|
||||
// Errorf performs a Printf() on the error logger
|
||||
func (l *Stdlog) Errorf(log Log) {
|
||||
if l.level >= LevelError {
|
||||
logStdMessage(l.err, log, CallDepth)
|
||||
format := fmt.Sprintf("%s %s", log.directionPrefix(), log.Format)
|
||||
_ = l.err.Output(CallDepth, fmt.Sprintf(format, log.Messages...))
|
||||
}
|
||||
}
|
||||
|
|
121
msg.go
121
msg.go
|
@ -367,45 +367,23 @@ func (m *Msg) SignWithTLSCertificate(keyPairTlS *tls.Certificate) error {
|
|||
return fmt.Errorf("failed to parse intermediate certificate: %w", err)
|
||||
}
|
||||
|
||||
leafCertificate, err := getLeafCertificate(keyPairTlS)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get leaf certificate: %w", err)
|
||||
}
|
||||
|
||||
switch keyPairTlS.PrivateKey.(type) {
|
||||
case *rsa.PrivateKey:
|
||||
if intermediateCertificate == nil {
|
||||
return m.SignWithSMimeRSA(keyPairTlS.PrivateKey.(*rsa.PrivateKey), leafCertificate, nil)
|
||||
return m.SignWithSMimeRSA(keyPairTlS.PrivateKey.(*rsa.PrivateKey), keyPairTlS.Leaf, nil)
|
||||
}
|
||||
return m.SignWithSMimeRSA(keyPairTlS.PrivateKey.(*rsa.PrivateKey), leafCertificate, intermediateCertificate)
|
||||
return m.SignWithSMimeRSA(keyPairTlS.PrivateKey.(*rsa.PrivateKey), keyPairTlS.Leaf, intermediateCertificate)
|
||||
|
||||
case *ecdsa.PrivateKey:
|
||||
if intermediateCertificate == nil {
|
||||
return m.SignWithSMimeECDSA(keyPairTlS.PrivateKey.(*ecdsa.PrivateKey), leafCertificate, nil)
|
||||
return m.SignWithSMimeECDSA(keyPairTlS.PrivateKey.(*ecdsa.PrivateKey), keyPairTlS.Leaf, nil)
|
||||
}
|
||||
return m.SignWithSMimeECDSA(keyPairTlS.PrivateKey.(*ecdsa.PrivateKey), leafCertificate, intermediateCertificate)
|
||||
return m.SignWithSMimeECDSA(keyPairTlS.PrivateKey.(*ecdsa.PrivateKey), keyPairTlS.Leaf, intermediateCertificate)
|
||||
default:
|
||||
return fmt.Errorf("unsupported private key type: %T", keyPairTlS.PrivateKey)
|
||||
}
|
||||
}
|
||||
|
||||
// getLeafCertificate returns the leaf certificate from a tls.Certificate.
|
||||
// PLEASE NOTE: Before Go 1.23 Certificate.Leaf was left nil, and the parsed certificate was
|
||||
// discarded. This behavior can be re-enabled by setting "x509keypairleaf=0"
|
||||
// in the GODEBUG environment variable.
|
||||
func getLeafCertificate(keyPairTlS *tls.Certificate) (*x509.Certificate, error) {
|
||||
if keyPairTlS.Leaf != nil {
|
||||
return keyPairTlS.Leaf, nil
|
||||
}
|
||||
|
||||
cert, err := x509.ParseCertificate(keyPairTlS.Certificate[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
// This method allows you to specify a character set for the email message. The charset is
|
||||
// important for ensuring that the content of the message is correctly interpreted by
|
||||
// mail clients. Common charset values include UTF-8, ISO-8859-1, and others. If a charset
|
||||
|
@ -654,9 +632,6 @@ func (m *Msg) SetAddrHeader(header AddrHeader, values ...string) error {
|
|||
// References:
|
||||
// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.4
|
||||
func (m *Msg) SetAddrHeaderIgnoreInvalid(header AddrHeader, values ...string) {
|
||||
if m.addrHeader == nil {
|
||||
m.addrHeader = make(map[AddrHeader][]*mail.Address)
|
||||
}
|
||||
var addresses []*mail.Address
|
||||
for _, addrVal := range values {
|
||||
address, err := mail.ParseAddress(m.encodeString(addrVal))
|
||||
|
@ -665,14 +640,7 @@ func (m *Msg) SetAddrHeaderIgnoreInvalid(header AddrHeader, values ...string) {
|
|||
}
|
||||
addresses = append(addresses, address)
|
||||
}
|
||||
switch header {
|
||||
case HeaderFrom:
|
||||
if len(addresses) > 0 {
|
||||
m.addrHeader[header] = []*mail.Address{addresses[0]}
|
||||
}
|
||||
default:
|
||||
m.addrHeader[header] = addresses
|
||||
}
|
||||
m.addrHeader[header] = addresses
|
||||
}
|
||||
|
||||
// EnvelopeFrom sets the envelope from address for the Msg.
|
||||
|
@ -824,16 +792,7 @@ func (m *Msg) ToIgnoreInvalid(rcpts ...string) {
|
|||
// References:
|
||||
// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3
|
||||
func (m *Msg) ToFromString(rcpts string) error {
|
||||
src := strings.Split(rcpts, ",")
|
||||
var dst []string
|
||||
for _, address := range src {
|
||||
address = strings.TrimSpace(address)
|
||||
if address == "" {
|
||||
continue
|
||||
}
|
||||
dst = append(dst, address)
|
||||
}
|
||||
return m.To(dst...)
|
||||
return m.To(strings.Split(rcpts, ",")...)
|
||||
}
|
||||
|
||||
// Cc sets one or more "CC" (carbon copy) addresses in the mail body for the Msg.
|
||||
|
@ -918,16 +877,7 @@ func (m *Msg) CcIgnoreInvalid(rcpts ...string) {
|
|||
// References:
|
||||
// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3
|
||||
func (m *Msg) CcFromString(rcpts string) error {
|
||||
src := strings.Split(rcpts, ",")
|
||||
var dst []string
|
||||
for _, address := range src {
|
||||
address = strings.TrimSpace(address)
|
||||
if address == "" {
|
||||
continue
|
||||
}
|
||||
dst = append(dst, address)
|
||||
}
|
||||
return m.Cc(dst...)
|
||||
return m.Cc(strings.Split(rcpts, ",")...)
|
||||
}
|
||||
|
||||
// Bcc sets one or more "BCC" (blind carbon copy) addresses in the mail body for the Msg.
|
||||
|
@ -1013,16 +963,7 @@ func (m *Msg) BccIgnoreInvalid(rcpts ...string) {
|
|||
// References:
|
||||
// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3
|
||||
func (m *Msg) BccFromString(rcpts string) error {
|
||||
src := strings.Split(rcpts, ",")
|
||||
var dst []string
|
||||
for _, address := range src {
|
||||
address = strings.TrimSpace(address)
|
||||
if address == "" {
|
||||
continue
|
||||
}
|
||||
dst = append(dst, address)
|
||||
}
|
||||
return m.Bcc(dst...)
|
||||
return m.Bcc(strings.Split(rcpts, ",")...)
|
||||
}
|
||||
|
||||
// ReplyTo sets the "Reply-To" address for the Msg, specifying where replies should be sent.
|
||||
|
@ -1158,7 +1099,8 @@ func (m *Msg) SetBulk() {
|
|||
// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.3
|
||||
// - https://datatracker.ietf.org/doc/html/rfc1123
|
||||
func (m *Msg) SetDate() {
|
||||
m.SetDateWithValue(time.Now())
|
||||
now := time.Now().Format(time.RFC1123Z)
|
||||
m.SetGenHeader(HeaderDate, now)
|
||||
}
|
||||
|
||||
// SetDateWithValue sets the "Date" header for the Msg using the provided time value in a valid RFC 1123 format.
|
||||
|
@ -1258,9 +1200,6 @@ func (m *Msg) IsDelivered() bool {
|
|||
// References:
|
||||
// - https://datatracker.ietf.org/doc/html/rfc8098
|
||||
func (m *Msg) RequestMDNTo(rcpts ...string) error {
|
||||
if m.genHeader == nil {
|
||||
m.genHeader = make(map[Header][]string)
|
||||
}
|
||||
var addresses []string
|
||||
for _, addrVal := range rcpts {
|
||||
address, err := mail.ParseAddress(addrVal)
|
||||
|
@ -1269,7 +1208,9 @@ func (m *Msg) RequestMDNTo(rcpts ...string) error {
|
|||
}
|
||||
addresses = append(addresses, address.String())
|
||||
}
|
||||
m.genHeader[HeaderDispositionNotificationTo] = addresses
|
||||
if _, ok := m.genHeader[HeaderDispositionNotificationTo]; ok {
|
||||
m.genHeader[HeaderDispositionNotificationTo] = addresses
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -1308,11 +1249,11 @@ func (m *Msg) RequestMDNAddTo(rcpt string) error {
|
|||
return fmt.Errorf(errParseMailAddr, rcpt, err)
|
||||
}
|
||||
var addresses []string
|
||||
if current, ok := m.genHeader[HeaderDispositionNotificationTo]; ok {
|
||||
addresses = current
|
||||
}
|
||||
addresses = append(addresses, m.genHeader[HeaderDispositionNotificationTo]...)
|
||||
addresses = append(addresses, address.String())
|
||||
m.genHeader[HeaderDispositionNotificationTo] = addresses
|
||||
if _, ok := m.genHeader[HeaderDispositionNotificationTo]; ok {
|
||||
m.genHeader[HeaderDispositionNotificationTo] = addresses
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -1752,11 +1693,11 @@ func (m *Msg) SetBodyHTMLTemplate(tpl *ht.Template, data interface{}, opts ...Pa
|
|||
if tpl == nil {
|
||||
return errors.New(errTplPointerNil)
|
||||
}
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
if err := tpl.Execute(buffer, data); err != nil {
|
||||
buffer := bytes.Buffer{}
|
||||
if err := tpl.Execute(&buffer, data); err != nil {
|
||||
return fmt.Errorf(errTplExecuteFailed, err)
|
||||
}
|
||||
writeFunc := writeFuncFromBuffer(buffer)
|
||||
writeFunc := writeFuncFromBuffer(&buffer)
|
||||
m.SetBodyWriter(TypeTextHTML, writeFunc, opts...)
|
||||
return nil
|
||||
}
|
||||
|
@ -1783,11 +1724,11 @@ func (m *Msg) SetBodyTextTemplate(tpl *tt.Template, data interface{}, opts ...Pa
|
|||
if tpl == nil {
|
||||
return errors.New(errTplPointerNil)
|
||||
}
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
if err := tpl.Execute(buffer, data); err != nil {
|
||||
buf := bytes.Buffer{}
|
||||
if err := tpl.Execute(&buf, data); err != nil {
|
||||
return fmt.Errorf(errTplExecuteFailed, err)
|
||||
}
|
||||
writeFunc := writeFuncFromBuffer(buffer)
|
||||
writeFunc := writeFuncFromBuffer(&buf)
|
||||
m.SetBodyWriter(TypeTextPlain, writeFunc, opts...)
|
||||
return nil
|
||||
}
|
||||
|
@ -1857,11 +1798,11 @@ func (m *Msg) AddAlternativeHTMLTemplate(tpl *ht.Template, data interface{}, opt
|
|||
if tpl == nil {
|
||||
return errors.New(errTplPointerNil)
|
||||
}
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
if err := tpl.Execute(buffer, data); err != nil {
|
||||
buffer := bytes.Buffer{}
|
||||
if err := tpl.Execute(&buffer, data); err != nil {
|
||||
return fmt.Errorf(errTplExecuteFailed, err)
|
||||
}
|
||||
writeFunc := writeFuncFromBuffer(buffer)
|
||||
writeFunc := writeFuncFromBuffer(&buffer)
|
||||
m.AddAlternativeWriter(TypeTextHTML, writeFunc, opts...)
|
||||
return nil
|
||||
}
|
||||
|
@ -1887,11 +1828,11 @@ func (m *Msg) AddAlternativeTextTemplate(tpl *tt.Template, data interface{}, opt
|
|||
if tpl == nil {
|
||||
return errors.New(errTplPointerNil)
|
||||
}
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
if err := tpl.Execute(buffer, data); err != nil {
|
||||
buffer := bytes.Buffer{}
|
||||
if err := tpl.Execute(&buffer, data); err != nil {
|
||||
return fmt.Errorf(errTplExecuteFailed, err)
|
||||
}
|
||||
writeFunc := writeFuncFromBuffer(buffer)
|
||||
writeFunc := writeFuncFromBuffer(&buffer)
|
||||
m.AddAlternativeWriter(TypeTextPlain, writeFunc, opts...)
|
||||
return nil
|
||||
}
|
||||
|
@ -2488,8 +2429,8 @@ func (m *Msg) WriteToSendmailWithContext(ctx context.Context, sendmailPath strin
|
|||
// - https://datatracker.ietf.org/doc/html/rfc5322
|
||||
func (m *Msg) NewReader() *Reader {
|
||||
reader := &Reader{}
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
_, err := m.Write(buffer)
|
||||
buffer := bytes.Buffer{}
|
||||
_, err := m.Write(&buffer)
|
||||
if err != nil {
|
||||
reader.err = fmt.Errorf("failed to write Msg to Reader buffer: %w", err)
|
||||
}
|
||||
|
|
85
msg_nowin_test.go
Normal file
85
msg_nowin_test.go
Normal file
|
@ -0,0 +1,85 @@
|
|||
// SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package mail
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestMsg_WriteToSendmailWithContext tests the WriteToSendmailWithContext() method of the Msg
|
||||
func TestMsg_WriteToSendmailWithContext(t *testing.T) {
|
||||
if os.Getenv("TEST_SKIP_SENDMAIL") != "" {
|
||||
t.Skipf("TEST_SKIP_SENDMAIL variable is set. Skipping sendmail test")
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
sp string
|
||||
sf bool
|
||||
}{
|
||||
{"Sendmail path: /dev/null", "/dev/null", true},
|
||||
{"Sendmail path: /bin/cat", "/bin/cat", true},
|
||||
{"Sendmail path: /is/invalid", "/is/invalid", true},
|
||||
{"Sendmail path: /bin/echo", "/bin/echo", false},
|
||||
}
|
||||
m := NewMsg()
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx, cfn := context.WithTimeout(context.Background(), time.Second*10)
|
||||
defer cfn()
|
||||
m.SetBodyString(TypeTextPlain, "Plain")
|
||||
if err := m.WriteToSendmailWithContext(ctx, tt.sp); err != nil && !tt.sf {
|
||||
t.Errorf("WriteToSendmailWithCommand() failed: %s", err)
|
||||
}
|
||||
m.Reset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestMsg_WriteToSendmail will test the output to the local sendmail command
|
||||
func TestMsg_WriteToSendmail(t *testing.T) {
|
||||
if os.Getenv("TEST_SKIP_SENDMAIL") != "" {
|
||||
t.Skipf("TEST_SKIP_SENDMAIL variable is set. Skipping sendmail test")
|
||||
}
|
||||
_, err := os.Stat(SendmailPath)
|
||||
if err != nil {
|
||||
t.Skipf("local sendmail command not found in expected path. Skipping")
|
||||
}
|
||||
|
||||
m := NewMsg()
|
||||
_ = m.From("Toni Tester <tester@example.com>")
|
||||
_ = m.To(TestRcpt)
|
||||
m.SetBodyString(TypeTextPlain, "This is a test")
|
||||
if err := m.WriteToSendmail(); err != nil {
|
||||
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")
|
||||
}
|
||||
}
|
9709
msg_test.go
9709
msg_test.go
File diff suppressed because it is too large
Load diff
146
msg_unix_test.go
146
msg_unix_test.go
|
@ -1,146 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build linux || freebsd
|
||||
// +build linux freebsd
|
||||
|
||||
package mail
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMsg_AttachFile_unixOnly(t *testing.T) {
|
||||
t.Run("AttachFile with fileFromFS fails on open", func(t *testing.T) {
|
||||
if os.Getenv("PERFORM_UNIX_OPEN_WRITE_TESTS") != "true" {
|
||||
t.Skipf("PERFORM_UNIX_OPEN_WRITE_TESTS variable is not set. Skipping unix open/write tests")
|
||||
}
|
||||
|
||||
tempFile, err := os.CreateTemp("", "attachfile-open-write-test.*.txt")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp file: %s", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if err := os.Remove(tempFile.Name()); err != nil {
|
||||
t.Errorf("failed to remove temp file: %s", err)
|
||||
}
|
||||
})
|
||||
if err = os.Chmod(tempFile.Name(), 0o000); err != nil {
|
||||
t.Fatalf("failed to chmod temp file: %s", err)
|
||||
}
|
||||
|
||||
message := NewMsg()
|
||||
if message == nil {
|
||||
t.Fatal("message is nil")
|
||||
}
|
||||
message.AttachFile(tempFile.Name())
|
||||
attachments := message.GetAttachments()
|
||||
if len(attachments) != 1 {
|
||||
t.Fatalf("failed to get attachments, expected 1, got: %d", len(attachments))
|
||||
}
|
||||
messageBuf := bytes.NewBuffer(nil)
|
||||
_, err = attachments[0].Writer(messageBuf)
|
||||
if err == nil {
|
||||
t.Error("writer func expected to fail, but didn't")
|
||||
}
|
||||
if !errors.Is(err, os.ErrPermission) {
|
||||
t.Errorf("expected error to be %s, got: %s", os.ErrPermission, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestMsg_EmbedFile_unixOnly(t *testing.T) {
|
||||
t.Run("EmbedFile with fileFromFS fails on open", func(t *testing.T) {
|
||||
if os.Getenv("PERFORM_UNIX_OPEN_WRITE_TESTS") != "true" {
|
||||
t.Skipf("PERFORM_UNIX_OPEN_WRITE_TESTS variable is not set. Skipping unix open/write tests")
|
||||
}
|
||||
|
||||
tempFile, err := os.CreateTemp("", "embedfile-open-write-test.*.txt")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp file: %s", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if err := os.Remove(tempFile.Name()); err != nil {
|
||||
t.Errorf("failed to remove temp file: %s", err)
|
||||
}
|
||||
})
|
||||
if err = os.Chmod(tempFile.Name(), 0o000); err != nil {
|
||||
t.Fatalf("failed to chmod temp file: %s", err)
|
||||
}
|
||||
|
||||
message := NewMsg()
|
||||
if message == nil {
|
||||
t.Fatal("message is nil")
|
||||
}
|
||||
message.EmbedFile(tempFile.Name())
|
||||
embeds := message.GetEmbeds()
|
||||
if len(embeds) != 1 {
|
||||
t.Fatalf("failed to get embeds, expected 1, got: %d", len(embeds))
|
||||
}
|
||||
messageBuf := bytes.NewBuffer(nil)
|
||||
_, err = embeds[0].Writer(messageBuf)
|
||||
if err == nil {
|
||||
t.Error("writer func expected to fail, but didn't")
|
||||
}
|
||||
if !errors.Is(err, os.ErrPermission) {
|
||||
t.Errorf("expected error to be %s, got: %s", os.ErrPermission, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestMsg_WriteToFile_unixOnly(t *testing.T) {
|
||||
t.Run("WriteToFile fails on create", func(t *testing.T) {
|
||||
if os.Getenv("PERFORM_UNIX_OPEN_WRITE_TESTS") != "true" {
|
||||
t.Skipf("PERFORM_UNIX_OPEN_WRITE_TESTS variable is not set. Skipping unix open/write tests")
|
||||
}
|
||||
|
||||
tempfile, err := os.CreateTemp("", "testmail-create.*.eml")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp file: %s", err)
|
||||
}
|
||||
if err = os.Chmod(tempfile.Name(), 0o000); err != nil {
|
||||
t.Fatalf("failed to chmod temp file: %s", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if err = tempfile.Close(); err != nil {
|
||||
t.Fatalf("failed to close temp file: %s", err)
|
||||
}
|
||||
if err = os.Remove(tempfile.Name()); err != nil {
|
||||
t.Fatalf("failed to remove temp file: %s", err)
|
||||
}
|
||||
})
|
||||
message := testMessage(t)
|
||||
if err = message.WriteToFile(tempfile.Name()); err == nil {
|
||||
t.Errorf("expected error, got nil")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestMsg_WriteToTempFile_unixOnly(t *testing.T) {
|
||||
if os.Getenv("PERFORM_UNIX_OPEN_WRITE_TESTS") != "true" {
|
||||
t.Skipf("PERFORM_UNIX_OPEN_WRITE_TESTS variable is not set. Skipping unix open/write tests")
|
||||
}
|
||||
|
||||
t.Run("WriteToTempFile fails on invalid TMPDIR", func(t *testing.T) {
|
||||
// We store the current TMPDIR variable so we can set it back when the test is over
|
||||
curTmpDir := os.Getenv("TMPDIR")
|
||||
t.Cleanup(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.Fatalf("failed to set TMPDIR environment variable: %s", err)
|
||||
}
|
||||
message := testMessage(t)
|
||||
_, err := message.WriteToTempFile()
|
||||
if err == nil {
|
||||
t.Errorf("expected writing to invalid TMPDIR to fail, got: %s", err)
|
||||
}
|
||||
})
|
||||
}
|
|
@ -6,675 +6,153 @@ package mail
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// brokenWriter implements a broken writer for io.Writer testing
|
||||
type brokenWriter struct {
|
||||
io.Writer
|
||||
}
|
||||
|
||||
// Write implements the io.Writer interface but intentionally returns an error at
|
||||
// any time
|
||||
func (bw *brokenWriter) Write([]byte) (int, error) {
|
||||
return 0, fmt.Errorf("intentionally failed")
|
||||
}
|
||||
|
||||
// TestMsgWriter_Write tests the WriteTo() method of the msgWriter
|
||||
func TestMsgWriter_Write(t *testing.T) {
|
||||
t.Run("msgWriter writes to memory for all charsets", func(t *testing.T) {
|
||||
for _, tt := range charsetTests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
msgwriter := &msgWriter{
|
||||
writer: buffer,
|
||||
charset: tt.value,
|
||||
encoder: mime.QEncoding,
|
||||
}
|
||||
_, err := msgwriter.Write([]byte("test"))
|
||||
if err != nil {
|
||||
t.Errorf("msgWriter failed to write: %s", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
t.Run("msgWriter writes to memory for all encodings", func(t *testing.T) {
|
||||
for _, tt := range encodingTests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
msgwriter := &msgWriter{
|
||||
writer: buffer,
|
||||
charset: CharsetUTF8,
|
||||
encoder: getEncoder(tt.value),
|
||||
}
|
||||
_, err := msgwriter.Write([]byte("test"))
|
||||
if err != nil {
|
||||
t.Errorf("msgWriter failed to write: %s", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
t.Run("msgWriter should fail on write", func(t *testing.T) {
|
||||
msgwriter := &msgWriter{
|
||||
writer: failReadWriteSeekCloser{},
|
||||
charset: CharsetUTF8,
|
||||
encoder: getEncoder(EncodingQP),
|
||||
}
|
||||
_, err := msgwriter.Write([]byte("test"))
|
||||
if err == nil {
|
||||
t.Fatalf("msgWriter was supposed to fail on write")
|
||||
}
|
||||
})
|
||||
t.Run("msgWriter should fail on previous error", func(t *testing.T) {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
msgwriter := &msgWriter{
|
||||
writer: buffer,
|
||||
charset: CharsetUTF8,
|
||||
encoder: getEncoder(EncodingQP),
|
||||
}
|
||||
_, err := msgwriter.Write([]byte("test"))
|
||||
if err != nil {
|
||||
t.Errorf("msgWriter failed to write: %s", err)
|
||||
}
|
||||
msgwriter.err = errors.New("intentionally failed")
|
||||
_, err = msgwriter.Write([]byte("test2"))
|
||||
if err == nil {
|
||||
t.Fatalf("msgWriter was supposed to fail on second write")
|
||||
}
|
||||
})
|
||||
bw := &brokenWriter{}
|
||||
mw := &msgWriter{writer: bw, charset: CharsetUTF8, encoder: mime.QEncoding}
|
||||
_, err := mw.Write([]byte("test"))
|
||||
if err == nil {
|
||||
t.Errorf("msgWriter WriteTo() with brokenWriter should fail, but didn't")
|
||||
}
|
||||
|
||||
// Also test the part when a previous error happened
|
||||
mw.err = fmt.Errorf("broken")
|
||||
_, err = mw.Write([]byte("test"))
|
||||
if err == nil {
|
||||
t.Errorf("msgWriter WriteTo() with brokenWriter should fail, but didn't")
|
||||
}
|
||||
}
|
||||
|
||||
// TestMsgWriter_writeMsg tests the writeMsg method of the msgWriter
|
||||
func TestMsgWriter_writeMsg(t *testing.T) {
|
||||
msgwriter := &msgWriter{
|
||||
charset: CharsetUTF8,
|
||||
encoder: getEncoder(EncodingQP),
|
||||
}
|
||||
t.Run("msgWriter writes a simple message", func(t *testing.T) {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
now := time.Now()
|
||||
msgwriter.writer = buffer
|
||||
message := testMessage(t)
|
||||
message.SetDateWithValue(now)
|
||||
message.SetMessageIDWithValue("message@id.com")
|
||||
message.SetBulk()
|
||||
msgwriter.writeMsg(message)
|
||||
if msgwriter.err != nil {
|
||||
t.Errorf("msgWriter failed to write: %s", msgwriter.err)
|
||||
}
|
||||
m := NewMsg()
|
||||
_ = m.From(`"Toni Tester" <test@example.com>`)
|
||||
_ = m.To(`"Toni Receiver" <receiver@example.com>`)
|
||||
m.Subject("This is a subject")
|
||||
m.SetBulk()
|
||||
now := time.Now()
|
||||
m.SetDateWithValue(now)
|
||||
m.SetMessageIDWithValue("message@id.com")
|
||||
m.SetBodyString(TypeTextPlain, "This is the body")
|
||||
m.AddAlternativeString(TypeTextHTML, "This is the alternative body")
|
||||
buf := bytes.Buffer{}
|
||||
mw := &msgWriter{writer: &buf, charset: CharsetUTF8, encoder: mime.QEncoding}
|
||||
mw.writeMsg(m)
|
||||
ms := buf.String()
|
||||
|
||||
var incorrectFields []string
|
||||
if !strings.Contains(buffer.String(), "MIME-Version: 1.0\r\n") {
|
||||
incorrectFields = append(incorrectFields, "MIME-Version")
|
||||
var ea []string
|
||||
if !strings.Contains(ms, `MIME-Version: 1.0`) {
|
||||
ea = append(ea, "MIME-Version")
|
||||
}
|
||||
if !strings.Contains(ms, fmt.Sprintf("Date: %s", now.Format(time.RFC1123Z))) {
|
||||
ea = append(ea, "Date")
|
||||
}
|
||||
if !strings.Contains(ms, `Message-ID: <message@id.com>`) {
|
||||
ea = append(ea, "Message-ID")
|
||||
}
|
||||
if !strings.Contains(ms, `Precedence: bulk`) {
|
||||
ea = append(ea, "Precedence")
|
||||
}
|
||||
if !strings.Contains(ms, `Subject: This is a subject`) {
|
||||
ea = append(ea, "Subject")
|
||||
}
|
||||
if !strings.Contains(ms, `User-Agent: go-mail v`) {
|
||||
ea = append(ea, "User-Agent")
|
||||
}
|
||||
if !strings.Contains(ms, `X-Mailer: go-mail v`) {
|
||||
ea = append(ea, "X-Mailer")
|
||||
}
|
||||
if !strings.Contains(ms, `From: "Toni Tester" <test@example.com>`) {
|
||||
ea = append(ea, "From")
|
||||
}
|
||||
if !strings.Contains(ms, `To: "Toni Receiver" <receiver@example.com>`) {
|
||||
ea = append(ea, "To")
|
||||
}
|
||||
if !strings.Contains(ms, `Content-Type: text/plain; charset=UTF-8`) {
|
||||
ea = append(ea, "Content-Type")
|
||||
}
|
||||
if !strings.Contains(ms, `Content-Transfer-Encoding: quoted-printable`) {
|
||||
ea = append(ea, "Content-Transfer-Encoding")
|
||||
}
|
||||
if !strings.Contains(ms, "\r\n\r\nThis is the body") {
|
||||
ea = append(ea, "Message body")
|
||||
}
|
||||
|
||||
pl := m.GetParts()
|
||||
if len(pl) <= 0 {
|
||||
t.Errorf("expected multiple parts but got none")
|
||||
return
|
||||
}
|
||||
if len(pl) == 2 {
|
||||
ap := pl[1]
|
||||
ap.SetCharset(CharsetISO88591)
|
||||
}
|
||||
buf.Reset()
|
||||
mw.writeMsg(m)
|
||||
ms = buf.String()
|
||||
if !strings.Contains(ms, "\r\n\r\nThis is the alternative body") {
|
||||
ea = append(ea, "Message alternative body")
|
||||
}
|
||||
if !strings.Contains(ms, `Content-Type: text/html; charset=ISO-8859-1`) {
|
||||
ea = append(ea, "alternative body charset")
|
||||
}
|
||||
|
||||
if len(ea) > 0 {
|
||||
em := "writeMsg() failed. The following errors occurred:\n"
|
||||
for e := range ea {
|
||||
em += fmt.Sprintf("* incorrect %q field", ea[e])
|
||||
}
|
||||
if !strings.Contains(buffer.String(), fmt.Sprintf("Date: %s\r\n", now.Format(time.RFC1123Z))) {
|
||||
incorrectFields = append(incorrectFields, "Date")
|
||||
}
|
||||
if !strings.Contains(buffer.String(), "Message-ID: <message@id.com>\r\n") {
|
||||
incorrectFields = append(incorrectFields, "Message-ID")
|
||||
}
|
||||
if !strings.Contains(buffer.String(), "Precedence: bulk\r\n") {
|
||||
incorrectFields = append(incorrectFields, "Precedence")
|
||||
}
|
||||
if !strings.Contains(buffer.String(), "X-Auto-Response-Suppress: All\r\n") {
|
||||
incorrectFields = append(incorrectFields, "X-Auto-Response-Suppress")
|
||||
}
|
||||
if !strings.Contains(buffer.String(), "Subject: Testmail\r\n") {
|
||||
incorrectFields = append(incorrectFields, "Subject")
|
||||
}
|
||||
if !strings.Contains(buffer.String(), "User-Agent: go-mail v") {
|
||||
incorrectFields = append(incorrectFields, "User-Agent")
|
||||
}
|
||||
if !strings.Contains(buffer.String(), "X-Mailer: go-mail v") {
|
||||
incorrectFields = append(incorrectFields, "X-Mailer")
|
||||
}
|
||||
if !strings.Contains(buffer.String(), `From: <`+TestSenderValid+`>`) {
|
||||
incorrectFields = append(incorrectFields, "From")
|
||||
}
|
||||
if !strings.Contains(buffer.String(), `To: <`+TestRcptValid+`>`) {
|
||||
incorrectFields = append(incorrectFields, "From")
|
||||
}
|
||||
if !strings.Contains(buffer.String(), "Content-Type: text/plain; charset=UTF-8\r\n") {
|
||||
incorrectFields = append(incorrectFields, "Content-Type")
|
||||
}
|
||||
if !strings.Contains(buffer.String(), "Content-Transfer-Encoding: quoted-printable\r\n") {
|
||||
incorrectFields = append(incorrectFields, "Content-Transfer-Encoding")
|
||||
}
|
||||
if !strings.HasSuffix(buffer.String(), "\r\n\r\nTestmail") {
|
||||
incorrectFields = append(incorrectFields, "Message body")
|
||||
}
|
||||
if len(incorrectFields) > 0 {
|
||||
t.Fatalf("msgWriter failed to write correct fields: %s - mail: %s",
|
||||
strings.Join(incorrectFields, ", "), buffer.String())
|
||||
}
|
||||
})
|
||||
t.Run("msgWriter with no from address uses envelope from", func(t *testing.T) {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
msgwriter.writer = buffer
|
||||
message := NewMsg()
|
||||
if message == nil {
|
||||
t.Fatal("failed to create new message")
|
||||
}
|
||||
if err := message.EnvelopeFrom(TestSenderValid); err != nil {
|
||||
t.Errorf("failed to set sender address: %s", err)
|
||||
}
|
||||
if err := message.To(TestRcptValid); err != nil {
|
||||
t.Errorf("failed to set recipient address: %s", err)
|
||||
}
|
||||
message.Subject("Testmail")
|
||||
message.SetBodyString(TypeTextPlain, "Testmail")
|
||||
msgwriter.writeMsg(message)
|
||||
if msgwriter.err != nil {
|
||||
t.Errorf("msgWriter failed to write: %s", msgwriter.err)
|
||||
}
|
||||
if !strings.Contains(buffer.String(), "From: <"+TestSenderValid+">") {
|
||||
t.Errorf("expected envelope from address as from address, got: %s", buffer.String())
|
||||
}
|
||||
})
|
||||
t.Run("msgWriter with no from address or envelope from", func(t *testing.T) {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
msgwriter.writer = buffer
|
||||
message := NewMsg()
|
||||
if message == nil {
|
||||
t.Fatal("failed to create new message")
|
||||
}
|
||||
msgwriter.writeMsg(message)
|
||||
if msgwriter.err != nil {
|
||||
t.Errorf("msgWriter failed to write: %s", msgwriter.err)
|
||||
}
|
||||
if strings.Contains(buffer.String(), "From:") {
|
||||
t.Errorf("expected no from address, got: %s", buffer.String())
|
||||
}
|
||||
})
|
||||
t.Run("msgWriter writes a multipart/mixed message", func(t *testing.T) {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
msgwriter.writer = buffer
|
||||
message := testMessage(t, WithBoundary("testboundary"))
|
||||
message.AttachFile("testdata/attachment.txt")
|
||||
msgwriter.writeMsg(message)
|
||||
if msgwriter.err != nil {
|
||||
t.Errorf("msgWriter failed to write: %s", msgwriter.err)
|
||||
}
|
||||
if !strings.Contains(buffer.String(), "Content-Type: multipart/mixed") {
|
||||
t.Errorf("expected multipart/mixed, got: %s", buffer.String())
|
||||
}
|
||||
if !strings.Contains(buffer.String(), "--testboundary\r\n") {
|
||||
t.Errorf("expected boundary, got: %s", buffer.String())
|
||||
}
|
||||
if !strings.Contains(buffer.String(), "--testboundary--") {
|
||||
t.Errorf("expected end boundary, got: %s", buffer.String())
|
||||
}
|
||||
})
|
||||
t.Run("msgWriter writes a multipart/related message", func(t *testing.T) {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
msgwriter.writer = buffer
|
||||
message := testMessage(t, WithBoundary("testboundary"))
|
||||
message.EmbedFile("testdata/embed.txt")
|
||||
msgwriter.writeMsg(message)
|
||||
if msgwriter.err != nil {
|
||||
t.Errorf("msgWriter failed to write: %s", msgwriter.err)
|
||||
}
|
||||
if !strings.Contains(buffer.String(), "Content-Type: multipart/related") {
|
||||
t.Errorf("expected multipart/related, got: %s", buffer.String())
|
||||
}
|
||||
if !strings.Contains(buffer.String(), "--testboundary\r\n") {
|
||||
t.Errorf("expected boundary, got: %s", buffer.String())
|
||||
}
|
||||
if !strings.Contains(buffer.String(), "--testboundary--") {
|
||||
t.Errorf("expected end boundary, got: %s", buffer.String())
|
||||
}
|
||||
})
|
||||
t.Run("msgWriter writes a multipart/alternative message", func(t *testing.T) {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
msgwriter.writer = buffer
|
||||
message := testMessage(t, WithBoundary("testboundary"))
|
||||
message.AddAlternativeString(TypeTextHTML, "<html><body><h1>Testmail</h1></body></html>")
|
||||
msgwriter.writeMsg(message)
|
||||
if msgwriter.err != nil {
|
||||
t.Errorf("msgWriter failed to write: %s", msgwriter.err)
|
||||
}
|
||||
if !strings.Contains(buffer.String(), "Content-Type: multipart/alternative") {
|
||||
t.Errorf("expected multipart/alternative, got: %s", buffer.String())
|
||||
}
|
||||
if !strings.Contains(buffer.String(), "--testboundary\r\n") {
|
||||
t.Errorf("expected boundary, got: %s", buffer.String())
|
||||
}
|
||||
if !strings.Contains(buffer.String(), "--testboundary--") {
|
||||
t.Errorf("expected end boundary, got: %s", buffer.String())
|
||||
}
|
||||
})
|
||||
t.Run("msgWriter writes a application/pgp-encrypted message", func(t *testing.T) {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
msgwriter.writer = buffer
|
||||
message := testMessage(t, WithPGPType(PGPEncrypt), WithBoundary("testboundary"))
|
||||
msgwriter.writeMsg(message)
|
||||
if msgwriter.err != nil {
|
||||
t.Errorf("msgWriter failed to write: %s", msgwriter.err)
|
||||
}
|
||||
if !strings.Contains(buffer.String(), "Content-Type: multipart/encrypted") {
|
||||
t.Errorf("expected multipart/encrypted, got: %s", buffer.String())
|
||||
}
|
||||
if !strings.Contains(buffer.String(), "--testboundary\r\n") {
|
||||
t.Errorf("expected boundary, got: %s", buffer.String())
|
||||
}
|
||||
})
|
||||
t.Run("msgWriter writes a application/pgp-signature message", func(t *testing.T) {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
msgwriter.writer = buffer
|
||||
message := testMessage(t, WithPGPType(PGPSignature), WithBoundary("testboundary"))
|
||||
msgwriter.writeMsg(message)
|
||||
if msgwriter.err != nil {
|
||||
t.Errorf("msgWriter failed to write: %s", msgwriter.err)
|
||||
}
|
||||
if !strings.Contains(buffer.String(), "Content-Type: multipart/signed") {
|
||||
t.Errorf("expected multipart/signed, got: %s", buffer.String())
|
||||
}
|
||||
if !strings.Contains(buffer.String(), "--testboundary\r\n") {
|
||||
t.Errorf("expected boundary, got: %s", buffer.String())
|
||||
}
|
||||
})
|
||||
t.Run("msgWriter should ignore NoPGP", func(t *testing.T) {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
msgwriter.writer = buffer
|
||||
message := testMessage(t, WithBoundary("testboundary"))
|
||||
message.pgptype = 9
|
||||
msgwriter.writeMsg(message)
|
||||
if msgwriter.err != nil {
|
||||
t.Errorf("msgWriter failed to write: %s", msgwriter.err)
|
||||
}
|
||||
if !strings.Contains(buffer.String(), "--testboundary\r\n") {
|
||||
t.Errorf("expected boundary, got: %s", buffer.String())
|
||||
}
|
||||
})
|
||||
em += fmt.Sprintf("\n\nFull message:\n%s", ms)
|
||||
t.Error(em)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMsgWriter_writePreformattedGenHeader(t *testing.T) {
|
||||
t.Run("message with no preformatted headerset", func(t *testing.T) {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
msgwriter := &msgWriter{
|
||||
writer: buffer,
|
||||
charset: CharsetUTF8,
|
||||
encoder: getEncoder(EncodingQP),
|
||||
}
|
||||
message := testMessage(t)
|
||||
message.SetGenHeaderPreformatted(HeaderContentID, "This is a content id")
|
||||
msgwriter.writeMsg(message)
|
||||
if !strings.Contains(buffer.String(), "Content-ID: This is a content id\r\n") {
|
||||
t.Errorf("expected preformatted header, got: %s", buffer.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestMsgWriter_addFiles(t *testing.T) {
|
||||
msgwriter := &msgWriter{
|
||||
charset: CharsetUTF8,
|
||||
encoder: getEncoder(EncodingQP),
|
||||
// TestMsgWriter_writeMsg_PGP tests the writeMsg method of the msgWriter with PGP types set
|
||||
func TestMsgWriter_writeMsg_PGP(t *testing.T) {
|
||||
m := NewMsg(WithPGPType(PGPEncrypt))
|
||||
_ = 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, `encrypted; protocol="application/pgp-encrypted"`) {
|
||||
t.Errorf("writeMsg failed. Expected PGP encoding header but didn't find it in message output")
|
||||
}
|
||||
t.Run("message with a single file attached", func(t *testing.T) {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
msgwriter.writer = buffer
|
||||
message := testMessage(t)
|
||||
message.AttachFile("testdata/attachment.txt")
|
||||
msgwriter.writeMsg(message)
|
||||
if msgwriter.err != nil {
|
||||
t.Errorf("msgWriter failed to write: %s", msgwriter.err)
|
||||
}
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
if !strings.Contains(buffer.String(), "VGhpcyBpcyBhIHRlc3QgYXR0YWNobWVudA0K") {
|
||||
t.Errorf("attachment not found in mail message. Mail: %s", buffer.String())
|
||||
}
|
||||
default:
|
||||
if !strings.Contains(buffer.String(), "VGhpcyBpcyBhIHRlc3QgYXR0YWNobWVudAo=") {
|
||||
t.Errorf("attachment not found in mail message. Mail: %s", buffer.String())
|
||||
}
|
||||
}
|
||||
if !strings.Contains(buffer.String(), `Content-Disposition: attachment; filename="attachment.txt"`) {
|
||||
t.Errorf("Content-Dispositon header not found for attachment. Mail: %s", buffer.String())
|
||||
}
|
||||
switch runtime.GOOS {
|
||||
case "freebsd":
|
||||
if !strings.Contains(buffer.String(), `Content-Type: application/octet-stream; name="attachment.txt"`) {
|
||||
t.Errorf("Content-Type header not found for attachment. Mail: %s", buffer.String())
|
||||
}
|
||||
default:
|
||||
if !strings.Contains(buffer.String(), `Content-Type: text/plain; charset=utf-8; name="attachment.txt"`) {
|
||||
t.Errorf("Content-Type header not found for attachment. Mail: %s", buffer.String())
|
||||
}
|
||||
}
|
||||
})
|
||||
t.Run("message with a single file attached no extension", func(t *testing.T) {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
msgwriter.writer = buffer
|
||||
message := testMessage(t)
|
||||
message.AttachFile("testdata/attachment")
|
||||
msgwriter.writeMsg(message)
|
||||
if msgwriter.err != nil {
|
||||
t.Errorf("msgWriter failed to write: %s", msgwriter.err)
|
||||
}
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
if !strings.Contains(buffer.String(), "VGhpcyBpcyBhIHRlc3QgYXR0YWNobWVudA0K") {
|
||||
t.Errorf("attachment not found in mail message. Mail: %s", buffer.String())
|
||||
}
|
||||
default:
|
||||
if !strings.Contains(buffer.String(), "VGhpcyBpcyBhIHRlc3QgYXR0YWNobWVudAo=") {
|
||||
t.Errorf("attachment not found in mail message. Mail: %s", buffer.String())
|
||||
}
|
||||
}
|
||||
if !strings.Contains(buffer.String(), `Content-Disposition: attachment; filename="attachment"`) {
|
||||
t.Errorf("Content-Dispositon header not found for attachment. Mail: %s", buffer.String())
|
||||
}
|
||||
if !strings.Contains(buffer.String(), `Content-Type: application/octet-stream; name="attachment"`) {
|
||||
t.Errorf("Content-Type header not found for attachment. Mail: %s", buffer.String())
|
||||
}
|
||||
})
|
||||
t.Run("message with a single file attached custom content-type", func(t *testing.T) {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
msgwriter.writer = buffer
|
||||
message := testMessage(t)
|
||||
message.AttachFile("testdata/attachment.txt", WithFileContentType(TypeAppOctetStream))
|
||||
msgwriter.writeMsg(message)
|
||||
if msgwriter.err != nil {
|
||||
t.Errorf("msgWriter failed to write: %s", msgwriter.err)
|
||||
}
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
if !strings.Contains(buffer.String(), "VGhpcyBpcyBhIHRlc3QgYXR0YWNobWVudA0K") {
|
||||
t.Errorf("attachment not found in mail message. Mail: %s", buffer.String())
|
||||
}
|
||||
default:
|
||||
if !strings.Contains(buffer.String(), "VGhpcyBpcyBhIHRlc3QgYXR0YWNobWVudAo=") {
|
||||
t.Errorf("attachment not found in mail message. Mail: %s", buffer.String())
|
||||
}
|
||||
}
|
||||
if !strings.Contains(buffer.String(), `Content-Disposition: attachment; filename="attachment.txt"`) {
|
||||
t.Errorf("Content-Dispositon header not found for attachment. Mail: %s", buffer.String())
|
||||
}
|
||||
if !strings.Contains(buffer.String(), `Content-Type: application/octet-stream; name="attachment.txt"`) {
|
||||
t.Errorf("Content-Type header not found for attachment. Mail: %s", buffer.String())
|
||||
}
|
||||
})
|
||||
t.Run("message with a single file attached custom transfer-encoding", func(t *testing.T) {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
msgwriter.writer = buffer
|
||||
message := testMessage(t)
|
||||
message.AttachFile("testdata/attachment.txt", WithFileEncoding(EncodingUSASCII))
|
||||
msgwriter.writeMsg(message)
|
||||
if msgwriter.err != nil {
|
||||
t.Errorf("msgWriter failed to write: %s", msgwriter.err)
|
||||
}
|
||||
if !strings.Contains(buffer.String(), "\r\n\r\nThis is a test attachment") {
|
||||
t.Errorf("attachment not found in mail message. Mail: %s", buffer.String())
|
||||
}
|
||||
if !strings.Contains(buffer.String(), `Content-Disposition: attachment; filename="attachment.txt"`) {
|
||||
t.Errorf("Content-Dispositon header not found for attachment. Mail: %s", buffer.String())
|
||||
}
|
||||
switch runtime.GOOS {
|
||||
case "freebsd":
|
||||
if !strings.Contains(buffer.String(), `Content-Type: application/octet-stream; name="attachment.txt"`) {
|
||||
t.Errorf("Content-Type header not found for attachment. Mail: %s", buffer.String())
|
||||
}
|
||||
default:
|
||||
if !strings.Contains(buffer.String(), `Content-Type: text/plain; charset=utf-8; name="attachment.txt"`) {
|
||||
t.Errorf("Content-Type header not found for attachment. Mail: %s", buffer.String())
|
||||
}
|
||||
}
|
||||
if !strings.Contains(buffer.String(), `Content-Transfer-Encoding: 7bit`) {
|
||||
t.Errorf("Content-Transfer-Encoding header not found for attachment. Mail: %s", buffer.String())
|
||||
}
|
||||
})
|
||||
t.Run("message with a single file attached custom description", func(t *testing.T) {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
msgwriter.writer = buffer
|
||||
message := testMessage(t)
|
||||
message.AttachFile("testdata/attachment.txt", WithFileDescription("Testdescription"))
|
||||
msgwriter.writeMsg(message)
|
||||
if msgwriter.err != nil {
|
||||
t.Errorf("msgWriter failed to write: %s", msgwriter.err)
|
||||
}
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
if !strings.Contains(buffer.String(), "VGhpcyBpcyBhIHRlc3QgYXR0YWNobWVudA0K") {
|
||||
t.Errorf("attachment not found in mail message. Mail: %s", buffer.String())
|
||||
}
|
||||
default:
|
||||
if !strings.Contains(buffer.String(), "VGhpcyBpcyBhIHRlc3QgYXR0YWNobWVudAo=") {
|
||||
t.Errorf("attachment not found in mail message. Mail: %s", buffer.String())
|
||||
}
|
||||
}
|
||||
if !strings.Contains(buffer.String(), `Content-Disposition: attachment; filename="attachment.txt"`) {
|
||||
t.Errorf("Content-Dispositon header not found for attachment. Mail: %s", buffer.String())
|
||||
}
|
||||
switch runtime.GOOS {
|
||||
case "freebsd":
|
||||
if !strings.Contains(buffer.String(), `Content-Type: application/octet-stream; name="attachment.txt"`) {
|
||||
t.Errorf("Content-Type header not found for attachment. Mail: %s", buffer.String())
|
||||
}
|
||||
default:
|
||||
if !strings.Contains(buffer.String(), `Content-Type: text/plain; charset=utf-8; name="attachment.txt"`) {
|
||||
t.Errorf("Content-Type header not found for attachment. Mail: %s", buffer.String())
|
||||
}
|
||||
}
|
||||
if !strings.Contains(buffer.String(), `Content-Transfer-Encoding: base64`) {
|
||||
t.Errorf("Content-Transfer-Encoding header not found for attachment. Mail: %s", buffer.String())
|
||||
}
|
||||
if !strings.Contains(buffer.String(), `Content-Description: Testdescription`) {
|
||||
t.Errorf("Content-Description header not found for attachment. Mail: %s", buffer.String())
|
||||
}
|
||||
})
|
||||
t.Run("message with attachment but no body part", func(t *testing.T) {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
msgwriter.writer = buffer
|
||||
message := testMessage(t)
|
||||
message.parts = nil
|
||||
message.AttachFile("testdata/attachment.txt")
|
||||
msgwriter.writeMsg(message)
|
||||
if msgwriter.err != nil {
|
||||
t.Errorf("msgWriter failed to write: %s", msgwriter.err)
|
||||
}
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
if !strings.Contains(buffer.String(), "VGhpcyBpcyBhIHRlc3QgYXR0YWNobWVudA0K") {
|
||||
t.Errorf("attachment not found in mail message. Mail: %s", buffer.String())
|
||||
}
|
||||
default:
|
||||
if !strings.Contains(buffer.String(), "VGhpcyBpcyBhIHRlc3QgYXR0YWNobWVudAo=") {
|
||||
t.Errorf("attachment not found in mail message. Mail: %s", buffer.String())
|
||||
}
|
||||
}
|
||||
if !strings.Contains(buffer.String(), `Content-Disposition: attachment; filename="attachment.txt"`) {
|
||||
t.Errorf("Content-Dispositon header not found for attachment. Mail: %s", buffer.String())
|
||||
}
|
||||
switch runtime.GOOS {
|
||||
case "freebsd":
|
||||
if !strings.Contains(buffer.String(), `Content-Type: application/octet-stream; name="attachment.txt"`) {
|
||||
t.Errorf("Content-Type header not found for attachment. Mail: %s", buffer.String())
|
||||
}
|
||||
default:
|
||||
if !strings.Contains(buffer.String(), `Content-Type: text/plain; charset=utf-8; name="attachment.txt"`) {
|
||||
t.Errorf("Content-Type header not found for attachment. Mail: %s", buffer.String())
|
||||
}
|
||||
}
|
||||
if !strings.Contains(buffer.String(), `Content-Transfer-Encoding: base64`) {
|
||||
t.Errorf("Content-Transfer-Encoding header not found for attachment. Mail: %s", buffer.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestMsgWriter_writePart(t *testing.T) {
|
||||
msgwriter := &msgWriter{
|
||||
charset: CharsetUTF8,
|
||||
encoder: getEncoder(EncodingQP),
|
||||
m = NewMsg(WithPGPType(PGPSignature))
|
||||
_ = 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, `signed; protocol="application/pgp-signature"`) {
|
||||
t.Errorf("writeMsg failed. Expected PGP encoding header but didn't find it in message output")
|
||||
}
|
||||
t.Run("message with no part charset should use default message charset", func(t *testing.T) {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
msgwriter.writer = buffer
|
||||
message := testMessage(t, WithCharset(CharsetUTF7))
|
||||
message.AddAlternativeString(TypeTextPlain, "thisisatest")
|
||||
message.parts[1].charset = ""
|
||||
msgwriter.writeMsg(message)
|
||||
if msgwriter.err != nil {
|
||||
t.Errorf("msgWriter failed to write: %s", msgwriter.err)
|
||||
}
|
||||
if !strings.Contains(buffer.String(), "ontent-Type: text/plain; charset=UTF-7\r\n\r\nTestmail") {
|
||||
t.Errorf("part not found in mail message. Mail: %s", buffer.String())
|
||||
}
|
||||
if !strings.Contains(buffer.String(), "ontent-Type: text/plain; charset=UTF-7\r\n\r\nthisisatest") {
|
||||
t.Errorf("part not found in mail message. Mail: %s", buffer.String())
|
||||
}
|
||||
})
|
||||
t.Run("message with parts that have a description", func(t *testing.T) {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
msgwriter.writer = buffer
|
||||
message := testMessage(t)
|
||||
message.AddAlternativeString(TypeTextPlain, "thisisatest")
|
||||
message.parts[1].description = "thisisadescription"
|
||||
msgwriter.writeMsg(message)
|
||||
if msgwriter.err != nil {
|
||||
t.Errorf("msgWriter failed to write: %s", msgwriter.err)
|
||||
}
|
||||
if !strings.Contains(buffer.String(), "Content-Description: thisisadescription") {
|
||||
t.Errorf("part description not found in mail message. Mail: %s", buffer.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestMsgWriter_writeString(t *testing.T) {
|
||||
msgwriter := &msgWriter{
|
||||
charset: CharsetUTF8,
|
||||
encoder: getEncoder(EncodingQP),
|
||||
}
|
||||
t.Run("writeString succeeds", func(t *testing.T) {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
msgwriter.writer = buffer
|
||||
msgwriter.writeString("thisisatest")
|
||||
if !strings.EqualFold(buffer.String(), "thisisatest") {
|
||||
t.Errorf("writeString failed, expected: thisisatest got: %s", buffer.String())
|
||||
}
|
||||
})
|
||||
t.Run("writeString fails", func(t *testing.T) {
|
||||
msgwriter.writer = failReadWriteSeekCloser{}
|
||||
msgwriter.writeString("thisisatest")
|
||||
if msgwriter.err == nil {
|
||||
t.Errorf("writeString succeeded, expected error")
|
||||
}
|
||||
})
|
||||
t.Run("writeString on errored writer should return", func(t *testing.T) {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
msgwriter.writer = buffer
|
||||
msgwriter.err = errors.New("intentional error")
|
||||
msgwriter.writeString("thisisatest")
|
||||
if !strings.EqualFold(buffer.String(), "") {
|
||||
t.Errorf("writeString succeeded, expected: empty string, got: %s", buffer.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestMsgWriter_writeHeader(t *testing.T) {
|
||||
msgwriter := &msgWriter{
|
||||
charset: CharsetUTF8,
|
||||
encoder: getEncoder(EncodingQP),
|
||||
}
|
||||
t.Run("writeHeader with single value", func(t *testing.T) {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
msgwriter.writer = buffer
|
||||
msgwriter.writeHeader(HeaderMessageID, "this.is.a.test")
|
||||
if !strings.EqualFold(buffer.String(), "Message-ID: this.is.a.test\r\n") {
|
||||
t.Errorf("writeHeader failed, expected: %s, got: %s", "Message-ID: this.is.a.test",
|
||||
buffer.String())
|
||||
}
|
||||
})
|
||||
t.Run("writeHeader with multiple values", func(t *testing.T) {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
msgwriter.writer = buffer
|
||||
msgwriter.writeHeader(HeaderMessageID, "this.is.a.test", "this.as.well")
|
||||
if !strings.EqualFold(buffer.String(), "Message-ID: this.is.a.test, this.as.well\r\n") {
|
||||
t.Errorf("writeHeader failed, expected: %s, got: %s", "Message-ID: this.is.a.test, this.as.well",
|
||||
buffer.String())
|
||||
}
|
||||
})
|
||||
t.Run("writeHeader with no values", func(t *testing.T) {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
msgwriter.writer = buffer
|
||||
msgwriter.writeHeader(HeaderMessageID)
|
||||
// While technically it is permitted to have empty headers, it's recommend to omit them if
|
||||
// no value is present. We follow this recommendation.
|
||||
if !strings.EqualFold(buffer.String(), "") {
|
||||
t.Errorf("writeHeader failed, expected: %s, got: %s", "", buffer.String())
|
||||
}
|
||||
})
|
||||
t.Run("writeHeader with very long value", func(t *testing.T) {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
msgwriter.writer = buffer
|
||||
msgwriter.writeHeader(HeaderMessageID, strings.Repeat("a", MaxHeaderLength-13), "next-row")
|
||||
want := "Message-ID:\r\n " + strings.Repeat("a", MaxHeaderLength-13) + ",\r\n next-row\r\n"
|
||||
if !strings.EqualFold(buffer.String(), want) {
|
||||
t.Errorf("writeHeader failed, expected: %s, got: %s", want, buffer.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestMsgWriter_writeBody(t *testing.T) {
|
||||
t.Log("We only cover some edge-cases here, most of the functionality is tested already very thoroughly.")
|
||||
|
||||
msgwriter := &msgWriter{
|
||||
charset: CharsetUTF8,
|
||||
encoder: getEncoder(EncodingQP),
|
||||
}
|
||||
t.Run("writeBody on NoEncoding", func(t *testing.T) {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
msgwriter.writer = buffer
|
||||
message := testMessage(t)
|
||||
msgwriter.writeBody(message.parts[0].writeFunc, NoEncoding, false)
|
||||
if msgwriter.err != nil {
|
||||
t.Errorf("writeBody failed to write: %s", msgwriter.err)
|
||||
}
|
||||
})
|
||||
t.Run("writeBody on NoEncoding fails on write", func(t *testing.T) {
|
||||
msgwriter.writer = failReadWriteSeekCloser{}
|
||||
message := testMessage(t)
|
||||
msgwriter.writeBody(message.parts[0].writeFunc, NoEncoding, false)
|
||||
if msgwriter.err == nil {
|
||||
t.Errorf("writeBody succeeded, expected error")
|
||||
}
|
||||
if !strings.EqualFold(msgwriter.err.Error(), "bodyWriter io.Copy: intentional write failure") {
|
||||
t.Errorf("expected error: bodyWriter io.Copy: intentional write failure, got: %s", msgwriter.err)
|
||||
}
|
||||
})
|
||||
t.Run("writeBody on NoEncoding fails on writeFunc", func(t *testing.T) {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
msgwriter.writer = buffer
|
||||
writeFunc := func(io.Writer) (int64, error) {
|
||||
return 0, errors.New("intentional write failure")
|
||||
}
|
||||
msgwriter.writeBody(writeFunc, NoEncoding, false)
|
||||
if msgwriter.err == nil {
|
||||
t.Errorf("writeBody succeeded, expected error")
|
||||
}
|
||||
if !strings.EqualFold(msgwriter.err.Error(), "bodyWriter function: intentional write failure") {
|
||||
t.Errorf("expected error: bodyWriter function: intentional write failure, got: %s", msgwriter.err)
|
||||
}
|
||||
})
|
||||
t.Run("writeBody Quoted-Printable fails on write", func(t *testing.T) {
|
||||
msgwriter.writer = failReadWriteSeekCloser{}
|
||||
message := testMessage(t)
|
||||
msgwriter.writeBody(message.parts[0].writeFunc, EncodingQP, false)
|
||||
if msgwriter.err == nil {
|
||||
t.Errorf("writeBody succeeded, expected error")
|
||||
}
|
||||
if !strings.EqualFold(msgwriter.err.Error(), "bodyWriter function: intentional write failure") {
|
||||
t.Errorf("expected error: bodyWriter function: intentional write failure, got: %s", msgwriter.err)
|
||||
}
|
||||
})
|
||||
t.Run("writeBody Quoted-Printable fails on writeFunc", func(t *testing.T) {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
msgwriter.writer = buffer
|
||||
writeFunc := func(io.Writer) (int64, error) {
|
||||
return 0, errors.New("intentional write failure")
|
||||
}
|
||||
msgwriter.writeBody(writeFunc, EncodingQP, false)
|
||||
if msgwriter.err == nil {
|
||||
t.Errorf("writeBody succeeded, expected error")
|
||||
}
|
||||
if !strings.EqualFold(msgwriter.err.Error(), "bodyWriter function: intentional write failure") {
|
||||
t.Errorf("expected error: bodyWriter function: intentional write failure, got: %s", msgwriter.err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestMsgWriter_writeMsg_SMime tests the writeMsg method of the msgWriter with S/MIME types set
|
||||
|
|
|
@ -10,10 +10,9 @@ import (
|
|||
|
||||
// loginAuth is the type that satisfies the Auth interface for the "SMTP LOGIN" auth
|
||||
type loginAuth struct {
|
||||
username, password string
|
||||
host string
|
||||
respStep uint8
|
||||
allowUnencryptedAuth bool
|
||||
username, password string
|
||||
host string
|
||||
respStep uint8
|
||||
}
|
||||
|
||||
// LoginAuth returns an [Auth] that implements the LOGIN authentication
|
||||
|
@ -30,14 +29,14 @@ type loginAuth struct {
|
|||
// See: https://datatracker.ietf.org/doc/html/draft-murchison-sasl-login-00
|
||||
// Since there is no official standard RFC and we've seen different implementations
|
||||
// of this mechanism (sending "Username:", "Username", "username", "User name", etc.)
|
||||
// we follow the IETF-Draft and ignore any server challenge to allow compatibility
|
||||
// we follow the IETF-Draft and ignore any server challange to allow compatiblity
|
||||
// with most mail servers/providers.
|
||||
//
|
||||
// LoginAuth will only send the credentials if the connection is using TLS
|
||||
// or is connected to localhost. Otherwise authentication will fail with an
|
||||
// error, without sending the credentials.
|
||||
func LoginAuth(username, password, host string, allowUnEnc bool) Auth {
|
||||
return &loginAuth{username, password, host, 0, allowUnEnc}
|
||||
func LoginAuth(username, password, host string) Auth {
|
||||
return &loginAuth{username, password, host, 0}
|
||||
}
|
||||
|
||||
// Start begins the SMTP authentication process by validating server's TLS status and hostname.
|
||||
|
@ -48,7 +47,7 @@ func (a *loginAuth) Start(server *ServerInfo) (string, []byte, error) {
|
|||
// In particular, it doesn't matter if the server advertises LOGIN auth.
|
||||
// That might just be the attacker saying
|
||||
// "it's ok, you can trust me with your password."
|
||||
if !a.allowUnencryptedAuth && !server.TLS && !isLocalhost(server.Name) {
|
||||
if !server.TLS && !isLocalhost(server.Name) {
|
||||
return "", nil, ErrUnencrypted
|
||||
}
|
||||
if server.Name != a.host {
|
||||
|
|
|
@ -17,7 +17,6 @@ package smtp
|
|||
type plainAuth struct {
|
||||
identity, username, password string
|
||||
host string
|
||||
allowUnencryptedAuth bool
|
||||
}
|
||||
|
||||
// PlainAuth returns an [Auth] that implements the PLAIN authentication
|
||||
|
@ -28,8 +27,8 @@ type plainAuth struct {
|
|||
// PlainAuth will only send the credentials if the connection is using TLS
|
||||
// or is connected to localhost. Otherwise authentication will fail with an
|
||||
// error, without sending the credentials.
|
||||
func PlainAuth(identity, username, password, host string, allowUnEnc bool) Auth {
|
||||
return &plainAuth{identity, username, password, host, allowUnEnc}
|
||||
func PlainAuth(identity, username, password, host string) Auth {
|
||||
return &plainAuth{identity, username, password, host}
|
||||
}
|
||||
|
||||
func (a *plainAuth) Start(server *ServerInfo) (string, []byte, error) {
|
||||
|
@ -38,7 +37,7 @@ func (a *plainAuth) Start(server *ServerInfo) (string, []byte, error) {
|
|||
// In particular, it doesn't matter if the server advertises PLAIN auth.
|
||||
// That might just be the attacker saying
|
||||
// "it's ok, you can trust me with your password."
|
||||
if !a.allowUnencryptedAuth && !server.TLS && !isLocalhost(server.Name) {
|
||||
if !server.TLS && !isLocalhost(server.Name) {
|
||||
return "", nil, ErrUnencrypted
|
||||
}
|
||||
if server.Name != a.host {
|
||||
|
|
|
@ -67,7 +67,7 @@ var (
|
|||
func ExamplePlainAuth() {
|
||||
// hostname is used by PlainAuth to validate the TLS certificate.
|
||||
hostname := "mail.example.com"
|
||||
auth := smtp.PlainAuth("", "user@example.com", "password", hostname, false)
|
||||
auth := smtp.PlainAuth("", "user@example.com", "password", hostname)
|
||||
|
||||
err := smtp.SendMail(hostname+":25", auth, from, recipients, msg)
|
||||
if err != nil {
|
||||
|
@ -77,7 +77,7 @@ func ExamplePlainAuth() {
|
|||
|
||||
func ExampleSendMail() {
|
||||
// Set up authentication information.
|
||||
auth := smtp.PlainAuth("", "user@example.com", "password", "mail.example.com", false)
|
||||
auth := smtp.PlainAuth("", "user@example.com", "password", "mail.example.com")
|
||||
|
||||
// Connect to the server, authenticate, set the sender and recipient,
|
||||
// and send the email all in one step.
|
||||
|
|
51
smtp/smtp.go
51
smtp/smtp.go
|
@ -54,9 +54,6 @@ type Client struct {
|
|||
// auth supported auth mechanisms
|
||||
auth []string
|
||||
|
||||
// authIsActive indicates that the Client is currently during SMTP authentication
|
||||
authIsActive bool
|
||||
|
||||
// keep a reference to the connection so it can be used to create a TLS connection later
|
||||
conn net.Conn
|
||||
|
||||
|
@ -81,9 +78,6 @@ type Client struct {
|
|||
// isConnected indicates if the Client has an active connection
|
||||
isConnected bool
|
||||
|
||||
// logAuthData indicates if the Client should include SMTP authentication data in the logs
|
||||
logAuthData bool
|
||||
|
||||
// localName is the name to use in HELO/EHLO
|
||||
localName string // the name to use in HELO/EHLO
|
||||
|
||||
|
@ -180,15 +174,7 @@ func (c *Client) Hello(localName string) error {
|
|||
func (c *Client) cmd(expectCode int, format string, args ...interface{}) (int, string, error) {
|
||||
c.mutex.Lock()
|
||||
|
||||
var logMsg []interface{}
|
||||
logMsg = args
|
||||
logFmt := format
|
||||
if c.authIsActive {
|
||||
logMsg = []interface{}{"<SMTP auth data redacted>"}
|
||||
logFmt = "%s"
|
||||
}
|
||||
c.debugLog(log.DirClientToServer, logFmt, logMsg...)
|
||||
|
||||
c.debugLog(log.DirClientToServer, format, args...)
|
||||
id, err := c.Text.Cmd(format, args...)
|
||||
if err != nil {
|
||||
c.mutex.Unlock()
|
||||
|
@ -196,13 +182,7 @@ func (c *Client) cmd(expectCode int, format string, args ...interface{}) (int, s
|
|||
}
|
||||
c.Text.StartResponse(id)
|
||||
code, msg, err := c.Text.ReadResponse(expectCode)
|
||||
|
||||
logMsg = []interface{}{code, msg}
|
||||
if c.authIsActive && code >= 300 && code <= 400 {
|
||||
logMsg = []interface{}{code, "<SMTP auth data redacted>"}
|
||||
}
|
||||
c.debugLog(log.DirServerToClient, "%d %s", logMsg...)
|
||||
|
||||
c.debugLog(log.DirServerToClient, "%d %s", code, msg)
|
||||
c.Text.EndResponse(id)
|
||||
c.mutex.Unlock()
|
||||
return code, msg, err
|
||||
|
@ -276,20 +256,6 @@ func (c *Client) Auth(a Auth) error {
|
|||
if err := c.hello(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.mutex.Lock()
|
||||
if !c.logAuthData {
|
||||
c.authIsActive = true
|
||||
}
|
||||
c.mutex.Unlock()
|
||||
defer func() {
|
||||
c.mutex.Lock()
|
||||
if !c.logAuthData {
|
||||
c.authIsActive = false
|
||||
}
|
||||
c.mutex.Unlock()
|
||||
}()
|
||||
|
||||
encoding := base64.StdEncoding
|
||||
mech, resp, err := a.Start(&ServerInfo{c.serverName, c.tls, c.auth})
|
||||
if err != nil {
|
||||
|
@ -554,9 +520,9 @@ func (c *Client) Noop() error {
|
|||
|
||||
// Quit sends the QUIT command and closes the connection to the server.
|
||||
func (c *Client) Quit() error {
|
||||
// See https://github.com/golang/go/issues/70011
|
||||
_ = c.hello() // ignore error; we're quitting anyhow
|
||||
|
||||
if err := c.hello(); err != nil {
|
||||
return err
|
||||
}
|
||||
_, _, err := c.cmd(221, "QUIT")
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -590,13 +556,6 @@ func (c *Client) SetLogger(l log.Logger) {
|
|||
c.logger = l
|
||||
}
|
||||
|
||||
// SetLogAuthData enables logging of authentication data in the Client.
|
||||
func (c *Client) SetLogAuthData() {
|
||||
c.mutex.Lock()
|
||||
c.logAuthData = true
|
||||
c.mutex.Unlock()
|
||||
}
|
||||
|
||||
// SetDSNMailReturnOption sets the DSN mail return option for the Mail method
|
||||
func (c *Client) SetDSNMailReturnOption(d string) {
|
||||
c.dsnmrtype = d
|
||||
|
|
|
@ -50,7 +50,7 @@ type authTest struct {
|
|||
|
||||
var authTests = []authTest{
|
||||
{
|
||||
PlainAuth("", "user", "pass", "testserver", false),
|
||||
PlainAuth("", "user", "pass", "testserver"),
|
||||
[]string{},
|
||||
"PLAIN",
|
||||
[]string{"\x00user\x00pass"},
|
||||
|
@ -58,15 +58,7 @@ var authTests = []authTest{
|
|||
false,
|
||||
},
|
||||
{
|
||||
PlainAuth("", "user", "pass", "testserver", true),
|
||||
[]string{},
|
||||
"PLAIN",
|
||||
[]string{"\x00user\x00pass"},
|
||||
[]bool{false, false},
|
||||
false,
|
||||
},
|
||||
{
|
||||
PlainAuth("foo", "bar", "baz", "testserver", false),
|
||||
PlainAuth("foo", "bar", "baz", "testserver"),
|
||||
[]string{},
|
||||
"PLAIN",
|
||||
[]string{"foo\x00bar\x00baz"},
|
||||
|
@ -74,7 +66,7 @@ var authTests = []authTest{
|
|||
false,
|
||||
},
|
||||
{
|
||||
PlainAuth("foo", "bar", "baz", "testserver", false),
|
||||
PlainAuth("foo", "bar", "baz", "testserver"),
|
||||
[]string{"foo"},
|
||||
"PLAIN",
|
||||
[]string{"foo\x00bar\x00baz", ""},
|
||||
|
@ -82,7 +74,7 @@ var authTests = []authTest{
|
|||
false,
|
||||
},
|
||||
{
|
||||
LoginAuth("user", "pass", "testserver", false),
|
||||
LoginAuth("user", "pass", "testserver"),
|
||||
[]string{"Username:", "Password:"},
|
||||
"LOGIN",
|
||||
[]string{"", "user", "pass"},
|
||||
|
@ -90,15 +82,7 @@ var authTests = []authTest{
|
|||
false,
|
||||
},
|
||||
{
|
||||
LoginAuth("user", "pass", "testserver", true),
|
||||
[]string{"Username:", "Password:"},
|
||||
"LOGIN",
|
||||
[]string{"", "user", "pass"},
|
||||
[]bool{false, false},
|
||||
false,
|
||||
},
|
||||
{
|
||||
LoginAuth("user", "pass", "testserver", false),
|
||||
LoginAuth("user", "pass", "testserver"),
|
||||
[]string{"User Name\x00", "Password\x00"},
|
||||
"LOGIN",
|
||||
[]string{"", "user", "pass"},
|
||||
|
@ -106,7 +90,7 @@ var authTests = []authTest{
|
|||
false,
|
||||
},
|
||||
{
|
||||
LoginAuth("user", "pass", "testserver", false),
|
||||
LoginAuth("user", "pass", "testserver"),
|
||||
[]string{"Invalid", "Invalid:"},
|
||||
"LOGIN",
|
||||
[]string{"", "user", "pass"},
|
||||
|
@ -114,7 +98,7 @@ var authTests = []authTest{
|
|||
false,
|
||||
},
|
||||
{
|
||||
LoginAuth("user", "pass", "testserver", false),
|
||||
LoginAuth("user", "pass", "testserver"),
|
||||
[]string{"Invalid", "Invalid:", "Too many"},
|
||||
"LOGIN",
|
||||
[]string{"", "user", "pass", ""},
|
||||
|
@ -253,47 +237,7 @@ func TestAuthPlain(t *testing.T) {
|
|||
},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
auth := PlainAuth("foo", "bar", "baz", tt.authName, false)
|
||||
_, _, err := auth.Start(tt.server)
|
||||
got := ""
|
||||
if err != nil {
|
||||
got = err.Error()
|
||||
}
|
||||
if got != tt.err {
|
||||
t.Errorf("%d. got error = %q; want %q", i, got, tt.err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthPlainNoEnc(t *testing.T) {
|
||||
tests := []struct {
|
||||
authName string
|
||||
server *ServerInfo
|
||||
err string
|
||||
}{
|
||||
{
|
||||
authName: "servername",
|
||||
server: &ServerInfo{Name: "servername", TLS: true},
|
||||
},
|
||||
{
|
||||
// OK to use PlainAuth on localhost without TLS
|
||||
authName: "localhost",
|
||||
server: &ServerInfo{Name: "localhost", TLS: false},
|
||||
},
|
||||
{
|
||||
// Also OK on non-TLS secured connections. The NoEnc mechanism is meant to allow
|
||||
// non-encrypted connections.
|
||||
authName: "servername",
|
||||
server: &ServerInfo{Name: "servername", Auth: []string{"PLAIN"}},
|
||||
},
|
||||
{
|
||||
authName: "servername",
|
||||
server: &ServerInfo{Name: "attacker", TLS: true},
|
||||
err: "wrong host name",
|
||||
},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
auth := PlainAuth("foo", "bar", "baz", tt.authName, true)
|
||||
auth := PlainAuth("foo", "bar", "baz", tt.authName)
|
||||
_, _, err := auth.Start(tt.server)
|
||||
got := ""
|
||||
if err != nil {
|
||||
|
@ -339,51 +283,7 @@ func TestAuthLogin(t *testing.T) {
|
|||
},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
auth := LoginAuth("foo", "bar", tt.authName, false)
|
||||
_, _, err := auth.Start(tt.server)
|
||||
got := ""
|
||||
if err != nil {
|
||||
got = err.Error()
|
||||
}
|
||||
if got != tt.err {
|
||||
t.Errorf("%d. got error = %q; want %q", i, got, tt.err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthLoginNoEnc(t *testing.T) {
|
||||
tests := []struct {
|
||||
authName string
|
||||
server *ServerInfo
|
||||
err string
|
||||
}{
|
||||
{
|
||||
authName: "servername",
|
||||
server: &ServerInfo{Name: "servername", TLS: true},
|
||||
},
|
||||
{
|
||||
// OK to use LoginAuth on localhost without TLS
|
||||
authName: "localhost",
|
||||
server: &ServerInfo{Name: "localhost", TLS: false},
|
||||
},
|
||||
{
|
||||
// Also OK on non-TLS secured connections. The NoEnc mechanism is meant to allow
|
||||
// non-encrypted connections.
|
||||
authName: "servername",
|
||||
server: &ServerInfo{Name: "servername", Auth: []string{"LOGIN"}},
|
||||
},
|
||||
{
|
||||
authName: "servername",
|
||||
server: &ServerInfo{Name: "servername", Auth: []string{"CRAM-MD5"}},
|
||||
},
|
||||
{
|
||||
authName: "servername",
|
||||
server: &ServerInfo{Name: "attacker", TLS: true},
|
||||
err: "wrong host name",
|
||||
},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
auth := LoginAuth("foo", "bar", tt.authName, true)
|
||||
auth := LoginAuth("foo", "bar", tt.authName)
|
||||
_, _, err := auth.Start(tt.server)
|
||||
got := ""
|
||||
if err != nil {
|
||||
|
@ -417,11 +317,7 @@ func TestXOAuth2OK(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatalf("NewClient: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := c.Close(); err != nil {
|
||||
t.Errorf("failed to close client: %s", err)
|
||||
}
|
||||
}()
|
||||
defer c.Close()
|
||||
|
||||
auth := XOAuth2Auth("user", "token")
|
||||
err = c.Auth(auth)
|
||||
|
@ -459,11 +355,7 @@ func TestXOAuth2Error(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatalf("NewClient: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := c.Close(); err != nil {
|
||||
t.Errorf("failed to close client: %s", err)
|
||||
}
|
||||
}()
|
||||
defer c.Close()
|
||||
|
||||
auth := XOAuth2Auth("user", "token")
|
||||
err = c.Auth(auth)
|
||||
|
@ -815,7 +707,7 @@ func TestBasic(t *testing.T) {
|
|||
// fake TLS so authentication won't complain
|
||||
c.tls = true
|
||||
c.serverName = "smtp.google.com"
|
||||
if err := c.Auth(PlainAuth("", "user", "pass", "smtp.google.com", false)); err != nil {
|
||||
if err := c.Auth(PlainAuth("", "user", "pass", "smtp.google.com")); err != nil {
|
||||
t.Fatalf("AUTH failed: %s", err)
|
||||
}
|
||||
|
||||
|
@ -900,35 +792,6 @@ Goodbye.
|
|||
QUIT
|
||||
`
|
||||
|
||||
func TestHELOFailed(t *testing.T) {
|
||||
serverLines := `502 EH?
|
||||
502 EH?
|
||||
221 OK
|
||||
`
|
||||
clientLines := `EHLO localhost
|
||||
HELO localhost
|
||||
QUIT
|
||||
`
|
||||
server := strings.Join(strings.Split(serverLines, "\n"), "\r\n")
|
||||
client := strings.Join(strings.Split(clientLines, "\n"), "\r\n")
|
||||
var cmdbuf strings.Builder
|
||||
bcmdbuf := bufio.NewWriter(&cmdbuf)
|
||||
var fake faker
|
||||
fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(server)), bcmdbuf)
|
||||
c := &Client{Text: textproto.NewConn(fake), localName: "localhost"}
|
||||
if err := c.Hello("localhost"); err == nil {
|
||||
t.Fatal("expected EHLO to fail")
|
||||
}
|
||||
if err := c.Quit(); err != nil {
|
||||
t.Errorf("QUIT failed: %s", err)
|
||||
}
|
||||
_ = bcmdbuf.Flush()
|
||||
actual := cmdbuf.String()
|
||||
if client != actual {
|
||||
t.Errorf("Got:\n%s\nWant:\n%s", actual, client)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtensions(t *testing.T) {
|
||||
fake := func(server string) (c *Client, bcmdbuf *bufio.Writer, cmdbuf *strings.Builder) {
|
||||
server = strings.Join(strings.Split(server, "\n"), "\r\n")
|
||||
|
@ -1248,32 +1111,6 @@ func TestClient_SetLogger(t *testing.T) {
|
|||
c.logger.Debugf(log.Log{Direction: log.DirServerToClient, Format: "%s", Messages: []interface{}{"test"}})
|
||||
}
|
||||
|
||||
func TestClient_SetLogAuthData(t *testing.T) {
|
||||
server := strings.Join(strings.Split(newClientServer, "\n"), "\r\n")
|
||||
|
||||
var cmdbuf strings.Builder
|
||||
bcmdbuf := bufio.NewWriter(&cmdbuf)
|
||||
out := func() string {
|
||||
if err := bcmdbuf.Flush(); err != nil {
|
||||
t.Errorf("failed to flush: %s", err)
|
||||
}
|
||||
return cmdbuf.String()
|
||||
}
|
||||
var fake faker
|
||||
fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(server)), bcmdbuf)
|
||||
c, err := NewClient(fake, "fake.host")
|
||||
if err != nil {
|
||||
t.Fatalf("NewClient: %v\n(after %v)", err, out())
|
||||
}
|
||||
defer func() {
|
||||
_ = c.Close()
|
||||
}()
|
||||
c.SetLogAuthData()
|
||||
if !c.logAuthData {
|
||||
t.Error("Expected logAuthData to be true but received false")
|
||||
}
|
||||
}
|
||||
|
||||
var newClientServer = `220 hello world
|
||||
250-mx.google.com at your service
|
||||
250-SIZE 35651584
|
||||
|
@ -1415,7 +1252,7 @@ func TestHello(t *testing.T) {
|
|||
case 3:
|
||||
c.tls = true
|
||||
c.serverName = "smtp.google.com"
|
||||
err = c.Auth(PlainAuth("", "user", "pass", "smtp.google.com", false))
|
||||
err = c.Auth(PlainAuth("", "user", "pass", "smtp.google.com"))
|
||||
case 4:
|
||||
err = c.Mail("test@example.com")
|
||||
case 5:
|
||||
|
@ -1660,7 +1497,7 @@ func TestSendMailWithAuth(t *testing.T) {
|
|||
}
|
||||
}()
|
||||
|
||||
err = SendMail(l.Addr().String(), PlainAuth("", "user", "pass", "smtp.google.com", false), "test@example.com", []string{"other@example.com"}, []byte(strings.Replace(`From: test@example.com
|
||||
err = SendMail(l.Addr().String(), PlainAuth("", "user", "pass", "smtp.google.com"), "test@example.com", []string{"other@example.com"}, []byte(strings.Replace(`From: test@example.com
|
||||
To: other@example.com
|
||||
Subject: SendMail test
|
||||
|
||||
|
@ -1668,7 +1505,6 @@ SendMail is working for me.
|
|||
`, "\n", "\r\n", -1)))
|
||||
if err == nil {
|
||||
t.Error("SendMail: Server doesn't support AUTH, expected to get an error, but got none ")
|
||||
return
|
||||
}
|
||||
if err.Error() != "smtp: server doesn't support AUTH" {
|
||||
t.Errorf("Expected: smtp: server doesn't support AUTH, got: %s", err)
|
||||
|
@ -1696,7 +1532,7 @@ func TestAuthFailed(t *testing.T) {
|
|||
|
||||
c.tls = true
|
||||
c.serverName = "smtp.google.com"
|
||||
err = c.Auth(PlainAuth("", "user", "pass", "smtp.google.com", false))
|
||||
err = c.Auth(PlainAuth("", "user", "pass", "smtp.google.com"))
|
||||
|
||||
if err == nil {
|
||||
t.Error("Auth: expected error; got none")
|
||||
|
@ -2301,7 +2137,7 @@ func SkipFlaky(t testing.TB, issue int) {
|
|||
}
|
||||
|
||||
// testSCRAMSMTPServer represents a test server for SCRAM-based SMTP authentication.
|
||||
// It does not do any acutal computation of the challenges but verifies that the expected
|
||||
// It does not do any acutal computation of the challanges but verifies that the expected
|
||||
// fields are present. We have actual real authentication tests for all SCRAM modes in the
|
||||
// go-mail client_test.go
|
||||
type testSCRAMSMTPServer struct {
|
||||
|
|
8
testdata/RFC5322-A1-1-invalid-from.eml
vendored
8
testdata/RFC5322-A1-1-invalid-from.eml
vendored
|
@ -1,8 +0,0 @@
|
|||
From: §§§§§§§§
|
||||
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".
|
|
@ -1,3 +0,0 @@
|
|||
// SPDX-FileCopyrightText: Copyright (c) 2022-2024 The go-mail Authors
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
8
testdata/RFC5322-A1-1.eml
vendored
8
testdata/RFC5322-A1-1.eml
vendored
|
@ -1,8 +0,0 @@
|
|||
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".
|
3
testdata/RFC5322-A1-1.eml.license
vendored
3
testdata/RFC5322-A1-1.eml.license
vendored
|
@ -1,3 +0,0 @@
|
|||
// SPDX-FileCopyrightText: Copyright (c) 2022-2024 The go-mail Authors
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
1
testdata/attachment
vendored
1
testdata/attachment
vendored
|
@ -1 +0,0 @@
|
|||
This is a test attachment
|
3
testdata/attachment.license
vendored
3
testdata/attachment.license
vendored
|
@ -1,3 +0,0 @@
|
|||
// SPDX-FileCopyrightText: Copyright (c) 2022-2024 The go-mail Authors
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
1
testdata/attachment.txt
vendored
1
testdata/attachment.txt
vendored
|
@ -1 +0,0 @@
|
|||
This is a test attachment
|
3
testdata/attachment.txt.license
vendored
3
testdata/attachment.txt.license
vendored
|
@ -1,3 +0,0 @@
|
|||
// SPDX-FileCopyrightText: Copyright (c) 2022-2024 The go-mail Authors
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
1
testdata/embed.txt
vendored
1
testdata/embed.txt
vendored
|
@ -1 +0,0 @@
|
|||
This is a test embed
|
3
testdata/embed.txt.license
vendored
3
testdata/embed.txt.license
vendored
|
@ -1,3 +0,0 @@
|
|||
// SPDX-FileCopyrightText: Copyright (c) 2022-2024 The go-mail Authors
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
1
testdata/logo.svg
vendored
1
testdata/logo.svg
vendored
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 20 KiB |
367
testdata/logo.svg.base64
vendored
367
testdata/logo.svg.base64
vendored
|
@ -1,367 +0,0 @@
|
|||
PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PCFE
|
||||
T0NUWVBFIHN2ZyBQVUJMSUMgIi0vL1czQy8vRFREIFNWRyAxLjEvL0VOIiAiaHR0cDovL3d3dy53
|
||||
My5vcmcvR3JhcGhpY3MvU1ZHLzEuMS9EVEQvc3ZnMTEuZHRkIj48c3ZnIHdpZHRoPSIxMDAlIiBo
|
||||
ZWlnaHQ9IjEwMCUiIHZpZXdCb3g9IjAgMCA2MDAgNjAwIiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJo
|
||||
dHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3Jn
|
||||
LzE5OTkveGxpbmsiIHhtbDpzcGFjZT0icHJlc2VydmUiIHhtbG5zOnNlcmlmPSJodHRwOi8vd3d3
|
||||
LnNlcmlmLmNvbS8iIHN0eWxlPSJmaWxsLXJ1bGU6ZXZlbm9kZDtjbGlwLXJ1bGU6ZXZlbm9kZDtz
|
||||
dHJva2UtbGluZWNhcDpyb3VuZDtzdHJva2UtbGluZWpvaW46cm91bmQ7c3Ryb2tlLW1pdGVybGlt
|
||||
aXQ6MS41OyI+PHJlY3QgaWQ9ItCc0L7QvdGC0LDQttC90LDRjy3QvtCx0LvQsNGB0YLRjDEiIHNl
|
||||
cmlmOmlkPSLQnNC+0L3RgtCw0LbQvdCw0Y8g0L7QsdC70LDRgdGC0YwxIiB4PSIwIiB5PSIwIiB3
|
||||
aWR0aD0iNjAwIiBoZWlnaHQ9IjYwMCIgc3R5bGU9ImZpbGw6bm9uZTsiLz48Zz48cGF0aCBkPSJN
|
||||
NTg0LjcyNiw5OC4zOTVjLTAsLTkuNzM4IC03LjkwNywtMTcuNjQ1IC0xNy42NDUsLTE3LjY0NWwt
|
||||
NTEwLjAyNywwYy05LjczOSwwIC0xNy42NDUsNy45MDcgLTE3LjY0NSwxNy42NDVsLTAsMzE5Ljc5
|
||||
NGMtMCw5LjczOSA3LjkwNiwxNy42NDYgMTcuNjQ1LDE3LjY0Nmw1MTAuMDI3LC0wYzkuNzM4LC0w
|
||||
IDE3LjY0NSwtNy45MDcgMTcuNjQ1LC0xNy42NDZsLTAsLTMxOS43OTRaIiBzdHlsZT0iZmlsbDoj
|
||||
MmMyYzJjO3N0cm9rZTojMDAwO3N0cm9rZS13aWR0aDoyLjFweDsiLz48cGF0aCBkPSJNNTg1LjEy
|
||||
Niw5OC4zOTVjLTAsLTkuNzM4IC03LjkwNywtMTcuNjQ1IC0xNy42NDUsLTE3LjY0NWwtNDk0Ljgz
|
||||
OSwwYy05LjczOCwwIC0xNy42NDUsNy45MDcgLTE3LjY0NSwxNy42NDVsMCwzMTkuNzk0YzAsOS43
|
||||
MzkgNy45MDcsMTcuNjQ2IDE3LjY0NSwxNy42NDZsNDk0LjgzOSwtMGM5LjczOCwtMCAxNy42NDUs
|
||||
LTcuOTA3IDE3LjY0NSwtMTcuNjQ2bC0wLC0zMTkuNzk0WiIgc3R5bGU9ImZpbGw6IzM3MzczNztz
|
||||
dHJva2U6IzAwMDtzdHJva2Utd2lkdGg6Mi4wN3B4OyIvPjxwYXRoIGQ9Ik01NjcuMjU5LDExNC40
|
||||
NzJjLTAsLTguNzYgLTcuMTEyLC0xNS44NzIgLTE1Ljg3MiwtMTUuODcybC00NjIuNjUxLDBjLTgu
|
||||
NzYsMCAtMTUuODcyLDcuMTEyIC0xNS44NzIsMTUuODcybDAsMjg3LjY0MWMwLDguNzYgNy4xMTIs
|
||||
MTUuODcxIDE1Ljg3MiwxNS44NzFsNDYyLjY1MSwwYzguNzYsMCAxNS44NzIsLTcuMTExIDE1Ljg3
|
||||
MiwtMTUuODcxbC0wLC0yODcuNjQxWiIgc3R5bGU9ImZpbGw6I2ZmZjJlYjtzdHJva2U6IzAwMDtz
|
||||
dHJva2Utd2lkdGg6MS45cHg7Ii8+PHBhdGggZD0iTTIzNi4xNDUsNDM1Ljg3OGwtMjMuNDIsOTEu
|
||||
MDA4bDEzMC43NjUsLTBsMjIuMzY1LC05MC43NTZsLTEyOS43MSwtMC4yNTJaIiBzdHlsZT0iZmls
|
||||
bDojMmMyYzJjO3N0cm9rZTojMDAwO3N0cm9rZS13aWR0aDoyLjQ3cHg7Ii8+PHBhdGggZD0iTTI0
|
||||
NS4zOTcsNDM1Ljg3OGwtMjMuNDIxLDkxLjAwOGwxMzAuNzY2LC0wbDIyLjM2NSwtOTAuNzU2bC0x
|
||||
MjkuNzEsLTAuMjUyWiIgc3R5bGU9ImZpbGw6IzM3MzczNztzdHJva2U6IzAwMDtzdHJva2Utd2lk
|
||||
dGg6Mi40N3B4OyIvPjxwYXRoIGQ9Ik00NzIuNTg0LDUzMS4wOTNjLTAsLTIuODI3IC0yLjI5NSwt
|
||||
NS4xMjEgLTUuMTIxLC01LjEyMWwtMjkyLjU0MSwtMGMtMi44MjYsLTAgLTUuMTIxLDIuMjk0IC01
|
||||
LjEyMSw1LjEyMWwwLDEwLjI0M2MwLDIuODI2IDIuMjk1LDUuMTIxIDUuMTIxLDUuMTIxbDI5Mi41
|
||||
NDEsLTBjMi44MjYsLTAgNS4xMjEsLTIuMjk1IDUuMTIxLC01LjEyMWwtMCwtMTAuMjQzWiIgc3R5
|
||||
bGU9ImZpbGw6IzJjMmMyYztzdHJva2U6IzAwMDtzdHJva2Utd2lkdGg6Mi40N3B4OyIvPjxnPjxw
|
||||
YXRoIGQ9Ik0xOTAuMzc5LDQzMS40NTVjLTAsLTEuOTE2IC0xLjU1NiwtMy40NzIgLTMuNDcyLC0z
|
||||
LjQ3MmwtNTQuNTYzLDBjLTEuOTE3LDAgLTMuNDcyLDEuNTU2IC0zLjQ3MiwzLjQ3MmwtMCw0Ni4z
|
||||
MDljLTAsMS45MTYgMS41NTUsMy40NzIgMy40NzIsMy40NzJsNTQuNTYzLC0wYzEuOTE2LC0wIDMu
|
||||
NDcyLC0xLjU1NiAzLjQ3MiwtMy40NzJsLTAsLTQ2LjMwOVoiIHN0eWxlPSJmaWxsOiNlZWFjMDE7
|
||||
c3Ryb2tlOiMwMDA7c3Ryb2tlLXdpZHRoOjMuMXB4OyIvPjxnPjxwYXRoIGQ9Ik0xMzkuOTk2LDQ0
|
||||
Mi44NTNjLTAsMC40MiAtMC4wNzEsMC45MjIgMC4wNjMsMS4zMjNjMC41MywxLjU5MiAyLjc1LDEu
|
||||
Mzg2IDQuMDk1LDEuMzg2YzIuMzk3LC0wIDQuNjc0LC0wLjEwOCA2LjQ4OSwtMS44MjdjMC40NSwt
|
||||
MC40MjYgMC45NjYsLTAuOTQgMS4wNzIsLTEuNTc1YzAuMDYxLC0wLjM2NyAwLjEwMiwtMS4yMDgg
|
||||
LTAuMTI2LC0xLjUxMmMtMC44MDMsLTEuMDcxIC0yLjg0NCwtMC4zNzMgLTMuNjU1LDAuMjUyYy0w
|
||||
LjcxNCwwLjU1IC0wLjgyMiwxLjgyNiAtMC45NDUsMi42NDZjLTAuNDA2LDIuNzA1IDAuMzg3LDUu
|
||||
MDMgMy4xNSw1Ljg1OWMzLjQwNSwxLjAyMSA2Ljc4NCwwLjA2IDEwLjA4NiwtMC44MDljMS41OTcs
|
||||
LTAuNDIxIDMuNjYsLTAuNzcgNS4wMTcsLTEuNzJjMC40NTIsLTAuMzE2IDAuNDQ0LC0wLjI4NCAw
|
||||
LjkwMSwtMC41NThjMS4yMDgsLTAuNzI1IDMuODY5LC0xLjkyNyAyLjQ1NywtMy41OTFjLTAuMTks
|
||||
LTAuMjI0IC0xLjEzOSwtMC4yMzcgLTEuMjYsMC4xMjZjLTAuNDIsMS4yNTcgLTEuMzI2LDMuMzAy
|
||||
IDAuNTY3LDMuNzhjMS4zOTgsMC4zNTMgMi45MTksMC4xODkgNC4zNDcsMC4xODljMC4xOTMsLTAg
|
||||
MS40ODUsLTAuMjc5IDEuNzAxLC0wLjA2M2MwLjU1NSwwLjU1NSAxLjU0MywwLjgxMSAyLjMzMSwx
|
||||
LjAwOGMwLjkzMiwwLjIzMyAxLjk0NCwwLjYwNiAyLjg5OSwwLjY5M2MwLjg2OSwwLjA3OSAxLjc5
|
||||
LC0wLjA0NSAyLjY0NiwwLjEyNiIgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2U6IzAwMDtzdHJva2Ut
|
||||
d2lkdGg6MS45NnB4OyIvPjxwYXRoIGQ9Ik0xNDIuNzA1LDQ1Mi42ODFjMC4xMTksMC41NTEgMC44
|
||||
OTMsMC42NjkgMS4zMTgsMC44NDZjMS41OTgsMC42NjYgMy41MTQsMC44NzQgNS4yMzQsMC45ODJj
|
||||
Mi4zNzgsMC4xNDggNC43MDQsLTAuMTU5IDcuMDU3LC0wLjMxNWMxLjg2MSwtMC4xMjUgMy43NDYs
|
||||
MC4xMTYgNS42MDcsLTBjMi4wNTEsLTAuMTI5IDQuMzIxLC0wLjM3NyA2LjM2NCwtMC4wNjNjMS4w
|
||||
NzIsMC4xNjQgMi4wODksMC40OCAzLjE1LDAuNjkzIiBzdHlsZT0iZmlsbDpub25lO3N0cm9rZToj
|
||||
MDAwO3N0cm9rZS13aWR0aDoxLjk2cHg7Ii8+PHBhdGggZD0iTTE3Ni4zNDksNDU1LjA3NmMxLjEx
|
||||
MywtMC4wMjEgMi4yMjYsLTAuMDQyIDMuMzQsLTAuMDYzIiBzdHlsZT0iZmlsbDpub25lO3N0cm9r
|
||||
ZTojMDAwO3N0cm9rZS13aWR0aDoxLjk2cHg7Ii8+PHBhdGggZD0iTTE0NS4xNjIsNDU5Ljg2NGwy
|
||||
MS42NzQsLTAiIHN0eWxlPSJmaWxsOm5vbmU7c3Ryb2tlOiMwMDA7c3Ryb2tlLXdpZHRoOjEuOTZw
|
||||
eDsiLz48L2c+PC9nPjxnPjxnPjxwYXRoIGQ9Ik03Ni4yNzMsMTIxLjQ5NGw0ODcuNTU3LC0wLjAx
|
||||
NSIgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2U6I2Y0ZGRkMDtzdHJva2Utd2lkdGg6NC45M3B4O3N0
|
||||
cm9rZS1saW5lY2FwOnNxdWFyZTsiLz48cGF0aCBkPSJNMjMwLjk1MSwyMjQuMTc1Yy0wLjgwMiwt
|
||||
My4zMjcgLTguMDU5LC03Ljk4NCAtMTEuMDA1LC04Ljk0NWMtMzUuODQ2LC0xMS42OTkgLTIwLjc4
|
||||
MywyNy4zNzIgLTAuNjE4LDI0LjM0MiIgc3R5bGU9ImZpbGw6IzYxZTBlZTsiLz48cGF0aCBkPSJN
|
||||
MjM0Ljk0NCwyMjMuMjEzYy0wLjM3OSwtMS41NzQgLTEuNTIyLC0zLjQzNiAtMy4yNzYsLTUuMTc0
|
||||
Yy0zLjA2MSwtMy4wMzMgLTguMDE0LC01LjkyIC0xMC40NDgsLTYuNzE0Yy0xMy4xMzUsLTQuMjg3
|
||||
IC0yMC42MTgsLTEuOTk0IC0yNC4wNzEsMS44MTljLTMuNjM4LDQuMDE3IC0zLjgzNiwxMC40OTIg
|
||||
LTAuODI1LDE2LjY1MWMzLjk4MSw4LjE0MyAxMy4zMjEsMTUuMzg2IDIzLjYxNSwxMy44MzljMi4y
|
||||
NDIsLTAuMzM3IDMuNzg4LC0yLjQzIDMuNDUxLC00LjY3MmMtMC4zMzcsLTIuMjQyIC0yLjQzLC0z
|
||||
Ljc4OSAtNC42NzIsLTMuNDUyYy02LjY0NywwLjk5OSAtMTIuNDQ0LC00LjA2NSAtMTUuMDE0LC05
|
||||
LjMyM2MtMC44MDEsLTEuNjM3IC0xLjI5MywtMy4zMDggLTEuMzAyLC00Ljg0N2MtMC4wMDUsLTEu
|
||||
MDIgMC4xOTUsLTEuOTc0IDAuODM2LC0yLjY4MmMwLjgzMiwtMC45MTggMi4yMjUsLTEuMzU0IDQu
|
||||
MTMzLC0xLjQ4MWMyLjg1MiwtMC4xOTEgNi41NjcsMC40MTIgMTEuMywxLjk1N2MxLjQ1NiwwLjQ3
|
||||
NSA0LjE2LDIuMDk2IDYuMjU3LDMuODY5YzAuNjM2LDAuNTM3IDEuMjE1LDEuMDg4IDEuNjU4LDEu
|
||||
NjM2YzAuMTUyLDAuMTg4IDAuMzMxLDAuMzI3IDAuMzcyLDAuNDk4YzAuNTMxLDIuMjA0IDIuNzUx
|
||||
LDMuNTYyIDQuOTU1LDMuMDMxYzIuMjA0LC0wLjUzMSAzLjU2MiwtMi43NTIgMy4wMzEsLTQuOTU1
|
||||
WiIvPjxwYXRoIGQ9Ik0zNDkuMzI2LDEyMy41MjdjMCwwIC0xNS4zNSwtMTMuODYyIC0xOC4xMTQs
|
||||
MjIuODM0IiBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojNjFlMGVlO3N0cm9rZS13aWR0aDo2LjE2
|
||||
cHg7Ii8+PHBhdGggZD0iTTI5OS42NjIsMTIzLjQ4NWMtMCwtMCAxMi42NzEsLTE1LjI2NSAyMC4x
|
||||
MTIsMjEuODk4IiBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojNjFlMGVlO3N0cm9rZS13aWR0aDo2
|
||||
LjE2cHg7Ii8+PHBhdGggZD0iTTMzNi4xMTEsMTA4Ljc3M2MtMCwwIC0xNi44MDgsLTkuMDk5IC0x
|
||||
MC44ODIsMzIuMTI0IiBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojNjFlMGVlO3N0cm9rZS13aWR0
|
||||
aDo2LjE2cHg7Ii8+PHBhdGggZD0iTTQxNy43OTYsMTg0LjQ1M2MzLjI4MiwwLjk3IDYuNTgzLC0x
|
||||
LjI5IDguODg5LC0zLjM2MWMyOC4wNTUsLTI1LjE5NCAtMTMuMTk4LC0zMS41MzggLTIwLjY1Niwt
|
||||
MTIuNTU5IiBzdHlsZT0iZmlsbDojNjFlMGVlOyIvPjxwYXRoIGQ9Ik00MTYuNjMyLDE4OC4zOTNj
|
||||
NC42NTcsMS4zNzUgOS41MjYsLTEuMzA3IDEyLjc5NywtNC4yNDVjMTAuMjc5LC05LjIzIDEyLjA0
|
||||
NiwtMTYuODQzIDEwLjQ4OCwtMjEuNzE2Yy0xLjY0NSwtNS4xNSAtNy4xNDIsLTguNTQ0IC0xMy45
|
||||
NzgsLTguOTljLTkuMDExLC0wLjU4NyAtMTkuOTI3LDMuOTA0IC0yMy43MzMsMTMuNTg5Yy0wLjgz
|
||||
LDIuMTEgMC4yMSw0LjQ5NiAyLjMyLDUuMzI1YzIuMTEsMC44MjkgNC40OTYsLTAuMjExIDUuMzI1
|
||||
LC0yLjMyMWMyLjQ2LC02LjI1OSA5LjczLC04Ljc3NSAxNS41NTMsLTguMzk1YzEuODA3LDAuMTE3
|
||||
IDMuNDg5LDAuNTE3IDQuODE3LDEuMjY5YzAuODcxLDAuNDk0IDEuNTg2LDEuMTMgMS44NzEsMi4w
|
||||
MjNjMC4zNzUsMS4xNzIgMC4wNDksMi41ODggLTAuNzk1LDQuMjk4Yy0xLjI2NiwyLjU2MiAtMy42
|
||||
NDksNS40NzcgLTcuMzU2LDguODA2Yy0wLjg2OCwwLjc4IC0xLjkwNywxLjYxOCAtMy4wNTQsMi4x
|
||||
NDdjLTAuNjE2LDAuMjg0IC0xLjI2NSwwLjUyNyAtMS45MjgsMC4zMzFjLTIuMTc0LC0wLjY0MiAt
|
||||
NC40NjEsMC42MDIgLTUuMTAzLDIuNzc2Yy0wLjY0MiwyLjE3NCAwLjYwMiw0LjQ2MSAyLjc3Niw1
|
||||
LjEwM1oiLz48cGF0aCBkPSJNNDM2LjAxNCw0MTkuNjQ1YzMuNjAxLC0xMC45MzQgOS4wMDMsLTMx
|
||||
LjYwOSA1LjY4MiwtNDkuNTYzYy0xNC45NTMsLTgwLjg0NyA3Ljg5NSwtOTguNTQ5IC01LjAzNSwt
|
||||
MTQ3LjU5M2MtNC40OSwtMTcuMDI5IC0xMi4wNDksLTM2LjI1IC0yMy41NTQsLTQ5Ljc5MmMtMjUu
|
||||
Mjg1LC0yOS43NjIgLTcwLjczNiwtNDIuOTEzIC0xMDguNzMsLTMyLjc4NWMtMTUuMjEzLDQuMDU1
|
||||
IC0yOC42NjIsMTEuNzI3IC00MS45MjMsMjAuMDM0Yy00Ni4xMzYsMjguOSAtNTIuMjU5LDgzLjM3
|
||||
NyAtNDIuNzkxLDEyOC4yNDhjNC4xOTEsMTkuODY0IDIzLjY3Myw0OS44OTcgMjIuNTg2LDcwLjE5
|
||||
NGMtMS4zODcsMjUuOTEgLTAuNzY1LDQ0LjQyNiAzLjM3Niw2MC44ODQiIHN0eWxlPSJmaWxsOiM2
|
||||
MWUwZWU7Ii8+PGNsaXBQYXRoIGlkPSJfY2xpcDEiPjxwYXRoIGQ9Ik00MzYuMDE0LDQxOS42NDVj
|
||||
My42MDEsLTEwLjkzNCA5LjAwMywtMzEuNjA5IDUuNjgyLC00OS41NjNjLTE0Ljk1MywtODAuODQ3
|
||||
IDcuODk1LC05OC41NDkgLTUuMDM1LC0xNDcuNTkzYy00LjQ5LC0xNy4wMjkgLTEyLjA0OSwtMzYu
|
||||
MjUgLTIzLjU1NCwtNDkuNzkyYy0yNS4yODUsLTI5Ljc2MiAtNzAuNzM2LC00Mi45MTMgLTEwOC43
|
||||
MywtMzIuNzg1Yy0xNS4yMTMsNC4wNTUgLTI4LjY2MiwxMS43MjcgLTQxLjkyMywyMC4wMzRjLTQ2
|
||||
LjEzNiwyOC45IC01Mi4yNTksODMuMzc3IC00Mi43OTEsMTI4LjI0OGM0LjE5MSwxOS44NjQgMjMu
|
||||
NjczLDQ5Ljg5NyAyMi41ODYsNzAuMTk0Yy0xLjM4NywyNS45MSAtMC43NjUsNDQuNDI2IDMuMzc2
|
||||
LDYwLjg4NCIvPjwvY2xpcFBhdGg+PGcgY2xpcC1wYXRoPSJ1cmwoI19jbGlwMSkiPjxwYXRoIGQ9
|
||||
Ik0zMTEuNzc2LDIzNi44MzVjMCwwIC0zLjA2OCwyNC4xOTkgLTMxLjU5NywyNC44MzhjLTI4LjUz
|
||||
LDAuNjM5IC0zMi41OTEsLTI4LjI0NiAtMzIuNTkxLC0yOC4yNDZsNjQuMTg4LDMuNDA4WiIgc3R5
|
||||
bGU9ImZpbGw6IzRmY2JlMDsiLz48cGF0aCBkPSJNNDMyLjUwOSwyMTYuMDIzYzAsLTAgLTMuMDY3
|
||||
LDI0LjE5OCAtMzEuNTk3LDI0LjgzN2MtMjguNTMsMC42MzkgLTMyLjU5MSwtMjguMjQ2IC0zMi41
|
||||
OTEsLTI4LjI0Nmw2NC4xODgsMy40MDlaIiBzdHlsZT0iZmlsbDojNGZjYmUwOyIvPjxwYXRoIGQ9
|
||||
Ik0yODYuODQ1LDQxOC45NjFsMTI3Ljg4NiwtMGMwLjM4NiwtMS4yNjcgMC42NjYsLTIuNDMyIDAu
|
||||
ODcsLTMuNDcxYzMuODY3LC0xOS43NDMgMy43MjksLTU3LjQ2OCAtMi40MTUsLTc3LjA5NWMtMi4x
|
||||
MzMsLTYuODE2IC01Ljc5MywtMTQuNDkgLTExLjQ3NiwtMTkuODUyYy0xMi40OSwtMTEuNzg1IC02
|
||||
NC4zOTgsLTkuMTgxIC04My41NjEsLTQuODM1Yy03LjY3MywxLjc0MSAtMTQuNDg3LDQuOTIyIC0y
|
||||
MS4yMSw4LjM1OGMtMTQuOTExLDcuNjIgLTIwLjI1Myw1OC43MTIgLTEwLjA5NCw5Ni44OTVaIiBz
|
||||
dHlsZT0iZmlsbDojYjdmOGZmOyIvPjxwYXRoIGQ9Ik0yNDYuNzExLDQxOC45NjFjLTUuMTA4LC0y
|
||||
MC42MTMgLTYuNDksLTQyLjk3NSAtNS41NjIsLTYwLjg1NWMyLjEzNiwtNDEuMTMzIC0zMS4zMTQs
|
||||
LTUwLjQ1MyAtMjUuNDYxLC0xMjEuNTEzYzE0LjUzNCwtNDIuNDA5IDE1LjA4OSwtNDAuMTA3IDI3
|
||||
LjY1MywtNTUuNDQ1YzAsMCAtMTMuMTQ3LDM5Ljk2OCAtMS4xOTMsNzAuNzI4YzExLjk1NCwzMC43
|
||||
NTkgMzIuMTc0LDczLjQxOCAzMi41OSwxMTcuNDkxYzAuMTg2LDE5Ljc3MSAxLjYzNCwzNi4zMSA0
|
||||
LjEzLDQ5LjU5NGwtMzIuMTU3LC0wWiIgc3R5bGU9ImZpbGw6IzRmY2JlMDsiLz48L2c+PHBhdGgg
|
||||
ZD0iTTQzOS45MTUsNDIwLjkzYzMuNzQ4LC0xMS4zOCA5LjI3NiwtMzIuOTA4IDUuODIsLTUxLjU5
|
||||
NWMtOC44NjUsLTQ3LjkzMyAtNC4yNTksLTczLjQwNSAtMS45MzUsLTk2LjU4YzEuNjEzLC0xNi4w
|
||||
OTMgMi4xNTYsLTMxLjEyMiAtMy4xNjcsLTUxLjMxM2MtNC42MzgsLTE3LjU5MSAtMTIuNTExLC0z
|
||||
Ny40MTYgLTI0LjM5NiwtNTEuNDA1Yy0yNi4yNjEsLTMwLjkxMSAtNzMuNDU2LC00NC42MTMgLTEx
|
||||
Mi45MTcsLTM0LjA5NGMtMTUuNjE0LDQuMTYxIC0yOS40MzcsMTEuOTk2IC00My4wNDcsMjAuNTIy
|
||||
Yy00Ny43MzUsMjkuOTAyIC01NC40MjYsODYuMTUgLTQ0LjYyOSwxMzIuNTc3YzIuMTM5LDEwLjEz
|
||||
OCA4LjEzNiwyMi44ODggMTMuNTA0LDM1LjY3NmM0Ljk5NCwxMS45IDkuNTE1LDIzLjgxIDguOTk5
|
||||
LDMzLjQ1Yy0xLjQxNSwyNi40MzIgLTAuNzMsNDUuMzE3IDMuNDk1LDYyLjEwN2wxLjAwMiwzLjk4
|
||||
M2w3Ljk2NywtMi4wMDVsLTEuMDAyLC0zLjk4M2MtNC4wNTgsLTE2LjEyNyAtNC42MTgsLTM0LjI3
|
||||
NCAtMy4yNTgsLTU5LjY2M2MwLjQ2LC04LjYwOSAtMi40NiwtMTguODk1IC02LjU0NSwtMjkuNDU3
|
||||
Yy01LjY5NCwtMTQuNzI0IC0xMy42NDYsLTMwLjA1OSAtMTYuMTI0LC00MS44MDRjLTkuMTQsLTQz
|
||||
LjMxNSAtMy41ODQsLTk2LjAyMiA0MC45NTIsLTEyMy45MTljMTIuOTEyLC04LjA4OSAyNS45ODks
|
||||
LTE1LjU5OCA0MC44MDEsLTE5LjU0N2MzNi41MjYsLTkuNzM1IDgwLjIzNCwyLjg2NSAxMDQuNTQx
|
||||
LDMxLjQ3NmMxMS4xMjYsMTMuMDk2IDE4LjM3MiwzMS43MTMgMjIuNzEzLDQ4LjE4YzYuMzEsMjMu
|
||||
OTMyIDMuODIsNDAuMjE5IDEuNjQ3LDYwLjM4M2MtMi4yNTcsMjAuOTQ5IC00LjI1OSw0NS45Mjcg
|
||||
My4zMjEsODYuOTFjMy4xODYsMTcuMjIyIC0yLjA5LDM3LjA0MyAtNS41NDQsNDcuNTMxbC0xLjI4
|
||||
NSwzLjkwMmw3LjgwMywyLjU2OWwxLjI4NCwtMy45MDFaIi8+PHBhdGggZD0iTTQyNC41MzksMjQy
|
||||
LjI0N2MtMS44NjgsMC4xMzQgLTMuMjc2LDEuNzU4IC0zLjE0MiwzLjYyNmMwLjEzMywxLjg2NyAx
|
||||
Ljc1NywzLjI3NSAzLjYyNSwzLjE0MWMxLjg2NywtMC4xMzMgMy4yNzUsLTEuNzU3IDMuMTQyLC0z
|
||||
LjYyNWMtMC4xMzQsLTEuODY3IC0xLjc1OCwtMy4yNzUgLTMuNjI1LC0zLjE0MloiIHN0eWxlPSJm
|
||||
aWxsOiNiN2Y4ZmY7Ii8+PHBhdGggZD0iTTI1Ni45OTcsMjYwLjM5MWMtMS44NjgsMC4xMzMgLTMu
|
||||
Mjc1LDEuNzU4IC0zLjE0MiwzLjYyNWMwLjEzMywxLjg2OCAxLjc1OCwzLjI3NSAzLjYyNSwzLjE0
|
||||
MmMxLjg2OCwtMC4xMzMgMy4yNzUsLTEuNzU4IDMuMTQyLC0zLjYyNWMtMC4xMzMsLTEuODY4IC0x
|
||||
Ljc1OCwtMy4yNzUgLTMuNjI1LC0zLjE0MloiIHN0eWxlPSJmaWxsOiNiN2Y4ZmY7Ii8+PGc+PHBh
|
||||
dGggZD0iTTM2NC4wOSwyNjkuMjI5Yy0xLjE4Niw0Ljk2NyAtMS40NDMsMTQuMzA2IC00LjMxNSwx
|
||||
OC40Yy0xLjM5MywxLjk4NSAtNC40OTUsMi4wODYgLTYuNTE2LDIuMTUzYy0xMS42MTIsMC4zODMg
|
||||
LTExLjQ0NiwtMTEuNTEzIC0xMy4xODEsLTIxLjg1NSIgc3R5bGU9ImZpbGw6I2VkZWRlZDsiLz48
|
||||
cGF0aCBkPSJNMzYxLjA5NCwyNjguNTEzYy0wLjcxOSwzLjAwOSAtMS4xMDYsNy42MDcgLTEuOTIy
|
||||
LDExLjY5OGMtMC40NDUsMi4yMjggLTAuOTcxLDQuMjk4IC0xLjkxOSw1LjY0OWMtMC4yNjMsMC4z
|
||||
NzYgLTAuNzQ5LDAuNDU5IC0xLjIwMywwLjU2NmMtMC45OTUsMC4yMzMgLTIuMDU2LDAuMjUgLTIu
|
||||
ODkzLDAuMjc3Yy0yLjA1NSwwLjA2OCAtMy42MSwtMC4zMjYgLTQuNzc0LC0xLjE5MmMtMS4xOTMs
|
||||
LTAuODg3IC0xLjk1NCwtMi4yMTIgLTIuNTQ0LC0zLjc0M2MtMS41NjEsLTQuMDQ3IC0xLjg5LC05
|
||||
LjM5IC0yLjcyMywtMTQuMzUxYy0wLjI4MSwtMS42NzcgLTEuODcxLC0yLjgxIC0zLjU0OCwtMi41
|
||||
MjhjLTEuNjc3LDAuMjgxIC0yLjgwOSwxLjg3MSAtMi41MjgsMy41NDhjMC45MDMsNS4zODEgMS4z
|
||||
NTgsMTEuMTU4IDMuMDUxLDE1LjU0OGMxLjAzNiwyLjY4OCAyLjUyLDQuOTEyIDQuNjE1LDYuNDdj
|
||||
Mi4xMjUsMS41ODEgNC45MDMsMi41MyA4LjY1NCwyLjQwNmMxLjQ2OCwtMC4wNDggMy40LC0wLjE1
|
||||
MiA1LjA3OSwtMC43MTRjMS41NiwtMC41MjIgMi45MTksLTEuNDEgMy44NTgsLTIuNzQ5YzEuMzUs
|
||||
LTEuOTI0IDIuMjg0LC00LjgwOCAyLjkxNywtNy45ODFjMC44LC00LjAxMSAxLjE2OCwtOC41MjIg
|
||||
MS44NzMsLTExLjQ3M2MwLjM5NSwtMS42NTMgLTAuNjI3LC0zLjMxNyAtMi4yODEsLTMuNzEyYy0x
|
||||
LjY1NCwtMC4zOTUgLTMuMzE3LDAuNjI3IC0zLjcxMiwyLjI4MVoiLz48cGF0aCBkPSJNMzQ5LjAw
|
||||
MSwyMzQuMDVjLTkuMzMyLDEuMjQ2IC0xNi4yNjUsNy4wNzUgLTE1LjQ3MywxMy4wMDhjMC43OTIs
|
||||
NS45MzMgOS4wMTIsOS43MzggMTguMzQ0LDguNDkyYzkuMzMzLC0xLjI0NiAxNi4yNjYsLTcuMDc1
|
||||
IDE1LjQ3NCwtMTMuMDA4Yy0wLjc5MiwtNS45MzMgLTkuMDEyLC05LjczOCAtMTguMzQ1LC04LjQ5
|
||||
MloiLz48cGF0aCBkPSJNMzQ4LjkwMSwyNTIuNTVjMC40Niw1LjIwNCAxLjY5NCwxMC4xNDIgMi40
|
||||
NTMsMTUuMjc3YzAuMjQ4LDEuNjgyIDEuODE1LDIuODQ2IDMuNDk3LDIuNTk4YzEuNjgyLC0wLjI0
|
||||
OCAyLjg0NiwtMS44MTYgMi41OTgsLTMuNDk4Yy0wLjc0MSwtNS4wMTUgLTEuOTYxLC05LjgzNyAt
|
||||
Mi40MTEsLTE0LjkyYy0wLjE1LC0xLjY5NCAtMS42NDcsLTIuOTQ3IC0zLjM0LC0yLjc5N2MtMS42
|
||||
OTQsMC4xNSAtMi45NDcsMS42NDcgLTIuNzk3LDMuMzRaIi8+PHBhdGggZD0iTTM3NC4yNiwyNTku
|
||||
ODE0Yy04LjAyMiw1LjM1IC0xNS45MDEsNy44MjUgLTIzLjU5NCw3LjI3NGMtNy42OTQsLTAuNTUx
|
||||
IC0xNS4xNDgsLTQuMTI0IC0yMi4zNDIsLTEwLjU4OWMtMS4yNjUsLTEuMTM2IC0zLjIxNCwtMS4w
|
||||
MzIgLTQuMzUxLDAuMjMzYy0xLjEzNiwxLjI2NCAtMS4wMzIsMy4yMTQgMC4yMzMsNC4zNWM4LjM1
|
||||
Niw3LjUwOCAxNy4wODQsMTEuNTExIDI2LjAyLDEyLjE1MWM4LjkzNiwwLjY0IDE4LjEzNSwtMi4w
|
||||
NzggMjcuNDUzLC04LjI5NGMxLjQxNCwtMC45NDMgMS43OTcsLTIuODU4IDAuODUzLC00LjI3MmMt
|
||||
MC45NDMsLTEuNDE1IC0yLjg1OCwtMS43OTcgLTQuMjcyLC0wLjg1M1oiLz48cGF0aCBkPSJNMzc4
|
||||
LjYwNSwxNTIuMTU2Yy0zLjY0NywtMC44MjYgLTYuOTU2LDAuMDM4IC03LjM4NSwxLjkyOWMtMC40
|
||||
MjgsMS44OTEgMi4xODUsNC4wOTcgNS44MzIsNC45MjRjMy42NDcsMC44MjcgNi45NTYsLTAuMDM3
|
||||
IDcuMzg1LC0xLjkyOGMwLjQyOCwtMS44OTEgLTIuMTg1LC00LjA5OCAtNS44MzIsLTQuOTI1WiIg
|
||||
c3R5bGU9ImZpbGw6I2I3ZjhmZjsiLz48cGF0aCBkPSJNMjQ5LjUsMTc3LjI3Yy0yLjE5NSwzLjAy
|
||||
OCAtMi43MDIsNi40MSAtMS4xMzIsNy41NDhjMS41NywxLjEzOCA0LjYyNiwtMC4zOTYgNi44MjEs
|
||||
LTMuNDI0YzIuMTk1LC0zLjAyOCAyLjcwMiwtNi40MSAxLjEzMiwtNy41NDhjLTEuNTcsLTEuMTM4
|
||||
IC00LjYyNiwwLjM5NiAtNi44MjEsMy40MjRaIiBzdHlsZT0iZmlsbDojYjdmOGZmOyIvPjxwYXRo
|
||||
IGQ9Ik00MDMuNjQ4LDE2Mi44MjVjLTE5LjIxMywtMS4zNSAtMzUuOTA3LDEzLjE1MSAtMzcuMjU3
|
||||
LDMyLjM2NGMtMS4zNSwxOS4yMTIgMTMuMTUyLDM1LjkwNiAzMi4zNjQsMzcuMjU2YzE5LjIxMywx
|
||||
LjM1MSAzNS45MDcsLTEzLjE1MSAzNy4yNTcsLTMyLjM2NGMxLjM1LC0xOS4yMTIgLTEzLjE1Miwt
|
||||
MzUuOTA2IC0zMi4zNjQsLTM3LjI1NloiIHN0eWxlPSJmaWxsOiNmZmY7c3Ryb2tlOiMwMDA7c3Ry
|
||||
b2tlLXdpZHRoOjYuMTZweDsiLz48cGF0aCBkPSJNMjgxLjY5NiwxODEuNzhjLTE5LjIxMiwtMS4z
|
||||
NSAtMzUuOTA2LDEzLjE1MiAtMzcuMjU3LDMyLjM2NGMtMS4zNSwxOS4yMTIgMTMuMTUyLDM1Ljkw
|
||||
NyAzMi4zNjUsMzcuMjU3YzE5LjIxMiwxLjM1IDM1LjkwNiwtMTMuMTUyIDM3LjI1NiwtMzIuMzY0
|
||||
YzEuMzUsLTE5LjIxMyAtMTMuMTUyLC0zNS45MDcgLTMyLjM2NCwtMzcuMjU3WiIgc3R5bGU9ImZp
|
||||
bGw6I2ZmZjtzdHJva2U6IzAwMDtzdHJva2Utd2lkdGg6Ni4xNnB4OyIvPjxwYXRoIGQ9Ik0zOTgu
|
||||
NDk5LDE5Mi44ODFjLTYuODI0LC0wLjQ3OSAtMTIuNzU0LDQuNjcyIC0xMy4yMzMsMTEuNDk2Yy0w
|
||||
LjQ4LDYuODI0IDQuNjcxLDEyLjc1MyAxMS40OTUsMTMuMjMzYzYuODI0LDAuNDc5IDEyLjc1NCwt
|
||||
NC42NzIgMTMuMjMzLC0xMS40OTZjMC40OCwtNi44MjQgLTQuNjcxLC0xMi43NTMgLTExLjQ5NSwt
|
||||
MTMuMjMzWiIvPjxwYXRoIGQ9Ik0yNzguMTQyLDIxMC4yMjdjLTYuODI0LC0wLjQ3OSAtMTIuNzU0
|
||||
LDQuNjcxIC0xMy4yMzMsMTEuNDk2Yy0wLjQ4LDYuODI0IDQuNjcxLDEyLjc1MyAxMS40OTUsMTMu
|
||||
MjMzYzYuODI0LDAuNDc5IDEyLjc1NCwtNC42NzIgMTMuMjMzLC0xMS40OTZjMC40OCwtNi44MjQg
|
||||
LTQuNjcxLC0xMi43NTMgLTExLjQ5NSwtMTMuMjMzWiIvPjxwYXRoIGQ9Ik0zOTkuODkyLDE5Ny45
|
||||
NjFjLTEuOTE2LC0wLjEzNSAtMy41ODEsMS4zMTEgLTMuNzE2LDMuMjI3Yy0wLjEzNCwxLjkxNiAx
|
||||
LjMxMiwzLjU4MSAzLjIyOCwzLjcxNmMxLjkxNiwwLjEzNCAzLjU4MSwtMS4zMTIgMy43MTUsLTMu
|
||||
MjI4YzAuMTM1LC0xLjkxNiAtMS4zMTEsLTMuNTgxIC0zLjIyNywtMy43MTVaIiBzdHlsZT0iZmls
|
||||
bDojZmZmOyIvPjxwYXRoIGQ9Ik0yNzkuNTM1LDIxNS4zMDZjLTEuOTE2LC0wLjEzNCAtMy41ODEs
|
||||
MS4zMTIgLTMuNzE1LDMuMjI4Yy0wLjEzNSwxLjkxNiAxLjMxMSwzLjU4MSAzLjIyNywzLjcxNmMx
|
||||
LjkxNiwwLjEzNCAzLjU4MSwtMS4zMTIgMy43MTYsLTMuMjI4YzAuMTM0LC0xLjkxNiAtMS4zMTIs
|
||||
LTMuNTgxIC0zLjIyOCwtMy43MTZaIiBzdHlsZT0iZmlsbDojZmZmOyIvPjwvZz48L2c+PHBhdGgg
|
||||
ZD0iTTUxMi4yOTEsMzMzLjcyNmMwLC0wIC0yNy40OTYsMTMuNDAyIC00My4yNDUsMy43OTJjLTE1
|
||||
Ljc1LC05LjYxIC0xMC44LC0yNC43ODggMy4xMDIsLTIyLjUyYzEzLjkwMywyLjI2OCAyNy4xNjgs
|
||||
My4xOTUgMzUuOTQzLC0zLjkzN2M4Ljc3NiwtNy4xMzIgMjEuNjMzLDE0LjM3NiA0LjIsMjIuNjY1
|
||||
WiIgc3R5bGU9ImZpbGw6I2Q2YzI5NTtzdHJva2U6IzAwMDtzdHJva2Utd2lkdGg6Ni4xNnB4OyIv
|
||||
PjwvZz48Zz48cGF0aCBkPSJNNDQxLjI2OCwyOTEuMzQ5Yy0wLDAgMjguMDgsNDcuNDg4IC0xMjIu
|
||||
NTkxLDExNC4wNGMtMTEuNjI5LC0yLjAxNyAtMTQuMzMsLTguNTE1IC0xNC4zMywtOC41MTVjMCwt
|
||||
MCA0My42MTcsLTEyLjU4NiA4My44MTIsLTQ0Ljg3NmM0My41OTcsLTM1LjAyNCA1My4xMDksLTYw
|
||||
LjY0OSA1My4xMDksLTYwLjY0OVoiIHN0eWxlPSJmaWxsOiM3YjQ3MjI7c3Ryb2tlOiMwMDA7c3Ry
|
||||
b2tlLXdpZHRoOjYuNjhweDsiLz48cGF0aCBkPSJNMjMzLjk1NywzNzIuMTM4YzMuNTYsLTUuNTA2
|
||||
IDEwLjcyOSwtNy4zOTIgMTYuNTM4LC00LjM1MWMxNy43NjcsOS4yMzUgNTMuNjE0LDI4LjAzMiA2
|
||||
Ny4xNTQsMzYuMzc2YzUuOTcxLDMuNjggLTYuNDc3LDE2LjkxMyAtMTkuMjgzLDM3LjkyN2MtMy4z
|
||||
NjgsNS41NjcgLTEwLjQzNiw3LjYyOCAtMTYuMjY5LDQuNzQ2Yy0yMS42NTYsLTEwLjcxNiAtNDEu
|
||||
MDQ3LC0yMC45NTggLTU0LjIzNCwtMjguMDczYy00LjU5MiwtMi41MDEgLTcuOTM0LC02LjgwMiAt
|
||||
OS4yMjMsLTExLjg2OWMtMS4yODgsLTUuMDY4IC0wLjQwNywtMTAuNDQzIDIuNDMzLC0xNC44MzNj
|
||||
NC4zMzUsLTYuNzA4IDkuMTYyLC0xNC4xNyAxMi44ODQsLTE5LjkyM1oiIHN0eWxlPSJmaWxsOiM3
|
||||
YjQ3MjI7c3Ryb2tlOiMwMDA7c3Ryb2tlLXdpZHRoOjQuOTVweDsiLz48cGF0aCBkPSJNMjM5Ljg1
|
||||
LDM2MS4xNzFsLTI1Ljg0OCwyMy4yOTZsNzkuMTcsNDUuNjA1bDI1LjE5NiwtMjQuMjk1bC03OC41
|
||||
MTgsLTQ0LjYwNloiIHN0eWxlPSJmaWxsOiNhZDZjNGE7c3Ryb2tlOiMwMDA7c3Ryb2tlLXdpZHRo
|
||||
OjQuOTVweDsiLz48cGF0aCBkPSJNMjM4LjA4MSwzNzcuNzA0YzIuNTI2LDAuNTMgNC4xNDcsMy4w
|
||||
MTEgMy42MTgsNS41MzdjLTAuNTMsMi41MjYgLTMuMDExLDQuMTQ3IC01LjUzOCwzLjYxOGMtMi41
|
||||
MjYsLTAuNTMgLTQuMTQ3LC0zLjAxMSAtMy42MTcsLTUuNTM4YzAuNTMsLTIuNTI2IDMuMDExLC00
|
||||
LjE0NyA1LjUzNywtMy42MTdaIiBzdHlsZT0iZmlsbDojZmZiMjVjO3N0cm9rZTojMDAwO3N0cm9r
|
||||
ZS13aWR0aDo0Ljk1cHg7Ii8+PHBhdGggZD0iTTI5MC4zMyw0MDcuOWMyLjUyNywwLjUzIDQuMTQ3
|
||||
LDMuMDExIDMuNjE4LDUuNTM3Yy0wLjUzLDIuNTI3IC0zLjAxMSw0LjE0NyAtNS41MzcsMy42MThj
|
||||
LTIuNTI3LC0wLjUzIC00LjE0OCwtMy4wMTEgLTMuNjE4LC01LjUzN2MwLjUzLC0yLjUyNyAzLjAx
|
||||
MSwtNC4xNDggNS41MzcsLTMuNjE4WiIgc3R5bGU9ImZpbGw6I2ZmYjI1YztzdHJva2U6IzAwMDtz
|
||||
dHJva2Utd2lkdGg6NC45NXB4OyIvPjwvZz48Zz48cGF0aCBkPSJNMzA1LjMyMywzMzYuMjM1YzAs
|
||||
MCAyNC4yMDQsMTEyLjU3OCAzNS4zNTcsMTE0LjM1NWMxMS4xNTMsMS43NzYgMTgyLjk5OCwtNTcu
|
||||
MTYyIDE4Mi45OTgsLTU3LjE2MmMwLC0wIC0yNC4yNywtMTA1Ljc3MyAtMzQuNDU2LC0xMTQuNjAy
|
||||
Yy0xMC4xODYsLTguODMgLTE4My44OTksNTcuNDA5IC0xODMuODk5LDU3LjQwOVoiIHN0eWxlPSJm
|
||||
aWxsOiNmZmYyZWI7c3Ryb2tlOiMwMDA7c3Ryb2tlLXdpZHRoOjQuNjhweDsiLz48cGF0aCBkPSJN
|
||||
NDEzLjU2MywzNjcuNjU2bC0xMDguMjQsLTMxLjQyMWwxMDguMjQsMzEuNDIxWiIgc3R5bGU9ImZp
|
||||
bGw6bm9uZTtzdHJva2U6IzAwMDtzdHJva2Utd2lkdGg6NS40NnB4OyIvPjxwYXRoIGQ9Ik00ODgu
|
||||
NTY2LDI3OS4xNTZsLTc1LjAwMyw4OC41IiBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMDAwO3N0
|
||||
cm9rZS13aWR0aDo1LjQ2cHg7Ii8+PHBhdGggZD0iTTMzOS4xNjcsNDQ5LjEyN2w2Mi40MDEsLTgz
|
||||
LjUwNSIgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2U6IzAwMDtzdHJva2Utd2lkdGg6NS40NnB4OyIv
|
||||
PjxwYXRoIGQ9Ik00MjMuMjcsMzU1LjgzNWw5OS4xOTQsMzcuMDAxIiBzdHlsZT0iZmlsbDpub25l
|
||||
O3N0cm9rZTojMDAwO3N0cm9rZS13aWR0aDo1LjQ2cHg7Ii8+PC9nPjxwYXRoIGQ9Ik0zMDQuNDE2
|
||||
LDM3NC43ODVjLTAsMCAtMzAuNDYsLTIuOCAtMzguOTQzLC0xOS4xODVjLTguNDgyLC0xNi4zODQg
|
||||
My42MjIsLTI2Ljc5NCAxNC4zMzIsLTE3LjY0NmMxMC43MTEsOS4xNDkgMjEuNTcyLDE2LjgyMSAz
|
||||
Mi43NzQsMTUuMjc0YzExLjIwMSwtMS41NDcgMTEuMDQsMjMuNTExIC04LjE2MywyMS41NTdaIiBz
|
||||
dHlsZT0iZmlsbDojZDZjMjk1O3N0cm9rZTojMDAwO3N0cm9rZS13aWR0aDo2LjE2cHg7Ii8+PHBh
|
||||
dGggZD0iTTEwOS45NTEsMzExLjcxMmw2OC4wNzYsLTAiIHN0eWxlPSJmaWxsOm5vbmU7c3Ryb2tl
|
||||
OiMwMDA7c3Ryb2tlLXdpZHRoOjQuOTVweDsiLz48cGF0aCBkPSJNMTM5LjMwNSwzNTEuNjgzbDY4
|
||||
LjA3NiwwIiBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMDAwO3N0cm9rZS13aWR0aDo0Ljk1cHg7
|
||||
Ii8+PHBhdGggZD0iTTEzOS42MTcsMzMxLjY5OGwzOC4wOTgsLTAiIHN0eWxlPSJmaWxsOm5vbmU7
|
||||
c3Ryb2tlOiMwMDA7c3Ryb2tlLXdpZHRoOjQuOTVweDsiLz48cGF0aCBkPSJNODUuMjAxLDIxMS4y
|
||||
NTNjLTAsLTIuMjEzIC0xLjc5NywtNC4wMSAtNC4wMSwtNC4wMWwtNTMuNDg3LC0wYy0yLjIxNCwt
|
||||
MCAtNC4wMTEsMS43OTcgLTQuMDExLDQuMDFsMCw1OC44OTRjMCwyLjIxMyAxLjc5Nyw0LjAxIDQu
|
||||
MDExLDQuMDFsNTMuNDg3LDBjMi4yMTMsMCA0LjAxLC0xLjc5NyA0LjAxLC00LjAxbC0wLC01OC44
|
||||
OTRaIiBzdHlsZT0iZmlsbDojZWVhYzAxO3N0cm9rZTojMDAwO3N0cm9rZS13aWR0aDozLjQzcHg7
|
||||
Ii8+PHBhdGggZD0iTTU2NC42NDMsNDI2LjU5MWMwLC0yLjIxMyAtMS43OTYsLTQuMDEgLTQuMDEs
|
||||
LTQuMDFsLTUzLjQ4NywtMGMtMi4yMTMsLTAgLTQuMDEsMS43OTcgLTQuMDEsNC4wMWwwLDU4Ljg5
|
||||
NGMwLDIuMjEzIDEuNzk3LDQuMDEgNC4wMSw0LjAxbDUzLjQ4NywwYzIuMjE0LDAgNC4wMSwtMS43
|
||||
OTcgNC4wMSwtNC4wMWwwLC01OC44OTRaIiBzdHlsZT0iZmlsbDojZWVhYzAxO3N0cm9rZTojMDAw
|
||||
O3N0cm9rZS13aWR0aDozLjQzcHg7Ii8+PGNpcmNsZSBjeD0iNTE3LjAxNCIgY3k9IjEwOC43NzEi
|
||||
IHI9IjQuMzUyIiBzdHlsZT0iZmlsbDojZDNkMmIzOyIvPjxjaXJjbGUgY3g9IjUzMS4zMiIgY3k9
|
||||
IjEwOC43NzEiIHI9IjQuMzUyIiBzdHlsZT0iZmlsbDojZjRiMzAzOyIvPjxjaXJjbGUgY3g9IjU0
|
||||
NC43OTIiIGN5PSIxMDguNzcxIiByPSI0LjM1MiIgc3R5bGU9ImZpbGw6I2Y5NjEzZDsiLz48cGF0
|
||||
aCBkPSJNMTA1LjI1Nyw0ODQuNDNjLTAsMCAyMC4zNzQsLTI1LjIyNSA0MS43MTksLTcuNzYxYzIx
|
||||
LjM0NCwxNy40NjMgLTEyLjYxMyw1MC40NSAtNDAuNzQ5LDQyLjY4OSIgc3R5bGU9ImZpbGw6bm9u
|
||||
ZTtzdHJva2U6IzAwMDtzdHJva2Utd2lkdGg6OS45cHg7Ii8+PHBhdGggZD0iTTExMy4wMTksNDYw
|
||||
LjE0MWMtMCwtNC4yNjUgLTMuNDYzLC03LjcyNyAtNy43MjgsLTcuNzI3bC03OC42NTUsLTBjLTQu
|
||||
MjY1LC0wIC03LjcyNywzLjQ2MiAtNy43MjcsNy43MjdsLTAsNzguNjU1Yy0wLDQuMjY1IDMuNDYy
|
||||
LDcuNzI4IDcuNzI3LDcuNzI4bDc4LjY1NSwtMGM0LjI2NSwtMCA3LjcyOCwtMy40NjMgNy43Mjgs
|
||||
LTcuNzI4bC0wLC03OC42NTVaIiBzdHlsZT0iZmlsbDojNjFlMGVlO3N0cm9rZTojMDAwO3N0cm9r
|
||||
ZS13aWR0aDo0LjU1cHg7c3Ryb2tlLWxpbmVjYXA6c3F1YXJlOyIvPjxwYXRoIGQ9Ik03Ny4yNzcs
|
||||
Mzk1LjQwMmMtMi4yMzQsOC43NTUgLTYuNDk2LDE1LjM2NiAtMTEuNjQ5LDE3LjcxNGMxLjk5NSwz
|
||||
LjA1NyAzLjE2Nyw2Ljc4MSAzLjE2NywxMC43OTdjMCwxMC4zODcgLTcuODQsMTguODIgLTE3LjQ5
|
||||
OCwxOC44MmMtOS42NTcsMCAtMTcuNDk4LC04LjQzMyAtMTcuNDk4LC0xOC44MmMwLC05LjczMyA2
|
||||
Ljg4NCwtMTcuNzUxIDE1LjY5NywtMTguNzIyYy0zLjM5OSwtNS45NiAtNS41MjQsLTE0LjQyMyAt
|
||||
NS41MjQsLTIzLjgwNWMwLC0xOC4wMjQgNy44NDEsLTMyLjY1NyAxNy40OTksLTMyLjY1N2M2Ljc5
|
||||
MiwtMCAxMi42ODUsNy4yMzggMTUuNTg0LDE3LjgwMmMxLjYyOSwtMC41OTIgMy4zODcsLTAuOTE1
|
||||
IDUuMjIsLTAuOTE1YzguNDUsMCAxNS4zMTEsNi44NjEgMTUuMzExLDE1LjMxMWMtMCw4LjQ1IC02
|
||||
Ljg2MSwxNS4zMTEgLTE1LjMxMSwxNS4zMTFjLTEuNzUsMCAtMy40MzIsLTAuMjk0IC00Ljk5OCwt
|
||||
MC44MzZaIiBzdHlsZT0iZmlsbDojZmZmO2ZpbGwtb3BhY2l0eTowLjU7Ii8+PHBhdGggZD0iTTMz
|
||||
LjU4MywyMzMuOTNjMi41MjksLTAuMzM3IDQuNTEsLTEuMTc4IDYuNzE2LC0yLjQwM2MwLjk1Nywt
|
||||
MC41MzIgMS45NjksLTAuOTA0IDIuODk4LC0xLjQ4NWMwLjc4OSwtMC40OTMgMS4zNDMsLTAuOTc2
|
||||
IDEuMzQzLC0xLjk3OWMwLC0wLjIzNiAwLjIyNCwtMC42MzIgMCwtMC43MDdjLTAuOTUyLC0wLjMx
|
||||
NyAtMi4zNjQsMC40NjUgLTMuMDQsMC45OWMtMC42MjUsMC40ODYgLTEuNDk0LDAuNTc5IC0yLjEy
|
||||
LDEuMDZjLTEuMDM2LDAuNzk2IC0xLjY4MiwyLjQ2MyAtMS43NjcsMy43NDZjLTAuMDYsMC44OTYg
|
||||
LTAuMzU3LDIuODQgMC41NjUsMy4zOTNjMC4zNTcsMC4yMTQgMC45MjksMC4xNDIgMS4zNDMsMC4x
|
||||
NDJjMS4wOCwtMCAyLjEwNCwwLjAxMiAzLjE4MSwtMC4xNDJjMi4wODIsLTAuMjk3IDQuMTY1LC0x
|
||||
LjQ1NSA2LjAwOSwtMi40MDNjMi4wMTQsLTEuMDM2IDQuMDEyLC0xLjkyMSA1LjU4NCwtMy42MDVj
|
||||
MC42NDIsLTAuNjg4IDEuMDEsLTEuMjg2IDEuMjcyLC0yLjE5MWMwLjA1OSwtMC4yMDMgLTAuMDYs
|
||||
LTAuNzE4IDAuMDcxLC0wLjg0OWMwLjEwOCwtMC4xMDggMC4wOTksMC4yMjYgMC4wNzEsMC4yODNj
|
||||
LTAuMTg1LDAuMzY3IC0wLjQ2MiwwLjc0IC0wLjYzNywxLjEzMWMtMC4zMjQsMC43MjkgLTAuMzUz
|
||||
LDEuNzU5IC0wLjM1MywyLjU0NWMwLDAuMTMyIC0wLjA3LDAuNTcxIDAsMC42MzZjMC4wNTUsMC4w
|
||||
NTEgMC4zNjMsMC4xOTMgMC40MjQsMC4yMTJjMi4wMDIsMC42MzMgNC4zNDIsLTAuNTMyIDYuMDA5
|
||||
LC0xLjQ4NGMwLjY0MSwtMC4zNjcgMS4zNjMsLTAuODUgMi4wNiwtMS4xMTJjMC4wMjYsLTAuMDEg
|
||||
MC42NzgsLTAuMTk3IDAuNjk2LC0wLjE2MWMwLjI4MiwwLjU2MyAwLjExMSwxLjUxMSAwLjIxMiwy
|
||||
LjEyMWMwLjI4NSwxLjcwNyAyLjM2NywxLjYxNSAzLjY3NiwxLjQ4NGMwLjIwNywtMC4wMiAwLjM2
|
||||
MywtMC4yMTIgMC41NjYsLTAuMjEyIiBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMDAwO3N0cm9r
|
||||
ZS13aWR0aDoyLjc1cHg7Ii8+PHBhdGggZD0iTTM2LjQ4MiwyNDIuMzQyYzEuMzQ2LC0wIDIuNjgy
|
||||
LC0wLjE1NCA0LjAyOSwtMC4yMTJjNC4zMzIsLTAuMTg5IDguNjcxLC0wLjE5NiAxMy4wMDYsLTAu
|
||||
MjgzYzYuNTI1LC0wLjEzMSAxMy4wNTYsMC4wNzEgMTkuNTgxLDAuMDcxIiBzdHlsZT0iZmlsbDpu
|
||||
b25lO3N0cm9rZTojMDAwO3N0cm9rZS13aWR0aDoyLjc1cHg7Ii8+PHBhdGggZD0iTTUxNi4wOSw0
|
||||
NDIuMDMxYy0wLDAuOTM2IC0wLjUzMiwxLjg5MiAtMC4xMjYsMi44MzljMC41MjYsMS4yMjggMi4z
|
||||
NjMsMS4yNzUgMy40NjksMC45NDZjMC42MjgsLTAuMTg3IDEuMzE2LC0xLjE3NiAxLjcwMywtMS42
|
||||
NGMxLjAzOCwtMS4yNDcgMi40MzUsLTIuMzc1IDIuNzEyLC00LjAzN2MwLjA0MSwtMC4yNDggMC4x
|
||||
OTQsLTAuOTM2IDAuMDYzLC0xLjE5OGMtMC43NjMsLTEuNTI1IC0zLjA1OSwtMS40NzggLTQuMTY4
|
||||
LC0wLjM2OWMtMC4xNjQsMC4xNjQgLTAuNDQ2LDAuMjI3IC0wLjU2MywwLjQzMmMtMC45OTQsMS43
|
||||
NDUgLTAuNDM5LDQuMzQxIDAuNDQyLDUuOTkyYzAuMzA0LDAuNTY5IDAuODI5LDEuMDQ4IDEuMzI0
|
||||
LDEuNDUxYzAuMzM5LDAuMjc1IDAuNzc1LDAuNzE2IDEuMTk5LDAuOTI4YzMuMjYyLDEuNjMxIDcu
|
||||
Njg5LC0wLjEzMyAxMC41OTYsLTEuNzQ4YzEuOTUyLC0xLjA4NSA0LjEsLTIuMTUyIDQuMSwtNC42
|
||||
NjhjLTAsLTAuMzI1IDAuMDM3LC0wLjY5NCAtMC4wNjMsLTEuMDA5Yy0wLjM2MSwtMS4xMzMgLTIu
|
||||
MzA3LC0xLjA0MyAtMy4xNTQsLTAuNjk0Yy0xLjE3OSwwLjQ4NiAtMi4zNzMsMS4zOTUgLTMuMDks
|
||||
Mi40NmMtMS4xNzQsMS43NDIgLTEuMjA3LDQuMjgzIDAuNTY3LDUuNjE0YzEuMjU2LDAuOTQxIDIu
|
||||
OTU4LDAuOTIzIDQuNDQ3LDAuNzY2YzIuMjQ3LC0wLjIzNyA0LjI1MywtMS4zMTMgNi4wMjMsLTIu
|
||||
NjU5YzAuODc1LC0wLjY2NCAyLjA5NCwtMS4zMTYgMi42NDksLTIuMzMzYzAuMTczLC0wLjMxNiAw
|
||||
LjIxNSwtMC43NDUgMC4zNzksLTEuMDczYzAuMDcxLC0wLjE0MyAtMC4xNTgsMC4yODUgLTAuMTg5
|
||||
LDAuNDQyYy0wLjA1NiwwLjI3NiAtMC4wOTQsMC44NzggLTAsMS4xMzVjMC4zNzYsMS4wMjggMS4y
|
||||
MzMsMS41MDkgMi4yNywxLjcwM2MxLjM0NiwwLjI1MiAzLjAwOSwtMC4wMSA0LjM1MiwtMC4xODlj
|
||||
MC45MzksLTAuMTI1IDEuODkyLC0wLjUxNyAxLjg5MiwwLjc1NyIgc3R5bGU9ImZpbGw6bm9uZTtz
|
||||
dHJva2U6IzAwMDtzdHJva2Utd2lkdGg6Mi40NXB4OyIvPjxwYXRoIGQ9Ik01MTQuMjU5LDQ2MC4z
|
||||
NTljMC4yNjYsMC4xMzQgMC4wOCwxLjM1NCAwLjU2NywxLjM4OGMwLjQ0MiwwLjAzMSAwLjg4NSww
|
||||
LjA0IDEuMzI4LDAuMDVjMC41MTcsMC4wMTIgMS4xMDQsMC4wOTggMS42MzMsLTAuMDM0YzEuNjg4
|
||||
LC0wLjQyMiAzLjc5NiwtMS41OTggNC40ODIsLTMuMjk2YzAuMTg4LC0wLjQ2NyAwLjI0NiwtMS4w
|
||||
OTQgMC4zMTUsLTEuNTc3YzAuMDc1LC0wLjUyNCAwLjM3OCwtMS43MTggLTAuMDYzLC0yLjE0NGMt
|
||||
MC4wODgsLTAuMDg1IC0wLjYwNCwtMC4wNjMgLTAuNjk0LC0wLjA2M2MtMC42OTUsLTAgLTEuNzk2
|
||||
LDAuMzkyIC0yLjIwNywxLjAwOWMtMC4yNzMsMC40MDkgLTAuMTg5LDAuODUxIC0wLjE4OSwxLjMy
|
||||
NGMtMCwwLjYyIC0wLjAzMywxLjIyNCAwLjEyNiwxLjgzYzAuMzYyLDEuMzc5IDIuMTExLDEuOTQx
|
||||
IDMuMzQzLDIuMDE4YzIuNjk2LDAuMTY4IDUuNDQ3LC0wLjI0NSA4LjEzNiwtMGMyLjIwNywwLjIg
|
||||
NC40NTQsMC43MTcgNi42ODYsMC41MDRjMS41OCwtMC4xNSAzLjE1NCwtMC41MjUgNC43MywtMC42
|
||||
M2MzLjM5NCwtMC4yMjYgNi43OTEsLTAuNTA1IDEwLjIxOCwtMC41MDUiIHN0eWxlPSJmaWxsOm5v
|
||||
bmU7c3Ryb2tlOiMwMDA7c3Ryb2tlLXdpZHRoOjIuNDVweDsiLz48cGF0aCBkPSJNNTE5LjA1NCw0
|
||||
NzEuNjhsMCwwLjk0NmMwLDAuMTU1IC0wLDAuMjUxIDAuMDYzLDAuMzc5bDAsMC4xMjZjMC42MDIs
|
||||
LTIuNDA3IDMuNzc1LC0yLjc4NCA1LjgxLC0yLjc4NGMwLjE1LC0wIDAuMjMyLDAuNDggMC4yNDUs
|
||||
MC41NzZjMC4wNTUsMC4zODUgLTAuMTkyLDEuNDUgMC4zNzksMS42NGMwLjk4OSwwLjMzIDIuNDA2
|
||||
LC0wLjMxOCAzLjM0MywtMC41NjhjMC4yMzUsLTAuMDYyIDEuMDA5LC0wLjQ0MSAxLjE5OCwtMC4y
|
||||
NTJjMC45NTQsMC45NTQgMi40MTIsMC41NjIgMy43MjEsMC42MzFjMy4zMzIsMC4xNzUgNi42Mjks
|
||||
LTAuMTg5IDkuOTY2LC0wLjE4OWMxLjA1OCwtMCAyLjE2MiwwLjA4NyAzLjIxNywtMGMwLjA2OCwt
|
||||
MC4wMDYgMS4wMDksLTAuMDI3IDEuMDA5LC0wLjI1MyIgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2U6
|
||||
IzAwMDtzdHJva2Utd2lkdGg6Mi40NXB4OyIvPjwvZz48Zz48cGF0aCBkPSJNMzc0LjU3OCwxMTQu
|
||||
NDU1Yy0wLjYxMywtNC4yMTYgLTQuNTMzLC03LjE0MiAtOC43NDksLTYuNTNsLTk5Ljk0NSwxNC41
|
||||
MTVjLTQuMjE2LDAuNjEzIC03LjE0Miw0LjUzMyAtNi41Myw4Ljc0OWwyLjIxOSwxNS4yNzhjMC42
|
||||
MTIsNC4yMTYgNC41MzIsNy4xNDIgOC43NDksNi41M2w5OS45NDUsLTE0LjUxNWM0LjIxNiwtMC42
|
||||
MTIgNy4xNDIsLTQuNTMzIDYuNTI5LC04Ljc0OWwtMi4yMTgsLTE1LjI3OFoiIHN0eWxlPSJmaWxs
|
||||
OiM5ZGFjY2Q7c3Ryb2tlOiMwMDA7c3Ryb2tlLXdpZHRoOjUuMjFweDsiLz48cGF0aCBkPSJNMzEz
|
||||
Ljg2OSwxMTcuNDE0YzAsMCA2Ni4xMDIsOS44NzggNzcuNTgyLC0yMy43MzNjMTEuNDgsLTMzLjYx
|
||||
MSAtMTIuOTg3LC01MS44NzMgLTQxLjYzNywtMzkuOTIxYy0yOC42NSwxMS45NTIgLTU0Ljg1LDQ2
|
||||
LjE0MiAtNTQuODUsNDYuMTQyYzAsMCAtMzAuMDA1LC04LjEwOCAtNDEuMzE0LDUuMjIxYy0xMS4z
|
||||
MDksMTMuMzMgLTE3LjE2NywyNi42NDYgLTIuNDM1LDMxLjUxOWMxNC43MzIsNC44NzMgNjIuNjU0
|
||||
LC0xOS4yMjggNjIuNjU0LC0xOS4yMjhaIiBzdHlsZT0iZmlsbDojNzM1ZDkzO3N0cm9rZTojMDAw
|
||||
O3N0cm9rZS13aWR0aDo1LjkzcHg7Ii8+PHBhdGggZD0iTTM3NC4xNDgsMTM2Ljg3NmMtMCwwIDM4
|
||||
LjQ5MywtOS4zMzggMzYuNTYzLC0zLjU0OWMtMS45Myw1Ljc5IC0zNy42MzgsMjYuOTAxIC0xMTQu
|
||||
ODMxLDE1Ljk2NmM3MC4zNDUsLTEwLjczMSA3OC4yNjgsLTEyLjQxNyA3OC4yNjgsLTEyLjQxN1oi
|
||||
IHN0eWxlPSJmaWxsOiM1MDNhNzE7c3Ryb2tlOiMwMDA7c3Ryb2tlLXdpZHRoOjQuOTVweDsiLz48
|
||||
cGF0aCBkPSJNMzU3LjM0NiwxMjQuOTA2bC01NC4wMjEsOS42MTJsLTAsNS4zOTlsNTQuMDIxLC05
|
||||
LjYxMmwwLC01LjM5OVoiIHN0eWxlPSJmaWxsOiNmZmY7Ii8+PGNpcmNsZSBjeD0iMzAwLjE1IiBj
|
||||
eT0iMTM3LjE5OCIgcj0iNC42NzciIHN0eWxlPSJmaWxsOiNmZmIyNWM7c3Ryb2tlOiMwMDA7c3Ry
|
||||
b2tlLXdpZHRoOjQuOTVweDsiLz48Y2lyY2xlIGN4PSIzNjEuNDEiIGN5PSIxMjcuNTgxIiByPSI0
|
||||
LjI4MiIgc3R5bGU9ImZpbGw6I2ZmYjI1YztzdHJva2U6IzAwMDtzdHJva2Utd2lkdGg6NC45NXB4
|
||||
OyIvPjwvZz48L3N2Zz4=
|
3
testdata/logo.svg.base64.license
vendored
3
testdata/logo.svg.base64.license
vendored
|
@ -1,3 +0,0 @@
|
|||
SPDX-FileCopyrightText: 2022 Maria Letta, the go-mail Team
|
||||
|
||||
SPDX-License-Identifier: CC-BY-ND-4.0
|
3
testdata/logo.svg.license
vendored
3
testdata/logo.svg.license
vendored
|
@ -1,3 +0,0 @@
|
|||
SPDX-FileCopyrightText: 2022 Maria Letta, the go-mail Team
|
||||
|
||||
SPDX-License-Identifier: CC-BY-ND-4.0
|
0
testdata/tmp/.gitkeep
vendored
0
testdata/tmp/.gitkeep
vendored
13
util_test.go
13
util_test.go
|
@ -19,7 +19,7 @@ const (
|
|||
keyECDSAFilePath = "dummy-child-key-ecdsa.pem"
|
||||
)
|
||||
|
||||
// getDummyRSACryptoMaterial loads a certificate (RSA), the associated private key and certificate (RSA) is loaded from local disk for testing purposes
|
||||
// getDummyRSACryptoMaterial loads a certificate (RSA) and the associated private key (ECDSA) form local disk for testing purposes
|
||||
func getDummyRSACryptoMaterial() (*rsa.PrivateKey, *x509.Certificate, *x509.Certificate, error) {
|
||||
keyPair, err := tls.LoadX509KeyPair(certRSAFilePath, keyRSAFilePath)
|
||||
if err != nil {
|
||||
|
@ -41,7 +41,7 @@ func getDummyRSACryptoMaterial() (*rsa.PrivateKey, *x509.Certificate, *x509.Cert
|
|||
return privateKey, certificate, intermediateCertificate, nil
|
||||
}
|
||||
|
||||
// getDummyECDSACryptoMaterial loads a certificate (ECDSA), the associated private key and certificate (ECDSA) is loaded from local disk for testing purposes
|
||||
// getDummyECDSACryptoMaterial loads a certificate (ECDSA) and the associated private key (ECDSA) form local disk for testing purposes
|
||||
func getDummyECDSACryptoMaterial() (*ecdsa.PrivateKey, *x509.Certificate, *x509.Certificate, error) {
|
||||
keyPair, err := tls.LoadX509KeyPair(certECDSAFilePath, keyECDSAFilePath)
|
||||
if err != nil {
|
||||
|
@ -62,12 +62,3 @@ func getDummyECDSACryptoMaterial() (*ecdsa.PrivateKey, *x509.Certificate, *x509.
|
|||
|
||||
return privateKey, certificate, intermediateCertificate, nil
|
||||
}
|
||||
|
||||
// getDummyKeyPairTLS loads a certificate (ECDSA) as *tls.Certificate, the associated private key and certificate (ECDSA) is loaded from local disk for testing purposes
|
||||
func getDummyKeyPairTLS() (*tls.Certificate, error) {
|
||||
keyPair, err := tls.LoadX509KeyPair(certECDSAFilePath, keyECDSAFilePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &keyPair, err
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue