feat(email): add 'Test connection' to incoming mail + tidy IMAP label

- emailIntakeService.testConnection(): logs in, opens the configured folder,
  reports message/unread counts (non-destructive). Accepts current form creds
  so it works before saving; masked password falls back to stored.
- route POST /admin/email/incoming-config/test
- IMAP card: 'Test connection' button beside Save; toast shows folder + counts
- capitalize 'IMAP Host' label to match 'SMTP Host'

Note: incoming uses IMAP (receiving) vs outgoing SMTP (sending) — genuinely
different servers/credentials, hence the distinct field set (Folder; no From).
This commit is contained in:
Luca
2026-06-12 14:47:14 +02:00
parent d04a6978e9
commit f017649bd5
6 changed files with 104 additions and 6 deletions
@@ -11,7 +11,7 @@ import React, { useEffect, useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { toast } from 'react-toastify';
import { Save, Server, User, Lock, Eye, EyeOff, FolderSearch } from 'lucide-react';
import { Save, Server, User, Lock, Eye, EyeOff, FolderSearch, PlugZap } from 'lucide-react';
import { Button, Card, Input, Loading } from '../common';
import { emailService, type IncomingMailConfig, type ImapFolder } from '../../services/email.service';
@@ -44,6 +44,12 @@ export const IncomingMailConfigCard: React.FC = () => {
onError: (e: any) => toast.error(e?.response?.data?.error || e?.response?.data?.errors?.[0]?.msg || e.message || 'Failed'),
});
const test = useMutation({
mutationFn: () => emailService.testIncoming(cfg),
onSuccess: (r) => toast.success(t('email.incoming.testOk', 'Connected to {{folder}} — {{messages}} messages, {{unseen}} unread.', { folder: r.folder, messages: r.messages, unseen: r.unseen })),
onError: (e: any) => toast.error(e?.response?.data?.error || e.message || t('email.incoming.testFailed', 'Connection failed.')),
});
const detect = useMutation({
mutationFn: () => emailService.listIncomingFolders(cfg),
onSuccess: (list) => {
@@ -152,9 +158,21 @@ export const IncomingMailConfigCard: React.FC = () => {
<p className="mt-1 text-xs text-neutral-500 dark:text-neutral-400">{t('email.incoming.folderHint', 'Enter host, username and password, then Detect to list the mailbox folders.')}</p>
</div>
<Button variant="primary" onClick={() => save.mutate()} isLoading={save.isPending} leftIcon={<Save className="w-5 h-5" />} className="w-full">
{t('email.incoming.save', 'Save Incoming Mail Settings')}
</Button>
<div className="flex gap-2">
<Button
variant="outline"
onClick={() => test.mutate()}
isLoading={test.isPending}
disabled={!cfg.imap_host || !cfg.imap_user}
leftIcon={<PlugZap className="w-5 h-5" />}
className="whitespace-nowrap"
>
{t('email.incoming.test', 'Test connection')}
</Button>
<Button variant="primary" onClick={() => save.mutate()} isLoading={save.isPending} leftIcon={<Save className="w-5 h-5" />} className="flex-1">
{t('email.incoming.save', 'Save Incoming Mail Settings')}
</Button>
</div>
</div>
</Card>
);