25 KiB
Mail Integration — Phase 7 : Admin Config + Sidebar + Polish
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Finaliser l'intégration mail avec l'UI admin de configuration, le lien sidebar avec badge unread temps réel (polling 30s), et la documentation utilisateur/opérationnelle finale.
Architecture: Onglet AdminMailTab.vue calqué sur AdminZimbraTab.vue (form IMAP/SMTP/credentials, bouton test connexion). Lien sidebar dans layouts/default.vue (visible ROLE_USER+ROLE_ADMIN seulement, masqué ROLE_CLIENT pur). Polling start au login / stop au logout via layout. Documentation finale dans docs/ + section README mail.
Tech Stack: Nuxt 4, Vue 3 Composition API, @malio/layer-ui, Pinia (useMailStore).
Fichiers créés / modifiés
| Fichier | Action |
|---|---|
frontend/components/admin/AdminMailTab.vue |
Créer |
frontend/pages/admin.vue |
Modifier (ajout onglet mail) |
frontend/layouts/default.vue |
Modifier (lien sidebar + polling lifecycle) |
frontend/i18n/locales/fr.json |
Modifier (clés mail.admin.* + mail.sidebar.*) |
frontend/i18n/locales/en.json |
Modifier si présent |
docs/mail-cron-setup.md |
Modifier (enrichir checklist prod + sécurité) |
docs/mail-integration.md |
Créer (doc complète intégration) |
Task 1 : Composant AdminMailTab.vue
Fichier cible : frontend/components/admin/AdminMailTab.vue
Modèle de référence : frontend/components/admin/AdminZimbraTab.vue — reproduire exactement le même pattern (reactive form, hasPassword, isSaving/isTesting, loadSettings onMounted, handleSave/handleTest).
Service à utiliser : useMailService() depuis ~/services/mail — méthodes getConfiguration, updateConfiguration, testConfiguration.
DTOs : MailConfigurationDto, MailConfigurationUpdateDto, MailTestConnectionResultDto depuis ~/services/dto/mail.
Étapes
- Créer
frontend/components/admin/AdminMailTab.vue - Déclarer le reactive form avec tous les champs de
MailConfigurationDto(saufhasPassword, qui est en lecture seule) :protocol: '' (lecture seule "imap" en MVP — champ disabled) imapHost: '' imapPort: 993 (default OVH) imapEncryption: 'ssl' (default OVH) smtpHost: '' smtpPort: 465 (default OVH) smtpEncryption: 'ssl' (default OVH) username: '' password: '' (write-only — jamais pré-rempli) sentFolderPath: '' (ex: "Sent Messages" ou "INBOX.Sent") enabled: false hasPassword:ref<boolean>(false)— alimenté pargetConfiguration().hasPasswordisSaving:ref<boolean>(false),isTesting:ref<boolean>(false)testResult:ref<boolean | null>(null)— réinitialisé à null au handleSaveloadSettings():async function loadSettings(): Promise<void> { const config = await getConfiguration() form.protocol = config.protocol ?? 'imap' form.imapHost = config.imapHost ?? '' form.imapPort = config.imapPort ?? 993 form.imapEncryption = config.imapEncryption ?? 'ssl' form.smtpHost = config.smtpHost ?? '' form.smtpPort = config.smtpPort ?? 465 form.smtpEncryption = config.smtpEncryption ?? 'ssl' form.username = config.username ?? '' form.sentFolderPath = config.sentFolderPath ?? '' form.enabled = config.enabled hasPassword.value = config.hasPassword // password jamais pré-rempli }handleSave(): construit unMailConfigurationUpdateDto— inclurepassworduniquement siform.passwordest non-vide, sinon omettre le champ. Après save réussi :hasPassword.value = result.hasPassword, viderform.password,testResult.value = nullhandleTest(): appelletestConfiguration(),testResult.value = result.ok. Le champresult.errorest affiché en sous-texte sitestResult.value === false- Template — sections IMAP et SMTP avec labels traduits :
- Titre
h2:$t('mail.admin.title') - Section IMAP (
fieldsetoudivavec titre$t('mail.admin.imapSection')) :MalioInputTextpourimapHost+ helper text$t('mail.admin.ovhDefaultsHelp')sous le champ (texte gris :ssl0.ovh.net)input[type=number]natif pourimapPort(MalioInputText n'accepte pas les number — voir convention CLAUDE.md)selectnatif pourimapEncryption(options :ssl,tls,none)
- Section SMTP (
$t('mail.admin.smtpSection')) :MalioInputTextpoursmtpHostinput[type=number]natif poursmtpPortselectnatif poursmtpEncryption(options :ssl,tls,none)
- Credentials :
MalioInputTextpourusernameMalioInputPasswordpourpassword+ indicateurhasPassword(même pattern queAdminZimbraTab.vue:<p v-if="hasPassword && !form.password">{{ $t('mail.admin.passwordSet') }}</p>)
MalioInputTextpoursentFolderPath(placeholder:Sent Messages)label+ checkbox natif pourenabled:$t('mail.admin.enabled')- Boutons côte à côte :
MalioButtonsubmit$t('mail.admin.save'):disabled="isSaving"→handleSaveMalioButtonvariant tertiary$t('mail.admin.test'):disabled="isTesting"→handleTest
- Résultat test :
<p v-if="testResult !== null">coloré vert/rouge selon valeur — si false ETtestError, affichertestErrorsous le résultat
- Titre
onMounted(() => { loadSettings() })- Vérifier indentation 4 espaces, pas d'imports inutilisés, TypeScript strict
Task 2 : Intégration AdminMailTab dans pages/admin.vue
Fichier cible : frontend/pages/admin.vue
Le pattern actuel utilise un tableau tabs as const + activeTab ref + v-if par composant. Il suffit d'ajouter l'entrée mail à la fin.
Étapes
- Ouvrir
frontend/pages/admin.vue - Dans le tableau
tabs, ajouter à la fin :Remarque : les labels dans{ key: 'mail', label: 'Mail' },tabssont des string litéraux inline (cf. autres onglets comme'Zimbra'), pas de i18n ici. - Le type
TabKeyest inféré automatiquement viatypeof tabs[number]['key']— pas de changement nécessaire - Dans le template, après
<AdminZimbraTab v-if="activeTab === 'zimbra'" />, ajouter :<AdminMailTab v-if="activeTab === 'mail'" /> - Vérifier que Nuxt auto-importe
AdminMailTab(fichier danscomponents/admin/→ auto-import OK) - Test manuel : naviguer vers
/admin, cliquer l'onglet "Mail", vérifier que le form se charge sans erreur 403 si connecté ROLE_ADMIN
Task 3 : Lien sidebar dans layouts/default.vue
Fichier cible : frontend/layouts/default.vue
Le composant SidebarLink accepte to, icon, label, collapsed. Il n'a pas de prop badge native — vérifier dans @malio/layer-ui/COMPONENTS.md si une prop badge existe. Si non, wrapper manuel avec un <div class="relative"> + badge absolu.
Étapes
- Lire
frontend/node_modules/@malio/layer-ui/COMPONENTS.mdpour vérifier les props deSidebarLink(présence propbadgeoubadgeCount) - Cas A — SidebarLink a une prop badge :
Utiliser directement :
<SidebarLink v-if="isMailVisible" to="/mail" icon="material-symbols:mail-outline" label="$t('mail.sidebar.title')" :collapsed="sidebarIsCollapsed" :badge="mailStore.globalUnreadCount > 0 ? mailStore.globalUnreadCount : undefined" aria-label="$t('mail.sidebar.ariaLabel')" @click="ui.closeMobileSidebar()" /> - Cas B — SidebarLink n'a pas de prop badge (plus probable) :
Wrapper avec badge manuel :
<div v-if="isMailVisible" class="relative"> <SidebarLink to="/mail" icon="material-symbols:mail-outline" :label="$t('mail.sidebar.title')" :collapsed="sidebarIsCollapsed" @click="ui.closeMobileSidebar()" /> <span v-if="mailStore.globalUnreadCount > 0" class="absolute right-2 top-1/2 -translate-y-1/2 flex h-5 min-w-5 items-center justify-center rounded-full bg-red-500 px-1 text-xs font-bold text-white" :aria-label="`${mailStore.globalUnreadCount} messages non lus`" > {{ mailStore.globalUnreadCount > 99 ? '99+' : mailStore.globalUnreadCount }} </span> </div> - Dans
<script setup>, ajouter :const mailStore = useMailStore() - Définir le computed
isMailVisible:const isMailVisible = computed(() => { const roles: string[] = auth.user?.roles ?? [] // Visible si ROLE_USER (ou ROLE_ADMIN) mais pas ROLE_CLIENT exclusif const isClient = roles.includes('ROLE_CLIENT') && !roles.includes('ROLE_ADMIN') && !roles.includes('ROLE_USER') return !isClient && (roles.includes('ROLE_USER') || roles.includes('ROLE_ADMIN')) }) - Placer le lien sidebar après
SidebarLink to="/my-tasks"et avantSidebarLink to="/projects"(ordre logique : dashboard → mes tâches → mail → projets → suivi de temps → admin) - Vérifier responsive : en mode collapsed (
sidebarIsCollapsed = true), le badge doit rester visible et accessible - Test manuel : utilisateur ROLE_CLIENT seul → lien absent. Utilisateur ROLE_USER → lien visible. Badge rouge si
globalUnreadCount > 0
Task 4 : Lifecycle polling start/stop
Fichier cible : frontend/layouts/default.vue
Le store useMailStore expose startPolling() (idempotent — guard if (pollTimer) return) et stopPolling(). Le polling doit démarrer au montage du layout (si l'utilisateur est autorisé) et s'arrêter au logout.
Étapes
- Dans
onMounteddelayouts/default.vue(qui contient déjàtimerStore.fetchActive()), ajouter après :if (isMailVisible.value) { mailStore.startPolling() } - Vérifier que
isMailVisibleest disponible dans le même scope (oui, c'est un computed défini dans<script setup>) - Pour le stop au logout : dans
useAuthStore, le logout vide l'user. Watcher surauth.userdans le layout :watch(() => auth.user, (user) => { if (!user) { mailStore.stopPolling() } else if (isMailVisible.value) { mailStore.startPolling() } }) - Vérifier l'idempotence :
startPolling()dans le store a déjàif (pollTimer) return— naviguer entre les pages ne crée pas plusieurs timers onUnmounteddans le layout n'est pas nécessaire car le layout persiste toute la session ; le watch surauth.usersuffit- Test manuel : ouvrir devtools → Network → vérifier un seul appel
GET /api/mail/folderstoutes les 30s, pas de rafale
Task 5 : i18n additionnels Phase 7
Fichiers cibles : frontend/i18n/locales/fr.json (et en.json si présent)
Clés à ajouter (section mail — fusionner avec les clés existantes des phases précédentes)
{
"mail": {
"sidebar": {
"title": "Messagerie",
"ariaLabel": "Accès à la messagerie, {count} messages non lus"
},
"admin": {
"title": "Configuration messagerie",
"protocol": "Protocole",
"imapSection": "Réception (IMAP)",
"smtpSection": "Envoi (SMTP)",
"host": "Serveur",
"port": "Port",
"encryption": "Chiffrement",
"username": "Adresse e-mail",
"password": "Mot de passe",
"passwordSet": "Mot de passe déjà configuré — laisser vide pour conserver",
"sentFolderPath": "Dossier des envois",
"enabled": "Activer la synchronisation mail",
"test": "Tester la connexion",
"testSuccess": "Connexion IMAP réussie",
"testFailed": "Échec de connexion",
"save": "Enregistrer",
"saveSuccess": "Configuration enregistrée",
"ovhDefaultsHelp": "OVH : ssl0.ovh.net (port 993 IMAP / 465 SMTP)"
}
}
}
Étapes
- Ouvrir
frontend/i18n/locales/fr.json - Localiser la section
mailexistante (créée en Phase 4/5) - Fusionner les clés
mail.sidebar.*etmail.admin.*sans écraser les clés existantes - Si
en.jsonexiste : ajouter les équivalents anglais (traduction directe — pas d'approximation) - Vérifier la cohérence JSON (virgules, pas de clés dupliquées)
make dev-nuxt→ console browser → 0 warning[vue-i18n] Missing locale message
Task 6 : Documentation finale
6a — Enrichir docs/mail-cron-setup.md
Fichier cible : docs/mail-cron-setup.md
Ce fichier existe déjà (créé Phase 2). Ajouter les sections manquantes :
- Ajouter section "Checklist setup production" après la section "Variables d'environnement" :
## Checklist setup production 1. [ ] Définir `ENCRYPTION_KEY` dans les variables d'environnement production 2. [ ] Créer le compte mail dédié (ex: `lesstime@votre-domaine.fr`) chez OVH 3. [ ] Accéder à `/admin` → onglet "Mail" → renseigner les credentials IMAP/SMTP 4. [ ] Cliquer "Tester la connexion" → vérifier le succès 5. [ ] Cocher "Activer la synchronisation" → Enregistrer 6. [ ] Installer le cron OS (voir section "Installation du cron") 7. [ ] Vérifier les logs après la première sync : `make logs-dev` (chercher `mail.sync`) - Ajouter section "Sécurité" (si absente ou incomplète) :
## Rappels sécurité - La page `/mail` et tous les endpoints `/api/mail/*` sont refusés aux `ROLE_CLIENT` exclusifs - Le sidebar "Messagerie" est masqué pour les utilisateurs ROLE_CLIENT sans ROLE_USER - Le password IMAP est chiffré via libsodium secretbox avant stockage (jamais en clair en base) - Les corps de mails sont sanitisés via DOMPurify avant affichage (voir `frontend/utils/sanitizeMailHtml.ts`) - Les pixels tracking distants sont remplacés par un placeholder - Aucun body mail, password ou contenu de pièce jointe n'est loggé
6b — Créer docs/mail-integration.md
Fichier cible : docs/mail-integration.md
- Créer le fichier avec les sections suivantes :
# Intégration Mail — Vue d'ensemble
## 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
- Aller sur
/admin→ onglet "Mail" - Renseigner les credentials IMAP/SMTP (OVH :
ssl0.ovh.net, port 993/465, SSL) - Cliquer "Tester la connexion"
- Activer la synchronisation → Enregistrer
- 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) |
### 6c — Vérifier `make mail-sync` dans le README
- [ ] Ouvrir `README.md` à la racine de Lesstime
- [ ] Vérifier si une section mail ou une mention de `make mail-sync` existe déjà
- [ ] Si absente : ajouter dans la section des commandes Makefile une ligne documentant `make mail-sync` avec la description courte (cf. le commentaire déjà présent dans le makefile)
---
## Task 7 : Vérifications sécurité finales
### Étapes
- [ ] Ouvrir `frontend/utils/sanitizeMailHtml.ts` — vérifier la config DOMPurify :
- `FORBID_TAGS` doit inclure : `script`, `iframe`, `object`, `embed`, `form`, `input`
- `FORBID_ATTR` doit inclure tous les handlers `on*` + `javascript:` dans `href`/`src`
- Les `<img src="http(s)://...">` distants sont remplacés par un placeholder (pas juste supprimés)
- Si manquant, noter la correction mais ne pas modifier (la correction est documentée ici pour le codeur)
- [ ] Test injection XSS manuel (dans la console browser, sur la page `/mail`) :
```js
import('/utils/sanitizeMailHtml').then(m => {
console.log(m.sanitizeMailHtml('<script>alert(1)</script><img src=x onerror=alert(2)><iframe src="javascript:alert(3)"></iframe>'))
})
Résultat attendu : chaîne sans <script>, sans onerror, sans <iframe>
- Grep logs — confirmer aucun body/password/attachment dans les logs :
Vérifier que les occurrences trouvées sont uniquement des définitions de propriétés, jamais passées à un logger
grep -rn "bodyHtml\|bodyText\|password\|attachment.*content" src/Mail/ src/Service/MailSyncService.php src/Controller/Mail/ --include="*.php" - Vérifier que
GET /api/mail/configurationne retourne jamais de champpassworddans la réponse JSON (tester aveccurl -s http://localhost:8082/api/mail/configuration -H "Cookie: BEARER=..."ou équivalent) - Vérifier que
POST /api/mail/foldersavec un cookie ROLE_CLIENT retourne bien 403
Task 8 : QA passe end-to-end
Étapes
make test→ 0 failure, 0 errormake php-cs-fixer-allow-risky→ idempotent (0 fichier modifié)cd frontend && npx tsc --noEmit→ 0 erreur TypeScriptmake dev-nuxt→ démarrage OK, 0 erreur console browser au load de/mail- Workflow admin :
- Se connecter en admin
- Aller sur
/admin→ onglet "Mail" - Renseigner
imapHost = ssl0.ovh.net,imapPort = 993,imapEncryption = ssl,username = test@example.com,password = test - Cliquer "Tester la connexion" → résultat affiché (succès ou échec selon config réelle)
- Enregistrer → toast "Configuration enregistrée"
- Rechargement de la page → les champs sont pré-remplis, indicateur "Mot de passe déjà configuré" visible
- Workflow sidebar :
- Se connecter en ROLE_USER
- Vérifier que le lien "Messagerie" est visible dans la sidebar
- Vérifier le badge si
globalUnreadCount > 0 - Se connecter en ROLE_CLIENT → vérifier l'absence du lien sidebar
- Workflow polling :
- Ouvrir les DevTools → Network → filtrer sur
mail/folders - Rester sur une page 90s → exactement 3 appels (1 immédiat + 2 toutes les 30s)
- Naviguer entre
/mailet/my-tasks→ pas de rafale, pas de duplication du polling
- Ouvrir les DevTools → Network → filtrer sur
- Workflow complet mail → tâche (régression Phase 6) :
- Ouvrir un mail dans
/mail - Cliquer "Créer tâche" → modal → sélectionner projet → créer
- Tâche apparaît dans
/my-tasksavec le mail lié - Depuis le TaskDrawer de la tâche → onglet "Mails" → mail visible → cliquer → redirection
/mail?messageId=X
- Ouvrir un mail dans
- Simulation sync :
make mail-sync DRYRUN=1→ commande retourne 0, pas d'erreur Symfony
Task 9 : Cleanup final
Étapes
- Grep debug dans tous les fichiers mail frontend :
Supprimer toute occurrence (sauf
grep -rn "console\.log\|console\.warn\|console\.error\|debugger" frontend/components/mail/ frontend/components/admin/AdminMailTab.vue frontend/stores/mail.ts frontend/services/mail.ts frontend/utils/sanitizeMailHtml.tsconsole.errorintentionnel avec commentaire explicatif) - Grep TODO/FIXME/HACK :
Résoudre ou supprimer chaque occurrence
grep -rn "TODO\|FIXME\|HACK\|XXX" frontend/components/mail/ frontend/components/admin/AdminMailTab.vue frontend/stores/mail.ts frontend/services/mail.ts - Vérifier qu'aucun import inutilisé ne traîne dans
AdminMailTab.vueet les fichiers modifiés danslayouts/default.vue cd frontend && npx tsc --noEmit→ toujours 0 erreur après cleanup- Si des modifications ont été faites depuis le dernier commit Phase 6, créer un commit final :
(deux commits séparés si les changements sont distincts)
feat(mail) : Phase 7 — admin config tab, sidebar badge, polling lifecycle docs(mail) : documentation intégration mail complète
Critères d'acceptation (Phase 7 complète)
- Admin peut accéder à
/admin→ onglet "Mail" → configurer IMAP/SMTP → tester → activer - Le sidebar affiche un badge unread actualisé toutes les 30s pour ROLE_USER/ROLE_ADMIN
- Le sidebar "Messagerie" est invisible pour ROLE_CLIENT exclusif
make testvertnpx tsc --noEmit0 erreur- 0 warning console browser au chargement
- 0 ERROR PHP dans
make logs-devpendant le workflow normal docs/mail-integration.mdcomplet et accessibledocs/mail-cron-setup.mdenrichi avec checklist prod et rappels sécurité
Dépendances
- Phase 5 (store
useMailStoreavecstartPolling/stopPolling+ page/mail) — DONE - Phase 6 (intégration tâches) — DONE
- Phase 3 (endpoints
/api/mail/configurationGET/PATCH/test, ROLE_CLIENT refusé) — DONE - Phase 4 (services
getConfiguration,updateConfiguration,testConfiguration, DTOs) — DONE