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