fix(email): match IMAP card to SMTP styling + auto-detect mailbox folders

- IncomingMailConfigCard rebuilt to mirror the outgoing SMTP card: Card
  padding=md, icon inputs (Server/User/Lock), password eye toggle, stacked
  full-width fields, full-width primary Save button
- Folder is now a dropdown auto-populated by a 'Detect' button instead of a
  free-text path: backend emailIntakeService.listFolders() lists IMAP
  mailboxes (POST /admin/email/incoming-config/folders, accepts current form
  creds, masked password falls back to stored); UI auto-selects the inbox
  (special-use) folder
- en/de strings added
This commit is contained in:
Luca
2026-06-12 01:11:31 +02:00
parent 7e0098edcd
commit abb23f01c7
6 changed files with 203 additions and 27 deletions
+23
View File
@@ -171,6 +171,29 @@ router.post('/incoming-config', [
});
// Received-emails log (the IMAP poller's audit trail) — "Received emails" tab.
// List IMAP folders so the UI can offer a dropdown (auto-detect) instead of a
// free-text path. Accepts optional creds in the body to detect before saving;
// falls back to the stored config (and stored password when masked).
router.post('/incoming-config/folders', adminAuth, requirePermission('email.view'), async (req, res) => {
try {
const { imap_host, imap_port, imap_secure, imap_user, imap_pass } = req.body || {};
if (imap_host) {
const { isPrivateIP } = require('../utils/networkValidation');
if (isPrivateIP(imap_host)) {
return res.status(400).json({ error: 'IMAP host cannot point to a private or internal network address' });
}
}
const emailIntakeService = require('../services/emailIntakeService');
const folders = await emailIntakeService.listFolders(
imap_host ? { host: imap_host, port: imap_port, secure: imap_secure, user: imap_user, pass: imap_pass } : undefined
);
res.json({ folders });
} catch (error) {
console.error('IMAP folder detection error:', error);
res.status(502).json({ error: 'Could not connect to the mailbox. Check host, port and credentials.' });
}
});
router.get('/received', adminAuth, requirePermission('email.view'), async (req, res) => {
try {
const page = Math.max(1, parseInt(req.query.page, 10) || 1);
+36 -1
View File
@@ -48,6 +48,41 @@ async function saveAttachment(att) {
return filePath;
}
/**
* List the mailbox folders on the IMAP server so the UI can offer a
* dropdown instead of a free-text path. Uses the saved config; an
* `override` ({ host, port, secure, user, pass }) lets the admin detect
* folders BEFORE saving. A masked/blank override password falls back to
* the stored one. Returns [{ path, name, specialUse }] (specialUse like
* '\\Inbox' lets the caller auto-select the inbox).
*/
async function listFolders(override) {
let cfg;
if (override && override.host && override.user) {
cfg = {
host: override.host,
port: override.port || 993,
secure: override.secure !== false && override.secure !== 0,
auth: { user: override.user, pass: override.pass || '' },
};
if (!cfg.auth.pass || cfg.auth.pass === '********') {
const stored = await getImapConfig();
cfg.auth.pass = stored?.auth?.pass || '';
}
} else {
cfg = await getImapConfig();
}
if (!cfg) return [];
const client = new ImapFlow({ host: cfg.host, port: cfg.port, secure: cfg.secure, auth: cfg.auth, logger: false });
await client.connect();
try {
const list = await client.list();
return (list || []).map((m) => ({ path: m.path, name: m.name, specialUse: m.specialUse || null }));
} finally {
await client.logout().catch(() => {});
}
}
/** Poll the mailbox once. Safe to call repeatedly; self-skips when busy/off. */
async function pollOnce() {
if (polling) return { skipped: 'busy' };
@@ -120,4 +155,4 @@ function startIncomingMailPoller() {
logger.info?.('Incoming-mail poller started (every 60s when enabled)');
}
module.exports = { pollOnce, startIncomingMailPoller, _internal: { getImapConfig, isEnabled, saveAttachment } };
module.exports = { pollOnce, startIncomingMailPoller, listFolders, _internal: { getImapConfig, isEnabled, saveAttachment } };