feat(email): add 'Test connection' to incoming mail + tidy IMAP label
- emailIntakeService.testConnection(): logs in, opens the configured folder, reports message/unread counts (non-destructive). Accepts current form creds so it works before saving; masked password falls back to stored. - route POST /admin/email/incoming-config/test - IMAP card: 'Test connection' button beside Save; toast shows folder + counts - capitalize 'IMAP Host' label to match 'SMTP Host' Note: incoming uses IMAP (receiving) vs outgoing SMTP (sending) — genuinely different servers/credentials, hence the distinct field set (Folder; no From).
This commit is contained in:
@@ -197,6 +197,31 @@ router.post('/incoming-config/folders', adminAuth, requirePermission('email.view
|
||||
}
|
||||
});
|
||||
|
||||
// Test the incoming-mail connection: log in + open the configured folder and
|
||||
// report message/unread counts. Accepts current form creds (test before save).
|
||||
router.post('/incoming-config/test', adminAuth, requirePermission('email.view'), async (req, res) => {
|
||||
try {
|
||||
const { imap_host, imap_port, imap_secure, imap_user, imap_pass, imap_folder } = 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 result = await emailIntakeService.testConnection(
|
||||
imap_host ? { host: imap_host, port: imap_port, secure: imap_secure, user: imap_user, pass: imap_pass, folder: imap_folder } : undefined
|
||||
);
|
||||
if (result && result.ok === false) {
|
||||
return res.status(400).json({ error: 'Incoming mail is not configured yet — enter host, username and password first.' });
|
||||
}
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
console.error('IMAP connection test error:', error);
|
||||
res.status(502).json({ error: 'Could not connect to the mailbox. Check host, port, credentials and folder.' });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/received', adminAuth, requirePermission('email.view'), async (req, res) => {
|
||||
try {
|
||||
const page = Math.max(1, parseInt(req.query.page, 10) || 1);
|
||||
|
||||
@@ -83,6 +83,43 @@ async function listFolders(override) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the IMAP connection: log in, open the configured folder, and report
|
||||
* the message + unread counts. Non-destructive (marks nothing seen, ingests
|
||||
* nothing) — proves host/port/user/pass AND that the chosen folder opens.
|
||||
* Accepts an `override` ({ host, port, secure, user, pass, folder }) so the
|
||||
* admin can test before saving; a masked/blank password falls back to stored.
|
||||
*/
|
||||
async function testConnection(override) {
|
||||
let cfg; let folder;
|
||||
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 || '' },
|
||||
};
|
||||
folder = override.folder || 'INBOX';
|
||||
if (!cfg.auth.pass || cfg.auth.pass === '********') {
|
||||
const stored = await getImapConfig();
|
||||
cfg.auth.pass = stored?.auth?.pass || '';
|
||||
}
|
||||
} else {
|
||||
const c = await getImapConfig();
|
||||
if (!c) return { ok: false, error: 'unconfigured' };
|
||||
cfg = { host: c.host, port: c.port, secure: c.secure, auth: c.auth };
|
||||
folder = c.folder;
|
||||
}
|
||||
const client = new ImapFlow({ host: cfg.host, port: cfg.port, secure: cfg.secure, auth: cfg.auth, logger: false });
|
||||
await client.connect();
|
||||
try {
|
||||
const status = await client.status(folder, { messages: true, unseen: true });
|
||||
return { ok: true, folder, messages: status.messages || 0, unseen: status.unseen || 0 };
|
||||
} 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' };
|
||||
@@ -155,4 +192,4 @@ function startIncomingMailPoller() {
|
||||
logger.info?.('Incoming-mail poller started (every 60s when enabled)');
|
||||
}
|
||||
|
||||
module.exports = { pollOnce, startIncomingMailPoller, listFolders, _internal: { getImapConfig, isEnabled, saveAttachment } };
|
||||
module.exports = { pollOnce, startIncomingMailPoller, listFolders, testConnection, _internal: { getImapConfig, isEnabled, saveAttachment } };
|
||||
|
||||
Reference in New Issue
Block a user