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:
@@ -141,6 +141,9 @@ router.post('/incoming-config', [
|
|||||||
requirePermission('email.edit'),
|
requirePermission('email.edit'),
|
||||||
body('imap_host').notEmpty().withMessage('IMAP host is required'),
|
body('imap_host').notEmpty().withMessage('IMAP host is required'),
|
||||||
body('imap_port').isInt({ min: 1, max: 65535 }).withMessage('Invalid port number'),
|
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) => {
|
], async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const errors = validationResult(req);
|
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 set = (k: keyof IncomingMailConfig, v: any) => setCfg((c) => ({ ...c, [k]: v }));
|
||||||
|
|
||||||
const save = useMutation({
|
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'] }); },
|
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'),
|
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 className="space-y-4">
|
||||||
<div>
|
<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
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
value={cfg.imap_host}
|
value={cfg.imap_host}
|
||||||
@@ -78,7 +86,7 @@ export const IncomingMailConfigCard: React.FC = () => {
|
|||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<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" />
|
<Input type="number" value={cfg.imap_port} onChange={(e) => set('imap_port', parseInt(e.target.value, 10) || 0)} placeholder="993" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -91,7 +99,7 @@ export const IncomingMailConfigCard: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
value={cfg.imap_user}
|
value={cfg.imap_user}
|
||||||
|
|||||||
@@ -2492,6 +2492,7 @@
|
|||||||
"foldersDetected": "{{count}} Ordner gefunden.",
|
"foldersDetected": "{{count}} Ordner gefunden.",
|
||||||
"noFolders": "Der Server hat keine Ordner zurückgegeben.",
|
"noFolders": "Der Server hat keine Ordner zurückgegeben.",
|
||||||
"detectFailed": "Ordner konnten nicht erkannt werden.",
|
"detectFailed": "Ordner konnten nicht erkannt werden.",
|
||||||
|
"requiredFields": "Host, Port und Benutzername sind erforderlich.",
|
||||||
"savedToast": "Einstellungen für eingehende E-Mails gespeichert."
|
"savedToast": "Einstellungen für eingehende E-Mails gespeichert."
|
||||||
},
|
},
|
||||||
"received": {
|
"received": {
|
||||||
|
|||||||
@@ -2065,6 +2065,7 @@
|
|||||||
"foldersDetected": "{{count}} folders found.",
|
"foldersDetected": "{{count}} folders found.",
|
||||||
"noFolders": "No folders returned by the server.",
|
"noFolders": "No folders returned by the server.",
|
||||||
"detectFailed": "Could not detect folders.",
|
"detectFailed": "Could not detect folders.",
|
||||||
|
"requiredFields": "Host, port and username are required.",
|
||||||
"savedToast": "Incoming mail settings saved."
|
"savedToast": "Incoming mail settings saved."
|
||||||
},
|
},
|
||||||
"received": {
|
"received": {
|
||||||
|
|||||||
Reference in New Issue
Block a user