fix(email): mark required fields on incoming mail to match outgoing SMTP

The IMAP card was restyled to match SMTP but didn't carry the required-field
markers. Aligned the required set (protocol differences kept):
- red asterisks on Host *, Port *, Username * (SMTP marks Host/Port/From-Email;
  IMAP has no From-Email but always needs a login)
- client-side guard mirroring handleSaveSmtp (block save without host/port/user)
- backend POST /incoming-config now requires imap_user (the poller's
  getImapConfig returns null without it)
- en/de requiredFields string
This commit is contained in:
Luca
2026-06-12 12:33:04 +02:00
parent abb23f01c7
commit fb48ba4cb4
4 changed files with 17 additions and 4 deletions
+3
View File
@@ -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);
@@ -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 = () => {
<div className="space-y-4">
<div>
<label className={labelCls}>{t('email.incoming.host', 'IMAP Host')}</label>
<label className={labelCls}>{t('email.incoming.host', 'IMAP Host')} <span className="text-red-500">*</span></label>
<Input
type="text"
value={cfg.imap_host}
@@ -78,7 +86,7 @@ export const IncomingMailConfigCard: React.FC = () => {
<div className="grid grid-cols-2 gap-4">
<div>
<label className={labelCls}>{t('email.incoming.port', 'Port')}</label>
<label className={labelCls}>{t('email.incoming.port', 'Port')} <span className="text-red-500">*</span></label>
<Input type="number" value={cfg.imap_port} onChange={(e) => set('imap_port', parseInt(e.target.value, 10) || 0)} placeholder="993" />
</div>
<div>
@@ -91,7 +99,7 @@ export const IncomingMailConfigCard: React.FC = () => {
</div>
<div>
<label className={labelCls}>{t('email.incoming.user', 'Username')}</label>
<label className={labelCls}>{t('email.incoming.user', 'Username')} <span className="text-red-500">*</span></label>
<Input
type="text"
value={cfg.imap_user}
+1
View File
@@ -2492,6 +2492,7 @@
"foldersDetected": "{{count}} Ordner gefunden.",
"noFolders": "Der Server hat keine Ordner zurückgegeben.",
"detectFailed": "Ordner konnten nicht erkannt werden.",
"requiredFields": "Host, Port und Benutzername sind erforderlich.",
"savedToast": "Einstellungen für eingehende E-Mails gespeichert."
},
"received": {
+1
View File
@@ -2065,6 +2065,7 @@
"foldersDetected": "{{count}} folders found.",
"noFolders": "No folders returned by the server.",
"detectFailed": "Could not detect folders.",
"requiredFields": "Host, port and username are required.",
"savedToast": "Incoming mail settings saved."
},
"received": {