email-forward-parser 0.2.0__tar.gz → 0.2.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- email_forward_parser-0.2.1/.githooks/pre-push +93 -0
- {email_forward_parser-0.2.0 → email_forward_parser-0.2.1}/.github/dependabot.yml +6 -0
- {email_forward_parser-0.2.0 → email_forward_parser-0.2.1}/.github/workflows/ci.yml +6 -11
- {email_forward_parser-0.2.0 → email_forward_parser-0.2.1}/.github/workflows/workflow.yml +22 -19
- {email_forward_parser-0.2.0 → email_forward_parser-0.2.1}/PKG-INFO +1 -1
- {email_forward_parser-0.2.0 → email_forward_parser-0.2.1}/emailforwardparser/client.py +44 -9
- {email_forward_parser-0.2.0 → email_forward_parser-0.2.1}/pyproject.toml +1 -1
- {email_forward_parser-0.2.0 → email_forward_parser-0.2.1}/tests/regressions_test.py +23 -0
- email_forward_parser-0.2.1/uv.lock +495 -0
- email_forward_parser-0.2.0/uv.lock +0 -495
- {email_forward_parser-0.2.0 → email_forward_parser-0.2.1}/.gitignore +0 -0
- {email_forward_parser-0.2.0 → email_forward_parser-0.2.1}/LICENSE +0 -0
- {email_forward_parser-0.2.0 → email_forward_parser-0.2.1}/NOTICE +0 -0
- {email_forward_parser-0.2.0 → email_forward_parser-0.2.1}/README.md +0 -0
- {email_forward_parser-0.2.0 → email_forward_parser-0.2.1}/emailforwardparser/__init__.py +0 -0
- {email_forward_parser-0.2.0 → email_forward_parser-0.2.1}/emailforwardparser/forward_parser.py +0 -0
- {email_forward_parser-0.2.0 → email_forward_parser-0.2.1}/emailforwardparser/loop.py +0 -0
- {email_forward_parser-0.2.0 → email_forward_parser-0.2.1}/emailforwardparser/regexs.py +0 -0
- {email_forward_parser-0.2.0 → email_forward_parser-0.2.1}/emailforwardparser/utils.py +0 -0
- {email_forward_parser-0.2.0 → email_forward_parser-0.2.1}/tests/__init__.py +0 -0
- {email_forward_parser-0.2.0 → email_forward_parser-0.2.1}/tests/client_test.py +0 -0
- {email_forward_parser-0.2.0 → email_forward_parser-0.2.1}/tests/coverage_edges_test.py +0 -0
- {email_forward_parser-0.2.0 → email_forward_parser-0.2.1}/tests/forward_parser_test.py +0 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Style and type checks on the commits being pushed; GitHub Actions runs the tests.
|
|
3
|
+
# Turn it on once per clone: git config core.hooksPath .githooks
|
|
4
|
+
|
|
5
|
+
fix='uv run black . && uv run isort . && uv run ruff check --fix .'
|
|
6
|
+
|
|
7
|
+
# The same install as CI's "Install locked dependencies".
|
|
8
|
+
setup() {
|
|
9
|
+
uv sync --frozen || return
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
checks() {
|
|
13
|
+
check uv run black --check .
|
|
14
|
+
check uv run isort --check-only .
|
|
15
|
+
check uv run ruff check .
|
|
16
|
+
check uv run mypy
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
# --- the same in every repo from here ---
|
|
20
|
+
|
|
21
|
+
set -u
|
|
22
|
+
unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE
|
|
23
|
+
common=$(cd "$(git rev-parse --git-common-dir)" && pwd)
|
|
24
|
+
checked="$common/pre-push-checked"
|
|
25
|
+
# A commit passes for this version of the hook: changing the checks checks it again.
|
|
26
|
+
version=$(cksum <"$0" | cut -d' ' -f1)
|
|
27
|
+
# One checkout per clone, kept between pushes so its dependencies and tool caches stay installed.
|
|
28
|
+
cache="${XDG_CACHE_HOME:-$HOME/.cache}/pre-push"
|
|
29
|
+
dir="$cache/$(basename "$(dirname "$common")")-$(printf %s "$common" | cksum | cut -d' ' -f1)"
|
|
30
|
+
lock="$dir.lock"
|
|
31
|
+
|
|
32
|
+
# Run one check and carry on, so a push shows every failure at once.
|
|
33
|
+
check() {
|
|
34
|
+
"$@" || failed=1
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
# Another push in this clone may be using the checkout: wait for it, or take over a lock its push left behind.
|
|
38
|
+
take_lock() {
|
|
39
|
+
mkdir -p "$cache"
|
|
40
|
+
until mkdir "$lock" 2>/dev/null; do
|
|
41
|
+
owner=$(cat "$lock/pid" 2>/dev/null)
|
|
42
|
+
if [ -n "$owner" ] && ! kill -0 "$owner" 2>/dev/null; then
|
|
43
|
+
rm -rf "$lock"
|
|
44
|
+
continue
|
|
45
|
+
fi
|
|
46
|
+
echo "pre-push: waiting for another push's checks to finish" >&2
|
|
47
|
+
sleep 5
|
|
48
|
+
done
|
|
49
|
+
echo $$ >"$lock/pid"
|
|
50
|
+
}
|
|
51
|
+
trap '[ "$(cat "$lock/pid" 2>/dev/null)" = $$ ] && rm -rf "$lock"' EXIT
|
|
52
|
+
trap 'exit 1' INT TERM
|
|
53
|
+
|
|
54
|
+
# Check out the commit itself, not the working tree: uncommitted files don't go out.
|
|
55
|
+
checkout() {
|
|
56
|
+
if [ -f "$dir/.git" ] && git -C "$dir" checkout --quiet --force --detach "$1" 2>/dev/null; then
|
|
57
|
+
git -C "$dir" clean -fdq
|
|
58
|
+
return
|
|
59
|
+
fi
|
|
60
|
+
# First push, or the folder or its registration went missing: start it again.
|
|
61
|
+
rm -rf "$dir"
|
|
62
|
+
git worktree add --quiet --force --detach "$dir" "$1"
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
status=0
|
|
66
|
+
while read -r _ sha _ _; do
|
|
67
|
+
# A deleted ref sends all zeros, which isn't a commit.
|
|
68
|
+
commit=$(git rev-parse --verify --quiet "$sha^{commit}") || continue
|
|
69
|
+
# Pushing the same commit to the next remote doesn't check it again.
|
|
70
|
+
grep -qsx "$commit $version" "$checked" && continue
|
|
71
|
+
|
|
72
|
+
[ -d "$lock" ] && [ "$(cat "$lock/pid" 2>/dev/null)" = $$ ] || take_lock
|
|
73
|
+
short=$(git rev-parse --short "$commit")
|
|
74
|
+
if ! checkout "$commit"; then
|
|
75
|
+
echo "pre-push: couldn't check out $short in $dir" >&2
|
|
76
|
+
status=1
|
|
77
|
+
continue
|
|
78
|
+
fi
|
|
79
|
+
if ! (cd "$dir" && setup) </dev/null >&2; then
|
|
80
|
+
echo "pre-push: installing the dependencies of $short failed" >&2
|
|
81
|
+
status=1
|
|
82
|
+
continue
|
|
83
|
+
fi
|
|
84
|
+
if (cd "$dir" && failed=0 && checks && exit $failed) </dev/null; then
|
|
85
|
+
echo "$commit $version" >>"$checked"
|
|
86
|
+
else
|
|
87
|
+
echo >&2
|
|
88
|
+
echo "pre-push: checks failed on $short. Style fixes itself with: $fix" >&2
|
|
89
|
+
echo "pre-push: type errors need a code change. Commit the fix and push again (git push --no-verify skips the checks)." >&2
|
|
90
|
+
status=1
|
|
91
|
+
fi
|
|
92
|
+
done
|
|
93
|
+
exit $status
|
|
@@ -12,8 +12,14 @@ updates:
|
|
|
12
12
|
- "dependencies"
|
|
13
13
|
- "python"
|
|
14
14
|
|
|
15
|
+
# The actions in .github/workflows are pinned to commit SHAs; this keeps the SHAs and their version comments
|
|
16
|
+
# current, in one pull request a week.
|
|
15
17
|
- package-ecosystem: "github-actions"
|
|
16
18
|
directory: "/"
|
|
19
|
+
groups:
|
|
20
|
+
github-actions:
|
|
21
|
+
patterns:
|
|
22
|
+
- "*"
|
|
17
23
|
schedule:
|
|
18
24
|
interval: "weekly"
|
|
19
25
|
day: "monday"
|
|
@@ -11,26 +11,21 @@ concurrency:
|
|
|
11
11
|
group: ci-${{ github.workflow }}-${{ github.ref }}
|
|
12
12
|
cancel-in-progress: true
|
|
13
13
|
|
|
14
|
+
# Style and type checks run before each push, in .githooks/pre-push; this workflow runs the rest.
|
|
15
|
+
# Runners are pinned to ubuntu-24.04 (ubuntu-latest moves to Ubuntu 26 from 2026-10-19) and actions to commit
|
|
16
|
+
# SHAs, which Dependabot keeps current.
|
|
14
17
|
jobs:
|
|
15
18
|
verify:
|
|
16
|
-
runs-on: ubuntu-
|
|
19
|
+
runs-on: ubuntu-24.04
|
|
17
20
|
timeout-minutes: 30
|
|
18
21
|
steps:
|
|
19
|
-
- uses: actions/checkout@v7
|
|
22
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
20
23
|
- name: Install uv and Python
|
|
21
|
-
uses: astral-sh/setup-uv@v10.0
|
|
24
|
+
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
|
22
25
|
with:
|
|
23
26
|
python-version: "3.14"
|
|
24
27
|
enable-cache: true
|
|
25
28
|
- name: Install locked dependencies
|
|
26
29
|
run: uv sync --frozen
|
|
27
|
-
- name: Check formatting
|
|
28
|
-
run: |
|
|
29
|
-
uv run black --check .
|
|
30
|
-
uv run isort --check-only .
|
|
31
|
-
- name: Lint
|
|
32
|
-
run: uv run ruff check .
|
|
33
|
-
- name: Type check
|
|
34
|
-
run: uv run mypy
|
|
35
30
|
- name: Test with configured coverage gate
|
|
36
31
|
run: uv run pytest
|
|
@@ -11,35 +11,36 @@ permissions:
|
|
|
11
11
|
jobs:
|
|
12
12
|
verify:
|
|
13
13
|
name: Verify package
|
|
14
|
-
runs-on: ubuntu-
|
|
14
|
+
runs-on: ubuntu-24.04
|
|
15
|
+
timeout-minutes: 30
|
|
15
16
|
steps:
|
|
16
|
-
- uses: actions/checkout@v7
|
|
17
|
-
-
|
|
17
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
18
|
+
- name: Install uv and Python
|
|
19
|
+
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
|
18
20
|
with:
|
|
19
21
|
python-version: "3.14"
|
|
20
22
|
enable-cache: true
|
|
21
|
-
-
|
|
23
|
+
- name: Install locked dependencies
|
|
24
|
+
run: uv sync --frozen
|
|
22
25
|
- name: Verify tag matches package version
|
|
23
26
|
run: |
|
|
24
27
|
PACKAGE_VERSION=$(python -c 'import tomllib; print(tomllib.load(open("pyproject.toml", "rb"))["project"]["version"])')
|
|
25
28
|
test "v${PACKAGE_VERSION}" = "${GITHUB_REF_NAME}"
|
|
26
|
-
-
|
|
27
|
-
|
|
28
|
-
- run: uv run ruff check .
|
|
29
|
-
- run: uv run mypy
|
|
30
|
-
- run: uv run pytest
|
|
29
|
+
- name: Test with configured coverage gate
|
|
30
|
+
run: uv run pytest
|
|
31
31
|
|
|
32
32
|
build:
|
|
33
33
|
name: Build distribution
|
|
34
34
|
needs: verify
|
|
35
|
-
runs-on: ubuntu-
|
|
35
|
+
runs-on: ubuntu-24.04
|
|
36
|
+
timeout-minutes: 10
|
|
36
37
|
|
|
37
38
|
steps:
|
|
38
39
|
- name: Checkout
|
|
39
|
-
uses: actions/checkout@v7
|
|
40
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
40
41
|
|
|
41
42
|
- name: Set up Python
|
|
42
|
-
uses: actions/setup-python@v7
|
|
43
|
+
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
|
43
44
|
with:
|
|
44
45
|
python-version: "3.14"
|
|
45
46
|
|
|
@@ -53,7 +54,7 @@ jobs:
|
|
|
53
54
|
run: python -m twine check dist/*
|
|
54
55
|
|
|
55
56
|
- name: Upload distribution artifacts
|
|
56
|
-
uses: actions/upload-artifact@v7
|
|
57
|
+
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
|
57
58
|
with:
|
|
58
59
|
name: python-package-distributions
|
|
59
60
|
path: dist/
|
|
@@ -61,7 +62,8 @@ jobs:
|
|
|
61
62
|
publish:
|
|
62
63
|
name: Publish distribution to PyPI
|
|
63
64
|
needs: build
|
|
64
|
-
runs-on: ubuntu-
|
|
65
|
+
runs-on: ubuntu-24.04
|
|
66
|
+
timeout-minutes: 10
|
|
65
67
|
environment: pypi
|
|
66
68
|
|
|
67
69
|
permissions:
|
|
@@ -70,32 +72,33 @@ jobs:
|
|
|
70
72
|
|
|
71
73
|
steps:
|
|
72
74
|
- name: Download distribution artifacts
|
|
73
|
-
uses: actions/download-artifact@v8
|
|
75
|
+
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
|
74
76
|
with:
|
|
75
77
|
name: python-package-distributions
|
|
76
78
|
path: dist/
|
|
77
79
|
|
|
78
80
|
- name: Publish package
|
|
79
|
-
uses: pypa/gh-action-pypi-publish@
|
|
81
|
+
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
|
|
80
82
|
|
|
81
83
|
release:
|
|
82
84
|
name: Publish GitHub release
|
|
83
85
|
needs: publish
|
|
84
86
|
if: startsWith(github.ref, 'refs/tags/')
|
|
85
|
-
runs-on: ubuntu-
|
|
87
|
+
runs-on: ubuntu-24.04
|
|
88
|
+
timeout-minutes: 10
|
|
86
89
|
|
|
87
90
|
permissions:
|
|
88
91
|
contents: write
|
|
89
92
|
|
|
90
93
|
steps:
|
|
91
94
|
- name: Download distribution artifacts
|
|
92
|
-
uses: actions/download-artifact@v8
|
|
95
|
+
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
|
93
96
|
with:
|
|
94
97
|
name: python-package-distributions
|
|
95
98
|
path: dist/
|
|
96
99
|
|
|
97
100
|
- name: Publish release
|
|
98
|
-
uses: softprops/action-gh-release@v3
|
|
101
|
+
uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3
|
|
99
102
|
with:
|
|
100
103
|
files: dist/*
|
|
101
104
|
generate_release_notes: true
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: email-forward-parser
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Python Library to parse forwarded email messages
|
|
5
5
|
Project-URL: Homepage, https://github.com/felipehertzer/email-forward-parser-py
|
|
6
6
|
Project-URL: Repository, https://github.com/felipehertzer/email-forward-parser-py
|
|
@@ -4,6 +4,8 @@ import base64
|
|
|
4
4
|
import binascii
|
|
5
5
|
import logging
|
|
6
6
|
from email import policy
|
|
7
|
+
from email.errors import HeaderParseError
|
|
8
|
+
from email.header import decode_header, make_header
|
|
7
9
|
from email.message import EmailMessage, Message
|
|
8
10
|
from email.mime.multipart import MIMEMultipart
|
|
9
11
|
from email.mime.text import MIMEText
|
|
@@ -38,7 +40,7 @@ class EmailParserClient:
|
|
|
38
40
|
:rtype: dict
|
|
39
41
|
"""
|
|
40
42
|
msg = self._parse_message(email)
|
|
41
|
-
send_to = self._first_header_address(
|
|
43
|
+
send_to = self._first_header_address(self._header(msg, "From"))
|
|
42
44
|
eml = self._get_eml_attachment(msg)
|
|
43
45
|
if eml:
|
|
44
46
|
return self._get_dict(eml, fp.OriginalMetadata(), False, send_to=send_to)
|
|
@@ -107,9 +109,20 @@ class EmailParserClient:
|
|
|
107
109
|
)
|
|
108
110
|
eml = self._build_original_email(email, source_message).as_string()
|
|
109
111
|
else:
|
|
110
|
-
eml =
|
|
112
|
+
eml = self._as_string(message) if isinstance(message, Message) else message
|
|
111
113
|
return {"forward": forwarded, "eml": eml, "Send-To": send_to}
|
|
112
114
|
|
|
115
|
+
@staticmethod
|
|
116
|
+
def _as_string(message: Message) -> str:
|
|
117
|
+
"""The message as text. A header longer than a line is folded again as it is written,
|
|
118
|
+
which builds its mailboxes again and refuses (ValueError) one whose encoded-word name
|
|
119
|
+
decodes to a line break: the message is then written with its headers as they came.
|
|
120
|
+
"""
|
|
121
|
+
try:
|
|
122
|
+
return message.as_string()
|
|
123
|
+
except ValueError:
|
|
124
|
+
return message.as_string(policy=policy.default.clone(refold_source="none"))
|
|
125
|
+
|
|
113
126
|
def _build_original_email(
|
|
114
127
|
self, metadata: fp.OriginalMetadata, message: Message
|
|
115
128
|
) -> EmailMessage | MIMEMultipart:
|
|
@@ -176,8 +189,7 @@ class EmailParserClient:
|
|
|
176
189
|
|
|
177
190
|
def _get_forwarded_metadata(self, message: Message) -> fp.ForwardMetadata:
|
|
178
191
|
body = self._get_body(message)
|
|
179
|
-
subject =
|
|
180
|
-
subject = str(subject) if subject is not None else ""
|
|
192
|
+
subject = self._header(message, "Subject")
|
|
181
193
|
if subject:
|
|
182
194
|
return fp.get_forwarded_metadata(body.strip(), subject.strip())
|
|
183
195
|
return fp.get_forwarded_metadata(body.strip())
|
|
@@ -233,16 +245,39 @@ class EmailParserClient:
|
|
|
233
245
|
return Parser(policy=policy.default).parsestr(email)
|
|
234
246
|
|
|
235
247
|
def _get_message_metadata(self, message: Message) -> fp.OriginalMetadata:
|
|
236
|
-
from_mailboxes = self._mailboxes_from_header(
|
|
248
|
+
from_mailboxes = self._mailboxes_from_header(self._header(message, "From"))
|
|
237
249
|
return fp.OriginalMetadata(
|
|
238
|
-
date=
|
|
239
|
-
subject=
|
|
250
|
+
date=self._header(message, "Date").strip(),
|
|
251
|
+
subject=self._header(message, "Subject").strip(),
|
|
240
252
|
body=self._get_body(message).strip(),
|
|
241
253
|
from_=from_mailboxes[0] if from_mailboxes else fp.MailboxResult(),
|
|
242
|
-
to=self._mailboxes_from_header(
|
|
243
|
-
cc=self._mailboxes_from_header(
|
|
254
|
+
to=self._mailboxes_from_header(self._header(message, "To")),
|
|
255
|
+
cc=self._mailboxes_from_header(self._header(message, "Cc")),
|
|
244
256
|
)
|
|
245
257
|
|
|
258
|
+
@staticmethod
|
|
259
|
+
def _header(message: Message, name: str) -> str:
|
|
260
|
+
"""The header's decoded value, or "" when the message has none.
|
|
261
|
+
|
|
262
|
+
The email package builds an address header's mailboxes as it reads it, and refuses
|
|
263
|
+
(ValueError) one whose encoded-word name decodes to a line break, so reading such a
|
|
264
|
+
From raised out of the parser. Its mailboxes are read from the raw value instead,
|
|
265
|
+
each name decoded with its line breaks as spaces.
|
|
266
|
+
"""
|
|
267
|
+
try:
|
|
268
|
+
value = message.get(name)
|
|
269
|
+
except ValueError:
|
|
270
|
+
raw = next(str(raw) for key, raw in message.raw_items() if key.lower() == name.lower())
|
|
271
|
+
mailboxes = []
|
|
272
|
+
for encoded_name, address in getaddresses([raw]):
|
|
273
|
+
try:
|
|
274
|
+
decoded = str(make_header(decode_header(encoded_name)))
|
|
275
|
+
except HeaderParseError, LookupError, UnicodeError:
|
|
276
|
+
decoded = encoded_name
|
|
277
|
+
mailboxes.append(formataddr((" ".join(decoded.split()), address)))
|
|
278
|
+
return ", ".join(mailboxes)
|
|
279
|
+
return str(value) if value is not None else ""
|
|
280
|
+
|
|
246
281
|
def _first_header_address(self, header: object) -> str:
|
|
247
282
|
mailboxes = self._mailboxes_from_header(header)
|
|
248
283
|
if not mailboxes:
|
|
@@ -193,3 +193,26 @@ def test_loop_regexes_match_keeps_earliest_then_first_listed() -> None:
|
|
|
193
193
|
assert loop.loop_regexes_match([earlier, later], "abc") == (["b"], earlier)
|
|
194
194
|
assert loop.loop_regexes_match([later, earlier], "abc") == (["b"], earlier)
|
|
195
195
|
assert loop.loop_regexes_match([earlier, tie], "bc") == (["b"], earlier)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def test_a_from_whose_encoded_name_decodes_to_a_line_break_is_read_without_raising() -> None:
|
|
199
|
+
"""The email package refuses to build an address with a line break in it (ValueError),
|
|
200
|
+
and raised it out of get_original_metadata, failing whoever parsed the email."""
|
|
201
|
+
raw = (
|
|
202
|
+
"From: =?utf-8?q?Jane=0D=0ABcc=3A_evil=40example=2Ecom?= <jane@example.com>\n"
|
|
203
|
+
"To: =?utf-8?q?Bob=0A?= <bob@example.com>, =?x-unknown?q?Ann?= <ann@example.com>\n"
|
|
204
|
+
"Subject: Hello\n\nNo forward here.\n"
|
|
205
|
+
)
|
|
206
|
+
client = EmailParserClient()
|
|
207
|
+
|
|
208
|
+
metadata = client.get_original_metadata(raw)
|
|
209
|
+
assert metadata.forwarded is False
|
|
210
|
+
assert metadata.email.from_ == MailboxResult("Jane Bcc: evil@example.com", "jane@example.com")
|
|
211
|
+
# A name in a charset Python has no codec for stays as it was written.
|
|
212
|
+
assert metadata.email.to == [
|
|
213
|
+
MailboxResult("Bob", "bob@example.com"),
|
|
214
|
+
MailboxResult("=?x-unknown?q?Ann?=", "ann@example.com"),
|
|
215
|
+
]
|
|
216
|
+
original = client.get_original_eml(raw)
|
|
217
|
+
assert original["Send-To"] == "jane@example.com"
|
|
218
|
+
assert original["eml"].startswith(raw.splitlines()[0])
|