diff --git a/frontend/services/mail.ts b/frontend/services/mail.ts new file mode 100644 index 0000000..496a750 --- /dev/null +++ b/frontend/services/mail.ts @@ -0,0 +1,214 @@ +import type { + MailConfigurationDto, + MailConfigurationUpdateDto, + MailTestConnectionResultDto, + MailFolderDto, + MailMessageHeaderDto, + MailMessageDetailDto, + MailMessagesPageDto, + MailMessageReadInput, + MailMessageFlagInput, + MailCreateTaskInput, + MailLinkTaskInput, + MailSyncResultDto, +} from './dto/mail' +import type { Task } from './dto/task' + +export function useMailService() { + const api = useApi() + + // ─── Configuration (Admin) ──────────────────────────────────────────────── + + /** + * Récupère la configuration mail singleton. + * Requiert ROLE_ADMIN — 403 sinon. + */ + async function getConfiguration(): Promise { + return api.get('/mail/configuration') + } + + /** + * Met à jour la configuration mail (PATCH merge). + * Si payload.password est fourni, il sera chiffré côté backend. + * Jamais retourné en clair dans la réponse. + */ + async function updateConfiguration( + payload: MailConfigurationUpdateDto, + ): Promise { + return api.patch( + '/mail/configuration', + payload as Record, + { toastSuccessKey: 'mail.configuration.saved' }, + ) + } + + /** + * Teste la connexion IMAP avec la configuration actuelle. + * Requiert ROLE_ADMIN. + */ + async function testConfiguration(): Promise { + return api.post('/mail/configuration/test', {}) + } + + // ─── Dossiers ───────────────────────────────────────────────────────────── + + /** + * Liste tous les dossiers mail depuis la base (cache BDD, pas live IMAP). + * Retourne une liste plate — la construction de l'arbre est faite dans le store + * via le getter `folderTree`. + */ + async function listFolders(): Promise { + return api.get('/mail/folders') + } + + // ─── Messages ───────────────────────────────────────────────────────────── + + /** + * Liste les messages d'un dossier, paginés par cursor. + * @param folderPath - Chemin du dossier (ex: "INBOX", "INBOX.Sent") + * @param cursor - Opaque cursor retourné par la page précédente (undefined = première page) + * @param limit - Nombre de messages par page (défaut backend : 50) + */ + async function listMessages( + folderPath: string, + cursor?: string, + limit?: number, + ): Promise { + const query: Record = { folder: folderPath } + if (cursor) query.cursor = cursor + if (limit) query.limit = limit + return api.get('/mail/messages', query) + } + + /** + * Récupère le détail complet d'un message (body live IMAP, cached 5 min). + * @param id - ID BDD du message (MailMessage.id) + */ + async function getMessage(id: number): Promise { + return api.get(`/mail/messages/${id}`) + } + + // ─── Actions sur les messages ───────────────────────────────────────────── + + /** + * Marque un message comme lu ou non-lu. + */ + async function markRead(id: number, read: boolean): Promise { + const payload: MailMessageReadInput = { read } + return api.post( + `/mail/messages/${id}/read`, + payload as unknown as Record, + ) + } + + /** + * Marque un message comme étoilé ou non-étoilé. + */ + async function markFlagged(id: number, flagged: boolean): Promise { + const payload: MailMessageFlagInput = { flagged } + return api.post( + `/mail/messages/${id}/flag`, + payload as unknown as Record, + ) + } + + // ─── Intégration tâches ─────────────────────────────────────────────────── + + /** + * Crée une nouvelle tâche à partir d'un mail (subject → titre, body → description). + * @param mailId - ID BDD du message + * @param input - Paramètres de la tâche à créer + */ + async function createTaskFromMail( + mailId: number, + input: MailCreateTaskInput, + ): Promise { + return api.post( + `/mail/messages/${mailId}/create-task`, + input as unknown as Record, + { toastSuccessKey: 'mail.task.created' }, + ) + } + + /** + * Lie un mail à une tâche existante. + * @param mailId - ID BDD du message + * @param taskId - ID de la tâche existante + */ + async function linkTask(mailId: number, taskId: number): Promise { + const payload: MailLinkTaskInput = { taskId } + await api.post( + `/mail/messages/${mailId}/link-task`, + payload as unknown as Record, + { toastSuccessKey: 'mail.task.linked' }, + ) + } + + /** + * Supprime le lien entre un mail et une tâche. + * @param mailId - ID BDD du message + * @param taskId - ID de la tâche + */ + async function unlinkTask(mailId: number, taskId: number): Promise { + await api.delete(`/mail/messages/${mailId}/link-task/${taskId}`, {}, { + toastSuccessKey: 'mail.task.unlinked', + }) + } + + /** + * Liste les mails liés à une tâche (pour l'onglet "Mails" du TaskDrawer — Phase 6). + * @param taskId - ID de la tâche + */ + async function listMailsForTask(taskId: number): Promise { + return api.get(`/tasks/${taskId}/mails`) + } + + // ─── Pièces jointes ─────────────────────────────────────────────────────── + + /** + * Télécharge une pièce jointe et retourne le Blob + headers. + * Content-Disposition: attachment est géré côté backend (jamais inline). + * @param downloadId - Identifiant opaque retourné dans MailAttachmentDto.downloadId + */ + async function downloadAttachment( + downloadId: string, + ): Promise<{ data: Blob; headers: Headers }> { + return api.getBlob(`/mail/attachments/${downloadId}`) + } + + // ─── Synchronisation ───────────────────────────────────────────────────── + + /** + * Déclenche une synchronisation IMAP asynchrone via Symfony Messenger. + * Retourne immédiatement ({ dispatched: true }) — la sync se fait en arrière-plan. + */ + async function triggerSync(): Promise { + return api.post('/mail/sync', {}, { + toastSuccessKey: 'mail.sync.dispatched', + }) + } + + return { + // Config + getConfiguration, + updateConfiguration, + testConfiguration, + // Dossiers + listFolders, + // Messages + listMessages, + getMessage, + // Actions + markRead, + markFlagged, + // Tâches + createTaskFromMail, + linkTask, + unlinkTask, + listMailsForTask, + // Pièces jointes + downloadAttachment, + // Sync + triggerSync, + } +}