"""Credential handling for the mail module. Both checks guard fixes for real defects: the account file used to be written at the umask and chmodded afterwards, and the IMAP/SMTP connections used to take Python's stdlib SSL context, which verifies nothing. The 0600-mode assertions are POSIX-only: NTFS has no rwx-owner/group/other bit model, so os.open(..., 0o600) on Windows creates a normal read-write file and stat.S_IMODE reports 0o666 regardless of the mode argument -- Python's mode param there only round-trips the read-only *attribute*, not real ACL-based per-user access control (that needs pywin32/icacls, out of scope for a local single-user app whose own user-profile directory is already the actual access boundary on Windows). Skip rather than assert something the OS can't provide. """ import json import os import ssl import stat import sys import pytest from modules.mail import backend as mail _WINDOWS_NO_POSIX_MODE = pytest.mark.skipif( sys.platform == "win32", reason="0600 is a POSIX permission model; NTFS has no equivalent bits to assert on", ) def test_account_file_is_never_group_or_world_readable(tmp_path, monkeypatch): monkeypatch.setattr(mail, "_ACCOUNT_FILE", tmp_path / "mail_accounts.json") mail._write_accounts([{**mail._DEFAULTS, "id": "abc", "username": "u", "password": "secret"}]) # The mode assertion only means something on POSIX; the rest of this test # (no temp file left behind, password round-trip) is platform-independent # and must keep running on Windows. if sys.platform != "win32": mode = stat.S_IMODE((tmp_path / "mail_accounts.json").stat().st_mode) assert mode == 0o600, f"account file is {oct(mode)}, expected 0o600" assert not list(tmp_path.glob("*.tmp")), "temp file left behind" # The password round-trips to disk but never to the API. assert mail.load_accounts()[0]["password"] == "secret" assert "password" not in mail.public_account(mail.get_account("abc")) assert mail.public_account(mail.get_account("abc"))["has_password"] is True assert json.loads((tmp_path / "mail_accounts.json").read_text())["accounts"] @_WINDOWS_NO_POSIX_MODE def test_account_file_is_0600_while_it_is_being_written(tmp_path, monkeypatch): """The old code wrote at the umask and chmodded afterwards, so the file sat world-readable for the length of the write. Assert the handle it is written through is already 0600 -- the pre-fix version never wrote through a handle at all (json.dumps to a string, then write_text), so this fails against it.""" monkeypatch.setattr(mail, "_ACCOUNT_FILE", tmp_path / "mail_accounts.json") seen = {} real_dump = mail.json.dump def spy(obj, fh, **kw): seen["mode"] = stat.S_IMODE(os.fstat(fh.fileno()).st_mode) return real_dump(obj, fh, **kw) monkeypatch.setattr(mail.json, "dump", spy) mail._write_accounts([{**mail._DEFAULTS, "id": "abc", "username": "u", "password": "secret"}]) assert seen["mode"] == 0o600, f"written through a {oct(seen['mode'])} handle" def test_tls_context_verifies_certificate_and_hostname(): # ssl._create_stdlib_context(), the imaplib/smtplib fallback, gives # CERT_NONE + check_hostname False -- which is what this replaced. assert mail._TLS.verify_mode is ssl.CERT_REQUIRED assert mail._TLS.check_hostname is True