mirror of
https://github.com/wneessen/go-mail.git
synced 2024-12-23 02:50:39 +01:00
Merge pull request #348 from wneessen/feature/overhaul-tests
Overhaul test suite and GH CI workflow
This commit is contained in:
commit
c8478fb6c0
42 changed files with 12648 additions and 6964 deletions
23
.cirrus.yml
23
.cirrus.yml
|
@ -1,23 +0,0 @@
|
|||
# SPDX-FileCopyrightText: 2022 Winni Neessen <winni@neessen.dev>
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
freebsd_task:
|
||||
name: FreeBSD
|
||||
|
||||
matrix:
|
||||
- name: FreeBSD 13.3
|
||||
freebsd_instance:
|
||||
image_family: freebsd-13-3
|
||||
- name: FreeBSD 14.0
|
||||
freebsd_instance:
|
||||
image_family: freebsd-14-0
|
||||
|
||||
env:
|
||||
TEST_SKIP_SENDMAIL: 1
|
||||
|
||||
pkginstall_script:
|
||||
- pkg install -y go
|
||||
|
||||
test_script:
|
||||
- go test -race -cover -shuffle=on ./...
|
218
.github/workflows/ci.yml
vendored
Normal file
218
.github/workflows/ci.yml
vendored
Normal file
|
@ -0,0 +1,218 @@
|
|||
# SPDX-FileCopyrightText: 2024 The go-mail Authors
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
name: CI
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref_name }}
|
||||
|
||||
jobs:
|
||||
codecov:
|
||||
name: Test with Codecov coverage (${{ matrix.os }} / ${{ matrix.go }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
concurrency:
|
||||
group: ci-codecov-${{ matrix.os }}-${{ matrix.go }}
|
||||
cancel-in-progress: true
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
go: ['1.23']
|
||||
env:
|
||||
PERFORM_ONLINE_TEST: ${{ vars.PERFORM_ONLINE_TEST }}
|
||||
PERFORM_UNIX_OPEN_WRITE_TESTS: ${{ vars.PERFORM_UNIX_OPEN_WRITE_TESTS }}
|
||||
PERFORM_SENDMAIL_TESTS: ${{ vars.PERFORM_SENDMAIL_TESTS }}
|
||||
TEST_HOST: ${{ secrets.TEST_HOST }}
|
||||
TEST_USER: ${{ secrets.TEST_USER }}
|
||||
TEST_PASS: ${{ secrets.TEST_PASS }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@61b9e3751b92087fd0b06925ba6dd6314e06f089 # master
|
||||
- name: Setup go
|
||||
uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
check-latest: true
|
||||
- name: Install sendmail
|
||||
run: |
|
||||
sudo apt-get -y update >/dev/null && sudo apt-get -y upgrade >/dev/null && sudo DEBIAN_FRONTEND=noninteractive apt-get -y install nullmailer >/dev/null && which sendmail
|
||||
- name: Run go test
|
||||
if: success()
|
||||
run: |
|
||||
go test -race -shuffle=on --coverprofile=coverage.coverprofile --covermode=atomic ./...
|
||||
- name: Upload coverage to Codecov
|
||||
if: success()
|
||||
uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4.6.0
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos
|
||||
lint:
|
||||
name: golangci-lint (${{ matrix.go }})
|
||||
runs-on: ubuntu-latest
|
||||
concurrency:
|
||||
group: ci-lint-${{ matrix.go }}
|
||||
cancel-in-progress: true
|
||||
strategy:
|
||||
matrix:
|
||||
go: ['1.23']
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: Setup go
|
||||
uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
check-latest: true
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@61b9e3751b92087fd0b06925ba6dd6314e06f089 # master
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@971e284b6050e8a5849b72094c50ab08da042db8 # v6.1.1
|
||||
with:
|
||||
version: latest
|
||||
dependency-review:
|
||||
name: Dependency review
|
||||
runs-on: ubuntu-latest
|
||||
concurrency:
|
||||
group: ci-dependency-review
|
||||
cancel-in-progress: true
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@61b9e3751b92087fd0b06925ba6dd6314e06f089 # master
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@a6993e2c61fd5dc440b409aa1d6904921c5e1894 # v4.3.5
|
||||
govulncheck:
|
||||
name: Go vulnerabilities check
|
||||
runs-on: ubuntu-latest
|
||||
concurrency:
|
||||
group: ci-govulncheck
|
||||
cancel-in-progress: true
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: Run govulncheck
|
||||
uses: golang/govulncheck-action@b625fbe08f3bccbe446d94fbf87fcc875a4f50ee # v1.0.4
|
||||
test:
|
||||
name: Test (${{ matrix.os }} / ${{ matrix.go }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
concurrency:
|
||||
group: ci-test-${{ matrix.os }}-${{ matrix.go }}
|
||||
cancel-in-progress: true
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
go: ['1.19', '1.20', '1.21', '1.22', '1.23']
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@61b9e3751b92087fd0b06925ba6dd6314e06f089 # master
|
||||
- name: Setup go
|
||||
uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
- name: Run go test
|
||||
run: |
|
||||
go test -race -shuffle=on ./...
|
||||
test-fbsd:
|
||||
name: Test on FreeBSD ${{ matrix.osver }}
|
||||
runs-on: ubuntu-latest
|
||||
concurrency:
|
||||
group: ci-test-freebsd-${{ matrix.osver }}
|
||||
cancel-in-progress: true
|
||||
strategy:
|
||||
matrix:
|
||||
osver: ['14.1', '14.0', 13.4']
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@61b9e3751b92087fd0b06925ba6dd6314e06f089 # master
|
||||
- name: Run go test on FreeBSD
|
||||
uses: vmactions/freebsd-vm@v1
|
||||
with:
|
||||
usesh: true
|
||||
copyback: false
|
||||
prepare: |
|
||||
pkg install -y go
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE;
|
||||
go test -race -shuffle=on ./...
|
||||
reuse:
|
||||
name: REUSE Compliance Check
|
||||
runs-on: ubuntu-latest
|
||||
concurrency:
|
||||
group: ci-reuse
|
||||
cancel-in-progress: true
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@61b9e3751b92087fd0b06925ba6dd6314e06f089 # master
|
||||
- name: REUSE Compliance Check
|
||||
uses: fsfe/reuse-action@3ae3c6bdf1257ab19397fab11fd3312144692083 # v4.0.0
|
||||
sonarqube:
|
||||
name: Test with SonarQube review (${{ matrix.os }} / ${{ matrix.go }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
concurrency:
|
||||
group: ci-codecov-${{ matrix.go }}
|
||||
cancel-in-progress: true
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
go: ['1.23']
|
||||
env:
|
||||
PERFORM_ONLINE_TEST: ${{ vars.PERFORM_ONLINE_TEST }}
|
||||
TEST_HOST: ${{ secrets.TEST_HOST }}
|
||||
TEST_USER: ${{ secrets.TEST_USER }}
|
||||
TEST_PASS: ${{ secrets.TEST_PASS }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@61b9e3751b92087fd0b06925ba6dd6314e06f089 # master
|
||||
- name: Setup go
|
||||
uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
check-latest: true
|
||||
- name: Run go test
|
||||
run: |
|
||||
go test -shuffle=on -race --coverprofile=./cov.out ./...
|
||||
- name: SonarQube scan
|
||||
uses: sonarsource/sonarqube-scan-action@884b79409bbd464b2a59edc326a4b77dc56b2195 # master
|
||||
if: success()
|
||||
env:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
|
||||
- name: SonarQube quality gate
|
||||
uses: sonarsource/sonarqube-quality-gate-action@dc2f7b0dd95544cd550de3028f89193576e958b9 # master
|
||||
timeout-minutes: 5
|
||||
env:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
|
67
.github/workflows/codecov.yml
vendored
67
.github/workflows/codecov.yml
vendored
|
@ -1,67 +0,0 @@
|
|||
# SPDX-FileCopyrightText: 2022 Winni Neessen <winni@neessen.dev>
|
||||
#
|
||||
# SPDX-License-Identifier: CC0-1.0
|
||||
|
||||
name: Codecov workflow
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- '**.go'
|
||||
- 'go.*'
|
||||
- '.github/workflows/codecov.yml'
|
||||
- 'codecov.yml'
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- '**.go'
|
||||
- 'go.*'
|
||||
- '.github/workflows/codecov.yml'
|
||||
- 'codecov.yml'
|
||||
env:
|
||||
TEST_HOST: ${{ secrets.TEST_HOST }}
|
||||
TEST_FROM: ${{ secrets.TEST_USER }}
|
||||
TEST_ALLOW_SEND: "1"
|
||||
TEST_SMTPAUTH_USER: ${{ secrets.TEST_USER }}
|
||||
TEST_SMTPAUTH_PASS: ${{ secrets.TEST_PASS }}
|
||||
TEST_SMTPAUTH_TYPE: "LOGIN"
|
||||
TEST_ONLINE_SCRAM: "1"
|
||||
TEST_HOST_SCRAM: ${{ secrets.TEST_HOST_SCRAM }}
|
||||
TEST_USER_SCRAM: ${{ secrets.TEST_USER_SCRAM }}
|
||||
TEST_PASS_SCRAM: ${{ secrets.TEST_PASS_SCRAM }}
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
run:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
go: ['1.23']
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@61b9e3751b92087fd0b06925ba6dd6314e06f089 # master
|
||||
- name: Setup go
|
||||
uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
- name: Install sendmail
|
||||
if: matrix.go == '1.23' && matrix.os == 'ubuntu-latest'
|
||||
run: |
|
||||
sudo apt-get -y install sendmail; which sendmail
|
||||
- name: Run Tests
|
||||
run: |
|
||||
go test -race --coverprofile=coverage.coverprofile --covermode=atomic ./...
|
||||
- name: Upload coverage to Codecov
|
||||
if: success() && matrix.go == '1.23' && matrix.os == 'ubuntu-latest'
|
||||
uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4.6.0
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos
|
31
.github/workflows/dependency-review.yml
vendored
31
.github/workflows/dependency-review.yml
vendored
|
@ -1,31 +0,0 @@
|
|||
# SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
|
||||
#
|
||||
# SPDX-License-Identifier: CC0-1.0
|
||||
|
||||
# Dependency Review Action
|
||||
#
|
||||
# This Action will scan dependency manifest files that change as part of a Pull Request,
|
||||
# surfacing known-vulnerable versions of the packages declared or updated in the PR.
|
||||
# Once installed, if the workflow run is marked as required,
|
||||
# PRs introducing known-vulnerable packages will be blocked from merging.
|
||||
#
|
||||
# Source repository: https://github.com/actions/dependency-review-action
|
||||
name: 'Dependency Review'
|
||||
on: [pull_request]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
dependency-review:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@a6993e2c61fd5dc440b409aa1d6904921c5e1894 # v4.3.5
|
54
.github/workflows/golangci-lint.yml
vendored
54
.github/workflows/golangci-lint.yml
vendored
|
@ -1,54 +0,0 @@
|
|||
# SPDX-FileCopyrightText: 2022 Winni Neessen <winni@neessen.dev>
|
||||
#
|
||||
# SPDX-License-Identifier: CC0-1.0
|
||||
|
||||
name: golangci-lint
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
permissions:
|
||||
contents: read
|
||||
# Optional: allow read access to pull request. Use with `only-new-issues` option.
|
||||
# pull-requests: read
|
||||
jobs:
|
||||
golangci:
|
||||
name: lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
|
||||
with:
|
||||
go-version: '1.23'
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@971e284b6050e8a5849b72094c50ab08da042db8 # v6.1.1
|
||||
with:
|
||||
# Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version
|
||||
version: latest
|
||||
|
||||
# Optional: working directory, useful for monorepos
|
||||
# working-directory: somedir
|
||||
|
||||
# Optional: golangci-lint command line arguments.
|
||||
# args: --issues-exit-code=0
|
||||
|
||||
# Optional: show only new issues if it's a pull request. The default value is `false`.
|
||||
# only-new-issues: true
|
||||
|
||||
# Optional: if set to true then the all caching functionality will be complete disabled,
|
||||
# takes precedence over all other caching options.
|
||||
# skip-cache: true
|
||||
|
||||
# Optional: if set to true then the action don't cache or restore ~/go/pkg.
|
||||
# skip-pkg-cache: true
|
||||
|
||||
# Optional: if set to true then the action don't cache or restore ~/.cache/go-build.
|
||||
# skip-build-cache: true
|
21
.github/workflows/govulncheck.yml
vendored
21
.github/workflows/govulncheck.yml
vendored
|
@ -1,21 +0,0 @@
|
|||
# SPDX-FileCopyrightText: 2022 Winni Neessen <winni@neessen.dev>
|
||||
#
|
||||
# SPDX-License-Identifier: CC0-1.0
|
||||
|
||||
name: Govulncheck Security Scan
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: Run govulncheck
|
||||
uses: golang/govulncheck-action@b625fbe08f3bccbe446d94fbf87fcc875a4f50ee # v1.0.4
|
45
.github/workflows/offline-tests.yml
vendored
45
.github/workflows/offline-tests.yml
vendored
|
@ -1,45 +0,0 @@
|
|||
# SPDX-FileCopyrightText: 2022 Winni Neessen <winni@neessen.dev>
|
||||
#
|
||||
# SPDX-License-Identifier: CC0-1.0
|
||||
|
||||
name: Offline tests workflow
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- '**.go'
|
||||
- 'go.*'
|
||||
- '.github/workflows/offline-tests.yml'
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- '**.go'
|
||||
- 'go.*'
|
||||
- '.github/workflows/offline-tests.yml'
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
run:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
go: ['1.19', '1.20', '1.21', '1.22', '1.23']
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@61b9e3751b92087fd0b06925ba6dd6314e06f089 # master
|
||||
- name: Setup go
|
||||
uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
- name: Run Tests
|
||||
run: |
|
||||
go test -race -shuffle=on ./...
|
23
.github/workflows/reuse.yml
vendored
23
.github/workflows/reuse.yml
vendored
|
@ -1,23 +0,0 @@
|
|||
# SPDX-FileCopyrightText: 2022 Winni Neessen <winni@neessen.dev>
|
||||
#
|
||||
# SPDX-License-Identifier: CC0-1.0
|
||||
|
||||
name: REUSE Compliance Check
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2.7.0
|
||||
- name: REUSE Compliance Check
|
||||
uses: fsfe/reuse-action@3ae3c6bdf1257ab19397fab11fd3312144692083 # v4.0.0
|
56
.github/workflows/sonarqube.yml
vendored
56
.github/workflows/sonarqube.yml
vendored
|
@ -1,56 +0,0 @@
|
|||
# SPDX-FileCopyrightText: 2022 Winni Neessen <winni@neessen.dev>
|
||||
#
|
||||
# SPDX-License-Identifier: CC0-1.0
|
||||
|
||||
name: SonarQube
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- '**.go'
|
||||
- 'go.*'
|
||||
- '.github/workflows/sonarqube.yml'
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- '**.go'
|
||||
- 'go.*'
|
||||
- '.github/workflows/sonarqube.yml'
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2.7.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
|
||||
with:
|
||||
go-version: '1.23'
|
||||
|
||||
- name: Run unit Tests
|
||||
run: |
|
||||
go test -shuffle=on -race --coverprofile=./cov.out ./...
|
||||
|
||||
- uses: sonarsource/sonarqube-scan-action@884b79409bbd464b2a59edc326a4b77dc56b2195 # master
|
||||
env:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
|
||||
|
||||
- uses: sonarsource/sonarqube-quality-gate-action@dc2f7b0dd95544cd550de3028f89193576e958b9 # master
|
||||
timeout-minutes: 5
|
||||
env:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -56,4 +56,6 @@ crashlytics.properties
|
|||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
testdata
|
||||
## Coverage data
|
||||
coverage.coverprofile
|
||||
coverage.html
|
|
@ -13,8 +13,8 @@ import (
|
|||
// in encoding processes.
|
||||
var newlineBytes = []byte(SingleNewLine)
|
||||
|
||||
// ErrNoOutWriter is the error message returned when no io.Writer is set for Base64LineBreaker.
|
||||
const ErrNoOutWriter = "no io.Writer set for Base64LineBreaker"
|
||||
// ErrNoOutWriter is the error returned when no io.Writer is set for Base64LineBreaker.
|
||||
var ErrNoOutWriter = errors.New("no io.Writer set for Base64LineBreaker")
|
||||
|
||||
// Base64LineBreaker handles base64 encoding with the insertion of new lines after a certain number
|
||||
// of characters.
|
||||
|
@ -44,7 +44,7 @@ type Base64LineBreaker struct {
|
|||
// - err: An error if one occurred during the write operation.
|
||||
func (l *Base64LineBreaker) Write(data []byte) (numBytes int, err error) {
|
||||
if l.out == nil {
|
||||
err = errors.New(ErrNoOutWriter)
|
||||
err = ErrNoOutWriter
|
||||
return
|
||||
}
|
||||
if l.used+len(data) < MaxBodyLength {
|
||||
|
|
|
@ -5,487 +5,165 @@
|
|||
package mail
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const logoB64 = `PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PCFE
|
||||
T0NUWVBFIHN2ZyBQVUJMSUMgIi0vL1czQy8vRFREIFNWRyAxLjEvL0VOIiAiaHR0cDovL3d3dy53
|
||||
My5vcmcvR3JhcGhpY3MvU1ZHLzEuMS9EVEQvc3ZnMTEuZHRkIj48c3ZnIHdpZHRoPSIxMDAlIiBo
|
||||
ZWlnaHQ9IjEwMCUiIHZpZXdCb3g9IjAgMCA2MDAgNjAwIiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJo
|
||||
dHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3Jn
|
||||
LzE5OTkveGxpbmsiIHhtbDpzcGFjZT0icHJlc2VydmUiIHhtbG5zOnNlcmlmPSJodHRwOi8vd3d3
|
||||
LnNlcmlmLmNvbS8iIHN0eWxlPSJmaWxsLXJ1bGU6ZXZlbm9kZDtjbGlwLXJ1bGU6ZXZlbm9kZDtz
|
||||
dHJva2UtbGluZWNhcDpyb3VuZDtzdHJva2UtbGluZWpvaW46cm91bmQ7c3Ryb2tlLW1pdGVybGlt
|
||||
aXQ6MS41OyI+PHJlY3QgaWQ9ItCc0L7QvdGC0LDQttC90LDRjy3QvtCx0LvQsNGB0YLRjDEiIHNl
|
||||
cmlmOmlkPSLQnNC+0L3RgtCw0LbQvdCw0Y8g0L7QsdC70LDRgdGC0YwxIiB4PSIwIiB5PSIwIiB3
|
||||
aWR0aD0iNjAwIiBoZWlnaHQ9IjYwMCIgc3R5bGU9ImZpbGw6bm9uZTsiLz48Zz48cGF0aCBkPSJN
|
||||
NTg0LjcyNiw5OC4zOTVjLTAsLTkuNzM4IC03LjkwNywtMTcuNjQ1IC0xNy42NDUsLTE3LjY0NWwt
|
||||
NTEwLjAyNywwYy05LjczOSwwIC0xNy42NDUsNy45MDcgLTE3LjY0NSwxNy42NDVsLTAsMzE5Ljc5
|
||||
NGMtMCw5LjczOSA3LjkwNiwxNy42NDYgMTcuNjQ1LDE3LjY0Nmw1MTAuMDI3LC0wYzkuNzM4LC0w
|
||||
IDE3LjY0NSwtNy45MDcgMTcuNjQ1LC0xNy42NDZsLTAsLTMxOS43OTRaIiBzdHlsZT0iZmlsbDoj
|
||||
MmMyYzJjO3N0cm9rZTojMDAwO3N0cm9rZS13aWR0aDoyLjFweDsiLz48cGF0aCBkPSJNNTg1LjEy
|
||||
Niw5OC4zOTVjLTAsLTkuNzM4IC03LjkwNywtMTcuNjQ1IC0xNy42NDUsLTE3LjY0NWwtNDk0Ljgz
|
||||
OSwwYy05LjczOCwwIC0xNy42NDUsNy45MDcgLTE3LjY0NSwxNy42NDVsMCwzMTkuNzk0YzAsOS43
|
||||
MzkgNy45MDcsMTcuNjQ2IDE3LjY0NSwxNy42NDZsNDk0LjgzOSwtMGM5LjczOCwtMCAxNy42NDUs
|
||||
LTcuOTA3IDE3LjY0NSwtMTcuNjQ2bC0wLC0zMTkuNzk0WiIgc3R5bGU9ImZpbGw6IzM3MzczNztz
|
||||
dHJva2U6IzAwMDtzdHJva2Utd2lkdGg6Mi4wN3B4OyIvPjxwYXRoIGQ9Ik01NjcuMjU5LDExNC40
|
||||
NzJjLTAsLTguNzYgLTcuMTEyLC0xNS44NzIgLTE1Ljg3MiwtMTUuODcybC00NjIuNjUxLDBjLTgu
|
||||
NzYsMCAtMTUuODcyLDcuMTEyIC0xNS44NzIsMTUuODcybDAsMjg3LjY0MWMwLDguNzYgNy4xMTIs
|
||||
MTUuODcxIDE1Ljg3MiwxNS44NzFsNDYyLjY1MSwwYzguNzYsMCAxNS44NzIsLTcuMTExIDE1Ljg3
|
||||
MiwtMTUuODcxbC0wLC0yODcuNjQxWiIgc3R5bGU9ImZpbGw6I2ZmZjJlYjtzdHJva2U6IzAwMDtz
|
||||
dHJva2Utd2lkdGg6MS45cHg7Ii8+PHBhdGggZD0iTTIzNi4xNDUsNDM1Ljg3OGwtMjMuNDIsOTEu
|
||||
MDA4bDEzMC43NjUsLTBsMjIuMzY1LC05MC43NTZsLTEyOS43MSwtMC4yNTJaIiBzdHlsZT0iZmls
|
||||
bDojMmMyYzJjO3N0cm9rZTojMDAwO3N0cm9rZS13aWR0aDoyLjQ3cHg7Ii8+PHBhdGggZD0iTTI0
|
||||
NS4zOTcsNDM1Ljg3OGwtMjMuNDIxLDkxLjAwOGwxMzAuNzY2LC0wbDIyLjM2NSwtOTAuNzU2bC0x
|
||||
MjkuNzEsLTAuMjUyWiIgc3R5bGU9ImZpbGw6IzM3MzczNztzdHJva2U6IzAwMDtzdHJva2Utd2lk
|
||||
dGg6Mi40N3B4OyIvPjxwYXRoIGQ9Ik00NzIuNTg0LDUzMS4wOTNjLTAsLTIuODI3IC0yLjI5NSwt
|
||||
NS4xMjEgLTUuMTIxLC01LjEyMWwtMjkyLjU0MSwtMGMtMi44MjYsLTAgLTUuMTIxLDIuMjk0IC01
|
||||
LjEyMSw1LjEyMWwwLDEwLjI0M2MwLDIuODI2IDIuMjk1LDUuMTIxIDUuMTIxLDUuMTIxbDI5Mi41
|
||||
NDEsLTBjMi44MjYsLTAgNS4xMjEsLTIuMjk1IDUuMTIxLC01LjEyMWwtMCwtMTAuMjQzWiIgc3R5
|
||||
bGU9ImZpbGw6IzJjMmMyYztzdHJva2U6IzAwMDtzdHJva2Utd2lkdGg6Mi40N3B4OyIvPjxnPjxw
|
||||
YXRoIGQ9Ik0xOTAuMzc5LDQzMS40NTVjLTAsLTEuOTE2IC0xLjU1NiwtMy40NzIgLTMuNDcyLC0z
|
||||
LjQ3MmwtNTQuNTYzLDBjLTEuOTE3LDAgLTMuNDcyLDEuNTU2IC0zLjQ3MiwzLjQ3MmwtMCw0Ni4z
|
||||
MDljLTAsMS45MTYgMS41NTUsMy40NzIgMy40NzIsMy40NzJsNTQuNTYzLC0wYzEuOTE2LC0wIDMu
|
||||
NDcyLC0xLjU1NiAzLjQ3MiwtMy40NzJsLTAsLTQ2LjMwOVoiIHN0eWxlPSJmaWxsOiNlZWFjMDE7
|
||||
c3Ryb2tlOiMwMDA7c3Ryb2tlLXdpZHRoOjMuMXB4OyIvPjxnPjxwYXRoIGQ9Ik0xMzkuOTk2LDQ0
|
||||
Mi44NTNjLTAsMC40MiAtMC4wNzEsMC45MjIgMC4wNjMsMS4zMjNjMC41MywxLjU5MiAyLjc1LDEu
|
||||
Mzg2IDQuMDk1LDEuMzg2YzIuMzk3LC0wIDQuNjc0LC0wLjEwOCA2LjQ4OSwtMS44MjdjMC40NSwt
|
||||
MC40MjYgMC45NjYsLTAuOTQgMS4wNzIsLTEuNTc1YzAuMDYxLC0wLjM2NyAwLjEwMiwtMS4yMDgg
|
||||
LTAuMTI2LC0xLjUxMmMtMC44MDMsLTEuMDcxIC0yLjg0NCwtMC4zNzMgLTMuNjU1LDAuMjUyYy0w
|
||||
LjcxNCwwLjU1IC0wLjgyMiwxLjgyNiAtMC45NDUsMi42NDZjLTAuNDA2LDIuNzA1IDAuMzg3LDUu
|
||||
MDMgMy4xNSw1Ljg1OWMzLjQwNSwxLjAyMSA2Ljc4NCwwLjA2IDEwLjA4NiwtMC44MDljMS41OTcs
|
||||
LTAuNDIxIDMuNjYsLTAuNzcgNS4wMTcsLTEuNzJjMC40NTIsLTAuMzE2IDAuNDQ0LC0wLjI4NCAw
|
||||
LjkwMSwtMC41NThjMS4yMDgsLTAuNzI1IDMuODY5LC0xLjkyNyAyLjQ1NywtMy41OTFjLTAuMTks
|
||||
LTAuMjI0IC0xLjEzOSwtMC4yMzcgLTEuMjYsMC4xMjZjLTAuNDIsMS4yNTcgLTEuMzI2LDMuMzAy
|
||||
IDAuNTY3LDMuNzhjMS4zOTgsMC4zNTMgMi45MTksMC4xODkgNC4zNDcsMC4xODljMC4xOTMsLTAg
|
||||
MS40ODUsLTAuMjc5IDEuNzAxLC0wLjA2M2MwLjU1NSwwLjU1NSAxLjU0MywwLjgxMSAyLjMzMSwx
|
||||
LjAwOGMwLjkzMiwwLjIzMyAxLjk0NCwwLjYwNiAyLjg5OSwwLjY5M2MwLjg2OSwwLjA3OSAxLjc5
|
||||
LC0wLjA0NSAyLjY0NiwwLjEyNiIgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2U6IzAwMDtzdHJva2Ut
|
||||
d2lkdGg6MS45NnB4OyIvPjxwYXRoIGQ9Ik0xNDIuNzA1LDQ1Mi42ODFjMC4xMTksMC41NTEgMC44
|
||||
OTMsMC42NjkgMS4zMTgsMC44NDZjMS41OTgsMC42NjYgMy41MTQsMC44NzQgNS4yMzQsMC45ODJj
|
||||
Mi4zNzgsMC4xNDggNC43MDQsLTAuMTU5IDcuMDU3LC0wLjMxNWMxLjg2MSwtMC4xMjUgMy43NDYs
|
||||
MC4xMTYgNS42MDcsLTBjMi4wNTEsLTAuMTI5IDQuMzIxLC0wLjM3NyA2LjM2NCwtMC4wNjNjMS4w
|
||||
NzIsMC4xNjQgMi4wODksMC40OCAzLjE1LDAuNjkzIiBzdHlsZT0iZmlsbDpub25lO3N0cm9rZToj
|
||||
MDAwO3N0cm9rZS13aWR0aDoxLjk2cHg7Ii8+PHBhdGggZD0iTTE3Ni4zNDksNDU1LjA3NmMxLjEx
|
||||
MywtMC4wMjEgMi4yMjYsLTAuMDQyIDMuMzQsLTAuMDYzIiBzdHlsZT0iZmlsbDpub25lO3N0cm9r
|
||||
ZTojMDAwO3N0cm9rZS13aWR0aDoxLjk2cHg7Ii8+PHBhdGggZD0iTTE0NS4xNjIsNDU5Ljg2NGwy
|
||||
MS42NzQsLTAiIHN0eWxlPSJmaWxsOm5vbmU7c3Ryb2tlOiMwMDA7c3Ryb2tlLXdpZHRoOjEuOTZw
|
||||
eDsiLz48L2c+PC9nPjxnPjxnPjxwYXRoIGQ9Ik03Ni4yNzMsMTIxLjQ5NGw0ODcuNTU3LC0wLjAx
|
||||
NSIgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2U6I2Y0ZGRkMDtzdHJva2Utd2lkdGg6NC45M3B4O3N0
|
||||
cm9rZS1saW5lY2FwOnNxdWFyZTsiLz48cGF0aCBkPSJNMjMwLjk1MSwyMjQuMTc1Yy0wLjgwMiwt
|
||||
My4zMjcgLTguMDU5LC03Ljk4NCAtMTEuMDA1LC04Ljk0NWMtMzUuODQ2LC0xMS42OTkgLTIwLjc4
|
||||
MywyNy4zNzIgLTAuNjE4LDI0LjM0MiIgc3R5bGU9ImZpbGw6IzYxZTBlZTsiLz48cGF0aCBkPSJN
|
||||
MjM0Ljk0NCwyMjMuMjEzYy0wLjM3OSwtMS41NzQgLTEuNTIyLC0zLjQzNiAtMy4yNzYsLTUuMTc0
|
||||
Yy0zLjA2MSwtMy4wMzMgLTguMDE0LC01LjkyIC0xMC40NDgsLTYuNzE0Yy0xMy4xMzUsLTQuMjg3
|
||||
IC0yMC42MTgsLTEuOTk0IC0yNC4wNzEsMS44MTljLTMuNjM4LDQuMDE3IC0zLjgzNiwxMC40OTIg
|
||||
LTAuODI1LDE2LjY1MWMzLjk4MSw4LjE0MyAxMy4zMjEsMTUuMzg2IDIzLjYxNSwxMy44MzljMi4y
|
||||
NDIsLTAuMzM3IDMuNzg4LC0yLjQzIDMuNDUxLC00LjY3MmMtMC4zMzcsLTIuMjQyIC0yLjQzLC0z
|
||||
Ljc4OSAtNC42NzIsLTMuNDUyYy02LjY0NywwLjk5OSAtMTIuNDQ0LC00LjA2NSAtMTUuMDE0LC05
|
||||
LjMyM2MtMC44MDEsLTEuNjM3IC0xLjI5MywtMy4zMDggLTEuMzAyLC00Ljg0N2MtMC4wMDUsLTEu
|
||||
MDIgMC4xOTUsLTEuOTc0IDAuODM2LC0yLjY4MmMwLjgzMiwtMC45MTggMi4yMjUsLTEuMzU0IDQu
|
||||
MTMzLC0xLjQ4MWMyLjg1MiwtMC4xOTEgNi41NjcsMC40MTIgMTEuMywxLjk1N2MxLjQ1NiwwLjQ3
|
||||
NSA0LjE2LDIuMDk2IDYuMjU3LDMuODY5YzAuNjM2LDAuNTM3IDEuMjE1LDEuMDg4IDEuNjU4LDEu
|
||||
NjM2YzAuMTUyLDAuMTg4IDAuMzMxLDAuMzI3IDAuMzcyLDAuNDk4YzAuNTMxLDIuMjA0IDIuNzUx
|
||||
LDMuNTYyIDQuOTU1LDMuMDMxYzIuMjA0LC0wLjUzMSAzLjU2MiwtMi43NTIgMy4wMzEsLTQuOTU1
|
||||
WiIvPjxwYXRoIGQ9Ik0zNDkuMzI2LDEyMy41MjdjMCwwIC0xNS4zNSwtMTMuODYyIC0xOC4xMTQs
|
||||
MjIuODM0IiBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojNjFlMGVlO3N0cm9rZS13aWR0aDo2LjE2
|
||||
cHg7Ii8+PHBhdGggZD0iTTI5OS42NjIsMTIzLjQ4NWMtMCwtMCAxMi42NzEsLTE1LjI2NSAyMC4x
|
||||
MTIsMjEuODk4IiBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojNjFlMGVlO3N0cm9rZS13aWR0aDo2
|
||||
LjE2cHg7Ii8+PHBhdGggZD0iTTMzNi4xMTEsMTA4Ljc3M2MtMCwwIC0xNi44MDgsLTkuMDk5IC0x
|
||||
MC44ODIsMzIuMTI0IiBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojNjFlMGVlO3N0cm9rZS13aWR0
|
||||
aDo2LjE2cHg7Ii8+PHBhdGggZD0iTTQxNy43OTYsMTg0LjQ1M2MzLjI4MiwwLjk3IDYuNTgzLC0x
|
||||
LjI5IDguODg5LC0zLjM2MWMyOC4wNTUsLTI1LjE5NCAtMTMuMTk4LC0zMS41MzggLTIwLjY1Niwt
|
||||
MTIuNTU5IiBzdHlsZT0iZmlsbDojNjFlMGVlOyIvPjxwYXRoIGQ9Ik00MTYuNjMyLDE4OC4zOTNj
|
||||
NC42NTcsMS4zNzUgOS41MjYsLTEuMzA3IDEyLjc5NywtNC4yNDVjMTAuMjc5LC05LjIzIDEyLjA0
|
||||
NiwtMTYuODQzIDEwLjQ4OCwtMjEuNzE2Yy0xLjY0NSwtNS4xNSAtNy4xNDIsLTguNTQ0IC0xMy45
|
||||
NzgsLTguOTljLTkuMDExLC0wLjU4NyAtMTkuOTI3LDMuOTA0IC0yMy43MzMsMTMuNTg5Yy0wLjgz
|
||||
LDIuMTEgMC4yMSw0LjQ5NiAyLjMyLDUuMzI1YzIuMTEsMC44MjkgNC40OTYsLTAuMjExIDUuMzI1
|
||||
LC0yLjMyMWMyLjQ2LC02LjI1OSA5LjczLC04Ljc3NSAxNS41NTMsLTguMzk1YzEuODA3LDAuMTE3
|
||||
IDMuNDg5LDAuNTE3IDQuODE3LDEuMjY5YzAuODcxLDAuNDk0IDEuNTg2LDEuMTMgMS44NzEsMi4w
|
||||
MjNjMC4zNzUsMS4xNzIgMC4wNDksMi41ODggLTAuNzk1LDQuMjk4Yy0xLjI2NiwyLjU2MiAtMy42
|
||||
NDksNS40NzcgLTcuMzU2LDguODA2Yy0wLjg2OCwwLjc4IC0xLjkwNywxLjYxOCAtMy4wNTQsMi4x
|
||||
NDdjLTAuNjE2LDAuMjg0IC0xLjI2NSwwLjUyNyAtMS45MjgsMC4zMzFjLTIuMTc0LC0wLjY0MiAt
|
||||
NC40NjEsMC42MDIgLTUuMTAzLDIuNzc2Yy0wLjY0MiwyLjE3NCAwLjYwMiw0LjQ2MSAyLjc3Niw1
|
||||
LjEwM1oiLz48cGF0aCBkPSJNNDM2LjAxNCw0MTkuNjQ1YzMuNjAxLC0xMC45MzQgOS4wMDMsLTMx
|
||||
LjYwOSA1LjY4MiwtNDkuNTYzYy0xNC45NTMsLTgwLjg0NyA3Ljg5NSwtOTguNTQ5IC01LjAzNSwt
|
||||
MTQ3LjU5M2MtNC40OSwtMTcuMDI5IC0xMi4wNDksLTM2LjI1IC0yMy41NTQsLTQ5Ljc5MmMtMjUu
|
||||
Mjg1LC0yOS43NjIgLTcwLjczNiwtNDIuOTEzIC0xMDguNzMsLTMyLjc4NWMtMTUuMjEzLDQuMDU1
|
||||
IC0yOC42NjIsMTEuNzI3IC00MS45MjMsMjAuMDM0Yy00Ni4xMzYsMjguOSAtNTIuMjU5LDgzLjM3
|
||||
NyAtNDIuNzkxLDEyOC4yNDhjNC4xOTEsMTkuODY0IDIzLjY3Myw0OS44OTcgMjIuNTg2LDcwLjE5
|
||||
NGMtMS4zODcsMjUuOTEgLTAuNzY1LDQ0LjQyNiAzLjM3Niw2MC44ODQiIHN0eWxlPSJmaWxsOiM2
|
||||
MWUwZWU7Ii8+PGNsaXBQYXRoIGlkPSJfY2xpcDEiPjxwYXRoIGQ9Ik00MzYuMDE0LDQxOS42NDVj
|
||||
My42MDEsLTEwLjkzNCA5LjAwMywtMzEuNjA5IDUuNjgyLC00OS41NjNjLTE0Ljk1MywtODAuODQ3
|
||||
IDcuODk1LC05OC41NDkgLTUuMDM1LC0xNDcuNTkzYy00LjQ5LC0xNy4wMjkgLTEyLjA0OSwtMzYu
|
||||
MjUgLTIzLjU1NCwtNDkuNzkyYy0yNS4yODUsLTI5Ljc2MiAtNzAuNzM2LC00Mi45MTMgLTEwOC43
|
||||
MywtMzIuNzg1Yy0xNS4yMTMsNC4wNTUgLTI4LjY2MiwxMS43MjcgLTQxLjkyMywyMC4wMzRjLTQ2
|
||||
LjEzNiwyOC45IC01Mi4yNTksODMuMzc3IC00Mi43OTEsMTI4LjI0OGM0LjE5MSwxOS44NjQgMjMu
|
||||
NjczLDQ5Ljg5NyAyMi41ODYsNzAuMTk0Yy0xLjM4NywyNS45MSAtMC43NjUsNDQuNDI2IDMuMzc2
|
||||
LDYwLjg4NCIvPjwvY2xpcFBhdGg+PGcgY2xpcC1wYXRoPSJ1cmwoI19jbGlwMSkiPjxwYXRoIGQ9
|
||||
Ik0zMTEuNzc2LDIzNi44MzVjMCwwIC0zLjA2OCwyNC4xOTkgLTMxLjU5NywyNC44MzhjLTI4LjUz
|
||||
LDAuNjM5IC0zMi41OTEsLTI4LjI0NiAtMzIuNTkxLC0yOC4yNDZsNjQuMTg4LDMuNDA4WiIgc3R5
|
||||
bGU9ImZpbGw6IzRmY2JlMDsiLz48cGF0aCBkPSJNNDMyLjUwOSwyMTYuMDIzYzAsLTAgLTMuMDY3
|
||||
LDI0LjE5OCAtMzEuNTk3LDI0LjgzN2MtMjguNTMsMC42MzkgLTMyLjU5MSwtMjguMjQ2IC0zMi41
|
||||
OTEsLTI4LjI0Nmw2NC4xODgsMy40MDlaIiBzdHlsZT0iZmlsbDojNGZjYmUwOyIvPjxwYXRoIGQ9
|
||||
Ik0yODYuODQ1LDQxOC45NjFsMTI3Ljg4NiwtMGMwLjM4NiwtMS4yNjcgMC42NjYsLTIuNDMyIDAu
|
||||
ODcsLTMuNDcxYzMuODY3LC0xOS43NDMgMy43MjksLTU3LjQ2OCAtMi40MTUsLTc3LjA5NWMtMi4x
|
||||
MzMsLTYuODE2IC01Ljc5MywtMTQuNDkgLTExLjQ3NiwtMTkuODUyYy0xMi40OSwtMTEuNzg1IC02
|
||||
NC4zOTgsLTkuMTgxIC04My41NjEsLTQuODM1Yy03LjY3MywxLjc0MSAtMTQuNDg3LDQuOTIyIC0y
|
||||
MS4yMSw4LjM1OGMtMTQuOTExLDcuNjIgLTIwLjI1Myw1OC43MTIgLTEwLjA5NCw5Ni44OTVaIiBz
|
||||
dHlsZT0iZmlsbDojYjdmOGZmOyIvPjxwYXRoIGQ9Ik0yNDYuNzExLDQxOC45NjFjLTUuMTA4LC0y
|
||||
MC42MTMgLTYuNDksLTQyLjk3NSAtNS41NjIsLTYwLjg1NWMyLjEzNiwtNDEuMTMzIC0zMS4zMTQs
|
||||
LTUwLjQ1MyAtMjUuNDYxLC0xMjEuNTEzYzE0LjUzNCwtNDIuNDA5IDE1LjA4OSwtNDAuMTA3IDI3
|
||||
LjY1MywtNTUuNDQ1YzAsMCAtMTMuMTQ3LDM5Ljk2OCAtMS4xOTMsNzAuNzI4YzExLjk1NCwzMC43
|
||||
NTkgMzIuMTc0LDczLjQxOCAzMi41OSwxMTcuNDkxYzAuMTg2LDE5Ljc3MSAxLjYzNCwzNi4zMSA0
|
||||
LjEzLDQ5LjU5NGwtMzIuMTU3LC0wWiIgc3R5bGU9ImZpbGw6IzRmY2JlMDsiLz48L2c+PHBhdGgg
|
||||
ZD0iTTQzOS45MTUsNDIwLjkzYzMuNzQ4LC0xMS4zOCA5LjI3NiwtMzIuOTA4IDUuODIsLTUxLjU5
|
||||
NWMtOC44NjUsLTQ3LjkzMyAtNC4yNTksLTczLjQwNSAtMS45MzUsLTk2LjU4YzEuNjEzLC0xNi4w
|
||||
OTMgMi4xNTYsLTMxLjEyMiAtMy4xNjcsLTUxLjMxM2MtNC42MzgsLTE3LjU5MSAtMTIuNTExLC0z
|
||||
Ny40MTYgLTI0LjM5NiwtNTEuNDA1Yy0yNi4yNjEsLTMwLjkxMSAtNzMuNDU2LC00NC42MTMgLTEx
|
||||
Mi45MTcsLTM0LjA5NGMtMTUuNjE0LDQuMTYxIC0yOS40MzcsMTEuOTk2IC00My4wNDcsMjAuNTIy
|
||||
Yy00Ny43MzUsMjkuOTAyIC01NC40MjYsODYuMTUgLTQ0LjYyOSwxMzIuNTc3YzIuMTM5LDEwLjEz
|
||||
OCA4LjEzNiwyMi44ODggMTMuNTA0LDM1LjY3NmM0Ljk5NCwxMS45IDkuNTE1LDIzLjgxIDguOTk5
|
||||
LDMzLjQ1Yy0xLjQxNSwyNi40MzIgLTAuNzMsNDUuMzE3IDMuNDk1LDYyLjEwN2wxLjAwMiwzLjk4
|
||||
M2w3Ljk2NywtMi4wMDVsLTEuMDAyLC0zLjk4M2MtNC4wNTgsLTE2LjEyNyAtNC42MTgsLTM0LjI3
|
||||
NCAtMy4yNTgsLTU5LjY2M2MwLjQ2LC04LjYwOSAtMi40NiwtMTguODk1IC02LjU0NSwtMjkuNDU3
|
||||
Yy01LjY5NCwtMTQuNzI0IC0xMy42NDYsLTMwLjA1OSAtMTYuMTI0LC00MS44MDRjLTkuMTQsLTQz
|
||||
LjMxNSAtMy41ODQsLTk2LjAyMiA0MC45NTIsLTEyMy45MTljMTIuOTEyLC04LjA4OSAyNS45ODks
|
||||
LTE1LjU5OCA0MC44MDEsLTE5LjU0N2MzNi41MjYsLTkuNzM1IDgwLjIzNCwyLjg2NSAxMDQuNTQx
|
||||
LDMxLjQ3NmMxMS4xMjYsMTMuMDk2IDE4LjM3MiwzMS43MTMgMjIuNzEzLDQ4LjE4YzYuMzEsMjMu
|
||||
OTMyIDMuODIsNDAuMjE5IDEuNjQ3LDYwLjM4M2MtMi4yNTcsMjAuOTQ5IC00LjI1OSw0NS45Mjcg
|
||||
My4zMjEsODYuOTFjMy4xODYsMTcuMjIyIC0yLjA5LDM3LjA0MyAtNS41NDQsNDcuNTMxbC0xLjI4
|
||||
NSwzLjkwMmw3LjgwMywyLjU2OWwxLjI4NCwtMy45MDFaIi8+PHBhdGggZD0iTTQyNC41MzksMjQy
|
||||
LjI0N2MtMS44NjgsMC4xMzQgLTMuMjc2LDEuNzU4IC0zLjE0MiwzLjYyNmMwLjEzMywxLjg2NyAx
|
||||
Ljc1NywzLjI3NSAzLjYyNSwzLjE0MWMxLjg2NywtMC4xMzMgMy4yNzUsLTEuNzU3IDMuMTQyLC0z
|
||||
LjYyNWMtMC4xMzQsLTEuODY3IC0xLjc1OCwtMy4yNzUgLTMuNjI1LC0zLjE0MloiIHN0eWxlPSJm
|
||||
aWxsOiNiN2Y4ZmY7Ii8+PHBhdGggZD0iTTI1Ni45OTcsMjYwLjM5MWMtMS44NjgsMC4xMzMgLTMu
|
||||
Mjc1LDEuNzU4IC0zLjE0MiwzLjYyNWMwLjEzMywxLjg2OCAxLjc1OCwzLjI3NSAzLjYyNSwzLjE0
|
||||
MmMxLjg2OCwtMC4xMzMgMy4yNzUsLTEuNzU4IDMuMTQyLC0zLjYyNWMtMC4xMzMsLTEuODY4IC0x
|
||||
Ljc1OCwtMy4yNzUgLTMuNjI1LC0zLjE0MloiIHN0eWxlPSJmaWxsOiNiN2Y4ZmY7Ii8+PGc+PHBh
|
||||
dGggZD0iTTM2NC4wOSwyNjkuMjI5Yy0xLjE4Niw0Ljk2NyAtMS40NDMsMTQuMzA2IC00LjMxNSwx
|
||||
OC40Yy0xLjM5MywxLjk4NSAtNC40OTUsMi4wODYgLTYuNTE2LDIuMTUzYy0xMS42MTIsMC4zODMg
|
||||
LTExLjQ0NiwtMTEuNTEzIC0xMy4xODEsLTIxLjg1NSIgc3R5bGU9ImZpbGw6I2VkZWRlZDsiLz48
|
||||
cGF0aCBkPSJNMzYxLjA5NCwyNjguNTEzYy0wLjcxOSwzLjAwOSAtMS4xMDYsNy42MDcgLTEuOTIy
|
||||
LDExLjY5OGMtMC40NDUsMi4yMjggLTAuOTcxLDQuMjk4IC0xLjkxOSw1LjY0OWMtMC4yNjMsMC4z
|
||||
NzYgLTAuNzQ5LDAuNDU5IC0xLjIwMywwLjU2NmMtMC45OTUsMC4yMzMgLTIuMDU2LDAuMjUgLTIu
|
||||
ODkzLDAuMjc3Yy0yLjA1NSwwLjA2OCAtMy42MSwtMC4zMjYgLTQuNzc0LC0xLjE5MmMtMS4xOTMs
|
||||
LTAuODg3IC0xLjk1NCwtMi4yMTIgLTIuNTQ0LC0zLjc0M2MtMS41NjEsLTQuMDQ3IC0xLjg5LC05
|
||||
LjM5IC0yLjcyMywtMTQuMzUxYy0wLjI4MSwtMS42NzcgLTEuODcxLC0yLjgxIC0zLjU0OCwtMi41
|
||||
MjhjLTEuNjc3LDAuMjgxIC0yLjgwOSwxLjg3MSAtMi41MjgsMy41NDhjMC45MDMsNS4zODEgMS4z
|
||||
NTgsMTEuMTU4IDMuMDUxLDE1LjU0OGMxLjAzNiwyLjY4OCAyLjUyLDQuOTEyIDQuNjE1LDYuNDdj
|
||||
Mi4xMjUsMS41ODEgNC45MDMsMi41MyA4LjY1NCwyLjQwNmMxLjQ2OCwtMC4wNDggMy40LC0wLjE1
|
||||
MiA1LjA3OSwtMC43MTRjMS41NiwtMC41MjIgMi45MTksLTEuNDEgMy44NTgsLTIuNzQ5YzEuMzUs
|
||||
LTEuOTI0IDIuMjg0LC00LjgwOCAyLjkxNywtNy45ODFjMC44LC00LjAxMSAxLjE2OCwtOC41MjIg
|
||||
MS44NzMsLTExLjQ3M2MwLjM5NSwtMS42NTMgLTAuNjI3LC0zLjMxNyAtMi4yODEsLTMuNzEyYy0x
|
||||
LjY1NCwtMC4zOTUgLTMuMzE3LDAuNjI3IC0zLjcxMiwyLjI4MVoiLz48cGF0aCBkPSJNMzQ5LjAw
|
||||
MSwyMzQuMDVjLTkuMzMyLDEuMjQ2IC0xNi4yNjUsNy4wNzUgLTE1LjQ3MywxMy4wMDhjMC43OTIs
|
||||
NS45MzMgOS4wMTIsOS43MzggMTguMzQ0LDguNDkyYzkuMzMzLC0xLjI0NiAxNi4yNjYsLTcuMDc1
|
||||
IDE1LjQ3NCwtMTMuMDA4Yy0wLjc5MiwtNS45MzMgLTkuMDEyLC05LjczOCAtMTguMzQ1LC04LjQ5
|
||||
MloiLz48cGF0aCBkPSJNMzQ4LjkwMSwyNTIuNTVjMC40Niw1LjIwNCAxLjY5NCwxMC4xNDIgMi40
|
||||
NTMsMTUuMjc3YzAuMjQ4LDEuNjgyIDEuODE1LDIuODQ2IDMuNDk3LDIuNTk4YzEuNjgyLC0wLjI0
|
||||
OCAyLjg0NiwtMS44MTYgMi41OTgsLTMuNDk4Yy0wLjc0MSwtNS4wMTUgLTEuOTYxLC05LjgzNyAt
|
||||
Mi40MTEsLTE0LjkyYy0wLjE1LC0xLjY5NCAtMS42NDcsLTIuOTQ3IC0zLjM0LC0yLjc5N2MtMS42
|
||||
OTQsMC4xNSAtMi45NDcsMS42NDcgLTIuNzk3LDMuMzRaIi8+PHBhdGggZD0iTTM3NC4yNiwyNTku
|
||||
ODE0Yy04LjAyMiw1LjM1IC0xNS45MDEsNy44MjUgLTIzLjU5NCw3LjI3NGMtNy42OTQsLTAuNTUx
|
||||
IC0xNS4xNDgsLTQuMTI0IC0yMi4zNDIsLTEwLjU4OWMtMS4yNjUsLTEuMTM2IC0zLjIxNCwtMS4w
|
||||
MzIgLTQuMzUxLDAuMjMzYy0xLjEzNiwxLjI2NCAtMS4wMzIsMy4yMTQgMC4yMzMsNC4zNWM4LjM1
|
||||
Niw3LjUwOCAxNy4wODQsMTEuNTExIDI2LjAyLDEyLjE1MWM4LjkzNiwwLjY0IDE4LjEzNSwtMi4w
|
||||
NzggMjcuNDUzLC04LjI5NGMxLjQxNCwtMC45NDMgMS43OTcsLTIuODU4IDAuODUzLC00LjI3MmMt
|
||||
MC45NDMsLTEuNDE1IC0yLjg1OCwtMS43OTcgLTQuMjcyLC0wLjg1M1oiLz48cGF0aCBkPSJNMzc4
|
||||
LjYwNSwxNTIuMTU2Yy0zLjY0NywtMC44MjYgLTYuOTU2LDAuMDM4IC03LjM4NSwxLjkyOWMtMC40
|
||||
MjgsMS44OTEgMi4xODUsNC4wOTcgNS44MzIsNC45MjRjMy42NDcsMC44MjcgNi45NTYsLTAuMDM3
|
||||
IDcuMzg1LC0xLjkyOGMwLjQyOCwtMS44OTEgLTIuMTg1LC00LjA5OCAtNS44MzIsLTQuOTI1WiIg
|
||||
c3R5bGU9ImZpbGw6I2I3ZjhmZjsiLz48cGF0aCBkPSJNMjQ5LjUsMTc3LjI3Yy0yLjE5NSwzLjAy
|
||||
OCAtMi43MDIsNi40MSAtMS4xMzIsNy41NDhjMS41NywxLjEzOCA0LjYyNiwtMC4zOTYgNi44MjEs
|
||||
LTMuNDI0YzIuMTk1LC0zLjAyOCAyLjcwMiwtNi40MSAxLjEzMiwtNy41NDhjLTEuNTcsLTEuMTM4
|
||||
IC00LjYyNiwwLjM5NiAtNi44MjEsMy40MjRaIiBzdHlsZT0iZmlsbDojYjdmOGZmOyIvPjxwYXRo
|
||||
IGQ9Ik00MDMuNjQ4LDE2Mi44MjVjLTE5LjIxMywtMS4zNSAtMzUuOTA3LDEzLjE1MSAtMzcuMjU3
|
||||
LDMyLjM2NGMtMS4zNSwxOS4yMTIgMTMuMTUyLDM1LjkwNiAzMi4zNjQsMzcuMjU2YzE5LjIxMywx
|
||||
LjM1MSAzNS45MDcsLTEzLjE1MSAzNy4yNTcsLTMyLjM2NGMxLjM1LC0xOS4yMTIgLTEzLjE1Miwt
|
||||
MzUuOTA2IC0zMi4zNjQsLTM3LjI1NloiIHN0eWxlPSJmaWxsOiNmZmY7c3Ryb2tlOiMwMDA7c3Ry
|
||||
b2tlLXdpZHRoOjYuMTZweDsiLz48cGF0aCBkPSJNMjgxLjY5NiwxODEuNzhjLTE5LjIxMiwtMS4z
|
||||
NSAtMzUuOTA2LDEzLjE1MiAtMzcuMjU3LDMyLjM2NGMtMS4zNSwxOS4yMTIgMTMuMTUyLDM1Ljkw
|
||||
NyAzMi4zNjUsMzcuMjU3YzE5LjIxMiwxLjM1IDM1LjkwNiwtMTMuMTUyIDM3LjI1NiwtMzIuMzY0
|
||||
YzEuMzUsLTE5LjIxMyAtMTMuMTUyLC0zNS45MDcgLTMyLjM2NCwtMzcuMjU3WiIgc3R5bGU9ImZp
|
||||
bGw6I2ZmZjtzdHJva2U6IzAwMDtzdHJva2Utd2lkdGg6Ni4xNnB4OyIvPjxwYXRoIGQ9Ik0zOTgu
|
||||
NDk5LDE5Mi44ODFjLTYuODI0LC0wLjQ3OSAtMTIuNzU0LDQuNjcyIC0xMy4yMzMsMTEuNDk2Yy0w
|
||||
LjQ4LDYuODI0IDQuNjcxLDEyLjc1MyAxMS40OTUsMTMuMjMzYzYuODI0LDAuNDc5IDEyLjc1NCwt
|
||||
NC42NzIgMTMuMjMzLC0xMS40OTZjMC40OCwtNi44MjQgLTQuNjcxLC0xMi43NTMgLTExLjQ5NSwt
|
||||
MTMuMjMzWiIvPjxwYXRoIGQ9Ik0yNzguMTQyLDIxMC4yMjdjLTYuODI0LC0wLjQ3OSAtMTIuNzU0
|
||||
LDQuNjcxIC0xMy4yMzMsMTEuNDk2Yy0wLjQ4LDYuODI0IDQuNjcxLDEyLjc1MyAxMS40OTUsMTMu
|
||||
MjMzYzYuODI0LDAuNDc5IDEyLjc1NCwtNC42NzIgMTMuMjMzLC0xMS40OTZjMC40OCwtNi44MjQg
|
||||
LTQuNjcxLC0xMi43NTMgLTExLjQ5NSwtMTMuMjMzWiIvPjxwYXRoIGQ9Ik0zOTkuODkyLDE5Ny45
|
||||
NjFjLTEuOTE2LC0wLjEzNSAtMy41ODEsMS4zMTEgLTMuNzE2LDMuMjI3Yy0wLjEzNCwxLjkxNiAx
|
||||
LjMxMiwzLjU4MSAzLjIyOCwzLjcxNmMxLjkxNiwwLjEzNCAzLjU4MSwtMS4zMTIgMy43MTUsLTMu
|
||||
MjI4YzAuMTM1LC0xLjkxNiAtMS4zMTEsLTMuNTgxIC0zLjIyNywtMy43MTVaIiBzdHlsZT0iZmls
|
||||
bDojZmZmOyIvPjxwYXRoIGQ9Ik0yNzkuNTM1LDIxNS4zMDZjLTEuOTE2LC0wLjEzNCAtMy41ODEs
|
||||
MS4zMTIgLTMuNzE1LDMuMjI4Yy0wLjEzNSwxLjkxNiAxLjMxMSwzLjU4MSAzLjIyNywzLjcxNmMx
|
||||
LjkxNiwwLjEzNCAzLjU4MSwtMS4zMTIgMy43MTYsLTMuMjI4YzAuMTM0LC0xLjkxNiAtMS4zMTIs
|
||||
LTMuNTgxIC0zLjIyOCwtMy43MTZaIiBzdHlsZT0iZmlsbDojZmZmOyIvPjwvZz48L2c+PHBhdGgg
|
||||
ZD0iTTUxMi4yOTEsMzMzLjcyNmMwLC0wIC0yNy40OTYsMTMuNDAyIC00My4yNDUsMy43OTJjLTE1
|
||||
Ljc1LC05LjYxIC0xMC44LC0yNC43ODggMy4xMDIsLTIyLjUyYzEzLjkwMywyLjI2OCAyNy4xNjgs
|
||||
My4xOTUgMzUuOTQzLC0zLjkzN2M4Ljc3NiwtNy4xMzIgMjEuNjMzLDE0LjM3NiA0LjIsMjIuNjY1
|
||||
WiIgc3R5bGU9ImZpbGw6I2Q2YzI5NTtzdHJva2U6IzAwMDtzdHJva2Utd2lkdGg6Ni4xNnB4OyIv
|
||||
PjwvZz48Zz48cGF0aCBkPSJNNDQxLjI2OCwyOTEuMzQ5Yy0wLDAgMjguMDgsNDcuNDg4IC0xMjIu
|
||||
NTkxLDExNC4wNGMtMTEuNjI5LC0yLjAxNyAtMTQuMzMsLTguNTE1IC0xNC4zMywtOC41MTVjMCwt
|
||||
MCA0My42MTcsLTEyLjU4NiA4My44MTIsLTQ0Ljg3NmM0My41OTcsLTM1LjAyNCA1My4xMDksLTYw
|
||||
LjY0OSA1My4xMDksLTYwLjY0OVoiIHN0eWxlPSJmaWxsOiM3YjQ3MjI7c3Ryb2tlOiMwMDA7c3Ry
|
||||
b2tlLXdpZHRoOjYuNjhweDsiLz48cGF0aCBkPSJNMjMzLjk1NywzNzIuMTM4YzMuNTYsLTUuNTA2
|
||||
IDEwLjcyOSwtNy4zOTIgMTYuNTM4LC00LjM1MWMxNy43NjcsOS4yMzUgNTMuNjE0LDI4LjAzMiA2
|
||||
Ny4xNTQsMzYuMzc2YzUuOTcxLDMuNjggLTYuNDc3LDE2LjkxMyAtMTkuMjgzLDM3LjkyN2MtMy4z
|
||||
NjgsNS41NjcgLTEwLjQzNiw3LjYyOCAtMTYuMjY5LDQuNzQ2Yy0yMS42NTYsLTEwLjcxNiAtNDEu
|
||||
MDQ3LC0yMC45NTggLTU0LjIzNCwtMjguMDczYy00LjU5MiwtMi41MDEgLTcuOTM0LC02LjgwMiAt
|
||||
OS4yMjMsLTExLjg2OWMtMS4yODgsLTUuMDY4IC0wLjQwNywtMTAuNDQzIDIuNDMzLC0xNC44MzNj
|
||||
NC4zMzUsLTYuNzA4IDkuMTYyLC0xNC4xNyAxMi44ODQsLTE5LjkyM1oiIHN0eWxlPSJmaWxsOiM3
|
||||
YjQ3MjI7c3Ryb2tlOiMwMDA7c3Ryb2tlLXdpZHRoOjQuOTVweDsiLz48cGF0aCBkPSJNMjM5Ljg1
|
||||
LDM2MS4xNzFsLTI1Ljg0OCwyMy4yOTZsNzkuMTcsNDUuNjA1bDI1LjE5NiwtMjQuMjk1bC03OC41
|
||||
MTgsLTQ0LjYwNloiIHN0eWxlPSJmaWxsOiNhZDZjNGE7c3Ryb2tlOiMwMDA7c3Ryb2tlLXdpZHRo
|
||||
OjQuOTVweDsiLz48cGF0aCBkPSJNMjM4LjA4MSwzNzcuNzA0YzIuNTI2LDAuNTMgNC4xNDcsMy4w
|
||||
MTEgMy42MTgsNS41MzdjLTAuNTMsMi41MjYgLTMuMDExLDQuMTQ3IC01LjUzOCwzLjYxOGMtMi41
|
||||
MjYsLTAuNTMgLTQuMTQ3LC0zLjAxMSAtMy42MTcsLTUuNTM4YzAuNTMsLTIuNTI2IDMuMDExLC00
|
||||
LjE0NyA1LjUzNywtMy42MTdaIiBzdHlsZT0iZmlsbDojZmZiMjVjO3N0cm9rZTojMDAwO3N0cm9r
|
||||
ZS13aWR0aDo0Ljk1cHg7Ii8+PHBhdGggZD0iTTI5MC4zMyw0MDcuOWMyLjUyNywwLjUzIDQuMTQ3
|
||||
LDMuMDExIDMuNjE4LDUuNTM3Yy0wLjUzLDIuNTI3IC0zLjAxMSw0LjE0NyAtNS41MzcsMy42MThj
|
||||
LTIuNTI3LC0wLjUzIC00LjE0OCwtMy4wMTEgLTMuNjE4LC01LjUzN2MwLjUzLC0yLjUyNyAzLjAx
|
||||
MSwtNC4xNDggNS41MzcsLTMuNjE4WiIgc3R5bGU9ImZpbGw6I2ZmYjI1YztzdHJva2U6IzAwMDtz
|
||||
dHJva2Utd2lkdGg6NC45NXB4OyIvPjwvZz48Zz48cGF0aCBkPSJNMzA1LjMyMywzMzYuMjM1YzAs
|
||||
MCAyNC4yMDQsMTEyLjU3OCAzNS4zNTcsMTE0LjM1NWMxMS4xNTMsMS43NzYgMTgyLjk5OCwtNTcu
|
||||
MTYyIDE4Mi45OTgsLTU3LjE2MmMwLC0wIC0yNC4yNywtMTA1Ljc3MyAtMzQuNDU2LC0xMTQuNjAy
|
||||
Yy0xMC4xODYsLTguODMgLTE4My44OTksNTcuNDA5IC0xODMuODk5LDU3LjQwOVoiIHN0eWxlPSJm
|
||||
aWxsOiNmZmYyZWI7c3Ryb2tlOiMwMDA7c3Ryb2tlLXdpZHRoOjQuNjhweDsiLz48cGF0aCBkPSJN
|
||||
NDEzLjU2MywzNjcuNjU2bC0xMDguMjQsLTMxLjQyMWwxMDguMjQsMzEuNDIxWiIgc3R5bGU9ImZp
|
||||
bGw6bm9uZTtzdHJva2U6IzAwMDtzdHJva2Utd2lkdGg6NS40NnB4OyIvPjxwYXRoIGQ9Ik00ODgu
|
||||
NTY2LDI3OS4xNTZsLTc1LjAwMyw4OC41IiBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMDAwO3N0
|
||||
cm9rZS13aWR0aDo1LjQ2cHg7Ii8+PHBhdGggZD0iTTMzOS4xNjcsNDQ5LjEyN2w2Mi40MDEsLTgz
|
||||
LjUwNSIgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2U6IzAwMDtzdHJva2Utd2lkdGg6NS40NnB4OyIv
|
||||
PjxwYXRoIGQ9Ik00MjMuMjcsMzU1LjgzNWw5OS4xOTQsMzcuMDAxIiBzdHlsZT0iZmlsbDpub25l
|
||||
O3N0cm9rZTojMDAwO3N0cm9rZS13aWR0aDo1LjQ2cHg7Ii8+PC9nPjxwYXRoIGQ9Ik0zMDQuNDE2
|
||||
LDM3NC43ODVjLTAsMCAtMzAuNDYsLTIuOCAtMzguOTQzLC0xOS4xODVjLTguNDgyLC0xNi4zODQg
|
||||
My42MjIsLTI2Ljc5NCAxNC4zMzIsLTE3LjY0NmMxMC43MTEsOS4xNDkgMjEuNTcyLDE2LjgyMSAz
|
||||
Mi43NzQsMTUuMjc0YzExLjIwMSwtMS41NDcgMTEuMDQsMjMuNTExIC04LjE2MywyMS41NTdaIiBz
|
||||
dHlsZT0iZmlsbDojZDZjMjk1O3N0cm9rZTojMDAwO3N0cm9rZS13aWR0aDo2LjE2cHg7Ii8+PHBh
|
||||
dGggZD0iTTEwOS45NTEsMzExLjcxMmw2OC4wNzYsLTAiIHN0eWxlPSJmaWxsOm5vbmU7c3Ryb2tl
|
||||
OiMwMDA7c3Ryb2tlLXdpZHRoOjQuOTVweDsiLz48cGF0aCBkPSJNMTM5LjMwNSwzNTEuNjgzbDY4
|
||||
LjA3NiwwIiBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMDAwO3N0cm9rZS13aWR0aDo0Ljk1cHg7
|
||||
Ii8+PHBhdGggZD0iTTEzOS42MTcsMzMxLjY5OGwzOC4wOTgsLTAiIHN0eWxlPSJmaWxsOm5vbmU7
|
||||
c3Ryb2tlOiMwMDA7c3Ryb2tlLXdpZHRoOjQuOTVweDsiLz48cGF0aCBkPSJNODUuMjAxLDIxMS4y
|
||||
NTNjLTAsLTIuMjEzIC0xLjc5NywtNC4wMSAtNC4wMSwtNC4wMWwtNTMuNDg3LC0wYy0yLjIxNCwt
|
||||
MCAtNC4wMTEsMS43OTcgLTQuMDExLDQuMDFsMCw1OC44OTRjMCwyLjIxMyAxLjc5Nyw0LjAxIDQu
|
||||
MDExLDQuMDFsNTMuNDg3LDBjMi4yMTMsMCA0LjAxLC0xLjc5NyA0LjAxLC00LjAxbC0wLC01OC44
|
||||
OTRaIiBzdHlsZT0iZmlsbDojZWVhYzAxO3N0cm9rZTojMDAwO3N0cm9rZS13aWR0aDozLjQzcHg7
|
||||
Ii8+PHBhdGggZD0iTTU2NC42NDMsNDI2LjU5MWMwLC0yLjIxMyAtMS43OTYsLTQuMDEgLTQuMDEs
|
||||
LTQuMDFsLTUzLjQ4NywtMGMtMi4yMTMsLTAgLTQuMDEsMS43OTcgLTQuMDEsNC4wMWwwLDU4Ljg5
|
||||
NGMwLDIuMjEzIDEuNzk3LDQuMDEgNC4wMSw0LjAxbDUzLjQ4NywwYzIuMjE0LDAgNC4wMSwtMS43
|
||||
OTcgNC4wMSwtNC4wMWwwLC01OC44OTRaIiBzdHlsZT0iZmlsbDojZWVhYzAxO3N0cm9rZTojMDAw
|
||||
O3N0cm9rZS13aWR0aDozLjQzcHg7Ii8+PGNpcmNsZSBjeD0iNTE3LjAxNCIgY3k9IjEwOC43NzEi
|
||||
IHI9IjQuMzUyIiBzdHlsZT0iZmlsbDojZDNkMmIzOyIvPjxjaXJjbGUgY3g9IjUzMS4zMiIgY3k9
|
||||
IjEwOC43NzEiIHI9IjQuMzUyIiBzdHlsZT0iZmlsbDojZjRiMzAzOyIvPjxjaXJjbGUgY3g9IjU0
|
||||
NC43OTIiIGN5PSIxMDguNzcxIiByPSI0LjM1MiIgc3R5bGU9ImZpbGw6I2Y5NjEzZDsiLz48cGF0
|
||||
aCBkPSJNMTA1LjI1Nyw0ODQuNDNjLTAsMCAyMC4zNzQsLTI1LjIyNSA0MS43MTksLTcuNzYxYzIx
|
||||
LjM0NCwxNy40NjMgLTEyLjYxMyw1MC40NSAtNDAuNzQ5LDQyLjY4OSIgc3R5bGU9ImZpbGw6bm9u
|
||||
ZTtzdHJva2U6IzAwMDtzdHJva2Utd2lkdGg6OS45cHg7Ii8+PHBhdGggZD0iTTExMy4wMTksNDYw
|
||||
LjE0MWMtMCwtNC4yNjUgLTMuNDYzLC03LjcyNyAtNy43MjgsLTcuNzI3bC03OC42NTUsLTBjLTQu
|
||||
MjY1LC0wIC03LjcyNywzLjQ2MiAtNy43MjcsNy43MjdsLTAsNzguNjU1Yy0wLDQuMjY1IDMuNDYy
|
||||
LDcuNzI4IDcuNzI3LDcuNzI4bDc4LjY1NSwtMGM0LjI2NSwtMCA3LjcyOCwtMy40NjMgNy43Mjgs
|
||||
LTcuNzI4bC0wLC03OC42NTVaIiBzdHlsZT0iZmlsbDojNjFlMGVlO3N0cm9rZTojMDAwO3N0cm9r
|
||||
ZS13aWR0aDo0LjU1cHg7c3Ryb2tlLWxpbmVjYXA6c3F1YXJlOyIvPjxwYXRoIGQ9Ik03Ny4yNzcs
|
||||
Mzk1LjQwMmMtMi4yMzQsOC43NTUgLTYuNDk2LDE1LjM2NiAtMTEuNjQ5LDE3LjcxNGMxLjk5NSwz
|
||||
LjA1NyAzLjE2Nyw2Ljc4MSAzLjE2NywxMC43OTdjMCwxMC4zODcgLTcuODQsMTguODIgLTE3LjQ5
|
||||
OCwxOC44MmMtOS42NTcsMCAtMTcuNDk4LC04LjQzMyAtMTcuNDk4LC0xOC44MmMwLC05LjczMyA2
|
||||
Ljg4NCwtMTcuNzUxIDE1LjY5NywtMTguNzIyYy0zLjM5OSwtNS45NiAtNS41MjQsLTE0LjQyMyAt
|
||||
NS41MjQsLTIzLjgwNWMwLC0xOC4wMjQgNy44NDEsLTMyLjY1NyAxNy40OTksLTMyLjY1N2M2Ljc5
|
||||
MiwtMCAxMi42ODUsNy4yMzggMTUuNTg0LDE3LjgwMmMxLjYyOSwtMC41OTIgMy4zODcsLTAuOTE1
|
||||
IDUuMjIsLTAuOTE1YzguNDUsMCAxNS4zMTEsNi44NjEgMTUuMzExLDE1LjMxMWMtMCw4LjQ1IC02
|
||||
Ljg2MSwxNS4zMTEgLTE1LjMxMSwxNS4zMTFjLTEuNzUsMCAtMy40MzIsLTAuMjk0IC00Ljk5OCwt
|
||||
MC44MzZaIiBzdHlsZT0iZmlsbDojZmZmO2ZpbGwtb3BhY2l0eTowLjU7Ii8+PHBhdGggZD0iTTMz
|
||||
LjU4MywyMzMuOTNjMi41MjksLTAuMzM3IDQuNTEsLTEuMTc4IDYuNzE2LC0yLjQwM2MwLjk1Nywt
|
||||
MC41MzIgMS45NjksLTAuOTA0IDIuODk4LC0xLjQ4NWMwLjc4OSwtMC40OTMgMS4zNDMsLTAuOTc2
|
||||
IDEuMzQzLC0xLjk3OWMwLC0wLjIzNiAwLjIyNCwtMC42MzIgMCwtMC43MDdjLTAuOTUyLC0wLjMx
|
||||
NyAtMi4zNjQsMC40NjUgLTMuMDQsMC45OWMtMC42MjUsMC40ODYgLTEuNDk0LDAuNTc5IC0yLjEy
|
||||
LDEuMDZjLTEuMDM2LDAuNzk2IC0xLjY4MiwyLjQ2MyAtMS43NjcsMy43NDZjLTAuMDYsMC44OTYg
|
||||
LTAuMzU3LDIuODQgMC41NjUsMy4zOTNjMC4zNTcsMC4yMTQgMC45MjksMC4xNDIgMS4zNDMsMC4x
|
||||
NDJjMS4wOCwtMCAyLjEwNCwwLjAxMiAzLjE4MSwtMC4xNDJjMi4wODIsLTAuMjk3IDQuMTY1LC0x
|
||||
LjQ1NSA2LjAwOSwtMi40MDNjMi4wMTQsLTEuMDM2IDQuMDEyLC0xLjkyMSA1LjU4NCwtMy42MDVj
|
||||
MC42NDIsLTAuNjg4IDEuMDEsLTEuMjg2IDEuMjcyLC0yLjE5MWMwLjA1OSwtMC4yMDMgLTAuMDYs
|
||||
LTAuNzE4IDAuMDcxLC0wLjg0OWMwLjEwOCwtMC4xMDggMC4wOTksMC4yMjYgMC4wNzEsMC4yODNj
|
||||
LTAuMTg1LDAuMzY3IC0wLjQ2MiwwLjc0IC0wLjYzNywxLjEzMWMtMC4zMjQsMC43MjkgLTAuMzUz
|
||||
LDEuNzU5IC0wLjM1MywyLjU0NWMwLDAuMTMyIC0wLjA3LDAuNTcxIDAsMC42MzZjMC4wNTUsMC4w
|
||||
NTEgMC4zNjMsMC4xOTMgMC40MjQsMC4yMTJjMi4wMDIsMC42MzMgNC4zNDIsLTAuNTMyIDYuMDA5
|
||||
LC0xLjQ4NGMwLjY0MSwtMC4zNjcgMS4zNjMsLTAuODUgMi4wNiwtMS4xMTJjMC4wMjYsLTAuMDEg
|
||||
MC42NzgsLTAuMTk3IDAuNjk2LC0wLjE2MWMwLjI4MiwwLjU2MyAwLjExMSwxLjUxMSAwLjIxMiwy
|
||||
LjEyMWMwLjI4NSwxLjcwNyAyLjM2NywxLjYxNSAzLjY3NiwxLjQ4NGMwLjIwNywtMC4wMiAwLjM2
|
||||
MywtMC4yMTIgMC41NjYsLTAuMjEyIiBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMDAwO3N0cm9r
|
||||
ZS13aWR0aDoyLjc1cHg7Ii8+PHBhdGggZD0iTTM2LjQ4MiwyNDIuMzQyYzEuMzQ2LC0wIDIuNjgy
|
||||
LC0wLjE1NCA0LjAyOSwtMC4yMTJjNC4zMzIsLTAuMTg5IDguNjcxLC0wLjE5NiAxMy4wMDYsLTAu
|
||||
MjgzYzYuNTI1LC0wLjEzMSAxMy4wNTYsMC4wNzEgMTkuNTgxLDAuMDcxIiBzdHlsZT0iZmlsbDpu
|
||||
b25lO3N0cm9rZTojMDAwO3N0cm9rZS13aWR0aDoyLjc1cHg7Ii8+PHBhdGggZD0iTTUxNi4wOSw0
|
||||
NDIuMDMxYy0wLDAuOTM2IC0wLjUzMiwxLjg5MiAtMC4xMjYsMi44MzljMC41MjYsMS4yMjggMi4z
|
||||
NjMsMS4yNzUgMy40NjksMC45NDZjMC42MjgsLTAuMTg3IDEuMzE2LC0xLjE3NiAxLjcwMywtMS42
|
||||
NGMxLjAzOCwtMS4yNDcgMi40MzUsLTIuMzc1IDIuNzEyLC00LjAzN2MwLjA0MSwtMC4yNDggMC4x
|
||||
OTQsLTAuOTM2IDAuMDYzLC0xLjE5OGMtMC43NjMsLTEuNTI1IC0zLjA1OSwtMS40NzggLTQuMTY4
|
||||
LC0wLjM2OWMtMC4xNjQsMC4xNjQgLTAuNDQ2LDAuMjI3IC0wLjU2MywwLjQzMmMtMC45OTQsMS43
|
||||
NDUgLTAuNDM5LDQuMzQxIDAuNDQyLDUuOTkyYzAuMzA0LDAuNTY5IDAuODI5LDEuMDQ4IDEuMzI0
|
||||
LDEuNDUxYzAuMzM5LDAuMjc1IDAuNzc1LDAuNzE2IDEuMTk5LDAuOTI4YzMuMjYyLDEuNjMxIDcu
|
||||
Njg5LC0wLjEzMyAxMC41OTYsLTEuNzQ4YzEuOTUyLC0xLjA4NSA0LjEsLTIuMTUyIDQuMSwtNC42
|
||||
NjhjLTAsLTAuMzI1IDAuMDM3LC0wLjY5NCAtMC4wNjMsLTEuMDA5Yy0wLjM2MSwtMS4xMzMgLTIu
|
||||
MzA3LC0xLjA0MyAtMy4xNTQsLTAuNjk0Yy0xLjE3OSwwLjQ4NiAtMi4zNzMsMS4zOTUgLTMuMDks
|
||||
Mi40NmMtMS4xNzQsMS43NDIgLTEuMjA3LDQuMjgzIDAuNTY3LDUuNjE0YzEuMjU2LDAuOTQxIDIu
|
||||
OTU4LDAuOTIzIDQuNDQ3LDAuNzY2YzIuMjQ3LC0wLjIzNyA0LjI1MywtMS4zMTMgNi4wMjMsLTIu
|
||||
NjU5YzAuODc1LC0wLjY2NCAyLjA5NCwtMS4zMTYgMi42NDksLTIuMzMzYzAuMTczLC0wLjMxNiAw
|
||||
LjIxNSwtMC43NDUgMC4zNzksLTEuMDczYzAuMDcxLC0wLjE0MyAtMC4xNTgsMC4yODUgLTAuMTg5
|
||||
LDAuNDQyYy0wLjA1NiwwLjI3NiAtMC4wOTQsMC44NzggLTAsMS4xMzVjMC4zNzYsMS4wMjggMS4y
|
||||
MzMsMS41MDkgMi4yNywxLjcwM2MxLjM0NiwwLjI1MiAzLjAwOSwtMC4wMSA0LjM1MiwtMC4xODlj
|
||||
MC45MzksLTAuMTI1IDEuODkyLC0wLjUxNyAxLjg5MiwwLjc1NyIgc3R5bGU9ImZpbGw6bm9uZTtz
|
||||
dHJva2U6IzAwMDtzdHJva2Utd2lkdGg6Mi40NXB4OyIvPjxwYXRoIGQ9Ik01MTQuMjU5LDQ2MC4z
|
||||
NTljMC4yNjYsMC4xMzQgMC4wOCwxLjM1NCAwLjU2NywxLjM4OGMwLjQ0MiwwLjAzMSAwLjg4NSww
|
||||
LjA0IDEuMzI4LDAuMDVjMC41MTcsMC4wMTIgMS4xMDQsMC4wOTggMS42MzMsLTAuMDM0YzEuNjg4
|
||||
LC0wLjQyMiAzLjc5NiwtMS41OTggNC40ODIsLTMuMjk2YzAuMTg4LC0wLjQ2NyAwLjI0NiwtMS4w
|
||||
OTQgMC4zMTUsLTEuNTc3YzAuMDc1LC0wLjUyNCAwLjM3OCwtMS43MTggLTAuMDYzLC0yLjE0NGMt
|
||||
MC4wODgsLTAuMDg1IC0wLjYwNCwtMC4wNjMgLTAuNjk0LC0wLjA2M2MtMC42OTUsLTAgLTEuNzk2
|
||||
LDAuMzkyIC0yLjIwNywxLjAwOWMtMC4yNzMsMC40MDkgLTAuMTg5LDAuODUxIC0wLjE4OSwxLjMy
|
||||
NGMtMCwwLjYyIC0wLjAzMywxLjIyNCAwLjEyNiwxLjgzYzAuMzYyLDEuMzc5IDIuMTExLDEuOTQx
|
||||
IDMuMzQzLDIuMDE4YzIuNjk2LDAuMTY4IDUuNDQ3LC0wLjI0NSA4LjEzNiwtMGMyLjIwNywwLjIg
|
||||
NC40NTQsMC43MTcgNi42ODYsMC41MDRjMS41OCwtMC4xNSAzLjE1NCwtMC41MjUgNC43MywtMC42
|
||||
M2MzLjM5NCwtMC4yMjYgNi43OTEsLTAuNTA1IDEwLjIxOCwtMC41MDUiIHN0eWxlPSJmaWxsOm5v
|
||||
bmU7c3Ryb2tlOiMwMDA7c3Ryb2tlLXdpZHRoOjIuNDVweDsiLz48cGF0aCBkPSJNNTE5LjA1NCw0
|
||||
NzEuNjhsMCwwLjk0NmMwLDAuMTU1IC0wLDAuMjUxIDAuMDYzLDAuMzc5bDAsMC4xMjZjMC42MDIs
|
||||
LTIuNDA3IDMuNzc1LC0yLjc4NCA1LjgxLC0yLjc4NGMwLjE1LC0wIDAuMjMyLDAuNDggMC4yNDUs
|
||||
MC41NzZjMC4wNTUsMC4zODUgLTAuMTkyLDEuNDUgMC4zNzksMS42NGMwLjk4OSwwLjMzIDIuNDA2
|
||||
LC0wLjMxOCAzLjM0MywtMC41NjhjMC4yMzUsLTAuMDYyIDEuMDA5LC0wLjQ0MSAxLjE5OCwtMC4y
|
||||
NTJjMC45NTQsMC45NTQgMi40MTIsMC41NjIgMy43MjEsMC42MzFjMy4zMzIsMC4xNzUgNi42Mjks
|
||||
LTAuMTg5IDkuOTY2LC0wLjE4OWMxLjA1OCwtMCAyLjE2MiwwLjA4NyAzLjIxNywtMGMwLjA2OCwt
|
||||
MC4wMDYgMS4wMDksLTAuMDI3IDEuMDA5LC0wLjI1MyIgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2U6
|
||||
IzAwMDtzdHJva2Utd2lkdGg6Mi40NXB4OyIvPjwvZz48Zz48cGF0aCBkPSJNMzc0LjU3OCwxMTQu
|
||||
NDU1Yy0wLjYxMywtNC4yMTYgLTQuNTMzLC03LjE0MiAtOC43NDksLTYuNTNsLTk5Ljk0NSwxNC41
|
||||
MTVjLTQuMjE2LDAuNjEzIC03LjE0Miw0LjUzMyAtNi41Myw4Ljc0OWwyLjIxOSwxNS4yNzhjMC42
|
||||
MTIsNC4yMTYgNC41MzIsNy4xNDIgOC43NDksNi41M2w5OS45NDUsLTE0LjUxNWM0LjIxNiwtMC42
|
||||
MTIgNy4xNDIsLTQuNTMzIDYuNTI5LC04Ljc0OWwtMi4yMTgsLTE1LjI3OFoiIHN0eWxlPSJmaWxs
|
||||
OiM5ZGFjY2Q7c3Ryb2tlOiMwMDA7c3Ryb2tlLXdpZHRoOjUuMjFweDsiLz48cGF0aCBkPSJNMzEz
|
||||
Ljg2OSwxMTcuNDE0YzAsMCA2Ni4xMDIsOS44NzggNzcuNTgyLC0yMy43MzNjMTEuNDgsLTMzLjYx
|
||||
MSAtMTIuOTg3LC01MS44NzMgLTQxLjYzNywtMzkuOTIxYy0yOC42NSwxMS45NTIgLTU0Ljg1LDQ2
|
||||
LjE0MiAtNTQuODUsNDYuMTQyYzAsMCAtMzAuMDA1LC04LjEwOCAtNDEuMzE0LDUuMjIxYy0xMS4z
|
||||
MDksMTMuMzMgLTE3LjE2NywyNi42NDYgLTIuNDM1LDMxLjUxOWMxNC43MzIsNC44NzMgNjIuNjU0
|
||||
LC0xOS4yMjggNjIuNjU0LC0xOS4yMjhaIiBzdHlsZT0iZmlsbDojNzM1ZDkzO3N0cm9rZTojMDAw
|
||||
O3N0cm9rZS13aWR0aDo1LjkzcHg7Ii8+PHBhdGggZD0iTTM3NC4xNDgsMTM2Ljg3NmMtMCwwIDM4
|
||||
LjQ5MywtOS4zMzggMzYuNTYzLC0zLjU0OWMtMS45Myw1Ljc5IC0zNy42MzgsMjYuOTAxIC0xMTQu
|
||||
ODMxLDE1Ljk2NmM3MC4zNDUsLTEwLjczMSA3OC4yNjgsLTEyLjQxNyA3OC4yNjgsLTEyLjQxN1oi
|
||||
IHN0eWxlPSJmaWxsOiM1MDNhNzE7c3Ryb2tlOiMwMDA7c3Ryb2tlLXdpZHRoOjQuOTVweDsiLz48
|
||||
cGF0aCBkPSJNMzU3LjM0NiwxMjQuOTA2bC01NC4wMjEsOS42MTJsLTAsNS4zOTlsNTQuMDIxLC05
|
||||
LjYxMmwwLC01LjM5OVoiIHN0eWxlPSJmaWxsOiNmZmY7Ii8+PGNpcmNsZSBjeD0iMzAwLjE1IiBj
|
||||
eT0iMTM3LjE5OCIgcj0iNC42NzciIHN0eWxlPSJmaWxsOiNmZmIyNWM7c3Ryb2tlOiMwMDA7c3Ry
|
||||
b2tlLXdpZHRoOjQuOTVweDsiLz48Y2lyY2xlIGN4PSIzNjEuNDEiIGN5PSIxMjcuNTgxIiByPSI0
|
||||
LjI4MiIgc3R5bGU9ImZpbGw6I2ZmYjI1YztzdHJva2U6IzAwMDtzdHJva2Utd2lkdGg6NC45NXB4
|
||||
OyIvPjwvZz48L3N2Zz4=
|
||||
`
|
||||
|
||||
var (
|
||||
errMockDefault = errors.New("mock write error")
|
||||
errMockNewline = errors.New("mock newline error")
|
||||
errClosedWriter = errors.New("writer is already closed")
|
||||
errMockDefault = errors.New("mock write error")
|
||||
errMockNewline = errors.New("mock newline error")
|
||||
)
|
||||
|
||||
// TestBase64LineBreaker tests the Write and Close methods of the Base64LineBreaker
|
||||
func TestBase64LineBreaker(t *testing.T) {
|
||||
l, err := os.Open("assets/gopher2.svg")
|
||||
if err != nil {
|
||||
t.Errorf("failed to open gopher logo asset: %s", err)
|
||||
return
|
||||
}
|
||||
defer func() { _ = l.Close() }()
|
||||
|
||||
var wbuf bytes.Buffer
|
||||
lb := Base64LineBreaker{out: &wbuf}
|
||||
bw := base64.NewEncoder(base64.StdEncoding, &lb)
|
||||
if _, err := io.Copy(bw, l); err != nil {
|
||||
t.Errorf("failed to write logo asset to line breaker: %s", err)
|
||||
return
|
||||
}
|
||||
if err := bw.Close(); err != nil {
|
||||
t.Errorf("failed to close b64 encoder: %s", err)
|
||||
}
|
||||
if err := lb.Close(); err != nil {
|
||||
t.Errorf("failed to close line breaker: %s", err)
|
||||
}
|
||||
ob := removeNewLines([]byte(logoB64))
|
||||
nb := removeNewLines(wbuf.Bytes())
|
||||
if string(ob) != string(nb) {
|
||||
t.Errorf("generated line breaker output differs from original data")
|
||||
}
|
||||
}
|
||||
|
||||
// TestBase64LineBreakerFailures tests the cases in which the Base64LineBreaker would fail
|
||||
func TestBase64LineBreakerFailures(t *testing.T) {
|
||||
stt := []byte("short")
|
||||
ltt := []byte(logoB64)
|
||||
|
||||
// No output writer defined
|
||||
lb := Base64LineBreaker{}
|
||||
if _, err := lb.Write(stt); err == nil {
|
||||
t.Errorf("writing to Base64LineBreaker with no output io.Writer was supposed to failed, but didn't")
|
||||
}
|
||||
if err := lb.Close(); err != nil {
|
||||
t.Errorf("failed to close Base64LineBreaker: %s", err)
|
||||
}
|
||||
|
||||
// Closed output writer
|
||||
wbuf := errorWriter{}
|
||||
fb := Base64LineBreaker{out: wbuf}
|
||||
if _, err := fb.Write(ltt); err == nil {
|
||||
t.Errorf("writing to Base64LineBreaker with errorWriter was supposed to failed, but didn't")
|
||||
}
|
||||
if err := fb.Close(); err != nil {
|
||||
t.Errorf("failed to close Base64LineBreaker: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBase64LineBreaker_WriteAndClose(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
data []byte
|
||||
writer io.Writer
|
||||
}{
|
||||
{
|
||||
name: "Write data within MaxBodyLength",
|
||||
data: []byte("testdata"),
|
||||
writer: &mockWriterExcess{writeError: errMockDefault},
|
||||
},
|
||||
{
|
||||
name: "Write data exceeds MaxBodyLength",
|
||||
data: []byte("verylongtestdataverylongtestdataverylongtestdata" +
|
||||
"verylongtestdataverylongtestdataverylongtestdata"),
|
||||
writer: &mockWriterExcess{writeError: errMockDefault},
|
||||
},
|
||||
{
|
||||
name: "Write data exceeds MaxBodyLength with newline",
|
||||
data: []byte("verylongtestdataverylongtestdataverylongtestdata" +
|
||||
"verylongtestdataverylongtestdataverylongtestdata"),
|
||||
writer: &mockWriterNewline{writeError: errMockDefault},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
blr := &Base64LineBreaker{out: tt.writer}
|
||||
|
||||
_, err := blr.Write(tt.data)
|
||||
if err != nil && !errors.Is(err, errMockDefault) && !errors.Is(err, errMockNewline) {
|
||||
t.Errorf("Unexpected error while writing: %v", err)
|
||||
}
|
||||
err = blr.Close()
|
||||
if err != nil && !errors.Is(err, errMockDefault) && !errors.Is(err, errMockNewline) {
|
||||
t.Errorf("Unexpected error while closing: %v", err)
|
||||
t.Run("write, copy and close", func(t *testing.T) {
|
||||
logoWriter := bytes.NewBuffer(nil)
|
||||
lineBreaker := &Base64LineBreaker{out: logoWriter}
|
||||
t.Cleanup(func() {
|
||||
if err := lineBreaker.Close(); err != nil {
|
||||
t.Errorf("failed to close line breaker: %s", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
if _, err := lineBreaker.Write([]byte("testdata")); err != nil {
|
||||
t.Errorf("failed to write to line breaker: %s", err)
|
||||
}
|
||||
})
|
||||
t.Run("write actual data and compare with expected results", func(t *testing.T) {
|
||||
logo, err := os.Open("testdata/logo.svg")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to open test data file: %s", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if err := logo.Close(); err != nil {
|
||||
t.Errorf("failed to close test data file: %s", err)
|
||||
}
|
||||
})
|
||||
|
||||
logoWriter := bytes.NewBuffer(nil)
|
||||
lineBreaker := &Base64LineBreaker{out: logoWriter}
|
||||
t.Cleanup(func() {
|
||||
if err := lineBreaker.Close(); err != nil {
|
||||
t.Errorf("failed to close line breaker: %s", err)
|
||||
}
|
||||
})
|
||||
base64Encoder := base64.NewEncoder(base64.StdEncoding, lineBreaker)
|
||||
t.Cleanup(func() {
|
||||
if err := base64Encoder.Close(); err != nil {
|
||||
t.Errorf("failed to close base64 encoder: %s", err)
|
||||
}
|
||||
})
|
||||
copiedBytes, err := io.Copy(base64Encoder, logo)
|
||||
if err != nil {
|
||||
t.Errorf("failed to copy test data to line breaker: %s", err)
|
||||
}
|
||||
if err = base64Encoder.Close(); err != nil {
|
||||
t.Errorf("failed to close base64 encoder: %s", err)
|
||||
}
|
||||
if err = lineBreaker.Close(); err != nil {
|
||||
t.Errorf("failed to close line breaker: %s", err)
|
||||
}
|
||||
|
||||
logoStat, err := os.Stat("testdata/logo.svg")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to stat test data file: %s", err)
|
||||
}
|
||||
if logoStat.Size() != copiedBytes {
|
||||
t.Errorf("copied %d bytes, but expected %d bytes", copiedBytes, logoStat.Size())
|
||||
}
|
||||
|
||||
expectedRaw, err := os.ReadFile("testdata/logo.svg.base64")
|
||||
if err != nil {
|
||||
t.Errorf("failed to read expected base64 data from file: %s", err)
|
||||
}
|
||||
expected := removeNewLines(t, expectedRaw)
|
||||
got := removeNewLines(t, logoWriter.Bytes())
|
||||
if !bytes.EqualFold(expected, got) {
|
||||
t.Errorf("generated line breaker output differs from expected data")
|
||||
}
|
||||
})
|
||||
t.Run("fail with no writer defined", func(t *testing.T) {
|
||||
lineBreaker := &Base64LineBreaker{}
|
||||
_, err := lineBreaker.Write([]byte("testdata"))
|
||||
if err == nil {
|
||||
t.Errorf("writing to Base64LineBreaker with no output io.Writer was supposed to failed, but didn't")
|
||||
}
|
||||
if !errors.Is(err, ErrNoOutWriter) {
|
||||
t.Errorf("unexpected error while writing to empty Base64LineBreaker: %s", err)
|
||||
}
|
||||
if err := lineBreaker.Close(); err != nil {
|
||||
t.Errorf("failed to close Base64LineBreaker: %s", err)
|
||||
}
|
||||
})
|
||||
t.Run("write on an already closed output writer", func(t *testing.T) {
|
||||
logo, err := os.Open("testdata/logo.svg")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to open test data file: %s", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if err := logo.Close(); err != nil {
|
||||
t.Errorf("failed to close test data file: %s", err)
|
||||
}
|
||||
})
|
||||
|
||||
writeBuffer := &errorWriter{}
|
||||
lineBreaker := &Base64LineBreaker{out: writeBuffer}
|
||||
_, err = io.Copy(lineBreaker, logo)
|
||||
if err == nil {
|
||||
t.Errorf("writing to Base64LineBreaker with an already closed output io.Writer was " +
|
||||
"supposed to failed, but didn't")
|
||||
}
|
||||
if !errors.Is(err, errClosedWriter) {
|
||||
t.Errorf("unexpected error while writing to Base64LineBreaker: %s", err)
|
||||
}
|
||||
})
|
||||
t.Run("fail on different scenarios with mock writer", func(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
data []byte
|
||||
writer io.Writer
|
||||
}{
|
||||
{
|
||||
name: "write data within MaxBodyLength",
|
||||
data: []byte("testdata"),
|
||||
writer: &mockWriterExcess{writeError: errMockDefault},
|
||||
},
|
||||
{
|
||||
name: "write data exceeds MaxBodyLength",
|
||||
data: []byte("verylongtestdataverylongtestdataverylongtestdata" +
|
||||
"verylongtestdataverylongtestdataverylongtestdata"),
|
||||
writer: &mockWriterExcess{writeError: errMockDefault},
|
||||
},
|
||||
{
|
||||
name: "write data exceeds MaxBodyLength with newline",
|
||||
data: []byte("verylongtestdataverylongtestdataverylongtestdata" +
|
||||
"verylongtestdataverylongtestdataverylongtestdata"),
|
||||
writer: &mockWriterNewline{writeError: errMockDefault},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
lineBreaker := &Base64LineBreaker{out: tt.writer}
|
||||
|
||||
_, err := lineBreaker.Write(tt.data)
|
||||
if err != nil && !errors.Is(err, errMockDefault) && !errors.Is(err, errMockNewline) {
|
||||
t.Errorf("unexpected error while writing to mock writer: %s", err)
|
||||
}
|
||||
err = lineBreaker.Close()
|
||||
if err != nil && !errors.Is(err, errMockDefault) && !errors.Is(err, errMockNewline) {
|
||||
t.Errorf("unexpected error while closing mock writer: %s", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// removeNewLines removes any newline characters from the given data
|
||||
func removeNewLines(data []byte) []byte {
|
||||
// removeNewLines is a test helper thatremoves all newline characters ('\r' and '\n') from the given byte slice.
|
||||
func removeNewLines(t *testing.T, data []byte) []byte {
|
||||
t.Helper()
|
||||
result := make([]byte, len(data))
|
||||
n := 0
|
||||
|
||||
|
@ -503,11 +181,11 @@ func removeNewLines(data []byte) []byte {
|
|||
type errorWriter struct{}
|
||||
|
||||
func (e errorWriter) Write([]byte) (int, error) {
|
||||
return 0, fmt.Errorf("supposed to always fail")
|
||||
return 0, errClosedWriter
|
||||
}
|
||||
|
||||
func (e errorWriter) Close() error {
|
||||
return fmt.Errorf("supposed to always fail")
|
||||
return errClosedWriter
|
||||
}
|
||||
|
||||
type mockWriterExcess struct {
|
||||
|
@ -539,19 +217,49 @@ func (w *mockWriterNewline) Write(p []byte) (n int, err error) {
|
|||
}
|
||||
}
|
||||
|
||||
func FuzzBase64LineBreaker_Write(f *testing.F) {
|
||||
f.Add([]byte("abc"))
|
||||
f.Add([]byte("def"))
|
||||
f.Add([]uint8{0o0, 0o1, 0o2, 30, 255})
|
||||
buf := bytes.Buffer{}
|
||||
bw := bufio.NewWriter(&buf)
|
||||
func FuzzBase64LineBreaker(f *testing.F) {
|
||||
seedData := [][]byte{
|
||||
[]byte(""),
|
||||
[]byte("abc"),
|
||||
[]byte("def"),
|
||||
[]byte("Hello, World!"),
|
||||
[]byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!§$%&/()=?`{[]}\\|^~*+#-._'"),
|
||||
[]byte("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"),
|
||||
bytes.Repeat([]byte("A"), MaxBodyLength-1), // Near the line length limit
|
||||
bytes.Repeat([]byte("A"), MaxBodyLength), // Exactly the line length limit
|
||||
bytes.Repeat([]byte("A"), MaxBodyLength+1), // Slightly above the line length limit
|
||||
bytes.Repeat([]byte("A"), MaxBodyLength*3), // Tripple exceed the line length limit
|
||||
bytes.Repeat([]byte("A"), MaxBodyLength*10), // Tenfold exceed the line length limit
|
||||
{0o0, 0o1, 0o2, 30, 255},
|
||||
}
|
||||
for _, data := range seedData {
|
||||
f.Add(data)
|
||||
}
|
||||
|
||||
f.Fuzz(func(t *testing.T, data []byte) {
|
||||
b := &Base64LineBreaker{out: bw}
|
||||
if _, err := b.Write(data); err != nil {
|
||||
t.Errorf("failed to write to B64LineBreaker: %s", err)
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
lineBreaker := &Base64LineBreaker{
|
||||
out: buffer,
|
||||
}
|
||||
if err := b.Close(); err != nil {
|
||||
t.Errorf("failed to close B64LineBreaker: %s", err)
|
||||
base64Encoder := base64.NewEncoder(base64.StdEncoding, lineBreaker)
|
||||
|
||||
_, err := base64Encoder.Write(data)
|
||||
if err != nil {
|
||||
t.Errorf("failed to write test data to base64 encoder: %s", err)
|
||||
}
|
||||
if err = base64Encoder.Close(); err != nil {
|
||||
t.Errorf("failed to close base64 encoder: %s", err)
|
||||
}
|
||||
if err = lineBreaker.Close(); err != nil {
|
||||
t.Errorf("failed to close base64 line breaker: %s", err)
|
||||
}
|
||||
|
||||
decode, err := base64.StdEncoding.DecodeString(buffer.String())
|
||||
if err != nil {
|
||||
t.Errorf("failed to decode base64 data: %s", err)
|
||||
}
|
||||
if !bytes.Equal(data, decode) {
|
||||
t.Error("generated line breaker output differs from original data")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
32
client.go
32
client.go
|
@ -242,6 +242,12 @@ var (
|
|||
// provided as argument to the WithDSN Option.
|
||||
ErrInvalidDSNRcptNotifyCombination = errors.New("DSN rcpt notify option NEVER cannot be " +
|
||||
"combined with any of SUCCESS, FAILURE or DELAY")
|
||||
|
||||
// ErrSMTPAuthMethodIsNil indicates that the SMTP authentication method provided is nil
|
||||
ErrSMTPAuthMethodIsNil = errors.New("SMTP auth method is nil")
|
||||
|
||||
// ErrDialContextFuncIsNil indicates that a required dial context function is not provided.
|
||||
ErrDialContextFuncIsNil = errors.New("dial context function is nil")
|
||||
)
|
||||
|
||||
// NewClient creates a new Client instance with the provided host and optional configuration Option functions.
|
||||
|
@ -510,6 +516,9 @@ func WithSMTPAuth(authtype SMTPAuthType) Option {
|
|||
// - An Option function that sets the custom SMTP authentication for the Client.
|
||||
func WithSMTPAuthCustom(smtpAuth smtp.Auth) Option {
|
||||
return func(c *Client) error {
|
||||
if smtpAuth == nil {
|
||||
return ErrSMTPAuthMethodIsNil
|
||||
}
|
||||
c.smtpAuth = smtpAuth
|
||||
c.smtpAuthType = SMTPAuthCustom
|
||||
return nil
|
||||
|
@ -671,6 +680,9 @@ func WithoutNoop() Option {
|
|||
// - An Option function that sets the custom DialContextFunc for the Client.
|
||||
func WithDialContextFunc(dialCtxFunc DialContextFunc) Option {
|
||||
return func(c *Client) error {
|
||||
if dialCtxFunc == nil {
|
||||
return ErrDialContextFuncIsNil
|
||||
}
|
||||
c.dialContextFunc = dialCtxFunc
|
||||
return nil
|
||||
}
|
||||
|
@ -739,6 +751,7 @@ func (c *Client) SetTLSPolicy(policy TLSPolicy) {
|
|||
func (c *Client) SetTLSPortPolicy(policy TLSPolicy) {
|
||||
if c.port == DefaultPort {
|
||||
c.port = DefaultPortTLS
|
||||
c.fallbackPort = 0
|
||||
|
||||
if policy == TLSOpportunistic {
|
||||
c.fallbackPort = DefaultPort
|
||||
|
@ -1081,10 +1094,6 @@ func (c *Client) DialAndSendWithContext(ctx context.Context, messages ...*Msg) e
|
|||
// - An error if the connection check fails, if no supported authentication method is found,
|
||||
// or if the authentication process fails.
|
||||
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 {
|
||||
hasSMTPAuth, smtpAuthType := c.smtpClient.Extension("AUTH")
|
||||
if !hasSMTPAuth {
|
||||
|
@ -1252,14 +1261,13 @@ func (c *Client) sendSingleMsg(message *Msg) error {
|
|||
affectedMsg: message,
|
||||
}
|
||||
}
|
||||
message.isDelivered = true
|
||||
|
||||
if err = writer.Close(); err != nil {
|
||||
return &SendError{
|
||||
Reason: ErrSMTPDataClose, errlist: []error{err}, isTemp: isTempError(err),
|
||||
affectedMsg: message,
|
||||
}
|
||||
}
|
||||
message.isDelivered = true
|
||||
|
||||
if err = c.Reset(); err != nil {
|
||||
return &SendError{
|
||||
|
@ -1267,12 +1275,6 @@ func (c *Client) sendSingleMsg(message *Msg) error {
|
|||
affectedMsg: message,
|
||||
}
|
||||
}
|
||||
if err = c.checkConn(); err != nil {
|
||||
return &SendError{
|
||||
Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err),
|
||||
affectedMsg: message,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -1289,6 +1291,9 @@ func (c *Client) sendSingleMsg(message *Msg) error {
|
|||
// - An error if there is no active connection, if the NOOP command fails, or if extending
|
||||
// the deadline fails; otherwise, returns nil.
|
||||
func (c *Client) checkConn() error {
|
||||
if c.smtpClient == nil {
|
||||
return ErrNoActiveConnection
|
||||
}
|
||||
if !c.smtpClient.HasConnection() {
|
||||
return ErrNoActiveConnection
|
||||
}
|
||||
|
@ -1347,9 +1352,6 @@ func (c *Client) setDefaultHelo() error {
|
|||
// - An error if there is no active connection, if STARTTLS is required but not supported,
|
||||
// or if there are issues during the TLS handshake; otherwise, returns nil.
|
||||
func (c *Client) tls() error {
|
||||
if !c.smtpClient.HasConnection() {
|
||||
return ErrNoActiveConnection
|
||||
}
|
||||
if !c.useSSL && c.tlspolicy != NoTLS {
|
||||
hasStartTLS := false
|
||||
extension, _ := c.smtpClient.Extension("STARTTLS")
|
||||
|
|
126
client_121_test.go
Normal file
126
client_121_test.go
Normal file
|
@ -0,0 +1,126 @@
|
|||
// SPDX-FileCopyrightText: 2024 The go-mail Authors
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build go1.21
|
||||
// +build go1.21
|
||||
|
||||
package mail
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/wneessen/go-mail/log"
|
||||
)
|
||||
|
||||
func TestNewClientNewVersionsOnly(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
option Option
|
||||
expectFunc func(c *Client) error
|
||||
shouldfail bool
|
||||
expectErr *error
|
||||
}{
|
||||
{
|
||||
"WithLogger log.JSONlog", WithLogger(log.NewJSON(os.Stderr, log.LevelDebug)),
|
||||
func(c *Client) error {
|
||||
if c.logger == nil {
|
||||
return errors.New("failed to set logger. Want logger bug got got nil")
|
||||
}
|
||||
loggerType := reflect.TypeOf(c.logger).String()
|
||||
if loggerType != "*log.JSONlog" {
|
||||
return fmt.Errorf("failed to set logger. Want logger type: %s, got: %s",
|
||||
"*log.JSONlog", loggerType)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
false, nil,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
client, err := NewClient(DefaultHost, tt.option)
|
||||
if !tt.shouldfail && err != nil {
|
||||
t.Fatalf("failed to create new client: %s", err)
|
||||
}
|
||||
if tt.shouldfail && err == nil {
|
||||
t.Errorf("client creation was supposed to fail, but it didn't")
|
||||
}
|
||||
if tt.shouldfail && tt.expectErr != nil {
|
||||
if !errors.Is(err, *tt.expectErr) {
|
||||
t.Errorf("error for NewClient mismatch. Expected: %s, got: %s",
|
||||
*tt.expectErr, err)
|
||||
}
|
||||
}
|
||||
if tt.expectFunc != nil {
|
||||
if err = tt.expectFunc(client); err != nil {
|
||||
t.Errorf("NewClient with custom option failed: %s", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_DialWithContextNewVersionsOnly(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
PortAdder.Add(1)
|
||||
serverPort := int(TestServerPortBase + PortAdder.Load())
|
||||
featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"
|
||||
go func() {
|
||||
if err := simpleSMTPServer(ctx, t, &serverProps{FeatureSet: featureSet, ListenPort: serverPort}); err != nil {
|
||||
t.Errorf("failed to start test server: %s", err)
|
||||
return
|
||||
}
|
||||
}()
|
||||
time.Sleep(time.Millisecond * 30)
|
||||
t.Run("connect with full debug logging and auth logging", func(t *testing.T) {
|
||||
ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500)
|
||||
t.Cleanup(cancelDial)
|
||||
|
||||
logBuffer := bytes.NewBuffer(nil)
|
||||
client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS),
|
||||
WithDebugLog(), WithLogAuthData(), WithLogger(log.NewJSON(logBuffer, log.LevelDebug)),
|
||||
WithSMTPAuth(SMTPAuthPlain), WithUsername("test"), WithPassword("password"))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create new client: %s", err)
|
||||
}
|
||||
|
||||
if err = client.DialWithContext(ctxDial); err != nil {
|
||||
var netErr net.Error
|
||||
if errors.As(err, &netErr) && netErr.Timeout() {
|
||||
t.Skip("failed to connect to the test server due to timeout")
|
||||
}
|
||||
t.Fatalf("failed to connect to the test server: %s", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if err = client.Close(); err != nil {
|
||||
t.Errorf("failed to close the client: %s", err)
|
||||
}
|
||||
})
|
||||
|
||||
logs := parseJSONLog(t, logBuffer)
|
||||
if len(logs.Lines) == 0 {
|
||||
t.Errorf("failed to enable debug logging, but no logs were found")
|
||||
}
|
||||
authFound := false
|
||||
for _, logline := range logs.Lines {
|
||||
if strings.EqualFold(logline.Message, "AUTH PLAIN AHRlc3QAcGFzc3dvcmQ=") &&
|
||||
logline.Direction.From == "client" && logline.Direction.To == "server" {
|
||||
authFound = true
|
||||
}
|
||||
}
|
||||
if !authFound {
|
||||
t.Errorf("logAuthData not working, no authentication info found in logs")
|
||||
}
|
||||
})
|
||||
}
|
6033
client_test.go
6033
client_test.go
File diff suppressed because it is too large
Load diff
|
@ -6,17 +6,17 @@ coverage:
|
|||
status:
|
||||
project:
|
||||
default:
|
||||
target: 85%
|
||||
threshold: 5%
|
||||
target: 90%
|
||||
threshold: 2%
|
||||
base: auto
|
||||
if_ci_failed: error
|
||||
only_pulls: false
|
||||
patch:
|
||||
default:
|
||||
target: 80%
|
||||
target: 90%
|
||||
base: auto
|
||||
if_ci_failed: error
|
||||
threshold: 5%
|
||||
threshold: 2%
|
||||
|
||||
comment:
|
||||
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)
|
||||
}
|
||||
|
||||
if err := parseEML(parsedMsg, bodybuf, msg); err != nil {
|
||||
if err = parseEML(parsedMsg, bodybuf, msg); err != nil {
|
||||
return msg, fmt.Errorf("failed to parse EML contents: %w", err)
|
||||
}
|
||||
|
||||
|
@ -93,7 +93,7 @@ func EMLToMsgFromFile(filePath string) (*Msg, error) {
|
|||
return msg, fmt.Errorf("failed to parse EML file: %w", err)
|
||||
}
|
||||
|
||||
if err := parseEML(parsedMsg, bodybuf, msg); err != nil {
|
||||
if err = parseEML(parsedMsg, bodybuf, msg); err != nil {
|
||||
return msg, fmt.Errorf("failed to parse EML contents: %w", err)
|
||||
}
|
||||
|
||||
|
@ -218,9 +218,9 @@ func parseEMLHeaders(mailHeader *netmail.Header, msg *Msg) error {
|
|||
for _, addr := range parsedAddrs {
|
||||
addrStrings = append(addrStrings, addr.String())
|
||||
}
|
||||
if err = addrFunc(addrStrings...); err != nil {
|
||||
return fmt.Errorf(`failed to parse %q header: %w`, addrHeader, err)
|
||||
}
|
||||
// We can skip the error checking here since netmail.ParseAddressList already performed the
|
||||
// same address checking that the msg methods do.
|
||||
_ = addrFunc(addrStrings...)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -600,6 +600,8 @@ func parseEMLAttachmentEmbed(contentDisposition []string, multiPart *multipart.P
|
|||
if err := msg.EmbedReader(filename, dataReader); err != nil {
|
||||
return fmt.Errorf("failed to embed multipart body: %w", err)
|
||||
}
|
||||
default:
|
||||
return errors.New("unsupported content disposition type")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
588
eml_test.go
588
eml_test.go
|
@ -6,11 +6,8 @@ package mail
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -22,6 +19,23 @@ Subject: Saying Hello
|
|||
Date: Fri, 21 Nov 1997 09:55:06 -0600
|
||||
Message-ID: <1234@local.machine.example>
|
||||
|
||||
This is a message just to say hello.
|
||||
So, "Hello".`
|
||||
exampleMailRFC5322A11InvalidFrom = `From: §§§§§§§§§
|
||||
To: Mary Smith <mary@example.net>
|
||||
Subject: Saying Hello
|
||||
Date: Fri, 21 Nov 1997 09:55:06 -0600
|
||||
Message-ID: <1234@local.machine.example>
|
||||
|
||||
This is a message just to say hello.
|
||||
So, "Hello".`
|
||||
exampleMailInvalidHeader = `From: John Doe <jdoe@machine.example>
|
||||
To: Mary Smith <mary@example.net>
|
||||
Inva@id*Header; This is a header
|
||||
Subject: Saying Hello
|
||||
Date: Fri, 21 Nov 1997 09:55:06 -0600
|
||||
Message-ID: <1234@local.machine.example>
|
||||
|
||||
This is a message just to say hello.
|
||||
So, "Hello".`
|
||||
exampleMailPlainNoEnc = `Date: Wed, 01 Nov 2023 00:00:00 +0000
|
||||
|
@ -42,6 +56,52 @@ This is a test mail. Please do not reply to this. Also this line is very long so
|
|||
should be wrapped.
|
||||
|
||||
|
||||
Thank your for your business!
|
||||
The go-mail team
|
||||
|
||||
--
|
||||
This is a signature`
|
||||
exampleMailPlainInvalidCTE = `Date: Wed, 01 Nov 2023 00:00:00 +0000
|
||||
MIME-Version: 1.0
|
||||
Message-ID: <1305604950.683004066175.AAAAAAAAaaaaaaaaB@go-mail.dev>
|
||||
Subject: Example mail // plain text without encoding
|
||||
User-Agent: go-mail v0.4.0 // https://github.com/wneessen/go-mail
|
||||
X-Mailer: go-mail v0.4.0 // https://github.com/wneessen/go-mail
|
||||
From: "Toni Tester" <go-mail@go-mail.dev>
|
||||
To: <go-mail+test@go-mail.dev>
|
||||
Cc: <go-mail+cc@go-mail.dev>
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: invalid
|
||||
|
||||
Dear Customer,
|
||||
|
||||
This is a test mail. Please do not reply to this. Also this line is very long so it
|
||||
should be wrapped.
|
||||
|
||||
|
||||
Thank your for your business!
|
||||
The go-mail team
|
||||
|
||||
--
|
||||
This is a signature`
|
||||
exampleMailInvalidContentType = `Date: Wed, 01 Nov 2023 00:00:00 +0000
|
||||
MIME-Version: 1.0
|
||||
Message-ID: <1305604950.683004066175.AAAAAAAAaaaaaaaaB@go-mail.dev>
|
||||
Subject: Example mail // plain text without encoding
|
||||
User-Agent: go-mail v0.4.0 // https://github.com/wneessen/go-mail
|
||||
X-Mailer: go-mail v0.4.0 // https://github.com/wneessen/go-mail
|
||||
From: "Toni Tester" <go-mail@go-mail.dev>
|
||||
To: <go-mail+test@go-mail.dev>
|
||||
Cc: <go-mail+cc@go-mail.dev>
|
||||
Content-Type: text/plain @ charset=UTF-8; $foo; bar; --invalid--
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
Dear Customer,
|
||||
|
||||
This is a test mail. Please do not reply to this. Also this line is very long so it
|
||||
should be wrapped.
|
||||
|
||||
|
||||
Thank your for your business!
|
||||
The go-mail team
|
||||
|
||||
|
@ -304,6 +364,128 @@ VGhpcyBpcyBhIHNpbXBsZSB0ZXN0IHRleHQgZmlsZSBhdHRhY2htZW50LgoKSXQgCiAgaGFzCiAg
|
|||
ICAgc2V2ZXJhbAogICAgICAgICAgICBuZXdsaW5lcwoJICAgICAgICAgICAgYW5kCgkgICAgc3Bh
|
||||
Y2VzCiAgICAgaW4KICBpdAouCgpBcyB3ZWxsIGFzIGFuIGVtb2ppOiDwn5mCCg==
|
||||
|
||||
--45c75ff528359022eb03679fbe91877d75343f2e1f8193e349deffa33ff7--`
|
||||
exampleMailPlainB64WithAttachmentNoContentType = `Date: Wed, 01 Nov 2023 00:00:00 +0000
|
||||
MIME-Version: 1.0
|
||||
Message-ID: <1305604950.683004066175.AAAAAAAAaaaaaaaaB@go-mail.dev>
|
||||
Subject: Example mail // plain text base64 with attachment
|
||||
User-Agent: go-mail v0.4.1 // https://github.com/wneessen/go-mail
|
||||
X-Mailer: go-mail v0.4.1 // https://github.com/wneessen/go-mail
|
||||
From: "Toni Tester" <go-mail@go-mail.dev>
|
||||
To: <go-mail+test@go-mail.dev>
|
||||
Cc: <go-mail+cc@go-mail.dev>
|
||||
Content-Type: multipart/mixed;
|
||||
boundary=45c75ff528359022eb03679fbe91877d75343f2e1f8193e349deffa33ff7
|
||||
|
||||
--45c75ff528359022eb03679fbe91877d75343f2e1f8193e349deffa33ff7
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
RGVhciBDdXN0b21lciwKClRoaXMgaXMgYSB0ZXN0IG1haWwuIFBsZWFzZSBkbyBub3QgcmVwbHkg
|
||||
dG8gdGhpcy4gQWxzbyB0aGlzIGxpbmUgaXMgdmVyeSBsb25nIHNvIGl0CnNob3VsZCBiZSB3cmFw
|
||||
cGVkLgoKClRoYW5rIHlvdXIgZm9yIHlvdXIgYnVzaW5lc3MhClRoZSBnby1tYWlsIHRlYW0KCi0t
|
||||
ClRoaXMgaXMgYSBzaWduYXR1cmU=
|
||||
|
||||
--45c75ff528359022eb03679fbe91877d75343f2e1f8193e349deffa33ff7
|
||||
Content-Disposition: attachment; filename="test.attachment"
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
VGhpcyBpcyBhIHNpbXBsZSB0ZXN0IHRleHQgZmlsZSBhdHRhY2htZW50LgoKSXQgCiAgaGFzCiAg
|
||||
ICAgc2V2ZXJhbAogICAgICAgICAgICBuZXdsaW5lcwoJICAgICAgICAgICAgYW5kCgkgICAgc3Bh
|
||||
Y2VzCiAgICAgaW4KICBpdAouCgpBcyB3ZWxsIGFzIGFuIGVtb2ppOiDwn5mCCg==
|
||||
|
||||
--45c75ff528359022eb03679fbe91877d75343f2e1f8193e349deffa33ff7--`
|
||||
exampleMailPlainB64WithAttachmentBrokenB64 = `Date: Wed, 01 Nov 2023 00:00:00 +0000
|
||||
MIME-Version: 1.0
|
||||
Message-ID: <1305604950.683004066175.AAAAAAAAaaaaaaaaB@go-mail.dev>
|
||||
Subject: Example mail // plain text base64 with attachment
|
||||
User-Agent: go-mail v0.4.1 // https://github.com/wneessen/go-mail
|
||||
X-Mailer: go-mail v0.4.1 // https://github.com/wneessen/go-mail
|
||||
From: "Toni Tester" <go-mail@go-mail.dev>
|
||||
To: <go-mail+test@go-mail.dev>
|
||||
Cc: <go-mail+cc@go-mail.dev>
|
||||
Content-Type: multipart/mixed;
|
||||
boundary=45c75ff528359022eb03679fbe91877d75343f2e1f8193e349deffa33ff7
|
||||
|
||||
--45c75ff528359022eb03679fbe91877d75343f2e1f8193e349deffa33ff7
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
|
||||
RGVhciBDdXN0b21lciwKClRoaXMgaXMgYSB0ZXN0IG1haWwuIFBsZWFzZSBkbyBub3QgcmVwbHkg
|
||||
dG8gdGhpcy4gQWxzbyB0aGl§§§§§@@@@@XMgdmVyeSBsb25nIHNvIGl0CnNob3VsZCBiZSB3cmFw
|
||||
cGVkLgoKClRoYW5rIHlvdXIgZm9yIHlvdXIgYnVzaW5lc3MhClRoZSBnby1tYWlsIHRlYW0KCi0t
|
||||
ClRoaXMgaXMgYSBzaWduYXR1cmU=
|
||||
|
||||
--45c75ff528359022eb03679fbe91877d75343f2e1f8193e349deffa33ff7
|
||||
Content-Disposition: attachment; filename="test.attachment"
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-Type: application/octet-stream; name="test.attachment"
|
||||
|
||||
VGhpcyBpcyBhIHNpbXBsZSB0ZXN0IHRleHQgZmlsZSBhdHRhY2htZW50LgoKSXQgCiAgaGFzCiAg
|
||||
ICAgc2V2ZXJhbAogICAg§§§§§@@@@@BuZXdsaW5lcwoJICAgICAgICAgICAgYW5kCgkgICAgc3Bh
|
||||
Y2VzCiAgICAgaW4KICBpdAouCgpBcyB3ZWxsIGFzIGFuIGVtb2ppOiDwn5mCCg==
|
||||
|
||||
--45c75ff528359022eb03679fbe91877d75343f2e1f8193e349deffa33ff7--`
|
||||
exampleMailPlainB64WithAttachmentInvalidCTE = `Date: Wed, 01 Nov 2023 00:00:00 +0000
|
||||
MIME-Version: 1.0
|
||||
Message-ID: <1305604950.683004066175.AAAAAAAAaaaaaaaaB@go-mail.dev>
|
||||
Subject: Example mail // plain text base64 with attachment
|
||||
User-Agent: go-mail v0.4.1 // https://github.com/wneessen/go-mail
|
||||
X-Mailer: go-mail v0.4.1 // https://github.com/wneessen/go-mail
|
||||
From: "Toni Tester" <go-mail@go-mail.dev>
|
||||
To: <go-mail+test@go-mail.dev>
|
||||
Cc: <go-mail+cc@go-mail.dev>
|
||||
Content-Type: multipart/mixed;
|
||||
boundary=45c75ff528359022eb03679fbe91877d75343f2e1f8193e349deffa33ff7
|
||||
|
||||
--45c75ff528359022eb03679fbe91877d75343f2e1f8193e349deffa33ff7
|
||||
Content-Transfer-Encoding: invalid
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
|
||||
RGVhciBDdXN0b21lciwKClRoaXMgaXMgYSB0ZXN0IG1haWwuIFBsZWFzZSBkbyBub3QgcmVwbHkg
|
||||
dG8gdGhpcy4gQWxzbyB0aGlzIGxpbmUgaXMgdmVyeSBsb25nIHNvIGl0CnNob3VsZCBiZSB3cmFw
|
||||
cGVkLgoKClRoYW5rIHlvdXIgZm9yIHlvdXIgYnVzaW5lc3MhClRoZSBnby1tYWlsIHRlYW0KCi0t
|
||||
ClRoaXMgaXMgYSBzaWduYXR1cmU=
|
||||
|
||||
--45c75ff528359022eb03679fbe91877d75343f2e1f8193e349deffa33ff7
|
||||
Content-Disposition: attachment; filename="test.attachment"
|
||||
Content-Transfer-Encoding: invalid
|
||||
Content-Type: application/octet-stream; name="test.attachment"
|
||||
|
||||
VGhpcyBpcyBhIHNpbXBsZSB0ZXN0IHRleHQgZmlsZSBhdHRhY2htZW50LgoKSXQgCiAgaGFzCiAg
|
||||
ICAgc2V2ZXJhbAogICAgICAgICAgICBuZXdsaW5lcwoJICAgICAgICAgICAgYW5kCgkgICAgc3Bh
|
||||
Y2VzCiAgICAgaW4KICBpdAouCgpBcyB3ZWxsIGFzIGFuIGVtb2ppOiDwn5mCCg==
|
||||
|
||||
--45c75ff528359022eb03679fbe91877d75343f2e1f8193e349deffa33ff7--`
|
||||
exampleMailPlainB64WithAttachmentInvalidContentType = `Date: Wed, 01 Nov 2023 00:00:00 +0000
|
||||
MIME-Version: 1.0
|
||||
Message-ID: <1305604950.683004066175.AAAAAAAAaaaaaaaaB@go-mail.dev>
|
||||
Subject: Example mail // plain text base64 with attachment
|
||||
User-Agent: go-mail v0.4.1 // https://github.com/wneessen/go-mail
|
||||
X-Mailer: go-mail v0.4.1 // https://github.com/wneessen/go-mail
|
||||
From: "Toni Tester" <go-mail@go-mail.dev>
|
||||
To: <go-mail+test@go-mail.dev>
|
||||
Cc: <go-mail+cc@go-mail.dev>
|
||||
Content-Type: multipart/mixed;
|
||||
boundary=45c75ff528359022eb03679fbe91877d75343f2e1f8193e349deffa33ff7
|
||||
|
||||
--45c75ff528359022eb03679fbe91877d75343f2e1f8193e349deffa33ff7
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
|
||||
RGVhciBDdXN0b21lciwKClRoaXMgaXMgYSB0ZXN0IG1haWwuIFBsZWFzZSBkbyBub3QgcmVwbHkg
|
||||
dG8gdGhpcy4gQWxzbyB0aGlzIGxpbmUgaXMgdmVyeSBsb25nIHNvIGl0CnNob3VsZCBiZSB3cmFw
|
||||
cGVkLgoKClRoYW5rIHlvdXIgZm9yIHlvdXIgYnVzaW5lc3MhClRoZSBnby1tYWlsIHRlYW0KCi0t
|
||||
ClRoaXMgaXMgYSBzaWduYXR1cmU=
|
||||
|
||||
--45c75ff528359022eb03679fbe91877d75343f2e1f8193e349deffa33ff7
|
||||
Content-Disposition: attachment; filename="test.attachment"
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-Type; text/plain @ charset=UTF-8; $foo; bar; --invalid--
|
||||
|
||||
VGhpcyBpcyBhIHNpbXBsZSB0ZXN0IHRleHQgZmlsZSBhdHRhY2htZW50LgoKSXQgCiAgaGFzCiAg
|
||||
ICAgc2V2ZXJhbAogICAgICAgICAgICBuZXdsaW5lcwoJICAgICAgICAgICAgYW5kCgkgICAgc3Bh
|
||||
Y2VzCiAgICAgaW4KICBpdAouCgpBcyB3ZWxsIGFzIGFuIGVtb2ppOiDwn5mCCg==
|
||||
|
||||
--45c75ff528359022eb03679fbe91877d75343f2e1f8193e349deffa33ff7--`
|
||||
exampleMailPlainB64WithAttachmentNoBoundary = `Date: Wed, 01 Nov 2023 00:00:00 +0000
|
||||
MIME-Version: 1.0
|
||||
|
@ -578,6 +760,39 @@ Content-Disposition: attachment;
|
|||
filename="testfile.txt"
|
||||
|
||||
VGhpcyBpcyBhIHRlc3QgaW4gQmFzZTY0
|
||||
--------------26A45336F6C6196BD8BBA2A2--`
|
||||
exampleMultiPart7BitBase64BrokenB64 = `Date: Wed, 01 Nov 2023 00:00:00 +0000
|
||||
MIME-Version: 1.0
|
||||
Message-ID: <1305604950.683004066175.AAAAAAAAaaaaaaaaB@go-mail.dev>
|
||||
Subject: Example mail // 7bit with base64 attachment
|
||||
User-Agent: go-mail v0.4.1 // https://github.com/wneessen/go-mail
|
||||
X-Mailer: go-mail v0.4.1 // https://github.com/wneessen/go-mail
|
||||
From: "Toni Tester" <go-mail@go-mail.dev>
|
||||
To: <go-mail+test@go-mail.dev>
|
||||
Cc: <go-mail+cc@go-mail.dev>
|
||||
Content-Type: multipart/mixed;
|
||||
boundary="------------26A45336F6C6196BD8BBA2A2"
|
||||
|
||||
This is a multi-part message in MIME format.
|
||||
--------------26A45336F6C6196BD8BBA2A2
|
||||
Content-Type: text/plain; charset=US-ASCII; format=flowed
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
testtest
|
||||
testtest
|
||||
testtest
|
||||
testtest
|
||||
testtest
|
||||
testtest
|
||||
|
||||
--------------26A45336F6C6196BD8BBA2A2
|
||||
Content-Type: text/plain; charset=UTF-8;
|
||||
name="testfile.txt"
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-Disposition: attachment;
|
||||
filename="testfile.txt"
|
||||
|
||||
VGh@@@@§§§§hIHRlc3QgaW4gQmFzZTY0
|
||||
--------------26A45336F6C6196BD8BBA2A2--`
|
||||
exampleMultiPart8BitBase64 = `Date: Wed, 01 Nov 2023 00:00:00 +0000
|
||||
MIME-Version: 1.0
|
||||
|
@ -612,8 +827,352 @@ Content-Disposition: attachment;
|
|||
|
||||
VGhpcyBpcyBhIHRlc3QgaW4gQmFzZTY0
|
||||
--------------26A45336F6C6196BD8BBA2A2--`
|
||||
exampleMailWithInlineEmbed = `Date: Wed, 01 Nov 2023 00:00:00 +0000
|
||||
MIME-Version: 1.0
|
||||
Message-ID: <1305604950.683004066175.AAAAAAAAaaaaaaaaB@go-mail.dev>
|
||||
Subject: Example mail with inline embed
|
||||
User-Agent: go-mail v0.4.1 // https://github.com/wneessen/go-mail
|
||||
X-Mailer: go-mail v0.4.1 // https://github.com/wneessen/go-mail
|
||||
From: "Toni Tester" <go-mail@go-mail.dev>
|
||||
To: <go-mail+test@go-mail.dev>
|
||||
Content-Type: multipart/related; boundary="abc123"
|
||||
|
||||
--abc123
|
||||
Content-Type: text/html; charset=UTF-8
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
<html>
|
||||
<body>
|
||||
<p>Hello,</p>
|
||||
<p>This is an example email with an inline image:</p>
|
||||
<img src="cid:12345@go-mail.dev" alt="Inline Image">
|
||||
<p>Best regards,<br>The go-mail team</p>
|
||||
</body>
|
||||
</html>
|
||||
--abc123
|
||||
Content-Type: image/png
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-ID: <12345@go-mail.dev>
|
||||
Content-Disposition: inline; filename="test.png"
|
||||
|
||||
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQIW2NgYGD4DwABBAEAwS2O
|
||||
UAAAAABJRU5ErkJggg==
|
||||
--abc123--`
|
||||
exampleMailWithInlineEmbedWrongDisposition = `Date: Wed, 01 Nov 2023 00:00:00 +0000
|
||||
MIME-Version: 1.0
|
||||
Message-ID: <1305604950.683004066175.AAAAAAAAaaaaaaaaB@go-mail.dev>
|
||||
Subject: Example mail with inline embed
|
||||
User-Agent: go-mail v0.4.1 // https://github.com/wneessen/go-mail
|
||||
X-Mailer: go-mail v0.4.1 // https://github.com/wneessen/go-mail
|
||||
From: "Toni Tester" <go-mail@go-mail.dev>
|
||||
To: <go-mail+test@go-mail.dev>
|
||||
Content-Type: multipart/related; boundary="abc123"
|
||||
|
||||
--abc123
|
||||
Content-Type: text/html; charset=UTF-8
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
<html>
|
||||
<body>
|
||||
<p>Hello,</p>
|
||||
<p>This is an example email with an inline image:</p>
|
||||
<img src="cid:12345@go-mail.dev" alt="Inline Image">
|
||||
<p>Best regards,<br>The go-mail team</p>
|
||||
</body>
|
||||
</html>
|
||||
--abc123
|
||||
Content-Type: image/png
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-ID: <12345@go-mail.dev>
|
||||
Content-Disposition: broken; filename="test.png"
|
||||
|
||||
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQIW2NgYGD4DwABBAEAwS2O
|
||||
UAAAAABJRU5ErkJggg==
|
||||
--abc123--`
|
||||
)
|
||||
|
||||
func TestEMLToMsgFromReader(t *testing.T) {
|
||||
t.Run("EMLToMsgFromReader via EMLToMsgFromString, check subject and encoding", func(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
emlString string
|
||||
wantEncoding Encoding
|
||||
wantSubject string
|
||||
}{
|
||||
{
|
||||
"RFC5322 A1.1 example mail", exampleMailRFC5322A11, EncodingUSASCII,
|
||||
"Saying Hello",
|
||||
},
|
||||
{
|
||||
"Plain text no encoding (7bit)", exampleMailPlain7Bit, EncodingUSASCII,
|
||||
"Example mail // plain text without encoding",
|
||||
},
|
||||
{
|
||||
"Plain text no encoding", exampleMailPlainNoEnc, NoEncoding,
|
||||
"Example mail // plain text without encoding",
|
||||
},
|
||||
{
|
||||
"Plain text quoted-printable", exampleMailPlainQP, EncodingQP,
|
||||
"Example mail // plain text quoted-printable",
|
||||
},
|
||||
{
|
||||
"Plain text base64", exampleMailPlainB64, EncodingB64,
|
||||
"Example mail // plain text base64",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
parsed, err := EMLToMsgFromString(tt.emlString)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse EML string: %s", err)
|
||||
}
|
||||
if parsed.Encoding() != tt.wantEncoding.String() {
|
||||
t.Errorf("failed to parse EML string: want encoding %s, got %s", tt.wantEncoding,
|
||||
parsed.Encoding())
|
||||
}
|
||||
gotSubject, ok := parsed.genHeader[HeaderSubject]
|
||||
if !ok {
|
||||
t.Fatalf("failed to parse EML string. No subject header found")
|
||||
}
|
||||
if len(gotSubject) != 1 {
|
||||
t.Fatalf("failed to parse EML string, more than one subject header found")
|
||||
}
|
||||
if !strings.EqualFold(gotSubject[0], tt.wantSubject) {
|
||||
t.Errorf("failed to parse EML string: want subject %s, got %s", tt.wantSubject,
|
||||
gotSubject[0])
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
t.Run("EMLToMsgFromReader fails on reader", func(t *testing.T) {
|
||||
emlReader := bytes.NewBufferString("invalid")
|
||||
if _, err := EMLToMsgFromReader(emlReader); err == nil {
|
||||
t.Errorf("EML parsing with invalid EML string should fail")
|
||||
}
|
||||
})
|
||||
t.Run("EMLToMsgFromReader fails on parseEML", func(t *testing.T) {
|
||||
emlReader := bytes.NewBufferString(exampleMailRFC5322A11InvalidFrom)
|
||||
if _, err := EMLToMsgFromReader(emlReader); err == nil {
|
||||
t.Errorf("EML parsing with invalid EML string should fail")
|
||||
}
|
||||
})
|
||||
t.Run("EMLToMsgFromReader via EMLToMsgFromString on different examples", func(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
emlString string
|
||||
shouldFail bool
|
||||
}{
|
||||
{
|
||||
name: "Valid RFC 5322 Example",
|
||||
emlString: exampleMailRFC5322A11,
|
||||
shouldFail: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid From Header (RFC 5322)",
|
||||
emlString: exampleMailRFC5322A11InvalidFrom,
|
||||
shouldFail: true,
|
||||
},
|
||||
{
|
||||
name: "Invalid Header",
|
||||
emlString: exampleMailInvalidHeader,
|
||||
shouldFail: true,
|
||||
},
|
||||
{
|
||||
name: "Plain broken Content-Type",
|
||||
emlString: exampleMailInvalidContentType,
|
||||
shouldFail: true,
|
||||
},
|
||||
{
|
||||
name: "Plain No Encoding",
|
||||
emlString: exampleMailPlainNoEnc,
|
||||
shouldFail: false,
|
||||
},
|
||||
{
|
||||
name: "Plain invalid CTE",
|
||||
emlString: exampleMailPlainInvalidCTE,
|
||||
shouldFail: true,
|
||||
},
|
||||
{
|
||||
name: "Plain 7bit",
|
||||
emlString: exampleMailPlain7Bit,
|
||||
shouldFail: false,
|
||||
},
|
||||
{
|
||||
name: "Broken Body Base64",
|
||||
emlString: exampleMailPlainBrokenBody,
|
||||
shouldFail: true,
|
||||
},
|
||||
{
|
||||
name: "Unknown Content Type",
|
||||
emlString: exampleMailPlainUnknownContentType,
|
||||
shouldFail: true,
|
||||
},
|
||||
{
|
||||
name: "Broken Header",
|
||||
emlString: exampleMailPlainBrokenHeader,
|
||||
shouldFail: true,
|
||||
},
|
||||
{
|
||||
name: "Broken From Header",
|
||||
emlString: exampleMailPlainBrokenFrom,
|
||||
shouldFail: true,
|
||||
},
|
||||
{
|
||||
name: "Broken To Header",
|
||||
emlString: exampleMailPlainBrokenTo,
|
||||
shouldFail: true,
|
||||
},
|
||||
{
|
||||
name: "Invalid Date",
|
||||
emlString: exampleMailPlainNoEncInvalidDate,
|
||||
shouldFail: true,
|
||||
},
|
||||
{
|
||||
name: "No Date Header",
|
||||
emlString: exampleMailPlainNoEncNoDate,
|
||||
shouldFail: false,
|
||||
},
|
||||
{
|
||||
name: "Quoted Printable Encoding",
|
||||
emlString: exampleMailPlainQP,
|
||||
shouldFail: false,
|
||||
},
|
||||
{
|
||||
name: "Unsupported Transfer Encoding",
|
||||
emlString: exampleMailPlainUnsupportedTransferEnc,
|
||||
shouldFail: true,
|
||||
},
|
||||
{
|
||||
name: "Base64 Encoding",
|
||||
emlString: exampleMailPlainB64,
|
||||
shouldFail: false,
|
||||
},
|
||||
{
|
||||
name: "Base64 with Attachment",
|
||||
emlString: exampleMailPlainB64WithAttachment,
|
||||
shouldFail: false,
|
||||
},
|
||||
{
|
||||
name: "Base64 with Attachment no content types",
|
||||
emlString: exampleMailPlainB64WithAttachmentNoContentType,
|
||||
shouldFail: true,
|
||||
},
|
||||
{
|
||||
name: "Multipart Base64 with Attachment broken Base64",
|
||||
emlString: exampleMailPlainB64WithAttachmentBrokenB64,
|
||||
shouldFail: true,
|
||||
},
|
||||
{
|
||||
name: "Base64 with Attachment with invalid content type in attachment",
|
||||
emlString: exampleMailPlainB64WithAttachmentInvalidContentType,
|
||||
shouldFail: true,
|
||||
},
|
||||
{
|
||||
name: "Base64 with Attachment with invalid CTE in attachment",
|
||||
emlString: exampleMailPlainB64WithAttachmentInvalidCTE,
|
||||
shouldFail: true,
|
||||
},
|
||||
{
|
||||
name: "Base64 with Attachment No Boundary",
|
||||
emlString: exampleMailPlainB64WithAttachmentNoBoundary,
|
||||
shouldFail: true,
|
||||
},
|
||||
{
|
||||
name: "Broken Body Base64",
|
||||
emlString: exampleMailPlainB64BrokenBody,
|
||||
shouldFail: true,
|
||||
},
|
||||
{
|
||||
name: "Base64 with Embedded Image",
|
||||
emlString: exampleMailPlainB64WithEmbed,
|
||||
shouldFail: false,
|
||||
},
|
||||
{
|
||||
name: "Base64 with Embed No Content-ID",
|
||||
emlString: exampleMailPlainB64WithEmbedNoContentID,
|
||||
shouldFail: false,
|
||||
},
|
||||
{
|
||||
name: "Multipart Mixed with Attachment, Embed, and Alternative Part",
|
||||
emlString: exampleMailMultipartMixedAlternativeRelated,
|
||||
shouldFail: false,
|
||||
},
|
||||
{
|
||||
name: "Multipart 7bit Base64",
|
||||
emlString: exampleMultiPart7BitBase64,
|
||||
shouldFail: false,
|
||||
},
|
||||
{
|
||||
name: "Multipart 7bit Base64 with broken Base64",
|
||||
emlString: exampleMultiPart7BitBase64BrokenB64,
|
||||
shouldFail: true,
|
||||
},
|
||||
{
|
||||
name: "Multipart 8bit Base64",
|
||||
emlString: exampleMultiPart8BitBase64,
|
||||
shouldFail: false,
|
||||
},
|
||||
{
|
||||
name: "Multipart with inline embed",
|
||||
emlString: exampleMailWithInlineEmbed,
|
||||
shouldFail: false,
|
||||
},
|
||||
{
|
||||
name: "Multipart with inline embed disposition broken",
|
||||
emlString: exampleMailWithInlineEmbedWrongDisposition,
|
||||
shouldFail: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := EMLToMsgFromString(tt.emlString)
|
||||
if tt.shouldFail && err == nil {
|
||||
t.Errorf("parsing of EML was supposed to fail, but it did not")
|
||||
}
|
||||
if !tt.shouldFail && err != nil {
|
||||
t.Errorf("parsing of EML failed: %s", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestEMLToMsgFromFile(t *testing.T) {
|
||||
t.Run("EMLToMsgFromFile succeeds", func(t *testing.T) {
|
||||
parsed, err := EMLToMsgFromFile("testdata/RFC5322-A1-1.eml")
|
||||
if err != nil {
|
||||
t.Fatalf("EMLToMsgFromFile failed: %s ", err)
|
||||
}
|
||||
if parsed.Encoding() != EncodingUSASCII.String() {
|
||||
t.Errorf("EMLToMsgFromFile failed: want encoding %s, got %s", EncodingUSASCII,
|
||||
parsed.Encoding())
|
||||
}
|
||||
gotSubject, ok := parsed.genHeader[HeaderSubject]
|
||||
if !ok {
|
||||
t.Fatalf("failed to parse EML string. No subject header found")
|
||||
}
|
||||
if len(gotSubject) != 1 {
|
||||
t.Fatalf("failed to parse EML string, more than one subject header found")
|
||||
}
|
||||
if !strings.EqualFold(gotSubject[0], "Saying Hello") {
|
||||
t.Errorf("failed to parse EML string: want subject %s, got %s", "Saying Hello",
|
||||
gotSubject[0])
|
||||
}
|
||||
})
|
||||
t.Run("EMLToMsgFromFile fails on file not found", func(t *testing.T) {
|
||||
if _, err := EMLToMsgFromFile("testdata/not-existing.eml"); err == nil {
|
||||
t.Errorf("EMLToMsgFromFile with invalid file should fail")
|
||||
}
|
||||
})
|
||||
t.Run("EMLToMsgFromFile fails on parseEML", func(t *testing.T) {
|
||||
if _, err := EMLToMsgFromFile("testdata/RFC5322-A1-1-invalid-from.eml"); err == nil {
|
||||
t.Errorf("EMLToMsgFromFile with invalid EML message should fail")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
func TestEMLToMsgFromString(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
@ -621,26 +1180,6 @@ func TestEMLToMsgFromString(t *testing.T) {
|
|||
enc string
|
||||
sub string
|
||||
}{
|
||||
{
|
||||
"RFC5322 A1.1", exampleMailRFC5322A11, "7bit",
|
||||
"Saying Hello",
|
||||
},
|
||||
{
|
||||
"Plain text no encoding (7bit)", exampleMailPlain7Bit, "7bit",
|
||||
"Example mail // plain text without encoding",
|
||||
},
|
||||
{
|
||||
"Plain text no encoding", exampleMailPlainNoEnc, "8bit",
|
||||
"Example mail // plain text without encoding",
|
||||
},
|
||||
{
|
||||
"Plain text quoted-printable", exampleMailPlainQP, "quoted-printable",
|
||||
"Example mail // plain text quoted-printable",
|
||||
},
|
||||
{
|
||||
"Plain text base64", exampleMailPlainB64, "base64",
|
||||
"Example mail // plain text base64",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
@ -1009,3 +1548,6 @@ func stringToTempFile(data, name string) (string, string, error) {
|
|||
}
|
||||
return tempDir, filePath, nil
|
||||
}
|
||||
|
||||
|
||||
*/
|
||||
|
|
309
file_test.go
309
file_test.go
|
@ -6,134 +6,183 @@ package mail
|
|||
|
||||
import "testing"
|
||||
|
||||
// TestFile_SetGetHeader tests the set-/getHeader method of the File object
|
||||
func TestFile_SetGetHeader(t *testing.T) {
|
||||
f := File{
|
||||
Name: "testfile.txt",
|
||||
Header: make(map[string][]string),
|
||||
}
|
||||
f.setHeader(HeaderContentType, "text/plain")
|
||||
fi, ok := f.getHeader(HeaderContentType)
|
||||
if !ok {
|
||||
t.Errorf("getHeader method of File did not return a value")
|
||||
return
|
||||
}
|
||||
if fi != "text/plain" {
|
||||
t.Errorf("getHeader returned wrong value. Expected: %s, got: %s", "text/plain", fi)
|
||||
}
|
||||
fi, ok = f.getHeader(HeaderContentTransferEnc)
|
||||
if ok {
|
||||
t.Errorf("getHeader method of File did return a value, but wasn't supposed to")
|
||||
return
|
||||
}
|
||||
if fi != "" {
|
||||
t.Errorf("getHeader returned wrong value. Expected: %s, got: %s", "", fi)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFile_WithFileDescription tests the WithFileDescription option
|
||||
func TestFile_WithFileDescription(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
desc string
|
||||
}{
|
||||
{"File description: test", "test"},
|
||||
{"File description: empty", ""},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
m := NewMsg()
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
m.AttachFile("file.go", WithFileDescription(tt.desc))
|
||||
al := m.GetAttachments()
|
||||
if len(al) <= 0 {
|
||||
t.Errorf("AttachFile() failed. Attachment list is empty")
|
||||
}
|
||||
a := al[0]
|
||||
if a.Desc != tt.desc {
|
||||
t.Errorf("WithFileDescription() failed. Expected: %s, got: %s", tt.desc, a.Desc)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestFile_WithContentID tests the WithFileContentID option
|
||||
func TestFile_WithContentID(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
contentid string
|
||||
}{
|
||||
{"File Content-ID: test", "test"},
|
||||
{"File Content-ID: empty", ""},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
m := NewMsg()
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
m.AttachFile("file.go", WithFileContentID(tt.contentid))
|
||||
al := m.GetAttachments()
|
||||
if len(al) <= 0 {
|
||||
t.Errorf("AttachFile() failed. Attachment list is empty")
|
||||
}
|
||||
a := al[0]
|
||||
if a.Header.Get(HeaderContentID.String()) != tt.contentid {
|
||||
t.Errorf("WithFileContentID() failed. Expected: %s, got: %s", tt.contentid,
|
||||
a.Header.Get(HeaderContentID.String()))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestFile_WithFileEncoding tests the WithFileEncoding option
|
||||
func TestFile_WithFileEncoding(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
enc Encoding
|
||||
want Encoding
|
||||
}{
|
||||
{"File encoding: 8bit raw", NoEncoding, NoEncoding},
|
||||
{"File encoding: Base64", EncodingB64, EncodingB64},
|
||||
{"File encoding: quoted-printable (not allowed)", EncodingQP, ""},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
m := NewMsg()
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
m.AttachFile("file.go", WithFileEncoding(tt.enc))
|
||||
al := m.GetAttachments()
|
||||
if len(al) <= 0 {
|
||||
t.Errorf("AttachFile() failed. Attachment list is empty")
|
||||
}
|
||||
a := al[0]
|
||||
if a.Enc != tt.want {
|
||||
t.Errorf("WithFileEncoding() failed. Expected: %s, got: %s", tt.enc, a.Enc)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestFile_WithFileContentType tests the WithFileContentType option
|
||||
func TestFile_WithFileContentType(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
ct ContentType
|
||||
want string
|
||||
}{
|
||||
{"File content-type: text/plain", TypeTextPlain, "text/plain"},
|
||||
{"File content-type: html/html", TypeTextHTML, "text/html"},
|
||||
{"File content-type: application/octet-stream", TypeAppOctetStream, "application/octet-stream"},
|
||||
{"File content-type: application/pgp-encrypted", TypePGPEncrypted, "application/pgp-encrypted"},
|
||||
{"File content-type: application/pgp-signature", TypePGPSignature, "application/pgp-signature"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
m := NewMsg()
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
m.AttachFile("file.go", WithFileContentType(tt.ct))
|
||||
al := m.GetAttachments()
|
||||
if len(al) <= 0 {
|
||||
t.Errorf("AttachFile() failed. Attachment list is empty")
|
||||
}
|
||||
a := al[0]
|
||||
if a.ContentType != ContentType(tt.want) {
|
||||
t.Errorf("WithFileContentType() failed. Expected: %s, got: %s", tt.want, a.ContentType)
|
||||
}
|
||||
})
|
||||
}
|
||||
func TestFile(t *testing.T) {
|
||||
t.Run("setHeader", func(t *testing.T) {
|
||||
f := File{
|
||||
Name: "testfile.txt",
|
||||
Header: make(map[string][]string),
|
||||
}
|
||||
f.setHeader(HeaderContentType, "text/plain")
|
||||
contentType, ok := f.Header[HeaderContentType.String()]
|
||||
if !ok {
|
||||
t.Fatalf("setHeader failed. Expected header %s to be set", HeaderContentType)
|
||||
}
|
||||
if len(contentType) != 1 {
|
||||
t.Fatalf("setHeader failed. Expected header %s to have one value, got: %d", HeaderContentType,
|
||||
len(contentType))
|
||||
}
|
||||
if contentType[0] != "text/plain" {
|
||||
t.Fatalf("setHeader failed. Expected header %s to have value %s, got: %s",
|
||||
HeaderContentType.String(), "text/plain", contentType[0])
|
||||
}
|
||||
})
|
||||
t.Run("getHeader", func(t *testing.T) {
|
||||
f := File{
|
||||
Name: "testfile.txt",
|
||||
Header: make(map[string][]string),
|
||||
}
|
||||
f.setHeader(HeaderContentType, "text/plain")
|
||||
contentType, ok := f.getHeader(HeaderContentType)
|
||||
if !ok {
|
||||
t.Fatalf("setHeader failed. Expected header %s to be set", HeaderContentType)
|
||||
}
|
||||
if contentType != "text/plain" {
|
||||
t.Fatalf("setHeader failed. Expected header %s to have value %s, got: %s",
|
||||
HeaderContentType.String(), "text/plain", contentType)
|
||||
}
|
||||
})
|
||||
t.Run("WithFileDescription", func(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
desc string
|
||||
}{
|
||||
{"File description: test", "test"},
|
||||
{"File description: with newline", "test\n"},
|
||||
{"File description: empty", ""},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
message := NewMsg()
|
||||
message.AttachFile("file.go", WithFileDescription(tt.desc))
|
||||
attachments := message.GetAttachments()
|
||||
if len(attachments) <= 0 {
|
||||
t.Fatalf("failed to retrieve attachments list")
|
||||
}
|
||||
firstAttachment := attachments[0]
|
||||
if firstAttachment == nil {
|
||||
t.Fatalf("failed to retrieve first attachment, got nil")
|
||||
}
|
||||
if firstAttachment.Desc != tt.desc {
|
||||
t.Errorf("WithFileDescription() failed. Expected: %s, got: %s", tt.desc,
|
||||
firstAttachment.Desc)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
t.Run("WithFileContentID", func(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
id string
|
||||
}{
|
||||
{"Content-ID: test", "test"},
|
||||
{"Content-ID: with newline", "test\n"},
|
||||
{"Content-ID: empty", ""},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
message := NewMsg()
|
||||
message.AttachFile("file.go", WithFileContentID(tt.id))
|
||||
attachments := message.GetAttachments()
|
||||
if len(attachments) <= 0 {
|
||||
t.Fatalf("failed to retrieve attachments list")
|
||||
}
|
||||
firstAttachment := attachments[0]
|
||||
if firstAttachment == nil {
|
||||
t.Fatalf("failed to retrieve first attachment, got nil")
|
||||
}
|
||||
contentID := firstAttachment.Header.Get(HeaderContentID.String())
|
||||
if contentID != tt.id {
|
||||
t.Errorf("WithFileContentID() failed. Expected: %s, got: %s", tt.id,
|
||||
contentID)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
t.Run("WithFileEncoding", func(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
encoding Encoding
|
||||
want Encoding
|
||||
}{
|
||||
{"File encoding: US-ASCII", EncodingUSASCII, EncodingUSASCII},
|
||||
{"File encoding: 8bit raw", NoEncoding, NoEncoding},
|
||||
{"File encoding: Base64", EncodingB64, EncodingB64},
|
||||
{"File encoding: quoted-printable (not allowed)", EncodingQP, ""},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
message := NewMsg()
|
||||
message.AttachFile("file.go", WithFileEncoding(tt.encoding))
|
||||
attachments := message.GetAttachments()
|
||||
if len(attachments) <= 0 {
|
||||
t.Fatalf("failed to retrieve attachments list")
|
||||
}
|
||||
firstAttachment := attachments[0]
|
||||
if firstAttachment == nil {
|
||||
t.Fatalf("failed to retrieve first attachment, got nil")
|
||||
}
|
||||
if firstAttachment.Enc != tt.want {
|
||||
t.Errorf("WithFileEncoding() failed. Expected: %s, got: %s", tt.want, firstAttachment.Enc)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
t.Run("WithFileName", func(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
fileName string
|
||||
}{
|
||||
{"File name: test", "test"},
|
||||
{"File name: with newline", "test\n"},
|
||||
{"File name: empty", ""},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
message := NewMsg()
|
||||
message.AttachFile("file.go", WithFileName(tt.fileName))
|
||||
attachments := message.GetAttachments()
|
||||
if len(attachments) <= 0 {
|
||||
t.Fatalf("failed to retrieve attachments list")
|
||||
}
|
||||
firstAttachment := attachments[0]
|
||||
if firstAttachment == nil {
|
||||
t.Fatalf("failed to retrieve first attachment, got nil")
|
||||
}
|
||||
if firstAttachment.Name != tt.fileName {
|
||||
t.Errorf("WithFileName() failed. Expected: %s, got: %s", tt.fileName,
|
||||
firstAttachment.Name)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
t.Run("WithFileContentType", func(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
contentType ContentType
|
||||
}{
|
||||
{"File content-type: text/plain", TypeTextPlain},
|
||||
{"File content-type: html/html", TypeTextHTML},
|
||||
{"File content-type: application/octet-stream", TypeAppOctetStream},
|
||||
{"File content-type: application/pgp-encrypted", TypePGPEncrypted},
|
||||
{"File content-type: application/pgp-signature", TypePGPSignature},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
message := NewMsg()
|
||||
message.AttachFile("file.go", WithFileContentType(tt.contentType))
|
||||
attachments := message.GetAttachments()
|
||||
if len(attachments) <= 0 {
|
||||
t.Fatalf("failed to retrieve attachments list")
|
||||
}
|
||||
firstAttachment := attachments[0]
|
||||
if firstAttachment == nil {
|
||||
t.Fatalf("failed to retrieve first attachment, got nil")
|
||||
}
|
||||
if firstAttachment.ContentType != tt.contentType {
|
||||
t.Errorf("WithFileContentType() failed. Expected: %s, got: %s", tt.contentType,
|
||||
firstAttachment.ContentType)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
149
header_test.go
149
header_test.go
|
@ -8,69 +8,13 @@ import (
|
|||
"testing"
|
||||
)
|
||||
|
||||
// TestImportance_StringFuncs tests the different string method of the Importance object
|
||||
func TestImportance_StringFuncs(t *testing.T) {
|
||||
tests := []struct {
|
||||
var (
|
||||
genHeaderTests = []struct {
|
||||
name string
|
||||
imp Importance
|
||||
wantns string
|
||||
xprio string
|
||||
header Header
|
||||
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
|
||||
}{
|
||||
{"Header: Content-Description", HeaderContentDescription, "Content-Description"},
|
||||
{"Header: Content-Disposition", HeaderContentDisposition, "Content-Disposition"},
|
||||
{"Header: Content-ID", HeaderContentID, "Content-ID"},
|
||||
{"Header: Content-Language", HeaderContentLang, "Content-Language"},
|
||||
|
@ -78,6 +22,10 @@ func TestHeader_String(t *testing.T) {
|
|||
{"Header: Content-Transfer-Encoding", HeaderContentTransferEnc, "Content-Transfer-Encoding"},
|
||||
{"Header: Content-Type", HeaderContentType, "Content-Type"},
|
||||
{"Header: Date", HeaderDate, "Date"},
|
||||
{
|
||||
"Header: Disposition-Notification-To", HeaderDispositionNotificationTo,
|
||||
"Disposition-Notification-To",
|
||||
},
|
||||
{"Header: Importance", HeaderImportance, "Importance"},
|
||||
{"Header: In-Reply-To", HeaderInReplyTo, "In-Reply-To"},
|
||||
{"Header: List-Unsubscribe", HeaderListUnsubscribe, "List-Unsubscribe"},
|
||||
|
@ -87,19 +35,90 @@ func TestHeader_String(t *testing.T) {
|
|||
{"Header: Organization", HeaderOrganization, "Organization"},
|
||||
{"Header: Precedence", HeaderPrecedence, "Precedence"},
|
||||
{"Header: Priority", HeaderPriority, "Priority"},
|
||||
{"Header: HeaderReferences", HeaderReferences, "References"},
|
||||
{"Header: References", HeaderReferences, "References"},
|
||||
{"Header: Reply-To", HeaderReplyTo, "Reply-To"},
|
||||
{"Header: Subject", HeaderSubject, "Subject"},
|
||||
{"Header: User-Agent", HeaderUserAgent, "User-Agent"},
|
||||
{"Header: X-Auto-Response-Suppress", HeaderXAutoResponseSuppress, "X-Auto-Response-Suppress"},
|
||||
{"Header: X-Mailer", HeaderXMailer, "X-Mailer"},
|
||||
{"Header: X-MSMail-Priority", HeaderXMSMailPriority, "X-MSMail-Priority"},
|
||||
{"Header: X-Priority", HeaderXPriority, "X-Priority"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
addrHeaderTests = []struct {
|
||||
name string
|
||||
header AddrHeader
|
||||
want string
|
||||
}{
|
||||
{"From", HeaderFrom, "From"},
|
||||
{"To", HeaderTo, "To"},
|
||||
{"Cc", HeaderCc, "Cc"},
|
||||
{"Bcc", HeaderBcc, "Bcc"},
|
||||
}
|
||||
)
|
||||
|
||||
func TestImportance_Stringer(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
imp Importance
|
||||
wantnum string
|
||||
xprio string
|
||||
want string
|
||||
}{
|
||||
{"Non-Urgent", ImportanceNonUrgent, "0", "5", "non-urgent"},
|
||||
{"Low", ImportanceLow, "0", "5", "low"},
|
||||
{"Normal", ImportanceNormal, "", "", ""},
|
||||
{"High", ImportanceHigh, "1", "1", "high"},
|
||||
{"Urgent", ImportanceUrgent, "1", "1", "urgent"},
|
||||
{"Unknown", 9, "", "", ""},
|
||||
}
|
||||
t.Run("String", func(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.imp.String() != tt.want {
|
||||
t.Errorf("wrong string for Importance returned. Expected: %s, got: %s", tt.want, tt.imp.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
t.Run("NumString", func(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.imp.NumString() != tt.wantnum {
|
||||
t.Errorf("wrong number string for Importance returned. Expected: %s, got: %s", tt.wantnum,
|
||||
tt.imp.NumString())
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
t.Run("XPrioString", func(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.imp.XPrioString() != tt.xprio {
|
||||
t.Errorf("wrong x-prio string for Importance returned. Expected: %s, got: %s", tt.xprio,
|
||||
tt.imp.XPrioString())
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestAddrHeader_Stringer(t *testing.T) {
|
||||
for _, tt := range addrHeaderTests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.h.String() != tt.want {
|
||||
t.Errorf("wrong string for Header returned. Expected: %s, got: %s",
|
||||
tt.want, tt.h.String())
|
||||
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",
|
||||
tt.want, tt.header.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
91
msg.go
91
msg.go
|
@ -583,6 +583,9 @@ func (m *Msg) SetAddrHeader(header AddrHeader, values ...string) error {
|
|||
// References:
|
||||
// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.4
|
||||
func (m *Msg) SetAddrHeaderIgnoreInvalid(header AddrHeader, values ...string) {
|
||||
if m.addrHeader == nil {
|
||||
m.addrHeader = make(map[AddrHeader][]*mail.Address)
|
||||
}
|
||||
var addresses []*mail.Address
|
||||
for _, addrVal := range values {
|
||||
address, err := mail.ParseAddress(m.encodeString(addrVal))
|
||||
|
@ -591,7 +594,14 @@ func (m *Msg) SetAddrHeaderIgnoreInvalid(header AddrHeader, values ...string) {
|
|||
}
|
||||
addresses = append(addresses, address)
|
||||
}
|
||||
m.addrHeader[header] = addresses
|
||||
switch header {
|
||||
case HeaderFrom:
|
||||
if len(addresses) > 0 {
|
||||
m.addrHeader[header] = []*mail.Address{addresses[0]}
|
||||
}
|
||||
default:
|
||||
m.addrHeader[header] = addresses
|
||||
}
|
||||
}
|
||||
|
||||
// EnvelopeFrom sets the envelope from address for the Msg.
|
||||
|
@ -743,7 +753,16 @@ func (m *Msg) ToIgnoreInvalid(rcpts ...string) {
|
|||
// References:
|
||||
// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3
|
||||
func (m *Msg) ToFromString(rcpts string) error {
|
||||
return m.To(strings.Split(rcpts, ",")...)
|
||||
src := strings.Split(rcpts, ",")
|
||||
var dst []string
|
||||
for _, address := range src {
|
||||
address = strings.TrimSpace(address)
|
||||
if address == "" {
|
||||
continue
|
||||
}
|
||||
dst = append(dst, address)
|
||||
}
|
||||
return m.To(dst...)
|
||||
}
|
||||
|
||||
// Cc sets one or more "CC" (carbon copy) addresses in the mail body for the Msg.
|
||||
|
@ -828,7 +847,16 @@ func (m *Msg) CcIgnoreInvalid(rcpts ...string) {
|
|||
// References:
|
||||
// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3
|
||||
func (m *Msg) CcFromString(rcpts string) error {
|
||||
return m.Cc(strings.Split(rcpts, ",")...)
|
||||
src := strings.Split(rcpts, ",")
|
||||
var dst []string
|
||||
for _, address := range src {
|
||||
address = strings.TrimSpace(address)
|
||||
if address == "" {
|
||||
continue
|
||||
}
|
||||
dst = append(dst, address)
|
||||
}
|
||||
return m.Cc(dst...)
|
||||
}
|
||||
|
||||
// Bcc sets one or more "BCC" (blind carbon copy) addresses in the mail body for the Msg.
|
||||
|
@ -914,7 +942,16 @@ func (m *Msg) BccIgnoreInvalid(rcpts ...string) {
|
|||
// References:
|
||||
// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3
|
||||
func (m *Msg) BccFromString(rcpts string) error {
|
||||
return m.Bcc(strings.Split(rcpts, ",")...)
|
||||
src := strings.Split(rcpts, ",")
|
||||
var dst []string
|
||||
for _, address := range src {
|
||||
address = strings.TrimSpace(address)
|
||||
if address == "" {
|
||||
continue
|
||||
}
|
||||
dst = append(dst, address)
|
||||
}
|
||||
return m.Bcc(dst...)
|
||||
}
|
||||
|
||||
// ReplyTo sets the "Reply-To" address for the Msg, specifying where replies should be sent.
|
||||
|
@ -1050,8 +1087,7 @@ func (m *Msg) SetBulk() {
|
|||
// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.3
|
||||
// - https://datatracker.ietf.org/doc/html/rfc1123
|
||||
func (m *Msg) SetDate() {
|
||||
now := time.Now().Format(time.RFC1123Z)
|
||||
m.SetGenHeader(HeaderDate, now)
|
||||
m.SetDateWithValue(time.Now())
|
||||
}
|
||||
|
||||
// SetDateWithValue sets the "Date" header for the Msg using the provided time value in a valid RFC 1123 format.
|
||||
|
@ -1151,6 +1187,9 @@ func (m *Msg) IsDelivered() bool {
|
|||
// References:
|
||||
// - https://datatracker.ietf.org/doc/html/rfc8098
|
||||
func (m *Msg) RequestMDNTo(rcpts ...string) error {
|
||||
if m.genHeader == nil {
|
||||
m.genHeader = make(map[Header][]string)
|
||||
}
|
||||
var addresses []string
|
||||
for _, addrVal := range rcpts {
|
||||
address, err := mail.ParseAddress(addrVal)
|
||||
|
@ -1159,9 +1198,7 @@ func (m *Msg) RequestMDNTo(rcpts ...string) error {
|
|||
}
|
||||
addresses = append(addresses, address.String())
|
||||
}
|
||||
if _, ok := m.genHeader[HeaderDispositionNotificationTo]; ok {
|
||||
m.genHeader[HeaderDispositionNotificationTo] = addresses
|
||||
}
|
||||
m.genHeader[HeaderDispositionNotificationTo] = addresses
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -1200,11 +1237,11 @@ func (m *Msg) RequestMDNAddTo(rcpt string) error {
|
|||
return fmt.Errorf(errParseMailAddr, rcpt, err)
|
||||
}
|
||||
var addresses []string
|
||||
addresses = append(addresses, m.genHeader[HeaderDispositionNotificationTo]...)
|
||||
addresses = append(addresses, address.String())
|
||||
if _, ok := m.genHeader[HeaderDispositionNotificationTo]; ok {
|
||||
m.genHeader[HeaderDispositionNotificationTo] = addresses
|
||||
if current, ok := m.genHeader[HeaderDispositionNotificationTo]; ok {
|
||||
addresses = current
|
||||
}
|
||||
addresses = append(addresses, address.String())
|
||||
m.genHeader[HeaderDispositionNotificationTo] = addresses
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -1644,11 +1681,11 @@ func (m *Msg) SetBodyHTMLTemplate(tpl *ht.Template, data interface{}, opts ...Pa
|
|||
if tpl == nil {
|
||||
return errors.New(errTplPointerNil)
|
||||
}
|
||||
buffer := bytes.Buffer{}
|
||||
if err := tpl.Execute(&buffer, data); err != nil {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
if err := tpl.Execute(buffer, data); err != nil {
|
||||
return fmt.Errorf(errTplExecuteFailed, err)
|
||||
}
|
||||
writeFunc := writeFuncFromBuffer(&buffer)
|
||||
writeFunc := writeFuncFromBuffer(buffer)
|
||||
m.SetBodyWriter(TypeTextHTML, writeFunc, opts...)
|
||||
return nil
|
||||
}
|
||||
|
@ -1675,11 +1712,11 @@ func (m *Msg) SetBodyTextTemplate(tpl *tt.Template, data interface{}, opts ...Pa
|
|||
if tpl == nil {
|
||||
return errors.New(errTplPointerNil)
|
||||
}
|
||||
buf := bytes.Buffer{}
|
||||
if err := tpl.Execute(&buf, data); err != nil {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
if err := tpl.Execute(buffer, data); err != nil {
|
||||
return fmt.Errorf(errTplExecuteFailed, err)
|
||||
}
|
||||
writeFunc := writeFuncFromBuffer(&buf)
|
||||
writeFunc := writeFuncFromBuffer(buffer)
|
||||
m.SetBodyWriter(TypeTextPlain, writeFunc, opts...)
|
||||
return nil
|
||||
}
|
||||
|
@ -1749,11 +1786,11 @@ func (m *Msg) AddAlternativeHTMLTemplate(tpl *ht.Template, data interface{}, opt
|
|||
if tpl == nil {
|
||||
return errors.New(errTplPointerNil)
|
||||
}
|
||||
buffer := bytes.Buffer{}
|
||||
if err := tpl.Execute(&buffer, data); err != nil {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
if err := tpl.Execute(buffer, data); err != nil {
|
||||
return fmt.Errorf(errTplExecuteFailed, err)
|
||||
}
|
||||
writeFunc := writeFuncFromBuffer(&buffer)
|
||||
writeFunc := writeFuncFromBuffer(buffer)
|
||||
m.AddAlternativeWriter(TypeTextHTML, writeFunc, opts...)
|
||||
return nil
|
||||
}
|
||||
|
@ -1779,11 +1816,11 @@ func (m *Msg) AddAlternativeTextTemplate(tpl *tt.Template, data interface{}, opt
|
|||
if tpl == nil {
|
||||
return errors.New(errTplPointerNil)
|
||||
}
|
||||
buffer := bytes.Buffer{}
|
||||
if err := tpl.Execute(&buffer, data); err != nil {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
if err := tpl.Execute(buffer, data); err != nil {
|
||||
return fmt.Errorf(errTplExecuteFailed, err)
|
||||
}
|
||||
writeFunc := writeFuncFromBuffer(&buffer)
|
||||
writeFunc := writeFuncFromBuffer(buffer)
|
||||
m.AddAlternativeWriter(TypeTextPlain, writeFunc, opts...)
|
||||
return nil
|
||||
}
|
||||
|
@ -2334,8 +2371,8 @@ func (m *Msg) WriteToSendmailWithContext(ctx context.Context, sendmailPath strin
|
|||
// - https://datatracker.ietf.org/doc/html/rfc5322
|
||||
func (m *Msg) NewReader() *Reader {
|
||||
reader := &Reader{}
|
||||
buffer := bytes.Buffer{}
|
||||
_, err := m.Write(&buffer)
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
_, err := m.Write(buffer)
|
||||
if err != nil {
|
||||
reader.err = fmt.Errorf("failed to write Msg to Reader buffer: %w", err)
|
||||
}
|
||||
|
|
|
@ -1,85 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package mail
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestMsg_WriteToSendmailWithContext tests the WriteToSendmailWithContext() method of the Msg
|
||||
func TestMsg_WriteToSendmailWithContext(t *testing.T) {
|
||||
if os.Getenv("TEST_SKIP_SENDMAIL") != "" {
|
||||
t.Skipf("TEST_SKIP_SENDMAIL variable is set. Skipping sendmail test")
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
sp string
|
||||
sf bool
|
||||
}{
|
||||
{"Sendmail path: /dev/null", "/dev/null", true},
|
||||
{"Sendmail path: /bin/cat", "/bin/cat", true},
|
||||
{"Sendmail path: /is/invalid", "/is/invalid", true},
|
||||
{"Sendmail path: /bin/echo", "/bin/echo", false},
|
||||
}
|
||||
m := NewMsg()
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx, cfn := context.WithTimeout(context.Background(), time.Second*10)
|
||||
defer cfn()
|
||||
m.SetBodyString(TypeTextPlain, "Plain")
|
||||
if err := m.WriteToSendmailWithContext(ctx, tt.sp); err != nil && !tt.sf {
|
||||
t.Errorf("WriteToSendmailWithCommand() failed: %s", err)
|
||||
}
|
||||
m.Reset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestMsg_WriteToSendmail will test the output to the local sendmail command
|
||||
func TestMsg_WriteToSendmail(t *testing.T) {
|
||||
if os.Getenv("TEST_SKIP_SENDMAIL") != "" {
|
||||
t.Skipf("TEST_SKIP_SENDMAIL variable is set. Skipping sendmail test")
|
||||
}
|
||||
_, err := os.Stat(SendmailPath)
|
||||
if err != nil {
|
||||
t.Skipf("local sendmail command not found in expected path. Skipping")
|
||||
}
|
||||
|
||||
m := NewMsg()
|
||||
_ = m.From("Toni Tester <tester@example.com>")
|
||||
_ = m.To(TestRcpt)
|
||||
m.SetBodyString(TypeTextPlain, "This is a test")
|
||||
if err := m.WriteToSendmail(); err != nil {
|
||||
t.Errorf("WriteToSendmail failed: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMsg_WriteToTempFileFailed(t *testing.T) {
|
||||
m := NewMsg()
|
||||
_ = m.From("Toni Tester <tester@example.com>")
|
||||
_ = m.To("Ellenor Tester <ellinor@example.com>")
|
||||
m.SetBodyString(TypeTextPlain, "This is a test")
|
||||
|
||||
curTmpDir := os.Getenv("TMPDIR")
|
||||
defer func() {
|
||||
if err := os.Setenv("TMPDIR", curTmpDir); err != nil {
|
||||
t.Errorf("failed to set TMPDIR environment variable: %s", err)
|
||||
}
|
||||
}()
|
||||
|
||||
if err := os.Setenv("TMPDIR", "/invalid/directory/that/does/not/exist"); err != nil {
|
||||
t.Errorf("failed to set TMPDIR environment variable: %s", err)
|
||||
}
|
||||
_, err := m.WriteToTempFile()
|
||||
if err == nil {
|
||||
t.Errorf("WriteToTempFile() did not fail as expected")
|
||||
}
|
||||
}
|
9590
msg_test.go
9590
msg_test.go
File diff suppressed because it is too large
Load diff
146
msg_unix_test.go
Normal file
146
msg_unix_test.go
Normal file
|
@ -0,0 +1,146 @@
|
|||
// SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build linux || freebsd
|
||||
// +build linux freebsd
|
||||
|
||||
package mail
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMsg_AttachFile_unixOnly(t *testing.T) {
|
||||
t.Run("AttachFile with fileFromFS fails on open", func(t *testing.T) {
|
||||
if os.Getenv("PERFORM_UNIX_OPEN_WRITE_TESTS") != "true" {
|
||||
t.Skipf("PERFORM_UNIX_OPEN_WRITE_TESTS variable is not set. Skipping unix open/write tests")
|
||||
}
|
||||
|
||||
tempFile, err := os.CreateTemp("", "attachfile-open-write-test.*.txt")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp file: %s", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if err := os.Remove(tempFile.Name()); err != nil {
|
||||
t.Errorf("failed to remove temp file: %s", err)
|
||||
}
|
||||
})
|
||||
if err = os.Chmod(tempFile.Name(), 0o000); err != nil {
|
||||
t.Fatalf("failed to chmod temp file: %s", err)
|
||||
}
|
||||
|
||||
message := NewMsg()
|
||||
if message == nil {
|
||||
t.Fatal("message is nil")
|
||||
}
|
||||
message.AttachFile(tempFile.Name())
|
||||
attachments := message.GetAttachments()
|
||||
if len(attachments) != 1 {
|
||||
t.Fatalf("failed to get attachments, expected 1, got: %d", len(attachments))
|
||||
}
|
||||
messageBuf := bytes.NewBuffer(nil)
|
||||
_, err = attachments[0].Writer(messageBuf)
|
||||
if err == nil {
|
||||
t.Error("writer func expected to fail, but didn't")
|
||||
}
|
||||
if !errors.Is(err, os.ErrPermission) {
|
||||
t.Errorf("expected error to be %s, got: %s", os.ErrPermission, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestMsg_EmbedFile_unixOnly(t *testing.T) {
|
||||
t.Run("EmbedFile with fileFromFS fails on open", func(t *testing.T) {
|
||||
if os.Getenv("PERFORM_UNIX_OPEN_WRITE_TESTS") != "true" {
|
||||
t.Skipf("PERFORM_UNIX_OPEN_WRITE_TESTS variable is not set. Skipping unix open/write tests")
|
||||
}
|
||||
|
||||
tempFile, err := os.CreateTemp("", "embedfile-open-write-test.*.txt")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp file: %s", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if err := os.Remove(tempFile.Name()); err != nil {
|
||||
t.Errorf("failed to remove temp file: %s", err)
|
||||
}
|
||||
})
|
||||
if err = os.Chmod(tempFile.Name(), 0o000); err != nil {
|
||||
t.Fatalf("failed to chmod temp file: %s", err)
|
||||
}
|
||||
|
||||
message := NewMsg()
|
||||
if message == nil {
|
||||
t.Fatal("message is nil")
|
||||
}
|
||||
message.EmbedFile(tempFile.Name())
|
||||
embeds := message.GetEmbeds()
|
||||
if len(embeds) != 1 {
|
||||
t.Fatalf("failed to get embeds, expected 1, got: %d", len(embeds))
|
||||
}
|
||||
messageBuf := bytes.NewBuffer(nil)
|
||||
_, err = embeds[0].Writer(messageBuf)
|
||||
if err == nil {
|
||||
t.Error("writer func expected to fail, but didn't")
|
||||
}
|
||||
if !errors.Is(err, os.ErrPermission) {
|
||||
t.Errorf("expected error to be %s, got: %s", os.ErrPermission, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestMsg_WriteToFile_unixOnly(t *testing.T) {
|
||||
t.Run("WriteToFile fails on create", func(t *testing.T) {
|
||||
if os.Getenv("PERFORM_UNIX_OPEN_WRITE_TESTS") != "true" {
|
||||
t.Skipf("PERFORM_UNIX_OPEN_WRITE_TESTS variable is not set. Skipping unix open/write tests")
|
||||
}
|
||||
|
||||
tempfile, err := os.CreateTemp("", "testmail-create.*.eml")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp file: %s", err)
|
||||
}
|
||||
if err = os.Chmod(tempfile.Name(), 0o000); err != nil {
|
||||
t.Fatalf("failed to chmod temp file: %s", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if err = tempfile.Close(); err != nil {
|
||||
t.Fatalf("failed to close temp file: %s", err)
|
||||
}
|
||||
if err = os.Remove(tempfile.Name()); err != nil {
|
||||
t.Fatalf("failed to remove temp file: %s", err)
|
||||
}
|
||||
})
|
||||
message := testMessage(t)
|
||||
if err = message.WriteToFile(tempfile.Name()); err == nil {
|
||||
t.Errorf("expected error, got nil")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestMsg_WriteToTempFile_unixOnly(t *testing.T) {
|
||||
if os.Getenv("PERFORM_UNIX_OPEN_WRITE_TESTS") != "true" {
|
||||
t.Skipf("PERFORM_UNIX_OPEN_WRITE_TESTS variable is not set. Skipping unix open/write tests")
|
||||
}
|
||||
|
||||
t.Run("WriteToTempFile fails on invalid TMPDIR", func(t *testing.T) {
|
||||
// We store the current TMPDIR variable so we can set it back when the test is over
|
||||
curTmpDir := os.Getenv("TMPDIR")
|
||||
t.Cleanup(func() {
|
||||
if err := os.Setenv("TMPDIR", curTmpDir); err != nil {
|
||||
t.Errorf("failed to set TMPDIR environment variable: %s", err)
|
||||
}
|
||||
})
|
||||
|
||||
if err := os.Setenv("TMPDIR", "/invalid/directory/that/does/not/exist"); err != nil {
|
||||
t.Fatalf("failed to set TMPDIR environment variable: %s", err)
|
||||
}
|
||||
message := testMessage(t)
|
||||
_, err := message.WriteToTempFile()
|
||||
if err == nil {
|
||||
t.Errorf("expected writing to invalid TMPDIR to fail, got: %s", err)
|
||||
}
|
||||
})
|
||||
}
|
|
@ -6,151 +6,673 @@ package mail
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// brokenWriter implements a broken writer for io.Writer testing
|
||||
type brokenWriter struct {
|
||||
io.Writer
|
||||
}
|
||||
|
||||
// Write implements the io.Writer interface but intentionally returns an error at
|
||||
// any time
|
||||
func (bw *brokenWriter) Write([]byte) (int, error) {
|
||||
return 0, fmt.Errorf("intentionally failed")
|
||||
}
|
||||
|
||||
// TestMsgWriter_Write tests the WriteTo() method of the msgWriter
|
||||
func TestMsgWriter_Write(t *testing.T) {
|
||||
bw := &brokenWriter{}
|
||||
mw := &msgWriter{writer: bw, charset: CharsetUTF8, encoder: mime.QEncoding}
|
||||
_, err := mw.Write([]byte("test"))
|
||||
if err == nil {
|
||||
t.Errorf("msgWriter WriteTo() with brokenWriter should fail, but didn't")
|
||||
}
|
||||
|
||||
// Also test the part when a previous error happened
|
||||
mw.err = fmt.Errorf("broken")
|
||||
_, err = mw.Write([]byte("test"))
|
||||
if err == nil {
|
||||
t.Errorf("msgWriter WriteTo() with brokenWriter should fail, but didn't")
|
||||
}
|
||||
}
|
||||
|
||||
// TestMsgWriter_writeMsg tests the writeMsg method of the msgWriter
|
||||
func TestMsgWriter_writeMsg(t *testing.T) {
|
||||
m := NewMsg()
|
||||
_ = m.From(`"Toni Tester" <test@example.com>`)
|
||||
_ = m.To(`"Toni Receiver" <receiver@example.com>`)
|
||||
m.Subject("This is a subject")
|
||||
m.SetBulk()
|
||||
now := time.Now()
|
||||
m.SetDateWithValue(now)
|
||||
m.SetMessageIDWithValue("message@id.com")
|
||||
m.SetBodyString(TypeTextPlain, "This is the body")
|
||||
m.AddAlternativeString(TypeTextHTML, "This is the alternative body")
|
||||
buf := bytes.Buffer{}
|
||||
mw := &msgWriter{writer: &buf, charset: CharsetUTF8, encoder: mime.QEncoding}
|
||||
mw.writeMsg(m)
|
||||
ms := buf.String()
|
||||
|
||||
var ea []string
|
||||
if !strings.Contains(ms, `MIME-Version: 1.0`) {
|
||||
ea = append(ea, "MIME-Version")
|
||||
}
|
||||
if !strings.Contains(ms, fmt.Sprintf("Date: %s", now.Format(time.RFC1123Z))) {
|
||||
ea = append(ea, "Date")
|
||||
}
|
||||
if !strings.Contains(ms, `Message-ID: <message@id.com>`) {
|
||||
ea = append(ea, "Message-ID")
|
||||
}
|
||||
if !strings.Contains(ms, `Precedence: bulk`) {
|
||||
ea = append(ea, "Precedence")
|
||||
}
|
||||
if !strings.Contains(ms, `Subject: This is a subject`) {
|
||||
ea = append(ea, "Subject")
|
||||
}
|
||||
if !strings.Contains(ms, `User-Agent: go-mail v`) {
|
||||
ea = append(ea, "User-Agent")
|
||||
}
|
||||
if !strings.Contains(ms, `X-Mailer: go-mail v`) {
|
||||
ea = append(ea, "X-Mailer")
|
||||
}
|
||||
if !strings.Contains(ms, `From: "Toni Tester" <test@example.com>`) {
|
||||
ea = append(ea, "From")
|
||||
}
|
||||
if !strings.Contains(ms, `To: "Toni Receiver" <receiver@example.com>`) {
|
||||
ea = append(ea, "To")
|
||||
}
|
||||
if !strings.Contains(ms, `Content-Type: text/plain; charset=UTF-8`) {
|
||||
ea = append(ea, "Content-Type")
|
||||
}
|
||||
if !strings.Contains(ms, `Content-Transfer-Encoding: quoted-printable`) {
|
||||
ea = append(ea, "Content-Transfer-Encoding")
|
||||
}
|
||||
if !strings.Contains(ms, "\r\n\r\nThis is the body") {
|
||||
ea = append(ea, "Message body")
|
||||
}
|
||||
|
||||
pl := m.GetParts()
|
||||
if len(pl) <= 0 {
|
||||
t.Errorf("expected multiple parts but got none")
|
||||
return
|
||||
}
|
||||
if len(pl) == 2 {
|
||||
ap := pl[1]
|
||||
ap.SetCharset(CharsetISO88591)
|
||||
}
|
||||
buf.Reset()
|
||||
mw.writeMsg(m)
|
||||
ms = buf.String()
|
||||
if !strings.Contains(ms, "\r\n\r\nThis is the alternative body") {
|
||||
ea = append(ea, "Message alternative body")
|
||||
}
|
||||
if !strings.Contains(ms, `Content-Type: text/html; charset=ISO-8859-1`) {
|
||||
ea = append(ea, "alternative body charset")
|
||||
}
|
||||
|
||||
if len(ea) > 0 {
|
||||
em := "writeMsg() failed. The following errors occurred:\n"
|
||||
for e := range ea {
|
||||
em += fmt.Sprintf("* incorrect %q field", ea[e])
|
||||
t.Run("msgWriter writes to memory for all charsets", func(t *testing.T) {
|
||||
for _, tt := range charsetTests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
msgwriter := &msgWriter{
|
||||
writer: buffer,
|
||||
charset: tt.value,
|
||||
encoder: mime.QEncoding,
|
||||
}
|
||||
_, err := msgwriter.Write([]byte("test"))
|
||||
if err != nil {
|
||||
t.Errorf("msgWriter failed to write: %s", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
em += fmt.Sprintf("\n\nFull message:\n%s", ms)
|
||||
t.Error(em)
|
||||
}
|
||||
})
|
||||
t.Run("msgWriter writes to memory for all encodings", func(t *testing.T) {
|
||||
for _, tt := range encodingTests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
msgwriter := &msgWriter{
|
||||
writer: buffer,
|
||||
charset: CharsetUTF8,
|
||||
encoder: getEncoder(tt.value),
|
||||
}
|
||||
_, err := msgwriter.Write([]byte("test"))
|
||||
if err != nil {
|
||||
t.Errorf("msgWriter failed to write: %s", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
t.Run("msgWriter should fail on write", func(t *testing.T) {
|
||||
msgwriter := &msgWriter{
|
||||
writer: failReadWriteSeekCloser{},
|
||||
charset: CharsetUTF8,
|
||||
encoder: getEncoder(EncodingQP),
|
||||
}
|
||||
_, err := msgwriter.Write([]byte("test"))
|
||||
if err == nil {
|
||||
t.Fatalf("msgWriter was supposed to fail on write")
|
||||
}
|
||||
})
|
||||
t.Run("msgWriter should fail on previous error", func(t *testing.T) {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
msgwriter := &msgWriter{
|
||||
writer: buffer,
|
||||
charset: CharsetUTF8,
|
||||
encoder: getEncoder(EncodingQP),
|
||||
}
|
||||
_, err := msgwriter.Write([]byte("test"))
|
||||
if err != nil {
|
||||
t.Errorf("msgWriter failed to write: %s", err)
|
||||
}
|
||||
msgwriter.err = errors.New("intentionally failed")
|
||||
_, err = msgwriter.Write([]byte("test2"))
|
||||
if err == nil {
|
||||
t.Fatalf("msgWriter was supposed to fail on second write")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestMsgWriter_writeMsg_PGP tests the writeMsg method of the msgWriter with PGP types set
|
||||
func TestMsgWriter_writeMsg_PGP(t *testing.T) {
|
||||
m := NewMsg(WithPGPType(PGPEncrypt))
|
||||
_ = m.From(`"Toni Tester" <test@example.com>`)
|
||||
_ = m.To(`"Toni Receiver" <receiver@example.com>`)
|
||||
m.Subject("This is a subject")
|
||||
m.SetBodyString(TypeTextPlain, "This is the body")
|
||||
buf := bytes.Buffer{}
|
||||
mw := &msgWriter{writer: &buf, charset: CharsetUTF8, encoder: mime.QEncoding}
|
||||
mw.writeMsg(m)
|
||||
ms := buf.String()
|
||||
if !strings.Contains(ms, `encrypted; protocol="application/pgp-encrypted"`) {
|
||||
t.Errorf("writeMsg failed. Expected PGP encoding header but didn't find it in message output")
|
||||
func TestMsgWriter_writeMsg(t *testing.T) {
|
||||
msgwriter := &msgWriter{
|
||||
charset: CharsetUTF8,
|
||||
encoder: getEncoder(EncodingQP),
|
||||
}
|
||||
t.Run("msgWriter writes a simple message", func(t *testing.T) {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
now := time.Now()
|
||||
msgwriter.writer = buffer
|
||||
message := testMessage(t)
|
||||
message.SetDateWithValue(now)
|
||||
message.SetMessageIDWithValue("message@id.com")
|
||||
message.SetBulk()
|
||||
msgwriter.writeMsg(message)
|
||||
if msgwriter.err != nil {
|
||||
t.Errorf("msgWriter failed to write: %s", msgwriter.err)
|
||||
}
|
||||
|
||||
m = NewMsg(WithPGPType(PGPSignature))
|
||||
_ = m.From(`"Toni Tester" <test@example.com>`)
|
||||
_ = m.To(`"Toni Receiver" <receiver@example.com>`)
|
||||
m.Subject("This is a subject")
|
||||
m.SetBodyString(TypeTextPlain, "This is the body")
|
||||
buf = bytes.Buffer{}
|
||||
mw = &msgWriter{writer: &buf, charset: CharsetUTF8, encoder: mime.QEncoding}
|
||||
mw.writeMsg(m)
|
||||
ms = buf.String()
|
||||
if !strings.Contains(ms, `signed; protocol="application/pgp-signature"`) {
|
||||
t.Errorf("writeMsg failed. Expected PGP encoding header but didn't find it in message output")
|
||||
}
|
||||
var incorrectFields []string
|
||||
if !strings.Contains(buffer.String(), "MIME-Version: 1.0\r\n") {
|
||||
incorrectFields = append(incorrectFields, "MIME-Version")
|
||||
}
|
||||
if !strings.Contains(buffer.String(), fmt.Sprintf("Date: %s\r\n", now.Format(time.RFC1123Z))) {
|
||||
incorrectFields = append(incorrectFields, "Date")
|
||||
}
|
||||
if !strings.Contains(buffer.String(), "Message-ID: <message@id.com>\r\n") {
|
||||
incorrectFields = append(incorrectFields, "Message-ID")
|
||||
}
|
||||
if !strings.Contains(buffer.String(), "Precedence: bulk\r\n") {
|
||||
incorrectFields = append(incorrectFields, "Precedence")
|
||||
}
|
||||
if !strings.Contains(buffer.String(), "X-Auto-Response-Suppress: All\r\n") {
|
||||
incorrectFields = append(incorrectFields, "X-Auto-Response-Suppress")
|
||||
}
|
||||
if !strings.Contains(buffer.String(), "Subject: Testmail\r\n") {
|
||||
incorrectFields = append(incorrectFields, "Subject")
|
||||
}
|
||||
if !strings.Contains(buffer.String(), "User-Agent: go-mail v") {
|
||||
incorrectFields = append(incorrectFields, "User-Agent")
|
||||
}
|
||||
if !strings.Contains(buffer.String(), "X-Mailer: go-mail v") {
|
||||
incorrectFields = append(incorrectFields, "X-Mailer")
|
||||
}
|
||||
if !strings.Contains(buffer.String(), `From: <`+TestSenderValid+`>`) {
|
||||
incorrectFields = append(incorrectFields, "From")
|
||||
}
|
||||
if !strings.Contains(buffer.String(), `To: <`+TestRcptValid+`>`) {
|
||||
incorrectFields = append(incorrectFields, "From")
|
||||
}
|
||||
if !strings.Contains(buffer.String(), "Content-Type: text/plain; charset=UTF-8\r\n") {
|
||||
incorrectFields = append(incorrectFields, "Content-Type")
|
||||
}
|
||||
if !strings.Contains(buffer.String(), "Content-Transfer-Encoding: quoted-printable\r\n") {
|
||||
incorrectFields = append(incorrectFields, "Content-Transfer-Encoding")
|
||||
}
|
||||
if !strings.HasSuffix(buffer.String(), "\r\n\r\nTestmail") {
|
||||
incorrectFields = append(incorrectFields, "Message body")
|
||||
}
|
||||
if len(incorrectFields) > 0 {
|
||||
t.Fatalf("msgWriter failed to write correct fields: %s - mail: %s",
|
||||
strings.Join(incorrectFields, ", "), buffer.String())
|
||||
}
|
||||
})
|
||||
t.Run("msgWriter with no from address uses envelope from", func(t *testing.T) {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
msgwriter.writer = buffer
|
||||
message := NewMsg()
|
||||
if message == nil {
|
||||
t.Fatal("failed to create new message")
|
||||
}
|
||||
if err := message.EnvelopeFrom(TestSenderValid); err != nil {
|
||||
t.Errorf("failed to set sender address: %s", err)
|
||||
}
|
||||
if err := message.To(TestRcptValid); err != nil {
|
||||
t.Errorf("failed to set recipient address: %s", err)
|
||||
}
|
||||
message.Subject("Testmail")
|
||||
message.SetBodyString(TypeTextPlain, "Testmail")
|
||||
msgwriter.writeMsg(message)
|
||||
if msgwriter.err != nil {
|
||||
t.Errorf("msgWriter failed to write: %s", msgwriter.err)
|
||||
}
|
||||
if !strings.Contains(buffer.String(), "From: <"+TestSenderValid+">") {
|
||||
t.Errorf("expected envelope from address as from address, got: %s", buffer.String())
|
||||
}
|
||||
})
|
||||
t.Run("msgWriter with no from address or envelope from", func(t *testing.T) {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
msgwriter.writer = buffer
|
||||
message := NewMsg()
|
||||
if message == nil {
|
||||
t.Fatal("failed to create new message")
|
||||
}
|
||||
msgwriter.writeMsg(message)
|
||||
if msgwriter.err != nil {
|
||||
t.Errorf("msgWriter failed to write: %s", msgwriter.err)
|
||||
}
|
||||
if strings.Contains(buffer.String(), "From:") {
|
||||
t.Errorf("expected no from address, got: %s", buffer.String())
|
||||
}
|
||||
})
|
||||
t.Run("msgWriter writes a multipart/mixed message", func(t *testing.T) {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
msgwriter.writer = buffer
|
||||
message := testMessage(t, WithBoundary("testboundary"))
|
||||
message.AttachFile("testdata/attachment.txt")
|
||||
msgwriter.writeMsg(message)
|
||||
if msgwriter.err != nil {
|
||||
t.Errorf("msgWriter failed to write: %s", msgwriter.err)
|
||||
}
|
||||
if !strings.Contains(buffer.String(), "Content-Type: multipart/mixed") {
|
||||
t.Errorf("expected multipart/mixed, got: %s", buffer.String())
|
||||
}
|
||||
if !strings.Contains(buffer.String(), "--testboundary\r\n") {
|
||||
t.Errorf("expected boundary, got: %s", buffer.String())
|
||||
}
|
||||
if !strings.Contains(buffer.String(), "--testboundary--") {
|
||||
t.Errorf("expected end boundary, got: %s", buffer.String())
|
||||
}
|
||||
})
|
||||
t.Run("msgWriter writes a multipart/related message", func(t *testing.T) {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
msgwriter.writer = buffer
|
||||
message := testMessage(t, WithBoundary("testboundary"))
|
||||
message.EmbedFile("testdata/embed.txt")
|
||||
msgwriter.writeMsg(message)
|
||||
if msgwriter.err != nil {
|
||||
t.Errorf("msgWriter failed to write: %s", msgwriter.err)
|
||||
}
|
||||
if !strings.Contains(buffer.String(), "Content-Type: multipart/related") {
|
||||
t.Errorf("expected multipart/related, got: %s", buffer.String())
|
||||
}
|
||||
if !strings.Contains(buffer.String(), "--testboundary\r\n") {
|
||||
t.Errorf("expected boundary, got: %s", buffer.String())
|
||||
}
|
||||
if !strings.Contains(buffer.String(), "--testboundary--") {
|
||||
t.Errorf("expected end boundary, got: %s", buffer.String())
|
||||
}
|
||||
})
|
||||
t.Run("msgWriter writes a multipart/alternative message", func(t *testing.T) {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
msgwriter.writer = buffer
|
||||
message := testMessage(t, WithBoundary("testboundary"))
|
||||
message.AddAlternativeString(TypeTextHTML, "<html><body><h1>Testmail</h1></body></html>")
|
||||
msgwriter.writeMsg(message)
|
||||
if msgwriter.err != nil {
|
||||
t.Errorf("msgWriter failed to write: %s", msgwriter.err)
|
||||
}
|
||||
if !strings.Contains(buffer.String(), "Content-Type: multipart/alternative") {
|
||||
t.Errorf("expected multipart/alternative, got: %s", buffer.String())
|
||||
}
|
||||
if !strings.Contains(buffer.String(), "--testboundary\r\n") {
|
||||
t.Errorf("expected boundary, got: %s", buffer.String())
|
||||
}
|
||||
if !strings.Contains(buffer.String(), "--testboundary--") {
|
||||
t.Errorf("expected end boundary, got: %s", buffer.String())
|
||||
}
|
||||
})
|
||||
t.Run("msgWriter writes a application/pgp-encrypted message", func(t *testing.T) {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
msgwriter.writer = buffer
|
||||
message := testMessage(t, WithPGPType(PGPEncrypt), WithBoundary("testboundary"))
|
||||
msgwriter.writeMsg(message)
|
||||
if msgwriter.err != nil {
|
||||
t.Errorf("msgWriter failed to write: %s", msgwriter.err)
|
||||
}
|
||||
if !strings.Contains(buffer.String(), "Content-Type: multipart/encrypted") {
|
||||
t.Errorf("expected multipart/encrypted, got: %s", buffer.String())
|
||||
}
|
||||
if !strings.Contains(buffer.String(), "--testboundary\r\n") {
|
||||
t.Errorf("expected boundary, got: %s", buffer.String())
|
||||
}
|
||||
})
|
||||
t.Run("msgWriter writes a application/pgp-signature message", func(t *testing.T) {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
msgwriter.writer = buffer
|
||||
message := testMessage(t, WithPGPType(PGPSignature), WithBoundary("testboundary"))
|
||||
msgwriter.writeMsg(message)
|
||||
if msgwriter.err != nil {
|
||||
t.Errorf("msgWriter failed to write: %s", msgwriter.err)
|
||||
}
|
||||
if !strings.Contains(buffer.String(), "Content-Type: multipart/signed") {
|
||||
t.Errorf("expected multipart/signed, got: %s", buffer.String())
|
||||
}
|
||||
if !strings.Contains(buffer.String(), "--testboundary\r\n") {
|
||||
t.Errorf("expected boundary, got: %s", buffer.String())
|
||||
}
|
||||
})
|
||||
t.Run("msgWriter should ignore NoPGP", func(t *testing.T) {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
msgwriter.writer = buffer
|
||||
message := testMessage(t, WithBoundary("testboundary"))
|
||||
message.pgptype = 9
|
||||
msgwriter.writeMsg(message)
|
||||
if msgwriter.err != nil {
|
||||
t.Errorf("msgWriter failed to write: %s", msgwriter.err)
|
||||
}
|
||||
if !strings.Contains(buffer.String(), "--testboundary\r\n") {
|
||||
t.Errorf("expected boundary, got: %s", buffer.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestMsgWriter_writePreformattedGenHeader(t *testing.T) {
|
||||
t.Run("message with no preformatted headerset", func(t *testing.T) {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
msgwriter := &msgWriter{
|
||||
writer: buffer,
|
||||
charset: CharsetUTF8,
|
||||
encoder: getEncoder(EncodingQP),
|
||||
}
|
||||
message := testMessage(t)
|
||||
message.SetGenHeaderPreformatted(HeaderContentID, "This is a content id")
|
||||
msgwriter.writeMsg(message)
|
||||
if !strings.Contains(buffer.String(), "Content-ID: This is a content id\r\n") {
|
||||
t.Errorf("expected preformatted header, got: %s", buffer.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestMsgWriter_addFiles(t *testing.T) {
|
||||
msgwriter := &msgWriter{
|
||||
charset: CharsetUTF8,
|
||||
encoder: getEncoder(EncodingQP),
|
||||
}
|
||||
t.Run("message with a single file attached", func(t *testing.T) {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
msgwriter.writer = buffer
|
||||
message := testMessage(t)
|
||||
message.AttachFile("testdata/attachment.txt")
|
||||
msgwriter.writeMsg(message)
|
||||
if msgwriter.err != nil {
|
||||
t.Errorf("msgWriter failed to write: %s", msgwriter.err)
|
||||
}
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
if !strings.Contains(buffer.String(), "VGhpcyBpcyBhIHRlc3QgYXR0YWNobWVudA0K") {
|
||||
t.Errorf("attachment not found in mail message. Mail: %s", buffer.String())
|
||||
}
|
||||
default:
|
||||
if !strings.Contains(buffer.String(), "VGhpcyBpcyBhIHRlc3QgYXR0YWNobWVudAo=") {
|
||||
t.Errorf("attachment not found in mail message. Mail: %s", buffer.String())
|
||||
}
|
||||
}
|
||||
if !strings.Contains(buffer.String(), `Content-Disposition: attachment; filename="attachment.txt"`) {
|
||||
t.Errorf("Content-Dispositon header not found for attachment. Mail: %s", buffer.String())
|
||||
}
|
||||
switch runtime.GOOS {
|
||||
case "freebsd":
|
||||
if !strings.Contains(buffer.String(), `Content-Type: application/octet-stream; name="attachment.txt"`) {
|
||||
t.Errorf("Content-Type header not found for attachment. Mail: %s", buffer.String())
|
||||
}
|
||||
default:
|
||||
if !strings.Contains(buffer.String(), `Content-Type: text/plain; charset=utf-8; name="attachment.txt"`) {
|
||||
t.Errorf("Content-Type header not found for attachment. Mail: %s", buffer.String())
|
||||
}
|
||||
}
|
||||
})
|
||||
t.Run("message with a single file attached no extension", func(t *testing.T) {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
msgwriter.writer = buffer
|
||||
message := testMessage(t)
|
||||
message.AttachFile("testdata/attachment")
|
||||
msgwriter.writeMsg(message)
|
||||
if msgwriter.err != nil {
|
||||
t.Errorf("msgWriter failed to write: %s", msgwriter.err)
|
||||
}
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
if !strings.Contains(buffer.String(), "VGhpcyBpcyBhIHRlc3QgYXR0YWNobWVudA0K") {
|
||||
t.Errorf("attachment not found in mail message. Mail: %s", buffer.String())
|
||||
}
|
||||
default:
|
||||
if !strings.Contains(buffer.String(), "VGhpcyBpcyBhIHRlc3QgYXR0YWNobWVudAo=") {
|
||||
t.Errorf("attachment not found in mail message. Mail: %s", buffer.String())
|
||||
}
|
||||
}
|
||||
if !strings.Contains(buffer.String(), `Content-Disposition: attachment; filename="attachment"`) {
|
||||
t.Errorf("Content-Dispositon header not found for attachment. Mail: %s", buffer.String())
|
||||
}
|
||||
if !strings.Contains(buffer.String(), `Content-Type: application/octet-stream; name="attachment"`) {
|
||||
t.Errorf("Content-Type header not found for attachment. Mail: %s", buffer.String())
|
||||
}
|
||||
})
|
||||
t.Run("message with a single file attached custom content-type", func(t *testing.T) {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
msgwriter.writer = buffer
|
||||
message := testMessage(t)
|
||||
message.AttachFile("testdata/attachment.txt", WithFileContentType(TypeAppOctetStream))
|
||||
msgwriter.writeMsg(message)
|
||||
if msgwriter.err != nil {
|
||||
t.Errorf("msgWriter failed to write: %s", msgwriter.err)
|
||||
}
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
if !strings.Contains(buffer.String(), "VGhpcyBpcyBhIHRlc3QgYXR0YWNobWVudA0K") {
|
||||
t.Errorf("attachment not found in mail message. Mail: %s", buffer.String())
|
||||
}
|
||||
default:
|
||||
if !strings.Contains(buffer.String(), "VGhpcyBpcyBhIHRlc3QgYXR0YWNobWVudAo=") {
|
||||
t.Errorf("attachment not found in mail message. Mail: %s", buffer.String())
|
||||
}
|
||||
}
|
||||
if !strings.Contains(buffer.String(), `Content-Disposition: attachment; filename="attachment.txt"`) {
|
||||
t.Errorf("Content-Dispositon header not found for attachment. Mail: %s", buffer.String())
|
||||
}
|
||||
if !strings.Contains(buffer.String(), `Content-Type: application/octet-stream; name="attachment.txt"`) {
|
||||
t.Errorf("Content-Type header not found for attachment. Mail: %s", buffer.String())
|
||||
}
|
||||
})
|
||||
t.Run("message with a single file attached custom transfer-encoding", func(t *testing.T) {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
msgwriter.writer = buffer
|
||||
message := testMessage(t)
|
||||
message.AttachFile("testdata/attachment.txt", WithFileEncoding(EncodingUSASCII))
|
||||
msgwriter.writeMsg(message)
|
||||
if msgwriter.err != nil {
|
||||
t.Errorf("msgWriter failed to write: %s", msgwriter.err)
|
||||
}
|
||||
if !strings.Contains(buffer.String(), "\r\n\r\nThis is a test attachment") {
|
||||
t.Errorf("attachment not found in mail message. Mail: %s", buffer.String())
|
||||
}
|
||||
if !strings.Contains(buffer.String(), `Content-Disposition: attachment; filename="attachment.txt"`) {
|
||||
t.Errorf("Content-Dispositon header not found for attachment. Mail: %s", buffer.String())
|
||||
}
|
||||
switch runtime.GOOS {
|
||||
case "freebsd":
|
||||
if !strings.Contains(buffer.String(), `Content-Type: application/octet-stream; name="attachment.txt"`) {
|
||||
t.Errorf("Content-Type header not found for attachment. Mail: %s", buffer.String())
|
||||
}
|
||||
default:
|
||||
if !strings.Contains(buffer.String(), `Content-Type: text/plain; charset=utf-8; name="attachment.txt"`) {
|
||||
t.Errorf("Content-Type header not found for attachment. Mail: %s", buffer.String())
|
||||
}
|
||||
}
|
||||
if !strings.Contains(buffer.String(), `Content-Transfer-Encoding: 7bit`) {
|
||||
t.Errorf("Content-Transfer-Encoding header not found for attachment. Mail: %s", buffer.String())
|
||||
}
|
||||
})
|
||||
t.Run("message with a single file attached custom description", func(t *testing.T) {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
msgwriter.writer = buffer
|
||||
message := testMessage(t)
|
||||
message.AttachFile("testdata/attachment.txt", WithFileDescription("Testdescription"))
|
||||
msgwriter.writeMsg(message)
|
||||
if msgwriter.err != nil {
|
||||
t.Errorf("msgWriter failed to write: %s", msgwriter.err)
|
||||
}
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
if !strings.Contains(buffer.String(), "VGhpcyBpcyBhIHRlc3QgYXR0YWNobWVudA0K") {
|
||||
t.Errorf("attachment not found in mail message. Mail: %s", buffer.String())
|
||||
}
|
||||
default:
|
||||
if !strings.Contains(buffer.String(), "VGhpcyBpcyBhIHRlc3QgYXR0YWNobWVudAo=") {
|
||||
t.Errorf("attachment not found in mail message. Mail: %s", buffer.String())
|
||||
}
|
||||
}
|
||||
if !strings.Contains(buffer.String(), `Content-Disposition: attachment; filename="attachment.txt"`) {
|
||||
t.Errorf("Content-Dispositon header not found for attachment. Mail: %s", buffer.String())
|
||||
}
|
||||
switch runtime.GOOS {
|
||||
case "freebsd":
|
||||
if !strings.Contains(buffer.String(), `Content-Type: application/octet-stream; name="attachment.txt"`) {
|
||||
t.Errorf("Content-Type header not found for attachment. Mail: %s", buffer.String())
|
||||
}
|
||||
default:
|
||||
if !strings.Contains(buffer.String(), `Content-Type: text/plain; charset=utf-8; name="attachment.txt"`) {
|
||||
t.Errorf("Content-Type header not found for attachment. Mail: %s", buffer.String())
|
||||
}
|
||||
}
|
||||
if !strings.Contains(buffer.String(), `Content-Transfer-Encoding: base64`) {
|
||||
t.Errorf("Content-Transfer-Encoding header not found for attachment. Mail: %s", buffer.String())
|
||||
}
|
||||
if !strings.Contains(buffer.String(), `Content-Description: Testdescription`) {
|
||||
t.Errorf("Content-Description header not found for attachment. Mail: %s", buffer.String())
|
||||
}
|
||||
})
|
||||
t.Run("message with attachment but no body part", func(t *testing.T) {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
msgwriter.writer = buffer
|
||||
message := testMessage(t)
|
||||
message.parts = nil
|
||||
message.AttachFile("testdata/attachment.txt")
|
||||
msgwriter.writeMsg(message)
|
||||
if msgwriter.err != nil {
|
||||
t.Errorf("msgWriter failed to write: %s", msgwriter.err)
|
||||
}
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
if !strings.Contains(buffer.String(), "VGhpcyBpcyBhIHRlc3QgYXR0YWNobWVudA0K") {
|
||||
t.Errorf("attachment not found in mail message. Mail: %s", buffer.String())
|
||||
}
|
||||
default:
|
||||
if !strings.Contains(buffer.String(), "VGhpcyBpcyBhIHRlc3QgYXR0YWNobWVudAo=") {
|
||||
t.Errorf("attachment not found in mail message. Mail: %s", buffer.String())
|
||||
}
|
||||
}
|
||||
if !strings.Contains(buffer.String(), `Content-Disposition: attachment; filename="attachment.txt"`) {
|
||||
t.Errorf("Content-Dispositon header not found for attachment. Mail: %s", buffer.String())
|
||||
}
|
||||
switch runtime.GOOS {
|
||||
case "freebsd":
|
||||
if !strings.Contains(buffer.String(), `Content-Type: application/octet-stream; name="attachment.txt"`) {
|
||||
t.Errorf("Content-Type header not found for attachment. Mail: %s", buffer.String())
|
||||
}
|
||||
default:
|
||||
if !strings.Contains(buffer.String(), `Content-Type: text/plain; charset=utf-8; name="attachment.txt"`) {
|
||||
t.Errorf("Content-Type header not found for attachment. Mail: %s", buffer.String())
|
||||
}
|
||||
}
|
||||
if !strings.Contains(buffer.String(), `Content-Transfer-Encoding: base64`) {
|
||||
t.Errorf("Content-Transfer-Encoding header not found for attachment. Mail: %s", buffer.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestMsgWriter_writePart(t *testing.T) {
|
||||
msgwriter := &msgWriter{
|
||||
charset: CharsetUTF8,
|
||||
encoder: getEncoder(EncodingQP),
|
||||
}
|
||||
t.Run("message with no part charset should use default message charset", func(t *testing.T) {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
msgwriter.writer = buffer
|
||||
message := testMessage(t, WithCharset(CharsetUTF7))
|
||||
message.AddAlternativeString(TypeTextPlain, "thisisatest")
|
||||
message.parts[1].charset = ""
|
||||
msgwriter.writeMsg(message)
|
||||
if msgwriter.err != nil {
|
||||
t.Errorf("msgWriter failed to write: %s", msgwriter.err)
|
||||
}
|
||||
if !strings.Contains(buffer.String(), "ontent-Type: text/plain; charset=UTF-7\r\n\r\nTestmail") {
|
||||
t.Errorf("part not found in mail message. Mail: %s", buffer.String())
|
||||
}
|
||||
if !strings.Contains(buffer.String(), "ontent-Type: text/plain; charset=UTF-7\r\n\r\nthisisatest") {
|
||||
t.Errorf("part not found in mail message. Mail: %s", buffer.String())
|
||||
}
|
||||
})
|
||||
t.Run("message with parts that have a description", func(t *testing.T) {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
msgwriter.writer = buffer
|
||||
message := testMessage(t)
|
||||
message.AddAlternativeString(TypeTextPlain, "thisisatest")
|
||||
message.parts[1].description = "thisisadescription"
|
||||
msgwriter.writeMsg(message)
|
||||
if msgwriter.err != nil {
|
||||
t.Errorf("msgWriter failed to write: %s", msgwriter.err)
|
||||
}
|
||||
if !strings.Contains(buffer.String(), "Content-Description: thisisadescription") {
|
||||
t.Errorf("part description not found in mail message. Mail: %s", buffer.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestMsgWriter_writeString(t *testing.T) {
|
||||
msgwriter := &msgWriter{
|
||||
charset: CharsetUTF8,
|
||||
encoder: getEncoder(EncodingQP),
|
||||
}
|
||||
t.Run("writeString succeeds", func(t *testing.T) {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
msgwriter.writer = buffer
|
||||
msgwriter.writeString("thisisatest")
|
||||
if !strings.EqualFold(buffer.String(), "thisisatest") {
|
||||
t.Errorf("writeString failed, expected: thisisatest got: %s", buffer.String())
|
||||
}
|
||||
})
|
||||
t.Run("writeString fails", func(t *testing.T) {
|
||||
msgwriter.writer = failReadWriteSeekCloser{}
|
||||
msgwriter.writeString("thisisatest")
|
||||
if msgwriter.err == nil {
|
||||
t.Errorf("writeString succeeded, expected error")
|
||||
}
|
||||
})
|
||||
t.Run("writeString on errored writer should return", func(t *testing.T) {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
msgwriter.writer = buffer
|
||||
msgwriter.err = errors.New("intentional error")
|
||||
msgwriter.writeString("thisisatest")
|
||||
if !strings.EqualFold(buffer.String(), "") {
|
||||
t.Errorf("writeString succeeded, expected: empty string, got: %s", buffer.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestMsgWriter_writeHeader(t *testing.T) {
|
||||
msgwriter := &msgWriter{
|
||||
charset: CharsetUTF8,
|
||||
encoder: getEncoder(EncodingQP),
|
||||
}
|
||||
t.Run("writeHeader with single value", func(t *testing.T) {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
msgwriter.writer = buffer
|
||||
msgwriter.writeHeader(HeaderMessageID, "this.is.a.test")
|
||||
if !strings.EqualFold(buffer.String(), "Message-ID: this.is.a.test\r\n") {
|
||||
t.Errorf("writeHeader failed, expected: %s, got: %s", "Message-ID: this.is.a.test",
|
||||
buffer.String())
|
||||
}
|
||||
})
|
||||
t.Run("writeHeader with multiple values", func(t *testing.T) {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
msgwriter.writer = buffer
|
||||
msgwriter.writeHeader(HeaderMessageID, "this.is.a.test", "this.as.well")
|
||||
if !strings.EqualFold(buffer.String(), "Message-ID: this.is.a.test, this.as.well\r\n") {
|
||||
t.Errorf("writeHeader failed, expected: %s, got: %s", "Message-ID: this.is.a.test, this.as.well",
|
||||
buffer.String())
|
||||
}
|
||||
})
|
||||
t.Run("writeHeader with no values", func(t *testing.T) {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
msgwriter.writer = buffer
|
||||
msgwriter.writeHeader(HeaderMessageID)
|
||||
// While technically it is permitted to have empty headers, it's recommend to omit them if
|
||||
// no value is present. We follow this recommendation.
|
||||
if !strings.EqualFold(buffer.String(), "") {
|
||||
t.Errorf("writeHeader failed, expected: %s, got: %s", "", buffer.String())
|
||||
}
|
||||
})
|
||||
t.Run("writeHeader with very long value", func(t *testing.T) {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
msgwriter.writer = buffer
|
||||
msgwriter.writeHeader(HeaderMessageID, strings.Repeat("a", MaxHeaderLength-13), "next-row")
|
||||
want := "Message-ID:\r\n " + strings.Repeat("a", MaxHeaderLength-13) + ",\r\n next-row\r\n"
|
||||
if !strings.EqualFold(buffer.String(), want) {
|
||||
t.Errorf("writeHeader failed, expected: %s, got: %s", want, buffer.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestMsgWriter_writeBody(t *testing.T) {
|
||||
t.Log("We only cover some edge-cases here, most of the functionality is tested already very thoroughly.")
|
||||
|
||||
msgwriter := &msgWriter{
|
||||
charset: CharsetUTF8,
|
||||
encoder: getEncoder(EncodingQP),
|
||||
}
|
||||
t.Run("writeBody on NoEncoding", func(t *testing.T) {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
msgwriter.writer = buffer
|
||||
message := testMessage(t)
|
||||
msgwriter.writeBody(message.parts[0].writeFunc, NoEncoding)
|
||||
if msgwriter.err != nil {
|
||||
t.Errorf("writeBody failed to write: %s", msgwriter.err)
|
||||
}
|
||||
})
|
||||
t.Run("writeBody on NoEncoding fails on write", func(t *testing.T) {
|
||||
msgwriter.writer = failReadWriteSeekCloser{}
|
||||
message := testMessage(t)
|
||||
msgwriter.writeBody(message.parts[0].writeFunc, NoEncoding)
|
||||
if msgwriter.err == nil {
|
||||
t.Errorf("writeBody succeeded, expected error")
|
||||
}
|
||||
if !strings.EqualFold(msgwriter.err.Error(), "bodyWriter io.Copy: intentional write failure") {
|
||||
t.Errorf("expected error: bodyWriter io.Copy: intentional write failure, got: %s", msgwriter.err)
|
||||
}
|
||||
})
|
||||
t.Run("writeBody on NoEncoding fails on writeFunc", func(t *testing.T) {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
msgwriter.writer = buffer
|
||||
writeFunc := func(io.Writer) (int64, error) {
|
||||
return 0, errors.New("intentional write failure")
|
||||
}
|
||||
msgwriter.writeBody(writeFunc, NoEncoding)
|
||||
if msgwriter.err == nil {
|
||||
t.Errorf("writeBody succeeded, expected error")
|
||||
}
|
||||
if !strings.EqualFold(msgwriter.err.Error(), "bodyWriter function: intentional write failure") {
|
||||
t.Errorf("expected error: bodyWriter function: intentional write failure, got: %s", msgwriter.err)
|
||||
}
|
||||
})
|
||||
t.Run("writeBody Quoted-Printable fails on write", func(t *testing.T) {
|
||||
msgwriter.writer = failReadWriteSeekCloser{}
|
||||
message := testMessage(t)
|
||||
msgwriter.writeBody(message.parts[0].writeFunc, EncodingQP)
|
||||
if msgwriter.err == nil {
|
||||
t.Errorf("writeBody succeeded, expected error")
|
||||
}
|
||||
if !strings.EqualFold(msgwriter.err.Error(), "bodyWriter function: intentional write failure") {
|
||||
t.Errorf("expected error: bodyWriter function: intentional write failure, got: %s", msgwriter.err)
|
||||
}
|
||||
})
|
||||
t.Run("writeBody Quoted-Printable fails on writeFunc", func(t *testing.T) {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
msgwriter.writer = buffer
|
||||
writeFunc := func(io.Writer) (int64, error) {
|
||||
return 0, errors.New("intentional write failure")
|
||||
}
|
||||
msgwriter.writeBody(writeFunc, EncodingQP)
|
||||
if msgwriter.err == nil {
|
||||
t.Errorf("writeBody succeeded, expected error")
|
||||
}
|
||||
if !strings.EqualFold(msgwriter.err.Error(), "bodyWriter function: intentional write failure") {
|
||||
t.Errorf("expected error: bodyWriter function: intentional write failure, got: %s", msgwriter.err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -554,9 +554,9 @@ func (c *Client) Noop() error {
|
|||
|
||||
// Quit sends the QUIT command and closes the connection to the server.
|
||||
func (c *Client) Quit() error {
|
||||
if err := c.hello(); err != nil {
|
||||
return err
|
||||
}
|
||||
// See https://github.com/golang/go/issues/70011
|
||||
_ = c.hello() // ignore error; we're quitting anyhow
|
||||
|
||||
_, _, err := c.cmd(221, "QUIT")
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
@ -900,6 +900,35 @@ Goodbye.
|
|||
QUIT
|
||||
`
|
||||
|
||||
func TestHELOFailed(t *testing.T) {
|
||||
serverLines := `502 EH?
|
||||
502 EH?
|
||||
221 OK
|
||||
`
|
||||
clientLines := `EHLO localhost
|
||||
HELO localhost
|
||||
QUIT
|
||||
`
|
||||
server := strings.Join(strings.Split(serverLines, "\n"), "\r\n")
|
||||
client := strings.Join(strings.Split(clientLines, "\n"), "\r\n")
|
||||
var cmdbuf strings.Builder
|
||||
bcmdbuf := bufio.NewWriter(&cmdbuf)
|
||||
var fake faker
|
||||
fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(server)), bcmdbuf)
|
||||
c := &Client{Text: textproto.NewConn(fake), localName: "localhost"}
|
||||
if err := c.Hello("localhost"); err == nil {
|
||||
t.Fatal("expected EHLO to fail")
|
||||
}
|
||||
if err := c.Quit(); err != nil {
|
||||
t.Errorf("QUIT failed: %s", err)
|
||||
}
|
||||
_ = bcmdbuf.Flush()
|
||||
actual := cmdbuf.String()
|
||||
if client != actual {
|
||||
t.Errorf("Got:\n%s\nWant:\n%s", actual, client)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtensions(t *testing.T) {
|
||||
fake := func(server string) (c *Client, bcmdbuf *bufio.Writer, cmdbuf *strings.Builder) {
|
||||
server = strings.Join(strings.Split(server, "\n"), "\r\n")
|
||||
|
|
8
testdata/RFC5322-A1-1-invalid-from.eml
vendored
Normal file
8
testdata/RFC5322-A1-1-invalid-from.eml
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
From: §§§§§§§§
|
||||
To: Mary Smith <mary@example.net>
|
||||
Subject: Saying Hello
|
||||
Date: Fri, 21 Nov 1997 09:55:06 -0600
|
||||
Message-ID: <1234@local.machine.example>
|
||||
|
||||
This is a message just to say hello.
|
||||
So, "Hello".
|
3
testdata/RFC5322-A1-1-invalid-from.eml.license
vendored
Normal file
3
testdata/RFC5322-A1-1-invalid-from.eml.license
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
// SPDX-FileCopyrightText: Copyright (c) 2022-2024 The go-mail Authors
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
8
testdata/RFC5322-A1-1.eml
vendored
Normal file
8
testdata/RFC5322-A1-1.eml
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
From: John Doe <jdoe@machine.example>
|
||||
To: Mary Smith <mary@example.net>
|
||||
Subject: Saying Hello
|
||||
Date: Fri, 21 Nov 1997 09:55:06 -0600
|
||||
Message-ID: <1234@local.machine.example>
|
||||
|
||||
This is a message just to say hello.
|
||||
So, "Hello".
|
3
testdata/RFC5322-A1-1.eml.license
vendored
Normal file
3
testdata/RFC5322-A1-1.eml.license
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
// SPDX-FileCopyrightText: Copyright (c) 2022-2024 The go-mail Authors
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
1
testdata/attachment
vendored
Normal file
1
testdata/attachment
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
This is a test attachment
|
3
testdata/attachment.license
vendored
Normal file
3
testdata/attachment.license
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
// SPDX-FileCopyrightText: Copyright (c) 2022-2024 The go-mail Authors
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
1
testdata/attachment.txt
vendored
Normal file
1
testdata/attachment.txt
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
This is a test attachment
|
3
testdata/attachment.txt.license
vendored
Normal file
3
testdata/attachment.txt.license
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
// SPDX-FileCopyrightText: Copyright (c) 2022-2024 The go-mail Authors
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
1
testdata/embed.txt
vendored
Normal file
1
testdata/embed.txt
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
This is a test embed
|
3
testdata/embed.txt.license
vendored
Normal file
3
testdata/embed.txt.license
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
// SPDX-FileCopyrightText: Copyright (c) 2022-2024 The go-mail Authors
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
1
testdata/logo.svg
vendored
Normal file
1
testdata/logo.svg
vendored
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 20 KiB |
367
testdata/logo.svg.base64
vendored
Normal file
367
testdata/logo.svg.base64
vendored
Normal file
|
@ -0,0 +1,367 @@
|
|||
PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PCFE
|
||||
T0NUWVBFIHN2ZyBQVUJMSUMgIi0vL1czQy8vRFREIFNWRyAxLjEvL0VOIiAiaHR0cDovL3d3dy53
|
||||
My5vcmcvR3JhcGhpY3MvU1ZHLzEuMS9EVEQvc3ZnMTEuZHRkIj48c3ZnIHdpZHRoPSIxMDAlIiBo
|
||||
ZWlnaHQ9IjEwMCUiIHZpZXdCb3g9IjAgMCA2MDAgNjAwIiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJo
|
||||
dHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3Jn
|
||||
LzE5OTkveGxpbmsiIHhtbDpzcGFjZT0icHJlc2VydmUiIHhtbG5zOnNlcmlmPSJodHRwOi8vd3d3
|
||||
LnNlcmlmLmNvbS8iIHN0eWxlPSJmaWxsLXJ1bGU6ZXZlbm9kZDtjbGlwLXJ1bGU6ZXZlbm9kZDtz
|
||||
dHJva2UtbGluZWNhcDpyb3VuZDtzdHJva2UtbGluZWpvaW46cm91bmQ7c3Ryb2tlLW1pdGVybGlt
|
||||
aXQ6MS41OyI+PHJlY3QgaWQ9ItCc0L7QvdGC0LDQttC90LDRjy3QvtCx0LvQsNGB0YLRjDEiIHNl
|
||||
cmlmOmlkPSLQnNC+0L3RgtCw0LbQvdCw0Y8g0L7QsdC70LDRgdGC0YwxIiB4PSIwIiB5PSIwIiB3
|
||||
aWR0aD0iNjAwIiBoZWlnaHQ9IjYwMCIgc3R5bGU9ImZpbGw6bm9uZTsiLz48Zz48cGF0aCBkPSJN
|
||||
NTg0LjcyNiw5OC4zOTVjLTAsLTkuNzM4IC03LjkwNywtMTcuNjQ1IC0xNy42NDUsLTE3LjY0NWwt
|
||||
NTEwLjAyNywwYy05LjczOSwwIC0xNy42NDUsNy45MDcgLTE3LjY0NSwxNy42NDVsLTAsMzE5Ljc5
|
||||
NGMtMCw5LjczOSA3LjkwNiwxNy42NDYgMTcuNjQ1LDE3LjY0Nmw1MTAuMDI3LC0wYzkuNzM4LC0w
|
||||
IDE3LjY0NSwtNy45MDcgMTcuNjQ1LC0xNy42NDZsLTAsLTMxOS43OTRaIiBzdHlsZT0iZmlsbDoj
|
||||
MmMyYzJjO3N0cm9rZTojMDAwO3N0cm9rZS13aWR0aDoyLjFweDsiLz48cGF0aCBkPSJNNTg1LjEy
|
||||
Niw5OC4zOTVjLTAsLTkuNzM4IC03LjkwNywtMTcuNjQ1IC0xNy42NDUsLTE3LjY0NWwtNDk0Ljgz
|
||||
OSwwYy05LjczOCwwIC0xNy42NDUsNy45MDcgLTE3LjY0NSwxNy42NDVsMCwzMTkuNzk0YzAsOS43
|
||||
MzkgNy45MDcsMTcuNjQ2IDE3LjY0NSwxNy42NDZsNDk0LjgzOSwtMGM5LjczOCwtMCAxNy42NDUs
|
||||
LTcuOTA3IDE3LjY0NSwtMTcuNjQ2bC0wLC0zMTkuNzk0WiIgc3R5bGU9ImZpbGw6IzM3MzczNztz
|
||||
dHJva2U6IzAwMDtzdHJva2Utd2lkdGg6Mi4wN3B4OyIvPjxwYXRoIGQ9Ik01NjcuMjU5LDExNC40
|
||||
NzJjLTAsLTguNzYgLTcuMTEyLC0xNS44NzIgLTE1Ljg3MiwtMTUuODcybC00NjIuNjUxLDBjLTgu
|
||||
NzYsMCAtMTUuODcyLDcuMTEyIC0xNS44NzIsMTUuODcybDAsMjg3LjY0MWMwLDguNzYgNy4xMTIs
|
||||
MTUuODcxIDE1Ljg3MiwxNS44NzFsNDYyLjY1MSwwYzguNzYsMCAxNS44NzIsLTcuMTExIDE1Ljg3
|
||||
MiwtMTUuODcxbC0wLC0yODcuNjQxWiIgc3R5bGU9ImZpbGw6I2ZmZjJlYjtzdHJva2U6IzAwMDtz
|
||||
dHJva2Utd2lkdGg6MS45cHg7Ii8+PHBhdGggZD0iTTIzNi4xNDUsNDM1Ljg3OGwtMjMuNDIsOTEu
|
||||
MDA4bDEzMC43NjUsLTBsMjIuMzY1LC05MC43NTZsLTEyOS43MSwtMC4yNTJaIiBzdHlsZT0iZmls
|
||||
bDojMmMyYzJjO3N0cm9rZTojMDAwO3N0cm9rZS13aWR0aDoyLjQ3cHg7Ii8+PHBhdGggZD0iTTI0
|
||||
NS4zOTcsNDM1Ljg3OGwtMjMuNDIxLDkxLjAwOGwxMzAuNzY2LC0wbDIyLjM2NSwtOTAuNzU2bC0x
|
||||
MjkuNzEsLTAuMjUyWiIgc3R5bGU9ImZpbGw6IzM3MzczNztzdHJva2U6IzAwMDtzdHJva2Utd2lk
|
||||
dGg6Mi40N3B4OyIvPjxwYXRoIGQ9Ik00NzIuNTg0LDUzMS4wOTNjLTAsLTIuODI3IC0yLjI5NSwt
|
||||
NS4xMjEgLTUuMTIxLC01LjEyMWwtMjkyLjU0MSwtMGMtMi44MjYsLTAgLTUuMTIxLDIuMjk0IC01
|
||||
LjEyMSw1LjEyMWwwLDEwLjI0M2MwLDIuODI2IDIuMjk1LDUuMTIxIDUuMTIxLDUuMTIxbDI5Mi41
|
||||
NDEsLTBjMi44MjYsLTAgNS4xMjEsLTIuMjk1IDUuMTIxLC01LjEyMWwtMCwtMTAuMjQzWiIgc3R5
|
||||
bGU9ImZpbGw6IzJjMmMyYztzdHJva2U6IzAwMDtzdHJva2Utd2lkdGg6Mi40N3B4OyIvPjxnPjxw
|
||||
YXRoIGQ9Ik0xOTAuMzc5LDQzMS40NTVjLTAsLTEuOTE2IC0xLjU1NiwtMy40NzIgLTMuNDcyLC0z
|
||||
LjQ3MmwtNTQuNTYzLDBjLTEuOTE3LDAgLTMuNDcyLDEuNTU2IC0zLjQ3MiwzLjQ3MmwtMCw0Ni4z
|
||||
MDljLTAsMS45MTYgMS41NTUsMy40NzIgMy40NzIsMy40NzJsNTQuNTYzLC0wYzEuOTE2LC0wIDMu
|
||||
NDcyLC0xLjU1NiAzLjQ3MiwtMy40NzJsLTAsLTQ2LjMwOVoiIHN0eWxlPSJmaWxsOiNlZWFjMDE7
|
||||
c3Ryb2tlOiMwMDA7c3Ryb2tlLXdpZHRoOjMuMXB4OyIvPjxnPjxwYXRoIGQ9Ik0xMzkuOTk2LDQ0
|
||||
Mi44NTNjLTAsMC40MiAtMC4wNzEsMC45MjIgMC4wNjMsMS4zMjNjMC41MywxLjU5MiAyLjc1LDEu
|
||||
Mzg2IDQuMDk1LDEuMzg2YzIuMzk3LC0wIDQuNjc0LC0wLjEwOCA2LjQ4OSwtMS44MjdjMC40NSwt
|
||||
MC40MjYgMC45NjYsLTAuOTQgMS4wNzIsLTEuNTc1YzAuMDYxLC0wLjM2NyAwLjEwMiwtMS4yMDgg
|
||||
LTAuMTI2LC0xLjUxMmMtMC44MDMsLTEuMDcxIC0yLjg0NCwtMC4zNzMgLTMuNjU1LDAuMjUyYy0w
|
||||
LjcxNCwwLjU1IC0wLjgyMiwxLjgyNiAtMC45NDUsMi42NDZjLTAuNDA2LDIuNzA1IDAuMzg3LDUu
|
||||
MDMgMy4xNSw1Ljg1OWMzLjQwNSwxLjAyMSA2Ljc4NCwwLjA2IDEwLjA4NiwtMC44MDljMS41OTcs
|
||||
LTAuNDIxIDMuNjYsLTAuNzcgNS4wMTcsLTEuNzJjMC40NTIsLTAuMzE2IDAuNDQ0LC0wLjI4NCAw
|
||||
LjkwMSwtMC41NThjMS4yMDgsLTAuNzI1IDMuODY5LC0xLjkyNyAyLjQ1NywtMy41OTFjLTAuMTks
|
||||
LTAuMjI0IC0xLjEzOSwtMC4yMzcgLTEuMjYsMC4xMjZjLTAuNDIsMS4yNTcgLTEuMzI2LDMuMzAy
|
||||
IDAuNTY3LDMuNzhjMS4zOTgsMC4zNTMgMi45MTksMC4xODkgNC4zNDcsMC4xODljMC4xOTMsLTAg
|
||||
MS40ODUsLTAuMjc5IDEuNzAxLC0wLjA2M2MwLjU1NSwwLjU1NSAxLjU0MywwLjgxMSAyLjMzMSwx
|
||||
LjAwOGMwLjkzMiwwLjIzMyAxLjk0NCwwLjYwNiAyLjg5OSwwLjY5M2MwLjg2OSwwLjA3OSAxLjc5
|
||||
LC0wLjA0NSAyLjY0NiwwLjEyNiIgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2U6IzAwMDtzdHJva2Ut
|
||||
d2lkdGg6MS45NnB4OyIvPjxwYXRoIGQ9Ik0xNDIuNzA1LDQ1Mi42ODFjMC4xMTksMC41NTEgMC44
|
||||
OTMsMC42NjkgMS4zMTgsMC44NDZjMS41OTgsMC42NjYgMy41MTQsMC44NzQgNS4yMzQsMC45ODJj
|
||||
Mi4zNzgsMC4xNDggNC43MDQsLTAuMTU5IDcuMDU3LC0wLjMxNWMxLjg2MSwtMC4xMjUgMy43NDYs
|
||||
MC4xMTYgNS42MDcsLTBjMi4wNTEsLTAuMTI5IDQuMzIxLC0wLjM3NyA2LjM2NCwtMC4wNjNjMS4w
|
||||
NzIsMC4xNjQgMi4wODksMC40OCAzLjE1LDAuNjkzIiBzdHlsZT0iZmlsbDpub25lO3N0cm9rZToj
|
||||
MDAwO3N0cm9rZS13aWR0aDoxLjk2cHg7Ii8+PHBhdGggZD0iTTE3Ni4zNDksNDU1LjA3NmMxLjEx
|
||||
MywtMC4wMjEgMi4yMjYsLTAuMDQyIDMuMzQsLTAuMDYzIiBzdHlsZT0iZmlsbDpub25lO3N0cm9r
|
||||
ZTojMDAwO3N0cm9rZS13aWR0aDoxLjk2cHg7Ii8+PHBhdGggZD0iTTE0NS4xNjIsNDU5Ljg2NGwy
|
||||
MS42NzQsLTAiIHN0eWxlPSJmaWxsOm5vbmU7c3Ryb2tlOiMwMDA7c3Ryb2tlLXdpZHRoOjEuOTZw
|
||||
eDsiLz48L2c+PC9nPjxnPjxnPjxwYXRoIGQ9Ik03Ni4yNzMsMTIxLjQ5NGw0ODcuNTU3LC0wLjAx
|
||||
NSIgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2U6I2Y0ZGRkMDtzdHJva2Utd2lkdGg6NC45M3B4O3N0
|
||||
cm9rZS1saW5lY2FwOnNxdWFyZTsiLz48cGF0aCBkPSJNMjMwLjk1MSwyMjQuMTc1Yy0wLjgwMiwt
|
||||
My4zMjcgLTguMDU5LC03Ljk4NCAtMTEuMDA1LC04Ljk0NWMtMzUuODQ2LC0xMS42OTkgLTIwLjc4
|
||||
MywyNy4zNzIgLTAuNjE4LDI0LjM0MiIgc3R5bGU9ImZpbGw6IzYxZTBlZTsiLz48cGF0aCBkPSJN
|
||||
MjM0Ljk0NCwyMjMuMjEzYy0wLjM3OSwtMS41NzQgLTEuNTIyLC0zLjQzNiAtMy4yNzYsLTUuMTc0
|
||||
Yy0zLjA2MSwtMy4wMzMgLTguMDE0LC01LjkyIC0xMC40NDgsLTYuNzE0Yy0xMy4xMzUsLTQuMjg3
|
||||
IC0yMC42MTgsLTEuOTk0IC0yNC4wNzEsMS44MTljLTMuNjM4LDQuMDE3IC0zLjgzNiwxMC40OTIg
|
||||
LTAuODI1LDE2LjY1MWMzLjk4MSw4LjE0MyAxMy4zMjEsMTUuMzg2IDIzLjYxNSwxMy44MzljMi4y
|
||||
NDIsLTAuMzM3IDMuNzg4LC0yLjQzIDMuNDUxLC00LjY3MmMtMC4zMzcsLTIuMjQyIC0yLjQzLC0z
|
||||
Ljc4OSAtNC42NzIsLTMuNDUyYy02LjY0NywwLjk5OSAtMTIuNDQ0LC00LjA2NSAtMTUuMDE0LC05
|
||||
LjMyM2MtMC44MDEsLTEuNjM3IC0xLjI5MywtMy4zMDggLTEuMzAyLC00Ljg0N2MtMC4wMDUsLTEu
|
||||
MDIgMC4xOTUsLTEuOTc0IDAuODM2LC0yLjY4MmMwLjgzMiwtMC45MTggMi4yMjUsLTEuMzU0IDQu
|
||||
MTMzLC0xLjQ4MWMyLjg1MiwtMC4xOTEgNi41NjcsMC40MTIgMTEuMywxLjk1N2MxLjQ1NiwwLjQ3
|
||||
NSA0LjE2LDIuMDk2IDYuMjU3LDMuODY5YzAuNjM2LDAuNTM3IDEuMjE1LDEuMDg4IDEuNjU4LDEu
|
||||
NjM2YzAuMTUyLDAuMTg4IDAuMzMxLDAuMzI3IDAuMzcyLDAuNDk4YzAuNTMxLDIuMjA0IDIuNzUx
|
||||
LDMuNTYyIDQuOTU1LDMuMDMxYzIuMjA0LC0wLjUzMSAzLjU2MiwtMi43NTIgMy4wMzEsLTQuOTU1
|
||||
WiIvPjxwYXRoIGQ9Ik0zNDkuMzI2LDEyMy41MjdjMCwwIC0xNS4zNSwtMTMuODYyIC0xOC4xMTQs
|
||||
MjIuODM0IiBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojNjFlMGVlO3N0cm9rZS13aWR0aDo2LjE2
|
||||
cHg7Ii8+PHBhdGggZD0iTTI5OS42NjIsMTIzLjQ4NWMtMCwtMCAxMi42NzEsLTE1LjI2NSAyMC4x
|
||||
MTIsMjEuODk4IiBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojNjFlMGVlO3N0cm9rZS13aWR0aDo2
|
||||
LjE2cHg7Ii8+PHBhdGggZD0iTTMzNi4xMTEsMTA4Ljc3M2MtMCwwIC0xNi44MDgsLTkuMDk5IC0x
|
||||
MC44ODIsMzIuMTI0IiBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojNjFlMGVlO3N0cm9rZS13aWR0
|
||||
aDo2LjE2cHg7Ii8+PHBhdGggZD0iTTQxNy43OTYsMTg0LjQ1M2MzLjI4MiwwLjk3IDYuNTgzLC0x
|
||||
LjI5IDguODg5LC0zLjM2MWMyOC4wNTUsLTI1LjE5NCAtMTMuMTk4LC0zMS41MzggLTIwLjY1Niwt
|
||||
MTIuNTU5IiBzdHlsZT0iZmlsbDojNjFlMGVlOyIvPjxwYXRoIGQ9Ik00MTYuNjMyLDE4OC4zOTNj
|
||||
NC42NTcsMS4zNzUgOS41MjYsLTEuMzA3IDEyLjc5NywtNC4yNDVjMTAuMjc5LC05LjIzIDEyLjA0
|
||||
NiwtMTYuODQzIDEwLjQ4OCwtMjEuNzE2Yy0xLjY0NSwtNS4xNSAtNy4xNDIsLTguNTQ0IC0xMy45
|
||||
NzgsLTguOTljLTkuMDExLC0wLjU4NyAtMTkuOTI3LDMuOTA0IC0yMy43MzMsMTMuNTg5Yy0wLjgz
|
||||
LDIuMTEgMC4yMSw0LjQ5NiAyLjMyLDUuMzI1YzIuMTEsMC44MjkgNC40OTYsLTAuMjExIDUuMzI1
|
||||
LC0yLjMyMWMyLjQ2LC02LjI1OSA5LjczLC04Ljc3NSAxNS41NTMsLTguMzk1YzEuODA3LDAuMTE3
|
||||
IDMuNDg5LDAuNTE3IDQuODE3LDEuMjY5YzAuODcxLDAuNDk0IDEuNTg2LDEuMTMgMS44NzEsMi4w
|
||||
MjNjMC4zNzUsMS4xNzIgMC4wNDksMi41ODggLTAuNzk1LDQuMjk4Yy0xLjI2NiwyLjU2MiAtMy42
|
||||
NDksNS40NzcgLTcuMzU2LDguODA2Yy0wLjg2OCwwLjc4IC0xLjkwNywxLjYxOCAtMy4wNTQsMi4x
|
||||
NDdjLTAuNjE2LDAuMjg0IC0xLjI2NSwwLjUyNyAtMS45MjgsMC4zMzFjLTIuMTc0LC0wLjY0MiAt
|
||||
NC40NjEsMC42MDIgLTUuMTAzLDIuNzc2Yy0wLjY0MiwyLjE3NCAwLjYwMiw0LjQ2MSAyLjc3Niw1
|
||||
LjEwM1oiLz48cGF0aCBkPSJNNDM2LjAxNCw0MTkuNjQ1YzMuNjAxLC0xMC45MzQgOS4wMDMsLTMx
|
||||
LjYwOSA1LjY4MiwtNDkuNTYzYy0xNC45NTMsLTgwLjg0NyA3Ljg5NSwtOTguNTQ5IC01LjAzNSwt
|
||||
MTQ3LjU5M2MtNC40OSwtMTcuMDI5IC0xMi4wNDksLTM2LjI1IC0yMy41NTQsLTQ5Ljc5MmMtMjUu
|
||||
Mjg1LC0yOS43NjIgLTcwLjczNiwtNDIuOTEzIC0xMDguNzMsLTMyLjc4NWMtMTUuMjEzLDQuMDU1
|
||||
IC0yOC42NjIsMTEuNzI3IC00MS45MjMsMjAuMDM0Yy00Ni4xMzYsMjguOSAtNTIuMjU5LDgzLjM3
|
||||
NyAtNDIuNzkxLDEyOC4yNDhjNC4xOTEsMTkuODY0IDIzLjY3Myw0OS44OTcgMjIuNTg2LDcwLjE5
|
||||
NGMtMS4zODcsMjUuOTEgLTAuNzY1LDQ0LjQyNiAzLjM3Niw2MC44ODQiIHN0eWxlPSJmaWxsOiM2
|
||||
MWUwZWU7Ii8+PGNsaXBQYXRoIGlkPSJfY2xpcDEiPjxwYXRoIGQ9Ik00MzYuMDE0LDQxOS42NDVj
|
||||
My42MDEsLTEwLjkzNCA5LjAwMywtMzEuNjA5IDUuNjgyLC00OS41NjNjLTE0Ljk1MywtODAuODQ3
|
||||
IDcuODk1LC05OC41NDkgLTUuMDM1LC0xNDcuNTkzYy00LjQ5LC0xNy4wMjkgLTEyLjA0OSwtMzYu
|
||||
MjUgLTIzLjU1NCwtNDkuNzkyYy0yNS4yODUsLTI5Ljc2MiAtNzAuNzM2LC00Mi45MTMgLTEwOC43
|
||||
MywtMzIuNzg1Yy0xNS4yMTMsNC4wNTUgLTI4LjY2MiwxMS43MjcgLTQxLjkyMywyMC4wMzRjLTQ2
|
||||
LjEzNiwyOC45IC01Mi4yNTksODMuMzc3IC00Mi43OTEsMTI4LjI0OGM0LjE5MSwxOS44NjQgMjMu
|
||||
NjczLDQ5Ljg5NyAyMi41ODYsNzAuMTk0Yy0xLjM4NywyNS45MSAtMC43NjUsNDQuNDI2IDMuMzc2
|
||||
LDYwLjg4NCIvPjwvY2xpcFBhdGg+PGcgY2xpcC1wYXRoPSJ1cmwoI19jbGlwMSkiPjxwYXRoIGQ9
|
||||
Ik0zMTEuNzc2LDIzNi44MzVjMCwwIC0zLjA2OCwyNC4xOTkgLTMxLjU5NywyNC44MzhjLTI4LjUz
|
||||
LDAuNjM5IC0zMi41OTEsLTI4LjI0NiAtMzIuNTkxLC0yOC4yNDZsNjQuMTg4LDMuNDA4WiIgc3R5
|
||||
bGU9ImZpbGw6IzRmY2JlMDsiLz48cGF0aCBkPSJNNDMyLjUwOSwyMTYuMDIzYzAsLTAgLTMuMDY3
|
||||
LDI0LjE5OCAtMzEuNTk3LDI0LjgzN2MtMjguNTMsMC42MzkgLTMyLjU5MSwtMjguMjQ2IC0zMi41
|
||||
OTEsLTI4LjI0Nmw2NC4xODgsMy40MDlaIiBzdHlsZT0iZmlsbDojNGZjYmUwOyIvPjxwYXRoIGQ9
|
||||
Ik0yODYuODQ1LDQxOC45NjFsMTI3Ljg4NiwtMGMwLjM4NiwtMS4yNjcgMC42NjYsLTIuNDMyIDAu
|
||||
ODcsLTMuNDcxYzMuODY3LC0xOS43NDMgMy43MjksLTU3LjQ2OCAtMi40MTUsLTc3LjA5NWMtMi4x
|
||||
MzMsLTYuODE2IC01Ljc5MywtMTQuNDkgLTExLjQ3NiwtMTkuODUyYy0xMi40OSwtMTEuNzg1IC02
|
||||
NC4zOTgsLTkuMTgxIC04My41NjEsLTQuODM1Yy03LjY3MywxLjc0MSAtMTQuNDg3LDQuOTIyIC0y
|
||||
MS4yMSw4LjM1OGMtMTQuOTExLDcuNjIgLTIwLjI1Myw1OC43MTIgLTEwLjA5NCw5Ni44OTVaIiBz
|
||||
dHlsZT0iZmlsbDojYjdmOGZmOyIvPjxwYXRoIGQ9Ik0yNDYuNzExLDQxOC45NjFjLTUuMTA4LC0y
|
||||
MC42MTMgLTYuNDksLTQyLjk3NSAtNS41NjIsLTYwLjg1NWMyLjEzNiwtNDEuMTMzIC0zMS4zMTQs
|
||||
LTUwLjQ1MyAtMjUuNDYxLC0xMjEuNTEzYzE0LjUzNCwtNDIuNDA5IDE1LjA4OSwtNDAuMTA3IDI3
|
||||
LjY1MywtNTUuNDQ1YzAsMCAtMTMuMTQ3LDM5Ljk2OCAtMS4xOTMsNzAuNzI4YzExLjk1NCwzMC43
|
||||
NTkgMzIuMTc0LDczLjQxOCAzMi41OSwxMTcuNDkxYzAuMTg2LDE5Ljc3MSAxLjYzNCwzNi4zMSA0
|
||||
LjEzLDQ5LjU5NGwtMzIuMTU3LC0wWiIgc3R5bGU9ImZpbGw6IzRmY2JlMDsiLz48L2c+PHBhdGgg
|
||||
ZD0iTTQzOS45MTUsNDIwLjkzYzMuNzQ4LC0xMS4zOCA5LjI3NiwtMzIuOTA4IDUuODIsLTUxLjU5
|
||||
NWMtOC44NjUsLTQ3LjkzMyAtNC4yNTksLTczLjQwNSAtMS45MzUsLTk2LjU4YzEuNjEzLC0xNi4w
|
||||
OTMgMi4xNTYsLTMxLjEyMiAtMy4xNjcsLTUxLjMxM2MtNC42MzgsLTE3LjU5MSAtMTIuNTExLC0z
|
||||
Ny40MTYgLTI0LjM5NiwtNTEuNDA1Yy0yNi4yNjEsLTMwLjkxMSAtNzMuNDU2LC00NC42MTMgLTEx
|
||||
Mi45MTcsLTM0LjA5NGMtMTUuNjE0LDQuMTYxIC0yOS40MzcsMTEuOTk2IC00My4wNDcsMjAuNTIy
|
||||
Yy00Ny43MzUsMjkuOTAyIC01NC40MjYsODYuMTUgLTQ0LjYyOSwxMzIuNTc3YzIuMTM5LDEwLjEz
|
||||
OCA4LjEzNiwyMi44ODggMTMuNTA0LDM1LjY3NmM0Ljk5NCwxMS45IDkuNTE1LDIzLjgxIDguOTk5
|
||||
LDMzLjQ1Yy0xLjQxNSwyNi40MzIgLTAuNzMsNDUuMzE3IDMuNDk1LDYyLjEwN2wxLjAwMiwzLjk4
|
||||
M2w3Ljk2NywtMi4wMDVsLTEuMDAyLC0zLjk4M2MtNC4wNTgsLTE2LjEyNyAtNC42MTgsLTM0LjI3
|
||||
NCAtMy4yNTgsLTU5LjY2M2MwLjQ2LC04LjYwOSAtMi40NiwtMTguODk1IC02LjU0NSwtMjkuNDU3
|
||||
Yy01LjY5NCwtMTQuNzI0IC0xMy42NDYsLTMwLjA1OSAtMTYuMTI0LC00MS44MDRjLTkuMTQsLTQz
|
||||
LjMxNSAtMy41ODQsLTk2LjAyMiA0MC45NTIsLTEyMy45MTljMTIuOTEyLC04LjA4OSAyNS45ODks
|
||||
LTE1LjU5OCA0MC44MDEsLTE5LjU0N2MzNi41MjYsLTkuNzM1IDgwLjIzNCwyLjg2NSAxMDQuNTQx
|
||||
LDMxLjQ3NmMxMS4xMjYsMTMuMDk2IDE4LjM3MiwzMS43MTMgMjIuNzEzLDQ4LjE4YzYuMzEsMjMu
|
||||
OTMyIDMuODIsNDAuMjE5IDEuNjQ3LDYwLjM4M2MtMi4yNTcsMjAuOTQ5IC00LjI1OSw0NS45Mjcg
|
||||
My4zMjEsODYuOTFjMy4xODYsMTcuMjIyIC0yLjA5LDM3LjA0MyAtNS41NDQsNDcuNTMxbC0xLjI4
|
||||
NSwzLjkwMmw3LjgwMywyLjU2OWwxLjI4NCwtMy45MDFaIi8+PHBhdGggZD0iTTQyNC41MzksMjQy
|
||||
LjI0N2MtMS44NjgsMC4xMzQgLTMuMjc2LDEuNzU4IC0zLjE0MiwzLjYyNmMwLjEzMywxLjg2NyAx
|
||||
Ljc1NywzLjI3NSAzLjYyNSwzLjE0MWMxLjg2NywtMC4xMzMgMy4yNzUsLTEuNzU3IDMuMTQyLC0z
|
||||
LjYyNWMtMC4xMzQsLTEuODY3IC0xLjc1OCwtMy4yNzUgLTMuNjI1LC0zLjE0MloiIHN0eWxlPSJm
|
||||
aWxsOiNiN2Y4ZmY7Ii8+PHBhdGggZD0iTTI1Ni45OTcsMjYwLjM5MWMtMS44NjgsMC4xMzMgLTMu
|
||||
Mjc1LDEuNzU4IC0zLjE0MiwzLjYyNWMwLjEzMywxLjg2OCAxLjc1OCwzLjI3NSAzLjYyNSwzLjE0
|
||||
MmMxLjg2OCwtMC4xMzMgMy4yNzUsLTEuNzU4IDMuMTQyLC0zLjYyNWMtMC4xMzMsLTEuODY4IC0x
|
||||
Ljc1OCwtMy4yNzUgLTMuNjI1LC0zLjE0MloiIHN0eWxlPSJmaWxsOiNiN2Y4ZmY7Ii8+PGc+PHBh
|
||||
dGggZD0iTTM2NC4wOSwyNjkuMjI5Yy0xLjE4Niw0Ljk2NyAtMS40NDMsMTQuMzA2IC00LjMxNSwx
|
||||
OC40Yy0xLjM5MywxLjk4NSAtNC40OTUsMi4wODYgLTYuNTE2LDIuMTUzYy0xMS42MTIsMC4zODMg
|
||||
LTExLjQ0NiwtMTEuNTEzIC0xMy4xODEsLTIxLjg1NSIgc3R5bGU9ImZpbGw6I2VkZWRlZDsiLz48
|
||||
cGF0aCBkPSJNMzYxLjA5NCwyNjguNTEzYy0wLjcxOSwzLjAwOSAtMS4xMDYsNy42MDcgLTEuOTIy
|
||||
LDExLjY5OGMtMC40NDUsMi4yMjggLTAuOTcxLDQuMjk4IC0xLjkxOSw1LjY0OWMtMC4yNjMsMC4z
|
||||
NzYgLTAuNzQ5LDAuNDU5IC0xLjIwMywwLjU2NmMtMC45OTUsMC4yMzMgLTIuMDU2LDAuMjUgLTIu
|
||||
ODkzLDAuMjc3Yy0yLjA1NSwwLjA2OCAtMy42MSwtMC4zMjYgLTQuNzc0LC0xLjE5MmMtMS4xOTMs
|
||||
LTAuODg3IC0xLjk1NCwtMi4yMTIgLTIuNTQ0LC0zLjc0M2MtMS41NjEsLTQuMDQ3IC0xLjg5LC05
|
||||
LjM5IC0yLjcyMywtMTQuMzUxYy0wLjI4MSwtMS42NzcgLTEuODcxLC0yLjgxIC0zLjU0OCwtMi41
|
||||
MjhjLTEuNjc3LDAuMjgxIC0yLjgwOSwxLjg3MSAtMi41MjgsMy41NDhjMC45MDMsNS4zODEgMS4z
|
||||
NTgsMTEuMTU4IDMuMDUxLDE1LjU0OGMxLjAzNiwyLjY4OCAyLjUyLDQuOTEyIDQuNjE1LDYuNDdj
|
||||
Mi4xMjUsMS41ODEgNC45MDMsMi41MyA4LjY1NCwyLjQwNmMxLjQ2OCwtMC4wNDggMy40LC0wLjE1
|
||||
MiA1LjA3OSwtMC43MTRjMS41NiwtMC41MjIgMi45MTksLTEuNDEgMy44NTgsLTIuNzQ5YzEuMzUs
|
||||
LTEuOTI0IDIuMjg0LC00LjgwOCAyLjkxNywtNy45ODFjMC44LC00LjAxMSAxLjE2OCwtOC41MjIg
|
||||
MS44NzMsLTExLjQ3M2MwLjM5NSwtMS42NTMgLTAuNjI3LC0zLjMxNyAtMi4yODEsLTMuNzEyYy0x
|
||||
LjY1NCwtMC4zOTUgLTMuMzE3LDAuNjI3IC0zLjcxMiwyLjI4MVoiLz48cGF0aCBkPSJNMzQ5LjAw
|
||||
MSwyMzQuMDVjLTkuMzMyLDEuMjQ2IC0xNi4yNjUsNy4wNzUgLTE1LjQ3MywxMy4wMDhjMC43OTIs
|
||||
NS45MzMgOS4wMTIsOS43MzggMTguMzQ0LDguNDkyYzkuMzMzLC0xLjI0NiAxNi4yNjYsLTcuMDc1
|
||||
IDE1LjQ3NCwtMTMuMDA4Yy0wLjc5MiwtNS45MzMgLTkuMDEyLC05LjczOCAtMTguMzQ1LC04LjQ5
|
||||
MloiLz48cGF0aCBkPSJNMzQ4LjkwMSwyNTIuNTVjMC40Niw1LjIwNCAxLjY5NCwxMC4xNDIgMi40
|
||||
NTMsMTUuMjc3YzAuMjQ4LDEuNjgyIDEuODE1LDIuODQ2IDMuNDk3LDIuNTk4YzEuNjgyLC0wLjI0
|
||||
OCAyLjg0NiwtMS44MTYgMi41OTgsLTMuNDk4Yy0wLjc0MSwtNS4wMTUgLTEuOTYxLC05LjgzNyAt
|
||||
Mi40MTEsLTE0LjkyYy0wLjE1LC0xLjY5NCAtMS42NDcsLTIuOTQ3IC0zLjM0LC0yLjc5N2MtMS42
|
||||
OTQsMC4xNSAtMi45NDcsMS42NDcgLTIuNzk3LDMuMzRaIi8+PHBhdGggZD0iTTM3NC4yNiwyNTku
|
||||
ODE0Yy04LjAyMiw1LjM1IC0xNS45MDEsNy44MjUgLTIzLjU5NCw3LjI3NGMtNy42OTQsLTAuNTUx
|
||||
IC0xNS4xNDgsLTQuMTI0IC0yMi4zNDIsLTEwLjU4OWMtMS4yNjUsLTEuMTM2IC0zLjIxNCwtMS4w
|
||||
MzIgLTQuMzUxLDAuMjMzYy0xLjEzNiwxLjI2NCAtMS4wMzIsMy4yMTQgMC4yMzMsNC4zNWM4LjM1
|
||||
Niw3LjUwOCAxNy4wODQsMTEuNTExIDI2LjAyLDEyLjE1MWM4LjkzNiwwLjY0IDE4LjEzNSwtMi4w
|
||||
NzggMjcuNDUzLC04LjI5NGMxLjQxNCwtMC45NDMgMS43OTcsLTIuODU4IDAuODUzLC00LjI3MmMt
|
||||
MC45NDMsLTEuNDE1IC0yLjg1OCwtMS43OTcgLTQuMjcyLC0wLjg1M1oiLz48cGF0aCBkPSJNMzc4
|
||||
LjYwNSwxNTIuMTU2Yy0zLjY0NywtMC44MjYgLTYuOTU2LDAuMDM4IC03LjM4NSwxLjkyOWMtMC40
|
||||
MjgsMS44OTEgMi4xODUsNC4wOTcgNS44MzIsNC45MjRjMy42NDcsMC44MjcgNi45NTYsLTAuMDM3
|
||||
IDcuMzg1LC0xLjkyOGMwLjQyOCwtMS44OTEgLTIuMTg1LC00LjA5OCAtNS44MzIsLTQuOTI1WiIg
|
||||
c3R5bGU9ImZpbGw6I2I3ZjhmZjsiLz48cGF0aCBkPSJNMjQ5LjUsMTc3LjI3Yy0yLjE5NSwzLjAy
|
||||
OCAtMi43MDIsNi40MSAtMS4xMzIsNy41NDhjMS41NywxLjEzOCA0LjYyNiwtMC4zOTYgNi44MjEs
|
||||
LTMuNDI0YzIuMTk1LC0zLjAyOCAyLjcwMiwtNi40MSAxLjEzMiwtNy41NDhjLTEuNTcsLTEuMTM4
|
||||
IC00LjYyNiwwLjM5NiAtNi44MjEsMy40MjRaIiBzdHlsZT0iZmlsbDojYjdmOGZmOyIvPjxwYXRo
|
||||
IGQ9Ik00MDMuNjQ4LDE2Mi44MjVjLTE5LjIxMywtMS4zNSAtMzUuOTA3LDEzLjE1MSAtMzcuMjU3
|
||||
LDMyLjM2NGMtMS4zNSwxOS4yMTIgMTMuMTUyLDM1LjkwNiAzMi4zNjQsMzcuMjU2YzE5LjIxMywx
|
||||
LjM1MSAzNS45MDcsLTEzLjE1MSAzNy4yNTcsLTMyLjM2NGMxLjM1LC0xOS4yMTIgLTEzLjE1Miwt
|
||||
MzUuOTA2IC0zMi4zNjQsLTM3LjI1NloiIHN0eWxlPSJmaWxsOiNmZmY7c3Ryb2tlOiMwMDA7c3Ry
|
||||
b2tlLXdpZHRoOjYuMTZweDsiLz48cGF0aCBkPSJNMjgxLjY5NiwxODEuNzhjLTE5LjIxMiwtMS4z
|
||||
NSAtMzUuOTA2LDEzLjE1MiAtMzcuMjU3LDMyLjM2NGMtMS4zNSwxOS4yMTIgMTMuMTUyLDM1Ljkw
|
||||
NyAzMi4zNjUsMzcuMjU3YzE5LjIxMiwxLjM1IDM1LjkwNiwtMTMuMTUyIDM3LjI1NiwtMzIuMzY0
|
||||
YzEuMzUsLTE5LjIxMyAtMTMuMTUyLC0zNS45MDcgLTMyLjM2NCwtMzcuMjU3WiIgc3R5bGU9ImZp
|
||||
bGw6I2ZmZjtzdHJva2U6IzAwMDtzdHJva2Utd2lkdGg6Ni4xNnB4OyIvPjxwYXRoIGQ9Ik0zOTgu
|
||||
NDk5LDE5Mi44ODFjLTYuODI0LC0wLjQ3OSAtMTIuNzU0LDQuNjcyIC0xMy4yMzMsMTEuNDk2Yy0w
|
||||
LjQ4LDYuODI0IDQuNjcxLDEyLjc1MyAxMS40OTUsMTMuMjMzYzYuODI0LDAuNDc5IDEyLjc1NCwt
|
||||
NC42NzIgMTMuMjMzLC0xMS40OTZjMC40OCwtNi44MjQgLTQuNjcxLC0xMi43NTMgLTExLjQ5NSwt
|
||||
MTMuMjMzWiIvPjxwYXRoIGQ9Ik0yNzguMTQyLDIxMC4yMjdjLTYuODI0LC0wLjQ3OSAtMTIuNzU0
|
||||
LDQuNjcxIC0xMy4yMzMsMTEuNDk2Yy0wLjQ4LDYuODI0IDQuNjcxLDEyLjc1MyAxMS40OTUsMTMu
|
||||
MjMzYzYuODI0LDAuNDc5IDEyLjc1NCwtNC42NzIgMTMuMjMzLC0xMS40OTZjMC40OCwtNi44MjQg
|
||||
LTQuNjcxLC0xMi43NTMgLTExLjQ5NSwtMTMuMjMzWiIvPjxwYXRoIGQ9Ik0zOTkuODkyLDE5Ny45
|
||||
NjFjLTEuOTE2LC0wLjEzNSAtMy41ODEsMS4zMTEgLTMuNzE2LDMuMjI3Yy0wLjEzNCwxLjkxNiAx
|
||||
LjMxMiwzLjU4MSAzLjIyOCwzLjcxNmMxLjkxNiwwLjEzNCAzLjU4MSwtMS4zMTIgMy43MTUsLTMu
|
||||
MjI4YzAuMTM1LC0xLjkxNiAtMS4zMTEsLTMuNTgxIC0zLjIyNywtMy43MTVaIiBzdHlsZT0iZmls
|
||||
bDojZmZmOyIvPjxwYXRoIGQ9Ik0yNzkuNTM1LDIxNS4zMDZjLTEuOTE2LC0wLjEzNCAtMy41ODEs
|
||||
MS4zMTIgLTMuNzE1LDMuMjI4Yy0wLjEzNSwxLjkxNiAxLjMxMSwzLjU4MSAzLjIyNywzLjcxNmMx
|
||||
LjkxNiwwLjEzNCAzLjU4MSwtMS4zMTIgMy43MTYsLTMuMjI4YzAuMTM0LC0xLjkxNiAtMS4zMTIs
|
||||
LTMuNTgxIC0zLjIyOCwtMy43MTZaIiBzdHlsZT0iZmlsbDojZmZmOyIvPjwvZz48L2c+PHBhdGgg
|
||||
ZD0iTTUxMi4yOTEsMzMzLjcyNmMwLC0wIC0yNy40OTYsMTMuNDAyIC00My4yNDUsMy43OTJjLTE1
|
||||
Ljc1LC05LjYxIC0xMC44LC0yNC43ODggMy4xMDIsLTIyLjUyYzEzLjkwMywyLjI2OCAyNy4xNjgs
|
||||
My4xOTUgMzUuOTQzLC0zLjkzN2M4Ljc3NiwtNy4xMzIgMjEuNjMzLDE0LjM3NiA0LjIsMjIuNjY1
|
||||
WiIgc3R5bGU9ImZpbGw6I2Q2YzI5NTtzdHJva2U6IzAwMDtzdHJva2Utd2lkdGg6Ni4xNnB4OyIv
|
||||
PjwvZz48Zz48cGF0aCBkPSJNNDQxLjI2OCwyOTEuMzQ5Yy0wLDAgMjguMDgsNDcuNDg4IC0xMjIu
|
||||
NTkxLDExNC4wNGMtMTEuNjI5LC0yLjAxNyAtMTQuMzMsLTguNTE1IC0xNC4zMywtOC41MTVjMCwt
|
||||
MCA0My42MTcsLTEyLjU4NiA4My44MTIsLTQ0Ljg3NmM0My41OTcsLTM1LjAyNCA1My4xMDksLTYw
|
||||
LjY0OSA1My4xMDksLTYwLjY0OVoiIHN0eWxlPSJmaWxsOiM3YjQ3MjI7c3Ryb2tlOiMwMDA7c3Ry
|
||||
b2tlLXdpZHRoOjYuNjhweDsiLz48cGF0aCBkPSJNMjMzLjk1NywzNzIuMTM4YzMuNTYsLTUuNTA2
|
||||
IDEwLjcyOSwtNy4zOTIgMTYuNTM4LC00LjM1MWMxNy43NjcsOS4yMzUgNTMuNjE0LDI4LjAzMiA2
|
||||
Ny4xNTQsMzYuMzc2YzUuOTcxLDMuNjggLTYuNDc3LDE2LjkxMyAtMTkuMjgzLDM3LjkyN2MtMy4z
|
||||
NjgsNS41NjcgLTEwLjQzNiw3LjYyOCAtMTYuMjY5LDQuNzQ2Yy0yMS42NTYsLTEwLjcxNiAtNDEu
|
||||
MDQ3LC0yMC45NTggLTU0LjIzNCwtMjguMDczYy00LjU5MiwtMi41MDEgLTcuOTM0LC02LjgwMiAt
|
||||
OS4yMjMsLTExLjg2OWMtMS4yODgsLTUuMDY4IC0wLjQwNywtMTAuNDQzIDIuNDMzLC0xNC44MzNj
|
||||
NC4zMzUsLTYuNzA4IDkuMTYyLC0xNC4xNyAxMi44ODQsLTE5LjkyM1oiIHN0eWxlPSJmaWxsOiM3
|
||||
YjQ3MjI7c3Ryb2tlOiMwMDA7c3Ryb2tlLXdpZHRoOjQuOTVweDsiLz48cGF0aCBkPSJNMjM5Ljg1
|
||||
LDM2MS4xNzFsLTI1Ljg0OCwyMy4yOTZsNzkuMTcsNDUuNjA1bDI1LjE5NiwtMjQuMjk1bC03OC41
|
||||
MTgsLTQ0LjYwNloiIHN0eWxlPSJmaWxsOiNhZDZjNGE7c3Ryb2tlOiMwMDA7c3Ryb2tlLXdpZHRo
|
||||
OjQuOTVweDsiLz48cGF0aCBkPSJNMjM4LjA4MSwzNzcuNzA0YzIuNTI2LDAuNTMgNC4xNDcsMy4w
|
||||
MTEgMy42MTgsNS41MzdjLTAuNTMsMi41MjYgLTMuMDExLDQuMTQ3IC01LjUzOCwzLjYxOGMtMi41
|
||||
MjYsLTAuNTMgLTQuMTQ3LC0zLjAxMSAtMy42MTcsLTUuNTM4YzAuNTMsLTIuNTI2IDMuMDExLC00
|
||||
LjE0NyA1LjUzNywtMy42MTdaIiBzdHlsZT0iZmlsbDojZmZiMjVjO3N0cm9rZTojMDAwO3N0cm9r
|
||||
ZS13aWR0aDo0Ljk1cHg7Ii8+PHBhdGggZD0iTTI5MC4zMyw0MDcuOWMyLjUyNywwLjUzIDQuMTQ3
|
||||
LDMuMDExIDMuNjE4LDUuNTM3Yy0wLjUzLDIuNTI3IC0zLjAxMSw0LjE0NyAtNS41MzcsMy42MThj
|
||||
LTIuNTI3LC0wLjUzIC00LjE0OCwtMy4wMTEgLTMuNjE4LC01LjUzN2MwLjUzLC0yLjUyNyAzLjAx
|
||||
MSwtNC4xNDggNS41MzcsLTMuNjE4WiIgc3R5bGU9ImZpbGw6I2ZmYjI1YztzdHJva2U6IzAwMDtz
|
||||
dHJva2Utd2lkdGg6NC45NXB4OyIvPjwvZz48Zz48cGF0aCBkPSJNMzA1LjMyMywzMzYuMjM1YzAs
|
||||
MCAyNC4yMDQsMTEyLjU3OCAzNS4zNTcsMTE0LjM1NWMxMS4xNTMsMS43NzYgMTgyLjk5OCwtNTcu
|
||||
MTYyIDE4Mi45OTgsLTU3LjE2MmMwLC0wIC0yNC4yNywtMTA1Ljc3MyAtMzQuNDU2LC0xMTQuNjAy
|
||||
Yy0xMC4xODYsLTguODMgLTE4My44OTksNTcuNDA5IC0xODMuODk5LDU3LjQwOVoiIHN0eWxlPSJm
|
||||
aWxsOiNmZmYyZWI7c3Ryb2tlOiMwMDA7c3Ryb2tlLXdpZHRoOjQuNjhweDsiLz48cGF0aCBkPSJN
|
||||
NDEzLjU2MywzNjcuNjU2bC0xMDguMjQsLTMxLjQyMWwxMDguMjQsMzEuNDIxWiIgc3R5bGU9ImZp
|
||||
bGw6bm9uZTtzdHJva2U6IzAwMDtzdHJva2Utd2lkdGg6NS40NnB4OyIvPjxwYXRoIGQ9Ik00ODgu
|
||||
NTY2LDI3OS4xNTZsLTc1LjAwMyw4OC41IiBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMDAwO3N0
|
||||
cm9rZS13aWR0aDo1LjQ2cHg7Ii8+PHBhdGggZD0iTTMzOS4xNjcsNDQ5LjEyN2w2Mi40MDEsLTgz
|
||||
LjUwNSIgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2U6IzAwMDtzdHJva2Utd2lkdGg6NS40NnB4OyIv
|
||||
PjxwYXRoIGQ9Ik00MjMuMjcsMzU1LjgzNWw5OS4xOTQsMzcuMDAxIiBzdHlsZT0iZmlsbDpub25l
|
||||
O3N0cm9rZTojMDAwO3N0cm9rZS13aWR0aDo1LjQ2cHg7Ii8+PC9nPjxwYXRoIGQ9Ik0zMDQuNDE2
|
||||
LDM3NC43ODVjLTAsMCAtMzAuNDYsLTIuOCAtMzguOTQzLC0xOS4xODVjLTguNDgyLC0xNi4zODQg
|
||||
My42MjIsLTI2Ljc5NCAxNC4zMzIsLTE3LjY0NmMxMC43MTEsOS4xNDkgMjEuNTcyLDE2LjgyMSAz
|
||||
Mi43NzQsMTUuMjc0YzExLjIwMSwtMS41NDcgMTEuMDQsMjMuNTExIC04LjE2MywyMS41NTdaIiBz
|
||||
dHlsZT0iZmlsbDojZDZjMjk1O3N0cm9rZTojMDAwO3N0cm9rZS13aWR0aDo2LjE2cHg7Ii8+PHBh
|
||||
dGggZD0iTTEwOS45NTEsMzExLjcxMmw2OC4wNzYsLTAiIHN0eWxlPSJmaWxsOm5vbmU7c3Ryb2tl
|
||||
OiMwMDA7c3Ryb2tlLXdpZHRoOjQuOTVweDsiLz48cGF0aCBkPSJNMTM5LjMwNSwzNTEuNjgzbDY4
|
||||
LjA3NiwwIiBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMDAwO3N0cm9rZS13aWR0aDo0Ljk1cHg7
|
||||
Ii8+PHBhdGggZD0iTTEzOS42MTcsMzMxLjY5OGwzOC4wOTgsLTAiIHN0eWxlPSJmaWxsOm5vbmU7
|
||||
c3Ryb2tlOiMwMDA7c3Ryb2tlLXdpZHRoOjQuOTVweDsiLz48cGF0aCBkPSJNODUuMjAxLDIxMS4y
|
||||
NTNjLTAsLTIuMjEzIC0xLjc5NywtNC4wMSAtNC4wMSwtNC4wMWwtNTMuNDg3LC0wYy0yLjIxNCwt
|
||||
MCAtNC4wMTEsMS43OTcgLTQuMDExLDQuMDFsMCw1OC44OTRjMCwyLjIxMyAxLjc5Nyw0LjAxIDQu
|
||||
MDExLDQuMDFsNTMuNDg3LDBjMi4yMTMsMCA0LjAxLC0xLjc5NyA0LjAxLC00LjAxbC0wLC01OC44
|
||||
OTRaIiBzdHlsZT0iZmlsbDojZWVhYzAxO3N0cm9rZTojMDAwO3N0cm9rZS13aWR0aDozLjQzcHg7
|
||||
Ii8+PHBhdGggZD0iTTU2NC42NDMsNDI2LjU5MWMwLC0yLjIxMyAtMS43OTYsLTQuMDEgLTQuMDEs
|
||||
LTQuMDFsLTUzLjQ4NywtMGMtMi4yMTMsLTAgLTQuMDEsMS43OTcgLTQuMDEsNC4wMWwwLDU4Ljg5
|
||||
NGMwLDIuMjEzIDEuNzk3LDQuMDEgNC4wMSw0LjAxbDUzLjQ4NywwYzIuMjE0LDAgNC4wMSwtMS43
|
||||
OTcgNC4wMSwtNC4wMWwwLC01OC44OTRaIiBzdHlsZT0iZmlsbDojZWVhYzAxO3N0cm9rZTojMDAw
|
||||
O3N0cm9rZS13aWR0aDozLjQzcHg7Ii8+PGNpcmNsZSBjeD0iNTE3LjAxNCIgY3k9IjEwOC43NzEi
|
||||
IHI9IjQuMzUyIiBzdHlsZT0iZmlsbDojZDNkMmIzOyIvPjxjaXJjbGUgY3g9IjUzMS4zMiIgY3k9
|
||||
IjEwOC43NzEiIHI9IjQuMzUyIiBzdHlsZT0iZmlsbDojZjRiMzAzOyIvPjxjaXJjbGUgY3g9IjU0
|
||||
NC43OTIiIGN5PSIxMDguNzcxIiByPSI0LjM1MiIgc3R5bGU9ImZpbGw6I2Y5NjEzZDsiLz48cGF0
|
||||
aCBkPSJNMTA1LjI1Nyw0ODQuNDNjLTAsMCAyMC4zNzQsLTI1LjIyNSA0MS43MTksLTcuNzYxYzIx
|
||||
LjM0NCwxNy40NjMgLTEyLjYxMyw1MC40NSAtNDAuNzQ5LDQyLjY4OSIgc3R5bGU9ImZpbGw6bm9u
|
||||
ZTtzdHJva2U6IzAwMDtzdHJva2Utd2lkdGg6OS45cHg7Ii8+PHBhdGggZD0iTTExMy4wMTksNDYw
|
||||
LjE0MWMtMCwtNC4yNjUgLTMuNDYzLC03LjcyNyAtNy43MjgsLTcuNzI3bC03OC42NTUsLTBjLTQu
|
||||
MjY1LC0wIC03LjcyNywzLjQ2MiAtNy43MjcsNy43MjdsLTAsNzguNjU1Yy0wLDQuMjY1IDMuNDYy
|
||||
LDcuNzI4IDcuNzI3LDcuNzI4bDc4LjY1NSwtMGM0LjI2NSwtMCA3LjcyOCwtMy40NjMgNy43Mjgs
|
||||
LTcuNzI4bC0wLC03OC42NTVaIiBzdHlsZT0iZmlsbDojNjFlMGVlO3N0cm9rZTojMDAwO3N0cm9r
|
||||
ZS13aWR0aDo0LjU1cHg7c3Ryb2tlLWxpbmVjYXA6c3F1YXJlOyIvPjxwYXRoIGQ9Ik03Ny4yNzcs
|
||||
Mzk1LjQwMmMtMi4yMzQsOC43NTUgLTYuNDk2LDE1LjM2NiAtMTEuNjQ5LDE3LjcxNGMxLjk5NSwz
|
||||
LjA1NyAzLjE2Nyw2Ljc4MSAzLjE2NywxMC43OTdjMCwxMC4zODcgLTcuODQsMTguODIgLTE3LjQ5
|
||||
OCwxOC44MmMtOS42NTcsMCAtMTcuNDk4LC04LjQzMyAtMTcuNDk4LC0xOC44MmMwLC05LjczMyA2
|
||||
Ljg4NCwtMTcuNzUxIDE1LjY5NywtMTguNzIyYy0zLjM5OSwtNS45NiAtNS41MjQsLTE0LjQyMyAt
|
||||
NS41MjQsLTIzLjgwNWMwLC0xOC4wMjQgNy44NDEsLTMyLjY1NyAxNy40OTksLTMyLjY1N2M2Ljc5
|
||||
MiwtMCAxMi42ODUsNy4yMzggMTUuNTg0LDE3LjgwMmMxLjYyOSwtMC41OTIgMy4zODcsLTAuOTE1
|
||||
IDUuMjIsLTAuOTE1YzguNDUsMCAxNS4zMTEsNi44NjEgMTUuMzExLDE1LjMxMWMtMCw4LjQ1IC02
|
||||
Ljg2MSwxNS4zMTEgLTE1LjMxMSwxNS4zMTFjLTEuNzUsMCAtMy40MzIsLTAuMjk0IC00Ljk5OCwt
|
||||
MC44MzZaIiBzdHlsZT0iZmlsbDojZmZmO2ZpbGwtb3BhY2l0eTowLjU7Ii8+PHBhdGggZD0iTTMz
|
||||
LjU4MywyMzMuOTNjMi41MjksLTAuMzM3IDQuNTEsLTEuMTc4IDYuNzE2LC0yLjQwM2MwLjk1Nywt
|
||||
MC41MzIgMS45NjksLTAuOTA0IDIuODk4LC0xLjQ4NWMwLjc4OSwtMC40OTMgMS4zNDMsLTAuOTc2
|
||||
IDEuMzQzLC0xLjk3OWMwLC0wLjIzNiAwLjIyNCwtMC42MzIgMCwtMC43MDdjLTAuOTUyLC0wLjMx
|
||||
NyAtMi4zNjQsMC40NjUgLTMuMDQsMC45OWMtMC42MjUsMC40ODYgLTEuNDk0LDAuNTc5IC0yLjEy
|
||||
LDEuMDZjLTEuMDM2LDAuNzk2IC0xLjY4MiwyLjQ2MyAtMS43NjcsMy43NDZjLTAuMDYsMC44OTYg
|
||||
LTAuMzU3LDIuODQgMC41NjUsMy4zOTNjMC4zNTcsMC4yMTQgMC45MjksMC4xNDIgMS4zNDMsMC4x
|
||||
NDJjMS4wOCwtMCAyLjEwNCwwLjAxMiAzLjE4MSwtMC4xNDJjMi4wODIsLTAuMjk3IDQuMTY1LC0x
|
||||
LjQ1NSA2LjAwOSwtMi40MDNjMi4wMTQsLTEuMDM2IDQuMDEyLC0xLjkyMSA1LjU4NCwtMy42MDVj
|
||||
MC42NDIsLTAuNjg4IDEuMDEsLTEuMjg2IDEuMjcyLC0yLjE5MWMwLjA1OSwtMC4yMDMgLTAuMDYs
|
||||
LTAuNzE4IDAuMDcxLC0wLjg0OWMwLjEwOCwtMC4xMDggMC4wOTksMC4yMjYgMC4wNzEsMC4yODNj
|
||||
LTAuMTg1LDAuMzY3IC0wLjQ2MiwwLjc0IC0wLjYzNywxLjEzMWMtMC4zMjQsMC43MjkgLTAuMzUz
|
||||
LDEuNzU5IC0wLjM1MywyLjU0NWMwLDAuMTMyIC0wLjA3LDAuNTcxIDAsMC42MzZjMC4wNTUsMC4w
|
||||
NTEgMC4zNjMsMC4xOTMgMC40MjQsMC4yMTJjMi4wMDIsMC42MzMgNC4zNDIsLTAuNTMyIDYuMDA5
|
||||
LC0xLjQ4NGMwLjY0MSwtMC4zNjcgMS4zNjMsLTAuODUgMi4wNiwtMS4xMTJjMC4wMjYsLTAuMDEg
|
||||
MC42NzgsLTAuMTk3IDAuNjk2LC0wLjE2MWMwLjI4MiwwLjU2MyAwLjExMSwxLjUxMSAwLjIxMiwy
|
||||
LjEyMWMwLjI4NSwxLjcwNyAyLjM2NywxLjYxNSAzLjY3NiwxLjQ4NGMwLjIwNywtMC4wMiAwLjM2
|
||||
MywtMC4yMTIgMC41NjYsLTAuMjEyIiBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMDAwO3N0cm9r
|
||||
ZS13aWR0aDoyLjc1cHg7Ii8+PHBhdGggZD0iTTM2LjQ4MiwyNDIuMzQyYzEuMzQ2LC0wIDIuNjgy
|
||||
LC0wLjE1NCA0LjAyOSwtMC4yMTJjNC4zMzIsLTAuMTg5IDguNjcxLC0wLjE5NiAxMy4wMDYsLTAu
|
||||
MjgzYzYuNTI1LC0wLjEzMSAxMy4wNTYsMC4wNzEgMTkuNTgxLDAuMDcxIiBzdHlsZT0iZmlsbDpu
|
||||
b25lO3N0cm9rZTojMDAwO3N0cm9rZS13aWR0aDoyLjc1cHg7Ii8+PHBhdGggZD0iTTUxNi4wOSw0
|
||||
NDIuMDMxYy0wLDAuOTM2IC0wLjUzMiwxLjg5MiAtMC4xMjYsMi44MzljMC41MjYsMS4yMjggMi4z
|
||||
NjMsMS4yNzUgMy40NjksMC45NDZjMC42MjgsLTAuMTg3IDEuMzE2LC0xLjE3NiAxLjcwMywtMS42
|
||||
NGMxLjAzOCwtMS4yNDcgMi40MzUsLTIuMzc1IDIuNzEyLC00LjAzN2MwLjA0MSwtMC4yNDggMC4x
|
||||
OTQsLTAuOTM2IDAuMDYzLC0xLjE5OGMtMC43NjMsLTEuNTI1IC0zLjA1OSwtMS40NzggLTQuMTY4
|
||||
LC0wLjM2OWMtMC4xNjQsMC4xNjQgLTAuNDQ2LDAuMjI3IC0wLjU2MywwLjQzMmMtMC45OTQsMS43
|
||||
NDUgLTAuNDM5LDQuMzQxIDAuNDQyLDUuOTkyYzAuMzA0LDAuNTY5IDAuODI5LDEuMDQ4IDEuMzI0
|
||||
LDEuNDUxYzAuMzM5LDAuMjc1IDAuNzc1LDAuNzE2IDEuMTk5LDAuOTI4YzMuMjYyLDEuNjMxIDcu
|
||||
Njg5LC0wLjEzMyAxMC41OTYsLTEuNzQ4YzEuOTUyLC0xLjA4NSA0LjEsLTIuMTUyIDQuMSwtNC42
|
||||
NjhjLTAsLTAuMzI1IDAuMDM3LC0wLjY5NCAtMC4wNjMsLTEuMDA5Yy0wLjM2MSwtMS4xMzMgLTIu
|
||||
MzA3LC0xLjA0MyAtMy4xNTQsLTAuNjk0Yy0xLjE3OSwwLjQ4NiAtMi4zNzMsMS4zOTUgLTMuMDks
|
||||
Mi40NmMtMS4xNzQsMS43NDIgLTEuMjA3LDQuMjgzIDAuNTY3LDUuNjE0YzEuMjU2LDAuOTQxIDIu
|
||||
OTU4LDAuOTIzIDQuNDQ3LDAuNzY2YzIuMjQ3LC0wLjIzNyA0LjI1MywtMS4zMTMgNi4wMjMsLTIu
|
||||
NjU5YzAuODc1LC0wLjY2NCAyLjA5NCwtMS4zMTYgMi42NDksLTIuMzMzYzAuMTczLC0wLjMxNiAw
|
||||
LjIxNSwtMC43NDUgMC4zNzksLTEuMDczYzAuMDcxLC0wLjE0MyAtMC4xNTgsMC4yODUgLTAuMTg5
|
||||
LDAuNDQyYy0wLjA1NiwwLjI3NiAtMC4wOTQsMC44NzggLTAsMS4xMzVjMC4zNzYsMS4wMjggMS4y
|
||||
MzMsMS41MDkgMi4yNywxLjcwM2MxLjM0NiwwLjI1MiAzLjAwOSwtMC4wMSA0LjM1MiwtMC4xODlj
|
||||
MC45MzksLTAuMTI1IDEuODkyLC0wLjUxNyAxLjg5MiwwLjc1NyIgc3R5bGU9ImZpbGw6bm9uZTtz
|
||||
dHJva2U6IzAwMDtzdHJva2Utd2lkdGg6Mi40NXB4OyIvPjxwYXRoIGQ9Ik01MTQuMjU5LDQ2MC4z
|
||||
NTljMC4yNjYsMC4xMzQgMC4wOCwxLjM1NCAwLjU2NywxLjM4OGMwLjQ0MiwwLjAzMSAwLjg4NSww
|
||||
LjA0IDEuMzI4LDAuMDVjMC41MTcsMC4wMTIgMS4xMDQsMC4wOTggMS42MzMsLTAuMDM0YzEuNjg4
|
||||
LC0wLjQyMiAzLjc5NiwtMS41OTggNC40ODIsLTMuMjk2YzAuMTg4LC0wLjQ2NyAwLjI0NiwtMS4w
|
||||
OTQgMC4zMTUsLTEuNTc3YzAuMDc1LC0wLjUyNCAwLjM3OCwtMS43MTggLTAuMDYzLC0yLjE0NGMt
|
||||
MC4wODgsLTAuMDg1IC0wLjYwNCwtMC4wNjMgLTAuNjk0LC0wLjA2M2MtMC42OTUsLTAgLTEuNzk2
|
||||
LDAuMzkyIC0yLjIwNywxLjAwOWMtMC4yNzMsMC40MDkgLTAuMTg5LDAuODUxIC0wLjE4OSwxLjMy
|
||||
NGMtMCwwLjYyIC0wLjAzMywxLjIyNCAwLjEyNiwxLjgzYzAuMzYyLDEuMzc5IDIuMTExLDEuOTQx
|
||||
IDMuMzQzLDIuMDE4YzIuNjk2LDAuMTY4IDUuNDQ3LC0wLjI0NSA4LjEzNiwtMGMyLjIwNywwLjIg
|
||||
NC40NTQsMC43MTcgNi42ODYsMC41MDRjMS41OCwtMC4xNSAzLjE1NCwtMC41MjUgNC43MywtMC42
|
||||
M2MzLjM5NCwtMC4yMjYgNi43OTEsLTAuNTA1IDEwLjIxOCwtMC41MDUiIHN0eWxlPSJmaWxsOm5v
|
||||
bmU7c3Ryb2tlOiMwMDA7c3Ryb2tlLXdpZHRoOjIuNDVweDsiLz48cGF0aCBkPSJNNTE5LjA1NCw0
|
||||
NzEuNjhsMCwwLjk0NmMwLDAuMTU1IC0wLDAuMjUxIDAuMDYzLDAuMzc5bDAsMC4xMjZjMC42MDIs
|
||||
LTIuNDA3IDMuNzc1LC0yLjc4NCA1LjgxLC0yLjc4NGMwLjE1LC0wIDAuMjMyLDAuNDggMC4yNDUs
|
||||
MC41NzZjMC4wNTUsMC4zODUgLTAuMTkyLDEuNDUgMC4zNzksMS42NGMwLjk4OSwwLjMzIDIuNDA2
|
||||
LC0wLjMxOCAzLjM0MywtMC41NjhjMC4yMzUsLTAuMDYyIDEuMDA5LC0wLjQ0MSAxLjE5OCwtMC4y
|
||||
NTJjMC45NTQsMC45NTQgMi40MTIsMC41NjIgMy43MjEsMC42MzFjMy4zMzIsMC4xNzUgNi42Mjks
|
||||
LTAuMTg5IDkuOTY2LC0wLjE4OWMxLjA1OCwtMCAyLjE2MiwwLjA4NyAzLjIxNywtMGMwLjA2OCwt
|
||||
MC4wMDYgMS4wMDksLTAuMDI3IDEuMDA5LC0wLjI1MyIgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2U6
|
||||
IzAwMDtzdHJva2Utd2lkdGg6Mi40NXB4OyIvPjwvZz48Zz48cGF0aCBkPSJNMzc0LjU3OCwxMTQu
|
||||
NDU1Yy0wLjYxMywtNC4yMTYgLTQuNTMzLC03LjE0MiAtOC43NDksLTYuNTNsLTk5Ljk0NSwxNC41
|
||||
MTVjLTQuMjE2LDAuNjEzIC03LjE0Miw0LjUzMyAtNi41Myw4Ljc0OWwyLjIxOSwxNS4yNzhjMC42
|
||||
MTIsNC4yMTYgNC41MzIsNy4xNDIgOC43NDksNi41M2w5OS45NDUsLTE0LjUxNWM0LjIxNiwtMC42
|
||||
MTIgNy4xNDIsLTQuNTMzIDYuNTI5LC04Ljc0OWwtMi4yMTgsLTE1LjI3OFoiIHN0eWxlPSJmaWxs
|
||||
OiM5ZGFjY2Q7c3Ryb2tlOiMwMDA7c3Ryb2tlLXdpZHRoOjUuMjFweDsiLz48cGF0aCBkPSJNMzEz
|
||||
Ljg2OSwxMTcuNDE0YzAsMCA2Ni4xMDIsOS44NzggNzcuNTgyLC0yMy43MzNjMTEuNDgsLTMzLjYx
|
||||
MSAtMTIuOTg3LC01MS44NzMgLTQxLjYzNywtMzkuOTIxYy0yOC42NSwxMS45NTIgLTU0Ljg1LDQ2
|
||||
LjE0MiAtNTQuODUsNDYuMTQyYzAsMCAtMzAuMDA1LC04LjEwOCAtNDEuMzE0LDUuMjIxYy0xMS4z
|
||||
MDksMTMuMzMgLTE3LjE2NywyNi42NDYgLTIuNDM1LDMxLjUxOWMxNC43MzIsNC44NzMgNjIuNjU0
|
||||
LC0xOS4yMjggNjIuNjU0LC0xOS4yMjhaIiBzdHlsZT0iZmlsbDojNzM1ZDkzO3N0cm9rZTojMDAw
|
||||
O3N0cm9rZS13aWR0aDo1LjkzcHg7Ii8+PHBhdGggZD0iTTM3NC4xNDgsMTM2Ljg3NmMtMCwwIDM4
|
||||
LjQ5MywtOS4zMzggMzYuNTYzLC0zLjU0OWMtMS45Myw1Ljc5IC0zNy42MzgsMjYuOTAxIC0xMTQu
|
||||
ODMxLDE1Ljk2NmM3MC4zNDUsLTEwLjczMSA3OC4yNjgsLTEyLjQxNyA3OC4yNjgsLTEyLjQxN1oi
|
||||
IHN0eWxlPSJmaWxsOiM1MDNhNzE7c3Ryb2tlOiMwMDA7c3Ryb2tlLXdpZHRoOjQuOTVweDsiLz48
|
||||
cGF0aCBkPSJNMzU3LjM0NiwxMjQuOTA2bC01NC4wMjEsOS42MTJsLTAsNS4zOTlsNTQuMDIxLC05
|
||||
LjYxMmwwLC01LjM5OVoiIHN0eWxlPSJmaWxsOiNmZmY7Ii8+PGNpcmNsZSBjeD0iMzAwLjE1IiBj
|
||||
eT0iMTM3LjE5OCIgcj0iNC42NzciIHN0eWxlPSJmaWxsOiNmZmIyNWM7c3Ryb2tlOiMwMDA7c3Ry
|
||||
b2tlLXdpZHRoOjQuOTVweDsiLz48Y2lyY2xlIGN4PSIzNjEuNDEiIGN5PSIxMjcuNTgxIiByPSI0
|
||||
LjI4MiIgc3R5bGU9ImZpbGw6I2ZmYjI1YztzdHJva2U6IzAwMDtzdHJva2Utd2lkdGg6NC45NXB4
|
||||
OyIvPjwvZz48L3N2Zz4=
|
3
testdata/logo.svg.base64.license
vendored
Normal file
3
testdata/logo.svg.base64.license
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
SPDX-FileCopyrightText: 2022 Maria Letta, the go-mail Team
|
||||
|
||||
SPDX-License-Identifier: CC-BY-ND-4.0
|
3
testdata/logo.svg.license
vendored
Normal file
3
testdata/logo.svg.license
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
SPDX-FileCopyrightText: 2022 Maria Letta, the go-mail Team
|
||||
|
||||
SPDX-License-Identifier: CC-BY-ND-4.0
|
0
testdata/tmp/.gitkeep
vendored
Normal file
0
testdata/tmp/.gitkeep
vendored
Normal file
Loading…
Reference in a new issue