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