# Mail Integration — Phase 1 : Backend Foundations > **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:** Créer les fondations BDD + interfaces de l'intégration mail OVH IMAP (4 entités, 4 repositories, 1 migration, 4 DTOs, 1 interface, 1 exception, 1 fixture, tests). Aucune logique métier. **Architecture:** Singleton `MailConfiguration` (calqué sur `ZimbraConfiguration`), `MailFolder` (cache arbre IMAP), `MailMessage` (cache headers + snippet), `TaskMailLink` (join). Interface `MailProviderInterface` définit le contrat — implémentation en Phase 2. **Tech Stack:** PHP 8.4, Symfony 8.0, Doctrine ORM avec attributs PHP 8, PostgreSQL 16, PHPUnit + DAMA DoctrineTestBundle. **Branche cible :** `feat/mail-integration` (créer si elle n'existe pas). **Fichiers créés/modifiés par le codeur :** | Fichier | Action | |---|---| | `src/Entity/MailConfiguration.php` | Créer | | `src/Entity/MailFolder.php` | Créer | | `src/Entity/MailMessage.php` | Créer | | `src/Entity/TaskMailLink.php` | Créer | | `src/Repository/MailConfigurationRepository.php` | Créer | | `src/Repository/MailFolderRepository.php` | Créer | | `src/Repository/MailMessageRepository.php` | Créer | | `src/Repository/TaskMailLinkRepository.php` | Créer | | `src/Mail/MailProviderInterface.php` | Créer | | `src/Mail/Exception/MailProviderException.php` | Créer | | `src/Mail/Dto/MailFolderDto.php` | Créer | | `src/Mail/Dto/MailMessageHeaderDto.php` | Créer | | `src/Mail/Dto/MailAttachmentDto.php` | Créer | | `src/Mail/Dto/MailMessageDetailDto.php` | Créer | | `migrations/Version.php` | Créer | | `src/DataFixtures/AppFixtures.php` | Modifier | | `tests/Unit/Repository/MailConfigurationRepositoryTest.php` | Créer | --- ### Task 1 : Préparation — branche + dossiers - [ ] **Step 1 : Vérifier / créer la branche** ```bash git checkout feat/mail-integration 2>/dev/null || git checkout -b feat/mail-integration ``` - [ ] **Step 2 : Créer les dossiers namespace** ```bash mkdir -p src/Mail/Dto src/Mail/Exception mkdir -p tests/Unit/Repository ``` - [ ] **Step 3 : Vérifier que les dossiers sont dans l'autoloader Composer** Ouvrir `composer.json` et confirmer que `"App\\": "src/"` est présent dans `autoload.psr-4`. Si oui, rien à faire (les sous-dossiers `src/Mail/` sont automatiquement couverts). --- ### Task 2 : Entité `MailConfiguration` + Repository + test singleton #### Step 1 : Écrire le test (TDD — il doit échouer) Créer `tests/Unit/Repository/MailConfigurationRepositoryTest.php` : ```php repository = $container->get(MailConfigurationRepository::class); } public function testFindSingletonReturnsNullWhenEmpty(): void { $result = $this->repository->findSingleton(); self::assertNull($result); } public function testFindSingletonReturnsFirstRecord(): void { $container = static::getContainer(); $em = $container->get('doctrine.orm.entity_manager'); $config = new MailConfiguration(); $config->setImapHost('ssl0.ovh.net'); $config->setEnabled(false); $em->persist($config); $em->flush(); $result = $this->repository->findSingleton(); self::assertInstanceOf(MailConfiguration::class, $result); self::assertSame('ssl0.ovh.net', $result->getImapHost()); self::assertFalse($result->isEnabled()); } } ``` - [ ] **Step 2 : Lancer le test pour vérifier qu'il échoue** ```bash docker exec php-lesstime-fpm php bin/phpunit tests/Unit/Repository/MailConfigurationRepositoryTest.php 2>&1 | tail -20 ``` Attendu : erreur de classe manquante (`App\Entity\MailConfiguration`). - [ ] **Step 3 : Créer l'entité `MailConfiguration`** Créer `src/Entity/MailConfiguration.php` : ```php id; } public function getProtocol(): string { return $this->protocol; } public function setProtocol(string $protocol): static { $this->protocol = $protocol; return $this; } public function getImapHost(): ?string { return $this->imapHost; } public function setImapHost(?string $imapHost): static { $this->imapHost = $imapHost; return $this; } public function getImapPort(): int { return $this->imapPort; } public function setImapPort(int $imapPort): static { $this->imapPort = $imapPort; return $this; } public function getImapEncryption(): string { return $this->imapEncryption; } public function setImapEncryption(string $imapEncryption): static { $this->imapEncryption = $imapEncryption; return $this; } public function getSmtpHost(): ?string { return $this->smtpHost; } public function setSmtpHost(?string $smtpHost): static { $this->smtpHost = $smtpHost; return $this; } public function getSmtpPort(): int { return $this->smtpPort; } public function setSmtpPort(int $smtpPort): static { $this->smtpPort = $smtpPort; return $this; } public function getSmtpEncryption(): string { return $this->smtpEncryption; } public function setSmtpEncryption(string $smtpEncryption): static { $this->smtpEncryption = $smtpEncryption; return $this; } public function getUsername(): ?string { return $this->username; } public function setUsername(?string $username): static { $this->username = $username; return $this; } public function getEncryptedPassword(): ?string { return $this->encryptedPassword; } public function setEncryptedPassword(?string $encryptedPassword): static { $this->encryptedPassword = $encryptedPassword; return $this; } public function getSentFolderPath(): string { return $this->sentFolderPath; } public function setSentFolderPath(string $sentFolderPath): static { $this->sentFolderPath = $sentFolderPath; return $this; } public function isEnabled(): bool { return $this->enabled; } public function setEnabled(bool $enabled): static { $this->enabled = $enabled; return $this; } public function hasPassword(): bool { return null !== $this->encryptedPassword; } } ``` - [ ] **Step 4 : Créer le repository `MailConfigurationRepository`** Créer `src/Repository/MailConfigurationRepository.php` : ```php createQueryBuilder('m') ->setMaxResults(1) ->getQuery() ->getOneOrNullResult() ; } } ``` - [ ] **Step 5 : Lancer le test pour vérifier qu'il passe** ```bash docker exec php-lesstime-fpm php bin/phpunit tests/Unit/Repository/MailConfigurationRepositoryTest.php 2>&1 | tail -20 ``` Attendu : `OK (2 tests, 2 assertions)`. > Note : si le test `testFindSingletonReturnsNullWhenEmpty` échoue car des données persistent entre tests, vérifier que `DAMA\DoctrineTestBundle\Doctrine\DBAL\StaticDriver` est bien configuré dans `config/packages/test/dama_doctrine_test_bundle.yaml`. Si non configuré, ajouter le `@runInSeparateProcess` en dernier recours — mais normalement le projet utilise déjà DAMA. - [ ] **Step 6 : Commit** ```bash make php-cs-fixer-allow-risky git add src/Entity/MailConfiguration.php src/Repository/MailConfigurationRepository.php tests/Unit/Repository/MailConfigurationRepositoryTest.php git commit -m "feat(mail) : MailConfiguration entity + repository + singleton test" ``` --- ### Task 3 : Entité `MailFolder` + Repository - [ ] **Step 1 : Créer l'entité `MailFolder`** Créer `src/Entity/MailFolder.php` : ```php id; } public function getPath(): string { return $this->path; } public function setPath(string $path): static { $this->path = $path; return $this; } public function getDisplayName(): string { return $this->displayName; } public function setDisplayName(string $displayName): static { $this->displayName = $displayName; return $this; } public function getParentPath(): ?string { return $this->parentPath; } public function setParentPath(?string $parentPath): static { $this->parentPath = $parentPath; return $this; } public function getUnreadCount(): int { return $this->unreadCount; } public function setUnreadCount(int $unreadCount): static { $this->unreadCount = $unreadCount; return $this; } public function getTotalCount(): int { return $this->totalCount; } public function setTotalCount(int $totalCount): static { $this->totalCount = $totalCount; return $this; } public function getLastSyncedAt(): ?DateTimeImmutable { return $this->lastSyncedAt; } public function setLastSyncedAt(?DateTimeImmutable $lastSyncedAt): static { $this->lastSyncedAt = $lastSyncedAt; return $this; } } ``` - [ ] **Step 2 : Créer le repository `MailFolderRepository`** Créer `src/Repository/MailFolderRepository.php` : ```php */ public function findAllOrderedByPath(): array { return $this->createQueryBuilder('f') ->orderBy('f.path', 'ASC') ->getQuery() ->getResult() ; } public function findByPath(string $path): ?MailFolder { return $this->findOneBy(['path' => $path]); } } ``` - [ ] **Step 3 : Vérifier la syntaxe PHP** ```bash docker exec php-lesstime-fpm php -l src/Entity/MailFolder.php docker exec php-lesstime-fpm php -l src/Repository/MailFolderRepository.php ``` Attendu : `No syntax errors detected`. - [ ] **Step 4 : Commit** ```bash make php-cs-fixer-allow-risky git add src/Entity/MailFolder.php src/Repository/MailFolderRepository.php git commit -m "feat(mail) : MailFolder entity + repository" ``` --- ### Task 4 : Entité `MailMessage` + Repository - [ ] **Step 1 : Créer l'entité `MailMessage`** Créer `src/Entity/MailMessage.php` : ```php id; } public function getMessageId(): string { return $this->messageId; } public function setMessageId(string $messageId): static { $this->messageId = $messageId; return $this; } public function getFolder(): MailFolder { return $this->folder; } public function setFolder(MailFolder $folder): static { $this->folder = $folder; return $this; } public function getUid(): int { return $this->uid; } public function setUid(int $uid): static { $this->uid = $uid; return $this; } public function getSubject(): ?string { return $this->subject; } public function setSubject(?string $subject): static { $this->subject = $subject; return $this; } public function getFromAddress(): string { return $this->fromAddress; } public function setFromAddress(string $fromAddress): static { $this->fromAddress = $fromAddress; return $this; } public function getFromName(): ?string { return $this->fromName; } public function setFromName(?string $fromName): static { $this->fromName = $fromName; return $this; } public function getToAddresses(): array { return $this->toAddresses; } public function setToAddresses(array $toAddresses): static { $this->toAddresses = $toAddresses; return $this; } public function getCcAddresses(): ?array { return $this->ccAddresses; } public function setCcAddresses(?array $ccAddresses): static { $this->ccAddresses = $ccAddresses; return $this; } public function getSentAt(): DateTimeImmutable { return $this->sentAt; } public function setSentAt(DateTimeImmutable $sentAt): static { $this->sentAt = $sentAt; return $this; } public function isRead(): bool { return $this->isRead; } public function setIsRead(bool $isRead): static { $this->isRead = $isRead; return $this; } public function isFlagged(): bool { return $this->isFlagged; } public function setIsFlagged(bool $isFlagged): static { $this->isFlagged = $isFlagged; return $this; } public function hasAttachments(): bool { return $this->hasAttachments; } public function setHasAttachments(bool $hasAttachments): static { $this->hasAttachments = $hasAttachments; return $this; } public function getSnippet(): ?string { return $this->snippet; } public function setSnippet(?string $snippet): static { $this->snippet = $snippet; return $this; } public function getSyncedAt(): DateTimeImmutable { return $this->syncedAt; } public function setSyncedAt(DateTimeImmutable $syncedAt): static { $this->syncedAt = $syncedAt; return $this; } } ``` - [ ] **Step 2 : Créer le repository `MailMessageRepository`** Créer `src/Repository/MailMessageRepository.php` : ```php findOneBy(['messageId' => $messageId]); } public function findByFolderAndUid(MailFolder $folder, int $uid): ?MailMessage { return $this->findOneBy(['folder' => $folder, 'uid' => $uid]); } /** * @return list */ public function findByFolderPaginated(MailFolder $folder, int $limit, int $offset): array { return $this->createQueryBuilder('m') ->andWhere('m.folder = :folder') ->setParameter('folder', $folder) ->orderBy('m.sentAt', 'DESC') ->addOrderBy('m.id', 'DESC') ->setMaxResults($limit) ->setFirstResult($offset) ->getQuery() ->getResult() ; } public function countUnreadByFolder(MailFolder $folder): int { return (int) $this->createQueryBuilder('m') ->select('COUNT(m.id)') ->andWhere('m.folder = :folder') ->andWhere('m.isRead = false') ->setParameter('folder', $folder) ->getQuery() ->getSingleScalarResult() ; } } ``` - [ ] **Step 3 : Vérifier la syntaxe PHP** ```bash docker exec php-lesstime-fpm php -l src/Entity/MailMessage.php docker exec php-lesstime-fpm php -l src/Repository/MailMessageRepository.php ``` Attendu : `No syntax errors detected`. - [ ] **Step 4 : Commit** ```bash make php-cs-fixer-allow-risky git add src/Entity/MailMessage.php src/Repository/MailMessageRepository.php git commit -m "feat(mail) : MailMessage entity + repository" ``` --- ### Task 5 : Entité `TaskMailLink` + Repository - [ ] **Step 1 : Créer l'entité `TaskMailLink`** Créer `src/Entity/TaskMailLink.php` : ```php id; } public function getTask(): Task { return $this->task; } public function setTask(Task $task): static { $this->task = $task; return $this; } public function getMailMessage(): MailMessage { return $this->mailMessage; } public function setMailMessage(MailMessage $mailMessage): static { $this->mailMessage = $mailMessage; return $this; } public function getLinkedAt(): DateTimeImmutable { return $this->linkedAt; } public function setLinkedAt(DateTimeImmutable $linkedAt): static { $this->linkedAt = $linkedAt; return $this; } public function getLinkedBy(): ?User { return $this->linkedBy; } public function setLinkedBy(?User $linkedBy): static { $this->linkedBy = $linkedBy; return $this; } } ``` - [ ] **Step 2 : Créer le repository `TaskMailLinkRepository`** Créer `src/Repository/TaskMailLinkRepository.php` : ```php */ public function findByTask(Task $task): array { return $this->createQueryBuilder('l') ->andWhere('l.task = :task') ->setParameter('task', $task) ->orderBy('l.linkedAt', 'DESC') ->getQuery() ->getResult() ; } public function findByTaskAndMessage(Task $task, MailMessage $message): ?TaskMailLink { return $this->findOneBy(['task' => $task, 'mailMessage' => $message]); } /** * @return list */ public function findByMessage(MailMessage $message): array { return $this->findBy(['mailMessage' => $message]); } } ``` - [ ] **Step 3 : Vérifier la syntaxe PHP** ```bash docker exec php-lesstime-fpm php -l src/Entity/TaskMailLink.php docker exec php-lesstime-fpm php -l src/Repository/TaskMailLinkRepository.php ``` Attendu : `No syntax errors detected`. - [ ] **Step 4 : Commit** ```bash make php-cs-fixer-allow-risky git add src/Entity/TaskMailLink.php src/Repository/TaskMailLinkRepository.php git commit -m "feat(mail) : TaskMailLink entity + repository" ``` --- ### Task 6 : Migration Doctrine unique (raw SQL, 4 tables + FK + index) - [ ] **Step 1 : Générer le squelette de migration vide** ```bash docker exec php-lesstime-fpm php bin/console doctrine:migrations:generate ``` Cela crée `migrations/Version.php`. Relever le nom exact du fichier créé (ex. `Version20260519120000.php`). - [ ] **Step 2 : Écrire le SQL UP complet dans la migration générée** Ouvrir le fichier `migrations/Version.php` et remplacer les méthodes `up()` et `down()` par : ```php public function getDescription(): string { return 'Mail integration: create mail_configuration, mail_folder, mail_message, task_mail_link tables'; } public function up(Schema $schema): void { $this->addSql(<<<'SQL' CREATE TABLE mail_configuration ( id SERIAL NOT NULL, protocol VARCHAR(10) NOT NULL DEFAULT 'imap', imap_host VARCHAR(255) DEFAULT NULL, imap_port INT NOT NULL DEFAULT 993, imap_encryption VARCHAR(10) NOT NULL DEFAULT 'ssl', smtp_host VARCHAR(255) DEFAULT NULL, smtp_port INT NOT NULL DEFAULT 465, smtp_encryption VARCHAR(10) NOT NULL DEFAULT 'ssl', username VARCHAR(255) DEFAULT NULL, encrypted_password TEXT DEFAULT NULL, sent_folder_path VARCHAR(255) NOT NULL DEFAULT 'Sent', enabled BOOLEAN NOT NULL DEFAULT false, PRIMARY KEY (id) ) SQL); $this->addSql(<<<'SQL' CREATE TABLE mail_folder ( id SERIAL NOT NULL, path VARCHAR(500) NOT NULL, display_name VARCHAR(255) NOT NULL, parent_path VARCHAR(500) DEFAULT NULL, unread_count INT NOT NULL DEFAULT 0, total_count INT NOT NULL DEFAULT 0, last_synced_at TIMESTAMPTZ DEFAULT NULL, PRIMARY KEY (id) ) SQL); $this->addSql('CREATE UNIQUE INDEX uq_mail_folder_path ON mail_folder (path)'); $this->addSql('CREATE INDEX idx_mail_folder_parent_path ON mail_folder (parent_path)'); $this->addSql(<<<'SQL' CREATE TABLE mail_message ( id SERIAL NOT NULL, message_id VARCHAR(500) NOT NULL, folder_id INT NOT NULL, uid INT NOT NULL, subject VARCHAR(500) DEFAULT NULL, from_address VARCHAR(255) NOT NULL, from_name VARCHAR(255) DEFAULT NULL, to_addresses JSONB NOT NULL, cc_addresses JSONB DEFAULT NULL, sent_at TIMESTAMPTZ NOT NULL, is_read BOOLEAN NOT NULL DEFAULT false, is_flagged BOOLEAN NOT NULL DEFAULT false, has_attachments BOOLEAN NOT NULL DEFAULT false, snippet TEXT DEFAULT NULL, synced_at TIMESTAMPTZ NOT NULL, PRIMARY KEY (id) ) SQL); $this->addSql('CREATE UNIQUE INDEX uq_mail_message_message_id ON mail_message (message_id)'); $this->addSql('CREATE UNIQUE INDEX uq_mail_message_folder_uid ON mail_message (folder_id, uid)'); $this->addSql('CREATE INDEX idx_mail_message_sent_at ON mail_message (sent_at)'); $this->addSql('CREATE INDEX idx_mail_message_is_read ON mail_message (is_read)'); $this->addSql('ALTER TABLE mail_message ADD CONSTRAINT fk_mail_message_folder FOREIGN KEY (folder_id) REFERENCES mail_folder (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); $this->addSql(<<<'SQL' CREATE TABLE task_mail_link ( id SERIAL NOT NULL, task_id INT NOT NULL, mail_message_id INT NOT NULL, linked_at TIMESTAMPTZ NOT NULL, linked_by_id INT DEFAULT NULL, PRIMARY KEY (id) ) SQL); $this->addSql('CREATE UNIQUE INDEX uq_task_mail_link ON task_mail_link (task_id, mail_message_id)'); $this->addSql('CREATE INDEX idx_task_mail_link_task ON task_mail_link (task_id)'); $this->addSql('CREATE INDEX idx_task_mail_link_message ON task_mail_link (mail_message_id)'); $this->addSql('ALTER TABLE task_mail_link ADD CONSTRAINT fk_task_mail_link_task FOREIGN KEY (task_id) REFERENCES task (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); $this->addSql('ALTER TABLE task_mail_link ADD CONSTRAINT fk_task_mail_link_message FOREIGN KEY (mail_message_id) REFERENCES mail_message (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); $this->addSql('ALTER TABLE task_mail_link ADD CONSTRAINT fk_task_mail_link_user FOREIGN KEY (linked_by_id) REFERENCES "user" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE'); } public function down(Schema $schema): void { $this->addSql('ALTER TABLE task_mail_link DROP CONSTRAINT fk_task_mail_link_task'); $this->addSql('ALTER TABLE task_mail_link DROP CONSTRAINT fk_task_mail_link_message'); $this->addSql('ALTER TABLE task_mail_link DROP CONSTRAINT fk_task_mail_link_user'); $this->addSql('DROP TABLE task_mail_link'); $this->addSql('ALTER TABLE mail_message DROP CONSTRAINT fk_mail_message_folder'); $this->addSql('DROP TABLE mail_message'); $this->addSql('DROP TABLE mail_folder'); $this->addSql('DROP TABLE mail_configuration'); } ``` > Attention : `declare(strict_types=1);` doit être présent en tête du fichier (il est présent dans le squelette généré par Doctrine). - [ ] **Step 3 : Lancer la migration** ```bash make migration-migrate ``` Attendu : migration exécutée sans erreur SQL. - [ ] **Step 4 : Valider le schéma Doctrine** ```bash docker exec php-lesstime-fpm php bin/console doctrine:schema:validate ``` Attendu : `[Mapping] OK - The mapping files are correct.` et `[Database] OK - The database schema is in sync with the mapping files.` > Si des différences sont signalées (ex. type `json` vs `jsonb`), ajuster l'attribut `#[ORM\Column(type: 'json')]` en `#[ORM\Column(type: 'jsonb')]` dans `MailMessage.php` si le mapping Doctrine du projet est configuré pour `jsonb`. Dans le doute, `json` est le type Doctrine standard et PostgreSQL accepte les deux. - [ ] **Step 5 : Commit** ```bash make php-cs-fixer-allow-risky git add migrations/ git commit -m "feat(mail) : migration — 4 tables mail_configuration, mail_folder, mail_message, task_mail_link" ``` --- ### Task 7 : DTOs (4 fichiers sous `src/Mail/Dto/`) - [ ] **Step 1 : Créer `MailFolderDto`** Créer `src/Mail/Dto/MailFolderDto.php` : ```php $attachments */ public function __construct( public MailMessageHeaderDto $header, public ?string $bodyHtml, public ?string $bodyText, public array $attachments, ) {} } ``` - [ ] **Step 5 : Vérifier la syntaxe des 4 DTOs** ```bash docker exec php-lesstime-fpm php -l src/Mail/Dto/MailFolderDto.php docker exec php-lesstime-fpm php -l src/Mail/Dto/MailMessageHeaderDto.php docker exec php-lesstime-fpm php -l src/Mail/Dto/MailAttachmentDto.php docker exec php-lesstime-fpm php -l src/Mail/Dto/MailMessageDetailDto.php ``` Attendu : `No syntax errors detected` pour chacun. - [ ] **Step 6 : Commit** ```bash make php-cs-fixer-allow-risky git add src/Mail/Dto/ git commit -m "feat(mail) : DTOs — MailFolderDto, MailMessageHeaderDto, MailAttachmentDto, MailMessageDetailDto" ``` --- ### Task 8 : Interface `MailProviderInterface` + Exception `MailProviderException` - [ ] **Step 1 : Créer `MailProviderException`** Créer `src/Mail/Exception/MailProviderException.php` : ```php * * @throws MailProviderException */ public function listFolders(): array; /** * Returns a paginated list of message headers for the given folder. * * @return list * * @throws MailProviderException */ public function listMessages(string $folderPath, int $limit, int $offset): array; /** * Fetches the full message (headers + body + attachments list) by UID. * * @throws MailProviderException */ public function fetchMessage(string $folderPath, int $uid): MailMessageDetailDto; /** * Marks a message as read or unread on the IMAP server. * * @throws MailProviderException */ public function markRead(string $folderPath, int $uid, bool $read): void; /** * Marks a message as flagged (starred) or unflagged on the IMAP server. * * @throws MailProviderException */ public function markFlagged(string $folderPath, int $uid, bool $flagged): void; /** * Moves a message from one folder to another on the IMAP server. * * @throws MailProviderException */ public function moveMessage(string $folderPath, int $uid, string $targetFolder): void; /** * Fetches the raw binary content of an attachment by its MIME part number. * * @throws MailProviderException */ public function fetchAttachment(string $folderPath, int $uid, string $partNumber): string; } ``` - [ ] **Step 3 : Vérifier la syntaxe** ```bash docker exec php-lesstime-fpm php -l src/Mail/Exception/MailProviderException.php docker exec php-lesstime-fpm php -l src/Mail/MailProviderInterface.php ``` Attendu : `No syntax errors detected`. - [ ] **Step 4 : Commit** ```bash make php-cs-fixer-allow-risky git add src/Mail/MailProviderInterface.php src/Mail/Exception/MailProviderException.php git commit -m "feat(mail) : MailProviderInterface + MailProviderException" ``` --- ### Task 9 : Fixture `MailConfiguration` désactivée (OVH defaults) - [ ] **Step 1 : Modifier `AppFixtures.php`** Ouvrir `src/DataFixtures/AppFixtures.php`. Ajouter l'import en tête du bloc `use` (après `use App\Entity\ZimbraConfiguration;`) : ```php use App\Entity\MailConfiguration; ``` Puis ajouter le bloc fixture juste **avant** `$manager->flush();` (après le bloc TaskRecurrence existant) : ```php // ============================================= // Mail Configuration // ============================================= $mailConfig = new MailConfiguration(); $mailConfig->setImapHost('ssl0.ovh.net'); $mailConfig->setImapPort(993); $mailConfig->setImapEncryption('ssl'); $mailConfig->setSmtpHost('ssl0.ovh.net'); $mailConfig->setSmtpPort(465); $mailConfig->setSmtpEncryption('ssl'); $mailConfig->setUsername('lesstime@ovh.fr'); $mailConfig->setSentFolderPath('Sent'); $mailConfig->setEnabled(false); $manager->persist($mailConfig); ``` - [ ] **Step 2 : Vérifier la syntaxe** ```bash docker exec php-lesstime-fpm php -l src/DataFixtures/AppFixtures.php ``` Attendu : `No syntax errors detected`. - [ ] **Step 3 : Charger les fixtures pour valider** ```bash make fixtures ``` Attendu : pas d'erreur. Vérifier en BDD : ```bash docker exec php-lesstime-fpm php bin/console dbal:run-sql "SELECT id, imap_host, smtp_host, enabled FROM mail_configuration" ``` Attendu : une ligne avec `ssl0.ovh.net`, `ssl0.ovh.net`, `f`. - [ ] **Step 4 : Commit** ```bash make php-cs-fixer-allow-risky git add src/DataFixtures/AppFixtures.php git commit -m "feat(mail) : fixture MailConfiguration OVH defaults (disabled)" ``` --- ### Task 10 : Validation finale - [ ] **Step 1 : Valider le mapping Doctrine** ```bash docker exec php-lesstime-fpm php bin/console doctrine:schema:validate ``` Attendu : ``` [Mapping] OK - The mapping files are correct. [Database] OK - The database schema is in sync with the mapping files. ``` Si des erreurs apparaissent, les corriger (type mismatch `json` vs `jsonb`, colonnes manquantes, etc.) avant de continuer. - [ ] **Step 2 : Lancer la suite de tests complète** ```bash make test ``` Attendu : tous les tests passent (au minimum les 2 tests `MailConfigurationRepositoryTest`). - [ ] **Step 3 : PHP CS Fixer sur l'ensemble des fichiers modifiés** ```bash make php-cs-fixer-allow-risky ``` Si des fichiers sont modifiés par le fixer, les re-stageer et committer : ```bash git add -p git commit -m "style(mail) : php-cs-fixer pass" ``` - [ ] **Step 4 : Vider le cache Symfony** ```bash make cache-clear ``` Attendu : pas d'erreur de configuration ou de service manquant. - [ ] **Step 5 : Vérification rapide du conteneur de services** ```bash docker exec php-lesstime-fpm php bin/console debug:autowiring MailConfiguration 2>&1 | head -20 ``` Attendu : `App\Repository\MailConfigurationRepository` apparaît dans la liste des services autowirables. - [ ] **Step 6 : Résumé des commits de la phase** Vérifier l'historique : ```bash git log --oneline feat/mail-integration ^develop | head -20 ``` Les commits attendus (dans l'ordre chronologique) : 1. `feat(mail) : MailConfiguration entity + repository + singleton test` 2. `feat(mail) : MailFolder entity + repository` 3. `feat(mail) : MailMessage entity + repository` 4. `feat(mail) : TaskMailLink entity + repository` 5. `feat(mail) : migration — 4 tables mail_configuration, mail_folder, mail_message, task_mail_link` 6. `feat(mail) : DTOs — MailFolderDto, MailMessageHeaderDto, MailAttachmentDto, MailMessageDetailDto` 7. `feat(mail) : MailProviderInterface + MailProviderException` 8. `feat(mail) : fixture MailConfiguration OVH defaults (disabled)` --- ### Self-Review #### Cohérence des noms | Concept | Classe PHP | Table SQL | Repository | |---|---|---|---| | Configuration singleton | `MailConfiguration` | `mail_configuration` | `MailConfigurationRepository` | | Cache dossiers IMAP | `MailFolder` | `mail_folder` | `MailFolderRepository` | | Cache messages IMAP | `MailMessage` | `mail_message` | `MailMessageRepository` | | Lien tâche ↔ mail | `TaskMailLink` | `task_mail_link` | `TaskMailLinkRepository` | | DTO dossier | `MailFolderDto` | — | — | | DTO header message | `MailMessageHeaderDto` | — | — | | DTO pièce jointe | `MailAttachmentDto` | — | — | | DTO message complet | `MailMessageDetailDto` | — | — | | Interface provider | `MailProviderInterface` | — | — | | Exception provider | `MailProviderException` | — | — | #### Checklist finale avant de valider Phase 1 - [ ] `declare(strict_types=1);` présent en tête de chaque fichier PHP créé - [ ] Toutes les FK définies avec `ON DELETE CASCADE` (sauf `task_mail_link.linked_by_id` → `ON DELETE SET NULL`) - [ ] `UNIQUE INDEX` sur `mail_folder.path`, `mail_message.message_id`, `(mail_message.folder_id, mail_message.uid)`, `(task_mail_link.task_id, task_mail_link.mail_message_id)` - [ ] `INDEX` sur `mail_folder.parent_path`, `mail_message.sent_at`, `mail_message.is_read` - [ ] Table `"user"` entre guillemets dans le SQL de la migration (réservé PostgreSQL) - [ ] `MailConfiguration.encryptedPassword` est `null` par défaut en fixture (pas de mot de passe en clair) - [ ] DTOs déclarés `final readonly class` (PHP 8.2+) - [ ] `MailProviderInterface` contient exactement 7 méthodes : `listFolders`, `listMessages`, `fetchMessage`, `markRead`, `markFlagged`, `moveMessage`, `fetchAttachment` - [ ] `make test` vert (2 tests minimum) - [ ] `doctrine:schema:validate` OK - [ ] Aucune logique métier IMAP implémentée (Phase 2) - [ ] `feat/mail-integration` est la branche de travail (pas `develop`) #### Remise à la review humaine Une fois tous les steps cochés, pousser la branche et notifier le user : ```bash git push -u origin feat/mail-integration ``` Indiquer au user : - Nombre de fichiers créés : 17 - Tables créées : 4 (`mail_configuration`, `mail_folder`, `mail_message`, `task_mail_link`) - Tests : 2 (MailConfigurationRepositoryTest) - Prêt pour Phase 2 : IMAP provider + MailSyncService