527 lines
25 KiB
Markdown
527 lines
25 KiB
Markdown
# 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
|