mirror of
https://github.com/wneessen/go-mail.git
synced 2024-11-25 07:00:49 +01:00
Compare commits
No commits in common. "edc5300b54ab4c097381cedbe1ede46656321e36" and "c47f08dc7fa407ff3dd9f29fb79769c41435f98b" have entirely different histories.
edc5300b54
...
c47f08dc7f
42 changed files with 6971 additions and 12658 deletions
23
.cirrus.yml
Normal file
23
.cirrus.yml
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
# SPDX-FileCopyrightText: 2022 Winni Neessen <winni@neessen.dev>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
freebsd_task:
|
||||||
|
name: FreeBSD
|
||||||
|
|
||||||
|
matrix:
|
||||||
|
- name: FreeBSD 13.3
|
||||||
|
freebsd_instance:
|
||||||
|
image_family: freebsd-13-3
|
||||||
|
- name: FreeBSD 14.0
|
||||||
|
freebsd_instance:
|
||||||
|
image_family: freebsd-14-0
|
||||||
|
|
||||||
|
env:
|
||||||
|
TEST_SKIP_SENDMAIL: 1
|
||||||
|
|
||||||
|
pkginstall_script:
|
||||||
|
- pkg install -y go
|
||||||
|
|
||||||
|
test_script:
|
||||||
|
- go test -race -cover -shuffle=on ./...
|
221
.github/workflows/ci.yml
vendored
221
.github/workflows/ci.yml
vendored
|
@ -1,221 +0,0 @@
|
||||||
# SPDX-FileCopyrightText: 2024 The go-mail Authors
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
name: CI
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.ref_name }}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
codecov:
|
|
||||||
name: Test with Codecov coverage (${{ matrix.os }} / ${{ matrix.go }})
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
concurrency:
|
|
||||||
group: ci-codecov-${{ matrix.os }}-${{ matrix.go }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
os: [ubuntu-latest]
|
|
||||||
go: ['1.23']
|
|
||||||
env:
|
|
||||||
PERFORM_ONLINE_TEST: ${{ vars.PERFORM_ONLINE_TEST }}
|
|
||||||
PERFORM_UNIX_OPEN_WRITE_TESTS: ${{ vars.PERFORM_UNIX_OPEN_WRITE_TESTS }}
|
|
||||||
PERFORM_SENDMAIL_TESTS: ${{ vars.PERFORM_SENDMAIL_TESTS }}
|
|
||||||
TEST_HOST: ${{ secrets.TEST_HOST }}
|
|
||||||
TEST_USER: ${{ secrets.TEST_USER }}
|
|
||||||
TEST_PASS: ${{ secrets.TEST_PASS }}
|
|
||||||
steps:
|
|
||||||
- name: Harden Runner
|
|
||||||
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
|
||||||
with:
|
|
||||||
egress-policy: audit
|
|
||||||
- name: Checkout Code
|
|
||||||
uses: actions/checkout@61b9e3751b92087fd0b06925ba6dd6314e06f089 # master
|
|
||||||
- name: Setup go
|
|
||||||
uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
|
|
||||||
with:
|
|
||||||
go-version: ${{ matrix.go }}
|
|
||||||
check-latest: true
|
|
||||||
- name: Install sendmail
|
|
||||||
run: |
|
|
||||||
sudo apt-get -y update >/dev/null && sudo apt-get -y upgrade >/dev/null && sudo DEBIAN_FRONTEND=noninteractive apt-get -y install nullmailer >/dev/null && which sendmail
|
|
||||||
- name: Run go test
|
|
||||||
if: success()
|
|
||||||
run: |
|
|
||||||
go test -race -shuffle=on --coverprofile=coverage.coverprofile --covermode=atomic ./...
|
|
||||||
- name: Upload coverage to Codecov
|
|
||||||
if: success()
|
|
||||||
uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4.6.0
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos
|
|
||||||
lint:
|
|
||||||
name: golangci-lint (${{ matrix.go }})
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
concurrency:
|
|
||||||
group: ci-lint-${{ matrix.go }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
go: ['1.23']
|
|
||||||
steps:
|
|
||||||
- name: Harden Runner
|
|
||||||
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
|
||||||
with:
|
|
||||||
egress-policy: audit
|
|
||||||
- name: Setup go
|
|
||||||
uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
|
|
||||||
with:
|
|
||||||
go-version: ${{ matrix.go }}
|
|
||||||
check-latest: true
|
|
||||||
- name: Checkout Code
|
|
||||||
uses: actions/checkout@61b9e3751b92087fd0b06925ba6dd6314e06f089 # master
|
|
||||||
- name: golangci-lint
|
|
||||||
uses: golangci/golangci-lint-action@971e284b6050e8a5849b72094c50ab08da042db8 # v6.1.1
|
|
||||||
with:
|
|
||||||
version: latest
|
|
||||||
dependency-review:
|
|
||||||
name: Dependency review
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
concurrency:
|
|
||||||
group: ci-dependency-review
|
|
||||||
cancel-in-progress: true
|
|
||||||
steps:
|
|
||||||
- name: Harden Runner
|
|
||||||
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
|
||||||
with:
|
|
||||||
egress-policy: audit
|
|
||||||
- name: Checkout Code
|
|
||||||
uses: actions/checkout@61b9e3751b92087fd0b06925ba6dd6314e06f089 # master
|
|
||||||
- name: 'Dependency Review'
|
|
||||||
uses: actions/dependency-review-action@a6993e2c61fd5dc440b409aa1d6904921c5e1894 # v4.3.5
|
|
||||||
with:
|
|
||||||
base-ref: ${{ github.event.pull_request.base.sha || 'main' }}
|
|
||||||
head-ref: ${{ github.event.pull_request.head.sha || github.ref }}
|
|
||||||
govulncheck:
|
|
||||||
name: Go vulnerabilities check
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
concurrency:
|
|
||||||
group: ci-govulncheck
|
|
||||||
cancel-in-progress: true
|
|
||||||
steps:
|
|
||||||
- name: Harden Runner
|
|
||||||
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
|
||||||
with:
|
|
||||||
egress-policy: audit
|
|
||||||
- name: Run govulncheck
|
|
||||||
uses: golang/govulncheck-action@b625fbe08f3bccbe446d94fbf87fcc875a4f50ee # v1.0.4
|
|
||||||
test:
|
|
||||||
name: Test (${{ matrix.os }} / ${{ matrix.go }})
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
concurrency:
|
|
||||||
group: ci-test-${{ matrix.os }}-${{ matrix.go }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
||||||
go: ['1.19', '1.20', '1.21', '1.22', '1.23']
|
|
||||||
steps:
|
|
||||||
- name: Harden Runner
|
|
||||||
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
|
||||||
with:
|
|
||||||
egress-policy: audit
|
|
||||||
- name: Checkout Code
|
|
||||||
uses: actions/checkout@61b9e3751b92087fd0b06925ba6dd6314e06f089 # master
|
|
||||||
- name: Setup go
|
|
||||||
uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
|
|
||||||
with:
|
|
||||||
go-version: ${{ matrix.go }}
|
|
||||||
- name: Run go test
|
|
||||||
run: |
|
|
||||||
go test -race -shuffle=on ./...
|
|
||||||
test-fbsd:
|
|
||||||
name: Test on FreeBSD ${{ matrix.osver }}
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
concurrency:
|
|
||||||
group: ci-test-freebsd-${{ matrix.osver }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
osver: ['14.1', '14.0', 13.4']
|
|
||||||
steps:
|
|
||||||
- name: Checkout Code
|
|
||||||
uses: actions/checkout@61b9e3751b92087fd0b06925ba6dd6314e06f089 # master
|
|
||||||
- name: Run go test on FreeBSD
|
|
||||||
uses: vmactions/freebsd-vm@v1
|
|
||||||
with:
|
|
||||||
usesh: true
|
|
||||||
copyback: false
|
|
||||||
prepare: |
|
|
||||||
pkg install -y go
|
|
||||||
run: |
|
|
||||||
cd $GITHUB_WORKSPACE;
|
|
||||||
go test -race -shuffle=on ./...
|
|
||||||
reuse:
|
|
||||||
name: REUSE Compliance Check
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
concurrency:
|
|
||||||
group: ci-reuse
|
|
||||||
cancel-in-progress: true
|
|
||||||
steps:
|
|
||||||
- name: Harden Runner
|
|
||||||
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
|
||||||
with:
|
|
||||||
egress-policy: audit
|
|
||||||
- name: Checkout Code
|
|
||||||
uses: actions/checkout@61b9e3751b92087fd0b06925ba6dd6314e06f089 # master
|
|
||||||
- name: REUSE Compliance Check
|
|
||||||
uses: fsfe/reuse-action@3ae3c6bdf1257ab19397fab11fd3312144692083 # v4.0.0
|
|
||||||
sonarqube:
|
|
||||||
name: Test with SonarQube review (${{ matrix.os }} / ${{ matrix.go }})
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
concurrency:
|
|
||||||
group: ci-codecov-${{ matrix.go }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
os: [ubuntu-latest]
|
|
||||||
go: ['1.23']
|
|
||||||
env:
|
|
||||||
PERFORM_ONLINE_TEST: ${{ vars.PERFORM_ONLINE_TEST }}
|
|
||||||
TEST_HOST: ${{ secrets.TEST_HOST }}
|
|
||||||
TEST_USER: ${{ secrets.TEST_USER }}
|
|
||||||
TEST_PASS: ${{ secrets.TEST_PASS }}
|
|
||||||
steps:
|
|
||||||
- name: Harden Runner
|
|
||||||
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
|
||||||
with:
|
|
||||||
egress-policy: audit
|
|
||||||
- name: Checkout Code
|
|
||||||
uses: actions/checkout@61b9e3751b92087fd0b06925ba6dd6314e06f089 # master
|
|
||||||
- name: Setup go
|
|
||||||
uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
|
|
||||||
with:
|
|
||||||
go-version: ${{ matrix.go }}
|
|
||||||
check-latest: true
|
|
||||||
- name: Run go test
|
|
||||||
run: |
|
|
||||||
go test -shuffle=on -race --coverprofile=./cov.out ./...
|
|
||||||
- name: SonarQube scan
|
|
||||||
uses: sonarsource/sonarqube-scan-action@884b79409bbd464b2a59edc326a4b77dc56b2195 # master
|
|
||||||
if: success()
|
|
||||||
env:
|
|
||||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
|
||||||
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
|
|
||||||
- name: SonarQube quality gate
|
|
||||||
uses: sonarsource/sonarqube-quality-gate-action@dc2f7b0dd95544cd550de3028f89193576e958b9 # master
|
|
||||||
timeout-minutes: 5
|
|
||||||
env:
|
|
||||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
|
||||||
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
|
|
67
.github/workflows/codecov.yml
vendored
Normal file
67
.github/workflows/codecov.yml
vendored
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
# SPDX-FileCopyrightText: 2022 Winni Neessen <winni@neessen.dev>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: CC0-1.0
|
||||||
|
|
||||||
|
name: Codecov workflow
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- '**.go'
|
||||||
|
- 'go.*'
|
||||||
|
- '.github/workflows/codecov.yml'
|
||||||
|
- 'codecov.yml'
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- '**.go'
|
||||||
|
- 'go.*'
|
||||||
|
- '.github/workflows/codecov.yml'
|
||||||
|
- 'codecov.yml'
|
||||||
|
env:
|
||||||
|
TEST_HOST: ${{ secrets.TEST_HOST }}
|
||||||
|
TEST_FROM: ${{ secrets.TEST_USER }}
|
||||||
|
TEST_ALLOW_SEND: "1"
|
||||||
|
TEST_SMTPAUTH_USER: ${{ secrets.TEST_USER }}
|
||||||
|
TEST_SMTPAUTH_PASS: ${{ secrets.TEST_PASS }}
|
||||||
|
TEST_SMTPAUTH_TYPE: "LOGIN"
|
||||||
|
TEST_ONLINE_SCRAM: "1"
|
||||||
|
TEST_HOST_SCRAM: ${{ secrets.TEST_HOST_SCRAM }}
|
||||||
|
TEST_USER_SCRAM: ${{ secrets.TEST_USER_SCRAM }}
|
||||||
|
TEST_PASS_SCRAM: ${{ secrets.TEST_PASS_SCRAM }}
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
run:
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||||
|
go: ['1.23']
|
||||||
|
steps:
|
||||||
|
- name: Harden Runner
|
||||||
|
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
|
- name: Checkout Code
|
||||||
|
uses: actions/checkout@61b9e3751b92087fd0b06925ba6dd6314e06f089 # master
|
||||||
|
- name: Setup go
|
||||||
|
uses: actions/setup-go@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
Normal file
31
.github/workflows/dependency-review.yml
vendored
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
# SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: CC0-1.0
|
||||||
|
|
||||||
|
# Dependency Review Action
|
||||||
|
#
|
||||||
|
# This Action will scan dependency manifest files that change as part of a Pull Request,
|
||||||
|
# surfacing known-vulnerable versions of the packages declared or updated in the PR.
|
||||||
|
# Once installed, if the workflow run is marked as required,
|
||||||
|
# PRs introducing known-vulnerable packages will be blocked from merging.
|
||||||
|
#
|
||||||
|
# Source repository: https://github.com/actions/dependency-review-action
|
||||||
|
name: 'Dependency Review'
|
||||||
|
on: [pull_request]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
dependency-review:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Harden Runner
|
||||||
|
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
|
- name: 'Checkout Repository'
|
||||||
|
uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||||
|
- name: 'Dependency Review'
|
||||||
|
uses: actions/dependency-review-action@a6993e2c61fd5dc440b409aa1d6904921c5e1894 # v4.3.5
|
54
.github/workflows/golangci-lint.yml
vendored
Normal file
54
.github/workflows/golangci-lint.yml
vendored
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
# SPDX-FileCopyrightText: 2022 Winni Neessen <winni@neessen.dev>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: CC0-1.0
|
||||||
|
|
||||||
|
name: golangci-lint
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- v*
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
# Optional: allow read access to pull request. Use with `only-new-issues` option.
|
||||||
|
# pull-requests: read
|
||||||
|
jobs:
|
||||||
|
golangci:
|
||||||
|
name: lint
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Harden Runner
|
||||||
|
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
|
- uses: actions/setup-go@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
Normal file
21
.github/workflows/govulncheck.yml
vendored
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
# SPDX-FileCopyrightText: 2022 Winni Neessen <winni@neessen.dev>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: CC0-1.0
|
||||||
|
|
||||||
|
name: Govulncheck Security Scan
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Harden Runner
|
||||||
|
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
- name: Run govulncheck
|
||||||
|
uses: golang/govulncheck-action@b625fbe08f3bccbe446d94fbf87fcc875a4f50ee # v1.0.4
|
45
.github/workflows/offline-tests.yml
vendored
Normal file
45
.github/workflows/offline-tests.yml
vendored
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
# SPDX-FileCopyrightText: 2022 Winni Neessen <winni@neessen.dev>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: CC0-1.0
|
||||||
|
|
||||||
|
name: Offline tests workflow
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- '**.go'
|
||||||
|
- 'go.*'
|
||||||
|
- '.github/workflows/offline-tests.yml'
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- '**.go'
|
||||||
|
- 'go.*'
|
||||||
|
- '.github/workflows/offline-tests.yml'
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
run:
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||||
|
go: ['1.19', '1.20', '1.21', '1.22', '1.23']
|
||||||
|
steps:
|
||||||
|
- name: Harden Runner
|
||||||
|
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
|
- name: Checkout Code
|
||||||
|
uses: actions/checkout@61b9e3751b92087fd0b06925ba6dd6314e06f089 # master
|
||||||
|
- name: Setup go
|
||||||
|
uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.go }}
|
||||||
|
- name: Run Tests
|
||||||
|
run: |
|
||||||
|
go test -race -shuffle=on ./...
|
23
.github/workflows/reuse.yml
vendored
Normal file
23
.github/workflows/reuse.yml
vendored
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
# SPDX-FileCopyrightText: 2022 Winni Neessen <winni@neessen.dev>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: CC0-1.0
|
||||||
|
|
||||||
|
name: REUSE Compliance Check
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Harden Runner
|
||||||
|
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
|
- uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2.7.0
|
||||||
|
- name: REUSE Compliance Check
|
||||||
|
uses: fsfe/reuse-action@3ae3c6bdf1257ab19397fab11fd3312144692083 # v4.0.0
|
56
.github/workflows/sonarqube.yml
vendored
Normal file
56
.github/workflows/sonarqube.yml
vendored
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
# SPDX-FileCopyrightText: 2022 Winni Neessen <winni@neessen.dev>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: CC0-1.0
|
||||||
|
|
||||||
|
name: SonarQube
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- '**.go'
|
||||||
|
- 'go.*'
|
||||||
|
- '.github/workflows/sonarqube.yml'
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- '**.go'
|
||||||
|
- 'go.*'
|
||||||
|
- '.github/workflows/sonarqube.yml'
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Harden Runner
|
||||||
|
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
|
- uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2.7.0
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@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,6 +56,4 @@ crashlytics.properties
|
||||||
crashlytics-build.properties
|
crashlytics-build.properties
|
||||||
fabric.properties
|
fabric.properties
|
||||||
|
|
||||||
## Coverage data
|
testdata
|
||||||
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 returned when no io.Writer is set for Base64LineBreaker.
|
// ErrNoOutWriter is the error message returned when no io.Writer is set for Base64LineBreaker.
|
||||||
var ErrNoOutWriter = errors.New("no io.Writer set for Base64LineBreaker")
|
const ErrNoOutWriter = "no io.Writer set for Base64LineBreaker"
|
||||||
|
|
||||||
// Base64LineBreaker handles base64 encoding with the insertion of new lines after a certain number
|
// 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 = ErrNoOutWriter
|
err = errors.New(ErrNoOutWriter)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if l.used+len(data) < MaxBodyLength {
|
if l.used+len(data) < MaxBodyLength {
|
||||||
|
|
|
@ -5,165 +5,487 @@
|
||||||
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 (
|
||||||
errClosedWriter = errors.New("writer is already closed")
|
|
||||||
errMockDefault = errors.New("mock write error")
|
errMockDefault = errors.New("mock write error")
|
||||||
errMockNewline = errors.New("mock newline 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) {
|
||||||
t.Run("write, copy and close", func(t *testing.T) {
|
l, err := os.Open("assets/gopher2.svg")
|
||||||
logoWriter := bytes.NewBuffer(nil)
|
if err != nil {
|
||||||
lineBreaker := &Base64LineBreaker{out: logoWriter}
|
t.Errorf("failed to open gopher logo asset: %s", err)
|
||||||
t.Cleanup(func() {
|
return
|
||||||
if err := lineBreaker.Close(); err != nil {
|
}
|
||||||
|
defer func() { _ = l.Close() }()
|
||||||
|
|
||||||
|
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)
|
t.Errorf("failed to close line breaker: %s", err)
|
||||||
}
|
}
|
||||||
})
|
ob := removeNewLines([]byte(logoB64))
|
||||||
if _, err := lineBreaker.Write([]byte("testdata")); err != nil {
|
nb := removeNewLines(wbuf.Bytes())
|
||||||
t.Errorf("failed to write to line breaker: %s", err)
|
if string(ob) != string(nb) {
|
||||||
|
t.Errorf("generated line breaker output differs from original data")
|
||||||
}
|
}
|
||||||
})
|
|
||||||
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")
|
// TestBase64LineBreakerFailures tests the cases in which the Base64LineBreaker would fail
|
||||||
if err != nil {
|
func TestBase64LineBreakerFailures(t *testing.T) {
|
||||||
t.Fatalf("failed to stat test data file: %s", err)
|
stt := []byte("short")
|
||||||
}
|
ltt := []byte(logoB64)
|
||||||
if logoStat.Size() != copiedBytes {
|
|
||||||
t.Errorf("copied %d bytes, but expected %d bytes", copiedBytes, logoStat.Size())
|
|
||||||
}
|
|
||||||
|
|
||||||
expectedRaw, err := os.ReadFile("testdata/logo.svg.base64")
|
// No output writer defined
|
||||||
if err != nil {
|
lb := Base64LineBreaker{}
|
||||||
t.Errorf("failed to read expected base64 data from file: %s", err)
|
if _, err := lb.Write(stt); err == nil {
|
||||||
}
|
|
||||||
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")
|
t.Errorf("writing to Base64LineBreaker with no output io.Writer was supposed to failed, but didn't")
|
||||||
}
|
}
|
||||||
if !errors.Is(err, ErrNoOutWriter) {
|
if err := lb.Close(); err != nil {
|
||||||
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.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{}
|
// Closed output writer
|
||||||
lineBreaker := &Base64LineBreaker{out: writeBuffer}
|
wbuf := errorWriter{}
|
||||||
_, err = io.Copy(lineBreaker, logo)
|
fb := Base64LineBreaker{out: wbuf}
|
||||||
if err == nil {
|
if _, err := fb.Write(ltt); err == nil {
|
||||||
t.Errorf("writing to Base64LineBreaker with an already closed output io.Writer was " +
|
t.Errorf("writing to Base64LineBreaker with errorWriter was supposed to failed, but didn't")
|
||||||
"supposed to failed, but didn't")
|
|
||||||
}
|
}
|
||||||
if !errors.Is(err, errClosedWriter) {
|
if err := fb.Close(); err != nil {
|
||||||
t.Errorf("unexpected error while writing to Base64LineBreaker: %s", err)
|
t.Errorf("failed to close Base64LineBreaker: %s", err)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
t.Run("fail on different scenarios with mock writer", func(t *testing.T) {
|
|
||||||
|
func TestBase64LineBreaker_WriteAndClose(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
data []byte
|
data []byte
|
||||||
writer io.Writer
|
writer io.Writer
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "write data within MaxBodyLength",
|
name: "Write data within MaxBodyLength",
|
||||||
data: []byte("testdata"),
|
data: []byte("testdata"),
|
||||||
writer: &mockWriterExcess{writeError: errMockDefault},
|
writer: &mockWriterExcess{writeError: errMockDefault},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "write data exceeds MaxBodyLength",
|
name: "Write data exceeds MaxBodyLength",
|
||||||
data: []byte("verylongtestdataverylongtestdataverylongtestdata" +
|
data: []byte("verylongtestdataverylongtestdataverylongtestdata" +
|
||||||
"verylongtestdataverylongtestdataverylongtestdata"),
|
"verylongtestdataverylongtestdataverylongtestdata"),
|
||||||
writer: &mockWriterExcess{writeError: errMockDefault},
|
writer: &mockWriterExcess{writeError: errMockDefault},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "write data exceeds MaxBodyLength with newline",
|
name: "Write data exceeds MaxBodyLength with newline",
|
||||||
data: []byte("verylongtestdataverylongtestdataverylongtestdata" +
|
data: []byte("verylongtestdataverylongtestdataverylongtestdata" +
|
||||||
"verylongtestdataverylongtestdataverylongtestdata"),
|
"verylongtestdataverylongtestdataverylongtestdata"),
|
||||||
writer: &mockWriterNewline{writeError: errMockDefault},
|
writer: &mockWriterNewline{writeError: errMockDefault},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
lineBreaker := &Base64LineBreaker{out: tt.writer}
|
blr := &Base64LineBreaker{out: tt.writer}
|
||||||
|
|
||||||
_, err := lineBreaker.Write(tt.data)
|
_, err := blr.Write(tt.data)
|
||||||
if err != nil && !errors.Is(err, errMockDefault) && !errors.Is(err, errMockNewline) {
|
if err != nil && !errors.Is(err, errMockDefault) && !errors.Is(err, errMockNewline) {
|
||||||
t.Errorf("unexpected error while writing to mock writer: %s", err)
|
t.Errorf("Unexpected error while writing: %v", err)
|
||||||
}
|
}
|
||||||
err = lineBreaker.Close()
|
err = blr.Close()
|
||||||
if err != nil && !errors.Is(err, errMockDefault) && !errors.Is(err, errMockNewline) {
|
if err != nil && !errors.Is(err, errMockDefault) && !errors.Is(err, errMockNewline) {
|
||||||
t.Errorf("unexpected error while closing mock writer: %s", err)
|
t.Errorf("Unexpected error while closing: %v", err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// removeNewLines is a test helper thatremoves all newline characters ('\r' and '\n') from the given byte slice.
|
// removeNewLines removes any newline characters from the given data
|
||||||
func removeNewLines(t *testing.T, data []byte) []byte {
|
func removeNewLines(data []byte) []byte {
|
||||||
t.Helper()
|
|
||||||
result := make([]byte, len(data))
|
result := make([]byte, len(data))
|
||||||
n := 0
|
n := 0
|
||||||
|
|
||||||
|
@ -181,11 +503,11 @@ func removeNewLines(t *testing.T, 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, errClosedWriter
|
return 0, fmt.Errorf("supposed to always fail")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e errorWriter) Close() error {
|
func (e errorWriter) Close() error {
|
||||||
return errClosedWriter
|
return fmt.Errorf("supposed to always fail")
|
||||||
}
|
}
|
||||||
|
|
||||||
type mockWriterExcess struct {
|
type mockWriterExcess struct {
|
||||||
|
@ -217,49 +539,19 @@ func (w *mockWriterNewline) Write(p []byte) (n int, err error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func FuzzBase64LineBreaker(f *testing.F) {
|
func FuzzBase64LineBreaker_Write(f *testing.F) {
|
||||||
seedData := [][]byte{
|
f.Add([]byte("abc"))
|
||||||
[]byte(""),
|
f.Add([]byte("def"))
|
||||||
[]byte("abc"),
|
f.Add([]uint8{0o0, 0o1, 0o2, 30, 255})
|
||||||
[]byte("def"),
|
buf := bytes.Buffer{}
|
||||||
[]byte("Hello, World!"),
|
bw := bufio.NewWriter(&buf)
|
||||||
[]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) {
|
||||||
buffer := bytes.NewBuffer(nil)
|
b := &Base64LineBreaker{out: bw}
|
||||||
lineBreaker := &Base64LineBreaker{
|
if _, err := b.Write(data); err != nil {
|
||||||
out: buffer,
|
t.Errorf("failed to write to B64LineBreaker: %s", err)
|
||||||
}
|
}
|
||||||
base64Encoder := base64.NewEncoder(base64.StdEncoding, lineBreaker)
|
if err := b.Close(); err != nil {
|
||||||
|
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,12 +242,6 @@ 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.
|
||||||
|
@ -516,9 +510,6 @@ 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
|
||||||
|
@ -680,9 +671,6 @@ 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
|
||||||
}
|
}
|
||||||
|
@ -751,7 +739,6 @@ 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
|
||||||
|
@ -1094,6 +1081,10 @@ 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 {
|
||||||
|
@ -1261,13 +1252,14 @@ 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{
|
||||||
|
@ -1275,6 +1267,12 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1291,9 +1289,6 @@ 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
|
||||||
}
|
}
|
||||||
|
@ -1352,6 +1347,9 @@ 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")
|
||||||
|
|
|
@ -1,126 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2024 The go-mail Authors
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
//go:build go1.21
|
|
||||||
// +build go1.21
|
|
||||||
|
|
||||||
package mail
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"reflect"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/wneessen/go-mail/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestNewClientNewVersionsOnly(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
option Option
|
|
||||||
expectFunc func(c *Client) error
|
|
||||||
shouldfail bool
|
|
||||||
expectErr *error
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
"WithLogger log.JSONlog", WithLogger(log.NewJSON(os.Stderr, log.LevelDebug)),
|
|
||||||
func(c *Client) error {
|
|
||||||
if c.logger == nil {
|
|
||||||
return errors.New("failed to set logger. Want logger bug got got nil")
|
|
||||||
}
|
|
||||||
loggerType := reflect.TypeOf(c.logger).String()
|
|
||||||
if loggerType != "*log.JSONlog" {
|
|
||||||
return fmt.Errorf("failed to set logger. Want logger type: %s, got: %s",
|
|
||||||
"*log.JSONlog", loggerType)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
false, nil,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
client, err := NewClient(DefaultHost, tt.option)
|
|
||||||
if !tt.shouldfail && err != nil {
|
|
||||||
t.Fatalf("failed to create new client: %s", err)
|
|
||||||
}
|
|
||||||
if tt.shouldfail && err == nil {
|
|
||||||
t.Errorf("client creation was supposed to fail, but it didn't")
|
|
||||||
}
|
|
||||||
if tt.shouldfail && tt.expectErr != nil {
|
|
||||||
if !errors.Is(err, *tt.expectErr) {
|
|
||||||
t.Errorf("error for NewClient mismatch. Expected: %s, got: %s",
|
|
||||||
*tt.expectErr, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if tt.expectFunc != nil {
|
|
||||||
if err = tt.expectFunc(client); err != nil {
|
|
||||||
t.Errorf("NewClient with custom option failed: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestClient_DialWithContextNewVersionsOnly(t *testing.T) {
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
defer cancel()
|
|
||||||
PortAdder.Add(1)
|
|
||||||
serverPort := int(TestServerPortBase + PortAdder.Load())
|
|
||||||
featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"
|
|
||||||
go func() {
|
|
||||||
if err := simpleSMTPServer(ctx, t, &serverProps{FeatureSet: featureSet, ListenPort: serverPort}); err != nil {
|
|
||||||
t.Errorf("failed to start test server: %s", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
time.Sleep(time.Millisecond * 30)
|
|
||||||
t.Run("connect with full debug logging and auth logging", func(t *testing.T) {
|
|
||||||
ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500)
|
|
||||||
t.Cleanup(cancelDial)
|
|
||||||
|
|
||||||
logBuffer := bytes.NewBuffer(nil)
|
|
||||||
client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS),
|
|
||||||
WithDebugLog(), WithLogAuthData(), WithLogger(log.NewJSON(logBuffer, log.LevelDebug)),
|
|
||||||
WithSMTPAuth(SMTPAuthPlain), WithUsername("test"), WithPassword("password"))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to create new client: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = client.DialWithContext(ctxDial); err != nil {
|
|
||||||
var netErr net.Error
|
|
||||||
if errors.As(err, &netErr) && netErr.Timeout() {
|
|
||||||
t.Skip("failed to connect to the test server due to timeout")
|
|
||||||
}
|
|
||||||
t.Fatalf("failed to connect to the test server: %s", err)
|
|
||||||
}
|
|
||||||
t.Cleanup(func() {
|
|
||||||
if err = client.Close(); err != nil {
|
|
||||||
t.Errorf("failed to close the client: %s", err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
logs := parseJSONLog(t, logBuffer)
|
|
||||||
if len(logs.Lines) == 0 {
|
|
||||||
t.Errorf("failed to enable debug logging, but no logs were found")
|
|
||||||
}
|
|
||||||
authFound := false
|
|
||||||
for _, logline := range logs.Lines {
|
|
||||||
if strings.EqualFold(logline.Message, "AUTH PLAIN AHRlc3QAcGFzc3dvcmQ=") &&
|
|
||||||
logline.Direction.From == "client" && logline.Direction.To == "server" {
|
|
||||||
authFound = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !authFound {
|
|
||||||
t.Errorf("logAuthData not working, no authentication info found in logs")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
5561
client_test.go
5561
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: 90%
|
target: 85%
|
||||||
threshold: 2%
|
threshold: 5%
|
||||||
base: auto
|
base: auto
|
||||||
if_ci_failed: error
|
if_ci_failed: error
|
||||||
only_pulls: false
|
only_pulls: false
|
||||||
patch:
|
patch:
|
||||||
default:
|
default:
|
||||||
target: 90%
|
target: 80%
|
||||||
base: auto
|
base: auto
|
||||||
if_ci_failed: error
|
if_ci_failed: error
|
||||||
threshold: 2%
|
threshold: 5%
|
||||||
|
|
||||||
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())
|
||||||
}
|
}
|
||||||
// We can skip the error checking here since netmail.ParseAddressList already performed the
|
if err = addrFunc(addrStrings...); err != nil {
|
||||||
// same address checking that the msg methods do.
|
return fmt.Errorf(`failed to parse %q header: %w`, addrHeader, err)
|
||||||
_ = addrFunc(addrStrings...)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -600,8 +600,6 @@ 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,8 +6,11 @@ package mail
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -19,23 +22,6 @@ 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
|
||||||
|
@ -56,52 +42,6 @@ This is a test mail. Please do not reply to this. Also this line is very long so
|
||||||
should be wrapped.
|
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
|
||||||
|
|
||||||
|
@ -364,128 +304,6 @@ 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
|
||||||
|
@ -760,39 +578,6 @@ 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
|
||||||
|
@ -827,352 +612,8 @@ 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
|
||||||
|
@ -1180,6 +621,26 @@ 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) {
|
||||||
|
@ -1548,6 +1009,3 @@ func stringToTempFile(data, name string) (string, string, error) {
|
||||||
}
|
}
|
||||||
return tempDir, filePath, nil
|
return tempDir, filePath, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
193
file_test.go
193
file_test.go
|
@ -6,183 +6,134 @@ package mail
|
||||||
|
|
||||||
import "testing"
|
import "testing"
|
||||||
|
|
||||||
func TestFile(t *testing.T) {
|
// TestFile_SetGetHeader tests the set-/getHeader method of the File object
|
||||||
t.Run("setHeader", func(t *testing.T) {
|
func TestFile_SetGetHeader(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")
|
||||||
contentType, ok := f.Header[HeaderContentType.String()]
|
fi, ok := f.getHeader(HeaderContentType)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Fatalf("setHeader failed. Expected header %s to be set", HeaderContentType)
|
t.Errorf("getHeader method of File did not return a value")
|
||||||
|
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))
|
|
||||||
}
|
}
|
||||||
if contentType[0] != "text/plain" {
|
fi, ok = f.getHeader(HeaderContentTransferEnc)
|
||||||
t.Fatalf("setHeader failed. Expected header %s to have value %s, got: %s",
|
if ok {
|
||||||
HeaderContentType.String(), "text/plain", contentType[0])
|
t.Errorf("getHeader method of File did return a value, but wasn't supposed to")
|
||||||
|
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),
|
|
||||||
}
|
}
|
||||||
f.setHeader(HeaderContentType, "text/plain")
|
|
||||||
contentType, ok := f.getHeader(HeaderContentType)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("setHeader failed. Expected header %s to be set", HeaderContentType)
|
|
||||||
}
|
}
|
||||||
if contentType != "text/plain" {
|
|
||||||
t.Fatalf("setHeader failed. Expected header %s to have value %s, got: %s",
|
// TestFile_WithFileDescription tests the WithFileDescription option
|
||||||
HeaderContentType.String(), "text/plain", contentType)
|
func TestFile_WithFileDescription(t *testing.T) {
|
||||||
}
|
|
||||||
})
|
|
||||||
t.Run("WithFileDescription", func(t *testing.T) {
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
desc string
|
desc string
|
||||||
}{
|
}{
|
||||||
{"File description: test", "test"},
|
{"File description: test", "test"},
|
||||||
{"File description: with newline", "test\n"},
|
|
||||||
{"File description: empty", ""},
|
{"File description: empty", ""},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
m := NewMsg()
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
message := NewMsg()
|
m.AttachFile("file.go", WithFileDescription(tt.desc))
|
||||||
message.AttachFile("file.go", WithFileDescription(tt.desc))
|
al := m.GetAttachments()
|
||||||
attachments := message.GetAttachments()
|
if len(al) <= 0 {
|
||||||
if len(attachments) <= 0 {
|
t.Errorf("AttachFile() failed. Attachment list is empty")
|
||||||
t.Fatalf("failed to retrieve attachments list")
|
|
||||||
}
|
}
|
||||||
firstAttachment := attachments[0]
|
a := al[0]
|
||||||
if firstAttachment == nil {
|
if a.Desc != tt.desc {
|
||||||
t.Fatalf("failed to retrieve first attachment, got nil")
|
t.Errorf("WithFileDescription() failed. Expected: %s, got: %s", tt.desc, a.Desc)
|
||||||
}
|
|
||||||
if firstAttachment.Desc != tt.desc {
|
|
||||||
t.Errorf("WithFileDescription() failed. Expected: %s, got: %s", tt.desc,
|
|
||||||
firstAttachment.Desc)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
t.Run("WithFileContentID", func(t *testing.T) {
|
|
||||||
|
// TestFile_WithContentID tests the WithFileContentID option
|
||||||
|
func TestFile_WithContentID(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
id string
|
contentid string
|
||||||
}{
|
}{
|
||||||
{"Content-ID: test", "test"},
|
{"File Content-ID: test", "test"},
|
||||||
{"Content-ID: with newline", "test\n"},
|
{"File Content-ID: empty", ""},
|
||||||
{"Content-ID: empty", ""},
|
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
m := NewMsg()
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
message := NewMsg()
|
m.AttachFile("file.go", WithFileContentID(tt.contentid))
|
||||||
message.AttachFile("file.go", WithFileContentID(tt.id))
|
al := m.GetAttachments()
|
||||||
attachments := message.GetAttachments()
|
if len(al) <= 0 {
|
||||||
if len(attachments) <= 0 {
|
t.Errorf("AttachFile() failed. Attachment list is empty")
|
||||||
t.Fatalf("failed to retrieve attachments list")
|
|
||||||
}
|
}
|
||||||
firstAttachment := attachments[0]
|
a := al[0]
|
||||||
if firstAttachment == nil {
|
if a.Header.Get(HeaderContentID.String()) != tt.contentid {
|
||||||
t.Fatalf("failed to retrieve first attachment, got nil")
|
t.Errorf("WithFileContentID() failed. Expected: %s, got: %s", tt.contentid,
|
||||||
}
|
a.Header.Get(HeaderContentID.String()))
|
||||||
contentID := firstAttachment.Header.Get(HeaderContentID.String())
|
|
||||||
if contentID != tt.id {
|
|
||||||
t.Errorf("WithFileContentID() failed. Expected: %s, got: %s", tt.id,
|
|
||||||
contentID)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
t.Run("WithFileEncoding", func(t *testing.T) {
|
|
||||||
|
// TestFile_WithFileEncoding tests the WithFileEncoding option
|
||||||
|
func TestFile_WithFileEncoding(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
encoding Encoding
|
enc Encoding
|
||||||
want Encoding
|
want Encoding
|
||||||
}{
|
}{
|
||||||
{"File encoding: US-ASCII", EncodingUSASCII, EncodingUSASCII},
|
|
||||||
{"File encoding: 8bit raw", NoEncoding, NoEncoding},
|
{"File encoding: 8bit raw", NoEncoding, NoEncoding},
|
||||||
{"File encoding: Base64", EncodingB64, EncodingB64},
|
{"File encoding: Base64", EncodingB64, EncodingB64},
|
||||||
{"File encoding: quoted-printable (not allowed)", EncodingQP, ""},
|
{"File encoding: quoted-printable (not allowed)", EncodingQP, ""},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
m := NewMsg()
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
message := NewMsg()
|
m.AttachFile("file.go", WithFileEncoding(tt.enc))
|
||||||
message.AttachFile("file.go", WithFileEncoding(tt.encoding))
|
al := m.GetAttachments()
|
||||||
attachments := message.GetAttachments()
|
if len(al) <= 0 {
|
||||||
if len(attachments) <= 0 {
|
t.Errorf("AttachFile() failed. Attachment list is empty")
|
||||||
t.Fatalf("failed to retrieve attachments list")
|
|
||||||
}
|
}
|
||||||
firstAttachment := attachments[0]
|
a := al[0]
|
||||||
if firstAttachment == nil {
|
if a.Enc != tt.want {
|
||||||
t.Fatalf("failed to retrieve first attachment, got nil")
|
t.Errorf("WithFileEncoding() failed. Expected: %s, got: %s", tt.enc, a.Enc)
|
||||||
}
|
|
||||||
if firstAttachment.Enc != tt.want {
|
|
||||||
t.Errorf("WithFileEncoding() failed. Expected: %s, got: %s", tt.want, firstAttachment.Enc)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
t.Run("WithFileName", func(t *testing.T) {
|
|
||||||
|
// TestFile_WithFileContentType tests the WithFileContentType option
|
||||||
|
func TestFile_WithFileContentType(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
fileName string
|
ct ContentType
|
||||||
|
want string
|
||||||
}{
|
}{
|
||||||
{"File name: test", "test"},
|
{"File content-type: text/plain", TypeTextPlain, "text/plain"},
|
||||||
{"File name: with newline", "test\n"},
|
{"File content-type: html/html", TypeTextHTML, "text/html"},
|
||||||
{"File name: empty", ""},
|
{"File content-type: application/octet-stream", TypeAppOctetStream, "application/octet-stream"},
|
||||||
|
{"File content-type: application/pgp-encrypted", TypePGPEncrypted, "application/pgp-encrypted"},
|
||||||
|
{"File content-type: application/pgp-signature", TypePGPSignature, "application/pgp-signature"},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
m := NewMsg()
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
message := NewMsg()
|
m.AttachFile("file.go", WithFileContentType(tt.ct))
|
||||||
message.AttachFile("file.go", WithFileName(tt.fileName))
|
al := m.GetAttachments()
|
||||||
attachments := message.GetAttachments()
|
if len(al) <= 0 {
|
||||||
if len(attachments) <= 0 {
|
t.Errorf("AttachFile() failed. Attachment list is empty")
|
||||||
t.Fatalf("failed to retrieve attachments list")
|
|
||||||
}
|
}
|
||||||
firstAttachment := attachments[0]
|
a := al[0]
|
||||||
if firstAttachment == nil {
|
if a.ContentType != ContentType(tt.want) {
|
||||||
t.Fatalf("failed to retrieve first attachment, got nil")
|
t.Errorf("WithFileContentType() failed. Expected: %s, got: %s", tt.want, a.ContentType)
|
||||||
}
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
145
header_test.go
145
header_test.go
|
@ -8,13 +8,69 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
// TestImportance_StringFuncs tests the different string method of the Importance object
|
||||||
genHeaderTests = []struct {
|
func TestImportance_StringFuncs(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
header Header
|
imp Importance
|
||||||
|
wantns string
|
||||||
|
xprio string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"Importance: Non-Urgent", ImportanceNonUrgent, "0", "5", "non-urgent"},
|
||||||
|
{"Importance: Low", ImportanceLow, "0", "5", "low"},
|
||||||
|
{"Importance: Normal", ImportanceNormal, "", "", ""},
|
||||||
|
{"Importance: High", ImportanceHigh, "1", "1", "high"},
|
||||||
|
{"Importance: Urgent", ImportanceUrgent, "1", "1", "urgent"},
|
||||||
|
{"Importance: Unknown", 9, "", "", ""},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if tt.imp.NumString() != tt.wantns {
|
||||||
|
t.Errorf("wrong number string for Importance returned. Expected: %s, got: %s",
|
||||||
|
tt.wantns, tt.imp.NumString())
|
||||||
|
}
|
||||||
|
if tt.imp.XPrioString() != tt.xprio {
|
||||||
|
t.Errorf("wrong x-prio string for Importance returned. Expected: %s, got: %s",
|
||||||
|
tt.xprio, tt.imp.XPrioString())
|
||||||
|
}
|
||||||
|
if tt.imp.String() != tt.want {
|
||||||
|
t.Errorf("wrong string for Importance returned. Expected: %s, got: %s",
|
||||||
|
tt.want, tt.imp.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestAddrHeader_String tests the string method of the AddrHeader object
|
||||||
|
func TestAddrHeader_String(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
ah AddrHeader
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"Address header: From", HeaderFrom, "From"},
|
||||||
|
{"Address header: To", HeaderTo, "To"},
|
||||||
|
{"Address header: Cc", HeaderCc, "Cc"},
|
||||||
|
{"Address header: Bcc", HeaderBcc, "Bcc"},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if tt.ah.String() != tt.want {
|
||||||
|
t.Errorf("wrong string for AddrHeader returned. Expected: %s, got: %s",
|
||||||
|
tt.want, tt.ah.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHeader_String tests the string method of the Header object
|
||||||
|
func TestHeader_String(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
h Header
|
||||||
want string
|
want string
|
||||||
}{
|
}{
|
||||||
{"Header: Content-Description", HeaderContentDescription, "Content-Description"},
|
|
||||||
{"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"},
|
||||||
|
@ -22,10 +78,6 @@ var (
|
||||||
{"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"},
|
||||||
|
@ -35,90 +87,19 @@ var (
|
||||||
{"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: References", HeaderReferences, "References"},
|
{"Header: HeaderReferences", 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"},
|
||||||
}
|
}
|
||||||
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 {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
if tt.imp.String() != tt.want {
|
if tt.h.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) {
|
|
||||||
if tt.header.String() != tt.want {
|
|
||||||
t.Errorf("wrong string for AddrHeader returned. Expected: %s, got: %s",
|
|
||||||
tt.want, tt.header.String())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHeader_Stringer(t *testing.T) {
|
|
||||||
for _, tt := range genHeaderTests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
if tt.header.String() != tt.want {
|
|
||||||
t.Errorf("wrong string for Header returned. Expected: %s, got: %s",
|
t.Errorf("wrong string for Header returned. Expected: %s, got: %s",
|
||||||
tt.want, tt.header.String())
|
tt.want, tt.h.String())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
85
msg.go
85
msg.go
|
@ -583,9 +583,6 @@ 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))
|
||||||
|
@ -594,15 +591,8 @@ func (m *Msg) SetAddrHeaderIgnoreInvalid(header AddrHeader, values ...string) {
|
||||||
}
|
}
|
||||||
addresses = append(addresses, address)
|
addresses = append(addresses, address)
|
||||||
}
|
}
|
||||||
switch header {
|
|
||||||
case HeaderFrom:
|
|
||||||
if len(addresses) > 0 {
|
|
||||||
m.addrHeader[header] = []*mail.Address{addresses[0]}
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
m.addrHeader[header] = addresses
|
m.addrHeader[header] = addresses
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// EnvelopeFrom sets the envelope from address for the Msg.
|
// EnvelopeFrom sets the envelope from address for the Msg.
|
||||||
//
|
//
|
||||||
|
@ -753,16 +743,7 @@ 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 {
|
||||||
src := strings.Split(rcpts, ",")
|
return m.To(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.
|
||||||
|
@ -847,16 +828,7 @@ 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 {
|
||||||
src := strings.Split(rcpts, ",")
|
return m.Cc(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.
|
||||||
|
@ -942,16 +914,7 @@ 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 {
|
||||||
src := strings.Split(rcpts, ",")
|
return m.Bcc(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.
|
||||||
|
@ -1087,7 +1050,8 @@ 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() {
|
||||||
m.SetDateWithValue(time.Now())
|
now := time.Now().Format(time.RFC1123Z)
|
||||||
|
m.SetGenHeader(HeaderDate, now)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetDateWithValue sets the "Date" header for the Msg using the provided time value in a valid RFC 1123 format.
|
// SetDateWithValue sets the "Date" header for the Msg using the provided time value in a valid RFC 1123 format.
|
||||||
|
@ -1187,9 +1151,6 @@ 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)
|
||||||
|
@ -1198,7 +1159,9 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1237,11 +1200,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
|
||||||
if current, ok := m.genHeader[HeaderDispositionNotificationTo]; ok {
|
addresses = append(addresses, m.genHeader[HeaderDispositionNotificationTo]...)
|
||||||
addresses = current
|
|
||||||
}
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1681,11 +1644,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.NewBuffer(nil)
|
buffer := bytes.Buffer{}
|
||||||
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
|
||||||
}
|
}
|
||||||
|
@ -1712,11 +1675,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)
|
||||||
}
|
}
|
||||||
buffer := bytes.NewBuffer(nil)
|
buf := bytes.Buffer{}
|
||||||
if err := tpl.Execute(buffer, data); err != nil {
|
if err := tpl.Execute(&buf, data); err != nil {
|
||||||
return fmt.Errorf(errTplExecuteFailed, err)
|
return fmt.Errorf(errTplExecuteFailed, err)
|
||||||
}
|
}
|
||||||
writeFunc := writeFuncFromBuffer(buffer)
|
writeFunc := writeFuncFromBuffer(&buf)
|
||||||
m.SetBodyWriter(TypeTextPlain, writeFunc, opts...)
|
m.SetBodyWriter(TypeTextPlain, writeFunc, opts...)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -1786,11 +1749,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.NewBuffer(nil)
|
buffer := bytes.Buffer{}
|
||||||
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
|
||||||
}
|
}
|
||||||
|
@ -1816,11 +1779,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.NewBuffer(nil)
|
buffer := bytes.Buffer{}
|
||||||
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
|
||||||
}
|
}
|
||||||
|
@ -2371,8 +2334,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.NewBuffer(nil)
|
buffer := bytes.Buffer{}
|
||||||
_, 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)
|
||||||
}
|
}
|
||||||
|
|
85
msg_nowin_test.go
Normal file
85
msg_nowin_test.go
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
//go:build !windows
|
||||||
|
// +build !windows
|
||||||
|
|
||||||
|
package mail
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestMsg_WriteToSendmailWithContext tests the WriteToSendmailWithContext() method of the Msg
|
||||||
|
func TestMsg_WriteToSendmailWithContext(t *testing.T) {
|
||||||
|
if os.Getenv("TEST_SKIP_SENDMAIL") != "" {
|
||||||
|
t.Skipf("TEST_SKIP_SENDMAIL variable is set. Skipping sendmail test")
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
sp string
|
||||||
|
sf bool
|
||||||
|
}{
|
||||||
|
{"Sendmail path: /dev/null", "/dev/null", true},
|
||||||
|
{"Sendmail path: /bin/cat", "/bin/cat", true},
|
||||||
|
{"Sendmail path: /is/invalid", "/is/invalid", true},
|
||||||
|
{"Sendmail path: /bin/echo", "/bin/echo", false},
|
||||||
|
}
|
||||||
|
m := NewMsg()
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
ctx, cfn := context.WithTimeout(context.Background(), time.Second*10)
|
||||||
|
defer cfn()
|
||||||
|
m.SetBodyString(TypeTextPlain, "Plain")
|
||||||
|
if err := m.WriteToSendmailWithContext(ctx, tt.sp); err != nil && !tt.sf {
|
||||||
|
t.Errorf("WriteToSendmailWithCommand() failed: %s", err)
|
||||||
|
}
|
||||||
|
m.Reset()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestMsg_WriteToSendmail will test the output to the local sendmail command
|
||||||
|
func TestMsg_WriteToSendmail(t *testing.T) {
|
||||||
|
if os.Getenv("TEST_SKIP_SENDMAIL") != "" {
|
||||||
|
t.Skipf("TEST_SKIP_SENDMAIL variable is set. Skipping sendmail test")
|
||||||
|
}
|
||||||
|
_, err := os.Stat(SendmailPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Skipf("local sendmail command not found in expected path. Skipping")
|
||||||
|
}
|
||||||
|
|
||||||
|
m := NewMsg()
|
||||||
|
_ = m.From("Toni Tester <tester@example.com>")
|
||||||
|
_ = m.To(TestRcpt)
|
||||||
|
m.SetBodyString(TypeTextPlain, "This is a test")
|
||||||
|
if err := m.WriteToSendmail(); err != nil {
|
||||||
|
t.Errorf("WriteToSendmail failed: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMsg_WriteToTempFileFailed(t *testing.T) {
|
||||||
|
m := NewMsg()
|
||||||
|
_ = m.From("Toni Tester <tester@example.com>")
|
||||||
|
_ = m.To("Ellenor Tester <ellinor@example.com>")
|
||||||
|
m.SetBodyString(TypeTextPlain, "This is a test")
|
||||||
|
|
||||||
|
curTmpDir := os.Getenv("TMPDIR")
|
||||||
|
defer func() {
|
||||||
|
if err := os.Setenv("TMPDIR", curTmpDir); err != nil {
|
||||||
|
t.Errorf("failed to set TMPDIR environment variable: %s", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err := os.Setenv("TMPDIR", "/invalid/directory/that/does/not/exist"); err != nil {
|
||||||
|
t.Errorf("failed to set TMPDIR environment variable: %s", err)
|
||||||
|
}
|
||||||
|
_, err := m.WriteToTempFile()
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("WriteToTempFile() did not fail as expected")
|
||||||
|
}
|
||||||
|
}
|
9580
msg_test.go
9580
msg_test.go
File diff suppressed because it is too large
Load diff
146
msg_unix_test.go
146
msg_unix_test.go
|
@ -1,146 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
//go:build linux || freebsd
|
|
||||||
// +build linux freebsd
|
|
||||||
|
|
||||||
package mail
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"errors"
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestMsg_AttachFile_unixOnly(t *testing.T) {
|
|
||||||
t.Run("AttachFile with fileFromFS fails on open", func(t *testing.T) {
|
|
||||||
if os.Getenv("PERFORM_UNIX_OPEN_WRITE_TESTS") != "true" {
|
|
||||||
t.Skipf("PERFORM_UNIX_OPEN_WRITE_TESTS variable is not set. Skipping unix open/write tests")
|
|
||||||
}
|
|
||||||
|
|
||||||
tempFile, err := os.CreateTemp("", "attachfile-open-write-test.*.txt")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to create temp file: %s", err)
|
|
||||||
}
|
|
||||||
t.Cleanup(func() {
|
|
||||||
if err := os.Remove(tempFile.Name()); err != nil {
|
|
||||||
t.Errorf("failed to remove temp file: %s", err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if err = os.Chmod(tempFile.Name(), 0o000); err != nil {
|
|
||||||
t.Fatalf("failed to chmod temp file: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
message := NewMsg()
|
|
||||||
if message == nil {
|
|
||||||
t.Fatal("message is nil")
|
|
||||||
}
|
|
||||||
message.AttachFile(tempFile.Name())
|
|
||||||
attachments := message.GetAttachments()
|
|
||||||
if len(attachments) != 1 {
|
|
||||||
t.Fatalf("failed to get attachments, expected 1, got: %d", len(attachments))
|
|
||||||
}
|
|
||||||
messageBuf := bytes.NewBuffer(nil)
|
|
||||||
_, err = attachments[0].Writer(messageBuf)
|
|
||||||
if err == nil {
|
|
||||||
t.Error("writer func expected to fail, but didn't")
|
|
||||||
}
|
|
||||||
if !errors.Is(err, os.ErrPermission) {
|
|
||||||
t.Errorf("expected error to be %s, got: %s", os.ErrPermission, err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMsg_EmbedFile_unixOnly(t *testing.T) {
|
|
||||||
t.Run("EmbedFile with fileFromFS fails on open", func(t *testing.T) {
|
|
||||||
if os.Getenv("PERFORM_UNIX_OPEN_WRITE_TESTS") != "true" {
|
|
||||||
t.Skipf("PERFORM_UNIX_OPEN_WRITE_TESTS variable is not set. Skipping unix open/write tests")
|
|
||||||
}
|
|
||||||
|
|
||||||
tempFile, err := os.CreateTemp("", "embedfile-open-write-test.*.txt")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to create temp file: %s", err)
|
|
||||||
}
|
|
||||||
t.Cleanup(func() {
|
|
||||||
if err := os.Remove(tempFile.Name()); err != nil {
|
|
||||||
t.Errorf("failed to remove temp file: %s", err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if err = os.Chmod(tempFile.Name(), 0o000); err != nil {
|
|
||||||
t.Fatalf("failed to chmod temp file: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
message := NewMsg()
|
|
||||||
if message == nil {
|
|
||||||
t.Fatal("message is nil")
|
|
||||||
}
|
|
||||||
message.EmbedFile(tempFile.Name())
|
|
||||||
embeds := message.GetEmbeds()
|
|
||||||
if len(embeds) != 1 {
|
|
||||||
t.Fatalf("failed to get embeds, expected 1, got: %d", len(embeds))
|
|
||||||
}
|
|
||||||
messageBuf := bytes.NewBuffer(nil)
|
|
||||||
_, err = embeds[0].Writer(messageBuf)
|
|
||||||
if err == nil {
|
|
||||||
t.Error("writer func expected to fail, but didn't")
|
|
||||||
}
|
|
||||||
if !errors.Is(err, os.ErrPermission) {
|
|
||||||
t.Errorf("expected error to be %s, got: %s", os.ErrPermission, err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMsg_WriteToFile_unixOnly(t *testing.T) {
|
|
||||||
t.Run("WriteToFile fails on create", func(t *testing.T) {
|
|
||||||
if os.Getenv("PERFORM_UNIX_OPEN_WRITE_TESTS") != "true" {
|
|
||||||
t.Skipf("PERFORM_UNIX_OPEN_WRITE_TESTS variable is not set. Skipping unix open/write tests")
|
|
||||||
}
|
|
||||||
|
|
||||||
tempfile, err := os.CreateTemp("", "testmail-create.*.eml")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to create temp file: %s", err)
|
|
||||||
}
|
|
||||||
if err = os.Chmod(tempfile.Name(), 0o000); err != nil {
|
|
||||||
t.Fatalf("failed to chmod temp file: %s", err)
|
|
||||||
}
|
|
||||||
t.Cleanup(func() {
|
|
||||||
if err = tempfile.Close(); err != nil {
|
|
||||||
t.Fatalf("failed to close temp file: %s", err)
|
|
||||||
}
|
|
||||||
if err = os.Remove(tempfile.Name()); err != nil {
|
|
||||||
t.Fatalf("failed to remove temp file: %s", err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
message := testMessage(t)
|
|
||||||
if err = message.WriteToFile(tempfile.Name()); err == nil {
|
|
||||||
t.Errorf("expected error, got nil")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMsg_WriteToTempFile_unixOnly(t *testing.T) {
|
|
||||||
if os.Getenv("PERFORM_UNIX_OPEN_WRITE_TESTS") != "true" {
|
|
||||||
t.Skipf("PERFORM_UNIX_OPEN_WRITE_TESTS variable is not set. Skipping unix open/write tests")
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("WriteToTempFile fails on invalid TMPDIR", func(t *testing.T) {
|
|
||||||
// We store the current TMPDIR variable so we can set it back when the test is over
|
|
||||||
curTmpDir := os.Getenv("TMPDIR")
|
|
||||||
t.Cleanup(func() {
|
|
||||||
if err := os.Setenv("TMPDIR", curTmpDir); err != nil {
|
|
||||||
t.Errorf("failed to set TMPDIR environment variable: %s", err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if err := os.Setenv("TMPDIR", "/invalid/directory/that/does/not/exist"); err != nil {
|
|
||||||
t.Fatalf("failed to set TMPDIR environment variable: %s", err)
|
|
||||||
}
|
|
||||||
message := testMessage(t)
|
|
||||||
_, err := message.WriteToTempFile()
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("expected writing to invalid TMPDIR to fail, got: %s", err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -6,673 +6,151 @@ 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) {
|
||||||
t.Run("msgWriter writes to memory for all charsets", func(t *testing.T) {
|
bw := &brokenWriter{}
|
||||||
for _, tt := range charsetTests {
|
mw := &msgWriter{writer: bw, charset: CharsetUTF8, encoder: mime.QEncoding}
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
_, err := mw.Write([]byte("test"))
|
||||||
buffer := bytes.NewBuffer(nil)
|
|
||||||
msgwriter := &msgWriter{
|
|
||||||
writer: buffer,
|
|
||||||
charset: tt.value,
|
|
||||||
encoder: mime.QEncoding,
|
|
||||||
}
|
|
||||||
_, err := msgwriter.Write([]byte("test"))
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("msgWriter failed to write: %s", err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
t.Run("msgWriter writes to memory for all encodings", func(t *testing.T) {
|
|
||||||
for _, tt := range encodingTests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
buffer := bytes.NewBuffer(nil)
|
|
||||||
msgwriter := &msgWriter{
|
|
||||||
writer: buffer,
|
|
||||||
charset: CharsetUTF8,
|
|
||||||
encoder: getEncoder(tt.value),
|
|
||||||
}
|
|
||||||
_, err := msgwriter.Write([]byte("test"))
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("msgWriter failed to write: %s", err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
t.Run("msgWriter should fail on write", func(t *testing.T) {
|
|
||||||
msgwriter := &msgWriter{
|
|
||||||
writer: failReadWriteSeekCloser{},
|
|
||||||
charset: CharsetUTF8,
|
|
||||||
encoder: getEncoder(EncodingQP),
|
|
||||||
}
|
|
||||||
_, err := msgwriter.Write([]byte("test"))
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatalf("msgWriter was supposed to fail on write")
|
t.Errorf("msgWriter WriteTo() with brokenWriter should fail, but didn't")
|
||||||
}
|
|
||||||
})
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Also test the part when a previous error happened
|
||||||
|
mw.err = fmt.Errorf("broken")
|
||||||
|
_, err = mw.Write([]byte("test"))
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("msgWriter WriteTo() with brokenWriter should fail, but didn't")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestMsgWriter_writeMsg tests the writeMsg method of the msgWriter
|
||||||
func TestMsgWriter_writeMsg(t *testing.T) {
|
func TestMsgWriter_writeMsg(t *testing.T) {
|
||||||
msgwriter := &msgWriter{
|
m := NewMsg()
|
||||||
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")
|
||||||
t.Run("msgWriter writes a simple message", func(t *testing.T) {
|
m.SetBulk()
|
||||||
buffer := bytes.NewBuffer(nil)
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
msgwriter.writer = buffer
|
m.SetDateWithValue(now)
|
||||||
message := testMessage(t)
|
m.SetMessageIDWithValue("message@id.com")
|
||||||
message.SetDateWithValue(now)
|
m.SetBodyString(TypeTextPlain, "This is the body")
|
||||||
message.SetMessageIDWithValue("message@id.com")
|
m.AddAlternativeString(TypeTextHTML, "This is the alternative body")
|
||||||
message.SetBulk()
|
buf := bytes.Buffer{}
|
||||||
msgwriter.writeMsg(message)
|
mw := &msgWriter{writer: &buf, charset: CharsetUTF8, encoder: mime.QEncoding}
|
||||||
if msgwriter.err != nil {
|
mw.writeMsg(m)
|
||||||
t.Errorf("msgWriter failed to write: %s", msgwriter.err)
|
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")
|
||||||
}
|
}
|
||||||
|
|
||||||
var incorrectFields []string
|
pl := m.GetParts()
|
||||||
if !strings.Contains(buffer.String(), "MIME-Version: 1.0\r\n") {
|
if len(pl) <= 0 {
|
||||||
incorrectFields = append(incorrectFields, "MIME-Version")
|
t.Errorf("expected multiple parts but got none")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
if !strings.Contains(buffer.String(), fmt.Sprintf("Date: %s\r\n", now.Format(time.RFC1123Z))) {
|
if len(pl) == 2 {
|
||||||
incorrectFields = append(incorrectFields, "Date")
|
ap := pl[1]
|
||||||
|
ap.SetCharset(CharsetISO88591)
|
||||||
}
|
}
|
||||||
if !strings.Contains(buffer.String(), "Message-ID: <message@id.com>\r\n") {
|
buf.Reset()
|
||||||
incorrectFields = append(incorrectFields, "Message-ID")
|
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(buffer.String(), "Precedence: bulk\r\n") {
|
if !strings.Contains(ms, `Content-Type: text/html; charset=ISO-8859-1`) {
|
||||||
incorrectFields = append(incorrectFields, "Precedence")
|
ea = append(ea, "alternative body charset")
|
||||||
}
|
|
||||||
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) {
|
if len(ea) > 0 {
|
||||||
t.Run("message with no preformatted headerset", func(t *testing.T) {
|
em := "writeMsg() failed. The following errors occurred:\n"
|
||||||
buffer := bytes.NewBuffer(nil)
|
for e := range ea {
|
||||||
msgwriter := &msgWriter{
|
em += fmt.Sprintf("* incorrect %q field", ea[e])
|
||||||
writer: buffer,
|
|
||||||
charset: CharsetUTF8,
|
|
||||||
encoder: getEncoder(EncodingQP),
|
|
||||||
}
|
}
|
||||||
message := testMessage(t)
|
em += fmt.Sprintf("\n\nFull message:\n%s", ms)
|
||||||
message.SetGenHeaderPreformatted(HeaderContentID, "This is a content id")
|
t.Error(em)
|
||||||
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) {
|
// TestMsgWriter_writeMsg_PGP tests the writeMsg method of the msgWriter with PGP types set
|
||||||
msgwriter := &msgWriter{
|
func TestMsgWriter_writeMsg_PGP(t *testing.T) {
|
||||||
charset: CharsetUTF8,
|
m := NewMsg(WithPGPType(PGPEncrypt))
|
||||||
encoder: getEncoder(EncodingQP),
|
_ = m.From(`"Toni Tester" <test@example.com>`)
|
||||||
}
|
_ = m.To(`"Toni Receiver" <receiver@example.com>`)
|
||||||
t.Run("message with a single file attached", func(t *testing.T) {
|
m.Subject("This is a subject")
|
||||||
buffer := bytes.NewBuffer(nil)
|
m.SetBodyString(TypeTextPlain, "This is the body")
|
||||||
msgwriter.writer = buffer
|
buf := bytes.Buffer{}
|
||||||
message := testMessage(t)
|
mw := &msgWriter{writer: &buf, charset: CharsetUTF8, encoder: mime.QEncoding}
|
||||||
message.AttachFile("testdata/attachment.txt")
|
mw.writeMsg(m)
|
||||||
msgwriter.writeMsg(message)
|
ms := buf.String()
|
||||||
if msgwriter.err != nil {
|
if !strings.Contains(ms, `encrypted; protocol="application/pgp-encrypted"`) {
|
||||||
t.Errorf("msgWriter failed to write: %s", msgwriter.err)
|
t.Errorf("writeMsg failed. Expected PGP encoding header but didn't find it in message output")
|
||||||
}
|
|
||||||
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) {
|
m = NewMsg(WithPGPType(PGPSignature))
|
||||||
msgwriter := &msgWriter{
|
_ = m.From(`"Toni Tester" <test@example.com>`)
|
||||||
charset: CharsetUTF8,
|
_ = m.To(`"Toni Receiver" <receiver@example.com>`)
|
||||||
encoder: getEncoder(EncodingQP),
|
m.Subject("This is a subject")
|
||||||
|
m.SetBodyString(TypeTextPlain, "This is the body")
|
||||||
|
buf = bytes.Buffer{}
|
||||||
|
mw = &msgWriter{writer: &buf, charset: CharsetUTF8, encoder: mime.QEncoding}
|
||||||
|
mw.writeMsg(m)
|
||||||
|
ms = buf.String()
|
||||||
|
if !strings.Contains(ms, `signed; protocol="application/pgp-signature"`) {
|
||||||
|
t.Errorf("writeMsg failed. Expected PGP encoding header but didn't find it in message output")
|
||||||
}
|
}
|
||||||
t.Run("message with no part charset should use default message charset", func(t *testing.T) {
|
|
||||||
buffer := bytes.NewBuffer(nil)
|
|
||||||
msgwriter.writer = buffer
|
|
||||||
message := testMessage(t, WithCharset(CharsetUTF7))
|
|
||||||
message.AddAlternativeString(TypeTextPlain, "thisisatest")
|
|
||||||
message.parts[1].charset = ""
|
|
||||||
msgwriter.writeMsg(message)
|
|
||||||
if msgwriter.err != nil {
|
|
||||||
t.Errorf("msgWriter failed to write: %s", msgwriter.err)
|
|
||||||
}
|
|
||||||
if !strings.Contains(buffer.String(), "ontent-Type: text/plain; charset=UTF-7\r\n\r\nTestmail") {
|
|
||||||
t.Errorf("part not found in mail message. Mail: %s", buffer.String())
|
|
||||||
}
|
|
||||||
if !strings.Contains(buffer.String(), "ontent-Type: text/plain; charset=UTF-7\r\n\r\nthisisatest") {
|
|
||||||
t.Errorf("part not found in mail message. Mail: %s", buffer.String())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
t.Run("message with parts that have a description", func(t *testing.T) {
|
|
||||||
buffer := bytes.NewBuffer(nil)
|
|
||||||
msgwriter.writer = buffer
|
|
||||||
message := testMessage(t)
|
|
||||||
message.AddAlternativeString(TypeTextPlain, "thisisatest")
|
|
||||||
message.parts[1].description = "thisisadescription"
|
|
||||||
msgwriter.writeMsg(message)
|
|
||||||
if msgwriter.err != nil {
|
|
||||||
t.Errorf("msgWriter failed to write: %s", msgwriter.err)
|
|
||||||
}
|
|
||||||
if !strings.Contains(buffer.String(), "Content-Description: thisisadescription") {
|
|
||||||
t.Errorf("part description not found in mail message. Mail: %s", buffer.String())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMsgWriter_writeString(t *testing.T) {
|
|
||||||
msgwriter := &msgWriter{
|
|
||||||
charset: CharsetUTF8,
|
|
||||||
encoder: getEncoder(EncodingQP),
|
|
||||||
}
|
|
||||||
t.Run("writeString succeeds", func(t *testing.T) {
|
|
||||||
buffer := bytes.NewBuffer(nil)
|
|
||||||
msgwriter.writer = buffer
|
|
||||||
msgwriter.writeString("thisisatest")
|
|
||||||
if !strings.EqualFold(buffer.String(), "thisisatest") {
|
|
||||||
t.Errorf("writeString failed, expected: thisisatest got: %s", buffer.String())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
t.Run("writeString fails", func(t *testing.T) {
|
|
||||||
msgwriter.writer = failReadWriteSeekCloser{}
|
|
||||||
msgwriter.writeString("thisisatest")
|
|
||||||
if msgwriter.err == nil {
|
|
||||||
t.Errorf("writeString succeeded, expected error")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
t.Run("writeString on errored writer should return", func(t *testing.T) {
|
|
||||||
buffer := bytes.NewBuffer(nil)
|
|
||||||
msgwriter.writer = buffer
|
|
||||||
msgwriter.err = errors.New("intentional error")
|
|
||||||
msgwriter.writeString("thisisatest")
|
|
||||||
if !strings.EqualFold(buffer.String(), "") {
|
|
||||||
t.Errorf("writeString succeeded, expected: empty string, got: %s", buffer.String())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMsgWriter_writeHeader(t *testing.T) {
|
|
||||||
msgwriter := &msgWriter{
|
|
||||||
charset: CharsetUTF8,
|
|
||||||
encoder: getEncoder(EncodingQP),
|
|
||||||
}
|
|
||||||
t.Run("writeHeader with single value", func(t *testing.T) {
|
|
||||||
buffer := bytes.NewBuffer(nil)
|
|
||||||
msgwriter.writer = buffer
|
|
||||||
msgwriter.writeHeader(HeaderMessageID, "this.is.a.test")
|
|
||||||
if !strings.EqualFold(buffer.String(), "Message-ID: this.is.a.test\r\n") {
|
|
||||||
t.Errorf("writeHeader failed, expected: %s, got: %s", "Message-ID: this.is.a.test",
|
|
||||||
buffer.String())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
t.Run("writeHeader with multiple values", func(t *testing.T) {
|
|
||||||
buffer := bytes.NewBuffer(nil)
|
|
||||||
msgwriter.writer = buffer
|
|
||||||
msgwriter.writeHeader(HeaderMessageID, "this.is.a.test", "this.as.well")
|
|
||||||
if !strings.EqualFold(buffer.String(), "Message-ID: this.is.a.test, this.as.well\r\n") {
|
|
||||||
t.Errorf("writeHeader failed, expected: %s, got: %s", "Message-ID: this.is.a.test, this.as.well",
|
|
||||||
buffer.String())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
t.Run("writeHeader with no values", func(t *testing.T) {
|
|
||||||
buffer := bytes.NewBuffer(nil)
|
|
||||||
msgwriter.writer = buffer
|
|
||||||
msgwriter.writeHeader(HeaderMessageID)
|
|
||||||
// While technically it is permitted to have empty headers, it's recommend to omit them if
|
|
||||||
// no value is present. We follow this recommendation.
|
|
||||||
if !strings.EqualFold(buffer.String(), "") {
|
|
||||||
t.Errorf("writeHeader failed, expected: %s, got: %s", "", buffer.String())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
t.Run("writeHeader with very long value", func(t *testing.T) {
|
|
||||||
buffer := bytes.NewBuffer(nil)
|
|
||||||
msgwriter.writer = buffer
|
|
||||||
msgwriter.writeHeader(HeaderMessageID, strings.Repeat("a", MaxHeaderLength-13), "next-row")
|
|
||||||
want := "Message-ID:\r\n " + strings.Repeat("a", MaxHeaderLength-13) + ",\r\n next-row\r\n"
|
|
||||||
if !strings.EqualFold(buffer.String(), want) {
|
|
||||||
t.Errorf("writeHeader failed, expected: %s, got: %s", want, buffer.String())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMsgWriter_writeBody(t *testing.T) {
|
|
||||||
t.Log("We only cover some edge-cases here, most of the functionality is tested already very thoroughly.")
|
|
||||||
|
|
||||||
msgwriter := &msgWriter{
|
|
||||||
charset: CharsetUTF8,
|
|
||||||
encoder: getEncoder(EncodingQP),
|
|
||||||
}
|
|
||||||
t.Run("writeBody on NoEncoding", func(t *testing.T) {
|
|
||||||
buffer := bytes.NewBuffer(nil)
|
|
||||||
msgwriter.writer = buffer
|
|
||||||
message := testMessage(t)
|
|
||||||
msgwriter.writeBody(message.parts[0].writeFunc, NoEncoding)
|
|
||||||
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 {
|
||||||
// See https://github.com/golang/go/issues/70011
|
if err := c.hello(); err != nil {
|
||||||
_ = c.hello() // ignore error; we're quitting anyhow
|
return err
|
||||||
|
}
|
||||||
_, _, err := c.cmd(221, "QUIT")
|
_, _, err := c.cmd(221, "QUIT")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -900,35 +900,6 @@ 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
8
testdata/RFC5322-A1-1-invalid-from.eml
vendored
|
@ -1,8 +0,0 @@
|
||||||
From: §§§§§§§§
|
|
||||||
To: Mary Smith <mary@example.net>
|
|
||||||
Subject: Saying Hello
|
|
||||||
Date: Fri, 21 Nov 1997 09:55:06 -0600
|
|
||||||
Message-ID: <1234@local.machine.example>
|
|
||||||
|
|
||||||
This is a message just to say hello.
|
|
||||||
So, "Hello".
|
|
|
@ -1,3 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: Copyright (c) 2022-2024 The go-mail Authors
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
8
testdata/RFC5322-A1-1.eml
vendored
8
testdata/RFC5322-A1-1.eml
vendored
|
@ -1,8 +0,0 @@
|
||||||
From: John Doe <jdoe@machine.example>
|
|
||||||
To: Mary Smith <mary@example.net>
|
|
||||||
Subject: Saying Hello
|
|
||||||
Date: Fri, 21 Nov 1997 09:55:06 -0600
|
|
||||||
Message-ID: <1234@local.machine.example>
|
|
||||||
|
|
||||||
This is a message just to say hello.
|
|
||||||
So, "Hello".
|
|
3
testdata/RFC5322-A1-1.eml.license
vendored
3
testdata/RFC5322-A1-1.eml.license
vendored
|
@ -1,3 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: Copyright (c) 2022-2024 The go-mail Authors
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
1
testdata/attachment
vendored
1
testdata/attachment
vendored
|
@ -1 +0,0 @@
|
||||||
This is a test attachment
|
|
3
testdata/attachment.license
vendored
3
testdata/attachment.license
vendored
|
@ -1,3 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: Copyright (c) 2022-2024 The go-mail Authors
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
1
testdata/attachment.txt
vendored
1
testdata/attachment.txt
vendored
|
@ -1 +0,0 @@
|
||||||
This is a test attachment
|
|
3
testdata/attachment.txt.license
vendored
3
testdata/attachment.txt.license
vendored
|
@ -1,3 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: Copyright (c) 2022-2024 The go-mail Authors
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
1
testdata/embed.txt
vendored
1
testdata/embed.txt
vendored
|
@ -1 +0,0 @@
|
||||||
This is a test embed
|
|
3
testdata/embed.txt.license
vendored
3
testdata/embed.txt.license
vendored
|
@ -1,3 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: Copyright (c) 2022-2024 The go-mail Authors
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
1
testdata/logo.svg
vendored
1
testdata/logo.svg
vendored
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 20 KiB |
367
testdata/logo.svg.base64
vendored
367
testdata/logo.svg.base64
vendored
|
@ -1,367 +0,0 @@
|
||||||
PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PCFE
|
|
||||||
T0NUWVBFIHN2ZyBQVUJMSUMgIi0vL1czQy8vRFREIFNWRyAxLjEvL0VOIiAiaHR0cDovL3d3dy53
|
|
||||||
My5vcmcvR3JhcGhpY3MvU1ZHLzEuMS9EVEQvc3ZnMTEuZHRkIj48c3ZnIHdpZHRoPSIxMDAlIiBo
|
|
||||||
ZWlnaHQ9IjEwMCUiIHZpZXdCb3g9IjAgMCA2MDAgNjAwIiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJo
|
|
||||||
dHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3Jn
|
|
||||||
LzE5OTkveGxpbmsiIHhtbDpzcGFjZT0icHJlc2VydmUiIHhtbG5zOnNlcmlmPSJodHRwOi8vd3d3
|
|
||||||
LnNlcmlmLmNvbS8iIHN0eWxlPSJmaWxsLXJ1bGU6ZXZlbm9kZDtjbGlwLXJ1bGU6ZXZlbm9kZDtz
|
|
||||||
dHJva2UtbGluZWNhcDpyb3VuZDtzdHJva2UtbGluZWpvaW46cm91bmQ7c3Ryb2tlLW1pdGVybGlt
|
|
||||||
aXQ6MS41OyI+PHJlY3QgaWQ9ItCc0L7QvdGC0LDQttC90LDRjy3QvtCx0LvQsNGB0YLRjDEiIHNl
|
|
||||||
cmlmOmlkPSLQnNC+0L3RgtCw0LbQvdCw0Y8g0L7QsdC70LDRgdGC0YwxIiB4PSIwIiB5PSIwIiB3
|
|
||||||
aWR0aD0iNjAwIiBoZWlnaHQ9IjYwMCIgc3R5bGU9ImZpbGw6bm9uZTsiLz48Zz48cGF0aCBkPSJN
|
|
||||||
NTg0LjcyNiw5OC4zOTVjLTAsLTkuNzM4IC03LjkwNywtMTcuNjQ1IC0xNy42NDUsLTE3LjY0NWwt
|
|
||||||
NTEwLjAyNywwYy05LjczOSwwIC0xNy42NDUsNy45MDcgLTE3LjY0NSwxNy42NDVsLTAsMzE5Ljc5
|
|
||||||
NGMtMCw5LjczOSA3LjkwNiwxNy42NDYgMTcuNjQ1LDE3LjY0Nmw1MTAuMDI3LC0wYzkuNzM4LC0w
|
|
||||||
IDE3LjY0NSwtNy45MDcgMTcuNjQ1LC0xNy42NDZsLTAsLTMxOS43OTRaIiBzdHlsZT0iZmlsbDoj
|
|
||||||
MmMyYzJjO3N0cm9rZTojMDAwO3N0cm9rZS13aWR0aDoyLjFweDsiLz48cGF0aCBkPSJNNTg1LjEy
|
|
||||||
Niw5OC4zOTVjLTAsLTkuNzM4IC03LjkwNywtMTcuNjQ1IC0xNy42NDUsLTE3LjY0NWwtNDk0Ljgz
|
|
||||||
OSwwYy05LjczOCwwIC0xNy42NDUsNy45MDcgLTE3LjY0NSwxNy42NDVsMCwzMTkuNzk0YzAsOS43
|
|
||||||
MzkgNy45MDcsMTcuNjQ2IDE3LjY0NSwxNy42NDZsNDk0LjgzOSwtMGM5LjczOCwtMCAxNy42NDUs
|
|
||||||
LTcuOTA3IDE3LjY0NSwtMTcuNjQ2bC0wLC0zMTkuNzk0WiIgc3R5bGU9ImZpbGw6IzM3MzczNztz
|
|
||||||
dHJva2U6IzAwMDtzdHJva2Utd2lkdGg6Mi4wN3B4OyIvPjxwYXRoIGQ9Ik01NjcuMjU5LDExNC40
|
|
||||||
NzJjLTAsLTguNzYgLTcuMTEyLC0xNS44NzIgLTE1Ljg3MiwtMTUuODcybC00NjIuNjUxLDBjLTgu
|
|
||||||
NzYsMCAtMTUuODcyLDcuMTEyIC0xNS44NzIsMTUuODcybDAsMjg3LjY0MWMwLDguNzYgNy4xMTIs
|
|
||||||
MTUuODcxIDE1Ljg3MiwxNS44NzFsNDYyLjY1MSwwYzguNzYsMCAxNS44NzIsLTcuMTExIDE1Ljg3
|
|
||||||
MiwtMTUuODcxbC0wLC0yODcuNjQxWiIgc3R5bGU9ImZpbGw6I2ZmZjJlYjtzdHJva2U6IzAwMDtz
|
|
||||||
dHJva2Utd2lkdGg6MS45cHg7Ii8+PHBhdGggZD0iTTIzNi4xNDUsNDM1Ljg3OGwtMjMuNDIsOTEu
|
|
||||||
MDA4bDEzMC43NjUsLTBsMjIuMzY1LC05MC43NTZsLTEyOS43MSwtMC4yNTJaIiBzdHlsZT0iZmls
|
|
||||||
bDojMmMyYzJjO3N0cm9rZTojMDAwO3N0cm9rZS13aWR0aDoyLjQ3cHg7Ii8+PHBhdGggZD0iTTI0
|
|
||||||
NS4zOTcsNDM1Ljg3OGwtMjMuNDIxLDkxLjAwOGwxMzAuNzY2LC0wbDIyLjM2NSwtOTAuNzU2bC0x
|
|
||||||
MjkuNzEsLTAuMjUyWiIgc3R5bGU9ImZpbGw6IzM3MzczNztzdHJva2U6IzAwMDtzdHJva2Utd2lk
|
|
||||||
dGg6Mi40N3B4OyIvPjxwYXRoIGQ9Ik00NzIuNTg0LDUzMS4wOTNjLTAsLTIuODI3IC0yLjI5NSwt
|
|
||||||
NS4xMjEgLTUuMTIxLC01LjEyMWwtMjkyLjU0MSwtMGMtMi44MjYsLTAgLTUuMTIxLDIuMjk0IC01
|
|
||||||
LjEyMSw1LjEyMWwwLDEwLjI0M2MwLDIuODI2IDIuMjk1LDUuMTIxIDUuMTIxLDUuMTIxbDI5Mi41
|
|
||||||
NDEsLTBjMi44MjYsLTAgNS4xMjEsLTIuMjk1IDUuMTIxLC01LjEyMWwtMCwtMTAuMjQzWiIgc3R5
|
|
||||||
bGU9ImZpbGw6IzJjMmMyYztzdHJva2U6IzAwMDtzdHJva2Utd2lkdGg6Mi40N3B4OyIvPjxnPjxw
|
|
||||||
YXRoIGQ9Ik0xOTAuMzc5LDQzMS40NTVjLTAsLTEuOTE2IC0xLjU1NiwtMy40NzIgLTMuNDcyLC0z
|
|
||||||
LjQ3MmwtNTQuNTYzLDBjLTEuOTE3LDAgLTMuNDcyLDEuNTU2IC0zLjQ3MiwzLjQ3MmwtMCw0Ni4z
|
|
||||||
MDljLTAsMS45MTYgMS41NTUsMy40NzIgMy40NzIsMy40NzJsNTQuNTYzLC0wYzEuOTE2LC0wIDMu
|
|
||||||
NDcyLC0xLjU1NiAzLjQ3MiwtMy40NzJsLTAsLTQ2LjMwOVoiIHN0eWxlPSJmaWxsOiNlZWFjMDE7
|
|
||||||
c3Ryb2tlOiMwMDA7c3Ryb2tlLXdpZHRoOjMuMXB4OyIvPjxnPjxwYXRoIGQ9Ik0xMzkuOTk2LDQ0
|
|
||||||
Mi44NTNjLTAsMC40MiAtMC4wNzEsMC45MjIgMC4wNjMsMS4zMjNjMC41MywxLjU5MiAyLjc1LDEu
|
|
||||||
Mzg2IDQuMDk1LDEuMzg2YzIuMzk3LC0wIDQuNjc0LC0wLjEwOCA2LjQ4OSwtMS44MjdjMC40NSwt
|
|
||||||
MC40MjYgMC45NjYsLTAuOTQgMS4wNzIsLTEuNTc1YzAuMDYxLC0wLjM2NyAwLjEwMiwtMS4yMDgg
|
|
||||||
LTAuMTI2LC0xLjUxMmMtMC44MDMsLTEuMDcxIC0yLjg0NCwtMC4zNzMgLTMuNjU1LDAuMjUyYy0w
|
|
||||||
LjcxNCwwLjU1IC0wLjgyMiwxLjgyNiAtMC45NDUsMi42NDZjLTAuNDA2LDIuNzA1IDAuMzg3LDUu
|
|
||||||
MDMgMy4xNSw1Ljg1OWMzLjQwNSwxLjAyMSA2Ljc4NCwwLjA2IDEwLjA4NiwtMC44MDljMS41OTcs
|
|
||||||
LTAuNDIxIDMuNjYsLTAuNzcgNS4wMTcsLTEuNzJjMC40NTIsLTAuMzE2IDAuNDQ0LC0wLjI4NCAw
|
|
||||||
LjkwMSwtMC41NThjMS4yMDgsLTAuNzI1IDMuODY5LC0xLjkyNyAyLjQ1NywtMy41OTFjLTAuMTks
|
|
||||||
LTAuMjI0IC0xLjEzOSwtMC4yMzcgLTEuMjYsMC4xMjZjLTAuNDIsMS4yNTcgLTEuMzI2LDMuMzAy
|
|
||||||
IDAuNTY3LDMuNzhjMS4zOTgsMC4zNTMgMi45MTksMC4xODkgNC4zNDcsMC4xODljMC4xOTMsLTAg
|
|
||||||
MS40ODUsLTAuMjc5IDEuNzAxLC0wLjA2M2MwLjU1NSwwLjU1NSAxLjU0MywwLjgxMSAyLjMzMSwx
|
|
||||||
LjAwOGMwLjkzMiwwLjIzMyAxLjk0NCwwLjYwNiAyLjg5OSwwLjY5M2MwLjg2OSwwLjA3OSAxLjc5
|
|
||||||
LC0wLjA0NSAyLjY0NiwwLjEyNiIgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2U6IzAwMDtzdHJva2Ut
|
|
||||||
d2lkdGg6MS45NnB4OyIvPjxwYXRoIGQ9Ik0xNDIuNzA1LDQ1Mi42ODFjMC4xMTksMC41NTEgMC44
|
|
||||||
OTMsMC42NjkgMS4zMTgsMC44NDZjMS41OTgsMC42NjYgMy41MTQsMC44NzQgNS4yMzQsMC45ODJj
|
|
||||||
Mi4zNzgsMC4xNDggNC43MDQsLTAuMTU5IDcuMDU3LC0wLjMxNWMxLjg2MSwtMC4xMjUgMy43NDYs
|
|
||||||
MC4xMTYgNS42MDcsLTBjMi4wNTEsLTAuMTI5IDQuMzIxLC0wLjM3NyA2LjM2NCwtMC4wNjNjMS4w
|
|
||||||
NzIsMC4xNjQgMi4wODksMC40OCAzLjE1LDAuNjkzIiBzdHlsZT0iZmlsbDpub25lO3N0cm9rZToj
|
|
||||||
MDAwO3N0cm9rZS13aWR0aDoxLjk2cHg7Ii8+PHBhdGggZD0iTTE3Ni4zNDksNDU1LjA3NmMxLjEx
|
|
||||||
MywtMC4wMjEgMi4yMjYsLTAuMDQyIDMuMzQsLTAuMDYzIiBzdHlsZT0iZmlsbDpub25lO3N0cm9r
|
|
||||||
ZTojMDAwO3N0cm9rZS13aWR0aDoxLjk2cHg7Ii8+PHBhdGggZD0iTTE0NS4xNjIsNDU5Ljg2NGwy
|
|
||||||
MS42NzQsLTAiIHN0eWxlPSJmaWxsOm5vbmU7c3Ryb2tlOiMwMDA7c3Ryb2tlLXdpZHRoOjEuOTZw
|
|
||||||
eDsiLz48L2c+PC9nPjxnPjxnPjxwYXRoIGQ9Ik03Ni4yNzMsMTIxLjQ5NGw0ODcuNTU3LC0wLjAx
|
|
||||||
NSIgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2U6I2Y0ZGRkMDtzdHJva2Utd2lkdGg6NC45M3B4O3N0
|
|
||||||
cm9rZS1saW5lY2FwOnNxdWFyZTsiLz48cGF0aCBkPSJNMjMwLjk1MSwyMjQuMTc1Yy0wLjgwMiwt
|
|
||||||
My4zMjcgLTguMDU5LC03Ljk4NCAtMTEuMDA1LC04Ljk0NWMtMzUuODQ2LC0xMS42OTkgLTIwLjc4
|
|
||||||
MywyNy4zNzIgLTAuNjE4LDI0LjM0MiIgc3R5bGU9ImZpbGw6IzYxZTBlZTsiLz48cGF0aCBkPSJN
|
|
||||||
MjM0Ljk0NCwyMjMuMjEzYy0wLjM3OSwtMS41NzQgLTEuNTIyLC0zLjQzNiAtMy4yNzYsLTUuMTc0
|
|
||||||
Yy0zLjA2MSwtMy4wMzMgLTguMDE0LC01LjkyIC0xMC40NDgsLTYuNzE0Yy0xMy4xMzUsLTQuMjg3
|
|
||||||
IC0yMC42MTgsLTEuOTk0IC0yNC4wNzEsMS44MTljLTMuNjM4LDQuMDE3IC0zLjgzNiwxMC40OTIg
|
|
||||||
LTAuODI1LDE2LjY1MWMzLjk4MSw4LjE0MyAxMy4zMjEsMTUuMzg2IDIzLjYxNSwxMy44MzljMi4y
|
|
||||||
NDIsLTAuMzM3IDMuNzg4LC0yLjQzIDMuNDUxLC00LjY3MmMtMC4zMzcsLTIuMjQyIC0yLjQzLC0z
|
|
||||||
Ljc4OSAtNC42NzIsLTMuNDUyYy02LjY0NywwLjk5OSAtMTIuNDQ0LC00LjA2NSAtMTUuMDE0LC05
|
|
||||||
LjMyM2MtMC44MDEsLTEuNjM3IC0xLjI5MywtMy4zMDggLTEuMzAyLC00Ljg0N2MtMC4wMDUsLTEu
|
|
||||||
MDIgMC4xOTUsLTEuOTc0IDAuODM2LC0yLjY4MmMwLjgzMiwtMC45MTggMi4yMjUsLTEuMzU0IDQu
|
|
||||||
MTMzLC0xLjQ4MWMyLjg1MiwtMC4xOTEgNi41NjcsMC40MTIgMTEuMywxLjk1N2MxLjQ1NiwwLjQ3
|
|
||||||
NSA0LjE2LDIuMDk2IDYuMjU3LDMuODY5YzAuNjM2LDAuNTM3IDEuMjE1LDEuMDg4IDEuNjU4LDEu
|
|
||||||
NjM2YzAuMTUyLDAuMTg4IDAuMzMxLDAuMzI3IDAuMzcyLDAuNDk4YzAuNTMxLDIuMjA0IDIuNzUx
|
|
||||||
LDMuNTYyIDQuOTU1LDMuMDMxYzIuMjA0LC0wLjUzMSAzLjU2MiwtMi43NTIgMy4wMzEsLTQuOTU1
|
|
||||||
WiIvPjxwYXRoIGQ9Ik0zNDkuMzI2LDEyMy41MjdjMCwwIC0xNS4zNSwtMTMuODYyIC0xOC4xMTQs
|
|
||||||
MjIuODM0IiBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojNjFlMGVlO3N0cm9rZS13aWR0aDo2LjE2
|
|
||||||
cHg7Ii8+PHBhdGggZD0iTTI5OS42NjIsMTIzLjQ4NWMtMCwtMCAxMi42NzEsLTE1LjI2NSAyMC4x
|
|
||||||
MTIsMjEuODk4IiBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojNjFlMGVlO3N0cm9rZS13aWR0aDo2
|
|
||||||
LjE2cHg7Ii8+PHBhdGggZD0iTTMzNi4xMTEsMTA4Ljc3M2MtMCwwIC0xNi44MDgsLTkuMDk5IC0x
|
|
||||||
MC44ODIsMzIuMTI0IiBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojNjFlMGVlO3N0cm9rZS13aWR0
|
|
||||||
aDo2LjE2cHg7Ii8+PHBhdGggZD0iTTQxNy43OTYsMTg0LjQ1M2MzLjI4MiwwLjk3IDYuNTgzLC0x
|
|
||||||
LjI5IDguODg5LC0zLjM2MWMyOC4wNTUsLTI1LjE5NCAtMTMuMTk4LC0zMS41MzggLTIwLjY1Niwt
|
|
||||||
MTIuNTU5IiBzdHlsZT0iZmlsbDojNjFlMGVlOyIvPjxwYXRoIGQ9Ik00MTYuNjMyLDE4OC4zOTNj
|
|
||||||
NC42NTcsMS4zNzUgOS41MjYsLTEuMzA3IDEyLjc5NywtNC4yNDVjMTAuMjc5LC05LjIzIDEyLjA0
|
|
||||||
NiwtMTYuODQzIDEwLjQ4OCwtMjEuNzE2Yy0xLjY0NSwtNS4xNSAtNy4xNDIsLTguNTQ0IC0xMy45
|
|
||||||
NzgsLTguOTljLTkuMDExLC0wLjU4NyAtMTkuOTI3LDMuOTA0IC0yMy43MzMsMTMuNTg5Yy0wLjgz
|
|
||||||
LDIuMTEgMC4yMSw0LjQ5NiAyLjMyLDUuMzI1YzIuMTEsMC44MjkgNC40OTYsLTAuMjExIDUuMzI1
|
|
||||||
LC0yLjMyMWMyLjQ2LC02LjI1OSA5LjczLC04Ljc3NSAxNS41NTMsLTguMzk1YzEuODA3LDAuMTE3
|
|
||||||
IDMuNDg5LDAuNTE3IDQuODE3LDEuMjY5YzAuODcxLDAuNDk0IDEuNTg2LDEuMTMgMS44NzEsMi4w
|
|
||||||
MjNjMC4zNzUsMS4xNzIgMC4wNDksMi41ODggLTAuNzk1LDQuMjk4Yy0xLjI2NiwyLjU2MiAtMy42
|
|
||||||
NDksNS40NzcgLTcuMzU2LDguODA2Yy0wLjg2OCwwLjc4IC0xLjkwNywxLjYxOCAtMy4wNTQsMi4x
|
|
||||||
NDdjLTAuNjE2LDAuMjg0IC0xLjI2NSwwLjUyNyAtMS45MjgsMC4zMzFjLTIuMTc0LC0wLjY0MiAt
|
|
||||||
NC40NjEsMC42MDIgLTUuMTAzLDIuNzc2Yy0wLjY0MiwyLjE3NCAwLjYwMiw0LjQ2MSAyLjc3Niw1
|
|
||||||
LjEwM1oiLz48cGF0aCBkPSJNNDM2LjAxNCw0MTkuNjQ1YzMuNjAxLC0xMC45MzQgOS4wMDMsLTMx
|
|
||||||
LjYwOSA1LjY4MiwtNDkuNTYzYy0xNC45NTMsLTgwLjg0NyA3Ljg5NSwtOTguNTQ5IC01LjAzNSwt
|
|
||||||
MTQ3LjU5M2MtNC40OSwtMTcuMDI5IC0xMi4wNDksLTM2LjI1IC0yMy41NTQsLTQ5Ljc5MmMtMjUu
|
|
||||||
Mjg1LC0yOS43NjIgLTcwLjczNiwtNDIuOTEzIC0xMDguNzMsLTMyLjc4NWMtMTUuMjEzLDQuMDU1
|
|
||||||
IC0yOC42NjIsMTEuNzI3IC00MS45MjMsMjAuMDM0Yy00Ni4xMzYsMjguOSAtNTIuMjU5LDgzLjM3
|
|
||||||
NyAtNDIuNzkxLDEyOC4yNDhjNC4xOTEsMTkuODY0IDIzLjY3Myw0OS44OTcgMjIuNTg2LDcwLjE5
|
|
||||||
NGMtMS4zODcsMjUuOTEgLTAuNzY1LDQ0LjQyNiAzLjM3Niw2MC44ODQiIHN0eWxlPSJmaWxsOiM2
|
|
||||||
MWUwZWU7Ii8+PGNsaXBQYXRoIGlkPSJfY2xpcDEiPjxwYXRoIGQ9Ik00MzYuMDE0LDQxOS42NDVj
|
|
||||||
My42MDEsLTEwLjkzNCA5LjAwMywtMzEuNjA5IDUuNjgyLC00OS41NjNjLTE0Ljk1MywtODAuODQ3
|
|
||||||
IDcuODk1LC05OC41NDkgLTUuMDM1LC0xNDcuNTkzYy00LjQ5LC0xNy4wMjkgLTEyLjA0OSwtMzYu
|
|
||||||
MjUgLTIzLjU1NCwtNDkuNzkyYy0yNS4yODUsLTI5Ljc2MiAtNzAuNzM2LC00Mi45MTMgLTEwOC43
|
|
||||||
MywtMzIuNzg1Yy0xNS4yMTMsNC4wNTUgLTI4LjY2MiwxMS43MjcgLTQxLjkyMywyMC4wMzRjLTQ2
|
|
||||||
LjEzNiwyOC45IC01Mi4yNTksODMuMzc3IC00Mi43OTEsMTI4LjI0OGM0LjE5MSwxOS44NjQgMjMu
|
|
||||||
NjczLDQ5Ljg5NyAyMi41ODYsNzAuMTk0Yy0xLjM4NywyNS45MSAtMC43NjUsNDQuNDI2IDMuMzc2
|
|
||||||
LDYwLjg4NCIvPjwvY2xpcFBhdGg+PGcgY2xpcC1wYXRoPSJ1cmwoI19jbGlwMSkiPjxwYXRoIGQ9
|
|
||||||
Ik0zMTEuNzc2LDIzNi44MzVjMCwwIC0zLjA2OCwyNC4xOTkgLTMxLjU5NywyNC44MzhjLTI4LjUz
|
|
||||||
LDAuNjM5IC0zMi41OTEsLTI4LjI0NiAtMzIuNTkxLC0yOC4yNDZsNjQuMTg4LDMuNDA4WiIgc3R5
|
|
||||||
bGU9ImZpbGw6IzRmY2JlMDsiLz48cGF0aCBkPSJNNDMyLjUwOSwyMTYuMDIzYzAsLTAgLTMuMDY3
|
|
||||||
LDI0LjE5OCAtMzEuNTk3LDI0LjgzN2MtMjguNTMsMC42MzkgLTMyLjU5MSwtMjguMjQ2IC0zMi41
|
|
||||||
OTEsLTI4LjI0Nmw2NC4xODgsMy40MDlaIiBzdHlsZT0iZmlsbDojNGZjYmUwOyIvPjxwYXRoIGQ9
|
|
||||||
Ik0yODYuODQ1LDQxOC45NjFsMTI3Ljg4NiwtMGMwLjM4NiwtMS4yNjcgMC42NjYsLTIuNDMyIDAu
|
|
||||||
ODcsLTMuNDcxYzMuODY3LC0xOS43NDMgMy43MjksLTU3LjQ2OCAtMi40MTUsLTc3LjA5NWMtMi4x
|
|
||||||
MzMsLTYuODE2IC01Ljc5MywtMTQuNDkgLTExLjQ3NiwtMTkuODUyYy0xMi40OSwtMTEuNzg1IC02
|
|
||||||
NC4zOTgsLTkuMTgxIC04My41NjEsLTQuODM1Yy03LjY3MywxLjc0MSAtMTQuNDg3LDQuOTIyIC0y
|
|
||||||
MS4yMSw4LjM1OGMtMTQuOTExLDcuNjIgLTIwLjI1Myw1OC43MTIgLTEwLjA5NCw5Ni44OTVaIiBz
|
|
||||||
dHlsZT0iZmlsbDojYjdmOGZmOyIvPjxwYXRoIGQ9Ik0yNDYuNzExLDQxOC45NjFjLTUuMTA4LC0y
|
|
||||||
MC42MTMgLTYuNDksLTQyLjk3NSAtNS41NjIsLTYwLjg1NWMyLjEzNiwtNDEuMTMzIC0zMS4zMTQs
|
|
||||||
LTUwLjQ1MyAtMjUuNDYxLC0xMjEuNTEzYzE0LjUzNCwtNDIuNDA5IDE1LjA4OSwtNDAuMTA3IDI3
|
|
||||||
LjY1MywtNTUuNDQ1YzAsMCAtMTMuMTQ3LDM5Ljk2OCAtMS4xOTMsNzAuNzI4YzExLjk1NCwzMC43
|
|
||||||
NTkgMzIuMTc0LDczLjQxOCAzMi41OSwxMTcuNDkxYzAuMTg2LDE5Ljc3MSAxLjYzNCwzNi4zMSA0
|
|
||||||
LjEzLDQ5LjU5NGwtMzIuMTU3LC0wWiIgc3R5bGU9ImZpbGw6IzRmY2JlMDsiLz48L2c+PHBhdGgg
|
|
||||||
ZD0iTTQzOS45MTUsNDIwLjkzYzMuNzQ4LC0xMS4zOCA5LjI3NiwtMzIuOTA4IDUuODIsLTUxLjU5
|
|
||||||
NWMtOC44NjUsLTQ3LjkzMyAtNC4yNTksLTczLjQwNSAtMS45MzUsLTk2LjU4YzEuNjEzLC0xNi4w
|
|
||||||
OTMgMi4xNTYsLTMxLjEyMiAtMy4xNjcsLTUxLjMxM2MtNC42MzgsLTE3LjU5MSAtMTIuNTExLC0z
|
|
||||||
Ny40MTYgLTI0LjM5NiwtNTEuNDA1Yy0yNi4yNjEsLTMwLjkxMSAtNzMuNDU2LC00NC42MTMgLTEx
|
|
||||||
Mi45MTcsLTM0LjA5NGMtMTUuNjE0LDQuMTYxIC0yOS40MzcsMTEuOTk2IC00My4wNDcsMjAuNTIy
|
|
||||||
Yy00Ny43MzUsMjkuOTAyIC01NC40MjYsODYuMTUgLTQ0LjYyOSwxMzIuNTc3YzIuMTM5LDEwLjEz
|
|
||||||
OCA4LjEzNiwyMi44ODggMTMuNTA0LDM1LjY3NmM0Ljk5NCwxMS45IDkuNTE1LDIzLjgxIDguOTk5
|
|
||||||
LDMzLjQ1Yy0xLjQxNSwyNi40MzIgLTAuNzMsNDUuMzE3IDMuNDk1LDYyLjEwN2wxLjAwMiwzLjk4
|
|
||||||
M2w3Ljk2NywtMi4wMDVsLTEuMDAyLC0zLjk4M2MtNC4wNTgsLTE2LjEyNyAtNC42MTgsLTM0LjI3
|
|
||||||
NCAtMy4yNTgsLTU5LjY2M2MwLjQ2LC04LjYwOSAtMi40NiwtMTguODk1IC02LjU0NSwtMjkuNDU3
|
|
||||||
Yy01LjY5NCwtMTQuNzI0IC0xMy42NDYsLTMwLjA1OSAtMTYuMTI0LC00MS44MDRjLTkuMTQsLTQz
|
|
||||||
LjMxNSAtMy41ODQsLTk2LjAyMiA0MC45NTIsLTEyMy45MTljMTIuOTEyLC04LjA4OSAyNS45ODks
|
|
||||||
LTE1LjU5OCA0MC44MDEsLTE5LjU0N2MzNi41MjYsLTkuNzM1IDgwLjIzNCwyLjg2NSAxMDQuNTQx
|
|
||||||
LDMxLjQ3NmMxMS4xMjYsMTMuMDk2IDE4LjM3MiwzMS43MTMgMjIuNzEzLDQ4LjE4YzYuMzEsMjMu
|
|
||||||
OTMyIDMuODIsNDAuMjE5IDEuNjQ3LDYwLjM4M2MtMi4yNTcsMjAuOTQ5IC00LjI1OSw0NS45Mjcg
|
|
||||||
My4zMjEsODYuOTFjMy4xODYsMTcuMjIyIC0yLjA5LDM3LjA0MyAtNS41NDQsNDcuNTMxbC0xLjI4
|
|
||||||
NSwzLjkwMmw3LjgwMywyLjU2OWwxLjI4NCwtMy45MDFaIi8+PHBhdGggZD0iTTQyNC41MzksMjQy
|
|
||||||
LjI0N2MtMS44NjgsMC4xMzQgLTMuMjc2LDEuNzU4IC0zLjE0MiwzLjYyNmMwLjEzMywxLjg2NyAx
|
|
||||||
Ljc1NywzLjI3NSAzLjYyNSwzLjE0MWMxLjg2NywtMC4xMzMgMy4yNzUsLTEuNzU3IDMuMTQyLC0z
|
|
||||||
LjYyNWMtMC4xMzQsLTEuODY3IC0xLjc1OCwtMy4yNzUgLTMuNjI1LC0zLjE0MloiIHN0eWxlPSJm
|
|
||||||
aWxsOiNiN2Y4ZmY7Ii8+PHBhdGggZD0iTTI1Ni45OTcsMjYwLjM5MWMtMS44NjgsMC4xMzMgLTMu
|
|
||||||
Mjc1LDEuNzU4IC0zLjE0MiwzLjYyNWMwLjEzMywxLjg2OCAxLjc1OCwzLjI3NSAzLjYyNSwzLjE0
|
|
||||||
MmMxLjg2OCwtMC4xMzMgMy4yNzUsLTEuNzU4IDMuMTQyLC0zLjYyNWMtMC4xMzMsLTEuODY4IC0x
|
|
||||||
Ljc1OCwtMy4yNzUgLTMuNjI1LC0zLjE0MloiIHN0eWxlPSJmaWxsOiNiN2Y4ZmY7Ii8+PGc+PHBh
|
|
||||||
dGggZD0iTTM2NC4wOSwyNjkuMjI5Yy0xLjE4Niw0Ljk2NyAtMS40NDMsMTQuMzA2IC00LjMxNSwx
|
|
||||||
OC40Yy0xLjM5MywxLjk4NSAtNC40OTUsMi4wODYgLTYuNTE2LDIuMTUzYy0xMS42MTIsMC4zODMg
|
|
||||||
LTExLjQ0NiwtMTEuNTEzIC0xMy4xODEsLTIxLjg1NSIgc3R5bGU9ImZpbGw6I2VkZWRlZDsiLz48
|
|
||||||
cGF0aCBkPSJNMzYxLjA5NCwyNjguNTEzYy0wLjcxOSwzLjAwOSAtMS4xMDYsNy42MDcgLTEuOTIy
|
|
||||||
LDExLjY5OGMtMC40NDUsMi4yMjggLTAuOTcxLDQuMjk4IC0xLjkxOSw1LjY0OWMtMC4yNjMsMC4z
|
|
||||||
NzYgLTAuNzQ5LDAuNDU5IC0xLjIwMywwLjU2NmMtMC45OTUsMC4yMzMgLTIuMDU2LDAuMjUgLTIu
|
|
||||||
ODkzLDAuMjc3Yy0yLjA1NSwwLjA2OCAtMy42MSwtMC4zMjYgLTQuNzc0LC0xLjE5MmMtMS4xOTMs
|
|
||||||
LTAuODg3IC0xLjk1NCwtMi4yMTIgLTIuNTQ0LC0zLjc0M2MtMS41NjEsLTQuMDQ3IC0xLjg5LC05
|
|
||||||
LjM5IC0yLjcyMywtMTQuMzUxYy0wLjI4MSwtMS42NzcgLTEuODcxLC0yLjgxIC0zLjU0OCwtMi41
|
|
||||||
MjhjLTEuNjc3LDAuMjgxIC0yLjgwOSwxLjg3MSAtMi41MjgsMy41NDhjMC45MDMsNS4zODEgMS4z
|
|
||||||
NTgsMTEuMTU4IDMuMDUxLDE1LjU0OGMxLjAzNiwyLjY4OCAyLjUyLDQuOTEyIDQuNjE1LDYuNDdj
|
|
||||||
Mi4xMjUsMS41ODEgNC45MDMsMi41MyA4LjY1NCwyLjQwNmMxLjQ2OCwtMC4wNDggMy40LC0wLjE1
|
|
||||||
MiA1LjA3OSwtMC43MTRjMS41NiwtMC41MjIgMi45MTksLTEuNDEgMy44NTgsLTIuNzQ5YzEuMzUs
|
|
||||||
LTEuOTI0IDIuMjg0LC00LjgwOCAyLjkxNywtNy45ODFjMC44LC00LjAxMSAxLjE2OCwtOC41MjIg
|
|
||||||
MS44NzMsLTExLjQ3M2MwLjM5NSwtMS42NTMgLTAuNjI3LC0zLjMxNyAtMi4yODEsLTMuNzEyYy0x
|
|
||||||
LjY1NCwtMC4zOTUgLTMuMzE3LDAuNjI3IC0zLjcxMiwyLjI4MVoiLz48cGF0aCBkPSJNMzQ5LjAw
|
|
||||||
MSwyMzQuMDVjLTkuMzMyLDEuMjQ2IC0xNi4yNjUsNy4wNzUgLTE1LjQ3MywxMy4wMDhjMC43OTIs
|
|
||||||
NS45MzMgOS4wMTIsOS43MzggMTguMzQ0LDguNDkyYzkuMzMzLC0xLjI0NiAxNi4yNjYsLTcuMDc1
|
|
||||||
IDE1LjQ3NCwtMTMuMDA4Yy0wLjc5MiwtNS45MzMgLTkuMDEyLC05LjczOCAtMTguMzQ1LC04LjQ5
|
|
||||||
MloiLz48cGF0aCBkPSJNMzQ4LjkwMSwyNTIuNTVjMC40Niw1LjIwNCAxLjY5NCwxMC4xNDIgMi40
|
|
||||||
NTMsMTUuMjc3YzAuMjQ4LDEuNjgyIDEuODE1LDIuODQ2IDMuNDk3LDIuNTk4YzEuNjgyLC0wLjI0
|
|
||||||
OCAyLjg0NiwtMS44MTYgMi41OTgsLTMuNDk4Yy0wLjc0MSwtNS4wMTUgLTEuOTYxLC05LjgzNyAt
|
|
||||||
Mi40MTEsLTE0LjkyYy0wLjE1LC0xLjY5NCAtMS42NDcsLTIuOTQ3IC0zLjM0LC0yLjc5N2MtMS42
|
|
||||||
OTQsMC4xNSAtMi45NDcsMS42NDcgLTIuNzk3LDMuMzRaIi8+PHBhdGggZD0iTTM3NC4yNiwyNTku
|
|
||||||
ODE0Yy04LjAyMiw1LjM1IC0xNS45MDEsNy44MjUgLTIzLjU5NCw3LjI3NGMtNy42OTQsLTAuNTUx
|
|
||||||
IC0xNS4xNDgsLTQuMTI0IC0yMi4zNDIsLTEwLjU4OWMtMS4yNjUsLTEuMTM2IC0zLjIxNCwtMS4w
|
|
||||||
MzIgLTQuMzUxLDAuMjMzYy0xLjEzNiwxLjI2NCAtMS4wMzIsMy4yMTQgMC4yMzMsNC4zNWM4LjM1
|
|
||||||
Niw3LjUwOCAxNy4wODQsMTEuNTExIDI2LjAyLDEyLjE1MWM4LjkzNiwwLjY0IDE4LjEzNSwtMi4w
|
|
||||||
NzggMjcuNDUzLC04LjI5NGMxLjQxNCwtMC45NDMgMS43OTcsLTIuODU4IDAuODUzLC00LjI3MmMt
|
|
||||||
MC45NDMsLTEuNDE1IC0yLjg1OCwtMS43OTcgLTQuMjcyLC0wLjg1M1oiLz48cGF0aCBkPSJNMzc4
|
|
||||||
LjYwNSwxNTIuMTU2Yy0zLjY0NywtMC44MjYgLTYuOTU2LDAuMDM4IC03LjM4NSwxLjkyOWMtMC40
|
|
||||||
MjgsMS44OTEgMi4xODUsNC4wOTcgNS44MzIsNC45MjRjMy42NDcsMC44MjcgNi45NTYsLTAuMDM3
|
|
||||||
IDcuMzg1LC0xLjkyOGMwLjQyOCwtMS44OTEgLTIuMTg1LC00LjA5OCAtNS44MzIsLTQuOTI1WiIg
|
|
||||||
c3R5bGU9ImZpbGw6I2I3ZjhmZjsiLz48cGF0aCBkPSJNMjQ5LjUsMTc3LjI3Yy0yLjE5NSwzLjAy
|
|
||||||
OCAtMi43MDIsNi40MSAtMS4xMzIsNy41NDhjMS41NywxLjEzOCA0LjYyNiwtMC4zOTYgNi44MjEs
|
|
||||||
LTMuNDI0YzIuMTk1LC0zLjAyOCAyLjcwMiwtNi40MSAxLjEzMiwtNy41NDhjLTEuNTcsLTEuMTM4
|
|
||||||
IC00LjYyNiwwLjM5NiAtNi44MjEsMy40MjRaIiBzdHlsZT0iZmlsbDojYjdmOGZmOyIvPjxwYXRo
|
|
||||||
IGQ9Ik00MDMuNjQ4LDE2Mi44MjVjLTE5LjIxMywtMS4zNSAtMzUuOTA3LDEzLjE1MSAtMzcuMjU3
|
|
||||||
LDMyLjM2NGMtMS4zNSwxOS4yMTIgMTMuMTUyLDM1LjkwNiAzMi4zNjQsMzcuMjU2YzE5LjIxMywx
|
|
||||||
LjM1MSAzNS45MDcsLTEzLjE1MSAzNy4yNTcsLTMyLjM2NGMxLjM1LC0xOS4yMTIgLTEzLjE1Miwt
|
|
||||||
MzUuOTA2IC0zMi4zNjQsLTM3LjI1NloiIHN0eWxlPSJmaWxsOiNmZmY7c3Ryb2tlOiMwMDA7c3Ry
|
|
||||||
b2tlLXdpZHRoOjYuMTZweDsiLz48cGF0aCBkPSJNMjgxLjY5NiwxODEuNzhjLTE5LjIxMiwtMS4z
|
|
||||||
NSAtMzUuOTA2LDEzLjE1MiAtMzcuMjU3LDMyLjM2NGMtMS4zNSwxOS4yMTIgMTMuMTUyLDM1Ljkw
|
|
||||||
NyAzMi4zNjUsMzcuMjU3YzE5LjIxMiwxLjM1IDM1LjkwNiwtMTMuMTUyIDM3LjI1NiwtMzIuMzY0
|
|
||||||
YzEuMzUsLTE5LjIxMyAtMTMuMTUyLC0zNS45MDcgLTMyLjM2NCwtMzcuMjU3WiIgc3R5bGU9ImZp
|
|
||||||
bGw6I2ZmZjtzdHJva2U6IzAwMDtzdHJva2Utd2lkdGg6Ni4xNnB4OyIvPjxwYXRoIGQ9Ik0zOTgu
|
|
||||||
NDk5LDE5Mi44ODFjLTYuODI0LC0wLjQ3OSAtMTIuNzU0LDQuNjcyIC0xMy4yMzMsMTEuNDk2Yy0w
|
|
||||||
LjQ4LDYuODI0IDQuNjcxLDEyLjc1MyAxMS40OTUsMTMuMjMzYzYuODI0LDAuNDc5IDEyLjc1NCwt
|
|
||||||
NC42NzIgMTMuMjMzLC0xMS40OTZjMC40OCwtNi44MjQgLTQuNjcxLC0xMi43NTMgLTExLjQ5NSwt
|
|
||||||
MTMuMjMzWiIvPjxwYXRoIGQ9Ik0yNzguMTQyLDIxMC4yMjdjLTYuODI0LC0wLjQ3OSAtMTIuNzU0
|
|
||||||
LDQuNjcxIC0xMy4yMzMsMTEuNDk2Yy0wLjQ4LDYuODI0IDQuNjcxLDEyLjc1MyAxMS40OTUsMTMu
|
|
||||||
MjMzYzYuODI0LDAuNDc5IDEyLjc1NCwtNC42NzIgMTMuMjMzLC0xMS40OTZjMC40OCwtNi44MjQg
|
|
||||||
LTQuNjcxLC0xMi43NTMgLTExLjQ5NSwtMTMuMjMzWiIvPjxwYXRoIGQ9Ik0zOTkuODkyLDE5Ny45
|
|
||||||
NjFjLTEuOTE2LC0wLjEzNSAtMy41ODEsMS4zMTEgLTMuNzE2LDMuMjI3Yy0wLjEzNCwxLjkxNiAx
|
|
||||||
LjMxMiwzLjU4MSAzLjIyOCwzLjcxNmMxLjkxNiwwLjEzNCAzLjU4MSwtMS4zMTIgMy43MTUsLTMu
|
|
||||||
MjI4YzAuMTM1LC0xLjkxNiAtMS4zMTEsLTMuNTgxIC0zLjIyNywtMy43MTVaIiBzdHlsZT0iZmls
|
|
||||||
bDojZmZmOyIvPjxwYXRoIGQ9Ik0yNzkuNTM1LDIxNS4zMDZjLTEuOTE2LC0wLjEzNCAtMy41ODEs
|
|
||||||
MS4zMTIgLTMuNzE1LDMuMjI4Yy0wLjEzNSwxLjkxNiAxLjMxMSwzLjU4MSAzLjIyNywzLjcxNmMx
|
|
||||||
LjkxNiwwLjEzNCAzLjU4MSwtMS4zMTIgMy43MTYsLTMuMjI4YzAuMTM0LC0xLjkxNiAtMS4zMTIs
|
|
||||||
LTMuNTgxIC0zLjIyOCwtMy43MTZaIiBzdHlsZT0iZmlsbDojZmZmOyIvPjwvZz48L2c+PHBhdGgg
|
|
||||||
ZD0iTTUxMi4yOTEsMzMzLjcyNmMwLC0wIC0yNy40OTYsMTMuNDAyIC00My4yNDUsMy43OTJjLTE1
|
|
||||||
Ljc1LC05LjYxIC0xMC44LC0yNC43ODggMy4xMDIsLTIyLjUyYzEzLjkwMywyLjI2OCAyNy4xNjgs
|
|
||||||
My4xOTUgMzUuOTQzLC0zLjkzN2M4Ljc3NiwtNy4xMzIgMjEuNjMzLDE0LjM3NiA0LjIsMjIuNjY1
|
|
||||||
WiIgc3R5bGU9ImZpbGw6I2Q2YzI5NTtzdHJva2U6IzAwMDtzdHJva2Utd2lkdGg6Ni4xNnB4OyIv
|
|
||||||
PjwvZz48Zz48cGF0aCBkPSJNNDQxLjI2OCwyOTEuMzQ5Yy0wLDAgMjguMDgsNDcuNDg4IC0xMjIu
|
|
||||||
NTkxLDExNC4wNGMtMTEuNjI5LC0yLjAxNyAtMTQuMzMsLTguNTE1IC0xNC4zMywtOC41MTVjMCwt
|
|
||||||
MCA0My42MTcsLTEyLjU4NiA4My44MTIsLTQ0Ljg3NmM0My41OTcsLTM1LjAyNCA1My4xMDksLTYw
|
|
||||||
LjY0OSA1My4xMDksLTYwLjY0OVoiIHN0eWxlPSJmaWxsOiM3YjQ3MjI7c3Ryb2tlOiMwMDA7c3Ry
|
|
||||||
b2tlLXdpZHRoOjYuNjhweDsiLz48cGF0aCBkPSJNMjMzLjk1NywzNzIuMTM4YzMuNTYsLTUuNTA2
|
|
||||||
IDEwLjcyOSwtNy4zOTIgMTYuNTM4LC00LjM1MWMxNy43NjcsOS4yMzUgNTMuNjE0LDI4LjAzMiA2
|
|
||||||
Ny4xNTQsMzYuMzc2YzUuOTcxLDMuNjggLTYuNDc3LDE2LjkxMyAtMTkuMjgzLDM3LjkyN2MtMy4z
|
|
||||||
NjgsNS41NjcgLTEwLjQzNiw3LjYyOCAtMTYuMjY5LDQuNzQ2Yy0yMS42NTYsLTEwLjcxNiAtNDEu
|
|
||||||
MDQ3LC0yMC45NTggLTU0LjIzNCwtMjguMDczYy00LjU5MiwtMi41MDEgLTcuOTM0LC02LjgwMiAt
|
|
||||||
OS4yMjMsLTExLjg2OWMtMS4yODgsLTUuMDY4IC0wLjQwNywtMTAuNDQzIDIuNDMzLC0xNC44MzNj
|
|
||||||
NC4zMzUsLTYuNzA4IDkuMTYyLC0xNC4xNyAxMi44ODQsLTE5LjkyM1oiIHN0eWxlPSJmaWxsOiM3
|
|
||||||
YjQ3MjI7c3Ryb2tlOiMwMDA7c3Ryb2tlLXdpZHRoOjQuOTVweDsiLz48cGF0aCBkPSJNMjM5Ljg1
|
|
||||||
LDM2MS4xNzFsLTI1Ljg0OCwyMy4yOTZsNzkuMTcsNDUuNjA1bDI1LjE5NiwtMjQuMjk1bC03OC41
|
|
||||||
MTgsLTQ0LjYwNloiIHN0eWxlPSJmaWxsOiNhZDZjNGE7c3Ryb2tlOiMwMDA7c3Ryb2tlLXdpZHRo
|
|
||||||
OjQuOTVweDsiLz48cGF0aCBkPSJNMjM4LjA4MSwzNzcuNzA0YzIuNTI2LDAuNTMgNC4xNDcsMy4w
|
|
||||||
MTEgMy42MTgsNS41MzdjLTAuNTMsMi41MjYgLTMuMDExLDQuMTQ3IC01LjUzOCwzLjYxOGMtMi41
|
|
||||||
MjYsLTAuNTMgLTQuMTQ3LC0zLjAxMSAtMy42MTcsLTUuNTM4YzAuNTMsLTIuNTI2IDMuMDExLC00
|
|
||||||
LjE0NyA1LjUzNywtMy42MTdaIiBzdHlsZT0iZmlsbDojZmZiMjVjO3N0cm9rZTojMDAwO3N0cm9r
|
|
||||||
ZS13aWR0aDo0Ljk1cHg7Ii8+PHBhdGggZD0iTTI5MC4zMyw0MDcuOWMyLjUyNywwLjUzIDQuMTQ3
|
|
||||||
LDMuMDExIDMuNjE4LDUuNTM3Yy0wLjUzLDIuNTI3IC0zLjAxMSw0LjE0NyAtNS41MzcsMy42MThj
|
|
||||||
LTIuNTI3LC0wLjUzIC00LjE0OCwtMy4wMTEgLTMuNjE4LC01LjUzN2MwLjUzLC0yLjUyNyAzLjAx
|
|
||||||
MSwtNC4xNDggNS41MzcsLTMuNjE4WiIgc3R5bGU9ImZpbGw6I2ZmYjI1YztzdHJva2U6IzAwMDtz
|
|
||||||
dHJva2Utd2lkdGg6NC45NXB4OyIvPjwvZz48Zz48cGF0aCBkPSJNMzA1LjMyMywzMzYuMjM1YzAs
|
|
||||||
MCAyNC4yMDQsMTEyLjU3OCAzNS4zNTcsMTE0LjM1NWMxMS4xNTMsMS43NzYgMTgyLjk5OCwtNTcu
|
|
||||||
MTYyIDE4Mi45OTgsLTU3LjE2MmMwLC0wIC0yNC4yNywtMTA1Ljc3MyAtMzQuNDU2LC0xMTQuNjAy
|
|
||||||
Yy0xMC4xODYsLTguODMgLTE4My44OTksNTcuNDA5IC0xODMuODk5LDU3LjQwOVoiIHN0eWxlPSJm
|
|
||||||
aWxsOiNmZmYyZWI7c3Ryb2tlOiMwMDA7c3Ryb2tlLXdpZHRoOjQuNjhweDsiLz48cGF0aCBkPSJN
|
|
||||||
NDEzLjU2MywzNjcuNjU2bC0xMDguMjQsLTMxLjQyMWwxMDguMjQsMzEuNDIxWiIgc3R5bGU9ImZp
|
|
||||||
bGw6bm9uZTtzdHJva2U6IzAwMDtzdHJva2Utd2lkdGg6NS40NnB4OyIvPjxwYXRoIGQ9Ik00ODgu
|
|
||||||
NTY2LDI3OS4xNTZsLTc1LjAwMyw4OC41IiBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMDAwO3N0
|
|
||||||
cm9rZS13aWR0aDo1LjQ2cHg7Ii8+PHBhdGggZD0iTTMzOS4xNjcsNDQ5LjEyN2w2Mi40MDEsLTgz
|
|
||||||
LjUwNSIgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2U6IzAwMDtzdHJva2Utd2lkdGg6NS40NnB4OyIv
|
|
||||||
PjxwYXRoIGQ9Ik00MjMuMjcsMzU1LjgzNWw5OS4xOTQsMzcuMDAxIiBzdHlsZT0iZmlsbDpub25l
|
|
||||||
O3N0cm9rZTojMDAwO3N0cm9rZS13aWR0aDo1LjQ2cHg7Ii8+PC9nPjxwYXRoIGQ9Ik0zMDQuNDE2
|
|
||||||
LDM3NC43ODVjLTAsMCAtMzAuNDYsLTIuOCAtMzguOTQzLC0xOS4xODVjLTguNDgyLC0xNi4zODQg
|
|
||||||
My42MjIsLTI2Ljc5NCAxNC4zMzIsLTE3LjY0NmMxMC43MTEsOS4xNDkgMjEuNTcyLDE2LjgyMSAz
|
|
||||||
Mi43NzQsMTUuMjc0YzExLjIwMSwtMS41NDcgMTEuMDQsMjMuNTExIC04LjE2MywyMS41NTdaIiBz
|
|
||||||
dHlsZT0iZmlsbDojZDZjMjk1O3N0cm9rZTojMDAwO3N0cm9rZS13aWR0aDo2LjE2cHg7Ii8+PHBh
|
|
||||||
dGggZD0iTTEwOS45NTEsMzExLjcxMmw2OC4wNzYsLTAiIHN0eWxlPSJmaWxsOm5vbmU7c3Ryb2tl
|
|
||||||
OiMwMDA7c3Ryb2tlLXdpZHRoOjQuOTVweDsiLz48cGF0aCBkPSJNMTM5LjMwNSwzNTEuNjgzbDY4
|
|
||||||
LjA3NiwwIiBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMDAwO3N0cm9rZS13aWR0aDo0Ljk1cHg7
|
|
||||||
Ii8+PHBhdGggZD0iTTEzOS42MTcsMzMxLjY5OGwzOC4wOTgsLTAiIHN0eWxlPSJmaWxsOm5vbmU7
|
|
||||||
c3Ryb2tlOiMwMDA7c3Ryb2tlLXdpZHRoOjQuOTVweDsiLz48cGF0aCBkPSJNODUuMjAxLDIxMS4y
|
|
||||||
NTNjLTAsLTIuMjEzIC0xLjc5NywtNC4wMSAtNC4wMSwtNC4wMWwtNTMuNDg3LC0wYy0yLjIxNCwt
|
|
||||||
MCAtNC4wMTEsMS43OTcgLTQuMDExLDQuMDFsMCw1OC44OTRjMCwyLjIxMyAxLjc5Nyw0LjAxIDQu
|
|
||||||
MDExLDQuMDFsNTMuNDg3LDBjMi4yMTMsMCA0LjAxLC0xLjc5NyA0LjAxLC00LjAxbC0wLC01OC44
|
|
||||||
OTRaIiBzdHlsZT0iZmlsbDojZWVhYzAxO3N0cm9rZTojMDAwO3N0cm9rZS13aWR0aDozLjQzcHg7
|
|
||||||
Ii8+PHBhdGggZD0iTTU2NC42NDMsNDI2LjU5MWMwLC0yLjIxMyAtMS43OTYsLTQuMDEgLTQuMDEs
|
|
||||||
LTQuMDFsLTUzLjQ4NywtMGMtMi4yMTMsLTAgLTQuMDEsMS43OTcgLTQuMDEsNC4wMWwwLDU4Ljg5
|
|
||||||
NGMwLDIuMjEzIDEuNzk3LDQuMDEgNC4wMSw0LjAxbDUzLjQ4NywwYzIuMjE0LDAgNC4wMSwtMS43
|
|
||||||
OTcgNC4wMSwtNC4wMWwwLC01OC44OTRaIiBzdHlsZT0iZmlsbDojZWVhYzAxO3N0cm9rZTojMDAw
|
|
||||||
O3N0cm9rZS13aWR0aDozLjQzcHg7Ii8+PGNpcmNsZSBjeD0iNTE3LjAxNCIgY3k9IjEwOC43NzEi
|
|
||||||
IHI9IjQuMzUyIiBzdHlsZT0iZmlsbDojZDNkMmIzOyIvPjxjaXJjbGUgY3g9IjUzMS4zMiIgY3k9
|
|
||||||
IjEwOC43NzEiIHI9IjQuMzUyIiBzdHlsZT0iZmlsbDojZjRiMzAzOyIvPjxjaXJjbGUgY3g9IjU0
|
|
||||||
NC43OTIiIGN5PSIxMDguNzcxIiByPSI0LjM1MiIgc3R5bGU9ImZpbGw6I2Y5NjEzZDsiLz48cGF0
|
|
||||||
aCBkPSJNMTA1LjI1Nyw0ODQuNDNjLTAsMCAyMC4zNzQsLTI1LjIyNSA0MS43MTksLTcuNzYxYzIx
|
|
||||||
LjM0NCwxNy40NjMgLTEyLjYxMyw1MC40NSAtNDAuNzQ5LDQyLjY4OSIgc3R5bGU9ImZpbGw6bm9u
|
|
||||||
ZTtzdHJva2U6IzAwMDtzdHJva2Utd2lkdGg6OS45cHg7Ii8+PHBhdGggZD0iTTExMy4wMTksNDYw
|
|
||||||
LjE0MWMtMCwtNC4yNjUgLTMuNDYzLC03LjcyNyAtNy43MjgsLTcuNzI3bC03OC42NTUsLTBjLTQu
|
|
||||||
MjY1LC0wIC03LjcyNywzLjQ2MiAtNy43MjcsNy43MjdsLTAsNzguNjU1Yy0wLDQuMjY1IDMuNDYy
|
|
||||||
LDcuNzI4IDcuNzI3LDcuNzI4bDc4LjY1NSwtMGM0LjI2NSwtMCA3LjcyOCwtMy40NjMgNy43Mjgs
|
|
||||||
LTcuNzI4bC0wLC03OC42NTVaIiBzdHlsZT0iZmlsbDojNjFlMGVlO3N0cm9rZTojMDAwO3N0cm9r
|
|
||||||
ZS13aWR0aDo0LjU1cHg7c3Ryb2tlLWxpbmVjYXA6c3F1YXJlOyIvPjxwYXRoIGQ9Ik03Ny4yNzcs
|
|
||||||
Mzk1LjQwMmMtMi4yMzQsOC43NTUgLTYuNDk2LDE1LjM2NiAtMTEuNjQ5LDE3LjcxNGMxLjk5NSwz
|
|
||||||
LjA1NyAzLjE2Nyw2Ljc4MSAzLjE2NywxMC43OTdjMCwxMC4zODcgLTcuODQsMTguODIgLTE3LjQ5
|
|
||||||
OCwxOC44MmMtOS42NTcsMCAtMTcuNDk4LC04LjQzMyAtMTcuNDk4LC0xOC44MmMwLC05LjczMyA2
|
|
||||||
Ljg4NCwtMTcuNzUxIDE1LjY5NywtMTguNzIyYy0zLjM5OSwtNS45NiAtNS41MjQsLTE0LjQyMyAt
|
|
||||||
NS41MjQsLTIzLjgwNWMwLC0xOC4wMjQgNy44NDEsLTMyLjY1NyAxNy40OTksLTMyLjY1N2M2Ljc5
|
|
||||||
MiwtMCAxMi42ODUsNy4yMzggMTUuNTg0LDE3LjgwMmMxLjYyOSwtMC41OTIgMy4zODcsLTAuOTE1
|
|
||||||
IDUuMjIsLTAuOTE1YzguNDUsMCAxNS4zMTEsNi44NjEgMTUuMzExLDE1LjMxMWMtMCw4LjQ1IC02
|
|
||||||
Ljg2MSwxNS4zMTEgLTE1LjMxMSwxNS4zMTFjLTEuNzUsMCAtMy40MzIsLTAuMjk0IC00Ljk5OCwt
|
|
||||||
MC44MzZaIiBzdHlsZT0iZmlsbDojZmZmO2ZpbGwtb3BhY2l0eTowLjU7Ii8+PHBhdGggZD0iTTMz
|
|
||||||
LjU4MywyMzMuOTNjMi41MjksLTAuMzM3IDQuNTEsLTEuMTc4IDYuNzE2LC0yLjQwM2MwLjk1Nywt
|
|
||||||
MC41MzIgMS45NjksLTAuOTA0IDIuODk4LC0xLjQ4NWMwLjc4OSwtMC40OTMgMS4zNDMsLTAuOTc2
|
|
||||||
IDEuMzQzLC0xLjk3OWMwLC0wLjIzNiAwLjIyNCwtMC42MzIgMCwtMC43MDdjLTAuOTUyLC0wLjMx
|
|
||||||
NyAtMi4zNjQsMC40NjUgLTMuMDQsMC45OWMtMC42MjUsMC40ODYgLTEuNDk0LDAuNTc5IC0yLjEy
|
|
||||||
LDEuMDZjLTEuMDM2LDAuNzk2IC0xLjY4MiwyLjQ2MyAtMS43NjcsMy43NDZjLTAuMDYsMC44OTYg
|
|
||||||
LTAuMzU3LDIuODQgMC41NjUsMy4zOTNjMC4zNTcsMC4yMTQgMC45MjksMC4xNDIgMS4zNDMsMC4x
|
|
||||||
NDJjMS4wOCwtMCAyLjEwNCwwLjAxMiAzLjE4MSwtMC4xNDJjMi4wODIsLTAuMjk3IDQuMTY1LC0x
|
|
||||||
LjQ1NSA2LjAwOSwtMi40MDNjMi4wMTQsLTEuMDM2IDQuMDEyLC0xLjkyMSA1LjU4NCwtMy42MDVj
|
|
||||||
MC42NDIsLTAuNjg4IDEuMDEsLTEuMjg2IDEuMjcyLC0yLjE5MWMwLjA1OSwtMC4yMDMgLTAuMDYs
|
|
||||||
LTAuNzE4IDAuMDcxLC0wLjg0OWMwLjEwOCwtMC4xMDggMC4wOTksMC4yMjYgMC4wNzEsMC4yODNj
|
|
||||||
LTAuMTg1LDAuMzY3IC0wLjQ2MiwwLjc0IC0wLjYzNywxLjEzMWMtMC4zMjQsMC43MjkgLTAuMzUz
|
|
||||||
LDEuNzU5IC0wLjM1MywyLjU0NWMwLDAuMTMyIC0wLjA3LDAuNTcxIDAsMC42MzZjMC4wNTUsMC4w
|
|
||||||
NTEgMC4zNjMsMC4xOTMgMC40MjQsMC4yMTJjMi4wMDIsMC42MzMgNC4zNDIsLTAuNTMyIDYuMDA5
|
|
||||||
LC0xLjQ4NGMwLjY0MSwtMC4zNjcgMS4zNjMsLTAuODUgMi4wNiwtMS4xMTJjMC4wMjYsLTAuMDEg
|
|
||||||
MC42NzgsLTAuMTk3IDAuNjk2LC0wLjE2MWMwLjI4MiwwLjU2MyAwLjExMSwxLjUxMSAwLjIxMiwy
|
|
||||||
LjEyMWMwLjI4NSwxLjcwNyAyLjM2NywxLjYxNSAzLjY3NiwxLjQ4NGMwLjIwNywtMC4wMiAwLjM2
|
|
||||||
MywtMC4yMTIgMC41NjYsLTAuMjEyIiBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMDAwO3N0cm9r
|
|
||||||
ZS13aWR0aDoyLjc1cHg7Ii8+PHBhdGggZD0iTTM2LjQ4MiwyNDIuMzQyYzEuMzQ2LC0wIDIuNjgy
|
|
||||||
LC0wLjE1NCA0LjAyOSwtMC4yMTJjNC4zMzIsLTAuMTg5IDguNjcxLC0wLjE5NiAxMy4wMDYsLTAu
|
|
||||||
MjgzYzYuNTI1LC0wLjEzMSAxMy4wNTYsMC4wNzEgMTkuNTgxLDAuMDcxIiBzdHlsZT0iZmlsbDpu
|
|
||||||
b25lO3N0cm9rZTojMDAwO3N0cm9rZS13aWR0aDoyLjc1cHg7Ii8+PHBhdGggZD0iTTUxNi4wOSw0
|
|
||||||
NDIuMDMxYy0wLDAuOTM2IC0wLjUzMiwxLjg5MiAtMC4xMjYsMi44MzljMC41MjYsMS4yMjggMi4z
|
|
||||||
NjMsMS4yNzUgMy40NjksMC45NDZjMC42MjgsLTAuMTg3IDEuMzE2LC0xLjE3NiAxLjcwMywtMS42
|
|
||||||
NGMxLjAzOCwtMS4yNDcgMi40MzUsLTIuMzc1IDIuNzEyLC00LjAzN2MwLjA0MSwtMC4yNDggMC4x
|
|
||||||
OTQsLTAuOTM2IDAuMDYzLC0xLjE5OGMtMC43NjMsLTEuNTI1IC0zLjA1OSwtMS40NzggLTQuMTY4
|
|
||||||
LC0wLjM2OWMtMC4xNjQsMC4xNjQgLTAuNDQ2LDAuMjI3IC0wLjU2MywwLjQzMmMtMC45OTQsMS43
|
|
||||||
NDUgLTAuNDM5LDQuMzQxIDAuNDQyLDUuOTkyYzAuMzA0LDAuNTY5IDAuODI5LDEuMDQ4IDEuMzI0
|
|
||||||
LDEuNDUxYzAuMzM5LDAuMjc1IDAuNzc1LDAuNzE2IDEuMTk5LDAuOTI4YzMuMjYyLDEuNjMxIDcu
|
|
||||||
Njg5LC0wLjEzMyAxMC41OTYsLTEuNzQ4YzEuOTUyLC0xLjA4NSA0LjEsLTIuMTUyIDQuMSwtNC42
|
|
||||||
NjhjLTAsLTAuMzI1IDAuMDM3LC0wLjY5NCAtMC4wNjMsLTEuMDA5Yy0wLjM2MSwtMS4xMzMgLTIu
|
|
||||||
MzA3LC0xLjA0MyAtMy4xNTQsLTAuNjk0Yy0xLjE3OSwwLjQ4NiAtMi4zNzMsMS4zOTUgLTMuMDks
|
|
||||||
Mi40NmMtMS4xNzQsMS43NDIgLTEuMjA3LDQuMjgzIDAuNTY3LDUuNjE0YzEuMjU2LDAuOTQxIDIu
|
|
||||||
OTU4LDAuOTIzIDQuNDQ3LDAuNzY2YzIuMjQ3LC0wLjIzNyA0LjI1MywtMS4zMTMgNi4wMjMsLTIu
|
|
||||||
NjU5YzAuODc1LC0wLjY2NCAyLjA5NCwtMS4zMTYgMi42NDksLTIuMzMzYzAuMTczLC0wLjMxNiAw
|
|
||||||
LjIxNSwtMC43NDUgMC4zNzksLTEuMDczYzAuMDcxLC0wLjE0MyAtMC4xNTgsMC4yODUgLTAuMTg5
|
|
||||||
LDAuNDQyYy0wLjA1NiwwLjI3NiAtMC4wOTQsMC44NzggLTAsMS4xMzVjMC4zNzYsMS4wMjggMS4y
|
|
||||||
MzMsMS41MDkgMi4yNywxLjcwM2MxLjM0NiwwLjI1MiAzLjAwOSwtMC4wMSA0LjM1MiwtMC4xODlj
|
|
||||||
MC45MzksLTAuMTI1IDEuODkyLC0wLjUxNyAxLjg5MiwwLjc1NyIgc3R5bGU9ImZpbGw6bm9uZTtz
|
|
||||||
dHJva2U6IzAwMDtzdHJva2Utd2lkdGg6Mi40NXB4OyIvPjxwYXRoIGQ9Ik01MTQuMjU5LDQ2MC4z
|
|
||||||
NTljMC4yNjYsMC4xMzQgMC4wOCwxLjM1NCAwLjU2NywxLjM4OGMwLjQ0MiwwLjAzMSAwLjg4NSww
|
|
||||||
LjA0IDEuMzI4LDAuMDVjMC41MTcsMC4wMTIgMS4xMDQsMC4wOTggMS42MzMsLTAuMDM0YzEuNjg4
|
|
||||||
LC0wLjQyMiAzLjc5NiwtMS41OTggNC40ODIsLTMuMjk2YzAuMTg4LC0wLjQ2NyAwLjI0NiwtMS4w
|
|
||||||
OTQgMC4zMTUsLTEuNTc3YzAuMDc1LC0wLjUyNCAwLjM3OCwtMS43MTggLTAuMDYzLC0yLjE0NGMt
|
|
||||||
MC4wODgsLTAuMDg1IC0wLjYwNCwtMC4wNjMgLTAuNjk0LC0wLjA2M2MtMC42OTUsLTAgLTEuNzk2
|
|
||||||
LDAuMzkyIC0yLjIwNywxLjAwOWMtMC4yNzMsMC40MDkgLTAuMTg5LDAuODUxIC0wLjE4OSwxLjMy
|
|
||||||
NGMtMCwwLjYyIC0wLjAzMywxLjIyNCAwLjEyNiwxLjgzYzAuMzYyLDEuMzc5IDIuMTExLDEuOTQx
|
|
||||||
IDMuMzQzLDIuMDE4YzIuNjk2LDAuMTY4IDUuNDQ3LC0wLjI0NSA4LjEzNiwtMGMyLjIwNywwLjIg
|
|
||||||
NC40NTQsMC43MTcgNi42ODYsMC41MDRjMS41OCwtMC4xNSAzLjE1NCwtMC41MjUgNC43MywtMC42
|
|
||||||
M2MzLjM5NCwtMC4yMjYgNi43OTEsLTAuNTA1IDEwLjIxOCwtMC41MDUiIHN0eWxlPSJmaWxsOm5v
|
|
||||||
bmU7c3Ryb2tlOiMwMDA7c3Ryb2tlLXdpZHRoOjIuNDVweDsiLz48cGF0aCBkPSJNNTE5LjA1NCw0
|
|
||||||
NzEuNjhsMCwwLjk0NmMwLDAuMTU1IC0wLDAuMjUxIDAuMDYzLDAuMzc5bDAsMC4xMjZjMC42MDIs
|
|
||||||
LTIuNDA3IDMuNzc1LC0yLjc4NCA1LjgxLC0yLjc4NGMwLjE1LC0wIDAuMjMyLDAuNDggMC4yNDUs
|
|
||||||
MC41NzZjMC4wNTUsMC4zODUgLTAuMTkyLDEuNDUgMC4zNzksMS42NGMwLjk4OSwwLjMzIDIuNDA2
|
|
||||||
LC0wLjMxOCAzLjM0MywtMC41NjhjMC4yMzUsLTAuMDYyIDEuMDA5LC0wLjQ0MSAxLjE5OCwtMC4y
|
|
||||||
NTJjMC45NTQsMC45NTQgMi40MTIsMC41NjIgMy43MjEsMC42MzFjMy4zMzIsMC4xNzUgNi42Mjks
|
|
||||||
LTAuMTg5IDkuOTY2LC0wLjE4OWMxLjA1OCwtMCAyLjE2MiwwLjA4NyAzLjIxNywtMGMwLjA2OCwt
|
|
||||||
MC4wMDYgMS4wMDksLTAuMDI3IDEuMDA5LC0wLjI1MyIgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2U6
|
|
||||||
IzAwMDtzdHJva2Utd2lkdGg6Mi40NXB4OyIvPjwvZz48Zz48cGF0aCBkPSJNMzc0LjU3OCwxMTQu
|
|
||||||
NDU1Yy0wLjYxMywtNC4yMTYgLTQuNTMzLC03LjE0MiAtOC43NDksLTYuNTNsLTk5Ljk0NSwxNC41
|
|
||||||
MTVjLTQuMjE2LDAuNjEzIC03LjE0Miw0LjUzMyAtNi41Myw4Ljc0OWwyLjIxOSwxNS4yNzhjMC42
|
|
||||||
MTIsNC4yMTYgNC41MzIsNy4xNDIgOC43NDksNi41M2w5OS45NDUsLTE0LjUxNWM0LjIxNiwtMC42
|
|
||||||
MTIgNy4xNDIsLTQuNTMzIDYuNTI5LC04Ljc0OWwtMi4yMTgsLTE1LjI3OFoiIHN0eWxlPSJmaWxs
|
|
||||||
OiM5ZGFjY2Q7c3Ryb2tlOiMwMDA7c3Ryb2tlLXdpZHRoOjUuMjFweDsiLz48cGF0aCBkPSJNMzEz
|
|
||||||
Ljg2OSwxMTcuNDE0YzAsMCA2Ni4xMDIsOS44NzggNzcuNTgyLC0yMy43MzNjMTEuNDgsLTMzLjYx
|
|
||||||
MSAtMTIuOTg3LC01MS44NzMgLTQxLjYzNywtMzkuOTIxYy0yOC42NSwxMS45NTIgLTU0Ljg1LDQ2
|
|
||||||
LjE0MiAtNTQuODUsNDYuMTQyYzAsMCAtMzAuMDA1LC04LjEwOCAtNDEuMzE0LDUuMjIxYy0xMS4z
|
|
||||||
MDksMTMuMzMgLTE3LjE2NywyNi42NDYgLTIuNDM1LDMxLjUxOWMxNC43MzIsNC44NzMgNjIuNjU0
|
|
||||||
LC0xOS4yMjggNjIuNjU0LC0xOS4yMjhaIiBzdHlsZT0iZmlsbDojNzM1ZDkzO3N0cm9rZTojMDAw
|
|
||||||
O3N0cm9rZS13aWR0aDo1LjkzcHg7Ii8+PHBhdGggZD0iTTM3NC4xNDgsMTM2Ljg3NmMtMCwwIDM4
|
|
||||||
LjQ5MywtOS4zMzggMzYuNTYzLC0zLjU0OWMtMS45Myw1Ljc5IC0zNy42MzgsMjYuOTAxIC0xMTQu
|
|
||||||
ODMxLDE1Ljk2NmM3MC4zNDUsLTEwLjczMSA3OC4yNjgsLTEyLjQxNyA3OC4yNjgsLTEyLjQxN1oi
|
|
||||||
IHN0eWxlPSJmaWxsOiM1MDNhNzE7c3Ryb2tlOiMwMDA7c3Ryb2tlLXdpZHRoOjQuOTVweDsiLz48
|
|
||||||
cGF0aCBkPSJNMzU3LjM0NiwxMjQuOTA2bC01NC4wMjEsOS42MTJsLTAsNS4zOTlsNTQuMDIxLC05
|
|
||||||
LjYxMmwwLC01LjM5OVoiIHN0eWxlPSJmaWxsOiNmZmY7Ii8+PGNpcmNsZSBjeD0iMzAwLjE1IiBj
|
|
||||||
eT0iMTM3LjE5OCIgcj0iNC42NzciIHN0eWxlPSJmaWxsOiNmZmIyNWM7c3Ryb2tlOiMwMDA7c3Ry
|
|
||||||
b2tlLXdpZHRoOjQuOTVweDsiLz48Y2lyY2xlIGN4PSIzNjEuNDEiIGN5PSIxMjcuNTgxIiByPSI0
|
|
||||||
LjI4MiIgc3R5bGU9ImZpbGw6I2ZmYjI1YztzdHJva2U6IzAwMDtzdHJva2Utd2lkdGg6NC45NXB4
|
|
||||||
OyIvPjwvZz48L3N2Zz4=
|
|
3
testdata/logo.svg.base64.license
vendored
3
testdata/logo.svg.base64.license
vendored
|
@ -1,3 +0,0 @@
|
||||||
SPDX-FileCopyrightText: 2022 Maria Letta, the go-mail Team
|
|
||||||
|
|
||||||
SPDX-License-Identifier: CC-BY-ND-4.0
|
|
3
testdata/logo.svg.license
vendored
3
testdata/logo.svg.license
vendored
|
@ -1,3 +0,0 @@
|
||||||
SPDX-FileCopyrightText: 2022 Maria Letta, the go-mail Team
|
|
||||||
|
|
||||||
SPDX-License-Identifier: CC-BY-ND-4.0
|
|
0
testdata/tmp/.gitkeep
vendored
0
testdata/tmp/.gitkeep
vendored
Loading…
Reference in a new issue