erplibre/script/todo/mail/account_setup.py
Mathieu Benoit 16aecc2d33 [ADD] mail: read and send email from the TODO CLI
An IMAP/SMTP client in the menu, with its tests against real servers. Most
of the work went into refusals: an application password is named only when
the server actually refuses, an accented password is reported as never
having left the machine, and a refusal is recognised by what the server
SAYS rather than by matching its wording. A malformed date no longer takes
the whole folder down, and a received email is never read as markup.

--- FR ---

Un client IMAP/SMTP dans le menu, avec ses tests contre de vrais serveurs.
L'essentiel du travail porte sur les refus : le mot de passe d'application
n'est nommé que lorsque le serveur refuse vraiment, un mot de passe accentué
est signalé comme n'ayant jamais quitté la machine, et un refus se reconnaît
à ce que le serveur DIT plutôt qu'à ses mots. Une date illisible n'emporte
plus le dossier entier, et un courriel reçu n'est jamais lu comme du balisage.

Assisted-by: Claude Opus 5
2026-08-16 03:56:34 -04:00

62 lines
2.5 KiB
Python

#!/usr/bin/env python3
# © 2026 TechnoLibre (http://www.technolibre.ca)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
"""La logique d'ajout de compte et de mise en place du coffre, sans UI.
`menu._add_account` et `menu._ensure_kdbx` mêlaient cette logique — pas
triviale, elle annule l'écriture du secret si la sauvegarde du compte échoue
— à des `input()`/`getpass.getpass()`. Le CLI et le TUI ont chacun leur façon
de demander l'information, mais doivent appeler exactement le même code une
fois qu'ils l'ont : sinon les deux copies dérivent. Ce module ne connaît ni
`input`, ni Textual, ni aucune bibliothèque d'interface.
"""
from __future__ import annotations
import os
from script.todo.mail import accounts as mail_accounts
from script.todo.mail.accounts import AccountError
from script.todo.mail.secrets import SecretError, create_kdbx
from script.todo.todo_i18n import t
# Chemin proposé par défaut pour un kdbx nouvellement créé. `private/` est un
# dossier versionné, mais `private/.gitignore` y ignore déjà `*.kdbx` : c'est
# la convention du dépôt pour les fichiers de coffre.
DEFAULT_KDBX_PATH = "private/erplibre.kdbx"
def save_new_account(secret_store, accounts, account, password) -> None:
"""Écrit le mot de passe puis sauvegarde `accounts` (qui doit déjà
contenir `account`, à la place voulue par l'appelant).
Si la sauvegarde échoue, le secret est retiré du coffre avant que
l'exception ne remonte : l'y laisser sous une référence qu'aucune
configuration ne désigne en ferait un déchet invisible.
"""
secret_store.set(account.secret_ref, password)
try:
mail_accounts.save(accounts)
except (AccountError, OSError):
try:
secret_store.delete(account.secret_ref)
except SecretError:
pass
raise
def kdbx_is_configured(config_file) -> bool:
"""Vrai si un kdbx est déjà désigné dans la configuration."""
return bool(config_file.get_config_value(["kdbx", "path"]))
def create_vault(config_file, path: str, password: str) -> None:
"""Crée un nouveau kdbx à `path`, puis l'enregistre comme coffre actif."""
create_kdbx(path, password)
config_file.set_config_value(["kdbx", "path"], path)
def use_existing_vault(config_file, path: str) -> None:
"""Adopte un kdbx déjà présent sur disque comme coffre actif."""
if not path or not os.path.isfile(path):
raise SecretError(f"{t('mail_kdbx_path_not_found')} {path}")
config_file.set_config_value(["kdbx", "path"], path)