|
@@ -19,6 +19,7 @@ import {
|
|
|
let db;
|
|
let db;
|
|
|
let secretKey = '';
|
|
let secretKey = '';
|
|
|
export const USER_STATUSES = new Set(['pending_email', 'pending_review', 'active', 'disabled']);
|
|
export const USER_STATUSES = new Set(['pending_email', 'pending_review', 'active', 'disabled']);
|
|
|
|
|
+export const STANDARD_INBOUND_FOLDERS = ['INBOX', 'Sent', 'Drafts', 'Trash', 'Junk', 'Archive'];
|
|
|
const auditSecretKeyPattern = /password|secret|token|key|credential|dkim[_-]?private|authorization/i;
|
|
const auditSecretKeyPattern = /password|secret|token|key|credential|dkim[_-]?private|authorization/i;
|
|
|
const auditDescriptorKeyPattern = /^(field|name|path|key|header)$/i;
|
|
const auditDescriptorKeyPattern = /^(field|name|path|key|header)$/i;
|
|
|
const auditDescriptorValuePattern = /password|secret|token|key|credential|dkim[_-]?private|authorization/i;
|
|
const auditDescriptorValuePattern = /password|secret|token|key|credential|dkim[_-]?private|authorization/i;
|
|
@@ -177,6 +178,7 @@ export function initDatabase(dataDir, secret = '') {
|
|
|
mailbox_id INTEGER NOT NULL,
|
|
mailbox_id INTEGER NOT NULL,
|
|
|
user_id INTEGER NOT NULL,
|
|
user_id INTEGER NOT NULL,
|
|
|
domain_id INTEGER NOT NULL,
|
|
domain_id INTEGER NOT NULL,
|
|
|
|
|
+ folder TEXT NOT NULL DEFAULT 'INBOX',
|
|
|
sender TEXT NOT NULL DEFAULT '',
|
|
sender TEXT NOT NULL DEFAULT '',
|
|
|
recipients_json TEXT NOT NULL DEFAULT '[]',
|
|
recipients_json TEXT NOT NULL DEFAULT '[]',
|
|
|
subject TEXT NOT NULL DEFAULT '',
|
|
subject TEXT NOT NULL DEFAULT '',
|
|
@@ -195,6 +197,20 @@ export function initDatabase(dataDir, secret = '') {
|
|
|
FOREIGN KEY(domain_id) REFERENCES domains(id) ON DELETE CASCADE
|
|
FOREIGN KEY(domain_id) REFERENCES domains(id) ON DELETE CASCADE
|
|
|
);
|
|
);
|
|
|
|
|
|
|
|
|
|
+ CREATE TABLE IF NOT EXISTS inbound_folders (
|
|
|
|
|
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
|
|
|
+ mailbox_id INTEGER NOT NULL,
|
|
|
|
|
+ user_id INTEGER NOT NULL,
|
|
|
|
|
+ name TEXT NOT NULL,
|
|
|
|
|
+ subscribed TEXT NOT NULL DEFAULT 'true',
|
|
|
|
|
+ created_at TEXT NOT NULL,
|
|
|
|
|
+ updated_at TEXT NOT NULL,
|
|
|
|
|
+ deleted_at TEXT,
|
|
|
|
|
+ UNIQUE(mailbox_id, name),
|
|
|
|
|
+ FOREIGN KEY(mailbox_id) REFERENCES inbound_mailboxes(id) ON DELETE CASCADE,
|
|
|
|
|
+ FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
CREATE TABLE IF NOT EXISTS api_tokens (
|
|
CREATE TABLE IF NOT EXISTS api_tokens (
|
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
|
user_id INTEGER NOT NULL,
|
|
user_id INTEGER NOT NULL,
|
|
@@ -291,6 +307,8 @@ export function initDatabase(dataDir, secret = '') {
|
|
|
CREATE INDEX IF NOT EXISTS idx_inbound_mailboxes_domain_id ON inbound_mailboxes(domain_id);
|
|
CREATE INDEX IF NOT EXISTS idx_inbound_mailboxes_domain_id ON inbound_mailboxes(domain_id);
|
|
|
CREATE INDEX IF NOT EXISTS idx_inbound_messages_user_received ON inbound_messages(user_id, received_at);
|
|
CREATE INDEX IF NOT EXISTS idx_inbound_messages_user_received ON inbound_messages(user_id, received_at);
|
|
|
CREATE INDEX IF NOT EXISTS idx_inbound_messages_mailbox_received ON inbound_messages(mailbox_id, received_at);
|
|
CREATE INDEX IF NOT EXISTS idx_inbound_messages_mailbox_received ON inbound_messages(mailbox_id, received_at);
|
|
|
|
|
+ CREATE INDEX IF NOT EXISTS idx_inbound_messages_mailbox_folder_received ON inbound_messages(mailbox_id, folder, received_at);
|
|
|
|
|
+ CREATE INDEX IF NOT EXISTS idx_inbound_folders_mailbox ON inbound_folders(mailbox_id, deleted_at);
|
|
|
`);
|
|
`);
|
|
|
ensureColumn('domains', 'user_id', 'INTEGER');
|
|
ensureColumn('domains', 'user_id', 'INTEGER');
|
|
|
ensureColumn('domains', 'dns_credential_id', 'INTEGER');
|
|
ensureColumn('domains', 'dns_credential_id', 'INTEGER');
|
|
@@ -313,6 +331,7 @@ export function initDatabase(dataDir, secret = '') {
|
|
|
ensureColumn('inbound_mailboxes', 'forward_to_json', "TEXT NOT NULL DEFAULT '[]'");
|
|
ensureColumn('inbound_mailboxes', 'forward_to_json', "TEXT NOT NULL DEFAULT '[]'");
|
|
|
ensureColumn('inbound_mailboxes', 'keep_forwarded', "TEXT NOT NULL DEFAULT 'true'");
|
|
ensureColumn('inbound_mailboxes', 'keep_forwarded', "TEXT NOT NULL DEFAULT 'true'");
|
|
|
ensureColumn('inbound_mailboxes', 'quota_mb', 'INTEGER');
|
|
ensureColumn('inbound_mailboxes', 'quota_mb', 'INTEGER');
|
|
|
|
|
+ ensureColumn('inbound_messages', 'folder', "TEXT NOT NULL DEFAULT 'INBOX'");
|
|
|
db.exec(`
|
|
db.exec(`
|
|
|
CREATE INDEX IF NOT EXISTS idx_domains_user_id ON domains(user_id);
|
|
CREATE INDEX IF NOT EXISTS idx_domains_user_id ON domains(user_id);
|
|
|
CREATE INDEX IF NOT EXISTS idx_domains_smtp_relay_id ON domains(smtp_relay_id);
|
|
CREATE INDEX IF NOT EXISTS idx_domains_smtp_relay_id ON domains(smtp_relay_id);
|
|
@@ -325,6 +344,8 @@ export function initDatabase(dataDir, secret = '') {
|
|
|
WHERE tracking_token_hash IS NOT NULL AND tracking_token_hash != '';
|
|
WHERE tracking_token_hash IS NOT NULL AND tracking_token_hash != '';
|
|
|
CREATE INDEX IF NOT EXISTS idx_smtp_credentials_user_id ON smtp_credentials(user_id);
|
|
CREATE INDEX IF NOT EXISTS idx_smtp_credentials_user_id ON smtp_credentials(user_id);
|
|
|
CREATE INDEX IF NOT EXISTS idx_smtp_relays_user_id ON smtp_relays(user_id);
|
|
CREATE INDEX IF NOT EXISTS idx_smtp_relays_user_id ON smtp_relays(user_id);
|
|
|
|
|
+ CREATE INDEX IF NOT EXISTS idx_inbound_messages_mailbox_folder_received ON inbound_messages(mailbox_id, folder, received_at);
|
|
|
|
|
+ CREATE INDEX IF NOT EXISTS idx_inbound_folders_mailbox ON inbound_folders(mailbox_id, deleted_at);
|
|
|
`);
|
|
`);
|
|
|
normalizeSendEventQueueIds();
|
|
normalizeSendEventQueueIds();
|
|
|
normalizeDkimPublicKeys();
|
|
normalizeDkimPublicKeys();
|
|
@@ -1069,20 +1090,23 @@ export function resolveInboundRecipient(address) {
|
|
|
export function createInboundMessage(mailbox, message = {}) {
|
|
export function createInboundMessage(mailbox, message = {}) {
|
|
|
if (!mailbox?.id || !mailbox?.userId || !mailbox?.domainId) throw new Error('收信邮箱不存在。');
|
|
if (!mailbox?.id || !mailbox?.userId || !mailbox?.domainId) throw new Error('收信邮箱不存在。');
|
|
|
const receivedAt = message.receivedAt || now();
|
|
const receivedAt = message.receivedAt || now();
|
|
|
|
|
+ const folder = normalizeInboundFolder(message.folder) || 'INBOX';
|
|
|
const textBody = String(message.textBody || '');
|
|
const textBody = String(message.textBody || '');
|
|
|
const htmlBody = String(message.htmlBody || '');
|
|
const htmlBody = String(message.htmlBody || '');
|
|
|
const rawMessage = String(message.rawMessage || '');
|
|
const rawMessage = String(message.rawMessage || '');
|
|
|
|
|
+ if (!isStandardInboundFolder(folder)) createInboundFolder(mailbox, folder);
|
|
|
const result = requireDb()
|
|
const result = requireDb()
|
|
|
.prepare(`
|
|
.prepare(`
|
|
|
INSERT INTO inbound_messages (
|
|
INSERT INTO inbound_messages (
|
|
|
- mailbox_id, user_id, domain_id, sender, recipients_json, subject, message_id,
|
|
|
|
|
|
|
+ mailbox_id, user_id, domain_id, folder, sender, recipients_json, subject, message_id,
|
|
|
raw_message, text_body, html_body, preview, read_state, received_at, created_at, updated_at
|
|
raw_message, text_body, html_body, preview, read_state, received_at, created_at, updated_at
|
|
|
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'false', ?, ?, ?)
|
|
|
|
|
|
|
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'false', ?, ?, ?)
|
|
|
`)
|
|
`)
|
|
|
.run(
|
|
.run(
|
|
|
mailbox.id,
|
|
mailbox.id,
|
|
|
mailbox.userId,
|
|
mailbox.userId,
|
|
|
mailbox.domainId,
|
|
mailbox.domainId,
|
|
|
|
|
+ folder,
|
|
|
normalizeEmail(message.sender) || String(message.sender || '').trim(),
|
|
normalizeEmail(message.sender) || String(message.sender || '').trim(),
|
|
|
JSON.stringify(normalizeRecipientList(message.recipients)),
|
|
JSON.stringify(normalizeRecipientList(message.recipients)),
|
|
|
String(message.subject || '').trim() || '(no subject)',
|
|
String(message.subject || '').trim() || '(no subject)',
|
|
@@ -1098,13 +1122,17 @@ export function createInboundMessage(mailbox, message = {}) {
|
|
|
return getInboundMessage(mailbox.userId, result.lastInsertRowid);
|
|
return getInboundMessage(mailbox.userId, result.lastInsertRowid);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-export function listInboundMessages(userId, { mailboxId = null } = {}) {
|
|
|
|
|
|
|
+export function listInboundMessages(userId, { mailboxId = null, folder = 'INBOX' } = {}) {
|
|
|
const where = ['msg.user_id = ?', 'msg.deleted_at IS NULL'];
|
|
const where = ['msg.user_id = ?', 'msg.deleted_at IS NULL'];
|
|
|
const params = [userId];
|
|
const params = [userId];
|
|
|
if (mailboxId) {
|
|
if (mailboxId) {
|
|
|
where.push('msg.mailbox_id = ?');
|
|
where.push('msg.mailbox_id = ?');
|
|
|
params.push(Number(mailboxId));
|
|
params.push(Number(mailboxId));
|
|
|
}
|
|
}
|
|
|
|
|
+ if (folder !== null) {
|
|
|
|
|
+ where.push('msg.folder = ?');
|
|
|
|
|
+ params.push(normalizeInboundFolder(folder) || 'INBOX');
|
|
|
|
|
+ }
|
|
|
return requireDb()
|
|
return requireDb()
|
|
|
.prepare(`
|
|
.prepare(`
|
|
|
SELECT msg.*, m.address AS mailbox_address, d.domain
|
|
SELECT msg.*, m.address AS mailbox_address, d.domain
|
|
@@ -1131,18 +1159,64 @@ export function getInboundMessage(userId, id) {
|
|
|
return publicInboundMessage(row, { includeBody: true });
|
|
return publicInboundMessage(row, { includeBody: true });
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-export function listInboundMailboxProtocolMessages(mailbox) {
|
|
|
|
|
|
|
+export function listInboundFolders(mailbox) {
|
|
|
|
|
+ if (!mailbox?.id || !mailbox?.userId) return [...STANDARD_INBOUND_FOLDERS];
|
|
|
|
|
+ const custom = requireDb()
|
|
|
|
|
+ .prepare(`
|
|
|
|
|
+ SELECT name
|
|
|
|
|
+ FROM inbound_folders
|
|
|
|
|
+ WHERE mailbox_id = ? AND user_id = ? AND deleted_at IS NULL
|
|
|
|
|
+ ORDER BY name COLLATE NOCASE
|
|
|
|
|
+ `)
|
|
|
|
|
+ .all(Number(mailbox.id), mailbox.userId)
|
|
|
|
|
+ .map((row) => row.name)
|
|
|
|
|
+ .filter((name) => !isStandardInboundFolder(name));
|
|
|
|
|
+ return [...STANDARD_INBOUND_FOLDERS, ...custom];
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export function createInboundFolder(mailbox, folder) {
|
|
|
|
|
+ if (!mailbox?.id || !mailbox?.userId) throw new Error('收信邮箱不存在。');
|
|
|
|
|
+ const name = normalizeInboundFolder(folder);
|
|
|
|
|
+ if (!name) throw new Error('IMAP 文件夹名称不正确。');
|
|
|
|
|
+ if (isStandardInboundFolder(name)) return { name, standard: true };
|
|
|
|
|
+ const createdAt = now();
|
|
|
|
|
+ requireDb()
|
|
|
|
|
+ .prepare(`
|
|
|
|
|
+ INSERT OR IGNORE INTO inbound_folders (mailbox_id, user_id, name, subscribed, created_at, updated_at)
|
|
|
|
|
+ VALUES (?, ?, ?, 'true', ?, ?)
|
|
|
|
|
+ `)
|
|
|
|
|
+ .run(Number(mailbox.id), mailbox.userId, name, createdAt, createdAt);
|
|
|
|
|
+ return { name, standard: false };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export function inboundFolderExists(mailbox, folder) {
|
|
|
|
|
+ if (!mailbox?.id || !mailbox?.userId) return false;
|
|
|
|
|
+ const name = normalizeInboundFolder(folder);
|
|
|
|
|
+ if (!name) return false;
|
|
|
|
|
+ if (isStandardInboundFolder(name)) return true;
|
|
|
|
|
+ return Boolean(requireDb()
|
|
|
|
|
+ .prepare(`
|
|
|
|
|
+ SELECT id
|
|
|
|
|
+ FROM inbound_folders
|
|
|
|
|
+ WHERE mailbox_id = ? AND user_id = ? AND name = ? AND deleted_at IS NULL
|
|
|
|
|
+ LIMIT 1
|
|
|
|
|
+ `)
|
|
|
|
|
+ .get(Number(mailbox.id), mailbox.userId, name));
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export function listInboundMailboxProtocolMessages(mailbox, { folder = 'INBOX' } = {}) {
|
|
|
if (!mailbox?.id || !mailbox?.userId) return [];
|
|
if (!mailbox?.id || !mailbox?.userId) return [];
|
|
|
|
|
+ const selectedFolder = normalizeInboundFolder(folder) || 'INBOX';
|
|
|
return requireDb()
|
|
return requireDb()
|
|
|
.prepare(`
|
|
.prepare(`
|
|
|
SELECT msg.*, m.address AS mailbox_address, d.domain
|
|
SELECT msg.*, m.address AS mailbox_address, d.domain
|
|
|
FROM inbound_messages msg
|
|
FROM inbound_messages msg
|
|
|
JOIN inbound_mailboxes m ON m.id = msg.mailbox_id
|
|
JOIN inbound_mailboxes m ON m.id = msg.mailbox_id
|
|
|
JOIN domains d ON d.id = msg.domain_id
|
|
JOIN domains d ON d.id = msg.domain_id
|
|
|
- WHERE msg.mailbox_id = ? AND msg.user_id = ? AND msg.deleted_at IS NULL
|
|
|
|
|
|
|
+ WHERE msg.mailbox_id = ? AND msg.user_id = ? AND msg.folder = ? AND msg.deleted_at IS NULL
|
|
|
ORDER BY msg.id ASC
|
|
ORDER BY msg.id ASC
|
|
|
`)
|
|
`)
|
|
|
- .all(Number(mailbox.id), mailbox.userId)
|
|
|
|
|
|
|
+ .all(Number(mailbox.id), mailbox.userId, selectedFolder)
|
|
|
.map((row) => publicInboundMessage(row, { includeBody: true }));
|
|
.map((row) => publicInboundMessage(row, { includeBody: true }));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -1155,20 +1229,24 @@ export function markInboundMessageRead(userId, id, read = true) {
|
|
|
return getInboundMessage(userId, id);
|
|
return getInboundMessage(userId, id);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-export function softDeleteInboundMessages(userId, mailboxId, ids) {
|
|
|
|
|
|
|
+export function softDeleteInboundMessages(userId, mailboxId, ids, { folder = null } = {}) {
|
|
|
const cleanIds = [...new Set((Array.isArray(ids) ? ids : [ids])
|
|
const cleanIds = [...new Set((Array.isArray(ids) ? ids : [ids])
|
|
|
.map((id) => Number(id))
|
|
.map((id) => Number(id))
|
|
|
.filter((id) => Number.isInteger(id) && id > 0))];
|
|
.filter((id) => Number.isInteger(id) && id > 0))];
|
|
|
if (!cleanIds.length) return 0;
|
|
if (!cleanIds.length) return 0;
|
|
|
|
|
+ const folderClause = folder === null ? '' : 'AND folder = ?';
|
|
|
const placeholders = cleanIds.map(() => '?').join(', ');
|
|
const placeholders = cleanIds.map(() => '?').join(', ');
|
|
|
const updatedAt = now();
|
|
const updatedAt = now();
|
|
|
|
|
+ const params = folder === null
|
|
|
|
|
+ ? [updatedAt, updatedAt, userId, Number(mailboxId), ...cleanIds]
|
|
|
|
|
+ : [updatedAt, updatedAt, userId, Number(mailboxId), normalizeInboundFolder(folder) || 'INBOX', ...cleanIds];
|
|
|
const result = requireDb()
|
|
const result = requireDb()
|
|
|
.prepare(`
|
|
.prepare(`
|
|
|
UPDATE inbound_messages
|
|
UPDATE inbound_messages
|
|
|
SET deleted_at = ?, updated_at = ?
|
|
SET deleted_at = ?, updated_at = ?
|
|
|
- WHERE user_id = ? AND mailbox_id = ? AND deleted_at IS NULL AND id IN (${placeholders})
|
|
|
|
|
|
|
+ WHERE user_id = ? AND mailbox_id = ? AND deleted_at IS NULL ${folderClause} AND id IN (${placeholders})
|
|
|
`)
|
|
`)
|
|
|
- .run(updatedAt, updatedAt, userId, Number(mailboxId), ...cleanIds);
|
|
|
|
|
|
|
+ .run(...params);
|
|
|
return result.changes;
|
|
return result.changes;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -3044,6 +3122,14 @@ function moveInboundDomainResources(domainId, targetUserId) {
|
|
|
const messageResult = requireDb()
|
|
const messageResult = requireDb()
|
|
|
.prepare('UPDATE inbound_messages SET user_id = ?, updated_at = ? WHERE domain_id = ? AND deleted_at IS NULL')
|
|
.prepare('UPDATE inbound_messages SET user_id = ?, updated_at = ? WHERE domain_id = ? AND deleted_at IS NULL')
|
|
|
.run(targetUserId, updatedAt, domainId);
|
|
.run(targetUserId, updatedAt, domainId);
|
|
|
|
|
+ requireDb()
|
|
|
|
|
+ .prepare(`
|
|
|
|
|
+ UPDATE inbound_folders
|
|
|
|
|
+ SET user_id = ?, updated_at = ?
|
|
|
|
|
+ WHERE deleted_at IS NULL
|
|
|
|
|
+ AND mailbox_id IN (SELECT id FROM inbound_mailboxes WHERE domain_id = ?)
|
|
|
|
|
+ `)
|
|
|
|
|
+ .run(targetUserId, updatedAt, domainId);
|
|
|
return {
|
|
return {
|
|
|
mailboxes: mailboxResult.changes,
|
|
mailboxes: mailboxResult.changes,
|
|
|
messages: messageResult.changes
|
|
messages: messageResult.changes
|
|
@@ -3070,6 +3156,15 @@ function moveInboundResourcesForUserDomains(sourceUserId, targetUserId) {
|
|
|
AND domain_id IN (SELECT id FROM domains WHERE user_id = ?)
|
|
AND domain_id IN (SELECT id FROM domains WHERE user_id = ?)
|
|
|
`)
|
|
`)
|
|
|
.run(targetUserId, updatedAt, sourceUserId, sourceUserId);
|
|
.run(targetUserId, updatedAt, sourceUserId, sourceUserId);
|
|
|
|
|
+ requireDb()
|
|
|
|
|
+ .prepare(`
|
|
|
|
|
+ UPDATE inbound_folders
|
|
|
|
|
+ SET user_id = ?, updated_at = ?
|
|
|
|
|
+ WHERE user_id = ?
|
|
|
|
|
+ AND deleted_at IS NULL
|
|
|
|
|
+ AND mailbox_id IN (SELECT id FROM inbound_mailboxes WHERE domain_id IN (SELECT id FROM domains WHERE user_id = ?))
|
|
|
|
|
+ `)
|
|
|
|
|
+ .run(targetUserId, updatedAt, sourceUserId, sourceUserId);
|
|
|
return {
|
|
return {
|
|
|
mailboxes: mailboxResult.changes,
|
|
mailboxes: mailboxResult.changes,
|
|
|
messages: messageResult.changes
|
|
messages: messageResult.changes
|
|
@@ -3272,6 +3367,7 @@ function publicInboundMessage(row, { includeBody = false } = {}) {
|
|
|
userId: row.user_id,
|
|
userId: row.user_id,
|
|
|
domainId: row.domain_id,
|
|
domainId: row.domain_id,
|
|
|
domain: row.domain || '',
|
|
domain: row.domain || '',
|
|
|
|
|
+ folder: row.folder || 'INBOX',
|
|
|
mailboxAddress: row.mailbox_address || '',
|
|
mailboxAddress: row.mailbox_address || '',
|
|
|
sender: row.sender,
|
|
sender: row.sender,
|
|
|
recipients: safeJson(row.recipients_json, []),
|
|
recipients: safeJson(row.recipients_json, []),
|
|
@@ -3677,6 +3773,24 @@ function normalizeInboundAddress(value) {
|
|
|
return `${localPart}@${domain}`;
|
|
return `${localPart}@${domain}`;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+function normalizeInboundFolder(value, fallback = '') {
|
|
|
|
|
+ const raw = String(value || fallback || '').trim().replace(/^"|"$/g, '').replace(/\\/g, '/');
|
|
|
|
|
+ if (!raw || /[\r\n\u0000]/.test(raw)) return '';
|
|
|
|
|
+ if (raw.toUpperCase() === 'INBOX') return 'INBOX';
|
|
|
|
|
+ const standard = STANDARD_INBOUND_FOLDERS.find((folder) => folder.toLowerCase() === raw.toLowerCase());
|
|
|
|
|
+ if (standard) return standard;
|
|
|
|
|
+ return raw
|
|
|
|
|
+ .split('/')
|
|
|
|
|
+ .map((part) => part.trim())
|
|
|
|
|
+ .filter(Boolean)
|
|
|
|
|
+ .join('/');
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function isStandardInboundFolder(value) {
|
|
|
|
|
+ const clean = normalizeInboundFolder(value);
|
|
|
|
|
+ return STANDARD_INBOUND_FOLDERS.some((folder) => folder === clean);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
function normalizeCatchAllAddress(value) {
|
|
function normalizeCatchAllAddress(value) {
|
|
|
const clean = String(value || '').trim().toLowerCase();
|
|
const clean = String(value || '').trim().toLowerCase();
|
|
|
if (!clean) return '';
|
|
if (!clean) return '';
|