# Intégration Mail — Vue d'ensemble > ## 🟢 Statut & reprise (handoff — MAJ 2026-05-20) > > **Branche** : `feat/mail-integration` · **MR Gitea** : https://gitea.malio.fr/MALIO-DEV/Lesstime/pulls/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 content` → `whereAll()` > 2. `getDate()`/`getSubject()` renvoient des `Attribute` webklex v6 → casts explicites > 3. Séquence par défaut `ST_MSGN` → `peek()` 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 > ```bash > 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é : ```bash # 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) |