Brings the public tree back in line with the development repo after several weeks of drift caused by a stale publish include list. New: - In-app update path: GET /update/check compares the checkout against origin/main and POST /update/apply runs `ncp upgrade` detached (pull, rebuild, restart). The sidebar shows the version, checks on click, and offers an "update available" pill. - Projects: a project workspace groups chats and RAG documents, with per-project instructions and document retrieval scoped to the active project. Replaces the standalone Documents page. - modules/: auto-discovered feature plugins (mail, network) with their frontend counterparts and tests. - Memory curation runs in-process (synapse/memory/curator.py) on the chat model when a conversation goes idle. The separate memory service on :8001 is gone, along with the launcher lines that started it. Also: the KDE theme, panel and Promethean terminal assets, the full test suite, and VERSION 1.2.0. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
54 lines
2.3 KiB
Python
54 lines
2.3 KiB
Python
"""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.
|
|
"""
|
|
import json
|
|
import os
|
|
import ssl
|
|
import stat
|
|
|
|
from modules.mail import backend as mail
|
|
|
|
|
|
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"}])
|
|
|
|
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"]
|
|
|
|
|
|
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
|