Trois causes racines révélées par une vraie synchro complète (139 dossiers) :
- contrainte UNIQUE globale sur message_id : fausse pour IMAP (un même Message-ID
existe dans plusieurs dossiers) → violation → fermeture de l'EntityManager →
cascade qui tuait tous les dossiers suivants. Migration : index simple à la place.
- 139 connexions IMAP (une par dossier) → throttling OVH (failed to authenticate) :
réutilisation d'une seule connexion (closeConnection() ajouté à l'interface).
- état de connexion corrompu après un dossier en erreur (must be in SELECTED state) :
reconnexion ciblée après chaque dossier en échec.
- garde anti-cascade : reset du ManagerRegistry + arrêt propre si l'EM se ferme.
Résultat : 456 messages sur 57 dossiers (avant : 188/30 puis crash). Les rares
dossiers à encodage spécial sont skippés proprement et réessayés au cycle suivant.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Quatre bugs révélés en testant contre une vraie boîte OVH (les tests mockaient
le provider, donc jamais exercés) :
- requête sans critère → "BAD parse error: zero-length content" : ajout de whereAll()
- getDate()/getSubject() renvoient des Attribute webklex v6, pas des scalaires : casts explicites
- séquence par défaut ST_MSGN → le peek() de webklex faisait un STORE par numéro de
séquence rejeté par OVH ("flag could not be removed") : force ST_UID sur toutes les requêtes
- snippet via getTextBody() forçait un fetch de corps par mail (sync 179s + peek) :
setFetchBody(false) au listing, snippet désormais optionnel
Sync INBOX : 9 messages en 1,6s (avant : échec en 179s).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Le guard enabled dans getClient() bloquait le test de connexion alors que le
workflow naturel est configurer → tester → activer. getClient(requireEnabled)
permet au nouveau testConnection() de se connecter sans exiger enabled=true.
Le controller (ROLE_ADMIN) renvoie désormais le détail de l'erreur pour faciliter
le diagnostic.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- dispatch MailSyncRequested au bus Messenger, retourne 202 immediat
- folderPath optionnel via body JSON pour sync ciblee
- en test : transport in-memory route le message en sync
- securite via MailAccessChecker
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- App\Message\MailSyncRequested (optionnel folderPath)
- App\MessageHandler\MailSyncRequestedHandler delegue a MailSyncService::syncFolder ou syncAll
- messenger.yaml : transport async via Doctrine DSN, retry 3x exponentiel, failure transport
- en test : transport in-memory (sync immediat)
- migration Version20260519220000 : cree messenger_messages table (idempotente, IF NOT EXISTS)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- downloadId = base64url(messageDbId:partNumber)
- Content-Disposition: attachment systematique (jamais inline pour eviter XSS via HTML attachments)
- X-Content-Type-Options: nosniff
- filename sanitise via basename pour eviter path traversal
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- POST /api/mail/messages/{id}/link-task body {taskId} : cree TaskMailLink (idempotent)
- DELETE /api/mail/messages/{id}/link-task/{taskId} : supprime le lien (204)
- GET /api/tasks/{id}/mails : liste les mails lies a une tache
- securite via MailAccessChecker, tests fonctionnels 401/403
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- cree une Task avec titre = subject du mail (max 255 chars)
- utilise findMaxNumberByProjectForUpdate pour numero (advisory lock PG)
- transaction wrapInTransaction pour eviter race conditions
- taskGroupId et priorityId optionnels via body JSON
- cree automatiquement le TaskMailLink (mail <-> tache)
- retourne 201 + taskId/taskNumber/taskTitle/messageId
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- POST /api/mail/messages/{id}/read body {read: bool} - synchro IMAP + BDD
- POST /api/mail/messages/{id}/flag body {flagged: bool} - synchro IMAP + BDD
- IMAP-side non bloquant : BDD est mise a jour meme si IMAP fail (resync au prochain cycle)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- recupere headers + body + attachments via ImapMailProvider::fetchMessage
- cache Symfony pool cache.app, cle mail_body_{md5(messageId)}, TTL 300s
- attachments serialises sans contenu binaire, avec downloadId base64url(messageDbId:partNumber)
- 503 si IMAP indisponible, 404 si message inconnu
- les tests read/flag ROLE_CLIENT/auth seront ajoutes en Task 10 (route deja existante)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- ensureCanAccessMail : refuse ROLE_CLIENT pur (sans ROLE_ADMIN)
- ensureIsAdmin : helper pour endpoints config
- service utilise par tous les controllers metier mail
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- endpoint ROLE_ADMIN qui teste la connexion IMAP via listFolders
- retourne ok:bool + foldersCount ou error sanitise (pas de leak interne)
- priority:1 obligatoire pour eviter conflit avec route API Platform {id}
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- ApiResource MailSettings expose les operations Get + Patch sur /api/mail/configuration
- Provider + Processor relient le DTO a l'entite MailConfiguration (singleton)
- password en write-only (jamais retourne) + hasPassword en lecture
- chiffrement password via TokenEncryptor (sodium)
- securite ROLE_ADMIN sur les deux operations
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- ajoute symfony/messenger ^8.0 et symfony/doctrine-messenger ^8.0 pour la sync mail async
- ajoute symfony/browser-kit + css-selector en dev pour tests fonctionnels WebTestCase
- ENCRYPTION_KEY ajoutee dans phpunit.dist.xml pour permettre le chiffrement en test
- MESSENGER_TRANSPORT_DSN configure (Doctrine), messenger.yaml minimal (sera enrichi en Task 12)
- fix(orm) : ClientTicket - migre uniqueConstraints en attribut separe (Doctrine ORM 4 deprecation)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Backend :
- POST /api/me/regenerate-api-token : nouveau controller, ROLE_USER (exclut CLIENT)
- User.apiToken exposé via groupe me:read sur GET /api/me
Frontend :
- Section 'Token API MCP' sur /profile (masquée pour les CLIENT du portail)
- Boutons Copier + Régénérer avec modal de confirmation
- Service api-token + DTO mis à jour + clés i18n fr
Avec MalioInputRichText qui émet désormais du HTML par défaut,
plusieurs points d'affichage rendaient les balises brutes au
lieu du texte. Ajoute un helper stripRichText() (frontend) et
descriptionToPlainText() (backend) pour neutraliser ces cas.
- TimeEntryList : strip avant truncate dans la liste des time
entries.
- ProjectGroupTab : strip dans la cellule description du
tableau des groupes.
- CalDavService : strip_tags + html_entity_decode avant injection
dans le DESCRIPTION VEVENT/VTODO iCal (sinon Outlook/Apple
Calendar affichaient les <p>...</p> à l'utilisateur).
Co-Authored-By: RuFlo <ruv@ruv.net>
Client.id/name and Project.id/name were missing the user:list group,
causing them to be serialized as IRI strings instead of embedded objects.
This broke the user edit form which expected object properties.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
GetCollection/Get required ROLE_USER which ROLE_CLIENT doesn't have.
Added TaskDocumentProvider to scope client access to their own tickets.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>