diff --git a/backend/src/routes/adminEmail.js b/backend/src/routes/adminEmail.js index da7f4f20..7f815d83 100644 --- a/backend/src/routes/adminEmail.js +++ b/backend/src/routes/adminEmail.js @@ -141,6 +141,9 @@ router.post('/incoming-config', [ requirePermission('email.edit'), body('imap_host').notEmpty().withMessage('IMAP host is required'), body('imap_port').isInt({ min: 1, max: 65535 }).withMessage('Invalid port number'), + // IMAP always needs a login (unlike SMTP relay) — the poller's + // getImapConfig() returns null without a username, so require it. + body('imap_user').notEmpty().withMessage('IMAP username is required'), ], async (req, res) => { try { const errors = validationResult(req); diff --git a/frontend/src/components/admin/IncomingMailConfigCard.tsx b/frontend/src/components/admin/IncomingMailConfigCard.tsx index c9d2f10f..5cf2fe5f 100644 --- a/frontend/src/components/admin/IncomingMailConfigCard.tsx +++ b/frontend/src/components/admin/IncomingMailConfigCard.tsx @@ -31,7 +31,15 @@ export const IncomingMailConfigCard: React.FC = () => { const set = (k: keyof IncomingMailConfig, v: any) => setCfg((c) => ({ ...c, [k]: v })); const save = useMutation({ - mutationFn: () => emailService.updateIncomingConfig(cfg), + 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'), }); @@ -66,7 +74,7 @@ export const IncomingMailConfigCard: React.FC = () => {