Files
Lesstime/docs/mail-integration.md

8.4 KiB

Intégration Mail — Vue d'ensemble

🟢 Statut & reprise (handoff — MAJ 2026-05-20)

Branche : feat/mail-integration · MR Gitea : #5 (base develop) Construit en 7 phases (plans dans docs/superpowers/plans/2026-05-19-mail-phase*.md).

Ce qui marche (testé contre une vraie boîte OVH contact@malio.fr)

  • Connexion IMAP + test connexion (admin → /admin onglet Mail)
  • Synchro complète multi-dossiers : 456 messages / 57 dossiers ramenés, ne crashe plus
  • Lecture dossiers/messages dans /mail, arbre repliable (chevrons, sous-dossiers masqués par défaut)
  • Lecture d'un mail, sanitization DOMPurify
  • Création/lien tâche depuis un mail

Bugs déjà corrigés ce soir (NE PAS ré-investiguer)

Tous dans ImapMailProvider / MailSyncService — les tests mockaient le provider, donc le fetch réel n'avait jamais été exercé avant le test live :

  1. Requête sans critère → BAD parse error: zero-length contentwhereAll()
  2. getDate()/getSubject() renvoient des Attribute webklex v6 → casts explicites
  3. Séquence par défaut ST_MSGNpeek() faisait un STORE rejeté par OVH (flag could not be removed) → forcé ST_UID partout
  4. Snippet via getTextBody() = fetch du corps de chaque mail (sync 179s + peek) → setFetchBody(false), snippet désactivé au listing
  5. Test connexion exigeait enabled=true → découplé via getClient(requireEnabled:false) + testConnection()
  6. Contrainte UNIQUE globale sur message_id → fausse pour IMAP (même Message-ID dans plusieurs dossiers) → fermait l'EntityManager → cascade. Migration Version20260520061736 : index simple. Garde anti-cascade dans MailSyncService (reset ManagerRegistry).
  7. 139 connexions IMAP (une/dossier) → throttling OVH → réutilisation d'1 connexion (closeConnection() sur l'interface) + reconnexion ciblée après dossier en erreur.
  • Contrat front/back réaligné dans frontend/services/mail.ts (route /mail/folders/{path}/messages, mapping messages→items, fromAddress→fromEmail, détail plat→imbriqué).

Points en suspens / à savoir

  • Mise à jour auto = cron OS lançant make mail-sync toutes les 10 min (cf docs/mail-cron-setup.md). Pas configuré en dev — lancer à la main.
  • Bouton "Actualiser" : dispatch async Messenger (MailSyncRequested → async). Sans worker messenger:consume async qui tourne, les demandes s'empilent sans s'exécuter. En prod : supervisor. En dev : lancer un worker.
  • ~7 dossiers/139 à encodage spécial (ex: INBOX/RH/.../SÉBASTIEN en UTF7-modifié) ou réponses vides sont skippés proprement et réessayés au cycle suivant. Edge case webklex non bloquant.
  • Dépendance : webklex/php-imap ^6.2 tire des paquets Laravel (illuminate/* via carbon ^3) dans ce projet Symfony — fonctionnel mais à valider en review.
  • 6 PHPUnit Notices (mocks sans expectations) non bloquantes.

Commandes utiles

make mail-sync                                                   # synchro complète
docker exec -i -u www-data php-lesstime-fpm php bin/console app:mail:sync --folder=INBOX -v
docker exec -i -u www-data php-lesstime-fpm php bin/console messenger:consume async -vv   # worker (fait marcher le bouton)
make test                                                        # 33 tests

Fixtures make fixtures plantent sur un état legacy workflow_id (hors-scope mail) — configurer la boîte via l'UI admin.

Fonctionnalités

  • Lecture de la boîte mail partagée (IMAP) depuis Lesstime
  • Navigation par dossiers (arbre récursif avec compteurs non-lus)
  • Liste paginée des messages (infinite scroll, cursor-based)
  • Lecture des corps de mail sanitisés (DOMPurify — protection XSS + pixels tracking)
  • Création d'une tâche Lesstime depuis un mail (sujet → titre, texte → description)
  • Lien mail ↔ tâche (bidirectionnel)
  • Onglet "Mails" dans le TaskDrawer pour retrouver les mails liés à une tâche
  • Synchronisation IMAP automatique via cron OS (toutes les 10 min)
  • Déclenchement manuel de sync depuis l'UI (bouton Refresh)
  • Badge non-lus en temps réel dans la sidebar (polling 30s)

Endpoints API

Méthode URL Rôle Description
GET /api/mail/configuration ROLE_ADMIN Lire la config singleton
PATCH /api/mail/configuration ROLE_ADMIN Mettre à jour la config
POST /api/mail/configuration/test ROLE_ADMIN Tester la connexion IMAP
GET /api/mail/folders ROLE_USER Arbre des dossiers + unread
GET /api/mail/messages ROLE_USER Liste paginée (param: folder, cursor, limit)
GET /api/mail/messages/{id} ROLE_USER Détail + body (cached 5 min)
POST /api/mail/messages/{id}/read ROLE_USER Marquer lu/non-lu
POST /api/mail/messages/{id}/flag ROLE_USER Marquer étoilé/non-étoilé
POST /api/mail/messages/{id}/create-task ROLE_USER Créer tâche depuis mail
POST /api/mail/messages/{id}/link-task ROLE_USER Lier mail à tâche existante
DELETE /api/mail/messages/{id}/link-task/{taskId} ROLE_USER Supprimer le lien
GET /api/tasks/{id}/mails ROLE_USER Mails liés à une tâche
GET /api/mail/attachments/{id} ROLE_USER Télécharger une pièce jointe
POST /api/mail/sync ROLE_USER Déclencher sync async (Messenger)

Tous les endpoints /api/mail/* refusent explicitement ROLE_CLIENT.

Sécurité

  • ROLE_CLIENT exclusif : accès refusé à tous les endpoints mail et à la page /mail
  • Le sidebar "Messagerie" est masqué pour les ROLE_CLIENT
  • Password IMAP chiffré via libsodium secretbox (env ENCRYPTION_KEY)
  • Corps de mail sanitisés via DOMPurify (sanitizeMailHtml.ts) — script/iframe/object/embed/on*/javascript: bloqués
  • Pixels tracking distants (img src http) remplacés par placeholder
  • Aucun body, password ou contenu de pièce jointe dans les logs

Dépendances

Backend

  • webklex/php-imap : client IMAP PHP
  • symfony/lock : Symfony Lock pour éviter les syncs parallèles
  • symfony/messenger : dispatch asynchrone MailSyncRequested
  • libsodium (ext PHP) : chiffrement du password IMAP

Frontend

  • dompurify + @types/dompurify : sanitization HTML des corps de mail

Fichiers clés

Backend

  • src/Entity/MailConfiguration.php — entité singleton (credentials, enabled)
  • src/Entity/MailFolder.php — dossier IMAP synced
  • src/Entity/MailMessage.php — message IMAP synced (headers, flags)
  • src/Entity/TaskMailLink.php — lien tâche ↔ mail
  • src/Mail/ImapMailProvider.php — implémentation IMAP (webklex)
  • src/Service/MailSyncService.php — algorithme de sync (UID FETCH, resync flags)
  • src/Controller/Mail/ — controllers custom (test, folders, messages, sync)
  • src/State/Mail/ — providers/processors API Platform (configuration)

Frontend

  • frontend/pages/mail.vue — page principale 3 colonnes
  • frontend/components/mail/ — MailFolderTree, MailMessageList, MailMessageViewer, MailRefreshButton
  • frontend/components/admin/AdminMailTab.vue — onglet config admin
  • frontend/stores/mail.ts — store Pinia (folders, messages, polling)
  • frontend/services/mail.ts — service API (toutes les méthodes)
  • frontend/services/dto/mail.ts — types TypeScript
  • frontend/utils/sanitizeMailHtml.ts — DOMPurify wrapper

Synchronisation cron

Voir docs/mail-cron-setup.md pour la configuration détaillée.

Résumé :

# Cron OS (toutes les 10 min)
*/10 * * * * cd /path/to/Lesstime && make mail-sync >> /var/log/lesstime-mail-sync.log 2>&1

# Commandes Makefile
make mail-sync              # Sync complète
make mail-sync FOLDER=INBOX # Sync d'un dossier
make mail-sync DRYRUN=1     # Simulation sans écriture

Configuration admin

  1. Aller sur /admin → onglet "Mail"
  2. Renseigner les credentials IMAP/SMTP (OVH : ssl0.ovh.net, port 993/465, SSL)
  3. Cliquer "Tester la connexion"
  4. Activer la synchronisation → Enregistrer
  5. Configurer le cron OS

Variables d'environnement

Variable Description Obligatoire
ENCRYPTION_KEY Clé hex 32 bytes libsodium pour chiffrer le password IMAP Oui
LOCK_DSN DSN Symfony Lock (défaut: flock) Non
MESSENGER_TRANSPORT_DSN Transport Messenger pour sync async Recommandé (prod)