# 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()` :
  ```ts
  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 :
  ```ts
  { 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 :
  ```html
  <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 :
  ```html
  <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 :
  ```html
  <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 :
  ```ts
  const mailStore = useMailStore()
  ```
- [ ] Définir le computed `isMailVisible` :
  ```ts
  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 :
  ```ts
  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 :
  ```ts
  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)

```json
{
  "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" :
  ```markdown
  ## 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) :
  ```markdown
  ## 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 :

```markdown
# 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 :
  ```bash
  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 :
  ```bash
  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 :
  ```bash
  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
