Explore the GmailAI-iOS codebase at /Users/neog/Apps/iOS/GmailAI-iOS to understand the current filtering system.

Find:
1. How filters are currently implemented (look for FilterSheet, filter-related code in InboxView, EmailStore)
2. How the filtering UI works (look for showingFilters, FilterSheet view)
3. Current filter options (category, priority, unread)
4. How filteredEmails computed property works in EmailStore

I need to understand the pattern so we can add date filtering (Today, Yesterday, Last 7 days, Last 30 days, Custom range).

Return the key files and code patterns used for filtering.


I'll explore the GmailAI-iOS codebase to understand the current filtering system. Let me start by getting an overview of the project structure and then finding the relevant filter-related code.


Bash

find /Users/neog/Apps/iOS/GmailAI-iOS -type f -name "*.swift" | head -20

List Swift files in GmailAI-iOS project


Error: EISDIR: illegal operation on a directory, read


Now let me search for the key filtering-related files:


/Users/neog/Apps/iOS/GmailAI-iOS/Views/InboxComponents.swift
/Users/neog/Apps/iOS/GmailAI-iOS/Views/InboxView.swift


Perfect! Now let me examine the key filtering files:


     1→import Foundation
     2→
     3→// MARK: - EmailStore Filtering Extension
     4→
     5→extension EmailStore {
     6→
     7→    // MARK: - Filtered Emails
     8→
     9→    var filteredEmails: [Email] {
    10→        emails.filter { email in
    11→            let matchesSearch = searchQuery.isEmpty ||
    12→                email.subject.localizedCaseInsensitiveContains(searchQuery) ||
    13→                email.from.localizedCaseInsensitiveContains(searchQuery) ||
    14→                email.snippet.localizedCaseInsensitiveContains(searchQuery)
    15→
    16→            let matchesCategory = selectedCategory == nil ||
    17→                email.analysis?.category == selectedCategory
    18→
    19→            let matchesPriority = selectedPriority == nil ||
    20→                email.analysis?.priority == selectedPriority
    21→
    22→            let matchesUnread = !showUnreadOnly || !email.isRead
    23→
    24→            return matchesSearch && matchesCategory && matchesPriority && matchesUnread
    25→        }
    26→        .sorted { $0.date > $1.date }
    27→    }
    28→
    29→    // MARK: - Smart Folders
    30→
    31→    /// Emails requiring user action (high priority, has action items, unread)
    32→    var requiresActionEmails: [Email] {
    33→        emails.filter { email in
    34→            guard let analysis = email.analysis else { return false }
    35→            return (analysis.priority == .high || analysis.priority == .critical) &&
    36→                   analysis.hasActionItems &&
    37→                   !email.isRead
    38→        }
    39→        .sorted { $0.date > $1.date }
    40→    }
    41→
    42→    /// Emails AI recommends as safe to delete (importanceScore < 30, not suspicious)
    43→    var safeToDeleteEmails: [Email] {
    44→        emails.filter { email in
    45→            guard let analysis = email.analysis else { return false }
    46→            return analysis.deleteRecommendation == .safeToDelete
    47→        }
    48→        .sorted { ($0.analysis?.importanceScore ?? 100) < ($1.analysis?.importanceScore ?? 100) }
    49→    }
    50→
    51→    /// Emails that need review before deletion (importanceScore 30-60)
    52→    var reviewEmails: [Email] {
    53→        emails.filter { email in
    54→            guard let analysis = email.analysis else { return false }
    55→            return analysis.deleteRecommendation == .review
    56→        }
    57→        .sorted { $0.date > $1.date }
    58→    }
    59→
    60→    /// Suspicious emails (phishing, spam)
    61→    var suspiciousEmails: [Email] {
    62→        emails.filter { email in
    63→            guard let analysis = email.analysis else { return false }
    64→            return analysis.deleteRecommendation == .suspicious
    65→        }
    66→        .sorted { $0.date > $1.date }
    67→    }
    68→
    69→    /// Old unread emails (> 7 days, unread)
    70→    var oldUnreadEmails: [Email] {
    71→        let sevenDaysAgo = Calendar.current.date(byAdding: .day, value: -7, to: Date()) ?? Date()
    72→        return emails.filter { email in
    73→            !email.isRead && email.date < sevenDaysAgo
    74→        }
    75→        .sorted { $0.date < $1.date }
    76→    }
    77→
    78→    /// Important emails to keep (importanceScore >= 60)
    79→    var importantEmails: [Email] {
    80→        emails.filter { email in
    81→            guard let analysis = email.analysis else { return false }
    82→            return analysis.deleteRecommendation == .keep
    83→        }
    84→        .sorted { ($0.analysis?.importanceScore ?? 0) > ($1.analysis?.importanceScore ?? 0) }
    85→    }
    86→
    87→    // MARK: - Cleanup Statistics
    88→
    89→    var cleanupStats: CleanupStats {
    90→        CleanupStats(
    91→            safeToDeleteCount: safeToDeleteEmails.count,
    92→            reviewCount: reviewEmails.count,
    93→            suspiciousCount: suspiciousEmails.count,
    94→            oldUnreadCount: oldUnreadEmails.count,
    95→            requiresActionCount: requiresActionEmails.count,
    96→            importantCount: importantEmails.count
    97→        )
    98→    }
    99→
   100→    /// Group safe-to-delete by category for review UI
   101→    var safeToDeleteByCategory: [EmailAnalysis.Category: [Email]] {
   102→        Dictionary(grouping: safeToDeleteEmails) { email in
   103→            email.analysis?.category ?? .other
   104→        }
   105→    }
   106→}
   107→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

import Foundation

// MARK: - EmailStore Filtering Extension

extension EmailStore {

    // MARK: - Filtered Emails

    var filteredEmails: [Email] {
        emails.filter { email in
            let matchesSearch = searchQuery.isEmpty ||
                email.subject.localizedCaseInsensitiveContains(searchQuery) ||
                email.from.localizedCaseInsensitiveContains(searchQuery) ||
                email.snippet.localizedCaseInsensitiveContains(searchQuery)

            let matchesCategory = selectedCategory == nil ||
                email.analysis?.category == selectedCategory

            let matchesPriority = selectedPriority == nil ||
                email.analysis?.priority == selectedPriority

            let matchesUnread = !showUnreadOnly || !email.isRead

            return matchesSearch && matchesCategory && matchesPriority && matchesUnread
        }
        .sorted { $0.date > $1.date }
    }

    // MARK: - Smart Folders

    /// Emails requiring user action (high priority, has action items, unread)
    var requiresActionEmails: [Email] {
        emails.filter { email in
            guard let analysis = email.analysis else { return false }
            return (analysis.priority == .high || analysis.priority == .critical) &&
                   analysis.hasActionItems &&
                   !email.isRead
        }
        .sorted { $0.date > $1.date }
    }

    /// Emails AI recommends as safe to delete (importanceScore < 30, not suspicious)
    var safeToDeleteEmails: [Email] {
        emails.filter { email in
            guard let analysis = email.analysis else { return false }
            return analysis.deleteRecommendation == .safeToDelete
        }
        .sorted { ($0.analysis?.importanceScore ?? 100) < ($1.analysis?.importanceScore ?? 100) }
    }

    /// Emails that need review before deletion (importanceScore 30-60)
    var reviewEmails: [Email] {
        emails.filter { email in
            guard let analysis = email.analysis else { return false }
            return analysis.deleteRecommendation == .review
        }
        .sorted { $0.date > $1.date }
    }

    /// Suspicious emails (phishing, spam)
    var suspiciousEmails: [Email] {
        emails.filter { email in
            guard let analysis = email.analysis else { return false }
            return analysis.deleteRecommendation == .suspicious
        }
        .sorted { $0.date > $1.date }
    }

    /// Old unread emails (> 7 days, unread)
    var oldUnreadEmails: [Email] {
        let sevenDaysAgo = Calendar.current.date(byAdding: .day, value: -7, to: Date()) ?? Date()
        return emails.filter { email in
            !email.isRead && email.date < sevenDaysAgo
        }
        .sorted { $0.date < $1.date }
    }

    /// Important emails to keep (importanceScore >= 60)
    var importantEmails: [Email] {
        emails.filter { email in
            guard let analysis = email.analysis else { return false }
            return analysis.deleteRecommendation == .keep
        }
        .sorted { ($0.analysis?.importanceScore ?? 0) > ($1.analysis?.importanceScore ?? 0) }
    }

    // MARK: - Cleanup Statistics

    var cleanupStats: CleanupStats {
        CleanupStats(
            safeToDeleteCount: safeToDeleteEmails.count,
            reviewCount: reviewEmails.count,
            suspiciousCount: suspiciousEmails.count,
            oldUnreadCount: oldUnreadEmails.count,
            requiresActionCount: requiresActionEmails.count,
            importantCount: importantEmails.count
        )
    }

    /// Group safe-to-delete by category for review UI
    var safeToDeleteByCategory: [EmailAnalysis.Category: [Email]] {
        Dictionary(grouping: safeToDeleteEmails) { email in
            email.analysis?.category ?? .other
        }
    }
}


     1→import SwiftUI
     2→
     3→struct InboxView: View {
     4→    @EnvironmentObject var emailStore: EmailStore
     5→    @StateObject private var draftService = DraftService.shared
     6→    @State private var showingFilters = false
     7→    @State private var selectedEmail: Email?
     8→    @State private var showingCompose = false
     9→    @State private var editingDraft: EmailDraft?
    10→    @State private var toast: ToastData?
    11→    @State private var showingSearch = false
    12→    @State private var showingLabels = false
    13→    @State private var isSelectMode = false
    14→    @State private var selectedEmails: Set<String> = []
    15→    @State private var recentlyDeletedEmail: Email?
    16→    @State private var recentlyArchivedEmail: Email?
    17→    @State private var showDraftsSection = true
    18→    @AppStorage("groupByThread") private var groupByThread = false
    19→
    20→    var body: some View {
    21→        NavigationStack {
    22→            ZStack(alignment: .bottomTrailing) {
    23→                Group {
    24→                    if emailStore.isLoading && emailStore.emails.isEmpty {
    25→                        LoadingView()
    26→                    } else if emailStore.emails.isEmpty {
    27→                        EmptyInboxView {
    28→                            Task {
    29→                                await emailStore.loadInitialEmails()
    30→                            }
    31→                        }
    32→                    } else {
    33→                        emailList
    34→                    }
    35→                }
    36→
    37→                // Floating Action Button for Compose
    38→                composeButton
    39→            }
    40→            .navigationTitle("Inbox (\(emailStore.totalEmailsLoaded))")
    41→            .toolbar { toolbarContent }
    42→            .searchable(text: $emailStore.searchQuery, prompt: "Search emails")
    43→            .sheet(isPresented: $showingFilters) {
    44→                FilterSheet()
    45→            }
    46→            .sheet(item: $selectedEmail) { email in
    47→                NavigationStack {
    48→                    EmailDetailView(email: email)
    49→                }
    50→            }
    51→            .sheet(isPresented: $showingCompose) {
    52→                ComposeView()
    53→            }
    54→            .sheet(item: $editingDraft) { draft in
    55→                ComposeView(existingDraft: draft)
    56→            }
    57→            .sheet(isPresented: $showingSearch) {
    58→                SearchView()
    59→            }
    60→            .sheet(isPresented: $showingLabels) {
    61→                LabelsView()
    62→            }
    63→            .refreshable {
    64→                AppLogger.userAction("Pull to refresh triggered", level: .info)
    65→                HapticManager.refresh()
    66→                await emailStore.refreshEmails()
    67→            }
    68→            .toast($toast)
    69→            .onChange(of: emailStore.error) { _, newError in
    70→                if let error = newError {
    71→                    toast = .error(friendlyErrorMessage(error))
    72→                }
    73→            }
    74→        }
    75→        .task {
    76→            if emailStore.emails.isEmpty {
    77→                await emailStore.loadInitialEmails()
    78→            }
    79→        }
    80→        .onAppear {
    81→            AppLogger.lifecycle("InboxView appeared", level: .debug)
    82→        }
    83→        .onDisappear {
    84→            AppLogger.lifecycle("InboxView disappeared", level: .debug)
    85→        }
    86→    }
    87→
    88→    // MARK: - Compose Button
    89→
    90→    private var composeButton: some View {
    91→        Button {
    92→            AppLogger.userAction("Compose button tapped", level: .info)
    93→            showingCompose = true
    94→        } label: {
    95→            Image(systemName: "square.and.pencil")
    96→                .font(.title2)
    97→                .fontWeight(.semibold)
    98→                .foregroundStyle(.white)
    99→                .frame(width: 56, height: 56)
   100→                .background(Circle().fill(.blue))
   101→                .shadow(color: .black.opacity(0.3), radius: 4, x: 0, y: 2)
   102→        }
   103→        .accessibilityLabel("Compose new email")
   104→        .accessibilityHint("Opens the email composer")
   105→        .padding(.trailing, 20)
   106→        .padding(.bottom, 20)
   107→    }
   108→
   109→    // MARK: - Toolbar
   110→
   111→    @ToolbarContentBuilder
   112→    private var toolbarContent: some ToolbarContent {
   113→        ToolbarItem(placement: .topBarLeading) {
   114→            if isSelectMode {
   115→                Button("Cancel") {
   116→                    isSelectMode = false
   117→                    selectedEmails.removeAll()
   118→                }
   119→            } else {
   120→                HStack(spacing: 16) {
   121→                    Button {
   122→                        showingFilters.toggle()
   123→                    } label: {
   124→                        Image(systemName: "line.3.horizontal.decrease.circle")
   125→                            .symbolVariant(hasActiveFilters ? .fill : .none)
   126→                    }
   127→                    .accessibilityLabel(hasActiveFilters ? "Filters active" : "Filter emails")
   128→                    .accessibilityHint("Opens filter options")
   129→
   130→                    Button {
   131→                        showingLabels = true
   132→                    } label: {
   133→                        Image(systemName: "tag")
   134→                    }
   135→                    .accessibilityLabel("Labels")
   136→                    .accessibilityHint("View and manage Gmail labels")
   137→                }
   138→            }
   139→        }
   140→
   141→        ToolbarItem(placement: .topBarTrailing) {
   142→            if isSelectMode {
   143→                Button {
   144→                    if selectedEmails.count == emailStore.filteredEmails.count {
   145→                        selectedEmails.removeAll()
   146→                    } else {
   147→                        selectedEmails = Set(emailStore.filteredEmails.map { $0.id })
   148→                    }
   149→                } label: {
   150→                    Text(selectedEmails.count == emailStore.filteredEmails.count ? "Deselect All" : "Select All")
   151→                        .font(.subheadline)
   152→                }
   153→            } else {
   154→                HStack(spacing: 16) {
   155→                    Button {
   156→                        isSelectMode = true
   157→                        selectedEmails.removeAll()
   158→                    } label: {
   159→                        Image(systemName: "checkmark.circle")
   160→                    }
   161→                    .accessibilityLabel("Select emails")
   162→                    .accessibilityHint("Enter selection mode to select multiple emails")
   163→
   164→                    Button {
   165→                        showingSearch = true
   166→                    } label: {
   167→                        Image(systemName: "magnifyingglass")
   168→                    }
   169→                    .accessibilityLabel("Search")
   170→                    .accessibilityHint("Search through your emails")
   171→
   172→                    Button {
   173→                        Task {
   174→                            await emailStore.refreshEmails()
   175→                        }
   176→                    } label: {
   177→                        if emailStore.isLoading {
   178→                            ProgressView()
   179→                        } else {
   180→                            Image(systemName: "arrow.clockwise")
   181→                        }
   182→                    }
   183→                    .accessibilityLabel(emailStore.isLoading ? "Loading emails" : "Refresh")
   184→                    .accessibilityHint("Refresh inbox to check for new emails")
   185→                }
   186→            }
   187→        }
   188→
   189→        // Selection actions toolbar
   190→        if isSelectMode && !selectedEmails.isEmpty {
   191→            ToolbarItem(placement: .bottomBar) {
   192→                InboxSelectionActionsBar(
   193→                    selectedEmails: $selectedEmails,
   194→                    isSelectMode: $isSelectMode,
   195→                    toast: $toast
   196→                )
   197→            }
   198→        }
   199→    }
   200→
   201→    // MARK: - Undo Actions
   202→
   203→    private func undoDelete() {
   204→        guard let email = recentlyDeletedEmail else { return }
   205→        Task {
   206→            await MainActor.run {
   207→                emailStore.emails.insert(email, at: 0)
   208→                emailStore.emails.sort { $0.date > $1.date }
   209→                recentlyDeletedEmail = nil
   210→                toast = .success("Delete undone")
   211→            }
   212→        }
   213→    }
   214→
   215→    private func undoArchive() {
   216→        guard let email = recentlyArchivedEmail else { return }
   217→        Task {
   218→            await emailStore.unarchiveEmail(email)
   219→            recentlyArchivedEmail = nil
   220→            toast = .success("Archive undone")
   221→        }
   222→    }
   223→
   224→    // MARK: - Helper Functions
   225→
   226→    private func friendlyErrorMessage(_ error: String) -> String {
   227→        if error.contains("401") || error.contains("unauthorized") {
   228→            return "Session expired. Please sign in again."
   229→        } else if error.contains("network") || error.contains("connection") {
   230→            return "No internet connection. Please check your network."
   231→        } else if error.contains("429") || error.contains("rate") {
   232→            return "Too many requests. Please wait a moment."
   233→        } else if error.contains("500") || error.contains("server") {
   234→            return "Server error. Please try again later."
   235→        } else {
   236→            return "Something went wrong. Pull to refresh."
   237→        }
   238→    }
   239→
   240→    private var hasActiveFilters: Bool {
   241→        emailStore.selectedCategory != nil ||
   242→        emailStore.selectedPriority != nil ||
   243→        emailStore.showUnreadOnly
   244→    }
   245→
   246→    // MARK: - Email List
   247→
   248→    private var emailList: some View {
   249→        List {
   250→            // AI Health Status Banner
   251→            AIHealthBanner()
   252→                .listRowInsets(EdgeInsets())
   253→                .listRowBackground(Color.clear)
   254→                .listRowSeparator(Visibility.hidden)
   255→
   256→            // Drafts section
   257→            draftsSection
   258→
   259→            // Emails
   260→            if groupByThread {
   261→                ForEach(groupedThreads) { thread in
   262→                    ThreadRowView(thread: thread)
   263→                }
   264→            } else {
   265→                emailRows
   266→            }
   267→
   268→            // Loading more indicator
   269→            loadMoreIndicator
   270→        }
   271→        .listStyle(.plain)
   272→    }
   273→
   274→    // MARK: - Drafts Section
   275→
   276→    @ViewBuilder
   277→    private var draftsSection: some View {
   278→        if draftService.hasDrafts && showDraftsSection && !isSelectMode {
   279→            Section {
   280→                ForEach(draftService.drafts) { draft in
   281→                    DraftRowView(draft: draft)
   282→                        .contentShape(Rectangle())
   283→                        .onTapGesture {
   284→                            editingDraft = draft
   285→                        }
   286→                        .swipeActions(edge: .trailing, allowsFullSwipe: true) {
   287→                            Button(role: .destructive) {
   288→                                HapticManager.swipeAction()
   289→                                draftService.deleteDraft(draft)
   290→                                toast = .success("Draft deleted")
   291→                            } label: {
   292→                                Label("Delete", systemImage: "trash")
   293→                            }
   294→                        }
   295→                }
   296→            } header: {
   297→                HStack {
   298→                    Label("Drafts (\(draftService.count))", systemImage: "doc.text")
   299→                    Spacer()
   300→                    Button {
   301→                        withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) {
   302→                            showDraftsSection.toggle()
   303→                        }
   304→                    } label: {
   305→                        Image(systemName: "chevron.up")
   306→                            .font(.caption)
   307→                            .rotationEffect(.degrees(showDraftsSection ? 0 : 180))
   308→                            .animation(.spring(response: 0.3, dampingFraction: 0.7), value: showDraftsSection)
   309→                    }
   310→                }
   311→            }
   312→        } else if draftService.hasDrafts && !showDraftsSection && !isSelectMode {
   313→            Button {
   314→                withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) {
   315→                    showDraftsSection = true
   316→                }
   317→            } label: {
   318→                HStack {
   319→                    Image(systemName: "doc.text")
   320→                        .foregroundStyle(.orange)
   321→                    Text("\(draftService.count) draft\(draftService.count == 1 ? "" : "s")")
   322→                        .font(.subheadline)
   323→                    Spacer()
   324→                    Image(systemName: "chevron.up")
   325→                        .font(.caption)
   326→                        .foregroundStyle(.secondary)
   327→                        .rotationEffect(.degrees(180))
   328→                }
   329→                .padding(.vertical, 4)
   330→            }
   331→            .listRowBackground(Color.orange.opacity(0.1))
   332→        }
   333→    }
   334→
   335→    // MARK: - Email Rows
   336→
   337→    private var emailRows: some View {
   338→        ForEach(emailStore.filteredEmails) { email in
   339→            HStack(spacing: 12) {
   340→                if isSelectMode {
   341→                    selectionCheckbox(for: email)
   342→                }
   343→                EmailRowView(email: email)
   344→            }
   345→            .contentShape(Rectangle())
   346→            .onTapGesture {
   347→                handleEmailTap(email)
   348→            }
   349→            .swipeActions(edge: .trailing, allowsFullSwipe: !isSelectMode) {
   350→                trailingSwipeActions(for: email)
   351→            }
   352→            .swipeActions(edge: .leading, allowsFullSwipe: true) {
   353→                leadingSwipeActions(for: email)
   354→            }
   355→            .onAppear {
   356→                loadMoreIfNeeded(email)
   357→            }
   358→        }
   359→    }
   360→
   361→    private func selectionCheckbox(for email: Email) -> some View {
   362→        Button {
   363→            HapticManager.selection()
   364→            withAnimation(.spring(response: 0.3, dampingFraction: 0.6)) {
   365→                if selectedEmails.contains(email.id) {
   366→                    selectedEmails.remove(email.id)
   367→                } else {
   368→                    selectedEmails.insert(email.id)
   369→                }
   370→            }
   371→        } label: {
   372→            Image(systemName: selectedEmails.contains(email.id) ? "checkmark.circle.fill" : "circle")
   373→                .font(.title2)
   374→                .foregroundStyle(selectedEmails.contains(email.id) ? .blue : .secondary)
   375→                .scaleEffect(selectedEmails.contains(email.id) ? 1.1 : 1.0)
   376→                .animation(.spring(response: 0.3, dampingFraction: 0.6), value: selectedEmails.contains(email.id))
   377→        }
   378→        .buttonStyle(.plain)
   379→        .transition(.scale.combined(with: .opacity))
   380→    }
   381→
   382→    private func handleEmailTap(_ email: Email) {
   383→        if isSelectMode {
   384→            HapticManager.selection()
   385→            if selectedEmails.contains(email.id) {
   386→                selectedEmails.remove(email.id)
   387→            } else {
   388→                selectedEmails.insert(email.id)
   389→            }
   390→        } else {
   391→            AppLogger.userAction("Email tapped: \(email.id) - '\(String(email.subject.prefix(30)))'", level: .info)
   392→            selectedEmail = email
   393→            if !email.isRead {
   394→                Task {
   395→                    await emailStore.markAsRead(email)
   396→                }
   397→            }
   398→        }
   399→    }
   400→
   401→    @ViewBuilder
   402→    private func trailingSwipeActions(for email: Email) -> some View {
   403→        Button(role: .destructive) {
   404→            AppLogger.userAction("Swipe delete on email \(email.id)", level: .info)
   405→            HapticManager.swipeAction()
   406→            recentlyDeletedEmail = email
   407→            Task {
   408→                await emailStore.deleteEmail(email)
   409→                HapticManager.success()
   410→                toast = .deleted("Email", undoAction: undoDelete)
   411→            }
   412→        } label: {
   413→            Label("Delete", systemImage: "trash")
   414→        }
   415→        .tint(.red)
   416→
   417→        Button {
   418→            AppLogger.userAction("Swipe archive on email \(email.id)", level: .info)
   419→            HapticManager.swipeAction()
   420→            recentlyArchivedEmail = email
   421→            Task {
   422→                let success = await emailStore.archiveEmail(email)
   423→                if success {
   424→                    HapticManager.success()
   425→                    toast = .archived("Email", undoAction: undoArchive)
   426→                }
   427→            }
   428→        } label: {
   429→            Label("Archive", systemImage: "archivebox")
   430→        }
   431→        .tint(.blue)
   432→
   433→        Button {
   434→            AppLogger.userAction("Swipe star toggle on email \(email.id)", level: .info)
   435→            HapticManager.swipeAction()
   436→            Task {
   437→                await emailStore.toggleStar(email)
   438→                HapticManager.emailStarred()
   439→            }
   440→        } label: {
   441→            Label("Star", systemImage: email.isStarred ? "star.slash" : "star")
   442→        }
   443→        .tint(.yellow)
   444→    }
   445→
   446→    @ViewBuilder
   447→    private func leadingSwipeActions(for email: Email) -> some View {
   448→        Button {
   449→            AppLogger.userAction("Swipe mark \(email.isRead ? "unread" : "read") on email \(email.id)", level: .info)
   450→            HapticManager.swipeAction()
   451→            Task {
   452→                if email.isRead {
   453→                    await emailStore.markAsUnread(email)
   454→                } else {
   455→                    await emailStore.markAsRead(email)
   456→                }
   457→                HapticManager.success()
   458→            }
   459→        } label: {
   460→            Label(email.isRead ? "Unread" : "Read", systemImage: email.isRead ? "envelope.badge" : "envelope.open")
   461→        }
   462→        .tint(.blue)
   463→
   464→        Button {
   465→            AppLogger.userAction("Swipe analyze on email \(email.id)", level: .info)
   466→            HapticManager.swipeAction()
   467→            Task {
   468→                await emailStore.analyzeEmail(email)
   469→                HapticManager.analysisComplete()
   470→            }
   471→        } label: {
   472→            Label("Analyze", systemImage: "brain.head.profile")
   473→        }
   474→        .tint(.purple)
   475→    }
   476→
   477→    private func loadMoreIfNeeded(_ email: Email) {
   478→        if email.id == emailStore.filteredEmails.last?.id && emailStore.hasMorePages {
   479→            Task {
   480→                await emailStore.loadMoreEmails()
   481→            }
   482→        }
   483→    }
   484→
   485→    // MARK: - Load More Indicator
   486→
   487→    @ViewBuilder
   488→    private var loadMoreIndicator: some View {
   489→        if emailStore.hasMorePages {
   490→            HStack {
   491→                Spacer()
   492→                if emailStore.isLoadingMore {
   493→                    ProgressView()
   494→                        .padding()
   495→                    Text("Loading more...")
   496→                        .font(.caption)
   497→                        .foregroundStyle(.secondary)
   498→                } else {
   499→                    Button {
   500→                        Task {
   501→                            await emailStore.loadMoreEmails()
   502→                        }
   503→                    } label: {
   504→                        Text("Load More")
   505→                            .font(.caption)
   506→                    }
   507→                    .padding()
   508→                }
   509→                Spacer()
   510→            }
   511→            .listRowSeparator(.hidden)
   512→        }
   513→    }
   514→
   515→    // MARK: - Grouped Threads
   516→
   517→    private var groupedThreads: [EmailThread] {
   518→        var threadMap: [String: [Email]] = [:]
   519→
   520→        for email in emailStore.filteredEmails {
   521→            threadMap[email.threadId, default: []].append(email)
   522→        }
   523→
   524→        return threadMap.map { threadId, emails in
   525→            let sortedEmails = emails.sorted { $0.date < $1.date }
   526→            var participants = Set<String>()
   527→            for email in sortedEmails {
   528→                participants.insert(email.fromEmail)
   529→            }
   530→
   531→            return EmailThread(
   532→                id: threadId,
   533→                emails: sortedEmails,
   534→                subject: sortedEmails.first?.subject ?? "(No Subject)",
   535→                participants: Array(participants),
   536→                lastMessageDate: sortedEmails.last?.date ?? Date()
   537→            )
   538→        }
   539→        .sorted { $0.lastMessageDate > $1.lastMessageDate }
   540→    }
   541→}
   542→
   543→// MARK: - Preview
   544→
   545→#Preview {
   546→    InboxView()
   547→        .environmentObject(GoogleAuthManager())
   548→        .environmentObject(EmailStore())
   549→}
   550→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

import SwiftUI

struct InboxView: View {
    @EnvironmentObject var emailStore: EmailStore
    @StateObject private var draftService = DraftService.shared
    @State private var showingFilters = false
    @State private var selectedEmail: Email?
    @State private var showingCompose = false
    @State private var editingDraft: EmailDraft?
    @State private var toast: ToastData?
    @State private var showingSearch = false
    @State private var showingLabels = false
    @State private var isSelectMode = false
    @State private var selectedEmails: Set<String> = []
    @State private var recentlyDeletedEmail: Email?
    @State private var recentlyArchivedEmail: Email?
    @State private var showDraftsSection = true
    @AppStorage("groupByThread") private var groupByThread = false

    var body: some View {
        NavigationStack {
            ZStack(alignment: .bottomTrailing) {
                Group {
                    if emailStore.isLoading && emailStore.emails.isEmpty {
                        LoadingView()
                    } else if emailStore.emails.isEmpty {
                        EmptyInboxView {
                            Task {
                                await emailStore.loadInitialEmails()
                            }
                        }
                    } else {
                        emailList
                    }
                }

                // Floating Action Button for Compose
                composeButton
            }
            .navigationTitle("Inbox (\(emailStore.totalEmailsLoaded))")
            .toolbar { toolbarContent }
            .searchable(text: $emailStore.searchQuery, prompt: "Search emails")
            .sheet(isPresented: $showingFilters) {
                FilterSheet()
            }
            .sheet(item: $selectedEmail) { email in
                NavigationStack {
                    EmailDetailView(email: email)
                }
            }
            .sheet(isPresented: $showingCompose) {
                ComposeView()
            }
            .sheet(item: $editingDraft) { draft in
                ComposeView(existingDraft: draft)
            }
            .sheet(isPresented: $showingSearch) {
                SearchView()
            }
            .sheet(isPresented: $showingLabels) {
                LabelsView()
            }
            .refreshable {
                AppLogger.userAction("Pull to refresh triggered", level: .info)
                HapticManager.refresh()
                await emailStore.refreshEmails()
            }
            .toast($toast)
            .onChange(of: emailStore.error) { _, newError in
                if let error = newError {
                    toast = .error(friendlyErrorMessage(error))
                }
            }
        }
        .task {
            if emailStore.emails.isEmpty {
                await emailStore.loadInitialEmails()
            }
        }
        .onAppear {
            AppLogger.lifecycle("InboxView appeared", level: .debug)
        }
        .onDisappear {
            AppLogger.lifecycle("InboxView disappeared", level: .debug)
        }
    }

    // MARK: - Compose Button

    private var composeButton: some View {
        Button {
            AppLogger.userAction("Compose button tapped", level: .info)
            showingCompose = true
        } label: {
            Image(systemName: "square.and.pencil")
                .font(.title2)
                .fontWeight(.semibold)
                .foregroundStyle(.white)
                .frame(width: 56, height: 56)
                .background(Circle().fill(.blue))
                .shadow(color: .black.opacity(0.3), radius: 4, x: 0, y: 2)
        }
        .accessibilityLabel("Compose new email")
        .accessibilityHint("Opens the email composer")
        .padding(.trailing, 20)
        .padding(.bottom, 20)
    }

    // MARK: - Toolbar

    @ToolbarContentBuilder
    private var toolbarContent: some ToolbarContent {
        ToolbarItem(placement: .topBarLeading) {
            if isSelectMode {
                Button("Cancel") {
                    isSelectMode = false
                    selectedEmails.removeAll()
                }
            } else {
                HStack(spacing: 16) {
                    Button {
                        showingFilters.toggle()
                    } label: {
                        Image(systemName: "line.3.horizontal.decrease.circle")
                            .symbolVariant(hasActiveFilters ? .fill : .none)
                    }
                    .accessibilityLabel(hasActiveFilters ? "Filters active" : "Filter emails")
                    .accessibilityHint("Opens filter options")

                    Button {
                        showingLabels = true
                    } label: {
                        Image(systemName: "tag")
                    }
                    .accessibilityLabel("Labels")
                    .accessibilityHint("View and manage Gmail labels")
                }
            }
        }

        ToolbarItem(placement: .topBarTrailing) {
            if isSelectMode {
                Button {
                    if selectedEmails.count == emailStore.filteredEmails.count {
                        selectedEmails.removeAll()
                    } else {
                        selectedEmails = Set(emailStore.filteredEmails.map { $0.id })
                    }
                } label: {
                    Text(selectedEmails.count == emailStore.filteredEmails.count ? "Deselect All" : "Select All")
                        .font(.subheadline)
                }
            } else {
                HStack(spacing: 16) {
                    Button {
                        isSelectMode = true
                        selectedEmails.removeAll()
                    } label: {
                        Image(systemName: "checkmark.circle")
                    }
                    .accessibilityLabel("Select emails")
                    .accessibilityHint("Enter selection mode to select multiple emails")

                    Button {
                        showingSearch = true
                    } label: {
                        Image(systemName: "magnifyingglass")
                    }
                    .accessibilityLabel("Search")
                    .accessibilityHint("Search through your emails")

                    Button {
                        Task {
                            await emailStore.refreshEmails()
                        }
                    } label: {
                        if emailStore.isLoading {
                            ProgressView()
                        } else {
                            Image(systemName: "arrow.clockwise")
                        }
                    }
                    .accessibilityLabel(emailStore.isLoading ? "Loading emails" : "Refresh")
                    .accessibilityHint("Refresh inbox to check for new emails")
                }
            }
        }

        // Selection actions toolbar
        if isSelectMode && !selectedEmails.isEmpty {
            ToolbarItem(placement: .bottomBar) {
                InboxSelectionActionsBar(
                    selectedEmails: $selectedEmails,
                    isSelectMode: $isSelectMode,
                    toast: $toast
                )
            }
        }
    }

    // MARK: - Undo Actions

    private func undoDelete() {
        guard let email = recentlyDeletedEmail else { return }
        Task {
            await MainActor.run {
                emailStore.emails.insert(email, at: 0)
                emailStore.emails.sort { $0.date > $1.date }
                recentlyDeletedEmail = nil
                toast = .success("Delete undone")
            }
        }
    }

    private func undoArchive() {
        guard let email = recentlyArchivedEmail else { return }
        Task {
            await emailStore.unarchiveEmail(email)
            recentlyArchivedEmail = nil
            toast = .success("Archive undone")
        }
    }

    // MARK: - Helper Functions

    private func friendlyErrorMessage(_ error: String) -> String {
        if error.contains("401") || error.contains("unauthorized") {
            return "Session expired. Please sign in again."
        } else if error.contains("network") || error.contains("connection") {
            return "No internet connection. Please check your network."
        } else if error.contains("429") || error.contains("rate") {
            return "Too many requests. Please wait a moment."
        } else if error.contains("500") || error.contains("server") {
            return "Server error. Please try again later."
        } else {
            return "Something went wrong. Pull to refresh."
        }
    }

    private var hasActiveFilters: Bool {
        emailStore.selectedCategory != nil ||
        emailStore.selectedPriority != nil ||
        emailStore.showUnreadOnly
    }

    // MARK: - Email List

    private var emailList: some View {
        List {
            // AI Health Status Banner
            AIHealthBanner()
                .listRowInsets(EdgeInsets())
                .listRowBackground(Color.clear)
                .listRowSeparator(Visibility.hidden)

            // Drafts section
            draftsSection

            // Emails
            if groupByThread {
                ForEach(groupedThreads) { thread in
                    ThreadRowView(thread: thread)
                }
            } else {
                emailRows
            }

            // Loading more indicator
            loadMoreIndicator
        }
        .listStyle(.plain)
    }

    // MARK: - Drafts Section

    @ViewBuilder
    private var draftsSection: some View {
        if draftService.hasDrafts && showDraftsSection && !isSelectMode {
            Section {
                ForEach(draftService.drafts) { draft in
                    DraftRowView(draft: draft)
                        .contentShape(Rectangle())
                        .onTapGesture {
                            editingDraft = draft
                        }
                        .swipeActions(edge: .trailing, allowsFullSwipe: true) {
                            Button(role: .destructive) {
                                HapticManager.swipeAction()
                                draftService.deleteDraft(draft)
                                toast = .success("Draft deleted")
                            } label: {
                                Label("Delete", systemImage: "trash")
                            }
                        }
                }
            } header: {
                HStack {
                    Label("Drafts (\(draftService.count))", systemImage: "doc.text")
                    Spacer()
                    Button {
                        withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) {
                            showDraftsSection.toggle()
                        }
                    } label: {
                        Image(systemName: "chevron.up")
                            .font(.caption)
                            .rotationEffect(.degrees(showDraftsSection ? 0 : 180))
                            .animation(.spring(response: 0.3, dampingFraction: 0.7), value: showDraftsSection)
                    }
                }
            }
        } else if draftService.hasDrafts && !showDraftsSection && !isSelectMode {
            Button {
                withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) {
                    showDraftsSection = true
                }
            } label: {
                HStack {
                    Image(systemName: "doc.text")
                        .foregroundStyle(.orange)
                    Text("\(draftService.count) draft\(draftService.count == 1 ? "" : "s")")
                        .font(.subheadline)
                    Spacer()
                    Image(systemName: "chevron.up")
                        .font(.caption)
                        .foregroundStyle(.secondary)
                        .rotationEffect(.degrees(180))
                }
                .padding(.vertical, 4)
            }
            .listRowBackground(Color.orange.opacity(0.1))
        }
    }

    // MARK: - Email Rows

    private var emailRows: some View {
        ForEach(emailStore.filteredEmails) { email in
            HStack(spacing: 12) {
                if isSelectMode {
                    selectionCheckbox(for: email)
                }
                EmailRowView(email: email)
            }
            .contentShape(Rectangle())
            .onTapGesture {
                handleEmailTap(email)
            }
            .swipeActions(edge: .trailing, allowsFullSwipe: !isSelectMode) {
                trailingSwipeActions(for: email)
            }
            .swipeActions(edge: .leading, allowsFullSwipe: true) {
                leadingSwipeActions(for: email)
            }
            .onAppear {
                loadMoreIfNeeded(email)
            }
        }
    }

    private func selectionCheckbox(for email: Email) -> some View {
        Button {
            HapticManager.selection()
            withAnimation(.spring(response: 0.3, dampingFraction: 0.6)) {
                if selectedEmails.contains(email.id) {
                    selectedEmails.remove(email.id)
                } else {
                    selectedEmails.insert(email.id)
                }
            }
        } label: {
            Image(systemName: selectedEmails.contains(email.id) ? "checkmark.circle.fill" : "circle")
                .font(.title2)
                .foregroundStyle(selectedEmails.contains(email.id) ? .blue : .secondary)
                .scaleEffect(selectedEmails.contains(email.id) ? 1.1 : 1.0)
                .animation(.spring(response: 0.3, dampingFraction: 0.6), value: selectedEmails.contains(email.id))
        }
        .buttonStyle(.plain)
        .transition(.scale.combined(with: .opacity))
    }

    private func handleEmailTap(_ email: Email) {
        if isSelectMode {
            HapticManager.selection()
            if selectedEmails.contains(email.id) {
                selectedEmails.remove(email.id)
            } else {
                selectedEmails.insert(email.id)
            }
        } else {
            AppLogger.userAction("Email tapped: \(email.id) - '\(String(email.subject.prefix(30)))'", level: .info)
            selectedEmail = email
            if !email.isRead {
                Task {
                    await emailStore.markAsRead(email)
                }
            }
        }
    }

    @ViewBuilder
    private func trailingSwipeActions(for email: Email) -> some View {
        Button(role: .destructive) {
            AppLogger.userAction("Swipe delete on email \(email.id)", level: .info)
            HapticManager.swipeAction()
            recentlyDeletedEmail = email
            Task {
                await emailStore.deleteEmail(email)
                HapticManager.success()
                toast = .deleted("Email", undoAction: undoDelete)
            }
        } label: {
            Label("Delete", systemImage: "trash")
        }
        .tint(.red)

        Button {
            AppLogger.userAction("Swipe archive on email \(email.id)", level: .info)
            HapticManager.swipeAction()
            recentlyArchivedEmail = email
            Task {
                let success = await emailStore.archiveEmail(email)
                if success {
                    HapticManager.success()
                    toast = .archived("Email", undoAction: undoArchive)
                }
            }
        } label: {
            Label("Archive", systemImage: "archivebox")
        }
        .tint(.blue)

        Button {
            AppLogger.userAction("Swipe star toggle on email \(email.id)", level: .info)
            HapticManager.swipeAction()
            Task {
                await emailStore.toggleStar(email)
                HapticManager.emailStarred()
            }
        } label: {
            Label("Star", systemImage: email.isStarred ? "star.slash" : "star")
        }
        .tint(.yellow)
    }

    @ViewBuilder
    private func leadingSwipeActions(for email: Email) -> some View {
        Button {
            AppLogger.userAction("Swipe mark \(email.isRead ? "unread" : "read") on email \(email.id)", level: .info)
            HapticManager.swipeAction()
            Task {
                if email.isRead {
                    await emailStore.markAsUnread(email)
                } else {
                    await emailStore.markAsRead(email)
                }
                HapticManager.success()
            }
        } label: {
            Label(email.isRead ? "Unread" : "Read", systemImage: email.isRead ? "envelope.badge" : "envelope.open")
        }
        .tint(.blue)

        Button {
            AppLogger.userAction("Swipe analyze on email \(email.id)", level: .info)
            HapticManager.swipeAction()
            Task {
                await emailStore.analyzeEmail(email)
                HapticManager.analysisComplete()
            }
        } label: {
            Label("Analyze", systemImage: "brain.head.profile")
        }
        .tint(.purple)
    }

    private func loadMoreIfNeeded(_ email: Email) {
        if email.id == emailStore.filteredEmails.last?.id && emailStore.hasMorePages {
            Task {
                await emailStore.loadMoreEmails()
            }
        }
    }

    // MARK: - Load More Indicator

    @ViewBuilder
    private var loadMoreIndicator: some View {
        if emailStore.hasMorePages {
            HStack {
                Spacer()
                if emailStore.isLoadingMore {
                    ProgressView()
                        .padding()
                    Text("Loading more...")
                        .font(.caption)
                        .foregroundStyle(.secondary)
                } else {
                    Button {
                        Task {
                            await emailStore.loadMoreEmails()
                        }
                    } label: {
                        Text("Load More")
                            .font(.caption)
                    }
                    .padding()
                }
                Spacer()
            }
            .listRowSeparator(.hidden)
        }
    }

    // MARK: - Grouped Threads

    private var groupedThreads: [EmailThread] {
        var threadMap: [String: [Email]] = [:]

        for email in emailStore.filteredEmails {
            threadMap[email.threadId, default: []].append(email)
        }

        return threadMap.map { threadId, emails in
            let sortedEmails = emails.sorted { $0.date < $1.date }
            var participants = Set<String>()
            for email in sortedEmails {
                participants.insert(email.fromEmail)
            }

            return EmailThread(
                id: threadId,
                emails: sortedEmails,
                subject: sortedEmails.first?.subject ?? "(No Subject)",
                participants: Array(participants),
                lastMessageDate: sortedEmails.last?.date ?? Date()
            )
        }
        .sorted { $0.lastMessageDate > $1.lastMessageDate }
    }
}

// MARK: - Preview

#Preview {
    InboxView()
        .environmentObject(GoogleAuthManager())
        .environmentObject(EmailStore())
}


     1→import SwiftUI
     2→
     3→// MARK: - Loading View
     4→
     5→struct LoadingView: View {
     6→    var body: some View {
     7→        SkeletonEmailList(count: 8)
     8→    }
     9→}
    10→
    11→// MARK: - Empty Inbox View
    12→
    13→struct EmptyInboxView: View {
    14→    let onRefresh: () -> Void
    15→
    16→    var body: some View {
    17→        EmptyStateView.emptyInbox(onRefresh: onRefresh)
    18→    }
    19→}
    20→
    21→// MARK: - Filter Sheet
    22→
    23→struct FilterSheet: View {
    24→    @EnvironmentObject var emailStore: EmailStore
    25→    @Environment(\.dismiss) var dismiss
    26→    @AppStorage("groupByThread") private var groupByThread = false
    27→
    28→    var body: some View {
    29→        NavigationStack {
    30→            Form {
    31→                Section("Category") {
    32→                    Picker("Category", selection: $emailStore.selectedCategory) {
    33→                        Text("All").tag(nil as EmailAnalysis.Category?)
    34→                        ForEach(EmailAnalysis.Category.allCases, id: \.self) { category in
    35→                            Label(category.rawValue.capitalized, systemImage: category.icon)
    36→                                .tag(category as EmailAnalysis.Category?)
    37→                        }
    38→                    }
    39→                    .pickerStyle(.navigationLink)
    40→                }
    41→
    42→                Section("Priority") {
    43→                    Picker("Priority", selection: $emailStore.selectedPriority) {
    44→                        Text("All").tag(nil as EmailAnalysis.Priority?)
    45→                        ForEach(EmailAnalysis.Priority.allCases, id: \.self) { priority in
    46→                            Text(priority.rawValue.capitalized)
    47→                                .tag(priority as EmailAnalysis.Priority?)
    48→                        }
    49→                    }
    50→                    .pickerStyle(.segmented)
    51→                }
    52→
    53→                Section("Display") {
    54→                    Toggle("Unread Only", isOn: $emailStore.showUnreadOnly)
    55→
    56→                    Toggle(isOn: $groupByThread) {
    57→                        Label("Group by Thread", systemImage: "text.bubble")
    58→                    }
    59→                }
    60→
    61→                Section {
    62→                    Button("Clear Filters") {
    63→                        emailStore.selectedCategory = nil
    64→                        emailStore.selectedPriority = nil
    65→                        emailStore.showUnreadOnly = false
    66→                    }
    67→                    .foregroundStyle(.red)
    68→                }
    69→            }
    70→            .navigationTitle("Filters")
    71→            .navigationBarTitleDisplayMode(.inline)
    72→            .toolbar {
    73→                ToolbarItem(placement: .topBarTrailing) {
    74→                    Button("Done") {
    75→                        dismiss()
    76→                    }
    77→                }
    78→            }
    79→        }
    80→        .presentationDetents([.medium])
    81→    }
    82→}
    83→
    84→// MARK: - Draft Row View
    85→
    86→struct DraftRowView: View {
    87→    let draft: EmailDraft
    88→
    89→    var body: some View {
    90→        HStack(alignment: .top, spacing: 12) {
    91→            // Draft icon
    92→            Image(systemName: "doc.text")
    93→                .foregroundStyle(.orange)
    94→                .frame(width: 24)
    95→                .padding(.top, 2)
    96→
    97→            VStack(alignment: .leading, spacing: 4) {
    98→                HStack {
    99→                    Text(draft.displayTitle)
   100→                        .font(.subheadline)
   101→                        .fontWeight(.medium)
   102→                        .lineLimit(1)
   103→
   104→                    Spacer()
   105→
   106→                    Text(draft.savedAt, style: .relative)
   107→                        .font(.caption2)
   108→                        .foregroundStyle(.secondary)
   109→                }
   110→
   111→                if !draft.to.isEmpty {
   112→                    Text("To: \(draft.to)")
   113→                        .font(.caption)
   114→                        .foregroundStyle(.secondary)
   115→                        .lineLimit(1)
   116→                }
   117→
   118→                Text(draft.snippet)
   119→                    .font(.caption)
   120→                    .foregroundStyle(.secondary)
   121→                    .lineLimit(2)
   122→
   123→                // Draft type indicator
   124→                HStack(spacing: 6) {
   125→                    if draft.isReply {
   126→                        Badge(text: "Reply", icon: "arrowshape.turn.up.left", color: .blue)
   127→                    } else if draft.isForward {
   128→                        Badge(text: "Forward", icon: "arrowshape.turn.up.right", color: .purple)
   129→                    }
   130→                }
   131→                .padding(.top, 2)
   132→            }
   133→        }
   134→        .padding(.vertical, 4)
   135→    }
   136→}
   137→
   138→// MARK: - Inbox Selection Actions Bar
   139→
   140→struct InboxSelectionActionsBar: View {
   141→    @EnvironmentObject var emailStore: EmailStore
   142→    @Binding var selectedEmails: Set<String>
   143→    @Binding var isSelectMode: Bool
   144→    @Binding var toast: ToastData?
   145→
   146→    var body: some View {
   147→        HStack(spacing: 20) {
   148→            // Delete
   149→            Button(role: .destructive) {
   150→                Task {
   151→                    let emailsToDelete = emailStore.emails.filter { selectedEmails.contains($0.id) }
   152→                    let deleted = await emailStore.deleteEmailsBatch(emailsToDelete)
   153→                    toast = .success("Deleted \(deleted) emails")
   154→                    selectedEmails.removeAll()
   155→                    isSelectMode = false
   156→                    HapticManager.success()
   157→                }
   158→            } label: {
   159→                VStack(spacing: 4) {
   160→                    Image(systemName: "trash")
   161→                    Text("Delete")
   162→                        .font(.caption2)
   163→                }
   164→            }
   165→
   166→            // Archive
   167→            Button {
   168→                Task {
   169→                    let emailsToArchive = emailStore.emails.filter { selectedEmails.contains($0.id) }
   170→                    let archived = await emailStore.archiveEmails(emailsToArchive)
   171→                    toast = .success("Archived \(archived) emails")
   172→                    selectedEmails.removeAll()
   173→                    isSelectMode = false
   174→                    HapticManager.success()
   175→                }
   176→            } label: {
   177→                VStack(spacing: 4) {
   178→                    Image(systemName: "archivebox")
   179→                    Text("Archive")
   180→                        .font(.caption2)
   181→                }
   182→            }
   183→
   184→            // Mark Read
   185→            Button {
   186→                Task {
   187→                    let emailsToMark = emailStore.emails.filter { selectedEmails.contains($0.id) }
   188→                    let marked = await emailStore.markEmailsAsRead(emailsToMark)
   189→                    toast = .success("Marked \(marked) as read")
   190→                    selectedEmails.removeAll()
   191→                    isSelectMode = false
   192→                    HapticManager.success()
   193→                }
   194→            } label: {
   195→                VStack(spacing: 4) {
   196→                    Image(systemName: "envelope.open")
   197→                    Text("Read")
   198→                        .font(.caption2)
   199→                }
   200→            }
   201→
   202→            // Mark Unread
   203→            Button {
   204→                Task {
   205→                    let emailsToMark = emailStore.emails.filter { selectedEmails.contains($0.id) }
   206→                    let marked = await emailStore.markEmailsAsUnread(emailsToMark)
   207→                    toast = .success("Marked \(marked) as unread")
   208→                    selectedEmails.removeAll()
   209→                    isSelectMode = false
   210→                    HapticManager.success()
   211→                }
   212→            } label: {
   213→                VStack(spacing: 4) {
   214→                    Image(systemName: "envelope.badge")
   215→                    Text("Unread")
   216→                        .font(.caption2)
   217→                }
   218→            }
   219→
   220→            Spacer()
   221→
   222→            Text("\(selectedEmails.count) selected")
   223→                .font(.caption)
   224→                .foregroundStyle(.secondary)
   225→        }
   226→    }
   227→}
   228→
   229→// MARK: - Previews
   230→
   231→#Preview("Filter Sheet") {
   232→    FilterSheet()
   233→        .environmentObject(EmailStore())
   234→}
   235→
   236→#Preview("Draft Row") {
   237→    List {
   238→        DraftRowView(draft: EmailDraft(
   239→            to: "test@example.com",
   240→            subject: "Test Subject",
   241→            body: "This is a test draft message that spans multiple lines to show how it looks."
   242→        ))
   243→        DraftRowView(draft: EmailDraft(
   244→            to: "",
   245→            subject: "",
   246→            body: "Just started typing..."
   247→        ))
   248→    }
   249→}
   250→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

import SwiftUI

// MARK: - Loading View

struct LoadingView: View {
    var body: some View {
        SkeletonEmailList(count: 8)
    }
}

// MARK: - Empty Inbox View

struct EmptyInboxView: View {
    let onRefresh: () -> Void

    var body: some View {
        EmptyStateView.emptyInbox(onRefresh: onRefresh)
    }
}

// MARK: - Filter Sheet

struct FilterSheet: View {
    @EnvironmentObject var emailStore: EmailStore
    @Environment(\.dismiss) var dismiss
    @AppStorage("groupByThread") private var groupByThread = false

    var body: some View {
        NavigationStack {
            Form {
                Section("Category") {
                    Picker("Category", selection: $emailStore.selectedCategory) {
                        Text("All").tag(nil as EmailAnalysis.Category?)
                        ForEach(EmailAnalysis.Category.allCases, id: \.self) { category in
                            Label(category.rawValue.capitalized, systemImage: category.icon)
                                .tag(category as EmailAnalysis.Category?)
                        }
                    }
                    .pickerStyle(.navigationLink)
                }

                Section("Priority") {
                    Picker("Priority", selection: $emailStore.selectedPriority) {
                        Text("All").tag(nil as EmailAnalysis.Priority?)
                        ForEach(EmailAnalysis.Priority.allCases, id: \.self) { priority in
                            Text(priority.rawValue.capitalized)
                                .tag(priority as EmailAnalysis.Priority?)
                        }
                    }
                    .pickerStyle(.segmented)
                }

                Section("Display") {
                    Toggle("Unread Only", isOn: $emailStore.showUnreadOnly)

                    Toggle(isOn: $groupByThread) {
                        Label("Group by Thread", systemImage: "text.bubble")
                    }
                }

                Section {
                    Button("Clear Filters") {
                        emailStore.selectedCategory = nil
                        emailStore.selectedPriority = nil
                        emailStore.showUnreadOnly = false
                    }
                    .foregroundStyle(.red)
                }
            }
            .navigationTitle("Filters")
            .navigationBarTitleDisplayMode(.inline)
            .toolbar {
                ToolbarItem(placement: .topBarTrailing) {
                    Button("Done") {
                        dismiss()
                    }
                }
            }
        }
        .presentationDetents([.medium])
    }
}

// MARK: - Draft Row View

struct DraftRowView: View {
    let draft: EmailDraft

    var body: some View {
        HStack(alignment: .top, spacing: 12) {
            // Draft icon
            Image(systemName: "doc.text")
                .foregroundStyle(.orange)
                .frame(width: 24)
                .padding(.top, 2)

            VStack(alignment: .leading, spacing: 4) {
                HStack {
                    Text(draft.displayTitle)
                        .font(.subheadline)
                        .fontWeight(.medium)
                        .lineLimit(1)

                    Spacer()

                    Text(draft.savedAt, style: .relative)
                        .font(.caption2)
                        .foregroundStyle(.secondary)
                }

                if !draft.to.isEmpty {
                    Text("To: \(draft.to)")
                        .font(.caption)
                        .foregroundStyle(.secondary)
                        .lineLimit(1)
                }

                Text(draft.snippet)
                    .font(.caption)
                    .foregroundStyle(.secondary)
                    .lineLimit(2)

                // Draft type indicator
                HStack(spacing: 6) {
                    if draft.isReply {
                        Badge(text: "Reply", icon: "arrowshape.turn.up.left", color: .blue)
                    } else if draft.isForward {
                        Badge(text: "Forward", icon: "arrowshape.turn.up.right", color: .purple)
                    }
                }
                .padding(.top, 2)
            }
        }
        .padding(.vertical, 4)
    }
}

// MARK: - Inbox Selection Actions Bar

struct InboxSelectionActionsBar: View {
    @EnvironmentObject var emailStore: EmailStore
    @Binding var selectedEmails: Set<String>
    @Binding var isSelectMode: Bool
    @Binding var toast: ToastData?

    var body: some View {
        HStack(spacing: 20) {
            // Delete
            Button(role: .destructive) {
                Task {
                    let emailsToDelete = emailStore.emails.filter { selectedEmails.contains($0.id) }
                    let deleted = await emailStore.deleteEmailsBatch(emailsToDelete)
                    toast = .success("Deleted \(deleted) emails")
                    selectedEmails.removeAll()
                    isSelectMode = false
                    HapticManager.success()
                }
            } label: {
                VStack(spacing: 4) {
                    Image(systemName: "trash")
                    Text("Delete")
                        .font(.caption2)
                }
            }

            // Archive
            Button {
                Task {
                    let emailsToArchive = emailStore.emails.filter { selectedEmails.contains($0.id) }
                    let archived = await emailStore.archiveEmails(emailsToArchive)
                    toast = .success("Archived \(archived) emails")
                    selectedEmails.removeAll()
                    isSelectMode = false
                    HapticManager.success()
                }
            } label: {
                VStack(spacing: 4) {
                    Image(systemName: "archivebox")
                    Text("Archive")
                        .font(.caption2)
                }
            }

            // Mark Read
            Button {
                Task {
                    let emailsToMark = emailStore.emails.filter { selectedEmails.contains($0.id) }
                    let marked = await emailStore.markEmailsAsRead(emailsToMark)
                    toast = .success("Marked \(marked) as read")
                    selectedEmails.removeAll()
                    isSelectMode = false
                    HapticManager.success()
                }
            } label: {
                VStack(spacing: 4) {
                    Image(systemName: "envelope.open")
                    Text("Read")
                        .font(.caption2)
                }
            }

            // Mark Unread
            Button {
                Task {
                    let emailsToMark = emailStore.emails.filter { selectedEmails.contains($0.id) }
                    let marked = await emailStore.markEmailsAsUnread(emailsToMark)
                    toast = .success("Marked \(marked) as unread")
                    selectedEmails.removeAll()
                    isSelectMode = false
                    HapticManager.success()
                }
            } label: {
                VStack(spacing: 4) {
                    Image(systemName: "envelope.badge")
                    Text("Unread")
                        .font(.caption2)
                }
            }

            Spacer()

            Text("\(selectedEmails.count) selected")
                .font(.caption)
                .foregroundStyle(.secondary)
        }
    }
}

// MARK: - Previews

#Preview("Filter Sheet") {
    FilterSheet()
        .environmentObject(EmailStore())
}

#Preview("Draft Row") {
    List {
        DraftRowView(draft: EmailDraft(
            to: "test@example.com",
            subject: "Test Subject",
            body: "This is a test draft message that spans multiple lines to show how it looks."
        ))
        DraftRowView(draft: EmailDraft(
            to: "",
            subject: "",
            body: "Just started typing..."
        ))
    }
}


Now let me check the EmailStore.swift to see the filter properties and structure:


     1→import Foundation
     2→import Combine
     3→
     4→// MARK: - EmailStore Core
     5→
     6→@MainActor
     7→class EmailStore: ObservableObject {
     8→    // MARK: - Published Properties
     9→
    10→    @Published var emails: [Email] = []
    11→    @Published var threads: [EmailThread] = []
    12→    @Published var isLoading = false
    13→    @Published var isLoadingMore = false
    14→    @Published var error: String?
    15→    @Published var selectedEmail: Email?
    16→
    17→    // Pagination
    18→    @Published var hasMorePages = true
    19→    @Published var totalEmailsLoaded = 0
    20→    var nextPageToken: String?
    21→    var pageSize: Int { AppSettings.shared.pageSize }
    22→
    23→    // Settings
    24→    var settings: AppSettings { AppSettings.shared }
    25→
    26→    // Filters
    27→    @Published var searchQuery = ""
    28→    @Published var selectedCategory: EmailAnalysis.Category?
    29→    @Published var selectedPriority: EmailAnalysis.Priority?
    30→    @Published var showUnreadOnly = false
    31→
    32→    // Stats
    33→    @Published var stats: EmailStats = EmailStats()
    34→
    35→    // Analysis State Manager (for live updates)
    36→    @Published var analysisState = AnalysisStateManager()
    37→
    38→    // Throttled updates
    39→    var lastStatsUpdate: Date = .distantPast
    40→    let statsUpdateInterval: TimeInterval = 0.1 // 100ms throttle
    41→
    42→    // Cache
    43→    @Published var isUsingCache = false
    44→    @Published var lastCacheUpdate: Date?
    45→    let cacheService = EmailCacheService.shared
    46→
    47→    // Services
    48→    var gmailService: GmailService
    49→    let aiService: AIAnalysisService
    50→    private var cancellables = Set<AnyCancellable>()
    51→
    52→    // MARK: - Initialization
    53→
    54→    init(gmailService: GmailService = GmailService(),
    55→         aiService: AIAnalysisService = AIAnalysisService()) {
    56→        self.gmailService = gmailService
    57→        self.aiService = aiService
    58→    }
    59→
    60→    // MARK: - Configuration
    61→
    62→    func configure(authManager: GoogleAuthManager) {
    63→        gmailService.setAuthManager(authManager)
    64→    }
    65→
    66→    // MARK: - Stats Update
    67→
    68→    func updateStats() {
    69→        stats = EmailStats(
    70→            totalEmails: emails.count,
    71→            unreadCount: emails.filter { !$0.isRead }.count,
    72→            analyzedCount: emails.filter { $0.analysis != nil }.count,
    73→            byCategory: Dictionary(grouping: emails.compactMap { $0.analysis?.category }) { $0 }
    74→                .mapValues { $0.count },
    75→            byPriority: Dictionary(grouping: emails.compactMap { $0.analysis?.priority }) { $0 }
    76→                .mapValues { $0.count },
    77→            bySentiment: Dictionary(grouping: emails.compactMap { $0.analysis?.sentiment }) { $0 }
    78→                .mapValues { $0.count }
    79→        )
    80→    }
    81→}
    82→
    83→// MARK: - Email Stats
    84→
    85→struct EmailStats {
    86→    var totalEmails: Int = 0
    87→    var unreadCount: Int = 0
    88→    var analyzedCount: Int = 0
    89→    var byCategory: [EmailAnalysis.Category: Int] = [:]
    90→    var byPriority: [EmailAnalysis.Priority: Int] = [:]
    91→    var bySentiment: [EmailAnalysis.Sentiment: Int] = [:]
    92→
    93→    var analyzedPercentage: Double {
    94→        guard totalEmails > 0 else { return 0 }
    95→        return Double(analyzedCount) / Double(totalEmails) * 100
    96→    }
    97→}
    98→
    99→// MARK: - Cleanup Statistics
   100→
   101→struct CleanupStats {
   102→    let safeToDeleteCount: Int
   103→    let reviewCount: Int
   104→    let suspiciousCount: Int
   105→    let oldUnreadCount: Int
   106→    let requiresActionCount: Int
   107→    let importantCount: Int
   108→
   109→    var totalCleanupCandidates: Int {
   110→        safeToDeleteCount + suspiciousCount
   111→    }
   112→
   113→    var hasCleanupOpportunities: Bool {
   114→        totalCleanupCandidates > 0
   115→    }
   116→
   117→    var cleanupSummary: String {
   118→        if totalCleanupCandidates == 0 {
   119→            return "Inbox is clean!"
   120→        }
   121→        var parts: [String] = []
   122→        if safeToDeleteCount > 0 {
   123→            parts.append("\(safeToDeleteCount) safe to delete")
   124→        }
   125→        if suspiciousCount > 0 {
   126→            parts.append("\(suspiciousCount) suspicious")
   127→        }
   128→        return parts.joined(separator: ", ")
   129→    }
   130→}
   131→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

import Foundation
import Combine

// MARK: - EmailStore Core

@MainActor
class EmailStore: ObservableObject {
    // MARK: - Published Properties

    @Published var emails: [Email] = []
    @Published var threads: [EmailThread] = []
    @Published var isLoading = false
    @Published var isLoadingMore = false
    @Published var error: String?
    @Published var selectedEmail: Email?

    // Pagination
    @Published var hasMorePages = true
    @Published var totalEmailsLoaded = 0
    var nextPageToken: String?
    var pageSize: Int { AppSettings.shared.pageSize }

    // Settings
    var settings: AppSettings { AppSettings.shared }

    // Filters
    @Published var searchQuery = ""
    @Published var selectedCategory: EmailAnalysis.Category?
    @Published var selectedPriority: EmailAnalysis.Priority?
    @Published var showUnreadOnly = false

    // Stats
    @Published var stats: EmailStats = EmailStats()

    // Analysis State Manager (for live updates)
    @Published var analysisState = AnalysisStateManager()

    // Throttled updates
    var lastStatsUpdate: Date = .distantPast
    let statsUpdateInterval: TimeInterval = 0.1 // 100ms throttle

    // Cache
    @Published var isUsingCache = false
    @Published var lastCacheUpdate: Date?
    let cacheService = EmailCacheService.shared

    // Services
    var gmailService: GmailService
    let aiService: AIAnalysisService
    private var cancellables = Set<AnyCancellable>()

    // MARK: - Initialization

    init(gmailService: GmailService = GmailService(),
         aiService: AIAnalysisService = AIAnalysisService()) {
        self.gmailService = gmailService
        self.aiService = aiService
    }

    // MARK: - Configuration

    func configure(authManager: GoogleAuthManager) {
        gmailService.setAuthManager(authManager)
    }

    // MARK: - Stats Update

    func updateStats() {
        stats = EmailStats(
            totalEmails: emails.count,
            unreadCount: emails.filter { !$0.isRead }.count,
            analyzedCount: emails.filter { $0.analysis != nil }.count,
            byCategory: Dictionary(grouping: emails.compactMap { $0.analysis?.category }) { $0 }
                .mapValues { $0.count },
            byPriority: Dictionary(grouping: emails.compactMap { $0.analysis?.priority }) { $0 }
                .mapValues { $0.count },
            bySentiment: Dictionary(grouping: emails.compactMap { $0.analysis?.sentiment }) { $0 }
                .mapValues { $0.count }
        )
    }
}

// MARK: - Email Stats

struct EmailStats {
    var totalEmails: Int = 0
    var unreadCount: Int = 0
    var analyzedCount: Int = 0
    var byCategory: [EmailAnalysis.Category: Int] = [:]
    var byPriority: [EmailAnalysis.Priority: Int] = [:]
    var bySentiment: [EmailAnalysis.Sentiment: Int] = [:]

    var analyzedPercentage: Double {
        guard totalEmails > 0 else { return 0 }
        return Double(analyzedCount) / Double(totalEmails) * 100
    }
}

// MARK: - Cleanup Statistics

struct CleanupStats {
    let safeToDeleteCount: Int
    let reviewCount: Int
    let suspiciousCount: Int
    let oldUnreadCount: Int
    let requiresActionCount: Int
    let importantCount: Int

    var totalCleanupCandidates: Int {
        safeToDeleteCount + suspiciousCount
    }

    var hasCleanupOpportunities: Bool {
        totalCleanupCandidates > 0
    }

    var cleanupSummary: String {
        if totalCleanupCandidates == 0 {
            return "Inbox is clean!"
        }
        var parts: [String] = []
        if safeToDeleteCount > 0 {
            parts.append("\(safeToDeleteCount) safe to delete")
        }
        if suspiciousCount > 0 {
            parts.append("\(suspiciousCount) suspicious")
        }
        return parts.joined(separator: ", ")
    }
}


Now let me check the Email model to understand the date structure:


     1→import Foundation
     2→
     3→struct Email: Identifiable, Codable {
     4→    let id: String
     5→    let threadId: String
     6→    let from: String
     7→    let to: String
     8→    let subject: String
     9→    let snippet: String
    10→    let body: String
    11→    let date: Date
    12→    var isRead: Bool
    13→    var isStarred: Bool
    14→    var labels: [String]
    15→
    16→    // Threading headers (for Reply)
    17→    var messageId: String?      // Header: Message-ID
    18→    var inReplyTo: String?      // Header: In-Reply-To
    19→    var references: String?     // Header: References
    20→
    21→    // Attachments
    22→    var attachments: [Attachment]
    23→
    24→    // AI Analysis
    25→    var analysis: EmailAnalysis?
    26→
    27→    var hasAttachments: Bool { !attachments.isEmpty }
    28→    var attachmentCount: Int { attachments.count }
    29→
    30→    var fromName: String {
    31→        if let match = from.firstMatch(of: /^([^<]+)\s*</) {
    32→            return String(match.1).trimmingCharacters(in: .whitespaces)
    33→        }
    34→        return from.components(separatedBy: "@").first ?? from
    35→    }
    36→
    37→    var fromEmail: String {
    38→        if let match = from.firstMatch(of: /<([^>]+)>/) {
    39→            return String(match.1)
    40→        }
    41→        return from
    42→    }
    43→}
    44→
    45→struct EmailAnalysis: Codable {
    46→    let summary: String
    47→    let sentiment: Sentiment
    48→    let priority: Priority
    49→    let category: Category
    50→    let actionItems: [String]
    51→    let keyTopics: [String]
    52→    let suggestedReply: String?
    53→    let analyzedAt: Date
    54→
    55→    // Phase 4: Advanced AI fields
    56→    var confidence: Double
    57→    var isSpam: Bool
    58→    var phishingRisk: PhishingRisk
    59→
    60→    enum PhishingRisk: String, Codable, CaseIterable {
    61→        case none, low, medium, high
    62→
    63→        var color: String {
    64→            switch self {
    65→            case .none: return "green"
    66→            case .low: return "yellow"
    67→            case .medium: return "orange"
    68→            case .high: return "red"
    69→            }
    70→        }
    71→
    72→        var icon: String {
    73→            switch self {
    74→            case .none: return "checkmark.shield"
    75→            case .low: return "shield"
    76→            case .medium: return "exclamationmark.shield"
    77→            case .high: return "xmark.shield.fill"
    78→            }
    79→        }
    80→    }
    81→
    82→    enum DeleteRecommendation: String, Codable, CaseIterable {
    83→        case safeToDelete   // importanceScore < 30
    84→        case review         // importanceScore 30-60
    85→        case keep           // importanceScore > 60
    86→        case suspicious     // phishingRisk >= medium OR isSpam
    87→
    88→        var icon: String {
    89→            switch self {
    90→            case .safeToDelete: return "trash.circle.fill"
    91→            case .review: return "eye.circle.fill"
    92→            case .keep: return "checkmark.circle.fill"
    93→            case .suspicious: return "exclamationmark.shield.fill"
    94→            }
    95→        }
    96→
    97→        var description: String {
    98→            switch self {
    99→            case .safeToDelete: return "Safe to delete"
   100→            case .review: return "Review before deleting"
   101→            case .keep: return "Keep this email"
   102→            case .suspicious: return "Suspicious - do not interact"
   103→            }
   104→        }
   105→    }
   106→
   107→    // Importance scoring for cleanup recommendations
   108→    var importanceScore: Int
   109→    var deleteRecommendation: DeleteRecommendation
   110→    var deleteReason: String?
   111→
   112→    // Computed properties
   113→    var confidencePercentage: Int {
   114→        Int(confidence * 100)
   115→    }
   116→
   117→    var isSuspicious: Bool {
   118→        isSpam || phishingRisk == .medium || phishingRisk == .high
   119→    }
   120→
   121→    var hasActionItems: Bool {
   122→        !actionItems.isEmpty
   123→    }
   124→
   125→    var importanceLevel: String {
   126→        switch importanceScore {
   127→        case 0..<30: return "Low"
   128→        case 30..<60: return "Medium"
   129→        case 60..<80: return "High"
   130→        default: return "Critical"
   131→        }
   132→    }
   133→
   134→    enum Sentiment: String, Codable, CaseIterable {
   135→        case positive, neutral, negative, urgent
   136→
   137→        var color: String {
   138→            switch self {
   139→            case .positive: return "green"
   140→            case .neutral: return "gray"
   141→            case .negative: return "red"
   142→            case .urgent: return "orange"
   143→            }
   144→        }
   145→
   146→        var icon: String {
   147→            switch self {
   148→            case .positive: return "face.smiling"
   149→            case .neutral: return "face.dashed"
   150→            case .negative: return "exclamationmark.triangle"
   151→            case .urgent: return "flame"
   152→            }
   153→        }
   154→    }
   155→
   156→    enum Priority: String, Codable, CaseIterable {
   157→        case low, medium, high, critical
   158→
   159→        var color: String {
   160→            switch self {
   161→            case .low: return "gray"
   162→            case .medium: return "blue"
   163→            case .high: return "orange"
   164→            case .critical: return "red"
   165→            }
   166→        }
   167→    }
   168→
   169→    enum Category: String, Codable, CaseIterable {
   170→        case work, personal, newsletter, promotional, social, finance, travel, other
   171→
   172→        var icon: String {
   173→            switch self {
   174→            case .work: return "briefcase.fill"
   175→            case .personal: return "person.fill"
   176→            case .newsletter: return "newspaper.fill"
   177→            case .promotional: return "tag.fill"
   178→            case .social: return "person.2.fill"
   179→            case .finance: return "dollarsign.circle.fill"
   180→            case .travel: return "airplane"
   181→            case .other: return "tray.fill"
   182→            }
   183→        }
   184→    }
   185→}
   186→
   187→struct EmailThread: Identifiable, Codable {
   188→    let id: String
   189→    var emails: [Email]
   190→    var subject: String
   191→    var participants: [String]
   192→    var lastMessageDate: Date
   193→
   194→    var unreadCount: Int {
   195→        emails.filter { !$0.isRead }.count
   196→    }
   197→
   198→    var hasAttachments: Bool {
   199→        emails.contains { $0.hasAttachments }
   200→    }
   201→
   202→    var latestEmail: Email? {
   203→        emails.max(by: { $0.date < $1.date })
   204→    }
   205→}
   206→
   207→// MARK: - Attachment Model
   208→
   209→struct Attachment: Identifiable, Codable {
   210→    let id: String           // attachmentId from Gmail
   211→    let messageId: String    // parent message ID
   212→    let filename: String
   213→    let mimeType: String
   214→    let size: Int
   215→
   216→    var sizeFormatted: String {
   217→        let formatter = ByteCountFormatter()
   218→        formatter.countStyle = .file
   219→        return formatter.string(fromByteCount: Int64(size))
   220→    }
   221→
   222→    var icon: String {
   223→        switch mimeType.lowercased() {
   224→        case let type where type.hasPrefix("image/"):
   225→            return "photo"
   226→        case let type where type.hasPrefix("video/"):
   227→            return "video"
   228→        case let type where type.hasPrefix("audio/"):
   229→            return "music.note"
   230→        case "application/pdf":
   231→            return "doc.fill"
   232→        case let type where type.contains("spreadsheet") || type.contains("excel"):
   233→            return "tablecells"
   234→        case let type where type.contains("document") || type.contains("word"):
   235→            return "doc.text"
   236→        case let type where type.contains("presentation") || type.contains("powerpoint"):
   237→            return "play.rectangle"
   238→        case let type where type.contains("zip") || type.contains("compressed"):
   239→            return "doc.zipper"
   240→        default:
   241→            return "paperclip"
   242→        }
   243→    }
   244→
   245→    var isImage: Bool {
   246→        mimeType.lowercased().hasPrefix("image/")
   247→    }
   248→
   249→    var isPDF: Bool {
   250→        mimeType.lowercased() == "application/pdf"
   251→    }
   252→
   253→    var fileExtension: String {
   254→        if let ext = filename.split(separator: ".").last {
   255→            return String(ext).lowercased()
   256→        }
   257→        return ""
   258→    }
   259→}
   260→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

import Foundation

struct Email: Identifiable, Codable {
    let id: String
    let threadId: String
    let from: String
    let to: String
    let subject: String
    let snippet: String
    let body: String
    let date: Date
    var isRead: Bool
    var isStarred: Bool
    var labels: [String]

    // Threading headers (for Reply)
    var messageId: String?      // Header: Message-ID
    var inReplyTo: String?      // Header: In-Reply-To
    var references: String?     // Header: References

    // Attachments
    var attachments: [Attachment]

    // AI Analysis
    var analysis: EmailAnalysis?

    var hasAttachments: Bool { !attachments.isEmpty }
    var attachmentCount: Int { attachments.count }

    var fromName: String {
        if let match = from.firstMatch(of: /^([^<]+)\s*</) {
            return String(match.1).trimmingCharacters(in: .whitespaces)
        }
        return from.components(separatedBy: "@").first ?? from
    }

    var fromEmail: String {
        if let match = from.firstMatch(of: /<([^>]+)>/) {
            return String(match.1)
        }
        return from
    }
}

struct EmailAnalysis: Codable {
    let summary: String
    let sentiment: Sentiment
    let priority: Priority
    let category: Category
    let actionItems: [String]
    let keyTopics: [String]
    let suggestedReply: String?
    let analyzedAt: Date

    // Phase 4: Advanced AI fields
    var confidence: Double
    var isSpam: Bool
    var phishingRisk: PhishingRisk

    enum PhishingRisk: String, Codable, CaseIterable {
        case none, low, medium, high

        var color: String {
            switch self {
            case .none: return "green"
            case .low: return "yellow"
            case .medium: return "orange"
            case .high: return "red"
            }
        }

        var icon: String {
            switch self {
            case .none: return "checkmark.shield"
            case .low: return "shield"
            case .medium: return "exclamationmark.shield"
            case .high: return "xmark.shield.fill"
            }
        }
    }

    enum DeleteRecommendation: String, Codable, CaseIterable {
        case safeToDelete   // importanceScore < 30
        case review         // importanceScore 30-60
        case keep           // importanceScore > 60
        case suspicious     // phishingRisk >= medium OR isSpam

        var icon: String {
            switch self {
            case .safeToDelete: return "trash.circle.fill"
            case .review: return "eye.circle.fill"
            case .keep: return "checkmark.circle.fill"
            case .suspicious: return "exclamationmark.shield.fill"
            }
        }

        var description: String {
            switch self {
            case .safeToDelete: return "Safe to delete"
            case .review: return "Review before deleting"
            case .keep: return "Keep this email"
            case .suspicious: return "Suspicious - do not interact"
            }
        }
    }

    // Importance scoring for cleanup recommendations
    var importanceScore: Int
    var deleteRecommendation: DeleteRecommendation
    var deleteReason: String?

    // Computed properties
    var confidencePercentage: Int {
        Int(confidence * 100)
    }

    var isSuspicious: Bool {
        isSpam || phishingRisk == .medium || phishingRisk == .high
    }

    var hasActionItems: Bool {
        !actionItems.isEmpty
    }

    var importanceLevel: String {
        switch importanceScore {
        case 0..<30: return "Low"
        case 30..<60: return "Medium"
        case 60..<80: return "High"
        default: return "Critical"
        }
    }

    enum Sentiment: String, Codable, CaseIterable {
        case positive, neutral, negative, urgent

        var color: String {
            switch self {
            case .positive: return "green"
            case .neutral: return "gray"
            case .negative: return "red"
            case .urgent: return "orange"
            }
        }

        var icon: String {
            switch self {
            case .positive: return "face.smiling"
            case .neutral: return "face.dashed"
            case .negative: return "exclamationmark.triangle"
            case .urgent: return "flame"
            }
        }
    }

    enum Priority: String, Codable, CaseIterable {
        case low, medium, high, critical

        var color: String {
            switch self {
            case .low: return "gray"
            case .medium: return "blue"
            case .high: return "orange"
            case .critical: return "red"
            }
        }
    }

    enum Category: String, Codable, CaseIterable {
        case work, personal, newsletter, promotional, social, finance, travel, other

        var icon: String {
            switch self {
            case .work: return "briefcase.fill"
            case .personal: return "person.fill"
            case .newsletter: return "newspaper.fill"
            case .promotional: return "tag.fill"
            case .social: return "person.2.fill"
            case .finance: return "dollarsign.circle.fill"
            case .travel: return "airplane"
            case .other: return "tray.fill"
            }
        }
    }
}

struct EmailThread: Identifiable, Codable {
    let id: String
    var emails: [Email]
    var subject: String
    var participants: [String]
    var lastMessageDate: Date

    var unreadCount: Int {
        emails.filter { !$0.isRead }.count
    }

    var hasAttachments: Bool {
        emails.contains { $0.hasAttachments }
    }

    var latestEmail: Email? {
        emails.max(by: { $0.date < $1.date })
    }
}

// MARK: - Attachment Model

struct Attachment: Identifiable, Codable {
    let id: String           // attachmentId from Gmail
    let messageId: String    // parent message ID
    let filename: String
    let mimeType: String
    let size: Int

    var sizeFormatted: String {
        let formatter = ByteCountFormatter()
        formatter.countStyle = .file
        return formatter.string(fromByteCount: Int64(size))
    }

    var icon: String {
        switch mimeType.lowercased() {
        case let type where type.hasPrefix("image/"):
            return "photo"
        case let type where type.hasPrefix("video/"):
            return "video"
        case let type where type.hasPrefix("audio/"):
            return "music.note"
        case "application/pdf":
            return "doc.fill"
        case let type where type.contains("spreadsheet") || type.contains("excel"):
            return "tablecells"
        case let type where type.contains("document") || type.contains("word"):
            return "doc.text"
        case let type where type.contains("presentation") || type.contains("powerpoint"):
            return "play.rectangle"
        case let type where type.contains("zip") || type.contains("compressed"):
            return "doc.zipper"
        default:
            return "paperclip"
        }
    }

    var isImage: Bool {
        mimeType.lowercased().hasPrefix("image/")
    }

    var isPDF: Bool {
        mimeType.lowercased() == "application/pdf"
    }

    var fileExtension: String {
        if let ext = filename.split(separator: ".").last {
            return String(ext).lowercased()
        }
        return ""
    }
}


Now let me check what other files might help understand the UI patterns better, especially for any date-related code:


26:        .sorted { $0.date > $1.date }
39:        .sorted { $0.date > $1.date }
57:        .sorted { $0.date > $1.date }
66:        .sorted { $0.date > $1.date }
71:        let sevenDaysAgo = Calendar.current.date(byAdding: .day, value: -7, to: Date()) ?? Date()
73:            !email.isRead && email.date < sevenDaysAgo
75:        .sorted { $0.date < $1.date }

[Showing results with pagination = limit: 50, offset: 0]


Found 9 files
Apps/iOS/GmailAI-iOS/Models/EmailStoreAnalysis.swift
Apps/iOS/GmailAI-iOS/Models/EmailStoreFetching.swift
Apps/iOS/GmailAI-iOS/Models/EmailStore.swift
Apps/iOS/GmailAI-iOS/Models/EmailStoreFiltering.swift
Apps/iOS/GmailAI-iOS/Models/AppSettings.swift
Apps/iOS/GmailAI-iOS/Models/AnalysisStateManager.swift
Apps/iOS/GmailAI-iOS/Models/AnalysisLogEntry.swift
Apps/iOS/GmailAI-iOS/Models/Email.swift
Apps/iOS/GmailAI-iOS/Models/EmailDraft.swift


Perfect! Let me also check the AppSettings to see if there are any existing settings patterns:


grep -A 30 "class AppSettings" /Users/neog/Apps/iOS/GmailAI-iOS/Models/AppSettings.swift | head -50

Check AppSettings class structure for patterns


class AppSettings: ObservableObject {
    static let shared = AppSettings()

    // MARK: - Performance Settings

    /// Number of emails to fetch per page (25-200)
    @AppStorage("settings_pageSize") var pageSize: Int = 100

    /// Maximum concurrent AI analysis requests (1-10)
    @AppStorage("settings_concurrentAnalysis") var concurrentAnalysis: Int = 5

    /// Number of retry attempts for failed requests (1-5)
    @AppStorage("settings_retryAttempts") var retryAttempts: Int = 3

    /// Maximum characters per chunk for long email analysis (500-3000)
    @AppStorage("settings_maxBodyChunkSize") var maxBodyChunkSize: Int = 1500

    // MARK: - AI Model Settings

    /// AI temperature (0.0-1.0) - lower = more focused, higher = more creative
    @AppStorage("settings_aiTemperature") var aiTemperature: Double = 0.3

    /// Maximum tokens per AI request (100-4000)
    @AppStorage("settings_aiMaxTokens") var aiMaxTokens: Int = 1000

    /// AI request timeout in seconds (10-120)
    @AppStorage("settings_aiTimeout") var aiTimeout: Int = 60

    /// Selected AI model ID (empty = default/auto)
    @AppStorage("settings_aiModelId") var aiModelId: String = ""


Excellent! Now let me get a comprehensive overview by checking the project structure one more time to confirm all key files:


find /Users/neog/Apps/iOS/GmailAI-iOS/Models -name "*.swift" -type f | sort

List all model files in GmailAI-iOS


find /Users/neog/Apps/iOS/GmailAI-iOS/Views -name "*Component*" -o -name "*Sheet*" | sort

Find view component files