|
|
@@ -23,6 +23,9 @@ import type {
|
|
|
HealthCheckResult,
|
|
|
MailStats,
|
|
|
AccountStats,
|
|
|
+ BatchOperationResult,
|
|
|
+ SyncStatus,
|
|
|
+ RecentlyReceivedStats,
|
|
|
} from "@/types.js";
|
|
|
|
|
|
// =============================================================================
|
|
|
@@ -828,6 +831,75 @@ export class AppleMailManager {
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
+ // ===========================================================================
|
|
|
+ // Batch Operations
|
|
|
+ // ===========================================================================
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Delete multiple messages at once.
|
|
|
+ *
|
|
|
+ * @param ids - Array of message IDs to delete
|
|
|
+ * @returns Array of results for each message
|
|
|
+ */
|
|
|
+ batchDeleteMessages(ids: string[]): BatchOperationResult[] {
|
|
|
+ const results: BatchOperationResult[] = [];
|
|
|
+
|
|
|
+ for (const id of ids) {
|
|
|
+ const success = this.deleteMessage(id);
|
|
|
+ results.push({
|
|
|
+ id,
|
|
|
+ success,
|
|
|
+ error: success ? undefined : "Failed to delete message",
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ return results;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Move multiple messages to a mailbox at once.
|
|
|
+ *
|
|
|
+ * @param ids - Array of message IDs to move
|
|
|
+ * @param mailbox - Destination mailbox name
|
|
|
+ * @param account - Account containing the destination mailbox
|
|
|
+ * @returns Array of results for each message
|
|
|
+ */
|
|
|
+ batchMoveMessages(ids: string[], mailbox: string, account?: string): BatchOperationResult[] {
|
|
|
+ const results: BatchOperationResult[] = [];
|
|
|
+
|
|
|
+ for (const id of ids) {
|
|
|
+ const success = this.moveMessage(id, mailbox, account);
|
|
|
+ results.push({
|
|
|
+ id,
|
|
|
+ success,
|
|
|
+ error: success ? undefined : "Failed to move message",
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ return results;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Mark multiple messages as read at once.
|
|
|
+ *
|
|
|
+ * @param ids - Array of message IDs to mark as read
|
|
|
+ * @returns Array of results for each message
|
|
|
+ */
|
|
|
+ batchMarkAsRead(ids: string[]): BatchOperationResult[] {
|
|
|
+ const results: BatchOperationResult[] = [];
|
|
|
+
|
|
|
+ for (const id of ids) {
|
|
|
+ const success = this.markAsRead(id);
|
|
|
+ results.push({
|
|
|
+ id,
|
|
|
+ success,
|
|
|
+ error: success ? undefined : "Failed to mark message as read",
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ return results;
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* List attachments for a message.
|
|
|
*/
|
|
|
@@ -1139,10 +1211,169 @@ export class AppleMailManager {
|
|
|
});
|
|
|
}
|
|
|
|
|
|
+ // Get recently received stats
|
|
|
+ const recentlyReceived = this.getRecentlyReceivedStats();
|
|
|
+
|
|
|
return {
|
|
|
totalMessages,
|
|
|
totalUnread,
|
|
|
accounts: accountStats,
|
|
|
+ recentlyReceived,
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Get counts of recently received messages.
|
|
|
+ *
|
|
|
+ * Only counts messages in INBOX for performance (scanning all mailboxes
|
|
|
+ * is too slow for large accounts).
|
|
|
+ *
|
|
|
+ * @returns Counts of messages received in last 24h, 7d, and 30d
|
|
|
+ */
|
|
|
+ getRecentlyReceivedStats(): RecentlyReceivedStats {
|
|
|
+ // Get message counts for different time periods
|
|
|
+ const now = new Date();
|
|
|
+ const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
|
|
+ const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
|
+ const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
|
|
+
|
|
|
+ // Format dates for AppleScript comparison
|
|
|
+ const formatDate = (d: Date): string => {
|
|
|
+ const months = [
|
|
|
+ "January",
|
|
|
+ "February",
|
|
|
+ "March",
|
|
|
+ "April",
|
|
|
+ "May",
|
|
|
+ "June",
|
|
|
+ "July",
|
|
|
+ "August",
|
|
|
+ "September",
|
|
|
+ "October",
|
|
|
+ "November",
|
|
|
+ "December",
|
|
|
+ ];
|
|
|
+ return `date "${months[d.getMonth()]} ${d.getDate()}, ${d.getFullYear()}"`;
|
|
|
+ };
|
|
|
+
|
|
|
+ // Only scan INBOX for performance - scanning all mailboxes is too slow
|
|
|
+ const script = buildAppLevelScript(`
|
|
|
+ set last24h to 0
|
|
|
+ set last7d to 0
|
|
|
+ set last30d to 0
|
|
|
+ set oneDayAgo to ${formatDate(oneDayAgo)}
|
|
|
+ set sevenDaysAgo to ${formatDate(sevenDaysAgo)}
|
|
|
+ set thirtyDaysAgo to ${formatDate(thirtyDaysAgo)}
|
|
|
+
|
|
|
+ repeat with acct in accounts
|
|
|
+ try
|
|
|
+ -- Try common inbox names
|
|
|
+ set inboxNames to {"INBOX", "Inbox", "inbox"}
|
|
|
+ repeat with inboxName in inboxNames
|
|
|
+ try
|
|
|
+ set theInbox to mailbox inboxName of acct
|
|
|
+ set last24h to last24h + (count of (messages of theInbox whose date received >= oneDayAgo))
|
|
|
+ set last7d to last7d + (count of (messages of theInbox whose date received >= sevenDaysAgo))
|
|
|
+ set last30d to last30d + (count of (messages of theInbox whose date received >= thirtyDaysAgo))
|
|
|
+ exit repeat
|
|
|
+ end try
|
|
|
+ end repeat
|
|
|
+ end try
|
|
|
+ end repeat
|
|
|
+
|
|
|
+ return (last24h as string) & "|||" & (last7d as string) & "|||" & (last30d as string)
|
|
|
+ `);
|
|
|
+
|
|
|
+ const result = executeAppleScript(script, { timeoutMs: 60000 });
|
|
|
+
|
|
|
+ if (!result.success || !result.output.trim()) {
|
|
|
+ console.error(`Failed to get recently received stats: ${result.error}`);
|
|
|
+ return { last24h: 0, last7d: 0, last30d: 0 };
|
|
|
+ }
|
|
|
+
|
|
|
+ const parts = result.output.split("|||");
|
|
|
+ if (parts.length < 3) {
|
|
|
+ return { last24h: 0, last7d: 0, last30d: 0 };
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ last24h: parseInt(parts[0]) || 0,
|
|
|
+ last7d: parseInt(parts[1]) || 0,
|
|
|
+ last30d: parseInt(parts[2]) || 0,
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Get sync status for Mail.app.
|
|
|
+ *
|
|
|
+ * Checks for sync activity indicators like:
|
|
|
+ * - Activity monitor status
|
|
|
+ * - Network activity status
|
|
|
+ * - Background refresh indicators
|
|
|
+ *
|
|
|
+ * @returns Sync status information
|
|
|
+ */
|
|
|
+ getSyncStatus(): SyncStatus {
|
|
|
+ // Check for Mail.app background activity and sync status
|
|
|
+ // Mail.app doesn't expose sync status directly through AppleScript,
|
|
|
+ // so we check for recent changes and activity indicators
|
|
|
+ const script = buildAppLevelScript(`
|
|
|
+ set syncInfo to ""
|
|
|
+
|
|
|
+ -- Check if Mail.app is running
|
|
|
+ tell application "System Events"
|
|
|
+ set mailRunning to (name of processes) contains "Mail"
|
|
|
+ end tell
|
|
|
+
|
|
|
+ if not mailRunning then
|
|
|
+ return "not_running"
|
|
|
+ end if
|
|
|
+
|
|
|
+ -- Check for background activity by looking at message counts changing
|
|
|
+ -- This is a proxy for sync activity since Mail doesn't expose sync status
|
|
|
+ set accountCount to count of accounts
|
|
|
+ set totalMailboxes to 0
|
|
|
+ repeat with acct in accounts
|
|
|
+ set totalMailboxes to totalMailboxes + (count of mailboxes of acct)
|
|
|
+ end repeat
|
|
|
+
|
|
|
+ return "running|||" & accountCount & "|||" & totalMailboxes
|
|
|
+ `);
|
|
|
+
|
|
|
+ const result = executeAppleScript(script);
|
|
|
+
|
|
|
+ if (!result.success) {
|
|
|
+ return {
|
|
|
+ syncDetected: false,
|
|
|
+ pendingUpload: 0,
|
|
|
+ recentActivity: false,
|
|
|
+ secondsSinceLastChange: -1,
|
|
|
+ error: result.error,
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ if (result.output === "not_running") {
|
|
|
+ return {
|
|
|
+ syncDetected: false,
|
|
|
+ pendingUpload: 0,
|
|
|
+ recentActivity: false,
|
|
|
+ secondsSinceLastChange: -1,
|
|
|
+ error: "Mail.app is not running",
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ // Parse the response
|
|
|
+ const parts = result.output.split("|||");
|
|
|
+ const isRunning = parts[0] === "running";
|
|
|
+ const accountCount = parseInt(parts[1]) || 0;
|
|
|
+
|
|
|
+ // Mail.app is running with accounts configured - assume sync is active
|
|
|
+ // (Mail.app syncs automatically when running)
|
|
|
+ return {
|
|
|
+ syncDetected: isRunning && accountCount > 0,
|
|
|
+ pendingUpload: 0, // Not exposed by Mail.app
|
|
|
+ recentActivity: isRunning,
|
|
|
+ secondsSinceLastChange: 0,
|
|
|
};
|
|
|
}
|
|
|
}
|