Files
Lesstime/docs/superpowers/plans/2026-05-19-mail-phase7-admin-polish.md

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 (sauf hasPassword, 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é par getConfiguration().hasPassword
  • isSaving : ref<boolean>(false), isTesting : ref<boolean>(false)
  • testResult : ref<boolean | null>(null) — réinitialisé à null au handleSave
  • loadSettings() :
    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 un MailConfigurationUpdateDto — inclure password uniquement si form.password est non-vide, sinon omettre le champ. Après save réussi : hasPassword.value = result.hasPassword, vider form.password, testResult.value = null
  • handleTest() : appelle testConfiguration(), testResult.value = result.ok. Le champ result.error est affiché en sous-texte si testResult.value === false
  • Template — sections IMAP et SMTP avec labels traduits :
    • Titre h2 : $t('mail.admin.title')
    • Section IMAP (fieldset ou div avec titre $t('mail.admin.imapSection')) :
      • MalioInputText pour imapHost + helper text $t('mail.admin.ovhDefaultsHelp') sous le champ (texte gris : ssl0.ovh.net)
      • input[type=number] natif pour imapPort (MalioInputText n'accepte pas les number — voir convention CLAUDE.md)
      • select natif pour imapEncryption (options : ssl, tls, none)
    • Section SMTP ($t('mail.admin.smtpSection')) :
      • MalioInputText pour smtpHost
      • input[type=number] natif pour smtpPort
      • select natif pour smtpEncryption (options : ssl, tls, none)
    • Credentials :
      • MalioInputText pour username
      • MalioInputPassword pour password + indicateur hasPassword (même pattern que AdminZimbraTab.vue : <p v-if="hasPassword && !form.password">{{ $t('mail.admin.passwordSet') }}</p>)
    • MalioInputText pour sentFolderPath (placeholder: Sent Messages)
    • label + checkbox natif pour enabled : $t('mail.admin.enabled')
    • Boutons côte à côte :
      • MalioButton submit $t('mail.admin.save') :disabled="isSaving"handleSave
      • MalioButton variant tertiary $t('mail.admin.test') :disabled="isTesting"handleTest
    • Résultat test : <p v-if="testResult !== null"> coloré vert/rouge selon valeur — si false ET testError, afficher testError sous le résultat
  • 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 :
    { key: 'mail', label: 'Mail' },
    
    Remarque : les labels dans tabs sont des string litéraux inline (cf. autres onglets comme 'Zimbra'), pas de i18n ici.
  • Le type TabKey est inféré automatiquement via typeof 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 dans components/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.md pour vérifier les props de SidebarLink (présence prop badge ou badgeCount)
  • 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 avant SidebarLink 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 onMounted de layouts/default.vue (qui contient déjà timerStore.fetchActive()), ajouter après :
    if (isMailVisible.value) {
        mailStore.startPolling()
    }
    
  • Vérifier que isMailVisible est 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 sur auth.user dans 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
  • onUnmounted dans le layout n'est pas nécessaire car le layout persiste toute la session ; le watch sur auth.user suffit
  • Test manuel : ouvrir devtools → Network → vérifier un seul appel GET /api/mail/folders toutes 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 mail existante (créée en Phase 4/5)
  • Fusionner les clés mail.sidebar.* et mail.admin.* sans écraser les clés existantes
  • Si en.json existe : 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

  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)

### 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 :
    grep -rn "bodyHtml\|bodyText\|password\|attachment.*content" src/Mail/ src/Service/MailSyncService.php src/Controller/Mail/ --include="*.php"
    
    Vérifier que les occurrences trouvées sont uniquement des définitions de propriétés, jamais passées à un logger
  • Vérifier que GET /api/mail/configuration ne retourne jamais de champ password dans la réponse JSON (tester avec curl -s http://localhost:8082/api/mail/configuration -H "Cookie: BEARER=..." ou équivalent)
  • Vérifier que POST /api/mail/folders avec un cookie ROLE_CLIENT retourne bien 403

Task 8 : QA passe end-to-end

Étapes

  • make test → 0 failure, 0 error
  • make php-cs-fixer-allow-risky → idempotent (0 fichier modifié)
  • cd frontend && npx tsc --noEmit → 0 erreur TypeScript
  • make 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 /mail et /my-tasks → pas de rafale, pas de duplication du polling
  • 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-tasks avec le mail lié
    • Depuis le TaskDrawer de la tâche → onglet "Mails" → mail visible → cliquer → redirection /mail?messageId=X
  • 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 :
    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.ts
    
    Supprimer toute occurrence (sauf console.error intentionnel avec commentaire explicatif)
  • Grep TODO/FIXME/HACK :
    grep -rn "TODO\|FIXME\|HACK\|XXX" frontend/components/mail/ frontend/components/admin/AdminMailTab.vue frontend/stores/mail.ts frontend/services/mail.ts
    
    Résoudre ou supprimer chaque occurrence
  • Vérifier qu'aucun import inutilisé ne traîne dans AdminMailTab.vue et les fichiers modifiés dans layouts/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 :
    feat(mail) : Phase 7 — admin config tab, sidebar badge, polling lifecycle
    docs(mail) : documentation intégration mail complète
    
    (deux commits séparés si les changements sont distincts)

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 test vert
  • npx tsc --noEmit 0 erreur
  • 0 warning console browser au chargement
  • 0 ERROR PHP dans make logs-dev pendant le workflow normal
  • docs/mail-integration.md complet et accessible
  • docs/mail-cron-setup.md enrichi avec checklist prod et rappels sécurité

Dépendances

  • Phase 5 (store useMailStore avec startPolling/stopPolling + page /mail) — DONE
  • Phase 6 (intégration tâches) — DONE
  • Phase 3 (endpoints /api/mail/configuration GET/PATCH/test, ROLE_CLIENT refusé) — DONE
  • Phase 4 (services getConfiguration, updateConfiguration, testConfiguration, DTOs) — DONE