|
|
@@ -150,6 +150,44 @@ export function initDatabase(dataDir, secret = '') {
|
|
|
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
|
);
|
|
|
|
|
|
+ CREATE TABLE IF NOT EXISTS inbound_mailboxes (
|
|
|
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
|
+ user_id INTEGER NOT NULL,
|
|
|
+ domain_id INTEGER NOT NULL,
|
|
|
+ address TEXT NOT NULL UNIQUE,
|
|
|
+ local_part TEXT NOT NULL,
|
|
|
+ display_name TEXT NOT NULL DEFAULT '',
|
|
|
+ status TEXT NOT NULL DEFAULT 'active',
|
|
|
+ created_at TEXT NOT NULL,
|
|
|
+ updated_at TEXT NOT NULL,
|
|
|
+ deleted_at TEXT,
|
|
|
+ FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
|
|
|
+ FOREIGN KEY(domain_id) REFERENCES domains(id) ON DELETE CASCADE
|
|
|
+ );
|
|
|
+
|
|
|
+ CREATE TABLE IF NOT EXISTS inbound_messages (
|
|
|
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
|
+ mailbox_id INTEGER NOT NULL,
|
|
|
+ user_id INTEGER NOT NULL,
|
|
|
+ domain_id INTEGER NOT NULL,
|
|
|
+ sender TEXT NOT NULL DEFAULT '',
|
|
|
+ recipients_json TEXT NOT NULL DEFAULT '[]',
|
|
|
+ subject TEXT NOT NULL DEFAULT '',
|
|
|
+ message_id TEXT NOT NULL DEFAULT '',
|
|
|
+ raw_message TEXT NOT NULL DEFAULT '',
|
|
|
+ text_body TEXT NOT NULL DEFAULT '',
|
|
|
+ html_body TEXT NOT NULL DEFAULT '',
|
|
|
+ preview TEXT NOT NULL DEFAULT '',
|
|
|
+ read_state TEXT NOT NULL DEFAULT 'false',
|
|
|
+ received_at TEXT NOT NULL,
|
|
|
+ created_at TEXT NOT NULL,
|
|
|
+ updated_at TEXT NOT NULL,
|
|
|
+ deleted_at TEXT,
|
|
|
+ FOREIGN KEY(mailbox_id) REFERENCES inbound_mailboxes(id) ON DELETE CASCADE,
|
|
|
+ FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
|
|
|
+ FOREIGN KEY(domain_id) REFERENCES domains(id) ON DELETE CASCADE
|
|
|
+ );
|
|
|
+
|
|
|
CREATE TABLE IF NOT EXISTS api_tokens (
|
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
|
user_id INTEGER NOT NULL,
|
|
|
@@ -242,6 +280,10 @@ export function initDatabase(dataDir, secret = '') {
|
|
|
CREATE INDEX IF NOT EXISTS idx_tracking_events_event_time ON tracking_events(send_event_id, occurred_at);
|
|
|
CREATE INDEX IF NOT EXISTS idx_tracking_events_link_time ON tracking_events(tracking_link_id, occurred_at);
|
|
|
CREATE INDEX IF NOT EXISTS idx_tracking_events_type_time ON tracking_events(event_type, occurred_at);
|
|
|
+ CREATE INDEX IF NOT EXISTS idx_inbound_mailboxes_user_id ON inbound_mailboxes(user_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_mailbox_received ON inbound_messages(mailbox_id, received_at);
|
|
|
`);
|
|
|
ensureColumn('domains', 'user_id', 'INTEGER');
|
|
|
ensureColumn('domains', 'dns_credential_id', 'INTEGER');
|
|
|
@@ -375,6 +417,8 @@ export function listUsersWithResourceCounts() {
|
|
|
(SELECT COUNT(*) FROM domains WHERE domains.user_id = users.id) AS domains_count,
|
|
|
(SELECT COUNT(*) FROM dns_credentials WHERE dns_credentials.user_id = users.id) AS dns_credentials_count,
|
|
|
(SELECT COUNT(*) FROM api_tokens WHERE api_tokens.user_id = users.id) AS api_tokens_count,
|
|
|
+ (SELECT COUNT(*) FROM inbound_mailboxes WHERE inbound_mailboxes.user_id = users.id AND inbound_mailboxes.deleted_at IS NULL) AS inbound_mailboxes_count,
|
|
|
+ (SELECT COUNT(*) FROM inbound_messages WHERE inbound_messages.user_id = users.id AND inbound_messages.deleted_at IS NULL) AS inbound_messages_count,
|
|
|
(SELECT COUNT(*) FROM send_events WHERE send_events.user_id = users.id) AS send_events_count,
|
|
|
(SELECT COUNT(*) FROM smtp_credentials WHERE smtp_credentials.user_id = users.id) AS smtp_credentials_count
|
|
|
FROM users
|
|
|
@@ -387,6 +431,8 @@ export function listUsersWithResourceCounts() {
|
|
|
domains: Number(row.domains_count || 0),
|
|
|
dnsCredentials: Number(row.dns_credentials_count || 0),
|
|
|
apiTokens: Number(row.api_tokens_count || 0),
|
|
|
+ inboundMailboxes: Number(row.inbound_mailboxes_count || 0),
|
|
|
+ inboundMessages: Number(row.inbound_messages_count || 0),
|
|
|
sendEvents: Number(row.send_events_count || 0),
|
|
|
smtpCredential: Number(row.smtp_credentials_count || 0)
|
|
|
}
|
|
|
@@ -411,12 +457,35 @@ export function getAdminResourceInventory() {
|
|
|
.prepare('SELECT * FROM api_tokens ORDER BY user_id, created_at DESC')
|
|
|
.all()
|
|
|
.map(publicApiToken);
|
|
|
+ const inboundMailboxes = requireDb()
|
|
|
+ .prepare(`
|
|
|
+ SELECT
|
|
|
+ m.*,
|
|
|
+ d.domain,
|
|
|
+ COUNT(msg.id) AS message_count,
|
|
|
+ COALESCE(SUM(CASE WHEN msg.read_state = 'false' THEN 1 ELSE 0 END), 0) AS unread_count,
|
|
|
+ MAX(msg.received_at) AS last_message_at
|
|
|
+ FROM inbound_mailboxes m
|
|
|
+ JOIN domains d ON d.id = m.domain_id
|
|
|
+ LEFT JOIN inbound_messages msg ON msg.mailbox_id = m.id AND msg.deleted_at IS NULL
|
|
|
+ WHERE m.deleted_at IS NULL
|
|
|
+ GROUP BY m.id
|
|
|
+ ORDER BY m.user_id, COALESCE(last_message_at, m.created_at) DESC, m.id DESC
|
|
|
+ `)
|
|
|
+ .all()
|
|
|
+ .map(publicInboundMailbox);
|
|
|
const sendEventCounts = new Map(
|
|
|
requireDb()
|
|
|
.prepare('SELECT user_id, COUNT(*) AS count FROM send_events GROUP BY user_id')
|
|
|
.all()
|
|
|
.map((row) => [row.user_id, Number(row.count || 0)])
|
|
|
);
|
|
|
+ const inboundMessageCounts = new Map(
|
|
|
+ requireDb()
|
|
|
+ .prepare('SELECT user_id, COUNT(*) AS count FROM inbound_messages WHERE deleted_at IS NULL GROUP BY user_id')
|
|
|
+ .all()
|
|
|
+ .map((row) => [row.user_id, Number(row.count || 0)])
|
|
|
+ );
|
|
|
const dnsCredentialById = new Map(dnsCredentials.map((credential) => [credential.id, credential]));
|
|
|
|
|
|
return {
|
|
|
@@ -426,6 +495,8 @@ export function getAdminResourceInventory() {
|
|
|
dnsCredentials: dnsCredentials.filter((credential) => credential.userId === user.id),
|
|
|
smtpCredential: smtpCredentials.find((credential) => credential.userId === user.id) || null,
|
|
|
apiTokens: apiTokens.filter((token) => token.userId === user.id),
|
|
|
+ inboundMailboxes: inboundMailboxes.filter((mailbox) => mailbox.userId === user.id),
|
|
|
+ inboundMessageCount: inboundMessageCounts.get(user.id) || 0,
|
|
|
sendEventCount: sendEventCounts.get(user.id) || 0
|
|
|
})),
|
|
|
warnings: domains.flatMap((domain) => {
|
|
|
@@ -453,6 +524,7 @@ export function transferDomain({ actorUserId, domainId, targetUserId, dnsCredent
|
|
|
requireDb()
|
|
|
.prepare('UPDATE domains SET user_id = ?, dns_credential_id = ?, updated_at = ? WHERE id = ?')
|
|
|
.run(target.id, nextDnsCredentialId, now(), domain.id);
|
|
|
+ const inboundCounts = moveInboundDomainResources(domain.id, target.id);
|
|
|
if (mode === 'with_dns_credential' && domain.dns_credential_id) {
|
|
|
const credential = requireDnsCredentialRow(domain.dns_credential_id);
|
|
|
if (credential.user_id !== domain.user_id) throw new Error('DNS 凭据归属不一致。');
|
|
|
@@ -472,7 +544,9 @@ export function transferDomain({ actorUserId, domainId, targetUserId, dnsCredent
|
|
|
fromUserId: domain.user_id,
|
|
|
toUserId: target.id,
|
|
|
dnsCredentialMode: mode,
|
|
|
- dnsCredentialId: domain.dns_credential_id || null
|
|
|
+ dnsCredentialId: domain.dns_credential_id || null,
|
|
|
+ inboundMailboxes: inboundCounts.mailboxes,
|
|
|
+ inboundMessages: inboundCounts.messages
|
|
|
}
|
|
|
});
|
|
|
return updated;
|
|
|
@@ -548,6 +622,8 @@ export function previewUserMerge({ sourceUserId, targetUserId }) {
|
|
|
domains: countRows('domains', source.id),
|
|
|
dnsCredentials: countRows('dns_credentials', source.id),
|
|
|
apiTokens: countRows('api_tokens', source.id),
|
|
|
+ inboundMailboxes: countRows('inbound_mailboxes', source.id, 'deleted_at IS NULL'),
|
|
|
+ inboundMessages: countRows('inbound_messages', source.id, 'deleted_at IS NULL'),
|
|
|
sendEvents: countRows('send_events', source.id),
|
|
|
smtpCredential: sourceSmtpCredentials.length
|
|
|
};
|
|
|
@@ -563,6 +639,8 @@ export function previewUserMerge({ sourceUserId, targetUserId }) {
|
|
|
domains: counts.domains,
|
|
|
dnsCredentials: counts.dnsCredentials,
|
|
|
apiTokens: counts.apiTokens,
|
|
|
+ inboundMailboxes: defaultOptions.transferDomains ? counts.inboundMailboxes : 0,
|
|
|
+ inboundMessages: defaultOptions.transferDomains ? counts.inboundMessages : 0,
|
|
|
sendEvents: counts.sendEvents,
|
|
|
smtpCredential: defaultOptions.transferSmtpCredential ? counts.smtpCredential : 0
|
|
|
};
|
|
|
@@ -592,10 +670,15 @@ export function executeUserMerge({ actorUserId, sourceUserId, targetUserId, opti
|
|
|
if (confirmation !== preview.confirmationText) throw new Error('确认文本不匹配。');
|
|
|
const sourceId = preview.sourceUser.id;
|
|
|
const targetId = preview.targetUser.id;
|
|
|
+ const inboundCounts = options.transferDomains === false
|
|
|
+ ? { mailboxes: 0, messages: 0 }
|
|
|
+ : moveInboundResourcesForUserDomains(sourceId, targetId);
|
|
|
const counts = {
|
|
|
domains: options.transferDomains === false ? 0 : moveRows('domains', sourceId, targetId),
|
|
|
dnsCredentials: options.transferDnsCredentials === false ? 0 : moveRows('dns_credentials', sourceId, targetId),
|
|
|
apiTokens: options.transferApiTokens === false ? 0 : moveRows('api_tokens', sourceId, targetId),
|
|
|
+ inboundMailboxes: inboundCounts.mailboxes,
|
|
|
+ inboundMessages: inboundCounts.messages,
|
|
|
sendEvents: options.transferSendEvents === false ? 0 : moveRows('send_events', sourceId, targetId),
|
|
|
smtpCredential: 0
|
|
|
};
|
|
|
@@ -748,6 +831,163 @@ export function createDomain(userId, domain) {
|
|
|
return getDomain(result.lastInsertRowid, { userId });
|
|
|
}
|
|
|
|
|
|
+export function createInboundMailbox(userId, mailbox = {}) {
|
|
|
+ const address = normalizeInboundAddress(mailbox.address);
|
|
|
+ if (!address) throw new Error('收信邮箱格式不正确。');
|
|
|
+ const [localPart, domainName] = address.split('@');
|
|
|
+ const domain = getDomainByName(domainName, { userId });
|
|
|
+ if (!domain) throw new Error('收信域名不存在。');
|
|
|
+ const createdAt = now();
|
|
|
+ const result = requireDb()
|
|
|
+ .prepare(`
|
|
|
+ INSERT INTO inbound_mailboxes (
|
|
|
+ user_id, domain_id, address, local_part, display_name, status, created_at, updated_at
|
|
|
+ ) VALUES (?, ?, ?, ?, ?, 'active', ?, ?)
|
|
|
+ `)
|
|
|
+ .run(
|
|
|
+ userId,
|
|
|
+ domain.id,
|
|
|
+ address,
|
|
|
+ localPart,
|
|
|
+ String(mailbox.displayName || '').trim(),
|
|
|
+ createdAt,
|
|
|
+ createdAt
|
|
|
+ );
|
|
|
+ return getInboundMailbox(result.lastInsertRowid, userId);
|
|
|
+}
|
|
|
+
|
|
|
+export function listInboundMailboxes(userId) {
|
|
|
+ return requireDb()
|
|
|
+ .prepare(`
|
|
|
+ SELECT
|
|
|
+ m.*,
|
|
|
+ d.domain,
|
|
|
+ COUNT(msg.id) AS message_count,
|
|
|
+ COALESCE(SUM(CASE WHEN msg.read_state = 'false' THEN 1 ELSE 0 END), 0) AS unread_count,
|
|
|
+ MAX(msg.received_at) AS last_message_at
|
|
|
+ FROM inbound_mailboxes m
|
|
|
+ JOIN domains d ON d.id = m.domain_id
|
|
|
+ LEFT JOIN inbound_messages msg ON msg.mailbox_id = m.id AND msg.deleted_at IS NULL
|
|
|
+ WHERE m.user_id = ? AND m.deleted_at IS NULL
|
|
|
+ GROUP BY m.id
|
|
|
+ ORDER BY COALESCE(last_message_at, m.created_at) DESC, m.id DESC
|
|
|
+ `)
|
|
|
+ .all(userId)
|
|
|
+ .map(publicInboundMailbox);
|
|
|
+}
|
|
|
+
|
|
|
+export function getInboundMailbox(id, userId) {
|
|
|
+ const row = requireDb()
|
|
|
+ .prepare(`
|
|
|
+ SELECT
|
|
|
+ m.*,
|
|
|
+ d.domain,
|
|
|
+ COUNT(msg.id) AS message_count,
|
|
|
+ COALESCE(SUM(CASE WHEN msg.read_state = 'false' THEN 1 ELSE 0 END), 0) AS unread_count,
|
|
|
+ MAX(msg.received_at) AS last_message_at
|
|
|
+ FROM inbound_mailboxes m
|
|
|
+ JOIN domains d ON d.id = m.domain_id
|
|
|
+ LEFT JOIN inbound_messages msg ON msg.mailbox_id = m.id AND msg.deleted_at IS NULL
|
|
|
+ WHERE m.id = ? AND m.user_id = ? AND m.deleted_at IS NULL
|
|
|
+ GROUP BY m.id
|
|
|
+ `)
|
|
|
+ .get(Number(id), userId);
|
|
|
+ return publicInboundMailbox(row);
|
|
|
+}
|
|
|
+
|
|
|
+export function getInboundMailboxByAddress(address) {
|
|
|
+ const cleanAddress = normalizeInboundAddress(address);
|
|
|
+ if (!cleanAddress) return null;
|
|
|
+ const row = requireDb()
|
|
|
+ .prepare(`
|
|
|
+ SELECT m.*, d.domain, 0 AS message_count, 0 AS unread_count, NULL AS last_message_at
|
|
|
+ FROM inbound_mailboxes m
|
|
|
+ JOIN domains d ON d.id = m.domain_id
|
|
|
+ JOIN users u ON u.id = m.user_id
|
|
|
+ WHERE m.address = ?
|
|
|
+ AND m.status = 'active'
|
|
|
+ AND m.deleted_at IS NULL
|
|
|
+ AND u.status = 'active'
|
|
|
+ LIMIT 1
|
|
|
+ `)
|
|
|
+ .get(cleanAddress);
|
|
|
+ return publicInboundMailbox(row);
|
|
|
+}
|
|
|
+
|
|
|
+export function createInboundMessage(mailbox, message = {}) {
|
|
|
+ if (!mailbox?.id || !mailbox?.userId || !mailbox?.domainId) throw new Error('收信邮箱不存在。');
|
|
|
+ const receivedAt = message.receivedAt || now();
|
|
|
+ const textBody = String(message.textBody || '');
|
|
|
+ const htmlBody = String(message.htmlBody || '');
|
|
|
+ const rawMessage = String(message.rawMessage || '');
|
|
|
+ const result = requireDb()
|
|
|
+ .prepare(`
|
|
|
+ INSERT INTO inbound_messages (
|
|
|
+ mailbox_id, user_id, domain_id, sender, recipients_json, subject, message_id,
|
|
|
+ raw_message, text_body, html_body, preview, read_state, received_at, created_at, updated_at
|
|
|
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'false', ?, ?, ?)
|
|
|
+ `)
|
|
|
+ .run(
|
|
|
+ mailbox.id,
|
|
|
+ mailbox.userId,
|
|
|
+ mailbox.domainId,
|
|
|
+ normalizeEmail(message.sender) || String(message.sender || '').trim(),
|
|
|
+ JSON.stringify(normalizeRecipientList(message.recipients)),
|
|
|
+ String(message.subject || '').trim() || '(no subject)',
|
|
|
+ String(message.messageId || '').trim(),
|
|
|
+ rawMessage,
|
|
|
+ textBody,
|
|
|
+ htmlBody,
|
|
|
+ inboundPreview(textBody || htmlToText(htmlBody) || rawMessage),
|
|
|
+ receivedAt,
|
|
|
+ receivedAt,
|
|
|
+ receivedAt
|
|
|
+ );
|
|
|
+ return getInboundMessage(mailbox.userId, result.lastInsertRowid);
|
|
|
+}
|
|
|
+
|
|
|
+export function listInboundMessages(userId, { mailboxId = null } = {}) {
|
|
|
+ const where = ['msg.user_id = ?', 'msg.deleted_at IS NULL'];
|
|
|
+ const params = [userId];
|
|
|
+ if (mailboxId) {
|
|
|
+ where.push('msg.mailbox_id = ?');
|
|
|
+ params.push(Number(mailboxId));
|
|
|
+ }
|
|
|
+ return requireDb()
|
|
|
+ .prepare(`
|
|
|
+ SELECT msg.*, m.address AS mailbox_address, d.domain
|
|
|
+ FROM inbound_messages msg
|
|
|
+ JOIN inbound_mailboxes m ON m.id = msg.mailbox_id
|
|
|
+ JOIN domains d ON d.id = msg.domain_id
|
|
|
+ WHERE ${where.join(' AND ')}
|
|
|
+ ORDER BY msg.received_at DESC, msg.id DESC
|
|
|
+ `)
|
|
|
+ .all(...params)
|
|
|
+ .map((row) => publicInboundMessage(row));
|
|
|
+}
|
|
|
+
|
|
|
+export function getInboundMessage(userId, id) {
|
|
|
+ const row = requireDb()
|
|
|
+ .prepare(`
|
|
|
+ SELECT msg.*, m.address AS mailbox_address, d.domain
|
|
|
+ FROM inbound_messages msg
|
|
|
+ JOIN inbound_mailboxes m ON m.id = msg.mailbox_id
|
|
|
+ JOIN domains d ON d.id = msg.domain_id
|
|
|
+ WHERE msg.id = ? AND msg.user_id = ? AND msg.deleted_at IS NULL
|
|
|
+ `)
|
|
|
+ .get(Number(id), userId);
|
|
|
+ return publicInboundMessage(row, { includeBody: true });
|
|
|
+}
|
|
|
+
|
|
|
+export function markInboundMessageRead(userId, id, read = true) {
|
|
|
+ const updatedAt = now();
|
|
|
+ const result = requireDb()
|
|
|
+ .prepare('UPDATE inbound_messages SET read_state = ?, updated_at = ? WHERE id = ? AND user_id = ? AND deleted_at IS NULL')
|
|
|
+ .run(read ? 'true' : 'false', updatedAt, Number(id), userId);
|
|
|
+ if (!result.changes) return null;
|
|
|
+ return getInboundMessage(userId, id);
|
|
|
+}
|
|
|
+
|
|
|
export function updateDomain(id, userId, patch) {
|
|
|
const current = getDomain(id, { userId, includePrivate: true });
|
|
|
if (!current) return null;
|
|
|
@@ -2562,13 +2802,16 @@ function mergeResourcesForUser(userId) {
|
|
|
domains: listDomains(userId),
|
|
|
dnsCredentials: listDnsCredentials(userId),
|
|
|
apiTokens: listApiTokens(userId),
|
|
|
+ inboundMailboxes: listInboundMailboxes(userId),
|
|
|
+ inboundMessageCount: countRows('inbound_messages', userId, 'deleted_at IS NULL'),
|
|
|
sendEventCount: countRows('send_events', userId),
|
|
|
smtpCredential: getSmtpCredential(userId)
|
|
|
};
|
|
|
}
|
|
|
|
|
|
-function countRows(table, userId) {
|
|
|
- return Number(requireDb().prepare(`SELECT COUNT(*) AS count FROM ${mergeResourceTable(table)} WHERE user_id = ?`).get(userId).count || 0);
|
|
|
+function countRows(table, userId, extraWhere = '') {
|
|
|
+ const suffix = extraWhere ? ` AND ${extraWhere}` : '';
|
|
|
+ return Number(requireDb().prepare(`SELECT COUNT(*) AS count FROM ${mergeResourceTable(table)} WHERE user_id = ?${suffix}`).get(userId).count || 0);
|
|
|
}
|
|
|
|
|
|
function moveRows(table, sourceUserId, targetUserId) {
|
|
|
@@ -2579,12 +2822,52 @@ function moveRows(table, sourceUserId, targetUserId) {
|
|
|
}
|
|
|
|
|
|
function mergeResourceTable(table) {
|
|
|
- if (!['domains', 'dns_credentials', 'api_tokens', 'send_events', 'smtp_credentials'].includes(table)) {
|
|
|
+ if (!['domains', 'dns_credentials', 'api_tokens', 'inbound_mailboxes', 'inbound_messages', 'send_events', 'smtp_credentials'].includes(table)) {
|
|
|
throw new Error('资源类型不正确。');
|
|
|
}
|
|
|
return table;
|
|
|
}
|
|
|
|
|
|
+function moveInboundDomainResources(domainId, targetUserId) {
|
|
|
+ const updatedAt = now();
|
|
|
+ const mailboxResult = requireDb()
|
|
|
+ .prepare('UPDATE inbound_mailboxes SET user_id = ?, updated_at = ? WHERE domain_id = ? AND deleted_at IS NULL')
|
|
|
+ .run(targetUserId, updatedAt, domainId);
|
|
|
+ const messageResult = requireDb()
|
|
|
+ .prepare('UPDATE inbound_messages SET user_id = ?, updated_at = ? WHERE domain_id = ? AND deleted_at IS NULL')
|
|
|
+ .run(targetUserId, updatedAt, domainId);
|
|
|
+ return {
|
|
|
+ mailboxes: mailboxResult.changes,
|
|
|
+ messages: messageResult.changes
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+function moveInboundResourcesForUserDomains(sourceUserId, targetUserId) {
|
|
|
+ const updatedAt = now();
|
|
|
+ const mailboxResult = requireDb()
|
|
|
+ .prepare(`
|
|
|
+ UPDATE inbound_mailboxes
|
|
|
+ SET user_id = ?, updated_at = ?
|
|
|
+ WHERE user_id = ?
|
|
|
+ AND deleted_at IS NULL
|
|
|
+ AND domain_id IN (SELECT id FROM domains WHERE user_id = ?)
|
|
|
+ `)
|
|
|
+ .run(targetUserId, updatedAt, sourceUserId, sourceUserId);
|
|
|
+ const messageResult = requireDb()
|
|
|
+ .prepare(`
|
|
|
+ UPDATE inbound_messages
|
|
|
+ SET user_id = ?, updated_at = ?
|
|
|
+ WHERE user_id = ?
|
|
|
+ AND deleted_at IS NULL
|
|
|
+ AND domain_id IN (SELECT id FROM domains WHERE user_id = ?)
|
|
|
+ `)
|
|
|
+ .run(targetUserId, updatedAt, sourceUserId, sourceUserId);
|
|
|
+ return {
|
|
|
+ mailboxes: mailboxResult.changes,
|
|
|
+ messages: messageResult.changes
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
function requireDomainRow(domainId) {
|
|
|
const domain = requireDb().prepare('SELECT * FROM domains WHERE id = ?').get(Number(domainId));
|
|
|
if (!domain) throw new Error('域名不存在。');
|
|
|
@@ -2744,6 +3027,51 @@ function privateDomainRow(row) {
|
|
|
return publicRow ? { ...publicRow, dkimPrivate: row.dkim_private } : null;
|
|
|
}
|
|
|
|
|
|
+function publicInboundMailbox(row) {
|
|
|
+ if (!row) return null;
|
|
|
+ return {
|
|
|
+ id: row.id,
|
|
|
+ userId: row.user_id,
|
|
|
+ domainId: row.domain_id,
|
|
|
+ domain: row.domain || '',
|
|
|
+ address: row.address,
|
|
|
+ localPart: row.local_part,
|
|
|
+ displayName: row.display_name,
|
|
|
+ status: row.status,
|
|
|
+ messageCount: Number(row.message_count || 0),
|
|
|
+ unreadCount: Number(row.unread_count || 0),
|
|
|
+ lastMessageAt: row.last_message_at || null,
|
|
|
+ createdAt: row.created_at,
|
|
|
+ updatedAt: row.updated_at
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+function publicInboundMessage(row, { includeBody = false } = {}) {
|
|
|
+ if (!row) return null;
|
|
|
+ return {
|
|
|
+ id: row.id,
|
|
|
+ mailboxId: row.mailbox_id,
|
|
|
+ userId: row.user_id,
|
|
|
+ domainId: row.domain_id,
|
|
|
+ domain: row.domain || '',
|
|
|
+ mailboxAddress: row.mailbox_address || '',
|
|
|
+ sender: row.sender,
|
|
|
+ recipients: safeJson(row.recipients_json, []),
|
|
|
+ subject: row.subject,
|
|
|
+ messageId: row.message_id,
|
|
|
+ preview: row.preview,
|
|
|
+ read: row.read_state === 'true',
|
|
|
+ receivedAt: row.received_at,
|
|
|
+ createdAt: row.created_at,
|
|
|
+ updatedAt: row.updated_at,
|
|
|
+ ...(includeBody ? {
|
|
|
+ rawMessage: row.raw_message,
|
|
|
+ textBody: row.text_body,
|
|
|
+ htmlBody: row.html_body
|
|
|
+ } : {})
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
function publicSmtpCredential(row, { includeHash = false, includePassword = false, includeSecret = false } = {}) {
|
|
|
if (!row) return null;
|
|
|
const password = includePassword ? decryptSecret(row.password_secret) : '';
|
|
|
@@ -3123,6 +3451,39 @@ function normalizeEmail(value) {
|
|
|
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) ? email : '';
|
|
|
}
|
|
|
|
|
|
+function normalizeInboundAddress(value) {
|
|
|
+ const email = normalizeEmail(value);
|
|
|
+ if (!email) return '';
|
|
|
+ const [localPart, domain] = email.split('@');
|
|
|
+ if (!localPart || !domain) return '';
|
|
|
+ return `${localPart}@${domain}`;
|
|
|
+}
|
|
|
+
|
|
|
+function normalizeRecipientList(values) {
|
|
|
+ const list = Array.isArray(values) ? values : [values];
|
|
|
+ return [...new Set(list.map(normalizeEmail).filter(Boolean))];
|
|
|
+}
|
|
|
+
|
|
|
+function inboundPreview(value) {
|
|
|
+ return String(value || '')
|
|
|
+ .replace(/\s+/g, ' ')
|
|
|
+ .trim()
|
|
|
+ .slice(0, 240);
|
|
|
+}
|
|
|
+
|
|
|
+function htmlToText(value) {
|
|
|
+ return String(value || '')
|
|
|
+ .replace(/<style[\s\S]*?<\/style>/gi, ' ')
|
|
|
+ .replace(/<script[\s\S]*?<\/script>/gi, ' ')
|
|
|
+ .replace(/<[^>]+>/g, ' ')
|
|
|
+ .replace(/ /gi, ' ')
|
|
|
+ .replace(/&/gi, '&')
|
|
|
+ .replace(/</gi, '<')
|
|
|
+ .replace(/>/gi, '>')
|
|
|
+ .replace(/"/gi, '"')
|
|
|
+ .replace(/'/g, "'");
|
|
|
+}
|
|
|
+
|
|
|
function normalizePort(value, fallback) {
|
|
|
const port = Number(value);
|
|
|
return Number.isInteger(port) && port > 0 && port <= 65535 ? port : fallback;
|