mirror of
https://github.com/wneessen/go-mail.git
synced 2024-11-15 02:12:55 +01:00
Merge pull request #348 from wneessen/feature/overhaul-tests
Overhaul test suite and GH CI workflow
This commit is contained in:
commit
c8478fb6c0
42 changed files with 12648 additions and 6964 deletions
23
.cirrus.yml
23
.cirrus.yml
|
@ -1,23 +0,0 @@
|
||||||
# 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 ./...
|
|
218
.github/workflows/ci.yml
vendored
Normal file
218
.github/workflows/ci.yml
vendored
Normal file
|
@ -0,0 +1,218 @@
|
||||||
|
# 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@a6993e2c61fd5dc440b409aa1d6904921c5e1894 # v4.3.5
|
||||||
|
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@dc2f7b0dd95544cd550de3028f89193576e958b9 # master
|
||||||
|
timeout-minutes: 5
|
||||||
|
env:
|
||||||
|
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||||
|
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
|
67
.github/workflows/codecov.yml
vendored
67
.github/workflows/codecov.yml
vendored
|
@ -1,67 +0,0 @@
|
||||||
# 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@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
|
|
||||||
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
|
|
31
.github/workflows/dependency-review.yml
vendored
31
.github/workflows/dependency-review.yml
vendored
|
@ -1,31 +0,0 @@
|
||||||
# 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@a6993e2c61fd5dc440b409aa1d6904921c5e1894 # v4.3.5
|
|
54
.github/workflows/golangci-lint.yml
vendored
54
.github/workflows/golangci-lint.yml
vendored
|
@ -1,54 +0,0 @@
|
||||||
# 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@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
|
|
||||||
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
21
.github/workflows/govulncheck.yml
vendored
|
@ -1,21 +0,0 @@
|
||||||
# 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
45
.github/workflows/offline-tests.yml
vendored
|
@ -1,45 +0,0 @@
|
||||||
# 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@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
|
|
||||||
with:
|
|
||||||
go-version: ${{ matrix.go }}
|
|
||||||
- name: Run Tests
|
|
||||||
run: |
|
|
||||||
go test -race -shuffle=on ./...
|
|
23
.github/workflows/reuse.yml
vendored
23
.github/workflows/reuse.yml
vendored
|
@ -1,23 +0,0 @@
|
||||||
# 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
|
|
56
.github/workflows/sonarqube.yml
vendored
56
.github/workflows/sonarqube.yml
vendored
|
@ -1,56 +0,0 @@
|
||||||
# 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@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
|
|
||||||
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,4 +56,6 @@ crashlytics.properties
|
||||||
crashlytics-build.properties
|
crashlytics-build.properties
|
||||||
fabric.properties
|
fabric.properties
|
||||||
|
|
||||||
testdata
|
## Coverage data
|
||||||
|
coverage.coverprofile
|
||||||
|
coverage.html
|
|
@ -13,8 +13,8 @@ import (
|
||||||
// in encoding processes.
|
// in encoding processes.
|
||||||
var newlineBytes = []byte(SingleNewLine)
|
var newlineBytes = []byte(SingleNewLine)
|
||||||
|
|
||||||
// ErrNoOutWriter is the error message returned when no io.Writer is set for Base64LineBreaker.
|
// ErrNoOutWriter is the error returned when no io.Writer is set for Base64LineBreaker.
|
||||||
const ErrNoOutWriter = "no io.Writer set for Base64LineBreaker"
|
var ErrNoOutWriter = errors.New("no io.Writer set for Base64LineBreaker")
|
||||||
|
|
||||||
// Base64LineBreaker handles base64 encoding with the insertion of new lines after a certain number
|
// Base64LineBreaker handles base64 encoding with the insertion of new lines after a certain number
|
||||||
// of characters.
|
// of characters.
|
||||||
|
@ -44,7 +44,7 @@ type Base64LineBreaker struct {
|
||||||
// - err: An error if one occurred during the write operation.
|
// - err: An error if one occurred during the write operation.
|
||||||
func (l *Base64LineBreaker) Write(data []byte) (numBytes int, err error) {
|
func (l *Base64LineBreaker) Write(data []byte) (numBytes int, err error) {
|
||||||
if l.out == nil {
|
if l.out == nil {
|
||||||
err = errors.New(ErrNoOutWriter)
|
err = ErrNoOutWriter
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if l.used+len(data) < MaxBodyLength {
|
if l.used+len(data) < MaxBodyLength {
|
||||||
|
|
|
@ -5,487 +5,165 @@
|
||||||
package mail
|
package mail
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"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 (
|
var (
|
||||||
errMockDefault = errors.New("mock write error")
|
errClosedWriter = errors.New("writer is already closed")
|
||||||
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) {
|
func TestBase64LineBreaker(t *testing.T) {
|
||||||
l, err := os.Open("assets/gopher2.svg")
|
t.Run("write, copy and close", func(t *testing.T) {
|
||||||
if err != nil {
|
logoWriter := bytes.NewBuffer(nil)
|
||||||
t.Errorf("failed to open gopher logo asset: %s", err)
|
lineBreaker := &Base64LineBreaker{out: logoWriter}
|
||||||
return
|
t.Cleanup(func() {
|
||||||
}
|
if err := lineBreaker.Close(); err != nil {
|
||||||
defer func() { _ = l.Close() }()
|
t.Errorf("failed to close line breaker: %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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// removeNewLines removes any newline characters from the given data
|
// removeNewLines is a test helper thatremoves all newline characters ('\r' and '\n') from the given byte slice.
|
||||||
func removeNewLines(data []byte) []byte {
|
func removeNewLines(t *testing.T, data []byte) []byte {
|
||||||
|
t.Helper()
|
||||||
result := make([]byte, len(data))
|
result := make([]byte, len(data))
|
||||||
n := 0
|
n := 0
|
||||||
|
|
||||||
|
@ -503,11 +181,11 @@ func removeNewLines(data []byte) []byte {
|
||||||
type errorWriter struct{}
|
type errorWriter struct{}
|
||||||
|
|
||||||
func (e errorWriter) Write([]byte) (int, error) {
|
func (e errorWriter) Write([]byte) (int, error) {
|
||||||
return 0, fmt.Errorf("supposed to always fail")
|
return 0, errClosedWriter
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e errorWriter) Close() error {
|
func (e errorWriter) Close() error {
|
||||||
return fmt.Errorf("supposed to always fail")
|
return errClosedWriter
|
||||||
}
|
}
|
||||||
|
|
||||||
type mockWriterExcess struct {
|
type mockWriterExcess struct {
|
||||||
|
@ -539,19 +217,49 @@ func (w *mockWriterNewline) Write(p []byte) (n int, err error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func FuzzBase64LineBreaker_Write(f *testing.F) {
|
func FuzzBase64LineBreaker(f *testing.F) {
|
||||||
f.Add([]byte("abc"))
|
seedData := [][]byte{
|
||||||
f.Add([]byte("def"))
|
[]byte(""),
|
||||||
f.Add([]uint8{0o0, 0o1, 0o2, 30, 255})
|
[]byte("abc"),
|
||||||
buf := bytes.Buffer{}
|
[]byte("def"),
|
||||||
bw := bufio.NewWriter(&buf)
|
[]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)
|
||||||
|
}
|
||||||
|
|
||||||
f.Fuzz(func(t *testing.T, data []byte) {
|
f.Fuzz(func(t *testing.T, data []byte) {
|
||||||
b := &Base64LineBreaker{out: bw}
|
buffer := bytes.NewBuffer(nil)
|
||||||
if _, err := b.Write(data); err != nil {
|
lineBreaker := &Base64LineBreaker{
|
||||||
t.Errorf("failed to write to B64LineBreaker: %s", err)
|
out: buffer,
|
||||||
}
|
}
|
||||||
if err := b.Close(); err != nil {
|
base64Encoder := base64.NewEncoder(base64.StdEncoding, lineBreaker)
|
||||||
t.Errorf("failed to close B64LineBreaker: %s", err)
|
|
||||||
|
_, 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")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
32
client.go
32
client.go
|
@ -242,6 +242,12 @@ var (
|
||||||
// provided as argument to the WithDSN Option.
|
// provided as argument to the WithDSN Option.
|
||||||
ErrInvalidDSNRcptNotifyCombination = errors.New("DSN rcpt notify option NEVER cannot be " +
|
ErrInvalidDSNRcptNotifyCombination = errors.New("DSN rcpt notify option NEVER cannot be " +
|
||||||
"combined with any of SUCCESS, FAILURE or DELAY")
|
"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.
|
// NewClient creates a new Client instance with the provided host and optional configuration Option functions.
|
||||||
|
@ -510,6 +516,9 @@ func WithSMTPAuth(authtype SMTPAuthType) Option {
|
||||||
// - An Option function that sets the custom SMTP authentication for the Client.
|
// - An Option function that sets the custom SMTP authentication for the Client.
|
||||||
func WithSMTPAuthCustom(smtpAuth smtp.Auth) Option {
|
func WithSMTPAuthCustom(smtpAuth smtp.Auth) Option {
|
||||||
return func(c *Client) error {
|
return func(c *Client) error {
|
||||||
|
if smtpAuth == nil {
|
||||||
|
return ErrSMTPAuthMethodIsNil
|
||||||
|
}
|
||||||
c.smtpAuth = smtpAuth
|
c.smtpAuth = smtpAuth
|
||||||
c.smtpAuthType = SMTPAuthCustom
|
c.smtpAuthType = SMTPAuthCustom
|
||||||
return nil
|
return nil
|
||||||
|
@ -671,6 +680,9 @@ func WithoutNoop() Option {
|
||||||
// - An Option function that sets the custom DialContextFunc for the Client.
|
// - An Option function that sets the custom DialContextFunc for the Client.
|
||||||
func WithDialContextFunc(dialCtxFunc DialContextFunc) Option {
|
func WithDialContextFunc(dialCtxFunc DialContextFunc) Option {
|
||||||
return func(c *Client) error {
|
return func(c *Client) error {
|
||||||
|
if dialCtxFunc == nil {
|
||||||
|
return ErrDialContextFuncIsNil
|
||||||
|
}
|
||||||
c.dialContextFunc = dialCtxFunc
|
c.dialContextFunc = dialCtxFunc
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -739,6 +751,7 @@ func (c *Client) SetTLSPolicy(policy TLSPolicy) {
|
||||||
func (c *Client) SetTLSPortPolicy(policy TLSPolicy) {
|
func (c *Client) SetTLSPortPolicy(policy TLSPolicy) {
|
||||||
if c.port == DefaultPort {
|
if c.port == DefaultPort {
|
||||||
c.port = DefaultPortTLS
|
c.port = DefaultPortTLS
|
||||||
|
c.fallbackPort = 0
|
||||||
|
|
||||||
if policy == TLSOpportunistic {
|
if policy == TLSOpportunistic {
|
||||||
c.fallbackPort = DefaultPort
|
c.fallbackPort = DefaultPort
|
||||||
|
@ -1081,10 +1094,6 @@ func (c *Client) DialAndSendWithContext(ctx context.Context, messages ...*Msg) e
|
||||||
// - An error if the connection check fails, if no supported authentication method is found,
|
// - An error if the connection check fails, if no supported authentication method is found,
|
||||||
// or if the authentication process fails.
|
// or if the authentication process fails.
|
||||||
func (c *Client) auth() error {
|
func (c *Client) auth() error {
|
||||||
if err := c.checkConn(); err != nil {
|
|
||||||
return fmt.Errorf("failed to authenticate: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.smtpAuth == nil && c.smtpAuthType != SMTPAuthNoAuth {
|
if c.smtpAuth == nil && c.smtpAuthType != SMTPAuthNoAuth {
|
||||||
hasSMTPAuth, smtpAuthType := c.smtpClient.Extension("AUTH")
|
hasSMTPAuth, smtpAuthType := c.smtpClient.Extension("AUTH")
|
||||||
if !hasSMTPAuth {
|
if !hasSMTPAuth {
|
||||||
|
@ -1252,14 +1261,13 @@ func (c *Client) sendSingleMsg(message *Msg) error {
|
||||||
affectedMsg: message,
|
affectedMsg: message,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
message.isDelivered = true
|
|
||||||
|
|
||||||
if err = writer.Close(); err != nil {
|
if err = writer.Close(); err != nil {
|
||||||
return &SendError{
|
return &SendError{
|
||||||
Reason: ErrSMTPDataClose, errlist: []error{err}, isTemp: isTempError(err),
|
Reason: ErrSMTPDataClose, errlist: []error{err}, isTemp: isTempError(err),
|
||||||
affectedMsg: message,
|
affectedMsg: message,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
message.isDelivered = true
|
||||||
|
|
||||||
if err = c.Reset(); err != nil {
|
if err = c.Reset(); err != nil {
|
||||||
return &SendError{
|
return &SendError{
|
||||||
|
@ -1267,12 +1275,6 @@ func (c *Client) sendSingleMsg(message *Msg) error {
|
||||||
affectedMsg: message,
|
affectedMsg: message,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err = c.checkConn(); err != nil {
|
|
||||||
return &SendError{
|
|
||||||
Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err),
|
|
||||||
affectedMsg: message,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1289,6 +1291,9 @@ func (c *Client) sendSingleMsg(message *Msg) error {
|
||||||
// - An error if there is no active connection, if the NOOP command fails, or if extending
|
// - An error if there is no active connection, if the NOOP command fails, or if extending
|
||||||
// the deadline fails; otherwise, returns nil.
|
// the deadline fails; otherwise, returns nil.
|
||||||
func (c *Client) checkConn() error {
|
func (c *Client) checkConn() error {
|
||||||
|
if c.smtpClient == nil {
|
||||||
|
return ErrNoActiveConnection
|
||||||
|
}
|
||||||
if !c.smtpClient.HasConnection() {
|
if !c.smtpClient.HasConnection() {
|
||||||
return ErrNoActiveConnection
|
return ErrNoActiveConnection
|
||||||
}
|
}
|
||||||
|
@ -1347,9 +1352,6 @@ func (c *Client) setDefaultHelo() error {
|
||||||
// - An error if there is no active connection, if STARTTLS is required but not supported,
|
// - 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.
|
// or if there are issues during the TLS handshake; otherwise, returns nil.
|
||||||
func (c *Client) tls() error {
|
func (c *Client) tls() error {
|
||||||
if !c.smtpClient.HasConnection() {
|
|
||||||
return ErrNoActiveConnection
|
|
||||||
}
|
|
||||||
if !c.useSSL && c.tlspolicy != NoTLS {
|
if !c.useSSL && c.tlspolicy != NoTLS {
|
||||||
hasStartTLS := false
|
hasStartTLS := false
|
||||||
extension, _ := c.smtpClient.Extension("STARTTLS")
|
extension, _ := c.smtpClient.Extension("STARTTLS")
|
||||||
|
|
126
client_121_test.go
Normal file
126
client_121_test.go
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
// 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")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
6033
client_test.go
6033
client_test.go
File diff suppressed because it is too large
Load diff
|
@ -6,17 +6,17 @@ coverage:
|
||||||
status:
|
status:
|
||||||
project:
|
project:
|
||||||
default:
|
default:
|
||||||
target: 85%
|
target: 90%
|
||||||
threshold: 5%
|
threshold: 2%
|
||||||
base: auto
|
base: auto
|
||||||
if_ci_failed: error
|
if_ci_failed: error
|
||||||
only_pulls: false
|
only_pulls: false
|
||||||
patch:
|
patch:
|
||||||
default:
|
default:
|
||||||
target: 80%
|
target: 90%
|
||||||
base: auto
|
base: auto
|
||||||
if_ci_failed: error
|
if_ci_failed: error
|
||||||
threshold: 5%
|
threshold: 2%
|
||||||
|
|
||||||
comment:
|
comment:
|
||||||
require_changes: true
|
require_changes: true
|
||||||
|
|
12
eml.go
12
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)
|
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)
|
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)
|
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)
|
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 {
|
for _, addr := range parsedAddrs {
|
||||||
addrStrings = append(addrStrings, addr.String())
|
addrStrings = append(addrStrings, addr.String())
|
||||||
}
|
}
|
||||||
if err = addrFunc(addrStrings...); err != nil {
|
// We can skip the error checking here since netmail.ParseAddressList already performed the
|
||||||
return fmt.Errorf(`failed to parse %q header: %w`, addrHeader, err)
|
// same address checking that the msg methods do.
|
||||||
}
|
_ = addrFunc(addrStrings...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -600,6 +600,8 @@ func parseEMLAttachmentEmbed(contentDisposition []string, multiPart *multipart.P
|
||||||
if err := msg.EmbedReader(filename, dataReader); err != nil {
|
if err := msg.EmbedReader(filename, dataReader); err != nil {
|
||||||
return fmt.Errorf("failed to embed multipart body: %w", err)
|
return fmt.Errorf("failed to embed multipart body: %w", err)
|
||||||
}
|
}
|
||||||
|
default:
|
||||||
|
return errors.New("unsupported content disposition type")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
588
eml_test.go
588
eml_test.go
|
@ -6,11 +6,8 @@ package mail
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -22,6 +19,23 @@ Subject: Saying Hello
|
||||||
Date: Fri, 21 Nov 1997 09:55:06 -0600
|
Date: Fri, 21 Nov 1997 09:55:06 -0600
|
||||||
Message-ID: <1234@local.machine.example>
|
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.
|
This is a message just to say hello.
|
||||||
So, "Hello".`
|
So, "Hello".`
|
||||||
exampleMailPlainNoEnc = `Date: Wed, 01 Nov 2023 00:00:00 +0000
|
exampleMailPlainNoEnc = `Date: Wed, 01 Nov 2023 00:00:00 +0000
|
||||||
|
@ -42,6 +56,52 @@ This is a test mail. Please do not reply to this. Also this line is very long so
|
||||||
should be wrapped.
|
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!
|
Thank your for your business!
|
||||||
The go-mail team
|
The go-mail team
|
||||||
|
|
||||||
|
@ -304,6 +364,128 @@ VGhpcyBpcyBhIHNpbXBsZSB0ZXN0IHRleHQgZmlsZSBhdHRhY2htZW50LgoKSXQgCiAgaGFzCiAg
|
||||||
ICAgc2V2ZXJhbAogICAgICAgICAgICBuZXdsaW5lcwoJICAgICAgICAgICAgYW5kCgkgICAgc3Bh
|
ICAgc2V2ZXJhbAogICAgICAgICAgICBuZXdsaW5lcwoJICAgICAgICAgICAgYW5kCgkgICAgc3Bh
|
||||||
Y2VzCiAgICAgaW4KICBpdAouCgpBcyB3ZWxsIGFzIGFuIGVtb2ppOiDwn5mCCg==
|
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--`
|
--45c75ff528359022eb03679fbe91877d75343f2e1f8193e349deffa33ff7--`
|
||||||
exampleMailPlainB64WithAttachmentNoBoundary = `Date: Wed, 01 Nov 2023 00:00:00 +0000
|
exampleMailPlainB64WithAttachmentNoBoundary = `Date: Wed, 01 Nov 2023 00:00:00 +0000
|
||||||
MIME-Version: 1.0
|
MIME-Version: 1.0
|
||||||
|
@ -578,6 +760,39 @@ Content-Disposition: attachment;
|
||||||
filename="testfile.txt"
|
filename="testfile.txt"
|
||||||
|
|
||||||
VGhpcyBpcyBhIHRlc3QgaW4gQmFzZTY0
|
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--`
|
--------------26A45336F6C6196BD8BBA2A2--`
|
||||||
exampleMultiPart8BitBase64 = `Date: Wed, 01 Nov 2023 00:00:00 +0000
|
exampleMultiPart8BitBase64 = `Date: Wed, 01 Nov 2023 00:00:00 +0000
|
||||||
MIME-Version: 1.0
|
MIME-Version: 1.0
|
||||||
|
@ -612,8 +827,352 @@ Content-Disposition: attachment;
|
||||||
|
|
||||||
VGhpcyBpcyBhIHRlc3QgaW4gQmFzZTY0
|
VGhpcyBpcyBhIHRlc3QgaW4gQmFzZTY0
|
||||||
--------------26A45336F6C6196BD8BBA2A2--`
|
--------------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) {
|
func TestEMLToMsgFromString(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
@ -621,26 +1180,6 @@ func TestEMLToMsgFromString(t *testing.T) {
|
||||||
enc string
|
enc string
|
||||||
sub 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 {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
@ -1009,3 +1548,6 @@ func stringToTempFile(data, name string) (string, string, error) {
|
||||||
}
|
}
|
||||||
return tempDir, filePath, nil
|
return tempDir, filePath, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
309
file_test.go
309
file_test.go
|
@ -6,134 +6,183 @@ package mail
|
||||||
|
|
||||||
import "testing"
|
import "testing"
|
||||||
|
|
||||||
// TestFile_SetGetHeader tests the set-/getHeader method of the File object
|
func TestFile(t *testing.T) {
|
||||||
func TestFile_SetGetHeader(t *testing.T) {
|
t.Run("setHeader", func(t *testing.T) {
|
||||||
f := File{
|
f := File{
|
||||||
Name: "testfile.txt",
|
Name: "testfile.txt",
|
||||||
Header: make(map[string][]string),
|
Header: make(map[string][]string),
|
||||||
}
|
}
|
||||||
f.setHeader(HeaderContentType, "text/plain")
|
f.setHeader(HeaderContentType, "text/plain")
|
||||||
fi, ok := f.getHeader(HeaderContentType)
|
contentType, ok := f.Header[HeaderContentType.String()]
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Errorf("getHeader method of File did not return a value")
|
t.Fatalf("setHeader failed. Expected header %s to be set", HeaderContentType)
|
||||||
return
|
}
|
||||||
}
|
if len(contentType) != 1 {
|
||||||
if fi != "text/plain" {
|
t.Fatalf("setHeader failed. Expected header %s to have one value, got: %d", HeaderContentType,
|
||||||
t.Errorf("getHeader returned wrong value. Expected: %s, got: %s", "text/plain", fi)
|
len(contentType))
|
||||||
}
|
}
|
||||||
fi, ok = f.getHeader(HeaderContentTransferEnc)
|
if contentType[0] != "text/plain" {
|
||||||
if ok {
|
t.Fatalf("setHeader failed. Expected header %s to have value %s, got: %s",
|
||||||
t.Errorf("getHeader method of File did return a value, but wasn't supposed to")
|
HeaderContentType.String(), "text/plain", contentType[0])
|
||||||
return
|
}
|
||||||
}
|
})
|
||||||
if fi != "" {
|
t.Run("getHeader", func(t *testing.T) {
|
||||||
t.Errorf("getHeader returned wrong value. Expected: %s, got: %s", "", fi)
|
f := File{
|
||||||
}
|
Name: "testfile.txt",
|
||||||
}
|
Header: make(map[string][]string),
|
||||||
|
}
|
||||||
// TestFile_WithFileDescription tests the WithFileDescription option
|
f.setHeader(HeaderContentType, "text/plain")
|
||||||
func TestFile_WithFileDescription(t *testing.T) {
|
contentType, ok := f.getHeader(HeaderContentType)
|
||||||
tests := []struct {
|
if !ok {
|
||||||
name string
|
t.Fatalf("setHeader failed. Expected header %s to be set", HeaderContentType)
|
||||||
desc string
|
}
|
||||||
}{
|
if contentType != "text/plain" {
|
||||||
{"File description: test", "test"},
|
t.Fatalf("setHeader failed. Expected header %s to have value %s, got: %s",
|
||||||
{"File description: empty", ""},
|
HeaderContentType.String(), "text/plain", contentType)
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
})
|
||||||
m := NewMsg()
|
t.Run("WithFileDescription", func(t *testing.T) {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
tests := []struct {
|
||||||
m.AttachFile("file.go", WithFileDescription(tt.desc))
|
name string
|
||||||
al := m.GetAttachments()
|
desc string
|
||||||
if len(al) <= 0 {
|
}{
|
||||||
t.Errorf("AttachFile() failed. Attachment list is empty")
|
{"File description: test", "test"},
|
||||||
}
|
{"File description: with newline", "test\n"},
|
||||||
a := al[0]
|
{"File description: empty", ""},
|
||||||
if a.Desc != tt.desc {
|
}
|
||||||
t.Errorf("WithFileDescription() failed. Expected: %s, got: %s", tt.desc, a.Desc)
|
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 {
|
||||||
// TestFile_WithContentID tests the WithFileContentID option
|
t.Fatalf("failed to retrieve attachments list")
|
||||||
func TestFile_WithContentID(t *testing.T) {
|
}
|
||||||
tests := []struct {
|
firstAttachment := attachments[0]
|
||||||
name string
|
if firstAttachment == nil {
|
||||||
contentid string
|
t.Fatalf("failed to retrieve first attachment, got nil")
|
||||||
}{
|
}
|
||||||
{"File Content-ID: test", "test"},
|
if firstAttachment.Desc != tt.desc {
|
||||||
{"File Content-ID: empty", ""},
|
t.Errorf("WithFileDescription() failed. Expected: %s, got: %s", tt.desc,
|
||||||
}
|
firstAttachment.Desc)
|
||||||
for _, tt := range tests {
|
}
|
||||||
m := NewMsg()
|
})
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
}
|
||||||
m.AttachFile("file.go", WithFileContentID(tt.contentid))
|
})
|
||||||
al := m.GetAttachments()
|
t.Run("WithFileContentID", func(t *testing.T) {
|
||||||
if len(al) <= 0 {
|
tests := []struct {
|
||||||
t.Errorf("AttachFile() failed. Attachment list is empty")
|
name string
|
||||||
}
|
id string
|
||||||
a := al[0]
|
}{
|
||||||
if a.Header.Get(HeaderContentID.String()) != tt.contentid {
|
{"Content-ID: test", "test"},
|
||||||
t.Errorf("WithFileContentID() failed. Expected: %s, got: %s", tt.contentid,
|
{"Content-ID: with newline", "test\n"},
|
||||||
a.Header.Get(HeaderContentID.String()))
|
{"Content-ID: empty", ""},
|
||||||
}
|
}
|
||||||
})
|
for _, tt := range tests {
|
||||||
}
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
}
|
message := NewMsg()
|
||||||
|
message.AttachFile("file.go", WithFileContentID(tt.id))
|
||||||
// TestFile_WithFileEncoding tests the WithFileEncoding option
|
attachments := message.GetAttachments()
|
||||||
func TestFile_WithFileEncoding(t *testing.T) {
|
if len(attachments) <= 0 {
|
||||||
tests := []struct {
|
t.Fatalf("failed to retrieve attachments list")
|
||||||
name string
|
}
|
||||||
enc Encoding
|
firstAttachment := attachments[0]
|
||||||
want Encoding
|
if firstAttachment == nil {
|
||||||
}{
|
t.Fatalf("failed to retrieve first attachment, got nil")
|
||||||
{"File encoding: 8bit raw", NoEncoding, NoEncoding},
|
}
|
||||||
{"File encoding: Base64", EncodingB64, EncodingB64},
|
contentID := firstAttachment.Header.Get(HeaderContentID.String())
|
||||||
{"File encoding: quoted-printable (not allowed)", EncodingQP, ""},
|
if contentID != tt.id {
|
||||||
}
|
t.Errorf("WithFileContentID() failed. Expected: %s, got: %s", tt.id,
|
||||||
for _, tt := range tests {
|
contentID)
|
||||||
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.Run("WithFileEncoding", func(t *testing.T) {
|
||||||
t.Errorf("AttachFile() failed. Attachment list is empty")
|
tests := []struct {
|
||||||
}
|
name string
|
||||||
a := al[0]
|
encoding Encoding
|
||||||
if a.Enc != tt.want {
|
want Encoding
|
||||||
t.Errorf("WithFileEncoding() failed. Expected: %s, got: %s", tt.enc, a.Enc)
|
}{
|
||||||
}
|
{"File encoding: US-ASCII", EncodingUSASCII, EncodingUSASCII},
|
||||||
})
|
{"File encoding: 8bit raw", NoEncoding, NoEncoding},
|
||||||
}
|
{"File encoding: Base64", EncodingB64, EncodingB64},
|
||||||
}
|
{"File encoding: quoted-printable (not allowed)", EncodingQP, ""},
|
||||||
|
}
|
||||||
// TestFile_WithFileContentType tests the WithFileContentType option
|
for _, tt := range tests {
|
||||||
func TestFile_WithFileContentType(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
tests := []struct {
|
message := NewMsg()
|
||||||
name string
|
message.AttachFile("file.go", WithFileEncoding(tt.encoding))
|
||||||
ct ContentType
|
attachments := message.GetAttachments()
|
||||||
want string
|
if len(attachments) <= 0 {
|
||||||
}{
|
t.Fatalf("failed to retrieve attachments list")
|
||||||
{"File content-type: text/plain", TypeTextPlain, "text/plain"},
|
}
|
||||||
{"File content-type: html/html", TypeTextHTML, "text/html"},
|
firstAttachment := attachments[0]
|
||||||
{"File content-type: application/octet-stream", TypeAppOctetStream, "application/octet-stream"},
|
if firstAttachment == nil {
|
||||||
{"File content-type: application/pgp-encrypted", TypePGPEncrypted, "application/pgp-encrypted"},
|
t.Fatalf("failed to retrieve first attachment, got nil")
|
||||||
{"File content-type: application/pgp-signature", TypePGPSignature, "application/pgp-signature"},
|
}
|
||||||
}
|
if firstAttachment.Enc != tt.want {
|
||||||
for _, tt := range tests {
|
t.Errorf("WithFileEncoding() failed. Expected: %s, got: %s", tt.want, firstAttachment.Enc)
|
||||||
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.Run("WithFileName", func(t *testing.T) {
|
||||||
t.Errorf("AttachFile() failed. Attachment list is empty")
|
tests := []struct {
|
||||||
}
|
name string
|
||||||
a := al[0]
|
fileName string
|
||||||
if a.ContentType != ContentType(tt.want) {
|
}{
|
||||||
t.Errorf("WithFileContentType() failed. Expected: %s, got: %s", tt.want, a.ContentType)
|
{"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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
149
header_test.go
149
header_test.go
|
@ -8,69 +8,13 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestImportance_StringFuncs tests the different string method of the Importance object
|
var (
|
||||||
func TestImportance_StringFuncs(t *testing.T) {
|
genHeaderTests = []struct {
|
||||||
tests := []struct {
|
|
||||||
name string
|
name string
|
||||||
imp Importance
|
header Header
|
||||||
wantns string
|
|
||||||
xprio string
|
|
||||||
want string
|
want string
|
||||||
}{
|
}{
|
||||||
{"Importance: Non-Urgent", ImportanceNonUrgent, "0", "5", "non-urgent"},
|
{"Header: Content-Description", HeaderContentDescription, "Content-Description"},
|
||||||
{"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-Disposition", HeaderContentDisposition, "Content-Disposition"},
|
||||||
{"Header: Content-ID", HeaderContentID, "Content-ID"},
|
{"Header: Content-ID", HeaderContentID, "Content-ID"},
|
||||||
{"Header: Content-Language", HeaderContentLang, "Content-Language"},
|
{"Header: Content-Language", HeaderContentLang, "Content-Language"},
|
||||||
|
@ -78,6 +22,10 @@ func TestHeader_String(t *testing.T) {
|
||||||
{"Header: Content-Transfer-Encoding", HeaderContentTransferEnc, "Content-Transfer-Encoding"},
|
{"Header: Content-Transfer-Encoding", HeaderContentTransferEnc, "Content-Transfer-Encoding"},
|
||||||
{"Header: Content-Type", HeaderContentType, "Content-Type"},
|
{"Header: Content-Type", HeaderContentType, "Content-Type"},
|
||||||
{"Header: Date", HeaderDate, "Date"},
|
{"Header: Date", HeaderDate, "Date"},
|
||||||
|
{
|
||||||
|
"Header: Disposition-Notification-To", HeaderDispositionNotificationTo,
|
||||||
|
"Disposition-Notification-To",
|
||||||
|
},
|
||||||
{"Header: Importance", HeaderImportance, "Importance"},
|
{"Header: Importance", HeaderImportance, "Importance"},
|
||||||
{"Header: In-Reply-To", HeaderInReplyTo, "In-Reply-To"},
|
{"Header: In-Reply-To", HeaderInReplyTo, "In-Reply-To"},
|
||||||
{"Header: List-Unsubscribe", HeaderListUnsubscribe, "List-Unsubscribe"},
|
{"Header: List-Unsubscribe", HeaderListUnsubscribe, "List-Unsubscribe"},
|
||||||
|
@ -87,19 +35,90 @@ func TestHeader_String(t *testing.T) {
|
||||||
{"Header: Organization", HeaderOrganization, "Organization"},
|
{"Header: Organization", HeaderOrganization, "Organization"},
|
||||||
{"Header: Precedence", HeaderPrecedence, "Precedence"},
|
{"Header: Precedence", HeaderPrecedence, "Precedence"},
|
||||||
{"Header: Priority", HeaderPriority, "Priority"},
|
{"Header: Priority", HeaderPriority, "Priority"},
|
||||||
{"Header: HeaderReferences", HeaderReferences, "References"},
|
{"Header: References", HeaderReferences, "References"},
|
||||||
{"Header: Reply-To", HeaderReplyTo, "Reply-To"},
|
{"Header: Reply-To", HeaderReplyTo, "Reply-To"},
|
||||||
{"Header: Subject", HeaderSubject, "Subject"},
|
{"Header: Subject", HeaderSubject, "Subject"},
|
||||||
{"Header: User-Agent", HeaderUserAgent, "User-Agent"},
|
{"Header: User-Agent", HeaderUserAgent, "User-Agent"},
|
||||||
|
{"Header: X-Auto-Response-Suppress", HeaderXAutoResponseSuppress, "X-Auto-Response-Suppress"},
|
||||||
{"Header: X-Mailer", HeaderXMailer, "X-Mailer"},
|
{"Header: X-Mailer", HeaderXMailer, "X-Mailer"},
|
||||||
{"Header: X-MSMail-Priority", HeaderXMSMailPriority, "X-MSMail-Priority"},
|
{"Header: X-MSMail-Priority", HeaderXMSMailPriority, "X-MSMail-Priority"},
|
||||||
{"Header: X-Priority", HeaderXPriority, "X-Priority"},
|
{"Header: X-Priority", HeaderXPriority, "X-Priority"},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
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 {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
if tt.h.String() != tt.want {
|
if tt.header.String() != tt.want {
|
||||||
t.Errorf("wrong string for Header returned. Expected: %s, got: %s",
|
t.Errorf("wrong string for AddrHeader returned. Expected: %s, got: %s",
|
||||||
tt.want, tt.h.String())
|
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 {
|
||||||
|
t.Errorf("wrong string for Header returned. Expected: %s, got: %s",
|
||||||
|
tt.want, tt.header.String())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
91
msg.go
91
msg.go
|
@ -583,6 +583,9 @@ func (m *Msg) SetAddrHeader(header AddrHeader, values ...string) error {
|
||||||
// References:
|
// References:
|
||||||
// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.4
|
// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.4
|
||||||
func (m *Msg) SetAddrHeaderIgnoreInvalid(header AddrHeader, values ...string) {
|
func (m *Msg) SetAddrHeaderIgnoreInvalid(header AddrHeader, values ...string) {
|
||||||
|
if m.addrHeader == nil {
|
||||||
|
m.addrHeader = make(map[AddrHeader][]*mail.Address)
|
||||||
|
}
|
||||||
var addresses []*mail.Address
|
var addresses []*mail.Address
|
||||||
for _, addrVal := range values {
|
for _, addrVal := range values {
|
||||||
address, err := mail.ParseAddress(m.encodeString(addrVal))
|
address, err := mail.ParseAddress(m.encodeString(addrVal))
|
||||||
|
@ -591,7 +594,14 @@ func (m *Msg) SetAddrHeaderIgnoreInvalid(header AddrHeader, values ...string) {
|
||||||
}
|
}
|
||||||
addresses = append(addresses, address)
|
addresses = append(addresses, address)
|
||||||
}
|
}
|
||||||
m.addrHeader[header] = addresses
|
switch header {
|
||||||
|
case HeaderFrom:
|
||||||
|
if len(addresses) > 0 {
|
||||||
|
m.addrHeader[header] = []*mail.Address{addresses[0]}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
m.addrHeader[header] = addresses
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// EnvelopeFrom sets the envelope from address for the Msg.
|
// EnvelopeFrom sets the envelope from address for the Msg.
|
||||||
|
@ -743,7 +753,16 @@ func (m *Msg) ToIgnoreInvalid(rcpts ...string) {
|
||||||
// References:
|
// References:
|
||||||
// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3
|
// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3
|
||||||
func (m *Msg) ToFromString(rcpts string) error {
|
func (m *Msg) ToFromString(rcpts string) error {
|
||||||
return m.To(strings.Split(rcpts, ",")...)
|
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...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cc sets one or more "CC" (carbon copy) addresses in the mail body for the Msg.
|
// Cc sets one or more "CC" (carbon copy) addresses in the mail body for the Msg.
|
||||||
|
@ -828,7 +847,16 @@ func (m *Msg) CcIgnoreInvalid(rcpts ...string) {
|
||||||
// References:
|
// References:
|
||||||
// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3
|
// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3
|
||||||
func (m *Msg) CcFromString(rcpts string) error {
|
func (m *Msg) CcFromString(rcpts string) error {
|
||||||
return m.Cc(strings.Split(rcpts, ",")...)
|
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...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bcc sets one or more "BCC" (blind carbon copy) addresses in the mail body for the Msg.
|
// Bcc sets one or more "BCC" (blind carbon copy) addresses in the mail body for the Msg.
|
||||||
|
@ -914,7 +942,16 @@ func (m *Msg) BccIgnoreInvalid(rcpts ...string) {
|
||||||
// References:
|
// References:
|
||||||
// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3
|
// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3
|
||||||
func (m *Msg) BccFromString(rcpts string) error {
|
func (m *Msg) BccFromString(rcpts string) error {
|
||||||
return m.Bcc(strings.Split(rcpts, ",")...)
|
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...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReplyTo sets the "Reply-To" address for the Msg, specifying where replies should be sent.
|
// ReplyTo sets the "Reply-To" address for the Msg, specifying where replies should be sent.
|
||||||
|
@ -1050,8 +1087,7 @@ func (m *Msg) SetBulk() {
|
||||||
// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.3
|
// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.3
|
||||||
// - https://datatracker.ietf.org/doc/html/rfc1123
|
// - https://datatracker.ietf.org/doc/html/rfc1123
|
||||||
func (m *Msg) SetDate() {
|
func (m *Msg) SetDate() {
|
||||||
now := time.Now().Format(time.RFC1123Z)
|
m.SetDateWithValue(time.Now())
|
||||||
m.SetGenHeader(HeaderDate, now)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetDateWithValue sets the "Date" header for the Msg using the provided time value in a valid RFC 1123 format.
|
// SetDateWithValue sets the "Date" header for the Msg using the provided time value in a valid RFC 1123 format.
|
||||||
|
@ -1151,6 +1187,9 @@ func (m *Msg) IsDelivered() bool {
|
||||||
// References:
|
// References:
|
||||||
// - https://datatracker.ietf.org/doc/html/rfc8098
|
// - https://datatracker.ietf.org/doc/html/rfc8098
|
||||||
func (m *Msg) RequestMDNTo(rcpts ...string) error {
|
func (m *Msg) RequestMDNTo(rcpts ...string) error {
|
||||||
|
if m.genHeader == nil {
|
||||||
|
m.genHeader = make(map[Header][]string)
|
||||||
|
}
|
||||||
var addresses []string
|
var addresses []string
|
||||||
for _, addrVal := range rcpts {
|
for _, addrVal := range rcpts {
|
||||||
address, err := mail.ParseAddress(addrVal)
|
address, err := mail.ParseAddress(addrVal)
|
||||||
|
@ -1159,9 +1198,7 @@ func (m *Msg) RequestMDNTo(rcpts ...string) error {
|
||||||
}
|
}
|
||||||
addresses = append(addresses, address.String())
|
addresses = append(addresses, address.String())
|
||||||
}
|
}
|
||||||
if _, ok := m.genHeader[HeaderDispositionNotificationTo]; ok {
|
m.genHeader[HeaderDispositionNotificationTo] = addresses
|
||||||
m.genHeader[HeaderDispositionNotificationTo] = addresses
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1200,11 +1237,11 @@ func (m *Msg) RequestMDNAddTo(rcpt string) error {
|
||||||
return fmt.Errorf(errParseMailAddr, rcpt, err)
|
return fmt.Errorf(errParseMailAddr, rcpt, err)
|
||||||
}
|
}
|
||||||
var addresses []string
|
var addresses []string
|
||||||
addresses = append(addresses, m.genHeader[HeaderDispositionNotificationTo]...)
|
if current, ok := m.genHeader[HeaderDispositionNotificationTo]; ok {
|
||||||
addresses = append(addresses, address.String())
|
addresses = current
|
||||||
if _, ok := m.genHeader[HeaderDispositionNotificationTo]; ok {
|
|
||||||
m.genHeader[HeaderDispositionNotificationTo] = addresses
|
|
||||||
}
|
}
|
||||||
|
addresses = append(addresses, address.String())
|
||||||
|
m.genHeader[HeaderDispositionNotificationTo] = addresses
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1644,11 +1681,11 @@ func (m *Msg) SetBodyHTMLTemplate(tpl *ht.Template, data interface{}, opts ...Pa
|
||||||
if tpl == nil {
|
if tpl == nil {
|
||||||
return errors.New(errTplPointerNil)
|
return errors.New(errTplPointerNil)
|
||||||
}
|
}
|
||||||
buffer := bytes.Buffer{}
|
buffer := bytes.NewBuffer(nil)
|
||||||
if err := tpl.Execute(&buffer, data); err != nil {
|
if err := tpl.Execute(buffer, data); err != nil {
|
||||||
return fmt.Errorf(errTplExecuteFailed, err)
|
return fmt.Errorf(errTplExecuteFailed, err)
|
||||||
}
|
}
|
||||||
writeFunc := writeFuncFromBuffer(&buffer)
|
writeFunc := writeFuncFromBuffer(buffer)
|
||||||
m.SetBodyWriter(TypeTextHTML, writeFunc, opts...)
|
m.SetBodyWriter(TypeTextHTML, writeFunc, opts...)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -1675,11 +1712,11 @@ func (m *Msg) SetBodyTextTemplate(tpl *tt.Template, data interface{}, opts ...Pa
|
||||||
if tpl == nil {
|
if tpl == nil {
|
||||||
return errors.New(errTplPointerNil)
|
return errors.New(errTplPointerNil)
|
||||||
}
|
}
|
||||||
buf := bytes.Buffer{}
|
buffer := bytes.NewBuffer(nil)
|
||||||
if err := tpl.Execute(&buf, data); err != nil {
|
if err := tpl.Execute(buffer, data); err != nil {
|
||||||
return fmt.Errorf(errTplExecuteFailed, err)
|
return fmt.Errorf(errTplExecuteFailed, err)
|
||||||
}
|
}
|
||||||
writeFunc := writeFuncFromBuffer(&buf)
|
writeFunc := writeFuncFromBuffer(buffer)
|
||||||
m.SetBodyWriter(TypeTextPlain, writeFunc, opts...)
|
m.SetBodyWriter(TypeTextPlain, writeFunc, opts...)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -1749,11 +1786,11 @@ func (m *Msg) AddAlternativeHTMLTemplate(tpl *ht.Template, data interface{}, opt
|
||||||
if tpl == nil {
|
if tpl == nil {
|
||||||
return errors.New(errTplPointerNil)
|
return errors.New(errTplPointerNil)
|
||||||
}
|
}
|
||||||
buffer := bytes.Buffer{}
|
buffer := bytes.NewBuffer(nil)
|
||||||
if err := tpl.Execute(&buffer, data); err != nil {
|
if err := tpl.Execute(buffer, data); err != nil {
|
||||||
return fmt.Errorf(errTplExecuteFailed, err)
|
return fmt.Errorf(errTplExecuteFailed, err)
|
||||||
}
|
}
|
||||||
writeFunc := writeFuncFromBuffer(&buffer)
|
writeFunc := writeFuncFromBuffer(buffer)
|
||||||
m.AddAlternativeWriter(TypeTextHTML, writeFunc, opts...)
|
m.AddAlternativeWriter(TypeTextHTML, writeFunc, opts...)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -1779,11 +1816,11 @@ func (m *Msg) AddAlternativeTextTemplate(tpl *tt.Template, data interface{}, opt
|
||||||
if tpl == nil {
|
if tpl == nil {
|
||||||
return errors.New(errTplPointerNil)
|
return errors.New(errTplPointerNil)
|
||||||
}
|
}
|
||||||
buffer := bytes.Buffer{}
|
buffer := bytes.NewBuffer(nil)
|
||||||
if err := tpl.Execute(&buffer, data); err != nil {
|
if err := tpl.Execute(buffer, data); err != nil {
|
||||||
return fmt.Errorf(errTplExecuteFailed, err)
|
return fmt.Errorf(errTplExecuteFailed, err)
|
||||||
}
|
}
|
||||||
writeFunc := writeFuncFromBuffer(&buffer)
|
writeFunc := writeFuncFromBuffer(buffer)
|
||||||
m.AddAlternativeWriter(TypeTextPlain, writeFunc, opts...)
|
m.AddAlternativeWriter(TypeTextPlain, writeFunc, opts...)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -2334,8 +2371,8 @@ func (m *Msg) WriteToSendmailWithContext(ctx context.Context, sendmailPath strin
|
||||||
// - https://datatracker.ietf.org/doc/html/rfc5322
|
// - https://datatracker.ietf.org/doc/html/rfc5322
|
||||||
func (m *Msg) NewReader() *Reader {
|
func (m *Msg) NewReader() *Reader {
|
||||||
reader := &Reader{}
|
reader := &Reader{}
|
||||||
buffer := bytes.Buffer{}
|
buffer := bytes.NewBuffer(nil)
|
||||||
_, err := m.Write(&buffer)
|
_, err := m.Write(buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
reader.err = fmt.Errorf("failed to write Msg to Reader buffer: %w", err)
|
reader.err = fmt.Errorf("failed to write Msg to Reader buffer: %w", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,85 +0,0 @@
|
||||||
// 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")
|
|
||||||
}
|
|
||||||
}
|
|
9590
msg_test.go
9590
msg_test.go
File diff suppressed because it is too large
Load diff
146
msg_unix_test.go
Normal file
146
msg_unix_test.go
Normal file
|
@ -0,0 +1,146 @@
|
||||||
|
// 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,151 +6,673 @@ package mail
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"mime"
|
"mime"
|
||||||
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"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) {
|
func TestMsgWriter_Write(t *testing.T) {
|
||||||
bw := &brokenWriter{}
|
t.Run("msgWriter writes to memory for all charsets", func(t *testing.T) {
|
||||||
mw := &msgWriter{writer: bw, charset: CharsetUTF8, encoder: mime.QEncoding}
|
for _, tt := range charsetTests {
|
||||||
_, err := mw.Write([]byte("test"))
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
if err == nil {
|
buffer := bytes.NewBuffer(nil)
|
||||||
t.Errorf("msgWriter WriteTo() with brokenWriter should fail, but didn't")
|
msgwriter := &msgWriter{
|
||||||
}
|
writer: buffer,
|
||||||
|
charset: tt.value,
|
||||||
// Also test the part when a previous error happened
|
encoder: mime.QEncoding,
|
||||||
mw.err = fmt.Errorf("broken")
|
}
|
||||||
_, err = mw.Write([]byte("test"))
|
_, err := msgwriter.Write([]byte("test"))
|
||||||
if err == nil {
|
if err != nil {
|
||||||
t.Errorf("msgWriter WriteTo() with brokenWriter should fail, but didn't")
|
t.Errorf("msgWriter failed to write: %s", err)
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
|
||||||
// TestMsgWriter_writeMsg tests the writeMsg method of the msgWriter
|
|
||||||
func TestMsgWriter_writeMsg(t *testing.T) {
|
|
||||||
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 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])
|
|
||||||
}
|
}
|
||||||
em += fmt.Sprintf("\n\nFull message:\n%s", ms)
|
})
|
||||||
t.Error(em)
|
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")
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestMsgWriter_writeMsg_PGP tests the writeMsg method of the msgWriter with PGP types set
|
func TestMsgWriter_writeMsg(t *testing.T) {
|
||||||
func TestMsgWriter_writeMsg_PGP(t *testing.T) {
|
msgwriter := &msgWriter{
|
||||||
m := NewMsg(WithPGPType(PGPEncrypt))
|
charset: CharsetUTF8,
|
||||||
_ = m.From(`"Toni Tester" <test@example.com>`)
|
encoder: getEncoder(EncodingQP),
|
||||||
_ = 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("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(WithPGPType(PGPSignature))
|
var incorrectFields []string
|
||||||
_ = m.From(`"Toni Tester" <test@example.com>`)
|
if !strings.Contains(buffer.String(), "MIME-Version: 1.0\r\n") {
|
||||||
_ = m.To(`"Toni Receiver" <receiver@example.com>`)
|
incorrectFields = append(incorrectFields, "MIME-Version")
|
||||||
m.Subject("This is a subject")
|
}
|
||||||
m.SetBodyString(TypeTextPlain, "This is the body")
|
if !strings.Contains(buffer.String(), fmt.Sprintf("Date: %s\r\n", now.Format(time.RFC1123Z))) {
|
||||||
buf = bytes.Buffer{}
|
incorrectFields = append(incorrectFields, "Date")
|
||||||
mw = &msgWriter{writer: &buf, charset: CharsetUTF8, encoder: mime.QEncoding}
|
}
|
||||||
mw.writeMsg(m)
|
if !strings.Contains(buffer.String(), "Message-ID: <message@id.com>\r\n") {
|
||||||
ms = buf.String()
|
incorrectFields = append(incorrectFields, "Message-ID")
|
||||||
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")
|
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())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
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)
|
||||||
|
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)
|
||||||
|
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)
|
||||||
|
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)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -554,9 +554,9 @@ func (c *Client) Noop() error {
|
||||||
|
|
||||||
// Quit sends the QUIT command and closes the connection to the server.
|
// Quit sends the QUIT command and closes the connection to the server.
|
||||||
func (c *Client) Quit() error {
|
func (c *Client) Quit() error {
|
||||||
if err := c.hello(); err != nil {
|
// See https://github.com/golang/go/issues/70011
|
||||||
return err
|
_ = c.hello() // ignore error; we're quitting anyhow
|
||||||
}
|
|
||||||
_, _, err := c.cmd(221, "QUIT")
|
_, _, err := c.cmd(221, "QUIT")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -900,6 +900,35 @@ Goodbye.
|
||||||
QUIT
|
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) {
|
func TestExtensions(t *testing.T) {
|
||||||
fake := func(server string) (c *Client, bcmdbuf *bufio.Writer, cmdbuf *strings.Builder) {
|
fake := func(server string) (c *Client, bcmdbuf *bufio.Writer, cmdbuf *strings.Builder) {
|
||||||
server = strings.Join(strings.Split(server, "\n"), "\r\n")
|
server = strings.Join(strings.Split(server, "\n"), "\r\n")
|
||||||
|
|
8
testdata/RFC5322-A1-1-invalid-from.eml
vendored
Normal file
8
testdata/RFC5322-A1-1-invalid-from.eml
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
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".
|
3
testdata/RFC5322-A1-1-invalid-from.eml.license
vendored
Normal file
3
testdata/RFC5322-A1-1-invalid-from.eml.license
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright (c) 2022-2024 The go-mail Authors
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
8
testdata/RFC5322-A1-1.eml
vendored
Normal file
8
testdata/RFC5322-A1-1.eml
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
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
Normal file
3
testdata/RFC5322-A1-1.eml.license
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright (c) 2022-2024 The go-mail Authors
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
1
testdata/attachment
vendored
Normal file
1
testdata/attachment
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
This is a test attachment
|
3
testdata/attachment.license
vendored
Normal file
3
testdata/attachment.license
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright (c) 2022-2024 The go-mail Authors
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
1
testdata/attachment.txt
vendored
Normal file
1
testdata/attachment.txt
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
This is a test attachment
|
3
testdata/attachment.txt.license
vendored
Normal file
3
testdata/attachment.txt.license
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright (c) 2022-2024 The go-mail Authors
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
1
testdata/embed.txt
vendored
Normal file
1
testdata/embed.txt
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
This is a test embed
|
3
testdata/embed.txt.license
vendored
Normal file
3
testdata/embed.txt.license
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright (c) 2022-2024 The go-mail Authors
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
1
testdata/logo.svg
vendored
Normal file
1
testdata/logo.svg
vendored
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 20 KiB |
367
testdata/logo.svg.base64
vendored
Normal file
367
testdata/logo.svg.base64
vendored
Normal file
|
@ -0,0 +1,367 @@
|
||||||
|
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
Normal file
3
testdata/logo.svg.base64.license
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
SPDX-FileCopyrightText: 2022 Maria Letta, the go-mail Team
|
||||||
|
|
||||||
|
SPDX-License-Identifier: CC-BY-ND-4.0
|
3
testdata/logo.svg.license
vendored
Normal file
3
testdata/logo.svg.license
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
SPDX-FileCopyrightText: 2022 Maria Letta, the go-mail Team
|
||||||
|
|
||||||
|
SPDX-License-Identifier: CC-BY-ND-4.0
|
0
testdata/tmp/.gitkeep
vendored
Normal file
0
testdata/tmp/.gitkeep
vendored
Normal file
Loading…
Reference in a new issue