aprf-reader 0.6.0__tar.gz → 0.6.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.
Files changed (21) hide show
  1. {aprf_reader-0.6.0/src/aprf_reader.egg-info → aprf_reader-0.6.1}/PKG-INFO +2 -2
  2. {aprf_reader-0.6.0 → aprf_reader-0.6.1}/README.md +1 -1
  3. {aprf_reader-0.6.0 → aprf_reader-0.6.1}/pyproject.toml +1 -1
  4. {aprf_reader-0.6.0 → aprf_reader-0.6.1}/src/aprf_reader/__init__.py +1 -1
  5. {aprf_reader-0.6.0 → aprf_reader-0.6.1}/src/aprf_reader/imap.py +11 -3
  6. {aprf_reader-0.6.0 → aprf_reader-0.6.1/src/aprf_reader.egg-info}/PKG-INFO +2 -2
  7. {aprf_reader-0.6.0 → aprf_reader-0.6.1}/tests/test_imap.py +15 -2
  8. {aprf_reader-0.6.0 → aprf_reader-0.6.1}/LICENSE +0 -0
  9. {aprf_reader-0.6.0 → aprf_reader-0.6.1}/setup.cfg +0 -0
  10. {aprf_reader-0.6.0 → aprf_reader-0.6.1}/src/aprf_reader/cli.py +0 -0
  11. {aprf_reader-0.6.0 → aprf_reader-0.6.1}/src/aprf_reader/load.py +0 -0
  12. {aprf_reader-0.6.0 → aprf_reader-0.6.1}/src/aprf_reader/render.py +0 -0
  13. {aprf_reader-0.6.0 → aprf_reader-0.6.1}/src/aprf_reader/report.py +0 -0
  14. {aprf_reader-0.6.0 → aprf_reader-0.6.1}/src/aprf_reader/summary.py +0 -0
  15. {aprf_reader-0.6.0 → aprf_reader-0.6.1}/src/aprf_reader.egg-info/SOURCES.txt +0 -0
  16. {aprf_reader-0.6.0 → aprf_reader-0.6.1}/src/aprf_reader.egg-info/dependency_links.txt +0 -0
  17. {aprf_reader-0.6.0 → aprf_reader-0.6.1}/src/aprf_reader.egg-info/entry_points.txt +0 -0
  18. {aprf_reader-0.6.0 → aprf_reader-0.6.1}/src/aprf_reader.egg-info/requires.txt +0 -0
  19. {aprf_reader-0.6.0 → aprf_reader-0.6.1}/src/aprf_reader.egg-info/top_level.txt +0 -0
  20. {aprf_reader-0.6.0 → aprf_reader-0.6.1}/tests/test_reader.py +0 -0
  21. {aprf_reader-0.6.0 → aprf_reader-0.6.1}/tests/test_web_parity.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aprf-reader
3
- Version: 0.6.0
3
+ Version: 0.6.1
4
4
  Summary: Summarise APRF (Aggregate Performance Reporting) inbox placement reports.
5
5
  License: MIT
6
6
  Requires-Python: >=3.11
@@ -79,7 +79,7 @@ marked read, moved or deleted. It reads messages whose subject contains
79
79
  from the `APRF_IMAP_PASSWORD` environment variable, or it asks. Gmail and
80
80
  Outlook.com need an app password, not your account password, and a Gmail
81
81
  label is read as a folder of the same name. The connection uses TLS on port
82
- 993; give another port as `--imap host:port`. Files it skips are listed
82
+ 993 and checks the server's certificate; give another port as `--imap host:port`. Files it skips are listed
83
83
  on stderr and at the end of the summary, each with a reason.
84
84
 
85
85
  Each stream (provider, signing domain, selector) gets a table with one row per
@@ -66,7 +66,7 @@ marked read, moved or deleted. It reads messages whose subject contains
66
66
  from the `APRF_IMAP_PASSWORD` environment variable, or it asks. Gmail and
67
67
  Outlook.com need an app password, not your account password, and a Gmail
68
68
  label is read as a folder of the same name. The connection uses TLS on port
69
- 993; give another port as `--imap host:port`. Files it skips are listed
69
+ 993 and checks the server's certificate; give another port as `--imap host:port`. Files it skips are listed
70
70
  on stderr and at the end of the summary, each with a reason.
71
71
 
72
72
  Each stream (provider, signing domain, selector) gets a table with one row per
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "aprf-reader"
7
- version = "0.6.0"
7
+ version = "0.6.1"
8
8
  description = "Summarise APRF (Aggregate Performance Reporting) inbox placement reports."
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -1,3 +1,3 @@
1
1
  """Read APRF aggregate performance reports."""
2
2
 
3
- __version__ = "0.6.0"
3
+ __version__ = "0.6.1"
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import imaplib
6
6
  import re
7
+ import ssl
7
8
  from collections.abc import Iterator
8
9
  from datetime import date
9
10
 
@@ -26,6 +27,12 @@ def _check(typ: str, data: list, what: str) -> list:
26
27
  return data
27
28
 
28
29
 
30
+ def _text(e: Exception) -> str:
31
+ # imaplib puts the server's reply bytes in the exception as they came
32
+ a = e.args[0] if e.args else ""
33
+ return a.decode(errors="replace") if isinstance(a, bytes) else str(e)
34
+
35
+
29
36
  def _quote(s: str) -> str:
30
37
  return '"' + s.replace("\\", "\\\\").replace('"', '\\"') + '"'
31
38
 
@@ -43,16 +50,17 @@ def fetch(host: str, user: str, password: str, folder: str = "INBOX", port: int
43
50
 
44
51
  The folder is opened read-only and bodies are fetched with BODY.PEEK, so no
45
52
  message is marked read, moved or deleted."""
46
- m = conn or imaplib.IMAP4_SSL(host, port, timeout=60)
53
+ # imaplib's own default context checks neither the certificate nor the host name
54
+ m = conn or imaplib.IMAP4_SSL(host, port, ssl_context=ssl.create_default_context(), timeout=60)
47
55
  try:
48
56
  try:
49
57
  _check(*m.login(user, password), "login")
50
58
  except imaplib.IMAP4.error as e:
51
- raise ImapError(f"login failed: {e}") from None
59
+ raise ImapError(f"login failed: {_text(e)}") from None
52
60
  try:
53
61
  _check(*m.select(_quote(folder), readonly=True), f"opening folder {folder}")
54
62
  except imaplib.IMAP4.error as e:
55
- raise ImapError(f"opening folder {folder} failed: {e}") from None
63
+ raise ImapError(f"opening folder {folder} failed: {_text(e)}") from None
56
64
  (ids,) = _check(*m.uid("SEARCH", *criteria(since, every)), "search")
57
65
  uids = (ids or b"").split()
58
66
  if len(uids) > MAX_MESSAGES:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aprf-reader
3
- Version: 0.6.0
3
+ Version: 0.6.1
4
4
  Summary: Summarise APRF (Aggregate Performance Reporting) inbox placement reports.
5
5
  License: MIT
6
6
  Requires-Python: >=3.11
@@ -79,7 +79,7 @@ marked read, moved or deleted. It reads messages whose subject contains
79
79
  from the `APRF_IMAP_PASSWORD` environment variable, or it asks. Gmail and
80
80
  Outlook.com need an app password, not your account password, and a Gmail
81
81
  label is read as a folder of the same name. The connection uses TLS on port
82
- 993; give another port as `--imap host:port`. Files it skips are listed
82
+ 993 and checks the server's certificate; give another port as `--imap host:port`. Files it skips are listed
83
83
  on stderr and at the end of the summary, each with a reason.
84
84
 
85
85
  Each stream (provider, signing domain, selector) gets a table with one row per
@@ -1,6 +1,7 @@
1
1
  import gzip
2
2
  import imaplib
3
3
  import json
4
+ import ssl
4
5
  from datetime import date
5
6
 
6
7
  import pytest
@@ -87,7 +88,7 @@ def test_fetched_mail_read_like_an_mbox():
87
88
 
88
89
  def test_cli_imap(monkeypatch, capsys):
89
90
  fake = FakeImap(mails(2))
90
- monkeypatch.setattr(imaplib, "IMAP4_SSL", lambda host, port, timeout: fake)
91
+ monkeypatch.setattr(imaplib, "IMAP4_SSL", lambda host, port, ssl_context, timeout: fake)
91
92
  monkeypatch.setenv("APRF_IMAP_PASSWORD", "right")
92
93
  assert cli.main(["--imap", "imap.example.com", "--user", "u", "--folder", "APRF", "--json"]) == 0
93
94
  out, err = capsys.readouterr()
@@ -96,10 +97,22 @@ def test_cli_imap(monkeypatch, capsys):
96
97
 
97
98
 
98
99
  def test_cli_imap_failure_and_usage(monkeypatch, capsys):
99
- monkeypatch.setattr(imaplib, "IMAP4_SSL", lambda host, port, timeout: FakeImap({}))
100
+ monkeypatch.setattr(imaplib, "IMAP4_SSL", lambda host, port, ssl_context, timeout: FakeImap({}))
100
101
  monkeypatch.setenv("APRF_IMAP_PASSWORD", "wrong")
101
102
  assert cli.main(["--imap", "h", "--user", "u"]) == 2
102
103
  assert "login failed" in capsys.readouterr().err
103
104
  for argv in ([], ["--imap", "h"], ["--imap", "h:x", "--user", "u"]):
104
105
  with pytest.raises(SystemExit):
105
106
  cli.main(argv)
107
+
108
+
109
+ def test_certificate_is_checked(monkeypatch):
110
+ seen = {}
111
+
112
+ def connect(host, port, ssl_context, timeout):
113
+ seen["ctx"] = ssl_context
114
+ return FakeImap({})
115
+
116
+ monkeypatch.setattr(imaplib, "IMAP4_SSL", connect)
117
+ list(fetch("h", "u", "right"))
118
+ assert seen["ctx"].verify_mode == ssl.CERT_REQUIRED and seen["ctx"].check_hostname
File without changes
File without changes