1package tui
2
3import (
4 "fmt"
5 "io"
6 "strings"
7 "time"
8
9 "charm.land/bubbles/v2/key"
10 "charm.land/bubbles/v2/list"
11 tea "charm.land/bubbletea/v2"
12 "charm.land/lipgloss/v2"
13 "github.com/floatpane/matcha/config"
14 "github.com/floatpane/matcha/fetcher"
15 "github.com/floatpane/matcha/theme"
16)
17
18var (
19 // In bubbles v2, list.DefaultStyles() takes a boolean for hasDarkBackground
20 paginationStyle = list.DefaultStyles(true).PaginationStyle.PaddingLeft(4)
21 inboxHelpStyle = list.DefaultStyles(true).HelpStyle.PaddingLeft(4).PaddingBottom(1)
22 tabStyle = lipgloss.NewStyle().Padding(0, 2)
23 activeTabStyle = lipgloss.NewStyle().Padding(0, 2).Foreground(lipgloss.Color("42")).Bold(true).Underline(true)
24 tabBarStyle = lipgloss.NewStyle().BorderStyle(lipgloss.NormalBorder()).BorderBottom(true).PaddingBottom(1).MarginBottom(1)
25)
26
27var dateStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("243"))
28var senderStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("250")).Bold(true)
29
30type item struct {
31 title, desc string
32 originalIndex int
33 uid uint32
34 accountID string
35 accountEmail string
36 date time.Time
37}
38
39func (i item) Title() string { return i.title }
40func (i item) Description() string { return i.desc }
41func (i item) FilterValue() string { return i.title + " " + i.desc }
42
43type itemDelegate struct{}
44
45func (d itemDelegate) Height() int { return 1 }
46func (d itemDelegate) Spacing() int { return 0 }
47func (d itemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd { return nil }
48func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) {
49 i, ok := listItem.(item)
50 if !ok {
51 return
52 }
53
54 prefix := fmt.Sprintf("%d. ", index+1)
55 sender := parseSenderName(i.desc)
56 styledSender := senderStyle.Render(sender)
57 separator := " · "
58
59 // For "ALL" view, show account indicator instead of number
60 if i.accountEmail != "" {
61 prefix = fmt.Sprintf("%d. [%s] ", index+1, truncateEmail(i.accountEmail))
62 }
63
64 // Format and right-align date
65 dateStr := formatRelativeDate(i.date)
66 styledDate := dateStyle.Render(dateStr)
67 dateWidth := lipgloss.Width(styledDate)
68
69 listWidth := m.Width()
70 isSelected := index == m.Index()
71 cursorWidth := 0
72 if isSelected {
73 cursorWidth = 2 // "> " prefix
74 }
75
76 // Available width for the whole left side (prefix + sender + separator + subject)
77 maxLeft := listWidth - dateWidth - 2 - cursorWidth // 2 for spacing
78 if maxLeft < 10 {
79 maxLeft = 10
80 }
81
82 prefixWidth := lipgloss.Width(prefix)
83 senderWidth := lipgloss.Width(styledSender)
84 sepWidth := len(separator)
85 subjectBudget := maxLeft - prefixWidth - senderWidth - sepWidth
86
87 subject := i.title
88 if subjectBudget < 4 {
89 subjectBudget = 4
90 }
91 if lipgloss.Width(subject) > subjectBudget {
92 for lipgloss.Width(subject) > subjectBudget-1 && len(subject) > 0 {
93 subject = subject[:len(subject)-1]
94 }
95 subject += "…"
96 }
97
98 str := prefix + styledSender + separator + subject
99
100 // Pad to push date to the right
101 padding := listWidth - lipgloss.Width(str) - dateWidth - cursorWidth
102 if padding < 1 {
103 padding = 1
104 }
105
106 fn := itemStyle.Render
107 if index == m.Index() {
108 fn = func(s ...string) string {
109 return selectedItemStyle.Render("> " + s[0])
110 }
111 }
112
113 fmt.Fprint(w, fn(str+strings.Repeat(" ", padding)+styledDate))
114}
115
116// formatRelativeDate formats a time as relative if within the last week,
117// otherwise as an absolute date.
118func formatRelativeDate(t time.Time) string {
119 if t.IsZero() {
120 return ""
121 }
122 now := time.Now()
123 d := now.Sub(t)
124
125 switch {
126 case d < time.Minute:
127 return "just now"
128 case d < time.Hour:
129 mins := int(d.Minutes())
130 if mins == 1 {
131 return "1 min ago"
132 }
133 return fmt.Sprintf("%d min ago", mins)
134 case d < 24*time.Hour:
135 hours := int(d.Hours())
136 if hours == 1 {
137 return "1 hour ago"
138 }
139 return fmt.Sprintf("%d hours ago", hours)
140 case d < 7*24*time.Hour:
141 days := int(d.Hours() / 24)
142 if days == 1 {
143 return "1 day ago"
144 }
145 return fmt.Sprintf("%d days ago", days)
146 default:
147 if t.Year() == now.Year() {
148 return t.Format("Jan 02")
149 }
150 return t.Format("Jan 02, 2006")
151 }
152}
153
154// parseSenderName extracts the display name from a "Name <email>" string,
155// falling back to the local part of the email address.
156func parseSenderName(from string) string {
157 if idx := strings.Index(from, " <"); idx > 0 {
158 return strings.TrimSpace(from[:idx])
159 }
160 // No display name — use local part of email
161 if idx := strings.Index(from, "@"); idx > 0 {
162 return from[:idx]
163 }
164 return from
165}
166
167// truncateEmail shortens an email for display
168func truncateEmail(email string) string {
169 parts := strings.Split(email, "@")
170 if len(parts) >= 1 && len(parts[0]) > 8 {
171 return parts[0][:8] + "..."
172 }
173 if len(parts) >= 1 {
174 return parts[0]
175 }
176 return email
177}
178
179// AccountTab represents a tab for an account
180type AccountTab struct {
181 ID string
182 Label string
183 Email string
184}
185
186type Inbox struct {
187 list list.Model
188 isFetching bool
189 isRefreshing bool
190 emailsCount int
191 accounts []config.Account
192 emailsByAccount map[string][]fetcher.Email
193 allEmails []fetcher.Email
194 tabs []AccountTab
195 activeTabIndex int
196 width int
197 height int
198 currentAccountID string // Empty means "ALL"
199 emailCountByAcct map[string]int
200 mailbox MailboxKind
201 folderName string // Custom folder name override for title
202 noMoreByAccount map[string]bool // Per-account: true when pagination returns 0 results
203 extraShortHelpKeys []key.Binding
204}
205
206func NewInbox(emails []fetcher.Email, accounts []config.Account) *Inbox {
207 return NewInboxWithMailbox(emails, accounts, MailboxInbox)
208}
209
210func NewSentInbox(emails []fetcher.Email, accounts []config.Account) *Inbox {
211 return NewInboxWithMailbox(emails, accounts, MailboxSent)
212}
213
214func NewTrashInbox(emails []fetcher.Email, accounts []config.Account) *Inbox {
215 return NewInboxWithMailbox(emails, accounts, MailboxTrash)
216}
217
218func NewArchiveInbox(emails []fetcher.Email, accounts []config.Account) *Inbox {
219 return NewInboxWithMailbox(emails, accounts, MailboxArchive)
220}
221
222func NewInboxWithMailbox(emails []fetcher.Email, accounts []config.Account, mailbox MailboxKind) *Inbox {
223 // Build tabs: empty for single account, "ALL" + accounts for multiple
224 var tabs []AccountTab
225 if len(accounts) <= 1 {
226 tabs = []AccountTab{{ID: "", Label: "", Email: ""}}
227 } else {
228 tabs = []AccountTab{{ID: "", Label: "ALL", Email: ""}}
229 for _, acc := range accounts {
230 // Use FetchEmail for display, fall back to Email if not set
231 displayEmail := acc.FetchEmail
232 if displayEmail == "" {
233 displayEmail = acc.Email
234 }
235 tabs = append(tabs, AccountTab{ID: acc.ID, Label: displayEmail, Email: displayEmail})
236 }
237 }
238
239 // Group emails by account
240 emailsByAccount := make(map[string][]fetcher.Email)
241 for _, email := range emails {
242 emailsByAccount[email.AccountID] = append(emailsByAccount[email.AccountID], email)
243 }
244
245 // Track email counts per account
246 emailCountByAcct := make(map[string]int)
247 for accID, accEmails := range emailsByAccount {
248 emailCountByAcct[accID] = len(accEmails)
249 }
250
251 inbox := &Inbox{
252 accounts: accounts,
253 emailsByAccount: emailsByAccount,
254 allEmails: emails,
255 tabs: tabs,
256 activeTabIndex: 0,
257 currentAccountID: "",
258 emailCountByAcct: emailCountByAcct,
259 mailbox: mailbox,
260 }
261
262 inbox.updateList()
263 return inbox
264}
265
266// NewInboxSingleAccount creates an inbox for a single account (legacy support)
267func NewInboxSingleAccount(emails []fetcher.Email) *Inbox {
268 return NewInbox(emails, nil)
269}
270
271func (m *Inbox) updateList() {
272 // Capture current index to restore later
273 currentIndex := m.list.Index()
274
275 var displayEmails []fetcher.Email
276 var showAccountLabel bool
277
278 if m.currentAccountID == "" {
279 // "ALL" view - show all emails sorted by date
280 displayEmails = m.allEmails
281 showAccountLabel = !(len(m.accounts) <= 1)
282 } else {
283 // Specific account view
284 displayEmails = m.emailsByAccount[m.currentAccountID]
285 showAccountLabel = false
286 }
287
288 m.emailsCount = len(displayEmails)
289
290 items := make([]list.Item, len(displayEmails))
291 for i, email := range displayEmails {
292 accountEmail := ""
293 if showAccountLabel {
294 // Find the account email for display
295 for _, acc := range m.accounts {
296 if acc.ID == email.AccountID {
297 accountEmail = acc.FetchEmail
298 break
299 }
300 }
301 }
302
303 items[i] = item{
304 title: email.Subject,
305 desc: email.From,
306 originalIndex: i,
307 uid: email.UID,
308 accountID: email.AccountID,
309 accountEmail: accountEmail,
310 date: email.Date,
311 }
312 }
313
314 l := list.New(items, itemDelegate{}, 20, 14)
315 l.Title = m.getTitle()
316 l.SetShowStatusBar(true)
317 l.SetFilteringEnabled(true)
318 l.Styles.Title = lipgloss.NewStyle().Foreground(theme.ActiveTheme.Accent).Bold(true)
319 l.Styles.PaginationStyle = paginationStyle
320 l.Styles.HelpStyle = inboxHelpStyle
321 l.SetStatusBarItemName("email", "emails")
322 l.AdditionalShortHelpKeys = func() []key.Binding {
323 bindings := []key.Binding{
324 key.NewBinding(key.WithKeys("d"), key.WithHelp("\uf014 d", "delete")),
325 key.NewBinding(key.WithKeys("a"), key.WithHelp("\uea98 a", "archive")),
326 key.NewBinding(key.WithKeys("r"), key.WithHelp("\ue348 r", "refresh")),
327 }
328 if len(m.tabs) > 1 {
329 bindings = append(bindings,
330 key.NewBinding(key.WithKeys("left", "h"), key.WithHelp("←/h", "prev tab")),
331 key.NewBinding(key.WithKeys("right", "l"), key.WithHelp("→/l", "next tab")),
332 )
333 }
334 bindings = append(bindings, m.extraShortHelpKeys...)
335 return bindings
336 }
337
338 l.KeyMap.Quit.SetEnabled(false)
339
340 // Disable default help to render it manually at the bottom
341 l.SetShowHelp(false)
342
343 if m.width > 0 {
344 l.SetWidth(m.width)
345 }
346 if m.height > 0 {
347 l.SetHeight(m.height / 2)
348 }
349
350 // Restore index
351 // If index is out of bounds (e.g. list shrank), clamp it.
352 if currentIndex >= len(items) {
353 currentIndex = len(items) - 1
354 }
355 if currentIndex < 0 {
356 currentIndex = 0
357 }
358 l.Select(currentIndex)
359
360 m.list = l
361}
362
363func (m *Inbox) getTitle() string {
364 var title string
365 if m.currentAccountID == "" {
366 title = m.getBaseTitle() + " - All Accounts"
367 } else {
368 title = m.getBaseTitle()
369 for _, acc := range m.accounts {
370 if acc.ID == m.currentAccountID {
371 if acc.Name != "" {
372 title = fmt.Sprintf("%s - %s", m.getBaseTitle(), acc.Name)
373 } else {
374 title = fmt.Sprintf("%s - %s", m.getBaseTitle(), acc.FetchEmail)
375 }
376 break
377 }
378 }
379 }
380 if m.isRefreshing {
381 title += " (refreshing...)"
382 }
383 if m.isFetching {
384 title += " (loading more...)"
385 }
386 return title
387}
388
389func (m *Inbox) getBaseTitle() string {
390 if m.folderName != "" {
391 return m.folderName
392 }
393 switch m.mailbox {
394 case MailboxSent:
395 return "Sent"
396 case MailboxTrash:
397 return "Trash"
398 case MailboxArchive:
399 return "Archive"
400 default:
401 return "Inbox"
402 }
403}
404
405func (m *Inbox) Init() tea.Cmd {
406 return nil
407}
408
409func (m *Inbox) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
410 var cmds []tea.Cmd
411
412 switch msg := msg.(type) {
413 case tea.KeyPressMsg:
414 if m.list.FilterState() == list.Filtering {
415 break
416 }
417 switch keypress := msg.String(); keypress {
418 case "left", "h":
419 if len(m.tabs) > 1 {
420 m.activeTabIndex--
421 if m.activeTabIndex < 0 {
422 m.activeTabIndex = len(m.tabs) - 1
423 }
424 m.currentAccountID = m.tabs[m.activeTabIndex].ID
425 m.updateList()
426 return m, nil
427 }
428 case "right", "l":
429 if len(m.tabs) > 1 {
430 m.activeTabIndex++
431 if m.activeTabIndex >= len(m.tabs) {
432 m.activeTabIndex = 0
433 }
434 m.currentAccountID = m.tabs[m.activeTabIndex].ID
435 m.updateList()
436 return m, nil
437 }
438 case "d":
439 selectedItem, ok := m.list.SelectedItem().(item)
440 if ok {
441 return m, func() tea.Msg {
442 return DeleteEmailMsg{UID: selectedItem.uid, AccountID: selectedItem.accountID, Mailbox: m.mailbox}
443 }
444 }
445 case "a":
446 selectedItem, ok := m.list.SelectedItem().(item)
447 if ok {
448 return m, func() tea.Msg {
449 return ArchiveEmailMsg{UID: selectedItem.uid, AccountID: selectedItem.accountID, Mailbox: m.mailbox}
450 }
451 }
452 case "r":
453 m.isRefreshing = true
454 m.list.Title = m.getTitle()
455 // Copy counts to avoid race conditions if used elsewhere (though here it's just passing data)
456 counts := make(map[string]int)
457 for k, v := range m.emailCountByAcct {
458 counts[k] = v
459 }
460 return m, func() tea.Msg {
461 return RequestRefreshMsg{Mailbox: m.mailbox, Counts: counts}
462 }
463 case "enter":
464 selectedItem, ok := m.list.SelectedItem().(item)
465 if ok {
466 idx := selectedItem.originalIndex
467 uid := selectedItem.uid
468 accountID := selectedItem.accountID
469 return m, func() tea.Msg {
470 return ViewEmailMsg{Index: idx, UID: uid, AccountID: accountID, Mailbox: m.mailbox}
471 }
472 }
473 }
474 case tea.WindowSizeMsg:
475 m.width = msg.Width
476 m.height = msg.Height
477 m.list.SetWidth(msg.Width)
478 m.list.SetHeight(msg.Height / 2)
479 if m.shouldFetchMore() {
480 return m, tea.Batch(m.fetchMoreCmds()...)
481 }
482 return m, nil
483
484 case FetchingMoreEmailsMsg:
485 m.isFetching = true
486 m.list.Title = m.getTitle()
487 return m, nil
488
489 case EmailsAppendedMsg:
490 if msg.Mailbox != m.mailbox {
491 return m, nil
492 }
493 m.isFetching = false
494 m.list.Title = m.getTitle()
495
496 if len(msg.Emails) == 0 {
497 if m.noMoreByAccount == nil {
498 m.noMoreByAccount = make(map[string]bool)
499 }
500 m.noMoreByAccount[msg.AccountID] = true
501 return m, nil
502 }
503
504 // Add emails to the appropriate account
505 for _, email := range msg.Emails {
506 m.emailsByAccount[email.AccountID] = append(m.emailsByAccount[email.AccountID], email)
507 m.allEmails = append(m.allEmails, email)
508 }
509 m.emailCountByAcct[msg.AccountID] = len(m.emailsByAccount[msg.AccountID])
510
511 m.updateList()
512 return m, nil
513
514 case RefreshingEmailsMsg:
515 if msg.Mailbox != m.mailbox {
516 return m, nil
517 }
518 m.isRefreshing = true
519 m.list.Title = m.getTitle()
520 return m, nil
521
522 case EmailsRefreshedMsg:
523 if msg.Mailbox != m.mailbox {
524 return m, nil
525 }
526 // Only clear the refreshing indicator. The actual email data is
527 // merged by the main model (preserving paginated emails) and
528 // pushed to us via SetEmails, so we must not overwrite it here.
529 m.isRefreshing = false
530 m.list.Title = m.getTitle()
531 return m, nil
532 }
533
534 var cmd tea.Cmd
535 m.list, cmd = m.list.Update(msg)
536 cmds = append(cmds, cmd)
537
538 if m.shouldFetchMore() {
539 cmds = append(cmds, m.fetchMoreCmds()...)
540 }
541 return m, tea.Batch(cmds...)
542}
543
544func (m *Inbox) shouldFetchMore() bool {
545 if m.isFetching || m.isRefreshing {
546 return false
547 }
548 if m.allAccountsExhausted() {
549 return false
550 }
551 if len(m.list.Items()) == 0 {
552 return false
553 }
554 if m.list.FilterState() == list.Filtering {
555 return false
556 }
557 // Fetch if we've reached the bottom OR if we don't have enough items to fill the view
558 return m.list.Index() >= len(m.list.Items())-1 || len(m.list.Items()) < m.list.Height()
559}
560
561// allAccountsExhausted returns true if all relevant accounts have no more emails to fetch.
562func (m *Inbox) allAccountsExhausted() bool {
563 if len(m.noMoreByAccount) == 0 {
564 return false
565 }
566 if m.currentAccountID != "" {
567 return m.noMoreByAccount[m.currentAccountID]
568 }
569 // "ALL" view: all accounts must be exhausted
570 for _, acc := range m.accounts {
571 if !m.noMoreByAccount[acc.ID] {
572 return false
573 }
574 }
575 return len(m.accounts) > 0
576}
577
578func (m *Inbox) fetchMoreCmds() []tea.Cmd {
579 var cmds []tea.Cmd
580 limit := uint32(m.list.Height())
581 if limit < 20 {
582 limit = 20
583 }
584
585 if m.currentAccountID == "" {
586 if len(m.accounts) == 0 {
587 return nil
588 }
589 for _, acc := range m.accounts {
590 accountID := acc.ID
591 if m.noMoreByAccount[accountID] {
592 continue
593 }
594 offset := uint32(len(m.emailsByAccount[accountID]))
595 cmds = append(cmds, func(id string, off uint32) tea.Cmd {
596 return func() tea.Msg {
597 return FetchMoreEmailsMsg{Offset: off, AccountID: id, Mailbox: m.mailbox, Limit: limit}
598 }
599 }(accountID, offset))
600 }
601 return cmds
602 }
603
604 if m.noMoreByAccount[m.currentAccountID] {
605 return nil
606 }
607 offset := uint32(len(m.emailsByAccount[m.currentAccountID]))
608 cmds = append(cmds, func(id string, off uint32) tea.Cmd {
609 return func() tea.Msg {
610 return FetchMoreEmailsMsg{Offset: off, AccountID: id, Mailbox: m.mailbox, Limit: limit}
611 }
612 }(m.currentAccountID, offset))
613 return cmds
614}
615
616func (m *Inbox) View() tea.View {
617 var b strings.Builder
618
619 // Render tabs if there are multiple accounts
620 if len(m.tabs) > 1 {
621 var tabViews []string
622 for i, tab := range m.tabs {
623 label := tab.Label
624 if tab.ID == "" {
625 label = "ALL"
626 }
627
628 if i == m.activeTabIndex {
629 tabViews = append(tabViews, activeTabStyle.Render(label))
630 } else {
631 tabViews = append(tabViews, tabStyle.Render(label))
632 }
633 }
634 tabBar := tabBarStyle.Render(lipgloss.JoinHorizontal(lipgloss.Top, tabViews...))
635 b.WriteString(tabBar)
636 b.WriteString("\n")
637 }
638
639 b.WriteString(m.list.View())
640
641 // Ensure we don't start gap calculation on the same line as the list
642 if !strings.HasSuffix(b.String(), "\n") {
643 b.WriteString("\n")
644 }
645
646 helpView := inboxHelpStyle.Render(m.list.Help.View(m.list))
647
648 if m.height > 0 {
649 usedHeight := lipgloss.Height(b.String())
650 helpHeight := lipgloss.Height(helpView)
651
652 gap := m.height - usedHeight - helpHeight
653 if gap > 0 {
654 b.WriteString(strings.Repeat("\n", gap))
655 }
656 } else {
657 b.WriteString("\n")
658 }
659
660 b.WriteString(helpView)
661
662 return tea.NewView(b.String())
663}
664
665// GetCurrentAccountID returns the currently selected account ID
666func (m *Inbox) GetCurrentAccountID() string {
667 return m.currentAccountID
668}
669
670// GetEmailAtIndex returns the email at the given index for the current view
671func (m *Inbox) GetEmailAtIndex(index int) *fetcher.Email {
672 var displayEmails []fetcher.Email
673 if m.currentAccountID == "" {
674 displayEmails = m.allEmails
675 } else {
676 displayEmails = m.emailsByAccount[m.currentAccountID]
677 }
678
679 if index >= 0 && index < len(displayEmails) {
680 return &displayEmails[index]
681 }
682 return nil
683}
684
685func (m *Inbox) GetMailbox() MailboxKind {
686 return m.mailbox
687}
688
689// RemoveEmail removes an email by UID and account ID
690func (m *Inbox) RemoveEmail(uid uint32, accountID string) {
691 // Remove from account-specific list
692 if emails, ok := m.emailsByAccount[accountID]; ok {
693 var filtered []fetcher.Email
694 for _, e := range emails {
695 if e.UID != uid {
696 filtered = append(filtered, e)
697 }
698 }
699 m.emailsByAccount[accountID] = filtered
700 }
701
702 // Remove from all emails list
703 var filteredAll []fetcher.Email
704 for _, e := range m.allEmails {
705 if !(e.UID == uid && e.AccountID == accountID) {
706 filteredAll = append(filteredAll, e)
707 }
708 }
709 m.allEmails = filteredAll
710
711 m.updateList()
712}
713
714// SetSize sets the width and height of the inbox, then updates the list.
715func (m *Inbox) SetSize(width, height int) {
716 m.width = width
717 m.height = height
718 m.list.SetWidth(width)
719 m.list.SetHeight(height / 2)
720}
721
722// SetFolderName sets a custom folder name for the inbox title.
723func (m *Inbox) SetFolderName(name string) {
724 m.folderName = name
725 m.list.Title = m.getTitle()
726}
727
728// SetEmails updates all emails (used after fetch)
729func (m *Inbox) SetEmails(emails []fetcher.Email, accounts []config.Account) {
730 m.accounts = accounts
731 m.allEmails = emails
732 m.noMoreByAccount = make(map[string]bool)
733
734 // Rebuild tabs: empty for single account, "ALL" + accounts for multiple
735 var tabs []AccountTab
736 if len(accounts) <= 1 {
737 tabs = []AccountTab{{ID: "", Label: "", Email: ""}}
738 } else {
739 tabs = []AccountTab{{ID: "", Label: "ALL", Email: ""}}
740 for _, acc := range accounts {
741 tabs = append(tabs, AccountTab{ID: acc.ID, Label: acc.FetchEmail, Email: acc.Email})
742 }
743 }
744 m.tabs = tabs
745
746 // Re-group emails by account
747 m.emailsByAccount = make(map[string][]fetcher.Email)
748 for _, email := range emails {
749 m.emailsByAccount[email.AccountID] = append(m.emailsByAccount[email.AccountID], email)
750 }
751
752 // Update email counts
753 m.emailCountByAcct = make(map[string]int)
754 for accID, accEmails := range m.emailsByAccount {
755 m.emailCountByAcct[accID] = len(accEmails)
756 }
757
758 m.updateList()
759}