/** * Incoming mail (IMAP) configuration — a second block under the outgoing SMTP * settings, styled to match the SMTP card (icon inputs, password eye toggle, * full-width Save). Shown only when the `incomingMail` feature flag is on. * * The Folder field auto-detects: "Detect folders" lists the mailboxes on the * server and offers them as a dropdown (auto-selecting the inbox), instead of * making the admin type a path. */ 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 { Button, Card, Input, Loading } from '../common'; import { emailService, type IncomingMailConfig, type ImapFolder } from '../../services/email.service'; const labelCls = 'block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1'; const selectCls = 'w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 bg-white dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-accent-dark'; export const IncomingMailConfigCard: React.FC = () => { const { t } = useTranslation(); const qc = useQueryClient(); const { data, isLoading } = useQuery({ queryKey: ['incoming-mail-config'], queryFn: () => emailService.getIncomingConfig() }); const [cfg, setCfg] = useState({ imap_host: '', imap_port: 993, imap_secure: true, imap_user: '', imap_pass: '', imap_folder: 'INBOX' }); const [showPassword, setShowPassword] = useState(false); const [folders, setFolders] = useState(null); useEffect(() => { if (data) setCfg(data); }, [data]); const set = (k: keyof IncomingMailConfig, v: any) => setCfg((c) => ({ ...c, [k]: v })); // Changing Security auto-fills the conventional IMAP port (SSL/TLS → 993, // STARTTLS/none → 143) so the port in the dropdown label isn't just // decoration. A non-standard custom port is left untouched. const onSecurityChange = (val: string) => { const secure = val === 'ssl'; setCfg((c) => { const next = { ...c, imap_secure: secure }; if (!c.imap_port || c.imap_port === 993 || c.imap_port === 143) { next.imap_port = secure ? 993 : 143; } return next; }); }; const save = useMutation({ mutationFn: () => { // Mirror the SMTP card's client-side required guard. Host + port + // username are needed for the poller to authenticate (getImapConfig // returns null without host+user). if (!cfg.imap_host || !cfg.imap_port || !cfg.imap_user) { return Promise.reject(new Error(t('email.incoming.requiredFields', 'Host, port and username are required.'))); } return emailService.updateIncomingConfig(cfg); }, onSuccess: () => { toast.success(t('email.incoming.savedToast', 'Incoming mail settings saved.')); qc.invalidateQueries({ queryKey: ['incoming-mail-config'] }); }, onError: (e: any) => toast.error(e?.response?.data?.error || e?.response?.data?.errors?.[0]?.msg || e.message || 'Failed'), }); const detect = useMutation({ mutationFn: () => emailService.listIncomingFolders(cfg), onSuccess: (list) => { setFolders(list); if (list.length) { // Auto-select the inbox (special-use '\Inbox', else a path named INBOX) // when the current folder isn't one of the detected ones. const has = list.some((f) => f.path === cfg.imap_folder); if (!has) { const inbox = list.find((f) => (f.specialUse || '').toLowerCase().includes('inbox')) || list.find((f) => f.path.toUpperCase() === 'INBOX') || list[0]; if (inbox) set('imap_folder', inbox.path); } toast.success(t('email.incoming.foldersDetected', '{{count}} folders found.', { count: list.length })); } else { toast.info(t('email.incoming.noFolders', 'No folders returned by the server.')); } }, onError: (e: any) => toast.error(e?.response?.data?.error || e.message || t('email.incoming.detectFailed', 'Could not detect folders.')), }); if (isLoading) return ; return (

{t('email.incoming.title', 'Incoming mail (IMAP)')}

{t('email.incoming.subtitle', 'A dedicated mailbox polled every minute; attachments land in Accounting → Incoming invoices.')}

set('imap_host', e.target.value)} placeholder="imap.example.com" leftIcon={} />
set('imap_port', parseInt(e.target.value, 10) || 0)} placeholder="993" />
set('imap_user', e.target.value)} autoComplete="off" placeholder="rechnungen@yourdomain.com" leftIcon={} />
set('imap_pass', e.target.value)} autoComplete="new-password" placeholder={t('email.enterPassword', 'Enter password')} leftIcon={} />
{folders && folders.length > 0 ? ( ) : ( set('imap_folder', e.target.value)} placeholder="INBOX" /> )}

{t('email.incoming.folderHint', 'Enter host, username and password, then Detect to list the mailbox folders.')}

); }; export default IncomingMailConfigCard;