From 11905a6f803a7d285d14f2ff4a50643fdde461d5 Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Sat, 7 Sep 2024 10:08:26 -0700 Subject: [PATCH 01/75] start on using core.List for mail messages --- mail/app.go | 9 +++++---- mail/read.go | 1 + 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/mail/app.go b/mail/app.go index d1ce9b53..7eb147a9 100644 --- a/mail/app.go +++ b/mail/app.go @@ -58,12 +58,14 @@ func (a *App) Init() { }) tree.AddChildAt(a, "splits", func(w *core.Splits) { + w.SetSplits(0.1, 0.2, 0.7) tree.AddChildAt(w, "mbox", func(w *core.Tree) { w.SetText("Mailboxes") }) - tree.AddChildAt(w, "list", func(w *core.Frame) { - w.Styler(func(s *styles.Style) { - s.Direction = styles.Column + tree.AddChildAt(w, "list", func(w *core.List) { + w.Updater(func() { + sl := a.Cache[a.CurrentEmail][a.CurrentMailbox] + w.SetSlice(&sl) }) }) tree.AddChildAt(w, "mail", func(w *core.Frame) { @@ -79,7 +81,6 @@ func (a *App) Init() { }) }) }) - w.SetSplits(0.1, 0.2, 0.7) }) a.Updater(func() { // a.UpdateReadMessage(ml, msv, mb) diff --git a/mail/read.go b/mail/read.go index b798bcb1..d1941a60 100644 --- a/mail/read.go +++ b/mail/read.go @@ -34,6 +34,7 @@ type ReadMessage struct { // UpdateMessageList updates the message list from [App.Cache]. func (a *App) UpdateMessageList() { + return cached := a.Cache[a.CurrentEmail][a.CurrentMailbox] a.AsyncLock() From 3d0d721c7ed6eb0a1df9221666672b91d4a3dc5f Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Sat, 7 Sep 2024 10:34:31 -0700 Subject: [PATCH 02/75] start on MessageListItem --- mail/read.go | 49 --------------------------------------------- mail/values.go | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 49 deletions(-) diff --git a/mail/read.go b/mail/read.go index d1941a60..c154c50f 100644 --- a/mail/read.go +++ b/mail/read.go @@ -14,12 +14,7 @@ import ( "cogentcore.org/core/base/errors" "cogentcore.org/core/core" - "cogentcore.org/core/cursors" - "cogentcore.org/core/events" "cogentcore.org/core/htmlcore" - "cogentcore.org/core/icons" - "cogentcore.org/core/styles" - "cogentcore.org/core/styles/abilities" "github.com/emersion/go-message/mail" ) @@ -52,50 +47,6 @@ func (a *App) UpdateMessageList() { return cmp.Compare(b.Date.UnixNano(), a.Date.UnixNano()) }) - for i, cd := range cached { - cd := cd - - if i > 100 { - break - } - - fr := core.NewFrame(list) - fr.Styler(func(s *styles.Style) { - s.Direction = styles.Column - }) - - fr.Styler(func(s *styles.Style) { - s.SetAbilities(true, abilities.Activatable, abilities.Hoverable) - s.Cursor = cursors.Pointer - }) - fr.OnClick(func(e events.Event) { - a.ReadMessage = cd - errors.Log(a.UpdateReadMessage()) - }) - fr.AddContextMenu(func(m *core.Scene) { - a.ReadMessage = cd - core.NewFuncButton(m).SetFunc(a.MoveMessage).SetIcon(icons.Move).SetText("Move") - }) - - ftxt := "" - for _, f := range cd.From { - if f.Name != "" { - ftxt += f.Name + " " - } else { - ftxt += f.Addr() + " " - } - } - - from := core.NewText(fr).SetType(core.TextTitleMedium).SetText(ftxt) - from.Styler(func(s *styles.Style) { - s.SetNonSelectable() - }) - subject := core.NewText(fr).SetType(core.TextBodyMedium).SetText(cd.Subject) - subject.Styler(func(s *styles.Style) { - s.SetNonSelectable() - }) - } - list.Update() } diff --git a/mail/values.go b/mail/values.go index eed7b6db..6d984ae2 100644 --- a/mail/values.go +++ b/mail/values.go @@ -9,12 +9,66 @@ import ( "net/mail" "cogentcore.org/core/core" + "cogentcore.org/core/cursors" + "cogentcore.org/core/events" + "cogentcore.org/core/styles" + "cogentcore.org/core/styles/abilities" ) func init() { + core.AddValueType[CacheData, MessageListItem]() core.AddValueType[mail.Address, AddressTextField]() } +// MessageListItem represents a [CacheData] with a [core.Frame] for the message list. +type MessageListItem struct { + core.Frame + Data *CacheData +} + +func (mi *MessageListItem) WidgetValue() any { return &mi.Data } + +func (mi *MessageListItem) Init() { + mi.Frame.Init() + mi.Styler(func(s *styles.Style) { + s.SetAbilities(true, abilities.Activatable, abilities.Hoverable) + s.Cursor = cursors.Pointer + s.Direction = styles.Column + s.Grow.Set(1, 0) + }) + mi.OnClick(func(e events.Event) { + // a.ReadMessage = mi.Data + // errors.Log(a.UpdateReadMessage()) + }) + mi.AddContextMenu(func(m *core.Scene) { + // a.ReadMessage = mi.Data + // core.NewFuncButton(m).SetFunc(a.MoveMessage).SetIcon(icons.Move).SetText("Move") + }) + + from := core.NewText(mi).SetType(core.TextTitleMedium) + from.Styler(func(s *styles.Style) { + s.SetNonSelectable() + }) + from.Updater(func() { + ftxt := "" + for _, f := range mi.Data.From { + if f.Name != "" { + ftxt += f.Name + " " + } else { + ftxt += f.Addr() + " " + } + } + from.SetText(ftxt) + }) + subject := core.NewText(mi).SetType(core.TextBodyMedium) + subject.Styler(func(s *styles.Style) { + s.SetNonSelectable() + }) + subject.Updater(func() { + subject.SetText(mi.Data.Subject) + }) +} + // AddressTextField represents a [mail.Address] with a [core.TextField]. type AddressTextField struct { core.TextField From 58014d99ca7c7b5a7bbf0145f0d5be7112e4c911 Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Sat, 7 Sep 2024 10:38:17 -0700 Subject: [PATCH 03/75] more attempts at getting message list working --- mail/app.go | 7 +++++++ mail/read.go | 22 +--------------------- mail/typegen.go | 11 +++++++++++ mail/values.go | 2 +- 4 files changed, 20 insertions(+), 22 deletions(-) diff --git a/mail/app.go b/mail/app.go index 7eb147a9..72709288 100644 --- a/mail/app.go +++ b/mail/app.go @@ -8,6 +8,9 @@ package mail //go:generate core generate import ( + "cmp" + "slices" + "cogentcore.org/core/core" "cogentcore.org/core/icons" "cogentcore.org/core/styles" @@ -63,8 +66,12 @@ func (a *App) Init() { w.SetText("Mailboxes") }) tree.AddChildAt(w, "list", func(w *core.List) { + w.SetReadOnly(true) w.Updater(func() { sl := a.Cache[a.CurrentEmail][a.CurrentMailbox] + slices.SortFunc(sl, func(a, b *CacheData) int { + return cmp.Compare(b.Date.UnixNano(), a.Date.UnixNano()) + }) w.SetSlice(&sl) }) }) diff --git a/mail/read.go b/mail/read.go index c154c50f..2a8695a6 100644 --- a/mail/read.go +++ b/mail/read.go @@ -5,11 +5,9 @@ package mail import ( - "cmp" "io" "os" "path/filepath" - "slices" "time" "cogentcore.org/core/base/errors" @@ -29,25 +27,7 @@ type ReadMessage struct { // UpdateMessageList updates the message list from [App.Cache]. func (a *App) UpdateMessageList() { - return - cached := a.Cache[a.CurrentEmail][a.CurrentMailbox] - - a.AsyncLock() - defer a.AsyncUnlock() - - list := a.FindPath("splits/list").(*core.Frame) - - if list.NumChildren() > 100 { - return - } - - list.DeleteChildren() // TODO(config) - - slices.SortFunc(cached, func(a, b *CacheData) int { - return cmp.Compare(b.Date.UnixNano(), a.Date.UnixNano()) - }) - - list.Update() + a.Update() } // UpdateReadMessage updates the view of the message currently being read. diff --git a/mail/typegen.go b/mail/typegen.go index 9664fc5c..ee875756 100644 --- a/mail/typegen.go +++ b/mail/typegen.go @@ -17,6 +17,17 @@ func NewApp(parent ...tree.Node) *App { return tree.New[App](parent...) } var _ = types.AddType(&types.Type{Name: "cogentcore.org/cogent/mail.SettingsData", IDName: "settings-data", Doc: "SettingsData is the data type for the global Cogent Mail settings.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Embeds: []types.Field{{Name: "SettingsBase"}}, Fields: []types.Field{{Name: "Accounts", Doc: "Accounts are the email accounts the user is signed into."}}}) +var _ = types.AddType(&types.Type{Name: "cogentcore.org/cogent/mail.MessageListItem", IDName: "message-list-item", Doc: "MessageListItem represents a [CacheData] with a [core.Frame] for the message list.", Embeds: []types.Field{{Name: "Frame"}}, Fields: []types.Field{{Name: "Data"}}}) + +// NewMessageListItem returns a new [MessageListItem] with the given optional parent: +// MessageListItem represents a [CacheData] with a [core.Frame] for the message list. +func NewMessageListItem(parent ...tree.Node) *MessageListItem { + return tree.New[MessageListItem](parent...) +} + +// SetData sets the [MessageListItem.Data] +func (t *MessageListItem) SetData(v *CacheData) *MessageListItem { t.Data = v; return t } + var _ = types.AddType(&types.Type{Name: "cogentcore.org/cogent/mail.AddressTextField", IDName: "address-text-field", Doc: "AddressTextField represents a [mail.Address] with a [core.TextField].", Embeds: []types.Field{{Name: "TextField"}}, Fields: []types.Field{{Name: "Address"}}}) // NewAddressTextField returns a new [AddressTextField] with the given optional parent: diff --git a/mail/values.go b/mail/values.go index 6d984ae2..b5bc595a 100644 --- a/mail/values.go +++ b/mail/values.go @@ -33,7 +33,7 @@ func (mi *MessageListItem) Init() { mi.Styler(func(s *styles.Style) { s.SetAbilities(true, abilities.Activatable, abilities.Hoverable) s.Cursor = cursors.Pointer - s.Direction = styles.Column + // s.Direction = styles.Column s.Grow.Set(1, 0) }) mi.OnClick(func(e events.Event) { From 6bd5d41dbc7c8a19b07ae3052552cdc8a3105b9a Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Sat, 7 Sep 2024 10:42:07 -0700 Subject: [PATCH 04/75] forgot that you need to use AddChild for core.Value Init child adding --- mail/values.go | 45 +++++++++++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/mail/values.go b/mail/values.go index b5bc595a..2376e9de 100644 --- a/mail/values.go +++ b/mail/values.go @@ -13,6 +13,7 @@ import ( "cogentcore.org/core/events" "cogentcore.org/core/styles" "cogentcore.org/core/styles/abilities" + "cogentcore.org/core/tree" ) func init() { @@ -33,7 +34,7 @@ func (mi *MessageListItem) Init() { mi.Styler(func(s *styles.Style) { s.SetAbilities(true, abilities.Activatable, abilities.Hoverable) s.Cursor = cursors.Pointer - // s.Direction = styles.Column + s.Direction = styles.Column s.Grow.Set(1, 0) }) mi.OnClick(func(e events.Event) { @@ -45,27 +46,31 @@ func (mi *MessageListItem) Init() { // core.NewFuncButton(m).SetFunc(a.MoveMessage).SetIcon(icons.Move).SetText("Move") }) - from := core.NewText(mi).SetType(core.TextTitleMedium) - from.Styler(func(s *styles.Style) { - s.SetNonSelectable() - }) - from.Updater(func() { - ftxt := "" - for _, f := range mi.Data.From { - if f.Name != "" { - ftxt += f.Name + " " - } else { - ftxt += f.Addr() + " " + tree.AddChild(mi, func(w *core.Text) { + w.SetType(core.TextTitleMedium) + w.Styler(func(s *styles.Style) { + s.SetNonSelectable() + }) + w.Updater(func() { + text := "" + for _, f := range mi.Data.From { + if f.Name != "" { + text += f.Name + " " + } else { + text += f.Addr() + " " + } } - } - from.SetText(ftxt) - }) - subject := core.NewText(mi).SetType(core.TextBodyMedium) - subject.Styler(func(s *styles.Style) { - s.SetNonSelectable() + w.SetText(text) + }) }) - subject.Updater(func() { - subject.SetText(mi.Data.Subject) + tree.AddChild(mi, func(w *core.Text) { + w.SetType(core.TextBodyMedium) + w.Styler(func(s *styles.Style) { + s.SetNonSelectable() + }) + w.Updater(func() { + w.SetText(mi.Data.Subject) + }) }) } From 3517ab59b27485c42212b84a6a36b01a7ec62fde Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Sat, 7 Sep 2024 10:49:11 -0700 Subject: [PATCH 05/75] add theApp --- mail/app.go | 5 +++++ mail/values.go | 10 ++++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/mail/app.go b/mail/app.go index 72709288..5d9c8846 100644 --- a/mail/app.go +++ b/mail/app.go @@ -52,7 +52,12 @@ type App struct { // needed for interface import var _ tree.Node = (*App)(nil) +// theApp is the current app instance. +// TODO: ideally we could remove this. +var theApp *App + func (a *App) Init() { + theApp = a a.Frame.Init() a.AuthToken = map[string]*oauth2.Token{} a.AuthClient = map[string]sasl.Client{} diff --git a/mail/values.go b/mail/values.go index 2376e9de..b25c5332 100644 --- a/mail/values.go +++ b/mail/values.go @@ -8,9 +8,11 @@ import ( "fmt" "net/mail" + "cogentcore.org/core/base/errors" "cogentcore.org/core/core" "cogentcore.org/core/cursors" "cogentcore.org/core/events" + "cogentcore.org/core/icons" "cogentcore.org/core/styles" "cogentcore.org/core/styles/abilities" "cogentcore.org/core/tree" @@ -38,12 +40,12 @@ func (mi *MessageListItem) Init() { s.Grow.Set(1, 0) }) mi.OnClick(func(e events.Event) { - // a.ReadMessage = mi.Data - // errors.Log(a.UpdateReadMessage()) + theApp.ReadMessage = mi.Data + errors.Log(theApp.UpdateReadMessage()) }) mi.AddContextMenu(func(m *core.Scene) { - // a.ReadMessage = mi.Data - // core.NewFuncButton(m).SetFunc(a.MoveMessage).SetIcon(icons.Move).SetText("Move") + theApp.ReadMessage = mi.Data + core.NewFuncButton(m).SetFunc(theApp.MoveMessage).SetIcon(icons.Move).SetText("Move") }) tree.AddChild(mi, func(w *core.Text) { From e35224dd36668a3e4906a01a49340f1b566a6cdf Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Sat, 7 Sep 2024 12:40:05 -0700 Subject: [PATCH 06/75] improve mail list updating --- mail/cache.go | 10 +++++++--- mail/read.go | 5 ----- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/mail/cache.go b/mail/cache.go index 4d8be215..359574a5 100644 --- a/mail/cache.go +++ b/mail/cache.go @@ -129,7 +129,7 @@ func (a *App) CacheMessagesForMailbox(c *imapclient.Client, email string, mailbo } core.NewTree(embox).SetText(mailbox).OnClick(func(e events.Event) { a.CurrentMailbox = mailbox - a.UpdateMessageList() + a.Update() }) a.AsyncUnlock() @@ -178,7 +178,9 @@ func (a *App) CacheMessagesForMailbox(c *imapclient.Client, email string, mailbo uids := uidsData.AllUIDs() if len(uids) == 0 { - a.UpdateMessageList() + a.AsyncLock() + a.Update() + a.AsyncUnlock() return nil } return a.CacheUIDs(uids, c, email, mailbox, dir, cached, cachedFile) @@ -261,7 +263,9 @@ func (a *App) CacheUIDs(uids []imap.UID, c *imapclient.Client, email string, mai } a.Cache[email][mailbox] = cached - a.UpdateMessageList() + a.AsyncLock() + a.Update() + a.AsyncUnlock() } err := mcmd.Close() diff --git a/mail/read.go b/mail/read.go index 2a8695a6..ddfee99e 100644 --- a/mail/read.go +++ b/mail/read.go @@ -25,11 +25,6 @@ type ReadMessage struct { Date time.Time } -// UpdateMessageList updates the message list from [App.Cache]. -func (a *App) UpdateMessageList() { - a.Update() -} - // UpdateReadMessage updates the view of the message currently being read. func (a *App) UpdateReadMessage() error { msv := a.FindPath("splits/mail/msv").(*core.Form) From 3c1e25c2293a4b9282d1e376d03e8e2f254becb8 Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Sat, 7 Sep 2024 12:55:46 -0700 Subject: [PATCH 07/75] improve mail currentCache handling and improve naming --- mail/actions.go | 4 ++-- mail/app.go | 39 +++++++++++++++++++++------------------ mail/auth.go | 6 +++--- mail/cache.go | 26 +++++++++++++------------- mail/read.go | 6 +++--- mail/send.go | 32 ++++++++++++++++---------------- mail/values.go | 4 ++-- 7 files changed, 60 insertions(+), 57 deletions(-) diff --git a/mail/actions.go b/mail/actions.go index 53b7fbed..9e623ce1 100644 --- a/mail/actions.go +++ b/mail/actions.go @@ -12,9 +12,9 @@ import ( // MoveMessage moves the current message to the given mailbox. func (a *App) MoveMessage(mailbox string) error { //types:add - c := a.IMAPClient[a.CurrentEmail] + c := a.imapClient[a.currentEmail] uidset := imap.UIDSet{} - uidset.AddNum(a.ReadMessage.UID) + uidset.AddNum(a.readMessage.UID) fmt.Println(uidset) mc := c.Move(uidset, mailbox) fmt.Println("mc", mc) diff --git a/mail/app.go b/mail/app.go index 5d9c8846..e5f08aaa 100644 --- a/mail/app.go +++ b/mail/app.go @@ -24,29 +24,32 @@ import ( type App struct { core.Frame - // AuthToken contains the [oauth2.Token] for each account. - AuthToken map[string]*oauth2.Token `set:"-"` + // authToken contains the [oauth2.Token] for each account. + authToken map[string]*oauth2.Token - // AuthClient contains the [sasl.Client] authentication for sending messages for each account. - AuthClient map[string]sasl.Client `set:"-"` + // authClient contains the [sasl.Client] authentication for sending messages for each account. + authClient map[string]sasl.Client // IMAPCLient contains the imap clients for each account. - IMAPClient map[string]*imapclient.Client `set:"-"` + imapClient map[string]*imapclient.Client - // ComposeMessage is the current message we are editing - ComposeMessage *SendMessage `set:"-"` + // composeMessage is the current message we are editing + composeMessage *SendMessage - // Cache contains the cache data, keyed by account and then mailbox. - Cache map[string]map[string][]*CacheData `set:"-"` + // cache contains the cache data, keyed by account and then mailbox. + cache map[string]map[string][]*CacheData - // ReadMessage is the current message we are reading - ReadMessage *CacheData `set:"-"` + // currentCache is [App.cache] for the current email account and mailbox. + currentCache []*CacheData + + // readMessage is the current message we are reading + readMessage *CacheData // The current email account - CurrentEmail string `set:"-"` + currentEmail string // The current mailbox - CurrentMailbox string `set:"-"` + currentMailbox string } // needed for interface import @@ -59,8 +62,8 @@ var theApp *App func (a *App) Init() { theApp = a a.Frame.Init() - a.AuthToken = map[string]*oauth2.Token{} - a.AuthClient = map[string]sasl.Client{} + a.authToken = map[string]*oauth2.Token{} + a.authClient = map[string]sasl.Client{} a.Styler(func(s *styles.Style) { s.Grow.Set(1, 1) }) @@ -73,11 +76,11 @@ func (a *App) Init() { tree.AddChildAt(w, "list", func(w *core.List) { w.SetReadOnly(true) w.Updater(func() { - sl := a.Cache[a.CurrentEmail][a.CurrentMailbox] - slices.SortFunc(sl, func(a, b *CacheData) int { + a.currentCache = a.cache[a.currentEmail][a.currentMailbox] + slices.SortFunc(a.currentCache, func(a, b *CacheData) int { return cmp.Compare(b.Date.UnixNano(), a.Date.UnixNano()) }) - w.SetSlice(&sl) + w.SetSlice(&a.currentCache) }) }) tree.AddChildAt(w, "mail", func(w *core.Frame) { diff --git a/mail/auth.go b/mail/auth.go index 5de7836e..f59ab8b1 100644 --- a/mail/auth.go +++ b/mail/auth.go @@ -25,7 +25,7 @@ func (a *App) Auth() error { return err } - a.AuthClient[email] = xoauth2.NewXoauth2Client(email, a.AuthToken[email].AccessToken) + a.authClient[email] = xoauth2.NewXoauth2Client(email, a.authToken[email].AccessToken) return nil } @@ -39,8 +39,8 @@ func (a *App) SignIn() (string, error) { Settings.Accounts = append(Settings.Accounts, userInfo.Email) errors.Log(core.SaveSettings(Settings)) } - a.CurrentEmail = userInfo.Email - a.AuthToken[userInfo.Email] = token + a.currentEmail = userInfo.Email + a.authToken[userInfo.Email] = token d.Close() email <- userInfo.Email } diff --git a/mail/cache.go b/mail/cache.go index 359574a5..fb621e15 100644 --- a/mail/cache.go +++ b/mail/cache.go @@ -56,11 +56,11 @@ func IMAPToMailAddresses(as []imap.Address) []*mail.Address { // CacheMessages caches all of the messages from the server that // have not already been cached. It caches them in the app's data directory. func (a *App) CacheMessages() error { - if a.Cache == nil { - a.Cache = map[string]map[string][]*CacheData{} + if a.cache == nil { + a.cache = map[string]map[string][]*CacheData{} } - if a.IMAPClient == nil { - a.IMAPClient = map[string]*imapclient.Client{} + if a.imapClient == nil { + a.imapClient = map[string]*imapclient.Client{} } mbox := a.FindPath("splits/mbox").(*core.Tree) mbox.AsyncLock() @@ -79,8 +79,8 @@ func (a *App) CacheMessages() error { // have not already been cached for the given email account. It // caches them in the app's data directory. func (a *App) CacheMessagesForAccount(email string) error { - if a.Cache[email] == nil { - a.Cache[email] = map[string][]*CacheData{} + if a.cache[email] == nil { + a.cache[email] = map[string][]*CacheData{} } c, err := imapclient.DialTLS("imap.gmail.com:993", nil) @@ -89,9 +89,9 @@ func (a *App) CacheMessagesForAccount(email string) error { } defer c.Logout() - a.IMAPClient[email] = c + a.imapClient[email] = c - err = c.Authenticate(a.AuthClient[email]) + err = c.Authenticate(a.authClient[email]) if err != nil { return fmt.Errorf("authenticating: %w", err) } @@ -114,8 +114,8 @@ func (a *App) CacheMessagesForAccount(email string) error { // that have not already been cached for the given email account and mailbox. // It caches them in the app's data directory. func (a *App) CacheMessagesForMailbox(c *imapclient.Client, email string, mailbox string) error { - if a.CurrentMailbox == "" { - a.CurrentMailbox = mailbox + if a.currentMailbox == "" { + a.currentMailbox = mailbox } bemail := FilenameBase32(email) @@ -128,7 +128,7 @@ func (a *App) CacheMessagesForMailbox(c *imapclient.Client, email string, mailbo embox.AsTree().SetName(bemail) } core.NewTree(embox).SetText(mailbox).OnClick(func(e events.Event) { - a.CurrentMailbox = mailbox + a.currentMailbox = mailbox a.Update() }) a.AsyncUnlock() @@ -150,7 +150,7 @@ func (a *App) CacheMessagesForMailbox(c *imapclient.Client, email string, mailbo if err != nil && !errors.Is(err, fs.ErrNotExist) && !errors.Is(err, io.EOF) { return fmt.Errorf("opening cache list: %w", err) } - a.Cache[email][mailbox] = cached + a.cache[email][mailbox] = cached _, err = c.Select(mailbox, nil).Wait() if err != nil { @@ -262,7 +262,7 @@ func (a *App) CacheUIDs(uids []imap.UID, c *imapclient.Client, email string, mai return fmt.Errorf("saving cache list: %w", err) } - a.Cache[email][mailbox] = cached + a.cache[email][mailbox] = cached a.AsyncLock() a.Update() a.AsyncUnlock() diff --git a/mail/read.go b/mail/read.go index ddfee99e..9d43a002 100644 --- a/mail/read.go +++ b/mail/read.go @@ -28,14 +28,14 @@ type ReadMessage struct { // UpdateReadMessage updates the view of the message currently being read. func (a *App) UpdateReadMessage() error { msv := a.FindPath("splits/mail/msv").(*core.Form) - msv.SetStruct(a.ReadMessage.ToMessage()) + msv.SetStruct(a.readMessage.ToMessage()) mb := a.FindPath("splits/mail/mb").(*core.Frame) mb.DeleteChildren() - bemail := FilenameBase32(a.CurrentEmail) + bemail := FilenameBase32(a.currentEmail) - f, err := os.Open(filepath.Join(core.TheApp.AppDataDir(), "mail", bemail, a.ReadMessage.Filename)) + f, err := os.Open(filepath.Join(core.TheApp.AppDataDir(), "mail", bemail, a.readMessage.Filename)) if err != nil { return err } diff --git a/mail/send.go b/mail/send.go index e93b89eb..9708d8dc 100644 --- a/mail/send.go +++ b/mail/send.go @@ -30,11 +30,11 @@ type SendMessage struct { // Compose pulls up a dialog to send a new message func (a *App) Compose() { //types:add - a.ComposeMessage = &SendMessage{} - a.ComposeMessage.From = []*mail.Address{{Address: Settings.Accounts[0]}} - a.ComposeMessage.To = []*mail.Address{{}} + a.composeMessage = &SendMessage{} + a.composeMessage.From = []*mail.Address{{Address: Settings.Accounts[0]}} + a.composeMessage.To = []*mail.Address{{}} b := core.NewBody("Send message") - core.NewForm(b).SetStruct(a.ComposeMessage) + core.NewForm(b).SetStruct(a.composeMessage) ed := texteditor.NewEditor(b) ed.Buffer.SetLanguage(fileinfo.Markdown) ed.Buffer.Options.LineNumbers = false @@ -44,7 +44,7 @@ func (a *App) Compose() { //types:add b.AddBottomBar(func(bar *core.Frame) { b.AddCancel(bar) b.AddOK(bar).SetText("Send").OnClick(func(e events.Event) { - a.ComposeMessage.Body = ed.Buffer.String() + a.composeMessage.Body = ed.Buffer.String() a.SendMessage() }) }) @@ -53,18 +53,18 @@ func (a *App) Compose() { //types:add // SendMessage sends the current message func (a *App) SendMessage() error { //types:add - if len(a.ComposeMessage.From) != 1 { - return fmt.Errorf("expected 1 sender, but got %d", len(a.ComposeMessage.From)) + if len(a.composeMessage.From) != 1 { + return fmt.Errorf("expected 1 sender, but got %d", len(a.composeMessage.From)) } - email := a.ComposeMessage.From[0].Address + email := a.composeMessage.From[0].Address var b bytes.Buffer var h mail.Header h.SetDate(time.Now()) - h.SetAddressList("From", a.ComposeMessage.From) - h.SetAddressList("To", a.ComposeMessage.To) - h.SetSubject(a.ComposeMessage.Subject) + h.SetAddressList("From", a.composeMessage.From) + h.SetAddressList("To", a.composeMessage.To) + h.SetSubject(a.composeMessage.Subject) mw, err := mail.CreateWriter(&b, h) if err != nil { @@ -83,7 +83,7 @@ func (a *App) SendMessage() error { //types:add if err != nil { return err } - pw.Write([]byte(a.ComposeMessage.Body)) + pw.Write([]byte(a.composeMessage.Body)) pw.Close() var hh mail.InlineHeader @@ -92,20 +92,20 @@ func (a *App) SendMessage() error { //types:add if err != nil { return err } - err = goldmark.Convert([]byte(a.ComposeMessage.Body), hw) + err = goldmark.Convert([]byte(a.composeMessage.Body), hw) if err != nil { return err } hw.Close() - to := make([]string, len(a.ComposeMessage.To)) - for i, t := range a.ComposeMessage.To { + to := make([]string, len(a.composeMessage.To)) + for i, t := range a.composeMessage.To { to[i] = t.Address } err = smtp.SendMail( "smtp.gmail.com:587", - a.AuthClient[email], + a.authClient[email], email, to, &b, diff --git a/mail/values.go b/mail/values.go index b25c5332..97fe2288 100644 --- a/mail/values.go +++ b/mail/values.go @@ -40,11 +40,11 @@ func (mi *MessageListItem) Init() { s.Grow.Set(1, 0) }) mi.OnClick(func(e events.Event) { - theApp.ReadMessage = mi.Data + theApp.readMessage = mi.Data errors.Log(theApp.UpdateReadMessage()) }) mi.AddContextMenu(func(m *core.Scene) { - theApp.ReadMessage = mi.Data + theApp.readMessage = mi.Data core.NewFuncButton(m).SetFunc(theApp.MoveMessage).SetIcon(icons.Move).SetText("Move") }) From 6472a60f0a0ad3d7be291c06d42c1a31ea441e8b Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Sat, 7 Sep 2024 14:01:54 -0700 Subject: [PATCH 08/75] move mailbox tree logic to maker --- mail/app.go | 21 +++++++++++++++++++++ mail/cache.go | 18 ------------------ 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/mail/app.go b/mail/app.go index e5f08aaa..9c0c6ce6 100644 --- a/mail/app.go +++ b/mail/app.go @@ -11,7 +11,10 @@ import ( "cmp" "slices" + "golang.org/x/exp/maps" + "cogentcore.org/core/core" + "cogentcore.org/core/events" "cogentcore.org/core/icons" "cogentcore.org/core/styles" "cogentcore.org/core/tree" @@ -72,6 +75,24 @@ func (a *App) Init() { w.SetSplits(0.1, 0.2, 0.7) tree.AddChildAt(w, "mbox", func(w *core.Tree) { w.SetText("Mailboxes") + w.Maker(func(p *tree.Plan) { + for _, email := range Settings.Accounts { + tree.AddAt(p, email, func(w *core.Tree) { + w.Maker(func(p *tree.Plan) { + mailboxes := maps.Keys(a.cache[email]) + slices.Sort(mailboxes) + for _, mailbox := range mailboxes { + tree.AddAt(p, mailbox, func(w *core.Tree) { + w.OnSelect(func(e events.Event) { + a.currentMailbox = mailbox + a.Update() + }) + }) + } + }) + }) + } + }) }) tree.AddChildAt(w, "list", func(w *core.List) { w.SetReadOnly(true) diff --git a/mail/cache.go b/mail/cache.go index fb621e15..b2e14700 100644 --- a/mail/cache.go +++ b/mail/cache.go @@ -17,7 +17,6 @@ import ( "cogentcore.org/core/base/iox/jsonx" "cogentcore.org/core/core" - "cogentcore.org/core/events" "github.com/emersion/go-imap/v2" "github.com/emersion/go-imap/v2/imapclient" ) @@ -62,10 +61,6 @@ func (a *App) CacheMessages() error { if a.imapClient == nil { a.imapClient = map[string]*imapclient.Client{} } - mbox := a.FindPath("splits/mbox").(*core.Tree) - mbox.AsyncLock() - mbox.DeleteChildren() - mbox.AsyncUnlock() for _, account := range Settings.Accounts { err := a.CacheMessagesForAccount(account) if err != nil { @@ -120,19 +115,6 @@ func (a *App) CacheMessagesForMailbox(c *imapclient.Client, email string, mailbo bemail := FilenameBase32(email) - a.AsyncLock() - mbox := a.FindPath("splits/mbox").(*core.Tree) - embox := mbox.ChildByName(bemail) - if embox == nil { - embox = core.NewTree(mbox).SetText(email) // TODO(config) - embox.AsTree().SetName(bemail) - } - core.NewTree(embox).SetText(mailbox).OnClick(func(e events.Event) { - a.currentMailbox = mailbox - a.Update() - }) - a.AsyncUnlock() - dir := filepath.Join(core.TheApp.AppDataDir(), "mail", bemail) err := os.MkdirAll(string(dir), 0700) if err != nil { From 3bc0f27abbc90719a69a366f6ada550227d4a949 Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Sat, 7 Sep 2024 14:07:31 -0700 Subject: [PATCH 09/75] start moving updateReadMessage logic to Updater --- mail/app.go | 9 ++++++--- mail/cache.go | 3 +++ mail/read.go | 18 ++++++++---------- mail/values.go | 3 +-- 4 files changed, 18 insertions(+), 15 deletions(-) diff --git a/mail/app.go b/mail/app.go index 9c0c6ce6..f5e2cb24 100644 --- a/mail/app.go +++ b/mail/app.go @@ -110,17 +110,20 @@ func (a *App) Init() { }) tree.AddChildAt(w, "msv", func(w *core.Form) { w.SetReadOnly(true) + w.Updater(func() { + w.SetStruct(a.readMessage.ToMessage()) + }) }) tree.AddChildAt(w, "mb", func(w *core.Frame) { w.Styler(func(s *styles.Style) { s.Direction = styles.Column }) + w.Updater(func() { + core.ErrorSnackbar(w, a.updateReadMessage(w), "Error reading message") + }) }) }) }) - a.Updater(func() { - // a.UpdateReadMessage(ml, msv, mb) - }) } func (a *App) MakeToolbar(p *tree.Plan) { diff --git a/mail/cache.go b/mail/cache.go index b2e14700..0889a157 100644 --- a/mail/cache.go +++ b/mail/cache.go @@ -32,6 +32,9 @@ type CacheData struct { // ToMessage converts the [CacheData] to a [ReadMessage]. func (cd *CacheData) ToMessage() *ReadMessage { + if cd == nil { + return nil + } return &ReadMessage{ From: IMAPToMailAddresses(cd.From), To: IMAPToMailAddresses(cd.To), diff --git a/mail/read.go b/mail/read.go index 9d43a002..0445ddb9 100644 --- a/mail/read.go +++ b/mail/read.go @@ -25,13 +25,13 @@ type ReadMessage struct { Date time.Time } -// UpdateReadMessage updates the view of the message currently being read. -func (a *App) UpdateReadMessage() error { - msv := a.FindPath("splits/mail/msv").(*core.Form) - msv.SetStruct(a.readMessage.ToMessage()) +// updateReadMessage updates the given frame to display the contents of the current message. +func (a *App) updateReadMessage(w *core.Frame) error { + w.DeleteChildren() - mb := a.FindPath("splits/mail/mb").(*core.Frame) - mb.DeleteChildren() + if a.readMessage == nil { + return nil + } bemail := FilenameBase32(a.currentEmail) @@ -68,7 +68,7 @@ func (a *App) UpdateReadMessage() error { case "text/plain": plain = p case "text/html": - err := htmlcore.ReadHTML(htmlcore.NewContext(), mb, p.Body) + err := htmlcore.ReadHTML(htmlcore.NewContext(), w, p.Body) if err != nil { return err } @@ -79,12 +79,10 @@ func (a *App) UpdateReadMessage() error { // we only handle the plain version if there is no HTML version if !gotHTML && plain != nil { - err := htmlcore.ReadMD(htmlcore.NewContext(), mb, errors.Log1(io.ReadAll(plain.Body))) + err := htmlcore.ReadMD(htmlcore.NewContext(), w, errors.Log1(io.ReadAll(plain.Body))) if err != nil { return err } } - - mb.Update() return nil } diff --git a/mail/values.go b/mail/values.go index 97fe2288..16ca71d5 100644 --- a/mail/values.go +++ b/mail/values.go @@ -8,7 +8,6 @@ import ( "fmt" "net/mail" - "cogentcore.org/core/base/errors" "cogentcore.org/core/core" "cogentcore.org/core/cursors" "cogentcore.org/core/events" @@ -41,7 +40,7 @@ func (mi *MessageListItem) Init() { }) mi.OnClick(func(e events.Event) { theApp.readMessage = mi.Data - errors.Log(theApp.UpdateReadMessage()) + theApp.Update() }) mi.AddContextMenu(func(m *core.Scene) { theApp.readMessage = mi.Data From e6f62f08ef2515bede26413a1d50e79e65fcd761 Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Sat, 7 Sep 2024 14:11:06 -0700 Subject: [PATCH 10/75] only updateReadMessage when needed --- mail/read.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/mail/read.go b/mail/read.go index 0445ddb9..54c680fc 100644 --- a/mail/read.go +++ b/mail/read.go @@ -25,10 +25,14 @@ type ReadMessage struct { Date time.Time } -// updateReadMessage updates the given frame to display the contents of the current message. +// updateReadMessage updates the given frame to display the contents of +// the current message, if it does not already. func (a *App) updateReadMessage(w *core.Frame) error { + if a.readMessage == w.Property("readMessage") { + return nil + } + w.SetProperty("readMessage", a.readMessage) w.DeleteChildren() - if a.readMessage == nil { return nil } From ea44f54e4e7a9957b01155913284e31857a9515f Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Sat, 7 Sep 2024 14:14:31 -0700 Subject: [PATCH 11/75] remove old and unnecessary names for mail widgets --- mail/app.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/mail/app.go b/mail/app.go index f5e2cb24..a739e74e 100644 --- a/mail/app.go +++ b/mail/app.go @@ -71,9 +71,9 @@ func (a *App) Init() { s.Grow.Set(1, 1) }) - tree.AddChildAt(a, "splits", func(w *core.Splits) { + tree.AddChild(a, func(w *core.Splits) { w.SetSplits(0.1, 0.2, 0.7) - tree.AddChildAt(w, "mbox", func(w *core.Tree) { + tree.AddChild(w, func(w *core.Tree) { w.SetText("Mailboxes") w.Maker(func(p *tree.Plan) { for _, email := range Settings.Accounts { @@ -94,7 +94,7 @@ func (a *App) Init() { } }) }) - tree.AddChildAt(w, "list", func(w *core.List) { + tree.AddChild(w, func(w *core.List) { w.SetReadOnly(true) w.Updater(func() { a.currentCache = a.cache[a.currentEmail][a.currentMailbox] @@ -104,17 +104,17 @@ func (a *App) Init() { w.SetSlice(&a.currentCache) }) }) - tree.AddChildAt(w, "mail", func(w *core.Frame) { + tree.AddChild(w, func(w *core.Frame) { w.Styler(func(s *styles.Style) { s.Direction = styles.Column }) - tree.AddChildAt(w, "msv", func(w *core.Form) { + tree.AddChild(w, func(w *core.Form) { w.SetReadOnly(true) w.Updater(func() { w.SetStruct(a.readMessage.ToMessage()) }) }) - tree.AddChildAt(w, "mb", func(w *core.Frame) { + tree.AddChild(w, func(w *core.Frame) { w.Styler(func(s *styles.Style) { s.Direction = styles.Column }) From 7a24e62eebca0085e7ea58c9746de8a69fc1425c Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Sat, 7 Sep 2024 14:36:56 -0700 Subject: [PATCH 12/75] add mail list item context menu to list context menu for now --- mail/app.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mail/app.go b/mail/app.go index a739e74e..ac16795f 100644 --- a/mail/app.go +++ b/mail/app.go @@ -103,6 +103,10 @@ func (a *App) Init() { }) w.SetSlice(&a.currentCache) }) + // TODO: duplicate; remove once element-specific context menu works + w.AddContextMenu(func(m *core.Scene) { + core.NewFuncButton(m).SetFunc(a.MoveMessage).SetIcon(icons.Move).SetText("Move") + }) }) tree.AddChild(w, func(w *core.Frame) { w.Styler(func(s *styles.Style) { From c255b7f08f355236cf614de61cbfe22fb9314442 Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Sat, 7 Sep 2024 16:27:53 -0700 Subject: [PATCH 13/75] start on imapMu --- mail/actions.go | 7 ++++--- mail/app.go | 6 +++++- mail/cache.go | 7 +++++++ 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/mail/actions.go b/mail/actions.go index 9e623ce1..a94a14ee 100644 --- a/mail/actions.go +++ b/mail/actions.go @@ -12,13 +12,14 @@ import ( // MoveMessage moves the current message to the given mailbox. func (a *App) MoveMessage(mailbox string) error { //types:add + mu := a.imapMu[a.currentEmail] + mu.Lock() + defer mu.Unlock() c := a.imapClient[a.currentEmail] uidset := imap.UIDSet{} uidset.AddNum(a.readMessage.UID) fmt.Println(uidset) mc := c.Move(uidset, mailbox) - fmt.Println("mc", mc) - md, err := mc.Wait() - fmt.Println("md", md, err) + _, err := mc.Wait() return err } diff --git a/mail/app.go b/mail/app.go index ac16795f..ebe02864 100644 --- a/mail/app.go +++ b/mail/app.go @@ -10,6 +10,7 @@ package mail import ( "cmp" "slices" + "sync" "golang.org/x/exp/maps" @@ -33,9 +34,12 @@ type App struct { // authClient contains the [sasl.Client] authentication for sending messages for each account. authClient map[string]sasl.Client - // IMAPCLient contains the imap clients for each account. + // imapClient contains the imap clients for each account. imapClient map[string]*imapclient.Client + // imapMu contains the imap client mutexes for each account. + imapMu map[string]*sync.Mutex + // composeMessage is the current message we are editing composeMessage *SendMessage diff --git a/mail/cache.go b/mail/cache.go index 0889a157..f6b406fb 100644 --- a/mail/cache.go +++ b/mail/cache.go @@ -14,6 +14,7 @@ import ( "path/filepath" "strconv" "strings" + "sync" "cogentcore.org/core/base/iox/jsonx" "cogentcore.org/core/core" @@ -64,6 +65,9 @@ func (a *App) CacheMessages() error { if a.imapClient == nil { a.imapClient = map[string]*imapclient.Client{} } + if a.imapMu == nil { + a.imapMu = map[string]*sync.Mutex{} + } for _, account := range Settings.Accounts { err := a.CacheMessagesForAccount(account) if err != nil { @@ -88,6 +92,7 @@ func (a *App) CacheMessagesForAccount(email string) error { defer c.Logout() a.imapClient[email] = c + a.imapMu[email] = &sync.Mutex{} err = c.Authenticate(a.authClient[email]) if err != nil { @@ -193,7 +198,9 @@ func (a *App) CacheUIDs(uids []imap.UID, c *imapclient.Client, email string, mai }, } + a.imapMu[email].Lock() mcmd := c.Fetch(fuidset, fetchOptions) + a.imapMu[email].Unlock() for { msg := mcmd.Next() From d4a172d6055a1ba5d27b15716bce628afb7865f3 Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Sat, 7 Sep 2024 16:28:45 -0700 Subject: [PATCH 14/75] add more imapMu coverage --- mail/cache.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/mail/cache.go b/mail/cache.go index f6b406fb..09a8c7d9 100644 --- a/mail/cache.go +++ b/mail/cache.go @@ -200,7 +200,6 @@ func (a *App) CacheUIDs(uids []imap.UID, c *imapclient.Client, email string, mai a.imapMu[email].Lock() mcmd := c.Fetch(fuidset, fetchOptions) - a.imapMu[email].Unlock() for { msg := mcmd.Next() @@ -210,6 +209,7 @@ func (a *App) CacheUIDs(uids []imap.UID, c *imapclient.Client, email string, mai mdata, err := msg.Collect() if err != nil { + a.imapMu[email].Unlock() return err } @@ -217,6 +217,7 @@ func (a *App) CacheUIDs(uids []imap.UID, c *imapclient.Client, email string, mai filename = strings.Repeat("0", 7-len(filename)) + filename f, err := os.Create(filepath.Join(dir, filename)) if err != nil { + a.imapMu[email].Unlock() return err } @@ -232,11 +233,13 @@ func (a *App) CacheUIDs(uids []imap.UID, c *imapclient.Client, email string, mai _, err = f.Write(append(header, text...)) if err != nil { + a.imapMu[email].Unlock() return fmt.Errorf("writing message: %w", err) } err = f.Close() if err != nil { + a.imapMu[email].Unlock() return fmt.Errorf("closing message: %w", err) } @@ -251,6 +254,7 @@ func (a *App) CacheUIDs(uids []imap.UID, c *imapclient.Client, email string, mai cached = append(cached, cd) err = jsonx.Save(&cached, cachedFile) if err != nil { + a.imapMu[email].Unlock() return fmt.Errorf("saving cache list: %w", err) } @@ -261,6 +265,7 @@ func (a *App) CacheUIDs(uids []imap.UID, c *imapclient.Client, email string, mai } err := mcmd.Close() + a.imapMu[email].Unlock() if err != nil { return fmt.Errorf("fetching messages: %w", err) } From 9d59067b27aef96b9e18359229cae59de55d2881 Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Sat, 7 Sep 2024 16:38:38 -0700 Subject: [PATCH 15/75] use a goroutine in mail MoveMessage to prevent deadlock; with that and the new imapMu, mail actions such as Move work without hanging while caching is happening! --- mail/actions.go | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/mail/actions.go b/mail/actions.go index a94a14ee..d29e1504 100644 --- a/mail/actions.go +++ b/mail/actions.go @@ -5,21 +5,23 @@ package mail import ( - "fmt" - + "cogentcore.org/core/core" "github.com/emersion/go-imap/v2" ) // MoveMessage moves the current message to the given mailbox. -func (a *App) MoveMessage(mailbox string) error { //types:add - mu := a.imapMu[a.currentEmail] - mu.Lock() - defer mu.Unlock() - c := a.imapClient[a.currentEmail] - uidset := imap.UIDSet{} - uidset.AddNum(a.readMessage.UID) - fmt.Println(uidset) - mc := c.Move(uidset, mailbox) - _, err := mc.Wait() - return err +func (a *App) MoveMessage(mailbox string) { //types:add + // Use a goroutine to prevent GUI freezing and a double mutex deadlock + // with a combination of the renderContext mutex and the imapMu. + go func() { + mu := a.imapMu[a.currentEmail] + mu.Lock() + defer mu.Unlock() + c := a.imapClient[a.currentEmail] + uidset := imap.UIDSet{} + uidset.AddNum(a.readMessage.UID) + mc := c.Move(uidset, mailbox) + _, err := mc.Wait() + core.ErrorSnackbar(a, err, "Error moving message") + }() } From 74ad9f332efc5cc228da0be4c78f97cb46c8807e Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Sun, 8 Sep 2024 15:17:02 -0700 Subject: [PATCH 16/75] disable text wrap for mail.MessageListItem; prevents https://github.com/cogentcore/core/issues/1209 from happening --- mail/values.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mail/values.go b/mail/values.go index 16ca71d5..0c46edef 100644 --- a/mail/values.go +++ b/mail/values.go @@ -51,6 +51,7 @@ func (mi *MessageListItem) Init() { w.SetType(core.TextTitleMedium) w.Styler(func(s *styles.Style) { s.SetNonSelectable() + s.SetTextWrap(false) }) w.Updater(func() { text := "" @@ -68,6 +69,7 @@ func (mi *MessageListItem) Init() { w.SetType(core.TextBodyMedium) w.Styler(func(s *styles.Style) { s.SetNonSelectable() + s.SetTextWrap(false) }) w.Updater(func() { w.SetText(mi.Data.Subject) From 39016f6f6cccce8a5efe9ee3573a6c23280d248b Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Sun, 8 Sep 2024 15:23:20 -0700 Subject: [PATCH 17/75] move MoveMessage to mail toolbar --- mail/app.go | 11 +++++++---- mail/values.go | 5 ----- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/mail/app.go b/mail/app.go index ebe02864..29d42708 100644 --- a/mail/app.go +++ b/mail/app.go @@ -107,10 +107,6 @@ func (a *App) Init() { }) w.SetSlice(&a.currentCache) }) - // TODO: duplicate; remove once element-specific context menu works - w.AddContextMenu(func(m *core.Scene) { - core.NewFuncButton(m).SetFunc(a.MoveMessage).SetIcon(icons.Move).SetText("Move") - }) }) tree.AddChild(w, func(w *core.Frame) { w.Styler(func(s *styles.Style) { @@ -138,6 +134,13 @@ func (a *App) MakeToolbar(p *tree.Plan) { tree.Add(p, func(w *core.FuncButton) { w.SetFunc(a.Compose).SetIcon(icons.Send) }) + + if a.readMessage != nil { + tree.Add(p, func(w *core.Separator) {}) + tree.Add(p, func(w *core.FuncButton) { + w.SetFunc(a.MoveMessage).SetText("Move").SetIcon(icons.Move) + }) + } } func (a *App) GetMail() error { diff --git a/mail/values.go b/mail/values.go index 0c46edef..4a8f8c2c 100644 --- a/mail/values.go +++ b/mail/values.go @@ -11,7 +11,6 @@ import ( "cogentcore.org/core/core" "cogentcore.org/core/cursors" "cogentcore.org/core/events" - "cogentcore.org/core/icons" "cogentcore.org/core/styles" "cogentcore.org/core/styles/abilities" "cogentcore.org/core/tree" @@ -42,10 +41,6 @@ func (mi *MessageListItem) Init() { theApp.readMessage = mi.Data theApp.Update() }) - mi.AddContextMenu(func(m *core.Scene) { - theApp.readMessage = mi.Data - core.NewFuncButton(m).SetFunc(theApp.MoveMessage).SetIcon(icons.Move).SetText("Move") - }) tree.AddChild(mi, func(w *core.Text) { w.SetType(core.TextTitleMedium) From df13b39f5c1b6e8b7d8ef2180e3aae23b8f52f35 Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Sun, 8 Sep 2024 15:26:06 -0700 Subject: [PATCH 18/75] improve move icon and make message body grow --- mail/app.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mail/app.go b/mail/app.go index 29d42708..432af225 100644 --- a/mail/app.go +++ b/mail/app.go @@ -121,6 +121,7 @@ func (a *App) Init() { tree.AddChild(w, func(w *core.Frame) { w.Styler(func(s *styles.Style) { s.Direction = styles.Column + s.Grow.Set(1, 0) }) w.Updater(func() { core.ErrorSnackbar(w, a.updateReadMessage(w), "Error reading message") @@ -138,7 +139,7 @@ func (a *App) MakeToolbar(p *tree.Plan) { if a.readMessage != nil { tree.Add(p, func(w *core.Separator) {}) tree.Add(p, func(w *core.FuncButton) { - w.SetFunc(a.MoveMessage).SetText("Move").SetIcon(icons.Move) + w.SetFunc(a.MoveMessage).SetText("Move").SetIcon(icons.DriveFileMove) }) } } From ecc7c8078e5fb8875b1d07f32295447ac2bddbad Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Sun, 8 Sep 2024 16:54:22 -0700 Subject: [PATCH 19/75] support changing address in AddressTextField --- mail/values.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/mail/values.go b/mail/values.go index 4a8f8c2c..1b3116f2 100644 --- a/mail/values.go +++ b/mail/values.go @@ -7,6 +7,7 @@ package mail import ( "fmt" "net/mail" + "strings" "cogentcore.org/core/core" "cogentcore.org/core/cursors" @@ -89,4 +90,12 @@ func (at *AddressTextField) Init() { } at.SetText(at.Address.Address) }) + at.SetValidator(func() error { + text := at.Text() + if !strings.Contains(text, "@") && !strings.Contains(text, ".") { + return fmt.Errorf("invalid email address") + } + at.Address.Address = text + return nil + }) } From ba0eadce3b8dba42cc24ea612951e20045699a29 Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Sun, 8 Sep 2024 17:44:12 -0700 Subject: [PATCH 20/75] skip gmail mailboxes for now --- mail/cache.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mail/cache.go b/mail/cache.go index 09a8c7d9..ff250119 100644 --- a/mail/cache.go +++ b/mail/cache.go @@ -105,6 +105,9 @@ func (a *App) CacheMessagesForAccount(email string) error { } for _, mailbox := range mailboxes { + if strings.HasPrefix(mailbox.Mailbox, "[Gmail]") { + continue // TODO: skipping for now until we figure out a good way to handle + } err := a.CacheMessagesForMailbox(c, email, mailbox.Mailbox) if err != nil { return fmt.Errorf("caching messages for mailbox %q: %w", mailbox.Mailbox, err) From 101b2db5b384281ee38f3e14ea039e7abe76e793 Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Sun, 8 Sep 2024 22:00:17 -0700 Subject: [PATCH 21/75] remove unnecessary constant SetSlice calling --- mail/app.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mail/app.go b/mail/app.go index 432af225..88a2dfe7 100644 --- a/mail/app.go +++ b/mail/app.go @@ -99,13 +99,13 @@ func (a *App) Init() { }) }) tree.AddChild(w, func(w *core.List) { + w.SetSlice(&a.currentCache) w.SetReadOnly(true) w.Updater(func() { a.currentCache = a.cache[a.currentEmail][a.currentMailbox] slices.SortFunc(a.currentCache, func(a, b *CacheData) int { return cmp.Compare(b.Date.UnixNano(), a.Date.UnixNano()) }) - w.SetSlice(&a.currentCache) }) }) tree.AddChild(w, func(w *core.Frame) { From a8d3e8af48ad04836f18ec54982851983c636b10 Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Sun, 8 Sep 2024 22:04:27 -0700 Subject: [PATCH 22/75] start adding mail shortcuts --- mail/app.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mail/app.go b/mail/app.go index 88a2dfe7..0dad424f 100644 --- a/mail/app.go +++ b/mail/app.go @@ -17,6 +17,7 @@ import ( "cogentcore.org/core/core" "cogentcore.org/core/events" "cogentcore.org/core/icons" + "cogentcore.org/core/keymap" "cogentcore.org/core/styles" "cogentcore.org/core/tree" "github.com/emersion/go-imap/v2/imapclient" @@ -133,13 +134,13 @@ func (a *App) Init() { func (a *App) MakeToolbar(p *tree.Plan) { tree.Add(p, func(w *core.FuncButton) { - w.SetFunc(a.Compose).SetIcon(icons.Send) + w.SetFunc(a.Compose).SetIcon(icons.Send).SetKey(keymap.New) }) if a.readMessage != nil { tree.Add(p, func(w *core.Separator) {}) tree.Add(p, func(w *core.FuncButton) { - w.SetFunc(a.MoveMessage).SetText("Move").SetIcon(icons.DriveFileMove) + w.SetFunc(a.MoveMessage).SetText("Move").SetIcon(icons.DriveFileMove).SetKey(keymap.Save) }) } } From 68d700ee85432c88e29d9cf9b5826b029da5e9d3 Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Mon, 9 Sep 2024 12:43:48 -0700 Subject: [PATCH 23/75] add mail.App.compose --- mail/send.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/mail/send.go b/mail/send.go index 9708d8dc..1172fe70 100644 --- a/mail/send.go +++ b/mail/send.go @@ -33,13 +33,20 @@ func (a *App) Compose() { //types:add a.composeMessage = &SendMessage{} a.composeMessage.From = []*mail.Address{{Address: Settings.Accounts[0]}} a.composeMessage.To = []*mail.Address{{}} - b := core.NewBody("Send message") + a.compose("Compose") +} + +// compose is the implementation of the email comoposition dialog, +// which is called by other higher-level functions. +func (a *App) compose(title string) { + b := core.NewBody(title) core.NewForm(b).SetStruct(a.composeMessage) ed := texteditor.NewEditor(b) ed.Buffer.SetLanguage(fileinfo.Markdown) ed.Buffer.Options.LineNumbers = false ed.Styler(func(s *styles.Style) { s.SetMono(false) + s.Grow.Set(1, 1) }) b.AddBottomBar(func(bar *core.Frame) { b.AddCancel(bar) @@ -48,7 +55,7 @@ func (a *App) Compose() { //types:add a.SendMessage() }) }) - b.RunFullDialog(a) + b.RunWindowDialog(a) } // SendMessage sends the current message From 9db9cb4e64171d963bc108da9f6834f49babb6fd Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Mon, 9 Sep 2024 12:47:49 -0700 Subject: [PATCH 24/75] start on mail reply --- mail/actions.go | 13 +++++++++++-- mail/app.go | 5 ++++- mail/send.go | 2 +- mail/typegen.go | 5 ++--- 4 files changed, 18 insertions(+), 7 deletions(-) diff --git a/mail/actions.go b/mail/actions.go index d29e1504..97cb0417 100644 --- a/mail/actions.go +++ b/mail/actions.go @@ -7,10 +7,11 @@ package mail import ( "cogentcore.org/core/core" "github.com/emersion/go-imap/v2" + "github.com/emersion/go-message/mail" ) -// MoveMessage moves the current message to the given mailbox. -func (a *App) MoveMessage(mailbox string) { //types:add +// Move moves the current message to the given mailbox. +func (a *App) Move(mailbox string) { //types:add // Use a goroutine to prevent GUI freezing and a double mutex deadlock // with a combination of the renderContext mutex and the imapMu. go func() { @@ -25,3 +26,11 @@ func (a *App) MoveMessage(mailbox string) { //types:add core.ErrorSnackbar(a, err, "Error moving message") }() } + +// Reply opens a dialog to reply to the current message. +func (a *App) Reply() { //types:add + a.composeMessage = &SendMessage{} + a.composeMessage.From = []*mail.Address{{Address: Settings.Accounts[0]}} + a.composeMessage.To = []*mail.Address{{}} + a.compose("Reply") +} diff --git a/mail/app.go b/mail/app.go index 0dad424f..90ec8a00 100644 --- a/mail/app.go +++ b/mail/app.go @@ -140,7 +140,10 @@ func (a *App) MakeToolbar(p *tree.Plan) { if a.readMessage != nil { tree.Add(p, func(w *core.Separator) {}) tree.Add(p, func(w *core.FuncButton) { - w.SetFunc(a.MoveMessage).SetText("Move").SetIcon(icons.DriveFileMove).SetKey(keymap.Save) + w.SetFunc(a.Move).SetIcon(icons.DriveFileMove).SetKey(keymap.Save) + }) + tree.Add(p, func(w *core.FuncButton) { + w.SetFunc(a.Reply).SetIcon(icons.Reply).SetKey(keymap.Replace) }) } } diff --git a/mail/send.go b/mail/send.go index 1172fe70..e4bb5370 100644 --- a/mail/send.go +++ b/mail/send.go @@ -28,7 +28,7 @@ type SendMessage struct { Body string `display:"-"` } -// Compose pulls up a dialog to send a new message +// Compose opens a dialog to send a new message. func (a *App) Compose() { //types:add a.composeMessage = &SendMessage{} a.composeMessage.From = []*mail.Address{{Address: Settings.Accounts[0]}} diff --git a/mail/typegen.go b/mail/typegen.go index ee875756..d9335988 100644 --- a/mail/typegen.go +++ b/mail/typegen.go @@ -3,13 +3,12 @@ package mail import ( - "net/mail" - "cogentcore.org/core/tree" "cogentcore.org/core/types" + "github.com/emersion/go-message/mail" ) -var _ = types.AddType(&types.Type{Name: "cogentcore.org/cogent/mail.App", IDName: "app", Doc: "App is an email client app.", Methods: []types.Method{{Name: "MoveMessage", Doc: "MoveMessage moves the current message to the given mailbox.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Args: []string{"mailbox"}, Returns: []string{"error"}}, {Name: "Compose", Doc: "Compose pulls up a dialog to send a new message", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "SendMessage", Doc: "SendMessage sends the current message", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Returns: []string{"error"}}}, Embeds: []types.Field{{Name: "Frame"}}, Fields: []types.Field{{Name: "AuthToken", Doc: "AuthToken contains the [oauth2.Token] for each account."}, {Name: "AuthClient", Doc: "AuthClient contains the [sasl.Client] authentication for sending messages for each account."}, {Name: "IMAPClient", Doc: "IMAPCLient contains the imap clients for each account."}, {Name: "ComposeMessage", Doc: "ComposeMessage is the current message we are editing"}, {Name: "Cache", Doc: "Cache contains the cache data, keyed by account and then mailbox."}, {Name: "ReadMessage", Doc: "ReadMessage is the current message we are reading"}, {Name: "CurrentEmail", Doc: "The current email account"}, {Name: "CurrentMailbox", Doc: "The current mailbox"}}}) +var _ = types.AddType(&types.Type{Name: "cogentcore.org/cogent/mail.App", IDName: "app", Doc: "App is an email client app.", Methods: []types.Method{{Name: "Move", Doc: "Move moves the current message to the given mailbox.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Args: []string{"mailbox"}}, {Name: "Reply", Doc: "Reply opens a dialog to reply to the current message.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "Compose", Doc: "Compose opens a dialog to send a new message.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "SendMessage", Doc: "SendMessage sends the current message", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Returns: []string{"error"}}}, Embeds: []types.Field{{Name: "Frame"}}, Fields: []types.Field{{Name: "authToken", Doc: "authToken contains the [oauth2.Token] for each account."}, {Name: "authClient", Doc: "authClient contains the [sasl.Client] authentication for sending messages for each account."}, {Name: "imapClient", Doc: "imapClient contains the imap clients for each account."}, {Name: "imapMu", Doc: "imapMu contains the imap client mutexes for each account."}, {Name: "composeMessage", Doc: "composeMessage is the current message we are editing"}, {Name: "cache", Doc: "cache contains the cache data, keyed by account and then mailbox."}, {Name: "currentCache", Doc: "currentCache is [App.cache] for the current email account and mailbox."}, {Name: "readMessage", Doc: "readMessage is the current message we are reading"}, {Name: "currentEmail", Doc: "The current email account"}, {Name: "currentMailbox", Doc: "The current mailbox"}}}) // NewApp returns a new [App] with the given optional parent: // App is an email client app. From b1b18d44526eb1f5f1d3d81dac1c5642219b2693 Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Mon, 9 Sep 2024 16:46:30 -0700 Subject: [PATCH 25/75] set default To for reply in mail to From of original message --- mail/actions.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mail/actions.go b/mail/actions.go index 97cb0417..a24135d7 100644 --- a/mail/actions.go +++ b/mail/actions.go @@ -31,6 +31,6 @@ func (a *App) Move(mailbox string) { //types:add func (a *App) Reply() { //types:add a.composeMessage = &SendMessage{} a.composeMessage.From = []*mail.Address{{Address: Settings.Accounts[0]}} - a.composeMessage.To = []*mail.Address{{}} + a.composeMessage.To = IMAPToMailAddresses(a.readMessage.From) a.compose("Reply") } From eb2ee8a722befcf783ea4cb65571440e54399786 Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Mon, 9 Sep 2024 16:56:23 -0700 Subject: [PATCH 26/75] add support for In-Reply-To header --- mail/actions.go | 1 + mail/send.go | 11 +++++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/mail/actions.go b/mail/actions.go index a24135d7..ab3c9e8c 100644 --- a/mail/actions.go +++ b/mail/actions.go @@ -32,5 +32,6 @@ func (a *App) Reply() { //types:add a.composeMessage = &SendMessage{} a.composeMessage.From = []*mail.Address{{Address: Settings.Accounts[0]}} a.composeMessage.To = IMAPToMailAddresses(a.readMessage.From) + a.composeMessage.inReplyTo = a.readMessage.MessageID a.compose("Reply") } diff --git a/mail/send.go b/mail/send.go index e4bb5370..bc36e36e 100644 --- a/mail/send.go +++ b/mail/send.go @@ -25,7 +25,9 @@ type SendMessage struct { From []*mail.Address `display:"inline"` To []*mail.Address `display:"inline"` Subject string - Body string `display:"-"` + + body string + inReplyTo string } // Compose opens a dialog to send a new message. @@ -51,7 +53,7 @@ func (a *App) compose(title string) { b.AddBottomBar(func(bar *core.Frame) { b.AddCancel(bar) b.AddOK(bar).SetText("Send").OnClick(func(e events.Event) { - a.composeMessage.Body = ed.Buffer.String() + a.composeMessage.body = ed.Buffer.String() a.SendMessage() }) }) @@ -72,6 +74,7 @@ func (a *App) SendMessage() error { //types:add h.SetAddressList("From", a.composeMessage.From) h.SetAddressList("To", a.composeMessage.To) h.SetSubject(a.composeMessage.Subject) + h.SetText("In-Reply-To", "<"+a.composeMessage.inReplyTo+">") mw, err := mail.CreateWriter(&b, h) if err != nil { @@ -90,7 +93,7 @@ func (a *App) SendMessage() error { //types:add if err != nil { return err } - pw.Write([]byte(a.composeMessage.Body)) + pw.Write([]byte(a.composeMessage.body)) pw.Close() var hh mail.InlineHeader @@ -99,7 +102,7 @@ func (a *App) SendMessage() error { //types:add if err != nil { return err } - err = goldmark.Convert([]byte(a.composeMessage.Body), hw) + err = goldmark.Convert([]byte(a.composeMessage.body), hw) if err != nil { return err } From 6297365b132779f3eac27eed160b02cec27a0feb Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Mon, 9 Sep 2024 16:59:29 -0700 Subject: [PATCH 27/75] add support for message references --- mail/actions.go | 1 + mail/send.go | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/mail/actions.go b/mail/actions.go index ab3c9e8c..2ac37106 100644 --- a/mail/actions.go +++ b/mail/actions.go @@ -33,5 +33,6 @@ func (a *App) Reply() { //types:add a.composeMessage.From = []*mail.Address{{Address: Settings.Accounts[0]}} a.composeMessage.To = IMAPToMailAddresses(a.readMessage.From) a.composeMessage.inReplyTo = a.readMessage.MessageID + a.composeMessage.references = []string{a.readMessage.MessageID} // TODO: append to any existing references in the readMessage a.compose("Reply") } diff --git a/mail/send.go b/mail/send.go index bc36e36e..0dbfc404 100644 --- a/mail/send.go +++ b/mail/send.go @@ -26,8 +26,9 @@ type SendMessage struct { To []*mail.Address `display:"inline"` Subject string - body string - inReplyTo string + body string + inReplyTo string + references []string } // Compose opens a dialog to send a new message. @@ -74,7 +75,8 @@ func (a *App) SendMessage() error { //types:add h.SetAddressList("From", a.composeMessage.From) h.SetAddressList("To", a.composeMessage.To) h.SetSubject(a.composeMessage.Subject) - h.SetText("In-Reply-To", "<"+a.composeMessage.inReplyTo+">") + h.SetMsgIDList("In-Reply-To", []string{a.composeMessage.inReplyTo}) + h.SetMsgIDList("References", a.composeMessage.references) mw, err := mail.CreateWriter(&b, h) if err != nil { From 2ac2f5fbf6804cfed1087662ebd447ba180fae3f Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Mon, 9 Sep 2024 17:04:32 -0700 Subject: [PATCH 28/75] add Re: to reply subject in mail --- mail/actions.go | 6 ++++++ mail/send.go | 8 ++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/mail/actions.go b/mail/actions.go index 2ac37106..92a019fe 100644 --- a/mail/actions.go +++ b/mail/actions.go @@ -5,6 +5,8 @@ package mail import ( + "strings" + "cogentcore.org/core/core" "github.com/emersion/go-imap/v2" "github.com/emersion/go-message/mail" @@ -32,6 +34,10 @@ func (a *App) Reply() { //types:add a.composeMessage = &SendMessage{} a.composeMessage.From = []*mail.Address{{Address: Settings.Accounts[0]}} a.composeMessage.To = IMAPToMailAddresses(a.readMessage.From) + a.composeMessage.Subject = a.readMessage.Subject + if !strings.HasPrefix(a.composeMessage.Subject, "Re: ") { + a.composeMessage.Subject = "Re: " + a.composeMessage.Subject + } a.composeMessage.inReplyTo = a.readMessage.MessageID a.composeMessage.references = []string{a.readMessage.MessageID} // TODO: append to any existing references in the readMessage a.compose("Reply") diff --git a/mail/send.go b/mail/send.go index 0dbfc404..2b4585ec 100644 --- a/mail/send.go +++ b/mail/send.go @@ -25,8 +25,8 @@ type SendMessage struct { From []*mail.Address `display:"inline"` To []*mail.Address `display:"inline"` Subject string + body string - body string inReplyTo string references []string } @@ -55,14 +55,14 @@ func (a *App) compose(title string) { b.AddCancel(bar) b.AddOK(bar).SetText("Send").OnClick(func(e events.Event) { a.composeMessage.body = ed.Buffer.String() - a.SendMessage() + a.Send() }) }) b.RunWindowDialog(a) } -// SendMessage sends the current message -func (a *App) SendMessage() error { //types:add +// Send sends the current message +func (a *App) Send() error { //types:add if len(a.composeMessage.From) != 1 { return fmt.Errorf("expected 1 sender, but got %d", len(a.composeMessage.From)) } From 0fcee7b3cf555b7c7f39edc0e3662426d6433209 Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Tue, 10 Sep 2024 09:36:32 -0700 Subject: [PATCH 29/75] add mail.App.reply --- mail/actions.go | 10 +++++++--- mail/send.go | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/mail/actions.go b/mail/actions.go index 92a019fe..d0ab157a 100644 --- a/mail/actions.go +++ b/mail/actions.go @@ -9,7 +9,6 @@ import ( "cogentcore.org/core/core" "github.com/emersion/go-imap/v2" - "github.com/emersion/go-message/mail" ) // Move moves the current message to the given mailbox. @@ -32,13 +31,18 @@ func (a *App) Move(mailbox string) { //types:add // Reply opens a dialog to reply to the current message. func (a *App) Reply() { //types:add a.composeMessage = &SendMessage{} - a.composeMessage.From = []*mail.Address{{Address: Settings.Accounts[0]}} a.composeMessage.To = IMAPToMailAddresses(a.readMessage.From) + a.reply("Reply") +} + +// reply is the implementation of the email reply dialog, +// used by other higher-level functions. +func (a *App) reply(title string) { a.composeMessage.Subject = a.readMessage.Subject if !strings.HasPrefix(a.composeMessage.Subject, "Re: ") { a.composeMessage.Subject = "Re: " + a.composeMessage.Subject } a.composeMessage.inReplyTo = a.readMessage.MessageID a.composeMessage.references = []string{a.readMessage.MessageID} // TODO: append to any existing references in the readMessage - a.compose("Reply") + a.compose(title) } diff --git a/mail/send.go b/mail/send.go index 2b4585ec..cdc0623d 100644 --- a/mail/send.go +++ b/mail/send.go @@ -34,7 +34,6 @@ type SendMessage struct { // Compose opens a dialog to send a new message. func (a *App) Compose() { //types:add a.composeMessage = &SendMessage{} - a.composeMessage.From = []*mail.Address{{Address: Settings.Accounts[0]}} a.composeMessage.To = []*mail.Address{{}} a.compose("Compose") } @@ -42,6 +41,7 @@ func (a *App) Compose() { //types:add // compose is the implementation of the email comoposition dialog, // which is called by other higher-level functions. func (a *App) compose(title string) { + a.composeMessage.From = []*mail.Address{{Address: Settings.Accounts[0]}} b := core.NewBody(title) core.NewForm(b).SetStruct(a.composeMessage) ed := texteditor.NewEditor(b) From 7f37ce8771800b5c4e5502453095fede71e72b5b Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Tue, 10 Sep 2024 16:23:49 -0700 Subject: [PATCH 30/75] add mail ReplyAll --- mail/actions.go | 7 +++++++ mail/app.go | 3 +++ mail/typegen.go | 5 +++-- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/mail/actions.go b/mail/actions.go index d0ab157a..e8b448d3 100644 --- a/mail/actions.go +++ b/mail/actions.go @@ -35,6 +35,13 @@ func (a *App) Reply() { //types:add a.reply("Reply") } +// ReplyAll opens a dialog to reply to all people involved in the current message. +func (a *App) ReplyAll() { //types:add + a.composeMessage = &SendMessage{} + a.composeMessage.To = append(IMAPToMailAddresses(a.readMessage.From), IMAPToMailAddresses(a.readMessage.To)...) + a.reply("Reply all") +} + // reply is the implementation of the email reply dialog, // used by other higher-level functions. func (a *App) reply(title string) { diff --git a/mail/app.go b/mail/app.go index 90ec8a00..6a38bb48 100644 --- a/mail/app.go +++ b/mail/app.go @@ -145,6 +145,9 @@ func (a *App) MakeToolbar(p *tree.Plan) { tree.Add(p, func(w *core.FuncButton) { w.SetFunc(a.Reply).SetIcon(icons.Reply).SetKey(keymap.Replace) }) + tree.Add(p, func(w *core.FuncButton) { + w.SetFunc(a.ReplyAll).SetIcon(icons.ReplyAll) + }) } } diff --git a/mail/typegen.go b/mail/typegen.go index d9335988..a011a260 100644 --- a/mail/typegen.go +++ b/mail/typegen.go @@ -3,12 +3,13 @@ package mail import ( + "net/mail" + "cogentcore.org/core/tree" "cogentcore.org/core/types" - "github.com/emersion/go-message/mail" ) -var _ = types.AddType(&types.Type{Name: "cogentcore.org/cogent/mail.App", IDName: "app", Doc: "App is an email client app.", Methods: []types.Method{{Name: "Move", Doc: "Move moves the current message to the given mailbox.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Args: []string{"mailbox"}}, {Name: "Reply", Doc: "Reply opens a dialog to reply to the current message.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "Compose", Doc: "Compose opens a dialog to send a new message.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "SendMessage", Doc: "SendMessage sends the current message", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Returns: []string{"error"}}}, Embeds: []types.Field{{Name: "Frame"}}, Fields: []types.Field{{Name: "authToken", Doc: "authToken contains the [oauth2.Token] for each account."}, {Name: "authClient", Doc: "authClient contains the [sasl.Client] authentication for sending messages for each account."}, {Name: "imapClient", Doc: "imapClient contains the imap clients for each account."}, {Name: "imapMu", Doc: "imapMu contains the imap client mutexes for each account."}, {Name: "composeMessage", Doc: "composeMessage is the current message we are editing"}, {Name: "cache", Doc: "cache contains the cache data, keyed by account and then mailbox."}, {Name: "currentCache", Doc: "currentCache is [App.cache] for the current email account and mailbox."}, {Name: "readMessage", Doc: "readMessage is the current message we are reading"}, {Name: "currentEmail", Doc: "The current email account"}, {Name: "currentMailbox", Doc: "The current mailbox"}}}) +var _ = types.AddType(&types.Type{Name: "cogentcore.org/cogent/mail.App", IDName: "app", Doc: "App is an email client app.", Methods: []types.Method{{Name: "Move", Doc: "Move moves the current message to the given mailbox.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Args: []string{"mailbox"}}, {Name: "Reply", Doc: "Reply opens a dialog to reply to the current message.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "ReplyAll", Doc: "ReplyAll opens a dialog to reply to all people involved in the current message.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "Compose", Doc: "Compose opens a dialog to send a new message.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "Send", Doc: "Send sends the current message", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Returns: []string{"error"}}}, Embeds: []types.Field{{Name: "Frame"}}, Fields: []types.Field{{Name: "authToken", Doc: "authToken contains the [oauth2.Token] for each account."}, {Name: "authClient", Doc: "authClient contains the [sasl.Client] authentication for sending messages for each account."}, {Name: "imapClient", Doc: "imapClient contains the imap clients for each account."}, {Name: "imapMu", Doc: "imapMu contains the imap client mutexes for each account."}, {Name: "composeMessage", Doc: "composeMessage is the current message we are editing"}, {Name: "cache", Doc: "cache contains the cache data, keyed by account and then mailbox."}, {Name: "currentCache", Doc: "currentCache is [App.cache] for the current email account and mailbox."}, {Name: "readMessage", Doc: "readMessage is the current message we are reading"}, {Name: "currentEmail", Doc: "The current email account"}, {Name: "currentMailbox", Doc: "The current mailbox"}}}) // NewApp returns a new [App] with the given optional parent: // App is an email client app. From 05a71b63f1504fafeb84ccd7c761daa2153a8152 Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Tue, 10 Sep 2024 16:30:21 -0700 Subject: [PATCH 31/75] add readMessageReferences --- mail/actions.go | 2 +- mail/app.go | 3 +++ mail/read.go | 6 ++++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/mail/actions.go b/mail/actions.go index e8b448d3..6fee3cb5 100644 --- a/mail/actions.go +++ b/mail/actions.go @@ -50,6 +50,6 @@ func (a *App) reply(title string) { a.composeMessage.Subject = "Re: " + a.composeMessage.Subject } a.composeMessage.inReplyTo = a.readMessage.MessageID - a.composeMessage.references = []string{a.readMessage.MessageID} // TODO: append to any existing references in the readMessage + a.composeMessage.references = append(a.readMessageReferences, a.readMessage.MessageID) a.compose(title) } diff --git a/mail/app.go b/mail/app.go index 6a38bb48..84c41a99 100644 --- a/mail/app.go +++ b/mail/app.go @@ -53,6 +53,9 @@ type App struct { // readMessage is the current message we are reading readMessage *CacheData + // readMessageReferences is the References header of the current readMessage. + readMessageReferences []string + // The current email account currentEmail string diff --git a/mail/read.go b/mail/read.go index 54c680fc..0d67f424 100644 --- a/mail/read.go +++ b/mail/read.go @@ -50,6 +50,12 @@ func (a *App) updateReadMessage(w *core.Frame) error { return err } + refs, err := mr.Header.MsgIDList("References") + if err != nil { + return err + } + a.readMessageReferences = refs + var plain *mail.Part var gotHTML bool From f97eec7ff75fe9801f2c2eb7253885ed3bac0333 Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Wed, 11 Sep 2024 11:58:30 -0700 Subject: [PATCH 32/75] start on original message quoting in mail reply --- mail/actions.go | 1 + mail/send.go | 1 + 2 files changed, 2 insertions(+) diff --git a/mail/actions.go b/mail/actions.go index 6fee3cb5..b871b5a3 100644 --- a/mail/actions.go +++ b/mail/actions.go @@ -51,5 +51,6 @@ func (a *App) reply(title string) { } a.composeMessage.inReplyTo = a.readMessage.MessageID a.composeMessage.references = append(a.readMessageReferences, a.readMessage.MessageID) + a.composeMessage.body = "\n\nOn " + a.readMessage.Date.Format("Mon, Jan 2, 2006 at 3:04 PM") + ", " + a.composeMessage.To[0].String() + " wrote:\n" a.compose(title) } diff --git a/mail/send.go b/mail/send.go index cdc0623d..845f3f36 100644 --- a/mail/send.go +++ b/mail/send.go @@ -45,6 +45,7 @@ func (a *App) compose(title string) { b := core.NewBody(title) core.NewForm(b).SetStruct(a.composeMessage) ed := texteditor.NewEditor(b) + core.Bind(&a.composeMessage.body, ed) ed.Buffer.SetLanguage(fileinfo.Markdown) ed.Buffer.Options.LineNumbers = false ed.Styler(func(s *styles.Style) { From 016eba605cdcdfae20d0abee1fc9653ad6d3c3e6 Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Wed, 11 Sep 2024 12:21:05 -0700 Subject: [PATCH 33/75] add readMessagePlain to get closer to full reply quoting --- mail/actions.go | 2 +- mail/app.go | 3 +++ mail/read.go | 9 +++++++-- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/mail/actions.go b/mail/actions.go index b871b5a3..1adead38 100644 --- a/mail/actions.go +++ b/mail/actions.go @@ -51,6 +51,6 @@ func (a *App) reply(title string) { } a.composeMessage.inReplyTo = a.readMessage.MessageID a.composeMessage.references = append(a.readMessageReferences, a.readMessage.MessageID) - a.composeMessage.body = "\n\nOn " + a.readMessage.Date.Format("Mon, Jan 2, 2006 at 3:04 PM") + ", " + a.composeMessage.To[0].String() + " wrote:\n" + a.composeMessage.body = "\n\nOn " + a.readMessage.Date.Format("Mon, Jan 2, 2006 at 3:04 PM") + ", " + a.composeMessage.To[0].String() + " wrote:\n\n" + a.readMessagePlain a.compose(title) } diff --git a/mail/app.go b/mail/app.go index 84c41a99..1ea29180 100644 --- a/mail/app.go +++ b/mail/app.go @@ -56,6 +56,9 @@ type App struct { // readMessageReferences is the References header of the current readMessage. readMessageReferences []string + // readMessagePlain is the plain text body of the current readMessage. + readMessagePlain string + // The current email account currentEmail string diff --git a/mail/read.go b/mail/read.go index 0d67f424..b3267a2a 100644 --- a/mail/read.go +++ b/mail/read.go @@ -10,7 +10,6 @@ import ( "path/filepath" "time" - "cogentcore.org/core/base/errors" "cogentcore.org/core/core" "cogentcore.org/core/htmlcore" "github.com/emersion/go-message/mail" @@ -87,9 +86,15 @@ func (a *App) updateReadMessage(w *core.Frame) error { } } + b, err := io.ReadAll(plain.Body) + if err != nil { + return err + } + a.readMessagePlain = string(b) + // we only handle the plain version if there is no HTML version if !gotHTML && plain != nil { - err := htmlcore.ReadMD(htmlcore.NewContext(), w, errors.Log1(io.ReadAll(plain.Body))) + err := htmlcore.ReadMDString(htmlcore.NewContext(), w, a.readMessagePlain) if err != nil { return err } From 27f3de86771c3f4689c8b34f9dfbff0b7a6c54ee Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Wed, 11 Sep 2024 12:22:52 -0700 Subject: [PATCH 34/75] more reply quoting work --- mail/actions.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mail/actions.go b/mail/actions.go index 1adead38..c686b1fb 100644 --- a/mail/actions.go +++ b/mail/actions.go @@ -51,6 +51,7 @@ func (a *App) reply(title string) { } a.composeMessage.inReplyTo = a.readMessage.MessageID a.composeMessage.references = append(a.readMessageReferences, a.readMessage.MessageID) - a.composeMessage.body = "\n\nOn " + a.readMessage.Date.Format("Mon, Jan 2, 2006 at 3:04 PM") + ", " + a.composeMessage.To[0].String() + " wrote:\n\n" + a.readMessagePlain + a.composeMessage.body = "\n\nOn " + a.readMessage.Date.Format("Mon, Jan 2, 2006 at 3:04 PM") + ", " + a.composeMessage.To[0].String() + " wrote:\n\n" + a.composeMessage.body += strings.ReplaceAll(a.readMessagePlain, "\n", "\n> ") a.compose(title) } From a0207521399183a3841478a24b589ca279afec5f Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Thu, 12 Sep 2024 08:28:07 -0700 Subject: [PATCH 35/75] add more message quoting for mail reply --- mail/actions.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mail/actions.go b/mail/actions.go index c686b1fb..9e6c26b0 100644 --- a/mail/actions.go +++ b/mail/actions.go @@ -51,7 +51,7 @@ func (a *App) reply(title string) { } a.composeMessage.inReplyTo = a.readMessage.MessageID a.composeMessage.references = append(a.readMessageReferences, a.readMessage.MessageID) - a.composeMessage.body = "\n\nOn " + a.readMessage.Date.Format("Mon, Jan 2, 2006 at 3:04 PM") + ", " + a.composeMessage.To[0].String() + " wrote:\n\n" + a.composeMessage.body = "\n\n> On " + a.readMessage.Date.Format("Mon, Jan 2, 2006 at 3:04 PM") + ", " + a.composeMessage.To[0].String() + " wrote:\n\n> " a.composeMessage.body += strings.ReplaceAll(a.readMessagePlain, "\n", "\n> ") a.compose(title) } From 68f832f166b113a8b38c8a7a521274e5c6ac35b5 Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Thu, 12 Sep 2024 08:31:50 -0700 Subject: [PATCH 36/75] add more reply quoting in mail --- mail/actions.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mail/actions.go b/mail/actions.go index 9e6c26b0..e231ec08 100644 --- a/mail/actions.go +++ b/mail/actions.go @@ -51,7 +51,7 @@ func (a *App) reply(title string) { } a.composeMessage.inReplyTo = a.readMessage.MessageID a.composeMessage.references = append(a.readMessageReferences, a.readMessage.MessageID) - a.composeMessage.body = "\n\n> On " + a.readMessage.Date.Format("Mon, Jan 2, 2006 at 3:04 PM") + ", " + a.composeMessage.To[0].String() + " wrote:\n\n> " + a.composeMessage.body = "\n\n> On " + a.readMessage.Date.Format("Mon, Jan 2, 2006 at 3:04 PM") + ", " + a.composeMessage.To[0].String() + " wrote:\n>\n> " a.composeMessage.body += strings.ReplaceAll(a.readMessagePlain, "\n", "\n> ") a.compose(title) } From 4ccb9e637423a0e46430723ad39622664808a774 Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Thu, 12 Sep 2024 09:17:41 -0700 Subject: [PATCH 37/75] start fetching and caching flags in mail --- mail/cache.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mail/cache.go b/mail/cache.go index ff250119..c9c99fa1 100644 --- a/mail/cache.go +++ b/mail/cache.go @@ -29,6 +29,7 @@ type CacheData struct { imap.Envelope UID imap.UID Filename string + Flags []imap.Flag } // ToMessage converts the [CacheData] to a [ReadMessage]. @@ -194,6 +195,7 @@ func (a *App) CacheUIDs(uids []imap.UID, c *imapclient.Client, email string, mai fetchOptions := &imap.FetchOptions{ Envelope: true, + Flags: true, UID: true, BodySection: []*imap.FetchItemBodySection{ {Specifier: imap.PartSpecifierHeader}, @@ -250,6 +252,7 @@ func (a *App) CacheUIDs(uids []imap.UID, c *imapclient.Client, email string, mai Envelope: *mdata.Envelope, UID: mdata.UID, Filename: filename, + Flags: mdata.Flags, } // we need to save the list of cached messages every time in case From c3000ff3c4421bc07331c008daf684ce7f72dcdd Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Thu, 12 Sep 2024 09:21:53 -0700 Subject: [PATCH 38/75] add indicator for unseen messages --- mail/values.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/mail/values.go b/mail/values.go index 1b3116f2..b4460c31 100644 --- a/mail/values.go +++ b/mail/values.go @@ -7,14 +7,17 @@ package mail import ( "fmt" "net/mail" + "slices" "strings" + "cogentcore.org/core/colors" "cogentcore.org/core/core" "cogentcore.org/core/cursors" "cogentcore.org/core/events" "cogentcore.org/core/styles" "cogentcore.org/core/styles/abilities" "cogentcore.org/core/tree" + "github.com/emersion/go-imap/v2" ) func init() { @@ -51,6 +54,9 @@ func (mi *MessageListItem) Init() { }) w.Updater(func() { text := "" + if !slices.Contains(mi.Data.Flags, imap.FlagSeen) { + text = fmt.Sprintf(`• `, colors.AsHex(colors.ToUniform(colors.Scheme.Primary.Base))) + } for _, f := range mi.Data.From { if f.Name != "" { text += f.Name + " " From 5eafc33070f431f218871714e877b19bd5fe5091 Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Thu, 12 Sep 2024 09:28:12 -0700 Subject: [PATCH 39/75] set Peek to true to prevent the messages from being marked as read automatically --- mail/cache.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mail/cache.go b/mail/cache.go index c9c99fa1..dc7a2919 100644 --- a/mail/cache.go +++ b/mail/cache.go @@ -198,8 +198,8 @@ func (a *App) CacheUIDs(uids []imap.UID, c *imapclient.Client, email string, mai Flags: true, UID: true, BodySection: []*imap.FetchItemBodySection{ - {Specifier: imap.PartSpecifierHeader}, - {Specifier: imap.PartSpecifierText}, + {Specifier: imap.PartSpecifierHeader, Peek: true}, + {Specifier: imap.PartSpecifierText, Peek: true}, }, } From 75448e7ec5da83c541d67831aa60bf943c10ecaa Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Thu, 12 Sep 2024 09:37:31 -0700 Subject: [PATCH 40/75] add MarkAsread and MarkAsUnread in mail; mark as read when clicking on mail by default --- mail/actions.go | 37 +++++++++++++++++++++++++++++++++++++ mail/app.go | 3 +++ mail/typegen.go | 2 +- mail/values.go | 1 + 4 files changed, 42 insertions(+), 1 deletion(-) diff --git a/mail/actions.go b/mail/actions.go index e231ec08..523d9586 100644 --- a/mail/actions.go +++ b/mail/actions.go @@ -5,6 +5,7 @@ package mail import ( + "slices" "strings" "cogentcore.org/core/core" @@ -55,3 +56,39 @@ func (a *App) reply(title string) { a.composeMessage.body += strings.ReplaceAll(a.readMessagePlain, "\n", "\n> ") a.compose(title) } + +// MarkAsRead marks the current message as read. +func (a *App) MarkAsRead() { //types:add + a.markSeen(true) +} + +// MarkAsUnread marks the current message as unread. +func (a *App) MarkAsUnread() { //types:add + a.markSeen(false) +} + +// markSeen sets the [imap.FlagSeen] flag of the current message. +func (a *App) markSeen(seen bool) { + if slices.Contains(a.readMessage.Flags, imap.FlagSeen) == seen { + // Already set correctly. + return + } + go func() { + mu := a.imapMu[a.currentEmail] + mu.Lock() + defer mu.Unlock() + c := a.imapClient[a.currentEmail] + uidset := imap.UIDSet{} + uidset.AddNum(a.readMessage.UID) + op := imap.StoreFlagsDel + if seen { + op = imap.StoreFlagsAdd + } + cmd := c.Store(uidset, &imap.StoreFlags{ + Op: op, + Flags: []imap.Flag{imap.FlagSeen}, + }, nil) + err := cmd.Wait() + core.ErrorSnackbar(a, err, "Error marking message as read") + }() +} diff --git a/mail/app.go b/mail/app.go index 1ea29180..86a0d1ef 100644 --- a/mail/app.go +++ b/mail/app.go @@ -154,6 +154,9 @@ func (a *App) MakeToolbar(p *tree.Plan) { tree.Add(p, func(w *core.FuncButton) { w.SetFunc(a.ReplyAll).SetIcon(icons.ReplyAll) }) + tree.Add(p, func(w *core.FuncButton) { + w.SetFunc(a.MarkAsUnread).SetIcon(icons.MarkAsUnread) + }) } } diff --git a/mail/typegen.go b/mail/typegen.go index a011a260..8e65d054 100644 --- a/mail/typegen.go +++ b/mail/typegen.go @@ -9,7 +9,7 @@ import ( "cogentcore.org/core/types" ) -var _ = types.AddType(&types.Type{Name: "cogentcore.org/cogent/mail.App", IDName: "app", Doc: "App is an email client app.", Methods: []types.Method{{Name: "Move", Doc: "Move moves the current message to the given mailbox.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Args: []string{"mailbox"}}, {Name: "Reply", Doc: "Reply opens a dialog to reply to the current message.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "ReplyAll", Doc: "ReplyAll opens a dialog to reply to all people involved in the current message.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "Compose", Doc: "Compose opens a dialog to send a new message.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "Send", Doc: "Send sends the current message", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Returns: []string{"error"}}}, Embeds: []types.Field{{Name: "Frame"}}, Fields: []types.Field{{Name: "authToken", Doc: "authToken contains the [oauth2.Token] for each account."}, {Name: "authClient", Doc: "authClient contains the [sasl.Client] authentication for sending messages for each account."}, {Name: "imapClient", Doc: "imapClient contains the imap clients for each account."}, {Name: "imapMu", Doc: "imapMu contains the imap client mutexes for each account."}, {Name: "composeMessage", Doc: "composeMessage is the current message we are editing"}, {Name: "cache", Doc: "cache contains the cache data, keyed by account and then mailbox."}, {Name: "currentCache", Doc: "currentCache is [App.cache] for the current email account and mailbox."}, {Name: "readMessage", Doc: "readMessage is the current message we are reading"}, {Name: "currentEmail", Doc: "The current email account"}, {Name: "currentMailbox", Doc: "The current mailbox"}}}) +var _ = types.AddType(&types.Type{Name: "cogentcore.org/cogent/mail.App", IDName: "app", Doc: "App is an email client app.", Methods: []types.Method{{Name: "Move", Doc: "Move moves the current message to the given mailbox.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Args: []string{"mailbox"}}, {Name: "Reply", Doc: "Reply opens a dialog to reply to the current message.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "ReplyAll", Doc: "ReplyAll opens a dialog to reply to all people involved in the current message.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "MarkAsRead", Doc: "MarkAsRead marks the current message as read.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "MarkAsUnread", Doc: "MarkAsUnread marks the current message as unread.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "Compose", Doc: "Compose opens a dialog to send a new message.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "Send", Doc: "Send sends the current message", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Returns: []string{"error"}}}, Embeds: []types.Field{{Name: "Frame"}}, Fields: []types.Field{{Name: "authToken", Doc: "authToken contains the [oauth2.Token] for each account."}, {Name: "authClient", Doc: "authClient contains the [sasl.Client] authentication for sending messages for each account."}, {Name: "imapClient", Doc: "imapClient contains the imap clients for each account."}, {Name: "imapMu", Doc: "imapMu contains the imap client mutexes for each account."}, {Name: "composeMessage", Doc: "composeMessage is the current message we are editing"}, {Name: "cache", Doc: "cache contains the cache data, keyed by account and then mailbox."}, {Name: "currentCache", Doc: "currentCache is [App.cache] for the current email account and mailbox."}, {Name: "readMessage", Doc: "readMessage is the current message we are reading"}, {Name: "readMessageReferences", Doc: "readMessageReferences is the References header of the current readMessage."}, {Name: "readMessagePlain", Doc: "readMessagePlain is the plain text body of the current readMessage."}, {Name: "currentEmail", Doc: "The current email account"}, {Name: "currentMailbox", Doc: "The current mailbox"}}}) // NewApp returns a new [App] with the given optional parent: // App is an email client app. diff --git a/mail/values.go b/mail/values.go index b4460c31..8622b1c1 100644 --- a/mail/values.go +++ b/mail/values.go @@ -43,6 +43,7 @@ func (mi *MessageListItem) Init() { }) mi.OnClick(func(e events.Event) { theApp.readMessage = mi.Data + theApp.MarkAsRead() theApp.Update() }) From 20c15d520c7be645881fd4d51a6aec29e2d35b2c Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Thu, 12 Sep 2024 10:01:09 -0700 Subject: [PATCH 41/75] start on mail forwarding --- mail/actions.go | 8 ++++++++ mail/app.go | 3 +++ mail/typegen.go | 2 +- 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/mail/actions.go b/mail/actions.go index 523d9586..8a5cff72 100644 --- a/mail/actions.go +++ b/mail/actions.go @@ -5,6 +5,7 @@ package mail import ( + "net/mail" "slices" "strings" @@ -43,6 +44,13 @@ func (a *App) ReplyAll() { //types:add a.reply("Reply all") } +// Forward opens a dialog to forward the current message to others. +func (a *App) Forward() { //types:add + a.composeMessage = &SendMessage{} + a.composeMessage.To = []*mail.Address{} + a.reply("Forward") +} + // reply is the implementation of the email reply dialog, // used by other higher-level functions. func (a *App) reply(title string) { diff --git a/mail/app.go b/mail/app.go index 86a0d1ef..7ed90b3c 100644 --- a/mail/app.go +++ b/mail/app.go @@ -154,6 +154,9 @@ func (a *App) MakeToolbar(p *tree.Plan) { tree.Add(p, func(w *core.FuncButton) { w.SetFunc(a.ReplyAll).SetIcon(icons.ReplyAll) }) + tree.Add(p, func(w *core.FuncButton) { + w.SetFunc(a.Forward).SetIcon(icons.Forward) + }) tree.Add(p, func(w *core.FuncButton) { w.SetFunc(a.MarkAsUnread).SetIcon(icons.MarkAsUnread) }) diff --git a/mail/typegen.go b/mail/typegen.go index 8e65d054..c574e643 100644 --- a/mail/typegen.go +++ b/mail/typegen.go @@ -9,7 +9,7 @@ import ( "cogentcore.org/core/types" ) -var _ = types.AddType(&types.Type{Name: "cogentcore.org/cogent/mail.App", IDName: "app", Doc: "App is an email client app.", Methods: []types.Method{{Name: "Move", Doc: "Move moves the current message to the given mailbox.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Args: []string{"mailbox"}}, {Name: "Reply", Doc: "Reply opens a dialog to reply to the current message.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "ReplyAll", Doc: "ReplyAll opens a dialog to reply to all people involved in the current message.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "MarkAsRead", Doc: "MarkAsRead marks the current message as read.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "MarkAsUnread", Doc: "MarkAsUnread marks the current message as unread.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "Compose", Doc: "Compose opens a dialog to send a new message.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "Send", Doc: "Send sends the current message", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Returns: []string{"error"}}}, Embeds: []types.Field{{Name: "Frame"}}, Fields: []types.Field{{Name: "authToken", Doc: "authToken contains the [oauth2.Token] for each account."}, {Name: "authClient", Doc: "authClient contains the [sasl.Client] authentication for sending messages for each account."}, {Name: "imapClient", Doc: "imapClient contains the imap clients for each account."}, {Name: "imapMu", Doc: "imapMu contains the imap client mutexes for each account."}, {Name: "composeMessage", Doc: "composeMessage is the current message we are editing"}, {Name: "cache", Doc: "cache contains the cache data, keyed by account and then mailbox."}, {Name: "currentCache", Doc: "currentCache is [App.cache] for the current email account and mailbox."}, {Name: "readMessage", Doc: "readMessage is the current message we are reading"}, {Name: "readMessageReferences", Doc: "readMessageReferences is the References header of the current readMessage."}, {Name: "readMessagePlain", Doc: "readMessagePlain is the plain text body of the current readMessage."}, {Name: "currentEmail", Doc: "The current email account"}, {Name: "currentMailbox", Doc: "The current mailbox"}}}) +var _ = types.AddType(&types.Type{Name: "cogentcore.org/cogent/mail.App", IDName: "app", Doc: "App is an email client app.", Methods: []types.Method{{Name: "Move", Doc: "Move moves the current message to the given mailbox.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Args: []string{"mailbox"}}, {Name: "Reply", Doc: "Reply opens a dialog to reply to the current message.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "ReplyAll", Doc: "ReplyAll opens a dialog to reply to all people involved in the current message.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "Forward", Doc: "Forward opens a dialog to forward the current message to others.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "MarkAsRead", Doc: "MarkAsRead marks the current message as read.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "MarkAsUnread", Doc: "MarkAsUnread marks the current message as unread.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "Compose", Doc: "Compose opens a dialog to send a new message.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "Send", Doc: "Send sends the current message", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Returns: []string{"error"}}}, Embeds: []types.Field{{Name: "Frame"}}, Fields: []types.Field{{Name: "authToken", Doc: "authToken contains the [oauth2.Token] for each account."}, {Name: "authClient", Doc: "authClient contains the [sasl.Client] authentication for sending messages for each account."}, {Name: "imapClient", Doc: "imapClient contains the imap clients for each account."}, {Name: "imapMu", Doc: "imapMu contains the imap client mutexes for each account."}, {Name: "composeMessage", Doc: "composeMessage is the current message we are editing"}, {Name: "cache", Doc: "cache contains the cache data, keyed by account and then mailbox."}, {Name: "currentCache", Doc: "currentCache is [App.cache] for the current email account and mailbox."}, {Name: "readMessage", Doc: "readMessage is the current message we are reading"}, {Name: "readMessageReferences", Doc: "readMessageReferences is the References header of the current readMessage."}, {Name: "readMessagePlain", Doc: "readMessagePlain is the plain text body of the current readMessage."}, {Name: "currentEmail", Doc: "The current email account"}, {Name: "currentMailbox", Doc: "The current mailbox"}}}) // NewApp returns a new [App] with the given optional parent: // App is an email client app. From 2a91629f4ee0e6365c44ebdde7e9a9d59b0d8062 Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Thu, 12 Sep 2024 19:54:37 -0700 Subject: [PATCH 42/75] if original message is from me, reply to original receiver, not me --- mail/actions.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mail/actions.go b/mail/actions.go index 8a5cff72..38860e80 100644 --- a/mail/actions.go +++ b/mail/actions.go @@ -34,6 +34,9 @@ func (a *App) Move(mailbox string) { //types:add func (a *App) Reply() { //types:add a.composeMessage = &SendMessage{} a.composeMessage.To = IMAPToMailAddresses(a.readMessage.From) + if a.composeMessage.To[0].Address == a.currentEmail { + a.composeMessage.To = IMAPToMailAddresses(a.readMessage.To) + } a.reply("Reply") } From 30f6c61ac4fe0cd9b6363dcfb4c03b137f114f10 Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Thu, 12 Sep 2024 20:00:26 -0700 Subject: [PATCH 43/75] finish logic for excluding ourself from mail reply receiver --- mail/actions.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/mail/actions.go b/mail/actions.go index 38860e80..6a6375ba 100644 --- a/mail/actions.go +++ b/mail/actions.go @@ -5,12 +5,12 @@ package mail import ( - "net/mail" "slices" "strings" "cogentcore.org/core/core" "github.com/emersion/go-imap/v2" + "github.com/emersion/go-message/mail" ) // Move moves the current message to the given mailbox. @@ -34,6 +34,7 @@ func (a *App) Move(mailbox string) { //types:add func (a *App) Reply() { //types:add a.composeMessage = &SendMessage{} a.composeMessage.To = IMAPToMailAddresses(a.readMessage.From) + // If we sent the original message, reply to the original receiver instead of ourself. if a.composeMessage.To[0].Address == a.currentEmail { a.composeMessage.To = IMAPToMailAddresses(a.readMessage.To) } @@ -57,6 +58,12 @@ func (a *App) Forward() { //types:add // reply is the implementation of the email reply dialog, // used by other higher-level functions. func (a *App) reply(title string) { + // If we have more than one receiver, then we should not be one of them. + if len(a.composeMessage.To) > 1 { + a.composeMessage.To = slices.DeleteFunc(a.composeMessage.To, func(ma *mail.Address) bool { + return ma.Address == a.currentEmail + }) + } a.composeMessage.Subject = a.readMessage.Subject if !strings.HasPrefix(a.composeMessage.Subject, "Re: ") { a.composeMessage.Subject = "Re: " + a.composeMessage.Subject From af7deec1f58d5eb7cf578997466f7d1f29c9e643 Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Fri, 13 Sep 2024 07:47:00 -0700 Subject: [PATCH 44/75] fully finish mail reply to ourself handling --- mail/actions.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mail/actions.go b/mail/actions.go index 6a6375ba..251fa6e0 100644 --- a/mail/actions.go +++ b/mail/actions.go @@ -63,6 +63,10 @@ func (a *App) reply(title string) { a.composeMessage.To = slices.DeleteFunc(a.composeMessage.To, func(ma *mail.Address) bool { return ma.Address == a.currentEmail }) + // If all of the receivers were us, then we should reply to ourself. + if len(a.composeMessage.To) == 0 { + a.composeMessage.To = []*mail.Address{{Address: a.currentEmail}} + } } a.composeMessage.Subject = a.readMessage.Subject if !strings.HasPrefix(a.composeMessage.Subject, "Re: ") { From 430c39fb5087e679eb5000c54650631c97b2a31a Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Fri, 13 Sep 2024 07:52:05 -0700 Subject: [PATCH 45/75] fix original sender fetching in mail reply --- mail/actions.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mail/actions.go b/mail/actions.go index 251fa6e0..eb62991b 100644 --- a/mail/actions.go +++ b/mail/actions.go @@ -51,7 +51,7 @@ func (a *App) ReplyAll() { //types:add // Forward opens a dialog to forward the current message to others. func (a *App) Forward() { //types:add a.composeMessage = &SendMessage{} - a.composeMessage.To = []*mail.Address{} + a.composeMessage.To = []*mail.Address{{}} a.reply("Forward") } @@ -74,7 +74,7 @@ func (a *App) reply(title string) { } a.composeMessage.inReplyTo = a.readMessage.MessageID a.composeMessage.references = append(a.readMessageReferences, a.readMessage.MessageID) - a.composeMessage.body = "\n\n> On " + a.readMessage.Date.Format("Mon, Jan 2, 2006 at 3:04 PM") + ", " + a.composeMessage.To[0].String() + " wrote:\n>\n> " + a.composeMessage.body = "\n\n> On " + a.readMessage.Date.Format("Mon, Jan 2, 2006 at 3:04 PM") + ", " + IMAPToMailAddresses(a.readMessage.From)[0].String() + " wrote:\n>\n> " a.composeMessage.body += strings.ReplaceAll(a.readMessagePlain, "\n", "\n> ") a.compose(title) } From a204effd32472520157e945614ca8d6afcf23f04 Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Fri, 13 Sep 2024 07:56:20 -0700 Subject: [PATCH 46/75] use correct Fwd: prefix for forwarding in mail --- mail/actions.go | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/mail/actions.go b/mail/actions.go index eb62991b..98e404bf 100644 --- a/mail/actions.go +++ b/mail/actions.go @@ -38,26 +38,27 @@ func (a *App) Reply() { //types:add if a.composeMessage.To[0].Address == a.currentEmail { a.composeMessage.To = IMAPToMailAddresses(a.readMessage.To) } - a.reply("Reply") + a.reply("Reply", false) } // ReplyAll opens a dialog to reply to all people involved in the current message. func (a *App) ReplyAll() { //types:add a.composeMessage = &SendMessage{} a.composeMessage.To = append(IMAPToMailAddresses(a.readMessage.From), IMAPToMailAddresses(a.readMessage.To)...) - a.reply("Reply all") + a.reply("Reply all", false) } // Forward opens a dialog to forward the current message to others. func (a *App) Forward() { //types:add a.composeMessage = &SendMessage{} a.composeMessage.To = []*mail.Address{{}} - a.reply("Forward") + a.reply("Forward", true) } // reply is the implementation of the email reply dialog, -// used by other higher-level functions. -func (a *App) reply(title string) { +// used by other higher-level functions. forward is whether +// this is actually a forward instead of a reply. +func (a *App) reply(title string, forward bool) { // If we have more than one receiver, then we should not be one of them. if len(a.composeMessage.To) > 1 { a.composeMessage.To = slices.DeleteFunc(a.composeMessage.To, func(ma *mail.Address) bool { @@ -69,8 +70,12 @@ func (a *App) reply(title string) { } } a.composeMessage.Subject = a.readMessage.Subject - if !strings.HasPrefix(a.composeMessage.Subject, "Re: ") { - a.composeMessage.Subject = "Re: " + a.composeMessage.Subject + prefix := "Re: " + if forward { + prefix = "Fwd: " + } + if !strings.HasPrefix(a.composeMessage.Subject, prefix) { + a.composeMessage.Subject = prefix + a.composeMessage.Subject } a.composeMessage.inReplyTo = a.readMessage.MessageID a.composeMessage.references = append(a.readMessageReferences, a.readMessage.MessageID) From c72387f2f32fdc49268bc2f94cda3137873f4b6c Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Fri, 13 Sep 2024 12:29:39 -0700 Subject: [PATCH 47/75] add correct front matter for mail forwarding --- mail/actions.go | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/mail/actions.go b/mail/actions.go index 98e404bf..14893cbe 100644 --- a/mail/actions.go +++ b/mail/actions.go @@ -79,7 +79,22 @@ func (a *App) reply(title string, forward bool) { } a.composeMessage.inReplyTo = a.readMessage.MessageID a.composeMessage.references = append(a.readMessageReferences, a.readMessage.MessageID) - a.composeMessage.body = "\n\n> On " + a.readMessage.Date.Format("Mon, Jan 2, 2006 at 3:04 PM") + ", " + IMAPToMailAddresses(a.readMessage.From)[0].String() + " wrote:\n>\n> " + from := IMAPToMailAddresses(a.readMessage.From)[0].String() + date := a.readMessage.Date.Format("Mon, Jan 2, 2006 at 3:04 PM") + if forward { + a.composeMessage.body = "\n\n> Begin forwarded message:\n>" + a.composeMessage.body += "\n> From: " + from + a.composeMessage.body += "\n> Subject: " + a.readMessage.Subject + a.composeMessage.body += "\n> Date: " + date + to := make([]string, len(a.readMessage.To)) + for i, addr := range IMAPToMailAddresses(a.readMessage.To) { + to[i] = addr.String() + } + a.composeMessage.body += "\n> To: " + strings.Join(to, ", ") + } else { + a.composeMessage.body = "\n\n> On " + date + ", " + from + " wrote:" + } + a.composeMessage.body += "\n>\n> " a.composeMessage.body += strings.ReplaceAll(a.readMessagePlain, "\n", "\n> ") a.compose(title) } From e5c167028f4ad399175aea8f1faf153bf1a3d96b Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Fri, 13 Sep 2024 12:35:40 -0700 Subject: [PATCH 48/75] fix plain message handling logic in mail --- mail/read.go | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/mail/read.go b/mail/read.go index b3267a2a..7b8d6bf8 100644 --- a/mail/read.go +++ b/mail/read.go @@ -55,7 +55,6 @@ func (a *App) updateReadMessage(w *core.Frame) error { } a.readMessageReferences = refs - var plain *mail.Part var gotHTML bool for { @@ -75,7 +74,11 @@ func (a *App) updateReadMessage(w *core.Frame) error { switch ct { case "text/plain": - plain = p + b, err := io.ReadAll(p.Body) + if err != nil { + return err + } + a.readMessagePlain = string(b) case "text/html": err := htmlcore.ReadHTML(htmlcore.NewContext(), w, p.Body) if err != nil { @@ -86,14 +89,8 @@ func (a *App) updateReadMessage(w *core.Frame) error { } } - b, err := io.ReadAll(plain.Body) - if err != nil { - return err - } - a.readMessagePlain = string(b) - // we only handle the plain version if there is no HTML version - if !gotHTML && plain != nil { + if !gotHTML { err := htmlcore.ReadMDString(htmlcore.NewContext(), w, a.readMessagePlain) if err != nil { return err From 2d8c13fcc8e95d66bc01218dbd8041808d5899df Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Fri, 13 Sep 2024 16:19:24 -0700 Subject: [PATCH 49/75] use 2 spaces to create markdown newlines for mail forwarding front matter --- mail/actions.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/mail/actions.go b/mail/actions.go index 14893cbe..c96c1b40 100644 --- a/mail/actions.go +++ b/mail/actions.go @@ -84,13 +84,14 @@ func (a *App) reply(title string, forward bool) { if forward { a.composeMessage.body = "\n\n> Begin forwarded message:\n>" a.composeMessage.body += "\n> From: " + from - a.composeMessage.body += "\n> Subject: " + a.readMessage.Subject - a.composeMessage.body += "\n> Date: " + date + // Need 2 spaces to create a newline in markdown. + a.composeMessage.body += " \n> Subject: " + a.readMessage.Subject + a.composeMessage.body += " \n> Date: " + date to := make([]string, len(a.readMessage.To)) for i, addr := range IMAPToMailAddresses(a.readMessage.To) { to[i] = addr.String() } - a.composeMessage.body += "\n> To: " + strings.Join(to, ", ") + a.composeMessage.body += " \n> To: " + strings.Join(to, ", ") } else { a.composeMessage.body = "\n\n> On " + date + ", " + from + " wrote:" } From b9fd12658becb65505b2b41f918335d0ca00304f Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Fri, 13 Sep 2024 16:58:33 -0700 Subject: [PATCH 50/75] fix the mail message writer Close() calls; this was causing the unexpected EOF error when reading a message sent from Cogent Mail in Cogent Mail, and it was also causing the HTML formatting to not work in Mac Mail; gmail apparently has some automatic tolerance thing that adds the part closing statements for you, which is why it worked there (it even fixed it in the so-called 'Original Message', so I only saw the actual problem when I downloaded the true original message locally) --- mail/send.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mail/send.go b/mail/send.go index 845f3f36..ec347412 100644 --- a/mail/send.go +++ b/mail/send.go @@ -88,7 +88,6 @@ func (a *App) Send() error { //types:add if err != nil { return err } - defer tw.Close() var ph mail.InlineHeader ph.Set("Content-Type", "text/plain") @@ -110,6 +109,8 @@ func (a *App) Send() error { //types:add return err } hw.Close() + tw.Close() + mw.Close() to := make([]string, len(a.composeMessage.To)) for i, t := range a.composeMessage.To { From 028d21020413e51b9a8b19b1ab14dbfc71ffa959 Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Sat, 14 Sep 2024 11:00:29 -0700 Subject: [PATCH 51/75] store cached mail per account as a map to allow for easy many-to-many labeling instead of more rigid one-to-many mailboxes --- mail/app.go | 14 ++++++++++---- mail/cache.go | 18 +++++++++--------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/mail/app.go b/mail/app.go index 7ed90b3c..c1dac815 100644 --- a/mail/app.go +++ b/mail/app.go @@ -44,10 +44,11 @@ type App struct { // composeMessage is the current message we are editing composeMessage *SendMessage - // cache contains the cache data, keyed by account and then mailbox. - cache map[string]map[string][]*CacheData + // cache contains the cached message data, keyed by account and then MessageID. + cache map[string]map[string]*CacheData - // currentCache is [App.cache] for the current email account and mailbox. + // currentCache is a sorted view of [App.cache] for the current email account + // and labels, used for displaying a [core.List] of messages. currentCache []*CacheData // readMessage is the current message we are reading @@ -109,7 +110,12 @@ func (a *App) Init() { w.SetSlice(&a.currentCache) w.SetReadOnly(true) w.Updater(func() { - a.currentCache = a.cache[a.currentEmail][a.currentMailbox] + a.currentCache = nil + mp := a.cache[a.currentEmail] + for _, cd := range mp { + // TODO: check if it matches current labels + a.currentCache = append(a.currentCache, cd) + } slices.SortFunc(a.currentCache, func(a, b *CacheData) int { return cmp.Compare(b.Date.UnixNano(), a.Date.UnixNano()) }) diff --git a/mail/cache.go b/mail/cache.go index dc7a2919..fcefd532 100644 --- a/mail/cache.go +++ b/mail/cache.go @@ -61,7 +61,7 @@ func IMAPToMailAddresses(as []imap.Address) []*mail.Address { // have not already been cached. It caches them in the app's data directory. func (a *App) CacheMessages() error { if a.cache == nil { - a.cache = map[string]map[string][]*CacheData{} + a.cache = map[string]map[string]*CacheData{} } if a.imapClient == nil { a.imapClient = map[string]*imapclient.Client{} @@ -83,7 +83,7 @@ func (a *App) CacheMessages() error { // caches them in the app's data directory. func (a *App) CacheMessagesForAccount(email string) error { if a.cache[email] == nil { - a.cache[email] = map[string][]*CacheData{} + a.cache[email] = map[string]*CacheData{} } c, err := imapclient.DialTLS("imap.gmail.com:993", nil) @@ -139,12 +139,12 @@ func (a *App) CacheMessagesForMailbox(c *imapclient.Client, email string, mailbo return err } - var cached []*CacheData + var cached map[string]*CacheData err = jsonx.Open(&cached, cachedFile) if err != nil && !errors.Is(err, fs.ErrNotExist) && !errors.Is(err, io.EOF) { return fmt.Errorf("opening cache list: %w", err) } - a.cache[email][mailbox] = cached + a.cache[email] = cached _, err = c.Select(mailbox, nil).Wait() if err != nil { @@ -184,7 +184,7 @@ func (a *App) CacheMessagesForMailbox(c *imapclient.Client, email string, mailbo // other given values, using an iterative batched approach that fetches the // five next most recent messages at a time, allowing for concurrent mail // modifiation operations and correct ordering. -func (a *App) CacheUIDs(uids []imap.UID, c *imapclient.Client, email string, mailbox string, dir string, cached []*CacheData, cachedFile string) error { +func (a *App) CacheUIDs(uids []imap.UID, c *imapclient.Client, email string, mailbox string, dir string, cached map[string]*CacheData, cachedFile string) error { for len(uids) > 0 { num := min(5, len(uids)) cuids := uids[len(uids)-num:] // the current batch of UIDs @@ -255,16 +255,16 @@ func (a *App) CacheUIDs(uids []imap.UID, c *imapclient.Client, email string, mai Flags: mdata.Flags, } - // we need to save the list of cached messages every time in case - // we get interrupted or have an error - cached = append(cached, cd) + // We need to save the list of cached messages every time in case + // we get interrupted or have an error. + cached[mdata.Envelope.MessageID] = cd // TODO: check if it already exists err = jsonx.Save(&cached, cachedFile) if err != nil { a.imapMu[email].Unlock() return fmt.Errorf("saving cache list: %w", err) } - a.cache[email][mailbox] = cached + a.cache[email] = cached a.AsyncLock() a.Update() a.AsyncUnlock() From 29a1bb94e7524479b19c52d86f7fe5dd098bd015 Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Sat, 14 Sep 2024 11:04:33 -0700 Subject: [PATCH 52/75] use base32 MessageID with .eml extension for cached mail filename --- mail/cache.go | 16 ++++++++-------- mail/read.go | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/mail/cache.go b/mail/cache.go index fcefd532..837bf381 100644 --- a/mail/cache.go +++ b/mail/cache.go @@ -12,7 +12,6 @@ import ( "net/mail" "os" "path/filepath" - "strconv" "strings" "sync" @@ -27,9 +26,8 @@ import ( // mail list in the GUI. type CacheData struct { imap.Envelope - UID imap.UID - Filename string - Flags []imap.Flag + UID imap.UID + Flags []imap.Flag } // ToMessage converts the [CacheData] to a [ReadMessage]. @@ -218,9 +216,7 @@ func (a *App) CacheUIDs(uids []imap.UID, c *imapclient.Client, email string, mai return err } - filename := strconv.FormatUint(uint64(mdata.UID), 32) - filename = strings.Repeat("0", 7-len(filename)) + filename - f, err := os.Create(filepath.Join(dir, filename)) + f, err := os.Create(filepath.Join(dir, messageFilename(mdata.Envelope))) if err != nil { a.imapMu[email].Unlock() return err @@ -251,7 +247,6 @@ func (a *App) CacheUIDs(uids []imap.UID, c *imapclient.Client, email string, mai cd := &CacheData{ Envelope: *mdata.Envelope, UID: mdata.UID, - Filename: filename, Flags: mdata.Flags, } @@ -278,3 +273,8 @@ func (a *App) CacheUIDs(uids []imap.UID, c *imapclient.Client, email string, mai } return nil } + +// messageFilename returns the filename for storing the message with the given envelope. +func messageFilename(env *imap.Envelope) string { + return FilenameBase32(env.MessageID) + ".eml" +} diff --git a/mail/read.go b/mail/read.go index 7b8d6bf8..8d3a9158 100644 --- a/mail/read.go +++ b/mail/read.go @@ -38,7 +38,7 @@ func (a *App) updateReadMessage(w *core.Frame) error { bemail := FilenameBase32(a.currentEmail) - f, err := os.Open(filepath.Join(core.TheApp.AppDataDir(), "mail", bemail, a.readMessage.Filename)) + f, err := os.Open(filepath.Join(core.TheApp.AppDataDir(), "mail", bemail, messageFilename(&a.readMessage.Envelope))) if err != nil { return err } From 850e5367b9fdc269723c7baa5766531be3aead32 Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Sat, 14 Sep 2024 11:17:33 -0700 Subject: [PATCH 53/75] add mail message labels --- mail/app.go | 4 +++- mail/cache.go | 19 ++++++++++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/mail/app.go b/mail/app.go index c1dac815..4dc83198 100644 --- a/mail/app.go +++ b/mail/app.go @@ -113,7 +113,9 @@ func (a *App) Init() { a.currentCache = nil mp := a.cache[a.currentEmail] for _, cd := range mp { - // TODO: check if it matches current labels + if !slices.Contains(cd.Labels, a.currentMailbox) { + continue + } a.currentCache = append(a.currentCache, cd) } slices.SortFunc(a.currentCache, func(a, b *CacheData) int { diff --git a/mail/cache.go b/mail/cache.go index 837bf381..0d34b8c0 100644 --- a/mail/cache.go +++ b/mail/cache.go @@ -12,6 +12,7 @@ import ( "net/mail" "os" "path/filepath" + "slices" "strings" "sync" @@ -28,6 +29,11 @@ type CacheData struct { imap.Envelope UID imap.UID Flags []imap.Flag + + // Labels are the labels associated with the message. + // Labels are many-to-many, similar to gmail. The first + // label is used for the IMAP mailbox. + Labels []string } // ToMessage converts the [CacheData] to a [ReadMessage]. @@ -248,11 +254,22 @@ func (a *App) CacheUIDs(uids []imap.UID, c *imapclient.Client, email string, mai Envelope: *mdata.Envelope, UID: mdata.UID, Flags: mdata.Flags, + Labels: []string{mailbox}, } + // If the message is already cached (likely in another mailbox), + // we update its labels to include this mailbox if it doesn't already. + // Otherwise, we add it as a new entry to the cache. + if _, already := cached[mdata.Envelope.MessageID]; already { + prev := cached[mdata.Envelope.MessageID] + if !slices.Contains(prev.Labels, mailbox) { + prev.Labels = append(prev.Labels, mailbox) + } + } else { + cached[mdata.Envelope.MessageID] = cd + } // We need to save the list of cached messages every time in case // we get interrupted or have an error. - cached[mdata.Envelope.MessageID] = cd // TODO: check if it already exists err = jsonx.Save(&cached, cachedFile) if err != nil { a.imapMu[email].Unlock() From f7afc90a4ba182e9b6306c48f29c235e071d0aff Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Sat, 14 Sep 2024 11:22:32 -0700 Subject: [PATCH 54/75] add more label logic and TODOs --- mail/actions.go | 2 ++ mail/cache.go | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/mail/actions.go b/mail/actions.go index c96c1b40..dcd9ada7 100644 --- a/mail/actions.go +++ b/mail/actions.go @@ -23,6 +23,8 @@ func (a *App) Move(mailbox string) { //types:add defer mu.Unlock() c := a.imapClient[a.currentEmail] uidset := imap.UIDSet{} + // TODO: we are not guaranteed to be in the right mailbox at this point. + // The same is true for other similar actions. uidset.AddNum(a.readMessage.UID) mc := c.Move(uidset, mailbox) _, err := mc.Wait() diff --git a/mail/cache.go b/mail/cache.go index 0d34b8c0..721424ae 100644 --- a/mail/cache.go +++ b/mail/cache.go @@ -160,6 +160,10 @@ func (a *App) CacheMessagesForMailbox(c *imapclient.Client, email string, mailbo if len(cached) > 0 { uidset := imap.UIDSet{} for _, c := range cached { + if !slices.Contains(c.Labels, mailbox) { + continue + } + // TODO: if we are in multiple mailboxes this UID could be the wrong one. uidset.AddNum(c.UID) } From 3ecd3148656f00c285323f492e35d567f8accbbe Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Sat, 14 Sep 2024 11:26:22 -0700 Subject: [PATCH 55/75] fix cached map initialization --- mail/cache.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mail/cache.go b/mail/cache.go index 721424ae..6bb7be24 100644 --- a/mail/cache.go +++ b/mail/cache.go @@ -143,7 +143,7 @@ func (a *App) CacheMessagesForMailbox(c *imapclient.Client, email string, mailbo return err } - var cached map[string]*CacheData + cached := map[string]*CacheData{} err = jsonx.Open(&cached, cachedFile) if err != nil && !errors.Is(err, fs.ErrNotExist) && !errors.Is(err, io.EOF) { return fmt.Errorf("opening cache list: %w", err) From f246f22104faf9881b197cfb97a994f81f39a642 Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Sun, 15 Sep 2024 10:16:14 -0700 Subject: [PATCH 56/75] clean up listCache naming --- mail/app.go | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/mail/app.go b/mail/app.go index 4dc83198..9a376ede 100644 --- a/mail/app.go +++ b/mail/app.go @@ -47,9 +47,10 @@ type App struct { // cache contains the cached message data, keyed by account and then MessageID. cache map[string]map[string]*CacheData - // currentCache is a sorted view of [App.cache] for the current email account - // and labels, used for displaying a [core.List] of messages. - currentCache []*CacheData + // listCache is a sorted view of [App.cache] for the current email account + // and labels, used for displaying a [core.List] of messages. It should not + // be used for any other purpose. + listCache []*CacheData // readMessage is the current message we are reading readMessage *CacheData @@ -107,18 +108,18 @@ func (a *App) Init() { }) }) tree.AddChild(w, func(w *core.List) { - w.SetSlice(&a.currentCache) + w.SetSlice(&a.listCache) w.SetReadOnly(true) w.Updater(func() { - a.currentCache = nil + a.listCache = nil mp := a.cache[a.currentEmail] for _, cd := range mp { if !slices.Contains(cd.Labels, a.currentMailbox) { continue } - a.currentCache = append(a.currentCache, cd) + a.listCache = append(a.listCache, cd) } - slices.SortFunc(a.currentCache, func(a, b *CacheData) int { + slices.SortFunc(a.listCache, func(a, b *CacheData) int { return cmp.Compare(b.Date.UnixNano(), a.Date.UnixNano()) }) }) From 947f4e04ec58c8ca9e9ff7f8c6561d824ef760c5 Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Sun, 15 Sep 2024 10:19:26 -0700 Subject: [PATCH 57/75] only save message content to a file if it is not already cached --- mail/cache.go | 60 ++++++++++++++++++++++++++------------------------- 1 file changed, 31 insertions(+), 29 deletions(-) diff --git a/mail/cache.go b/mail/cache.go index 6bb7be24..1e23e604 100644 --- a/mail/cache.go +++ b/mail/cache.go @@ -226,34 +226,6 @@ func (a *App) CacheUIDs(uids []imap.UID, c *imapclient.Client, email string, mai return err } - f, err := os.Create(filepath.Join(dir, messageFilename(mdata.Envelope))) - if err != nil { - a.imapMu[email].Unlock() - return err - } - - var header, text []byte - - for k, v := range mdata.BodySection { - if k.Specifier == imap.PartSpecifierHeader { - header = v - } else if k.Specifier == imap.PartSpecifierText { - text = v - } - } - - _, err = f.Write(append(header, text...)) - if err != nil { - a.imapMu[email].Unlock() - return fmt.Errorf("writing message: %w", err) - } - - err = f.Close() - if err != nil { - a.imapMu[email].Unlock() - return fmt.Errorf("closing message: %w", err) - } - cd := &CacheData{ Envelope: *mdata.Envelope, UID: mdata.UID, @@ -263,15 +235,45 @@ func (a *App) CacheUIDs(uids []imap.UID, c *imapclient.Client, email string, mai // If the message is already cached (likely in another mailbox), // we update its labels to include this mailbox if it doesn't already. - // Otherwise, we add it as a new entry to the cache. if _, already := cached[mdata.Envelope.MessageID]; already { prev := cached[mdata.Envelope.MessageID] if !slices.Contains(prev.Labels, mailbox) { prev.Labels = append(prev.Labels, mailbox) } } else { + // Otherwise, we add it as a new entry to the cache + // and save the content to a file. cached[mdata.Envelope.MessageID] = cd + + f, err := os.Create(filepath.Join(dir, messageFilename(mdata.Envelope))) + if err != nil { + a.imapMu[email].Unlock() + return err + } + + var header, text []byte + + for k, v := range mdata.BodySection { + if k.Specifier == imap.PartSpecifierHeader { + header = v + } else if k.Specifier == imap.PartSpecifierText { + text = v + } + } + + _, err = f.Write(append(header, text...)) + if err != nil { + a.imapMu[email].Unlock() + return fmt.Errorf("writing message: %w", err) + } + + err = f.Close() + if err != nil { + a.imapMu[email].Unlock() + return fmt.Errorf("closing message: %w", err) + } } + // We need to save the list of cached messages every time in case // we get interrupted or have an error. err = jsonx.Save(&cached, cachedFile) From 7211b6a37b88fc8fcb7fcf31221af6e291be8f1e Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Sun, 15 Sep 2024 10:30:22 -0700 Subject: [PATCH 58/75] store a UID for each label/mailbox a message is in --- mail/app.go | 4 +++- mail/cache.go | 48 +++++++++++++++++++++++++++--------------------- 2 files changed, 30 insertions(+), 22 deletions(-) diff --git a/mail/app.go b/mail/app.go index 9a376ede..41d312c7 100644 --- a/mail/app.go +++ b/mail/app.go @@ -114,7 +114,9 @@ func (a *App) Init() { a.listCache = nil mp := a.cache[a.currentEmail] for _, cd := range mp { - if !slices.Contains(cd.Labels, a.currentMailbox) { + if !slices.ContainsFunc(cd.Labels, func(lbl label) bool { + return lbl.Name == a.currentMailbox + }) { continue } a.listCache = append(a.listCache, cd) diff --git a/mail/cache.go b/mail/cache.go index 1e23e604..d6f961d0 100644 --- a/mail/cache.go +++ b/mail/cache.go @@ -27,13 +27,20 @@ import ( // mail list in the GUI. type CacheData struct { imap.Envelope - UID imap.UID Flags []imap.Flag // Labels are the labels associated with the message. - // Labels are many-to-many, similar to gmail. The first - // label is used for the IMAP mailbox. - Labels []string + // Labels are many-to-many, similar to gmail. All labels + // also correspond to IMAP mailboxes. + Labels []label +} + +// label represents a label associated with a message. +// It contains the name of the label and the UID of the message +// in the IMAP mailbox corresponding to the label. +type label struct { + Name string + UID imap.UID } // ToMessage converts the [CacheData] to a [ReadMessage]. @@ -155,16 +162,16 @@ func (a *App) CacheMessagesForMailbox(c *imapclient.Client, email string, mailbo return fmt.Errorf("opening mailbox: %w", err) } - // we want messages with UIDs not in the list we already cached + // We want messages in this mailbox with UIDs we haven't already cached. criteria := &imap.SearchCriteria{} if len(cached) > 0 { uidset := imap.UIDSet{} - for _, c := range cached { - if !slices.Contains(c.Labels, mailbox) { - continue + for _, cd := range cached { + for _, lbl := range cd.Labels { + if lbl.Name == mailbox { + uidset.AddNum(lbl.UID) + } } - // TODO: if we are in multiple mailboxes this UID could be the wrong one. - uidset.AddNum(c.UID) } nc := imap.SearchCriteria{} @@ -226,24 +233,23 @@ func (a *App) CacheUIDs(uids []imap.UID, c *imapclient.Client, email string, mai return err } - cd := &CacheData{ - Envelope: *mdata.Envelope, - UID: mdata.UID, - Flags: mdata.Flags, - Labels: []string{mailbox}, - } - // If the message is already cached (likely in another mailbox), // we update its labels to include this mailbox if it doesn't already. if _, already := cached[mdata.Envelope.MessageID]; already { - prev := cached[mdata.Envelope.MessageID] - if !slices.Contains(prev.Labels, mailbox) { - prev.Labels = append(prev.Labels, mailbox) + cd := cached[mdata.Envelope.MessageID] + if !slices.ContainsFunc(cd.Labels, func(lbl label) bool { + return lbl.Name == mailbox + }) { + cd.Labels = append(cd.Labels, label{mailbox, mdata.UID}) } } else { // Otherwise, we add it as a new entry to the cache // and save the content to a file. - cached[mdata.Envelope.MessageID] = cd + cached[mdata.Envelope.MessageID] = &CacheData{ + Envelope: *mdata.Envelope, + Flags: mdata.Flags, + Labels: []label{{mailbox, mdata.UID}}, + } f, err := os.Create(filepath.Join(dir, messageFilename(mdata.Envelope))) if err != nil { From 49e55afe3d529d2c2cc0c5e45799d32af68ab765 Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Sun, 15 Sep 2024 10:38:47 -0700 Subject: [PATCH 59/75] start on App.action helper function in mail --- mail/actions.go | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/mail/actions.go b/mail/actions.go index dcd9ada7..d95c1805 100644 --- a/mail/actions.go +++ b/mail/actions.go @@ -13,14 +13,25 @@ import ( "github.com/emersion/go-message/mail" ) -// Move moves the current message to the given mailbox. -func (a *App) Move(mailbox string) { //types:add +// action executes the given function in a goroutine with proper locking. +// This should be used for any user action that interacts with a message in IMAP. +func (a *App) action(f func()) { // Use a goroutine to prevent GUI freezing and a double mutex deadlock // with a combination of the renderContext mutex and the imapMu. go func() { mu := a.imapMu[a.currentEmail] mu.Lock() - defer mu.Unlock() + f() + mu.Unlock() + a.AsyncLock() + a.Update() + a.AsyncUnlock() + }() +} + +// Move moves the current message to the given mailbox. +func (a *App) Move(mailbox string) { //types:add + a.action(func() { c := a.imapClient[a.currentEmail] uidset := imap.UIDSet{} // TODO: we are not guaranteed to be in the right mailbox at this point. @@ -29,7 +40,7 @@ func (a *App) Move(mailbox string) { //types:add mc := c.Move(uidset, mailbox) _, err := mc.Wait() core.ErrorSnackbar(a, err, "Error moving message") - }() + }) } // Reply opens a dialog to reply to the current message. @@ -118,10 +129,7 @@ func (a *App) markSeen(seen bool) { // Already set correctly. return } - go func() { - mu := a.imapMu[a.currentEmail] - mu.Lock() - defer mu.Unlock() + a.action(func() { c := a.imapClient[a.currentEmail] uidset := imap.UIDSet{} uidset.AddNum(a.readMessage.UID) @@ -135,5 +143,5 @@ func (a *App) markSeen(seen bool) { }, nil) err := cmd.Wait() core.ErrorSnackbar(a, err, "Error marking message as read") - }() + }) } From 25801c4a01cfb28ae0fbf827e128d0fbb65c1041 Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Sun, 15 Sep 2024 10:45:18 -0700 Subject: [PATCH 60/75] add actionLabels in mail --- mail/actions.go | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/mail/actions.go b/mail/actions.go index d95c1805..76386012 100644 --- a/mail/actions.go +++ b/mail/actions.go @@ -10,18 +10,19 @@ import ( "cogentcore.org/core/core" "github.com/emersion/go-imap/v2" + "github.com/emersion/go-imap/v2/imapclient" "github.com/emersion/go-message/mail" ) // action executes the given function in a goroutine with proper locking. // This should be used for any user action that interacts with a message in IMAP. -func (a *App) action(f func()) { +func (a *App) action(f func(c *imapclient.Client)) { // Use a goroutine to prevent GUI freezing and a double mutex deadlock // with a combination of the renderContext mutex and the imapMu. go func() { mu := a.imapMu[a.currentEmail] mu.Lock() - f() + f(a.imapClient[a.currentEmail]) mu.Unlock() a.AsyncLock() a.Update() @@ -29,14 +30,27 @@ func (a *App) action(f func()) { }() } +// actionLabels executes the given function for each label of the current message, +// selecting the mailbox for each one first. +func (a *App) actionLabels(f func(c *imapclient.Client, lbl label)) { + a.action(func(c *imapclient.Client) { + for _, lbl := range a.readMessage.Labels { + _, err := c.Select(lbl.Name, nil).Wait() + if err != nil { + core.ErrorSnackbar(a, err, "Error selecting mailbox") + return + } + f(c, lbl) + } + }) +} + // Move moves the current message to the given mailbox. func (a *App) Move(mailbox string) { //types:add - a.action(func() { - c := a.imapClient[a.currentEmail] + // TODO: Move needs to be redesigned with the new many-to-many labeling paradigm. + a.actionLabels(func(c *imapclient.Client, lbl label) { uidset := imap.UIDSet{} - // TODO: we are not guaranteed to be in the right mailbox at this point. - // The same is true for other similar actions. - uidset.AddNum(a.readMessage.UID) + uidset.AddNum(lbl.UID) mc := c.Move(uidset, mailbox) _, err := mc.Wait() core.ErrorSnackbar(a, err, "Error moving message") @@ -129,10 +143,9 @@ func (a *App) markSeen(seen bool) { // Already set correctly. return } - a.action(func() { - c := a.imapClient[a.currentEmail] + a.actionLabels(func(c *imapclient.Client, lbl label) { uidset := imap.UIDSet{} - uidset.AddNum(a.readMessage.UID) + uidset.AddNum(lbl.UID) op := imap.StoreFlagsDel if seen { op = imap.StoreFlagsAdd From 507d8137f785fd0d43a17dbe6c70168167e80708 Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Sun, 15 Sep 2024 10:58:55 -0700 Subject: [PATCH 61/75] clean up mail label naming and start on better label listing --- mail/actions.go | 16 ++++++++-------- mail/app.go | 24 ++++++++++++++---------- mail/cache.go | 28 ++++++++++++---------------- 3 files changed, 34 insertions(+), 34 deletions(-) diff --git a/mail/actions.go b/mail/actions.go index 76386012..02af283e 100644 --- a/mail/actions.go +++ b/mail/actions.go @@ -32,15 +32,15 @@ func (a *App) action(f func(c *imapclient.Client)) { // actionLabels executes the given function for each label of the current message, // selecting the mailbox for each one first. -func (a *App) actionLabels(f func(c *imapclient.Client, lbl label)) { +func (a *App) actionLabels(f func(c *imapclient.Client, label Label)) { a.action(func(c *imapclient.Client) { - for _, lbl := range a.readMessage.Labels { - _, err := c.Select(lbl.Name, nil).Wait() + for _, label := range a.readMessage.Labels { + _, err := c.Select(label.Name, nil).Wait() if err != nil { core.ErrorSnackbar(a, err, "Error selecting mailbox") return } - f(c, lbl) + f(c, label) } }) } @@ -48,9 +48,9 @@ func (a *App) actionLabels(f func(c *imapclient.Client, lbl label)) { // Move moves the current message to the given mailbox. func (a *App) Move(mailbox string) { //types:add // TODO: Move needs to be redesigned with the new many-to-many labeling paradigm. - a.actionLabels(func(c *imapclient.Client, lbl label) { + a.actionLabels(func(c *imapclient.Client, label Label) { uidset := imap.UIDSet{} - uidset.AddNum(lbl.UID) + uidset.AddNum(label.UID) mc := c.Move(uidset, mailbox) _, err := mc.Wait() core.ErrorSnackbar(a, err, "Error moving message") @@ -143,9 +143,9 @@ func (a *App) markSeen(seen bool) { // Already set correctly. return } - a.actionLabels(func(c *imapclient.Client, lbl label) { + a.actionLabels(func(c *imapclient.Client, label Label) { uidset := imap.UIDSet{} - uidset.AddNum(lbl.UID) + uidset.AddNum(label.UID) op := imap.StoreFlagsDel if seen { op = imap.StoreFlagsAdd diff --git a/mail/app.go b/mail/app.go index 41d312c7..2550aa24 100644 --- a/mail/app.go +++ b/mail/app.go @@ -61,11 +61,14 @@ type App struct { // readMessagePlain is the plain text body of the current readMessage. readMessagePlain string - // The current email account + // currentEmail is the current email account. currentEmail string - // The current mailbox - currentMailbox string + // labels are all of the possible labels that messages have. + labels map[string]bool + + // showLabel is the current label to show messages for. + showLabel string } // needed for interface import @@ -78,6 +81,7 @@ var theApp *App func (a *App) Init() { theApp = a a.Frame.Init() + a.showLabel = "INBOX" a.authToken = map[string]*oauth2.Token{} a.authClient = map[string]sasl.Client{} a.Styler(func(s *styles.Style) { @@ -92,12 +96,12 @@ func (a *App) Init() { for _, email := range Settings.Accounts { tree.AddAt(p, email, func(w *core.Tree) { w.Maker(func(p *tree.Plan) { - mailboxes := maps.Keys(a.cache[email]) - slices.Sort(mailboxes) - for _, mailbox := range mailboxes { - tree.AddAt(p, mailbox, func(w *core.Tree) { + labels := maps.Keys(a.labels) + slices.Sort(labels) + for _, label := range labels { + tree.AddAt(p, label, func(w *core.Tree) { w.OnSelect(func(e events.Event) { - a.currentMailbox = mailbox + a.showLabel = label a.Update() }) }) @@ -114,8 +118,8 @@ func (a *App) Init() { a.listCache = nil mp := a.cache[a.currentEmail] for _, cd := range mp { - if !slices.ContainsFunc(cd.Labels, func(lbl label) bool { - return lbl.Name == a.currentMailbox + if !slices.ContainsFunc(cd.Labels, func(label Label) bool { + return label.Name == a.showLabel }) { continue } diff --git a/mail/cache.go b/mail/cache.go index d6f961d0..991c84eb 100644 --- a/mail/cache.go +++ b/mail/cache.go @@ -32,13 +32,13 @@ type CacheData struct { // Labels are the labels associated with the message. // Labels are many-to-many, similar to gmail. All labels // also correspond to IMAP mailboxes. - Labels []label + Labels []Label } -// label represents a label associated with a message. -// It contains the name of the label and the UID of the message -// in the IMAP mailbox corresponding to the label. -type label struct { +// Label represents a Label associated with a message. +// It contains the name of the Label and the UID of the message +// in the IMAP mailbox corresponding to the Label. +type Label struct { Name string UID imap.UID } @@ -132,10 +132,6 @@ func (a *App) CacheMessagesForAccount(email string) error { // that have not already been cached for the given email account and mailbox. // It caches them in the app's data directory. func (a *App) CacheMessagesForMailbox(c *imapclient.Client, email string, mailbox string) error { - if a.currentMailbox == "" { - a.currentMailbox = mailbox - } - bemail := FilenameBase32(email) dir := filepath.Join(core.TheApp.AppDataDir(), "mail", bemail) @@ -167,9 +163,9 @@ func (a *App) CacheMessagesForMailbox(c *imapclient.Client, email string, mailbo if len(cached) > 0 { uidset := imap.UIDSet{} for _, cd := range cached { - for _, lbl := range cd.Labels { - if lbl.Name == mailbox { - uidset.AddNum(lbl.UID) + for _, label := range cd.Labels { + if label.Name == mailbox { + uidset.AddNum(label.UID) } } } @@ -237,10 +233,10 @@ func (a *App) CacheUIDs(uids []imap.UID, c *imapclient.Client, email string, mai // we update its labels to include this mailbox if it doesn't already. if _, already := cached[mdata.Envelope.MessageID]; already { cd := cached[mdata.Envelope.MessageID] - if !slices.ContainsFunc(cd.Labels, func(lbl label) bool { - return lbl.Name == mailbox + if !slices.ContainsFunc(cd.Labels, func(label Label) bool { + return label.Name == mailbox }) { - cd.Labels = append(cd.Labels, label{mailbox, mdata.UID}) + cd.Labels = append(cd.Labels, Label{mailbox, mdata.UID}) } } else { // Otherwise, we add it as a new entry to the cache @@ -248,7 +244,7 @@ func (a *App) CacheUIDs(uids []imap.UID, c *imapclient.Client, email string, mai cached[mdata.Envelope.MessageID] = &CacheData{ Envelope: *mdata.Envelope, Flags: mdata.Flags, - Labels: []label{{mailbox, mdata.UID}}, + Labels: []Label{{mailbox, mdata.UID}}, } f, err := os.Create(filepath.Join(dir, messageFilename(mdata.Envelope))) From 9fc07851f2354479f68c1b8c6bbbc45436cad200 Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Sun, 15 Sep 2024 11:02:43 -0700 Subject: [PATCH 62/75] get labels tree working --- mail/app.go | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/mail/app.go b/mail/app.go index 2550aa24..4a39b533 100644 --- a/mail/app.go +++ b/mail/app.go @@ -81,9 +81,10 @@ var theApp *App func (a *App) Init() { theApp = a a.Frame.Init() - a.showLabel = "INBOX" a.authToken = map[string]*oauth2.Token{} a.authClient = map[string]sasl.Client{} + a.labels = map[string]bool{} + a.showLabel = "INBOX" a.Styler(func(s *styles.Style) { s.Grow.Set(1, 1) }) @@ -91,7 +92,7 @@ func (a *App) Init() { tree.AddChild(a, func(w *core.Splits) { w.SetSplits(0.1, 0.2, 0.7) tree.AddChild(w, func(w *core.Tree) { - w.SetText("Mailboxes") + w.SetText("Labels") w.Maker(func(p *tree.Plan) { for _, email := range Settings.Accounts { tree.AddAt(p, email, func(w *core.Tree) { @@ -118,12 +119,13 @@ func (a *App) Init() { a.listCache = nil mp := a.cache[a.currentEmail] for _, cd := range mp { - if !slices.ContainsFunc(cd.Labels, func(label Label) bool { - return label.Name == a.showLabel - }) { - continue + for _, label := range cd.Labels { + a.labels[label.Name] = true + if label.Name == a.showLabel { + a.listCache = append(a.listCache, cd) + break + } } - a.listCache = append(a.listCache, cd) } slices.SortFunc(a.listCache, func(a, b *CacheData) int { return cmp.Compare(b.Date.UnixNano(), a.Date.UnixNano()) From 86220c70c18f86d81c07533341c6f177879c3911 Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Sun, 15 Sep 2024 11:08:03 -0700 Subject: [PATCH 63/75] add initial friendlyLabelName to mail --- mail/app.go | 3 ++- mail/settings.go | 12 ++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/mail/app.go b/mail/app.go index 4a39b533..90937e0e 100644 --- a/mail/app.go +++ b/mail/app.go @@ -92,7 +92,7 @@ func (a *App) Init() { tree.AddChild(a, func(w *core.Splits) { w.SetSplits(0.1, 0.2, 0.7) tree.AddChild(w, func(w *core.Tree) { - w.SetText("Labels") + w.SetText("Accounts") w.Maker(func(p *tree.Plan) { for _, email := range Settings.Accounts { tree.AddAt(p, email, func(w *core.Tree) { @@ -101,6 +101,7 @@ func (a *App) Init() { slices.Sort(labels) for _, label := range labels { tree.AddAt(p, label, func(w *core.Tree) { + w.SetText(friendlyLabelName(label)) w.OnSelect(func(e events.Event) { a.showLabel = label a.Update() diff --git a/mail/settings.go b/mail/settings.go index 3536fdd7..129ad484 100644 --- a/mail/settings.go +++ b/mail/settings.go @@ -30,3 +30,15 @@ type SettingsData struct { //types:add // Accounts are the email accounts the user is signed into. Accounts []string } + +// friendlyLabelName converts the given label name to a user-friendly version. +func friendlyLabelName(name string) string { + if f, ok := friendlyLabelNames[name]; ok { + return f + } + return name +} + +var friendlyLabelNames = map[string]string{ + "INBOX": "Inbox", +} From cf381f9f5c2aea1c8171d0d7eb2b937bfe7ac838 Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Sun, 15 Sep 2024 11:50:16 -0700 Subject: [PATCH 64/75] update the cache in markSeen for mail in addition to IMAP --- mail/actions.go | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/mail/actions.go b/mail/actions.go index 02af283e..81421389 100644 --- a/mail/actions.go +++ b/mail/actions.go @@ -155,6 +155,18 @@ func (a *App) markSeen(seen bool) { Flags: []imap.Flag{imap.FlagSeen}, }, nil) err := cmd.Wait() - core.ErrorSnackbar(a, err, "Error marking message as read") + if err != nil { + core.ErrorSnackbar(a, err, "Error marking message as read") + return + } + // Update the cache in addition to IMAP. + flags := &a.cache[a.currentEmail][a.readMessage.MessageID].Flags + if seen && !slices.Contains(*flags, imap.FlagSeen) { + *flags = append(*flags, imap.FlagSeen) + } else if !seen { + *flags = slices.DeleteFunc(*flags, func(flag imap.Flag) bool { + return flag == imap.FlagSeen + }) + } }) } From 445142c946ba493098325856cbdce3754340c5e4 Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Sun, 15 Sep 2024 11:53:52 -0700 Subject: [PATCH 65/75] minor comment cleanup --- mail/actions.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mail/actions.go b/mail/actions.go index 81421389..dd139813 100644 --- a/mail/actions.go +++ b/mail/actions.go @@ -159,7 +159,7 @@ func (a *App) markSeen(seen bool) { core.ErrorSnackbar(a, err, "Error marking message as read") return } - // Update the cache in addition to IMAP. + // Also directly update the cache: flags := &a.cache[a.currentEmail][a.readMessage.MessageID].Flags if seen && !slices.Contains(*flags, imap.FlagSeen) { *flags = append(*flags, imap.FlagSeen) From 22efacd52f3a2ffd9a761e3b54337e50d782b569 Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Tue, 17 Sep 2024 11:38:04 -0700 Subject: [PATCH 66/75] update cached mail labels to be stored per email account --- mail/app.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/mail/app.go b/mail/app.go index 90937e0e..64604a2e 100644 --- a/mail/app.go +++ b/mail/app.go @@ -65,7 +65,9 @@ type App struct { currentEmail string // labels are all of the possible labels that messages have. - labels map[string]bool + // The first key is the account for which the labels are stored, + // and the second key is for each label name. + labels map[string]map[string]bool // showLabel is the current label to show messages for. showLabel string @@ -83,7 +85,7 @@ func (a *App) Init() { a.Frame.Init() a.authToken = map[string]*oauth2.Token{} a.authClient = map[string]sasl.Client{} - a.labels = map[string]bool{} + a.labels = map[string]map[string]bool{} a.showLabel = "INBOX" a.Styler(func(s *styles.Style) { s.Grow.Set(1, 1) @@ -96,8 +98,9 @@ func (a *App) Init() { w.Maker(func(p *tree.Plan) { for _, email := range Settings.Accounts { tree.AddAt(p, email, func(w *core.Tree) { + a.labels[email] = map[string]bool{} w.Maker(func(p *tree.Plan) { - labels := maps.Keys(a.labels) + labels := maps.Keys(a.labels[email]) slices.Sort(labels) for _, label := range labels { tree.AddAt(p, label, func(w *core.Tree) { @@ -121,7 +124,7 @@ func (a *App) Init() { mp := a.cache[a.currentEmail] for _, cd := range mp { for _, label := range cd.Labels { - a.labels[label.Name] = true + a.labels[a.currentEmail][label.Name] = true if label.Name == a.showLabel { a.listCache = append(a.listCache, cd) break From 29d243b204c59a4fc57f66b70a9f0140ba44d5d6 Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Tue, 17 Sep 2024 11:44:16 -0700 Subject: [PATCH 67/75] add mail selectMailbox with selectedMailbox cache to avoid unnecessary mailbox selection --- mail/actions.go | 4 ++-- mail/app.go | 18 ++++++++++++++++++ mail/cache.go | 4 ++-- 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/mail/actions.go b/mail/actions.go index dd139813..b95cf539 100644 --- a/mail/actions.go +++ b/mail/actions.go @@ -35,9 +35,9 @@ func (a *App) action(f func(c *imapclient.Client)) { func (a *App) actionLabels(f func(c *imapclient.Client, label Label)) { a.action(func(c *imapclient.Client) { for _, label := range a.readMessage.Labels { - _, err := c.Select(label.Name, nil).Wait() + err := a.selectMailbox(c, a.currentEmail, label.Name) if err != nil { - core.ErrorSnackbar(a, err, "Error selecting mailbox") + core.ErrorSnackbar(a, err) return } f(c, label) diff --git a/mail/app.go b/mail/app.go index 64604a2e..3455c2a2 100644 --- a/mail/app.go +++ b/mail/app.go @@ -9,6 +9,7 @@ package mail import ( "cmp" + "fmt" "slices" "sync" @@ -64,6 +65,9 @@ type App struct { // currentEmail is the current email account. currentEmail string + // selectedMailbox is the currently selected mailbox for each email account in IMAP. + selectedMailbox map[string]string + // labels are all of the possible labels that messages have. // The first key is the account for which the labels are stored, // and the second key is for each label name. @@ -198,3 +202,17 @@ func (a *App) GetMail() error { }() return nil } + +// selectMailbox selects the given mailbox for the given email for the given client. +// It does nothing if the given mailbox is already selected. +func (a *App) selectMailbox(c *imapclient.Client, email string, mailbox string) error { + if a.selectedMailbox[email] == mailbox { + return nil // already selected + } + _, err := c.Select(mailbox, nil).Wait() + if err != nil { + return fmt.Errorf("selecting mailbox: %w", err) + } + a.selectedMailbox[email] = mailbox + return nil +} diff --git a/mail/cache.go b/mail/cache.go index 991c84eb..9b245ac5 100644 --- a/mail/cache.go +++ b/mail/cache.go @@ -153,9 +153,9 @@ func (a *App) CacheMessagesForMailbox(c *imapclient.Client, email string, mailbo } a.cache[email] = cached - _, err = c.Select(mailbox, nil).Wait() + err = a.selectMailbox(c, email, mailbox) if err != nil { - return fmt.Errorf("opening mailbox: %w", err) + return err } // We want messages in this mailbox with UIDs we haven't already cached. From 0f5caacbff5aa54d808f2d5e4375b64715c7b943 Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Tue, 17 Sep 2024 11:45:18 -0700 Subject: [PATCH 68/75] fix nil map panic --- mail/app.go | 1 + 1 file changed, 1 insertion(+) diff --git a/mail/app.go b/mail/app.go index 3455c2a2..91ebb428 100644 --- a/mail/app.go +++ b/mail/app.go @@ -89,6 +89,7 @@ func (a *App) Init() { a.Frame.Init() a.authToken = map[string]*oauth2.Token{} a.authClient = map[string]sasl.Client{} + a.selectedMailbox = map[string]string{} a.labels = map[string]map[string]bool{} a.showLabel = "INBOX" a.Styler(func(s *styles.Style) { From a10e77d49c84f38f730662cb1f6854108f6266ab Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Tue, 17 Sep 2024 11:49:27 -0700 Subject: [PATCH 69/75] reselect the mailbox for each caching round in case it has been changed by user actions; no-op if already selected --- mail/cache.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/mail/cache.go b/mail/cache.go index 9b245ac5..6609e79b 100644 --- a/mail/cache.go +++ b/mail/cache.go @@ -215,6 +215,13 @@ func (a *App) CacheUIDs(uids []imap.UID, c *imapclient.Client, email string, mai } a.imapMu[email].Lock() + // We must reselect the mailbox in case the user has changed it + // by doing actions in another mailbox. This is a no-op if it is + // already selected. + err := a.selectMailbox(c, email, mailbox) + if err != nil { + return err + } mcmd := c.Fetch(fuidset, fetchOptions) for { @@ -290,7 +297,7 @@ func (a *App) CacheUIDs(uids []imap.UID, c *imapclient.Client, email string, mai a.AsyncUnlock() } - err := mcmd.Close() + err = mcmd.Close() a.imapMu[email].Unlock() if err != nil { return fmt.Errorf("fetching messages: %w", err) From 5904a385405d8fd3aea787b13adfbc3355a77cf8 Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Tue, 17 Sep 2024 11:54:27 -0700 Subject: [PATCH 70/75] add cacheFilename function for mail --- mail/app.go | 7 +++++++ mail/cache.go | 16 +++++++--------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/mail/app.go b/mail/app.go index 91ebb428..9231b9bf 100644 --- a/mail/app.go +++ b/mail/app.go @@ -10,6 +10,7 @@ package mail import ( "cmp" "fmt" + "path/filepath" "slices" "sync" @@ -217,3 +218,9 @@ func (a *App) selectMailbox(c *imapclient.Client, email string, mailbox string) a.selectedMailbox[email] = mailbox return nil } + +// cacheFilename returns the filename for the cached messages JSON file +// for the given email address. +func (a *App) cacheFilename(email string) string { + return filepath.Join(core.TheApp.AppDataDir(), "caching", FilenameBase32(email), "cached-messages.json") +} diff --git a/mail/cache.go b/mail/cache.go index 6609e79b..1f373549 100644 --- a/mail/cache.go +++ b/mail/cache.go @@ -132,22 +132,20 @@ func (a *App) CacheMessagesForAccount(email string) error { // that have not already been cached for the given email account and mailbox. // It caches them in the app's data directory. func (a *App) CacheMessagesForMailbox(c *imapclient.Client, email string, mailbox string) error { - bemail := FilenameBase32(email) - - dir := filepath.Join(core.TheApp.AppDataDir(), "mail", bemail) + dir := filepath.Join(core.TheApp.AppDataDir(), "mail", FilenameBase32(email)) err := os.MkdirAll(string(dir), 0700) if err != nil { return err } - cachedFile := filepath.Join(core.TheApp.AppDataDir(), "caching", bemail, "cached-messages.json") - err = os.MkdirAll(filepath.Dir(cachedFile), 0700) + cacheFile := a.cacheFilename(email) + err = os.MkdirAll(filepath.Dir(cacheFile), 0700) if err != nil { return err } cached := map[string]*CacheData{} - err = jsonx.Open(&cached, cachedFile) + err = jsonx.Open(&cached, cacheFile) if err != nil && !errors.Is(err, fs.ErrNotExist) && !errors.Is(err, io.EOF) { return fmt.Errorf("opening cache list: %w", err) } @@ -188,14 +186,14 @@ func (a *App) CacheMessagesForMailbox(c *imapclient.Client, email string, mailbo a.AsyncUnlock() return nil } - return a.CacheUIDs(uids, c, email, mailbox, dir, cached, cachedFile) + return a.CacheUIDs(uids, c, email, mailbox, dir, cached, cacheFile) } // CacheUIDs caches the messages with the given UIDs in the context of the // other given values, using an iterative batched approach that fetches the // five next most recent messages at a time, allowing for concurrent mail // modifiation operations and correct ordering. -func (a *App) CacheUIDs(uids []imap.UID, c *imapclient.Client, email string, mailbox string, dir string, cached map[string]*CacheData, cachedFile string) error { +func (a *App) CacheUIDs(uids []imap.UID, c *imapclient.Client, email string, mailbox string, dir string, cached map[string]*CacheData, cacheFile string) error { for len(uids) > 0 { num := min(5, len(uids)) cuids := uids[len(uids)-num:] // the current batch of UIDs @@ -285,7 +283,7 @@ func (a *App) CacheUIDs(uids []imap.UID, c *imapclient.Client, email string, mai // We need to save the list of cached messages every time in case // we get interrupted or have an error. - err = jsonx.Save(&cached, cachedFile) + err = jsonx.Save(&cached, cacheFile) if err != nil { a.imapMu[email].Unlock() return fmt.Errorf("saving cache list: %w", err) From 4bfd741c869c9aecb72ac5c64d0a089fa19fc6dd Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Tue, 17 Sep 2024 11:59:56 -0700 Subject: [PATCH 71/75] automatically save the mail cache after each action is completed --- mail/actions.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mail/actions.go b/mail/actions.go index b95cf539..5ee2a26d 100644 --- a/mail/actions.go +++ b/mail/actions.go @@ -8,6 +8,7 @@ import ( "slices" "strings" + "cogentcore.org/core/base/iox/jsonx" "cogentcore.org/core/core" "github.com/emersion/go-imap/v2" "github.com/emersion/go-imap/v2/imapclient" @@ -16,6 +17,7 @@ import ( // action executes the given function in a goroutine with proper locking. // This should be used for any user action that interacts with a message in IMAP. +// It also automatically saves the cache after the action is completed. func (a *App) action(f func(c *imapclient.Client)) { // Use a goroutine to prevent GUI freezing and a double mutex deadlock // with a combination of the renderContext mutex and the imapMu. @@ -23,6 +25,8 @@ func (a *App) action(f func(c *imapclient.Client)) { mu := a.imapMu[a.currentEmail] mu.Lock() f(a.imapClient[a.currentEmail]) + err := jsonx.Save(a.cache[a.currentEmail], a.cacheFilename(a.currentEmail)) + core.ErrorSnackbar(a, err, "Error saving cache") mu.Unlock() a.AsyncLock() a.Update() From 1bf1b9d0053cf4bba59e6bcf0bb45a33c60c3f5c Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Tue, 17 Sep 2024 12:04:16 -0700 Subject: [PATCH 72/75] start on new label dialog --- mail/actions.go | 20 +++++++++++--------- mail/app.go | 2 +- mail/typegen.go | 5 ++--- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/mail/actions.go b/mail/actions.go index 5ee2a26d..afdbce73 100644 --- a/mail/actions.go +++ b/mail/actions.go @@ -49,16 +49,18 @@ func (a *App) actionLabels(f func(c *imapclient.Client, label Label)) { }) } -// Move moves the current message to the given mailbox. -func (a *App) Move(mailbox string) { //types:add +// Label opens a dialog for changing the labels (mailboxes) of the current message. +func (a *App) Label() { //types:add + d := core.NewBody("Label") + d.RunDialog(a) // TODO: Move needs to be redesigned with the new many-to-many labeling paradigm. - a.actionLabels(func(c *imapclient.Client, label Label) { - uidset := imap.UIDSet{} - uidset.AddNum(label.UID) - mc := c.Move(uidset, mailbox) - _, err := mc.Wait() - core.ErrorSnackbar(a, err, "Error moving message") - }) + // a.actionLabels(func(c *imapclient.Client, label Label) { + // uidset := imap.UIDSet{} + // uidset.AddNum(label.UID) + // mc := c.Move(uidset, mailbox) + // _, err := mc.Wait() + // core.ErrorSnackbar(a, err, "Error moving message") + // }) } // Reply opens a dialog to reply to the current message. diff --git a/mail/app.go b/mail/app.go index 9231b9bf..73b586b1 100644 --- a/mail/app.go +++ b/mail/app.go @@ -173,7 +173,7 @@ func (a *App) MakeToolbar(p *tree.Plan) { if a.readMessage != nil { tree.Add(p, func(w *core.Separator) {}) tree.Add(p, func(w *core.FuncButton) { - w.SetFunc(a.Move).SetIcon(icons.DriveFileMove).SetKey(keymap.Save) + w.SetFunc(a.Label).SetIcon(icons.DriveFileMove).SetKey(keymap.Save) }) tree.Add(p, func(w *core.FuncButton) { w.SetFunc(a.Reply).SetIcon(icons.Reply).SetKey(keymap.Replace) diff --git a/mail/typegen.go b/mail/typegen.go index c574e643..890f9fbc 100644 --- a/mail/typegen.go +++ b/mail/typegen.go @@ -3,13 +3,12 @@ package mail import ( - "net/mail" - "cogentcore.org/core/tree" "cogentcore.org/core/types" + "github.com/emersion/go-message/mail" ) -var _ = types.AddType(&types.Type{Name: "cogentcore.org/cogent/mail.App", IDName: "app", Doc: "App is an email client app.", Methods: []types.Method{{Name: "Move", Doc: "Move moves the current message to the given mailbox.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Args: []string{"mailbox"}}, {Name: "Reply", Doc: "Reply opens a dialog to reply to the current message.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "ReplyAll", Doc: "ReplyAll opens a dialog to reply to all people involved in the current message.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "Forward", Doc: "Forward opens a dialog to forward the current message to others.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "MarkAsRead", Doc: "MarkAsRead marks the current message as read.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "MarkAsUnread", Doc: "MarkAsUnread marks the current message as unread.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "Compose", Doc: "Compose opens a dialog to send a new message.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "Send", Doc: "Send sends the current message", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Returns: []string{"error"}}}, Embeds: []types.Field{{Name: "Frame"}}, Fields: []types.Field{{Name: "authToken", Doc: "authToken contains the [oauth2.Token] for each account."}, {Name: "authClient", Doc: "authClient contains the [sasl.Client] authentication for sending messages for each account."}, {Name: "imapClient", Doc: "imapClient contains the imap clients for each account."}, {Name: "imapMu", Doc: "imapMu contains the imap client mutexes for each account."}, {Name: "composeMessage", Doc: "composeMessage is the current message we are editing"}, {Name: "cache", Doc: "cache contains the cache data, keyed by account and then mailbox."}, {Name: "currentCache", Doc: "currentCache is [App.cache] for the current email account and mailbox."}, {Name: "readMessage", Doc: "readMessage is the current message we are reading"}, {Name: "readMessageReferences", Doc: "readMessageReferences is the References header of the current readMessage."}, {Name: "readMessagePlain", Doc: "readMessagePlain is the plain text body of the current readMessage."}, {Name: "currentEmail", Doc: "The current email account"}, {Name: "currentMailbox", Doc: "The current mailbox"}}}) +var _ = types.AddType(&types.Type{Name: "cogentcore.org/cogent/mail.App", IDName: "app", Doc: "App is an email client app.", Methods: []types.Method{{Name: "Label", Doc: "Label opens a dialog for changing the labels (mailboxes) of the current message.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "Reply", Doc: "Reply opens a dialog to reply to the current message.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "ReplyAll", Doc: "ReplyAll opens a dialog to reply to all people involved in the current message.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "Forward", Doc: "Forward opens a dialog to forward the current message to others.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "MarkAsRead", Doc: "MarkAsRead marks the current message as read.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "MarkAsUnread", Doc: "MarkAsUnread marks the current message as unread.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "Compose", Doc: "Compose opens a dialog to send a new message.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "Send", Doc: "Send sends the current message", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Returns: []string{"error"}}}, Embeds: []types.Field{{Name: "Frame"}}, Fields: []types.Field{{Name: "authToken", Doc: "authToken contains the [oauth2.Token] for each account."}, {Name: "authClient", Doc: "authClient contains the [sasl.Client] authentication for sending messages for each account."}, {Name: "imapClient", Doc: "imapClient contains the imap clients for each account."}, {Name: "imapMu", Doc: "imapMu contains the imap client mutexes for each account."}, {Name: "composeMessage", Doc: "composeMessage is the current message we are editing"}, {Name: "cache", Doc: "cache contains the cached message data, keyed by account and then MessageID."}, {Name: "listCache", Doc: "listCache is a sorted view of [App.cache] for the current email account\nand labels, used for displaying a [core.List] of messages. It should not\nbe used for any other purpose."}, {Name: "readMessage", Doc: "readMessage is the current message we are reading"}, {Name: "readMessageReferences", Doc: "readMessageReferences is the References header of the current readMessage."}, {Name: "readMessagePlain", Doc: "readMessagePlain is the plain text body of the current readMessage."}, {Name: "currentEmail", Doc: "currentEmail is the current email account."}, {Name: "selectedMailbox", Doc: "selectedMailbox is the currently selected mailbox for each email account in IMAP."}, {Name: "labels", Doc: "labels are all of the possible labels that messages have.\nThe first key is the account for which the labels are stored,\nand the second key is for each label name."}, {Name: "showLabel", Doc: "showLabel is the current label to show messages for."}}}) // NewApp returns a new [App] with the given optional parent: // App is an email client app. From 30d980d2bb155d7ad85ec48fee3fabfc8c7d214b Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Tue, 17 Sep 2024 12:06:11 -0700 Subject: [PATCH 73/75] more work on mail labeling dialog --- mail/actions.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/mail/actions.go b/mail/actions.go index afdbce73..3d492a33 100644 --- a/mail/actions.go +++ b/mail/actions.go @@ -52,6 +52,11 @@ func (a *App) actionLabels(f func(c *imapclient.Client, label Label)) { // Label opens a dialog for changing the labels (mailboxes) of the current message. func (a *App) Label() { //types:add d := core.NewBody("Label") + core.NewList(d).SetSlice(a.readMessage.Labels) + d.AddBottomBar(func(bar *core.Frame) { + d.AddCancel(bar) + d.AddOK(bar).SetText("Save") + }) d.RunDialog(a) // TODO: Move needs to be redesigned with the new many-to-many labeling paradigm. // a.actionLabels(func(c *imapclient.Client, label Label) { From 24f47a25b067ccbaf0bdcf61f5aab800644a3413 Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Tue, 17 Sep 2024 12:14:20 -0700 Subject: [PATCH 74/75] more label list gui work --- mail/actions.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/mail/actions.go b/mail/actions.go index 3d492a33..9cf4bdc2 100644 --- a/mail/actions.go +++ b/mail/actions.go @@ -10,6 +10,7 @@ import ( "cogentcore.org/core/base/iox/jsonx" "cogentcore.org/core/core" + "cogentcore.org/core/events" "github.com/emersion/go-imap/v2" "github.com/emersion/go-imap/v2/imapclient" "github.com/emersion/go-message/mail" @@ -52,7 +53,15 @@ func (a *App) actionLabels(f func(c *imapclient.Client, label Label)) { // Label opens a dialog for changing the labels (mailboxes) of the current message. func (a *App) Label() { //types:add d := core.NewBody("Label") - core.NewList(d).SetSlice(a.readMessage.Labels) + labels := make([]string, len(a.readMessage.Labels)) + for i, label := range a.readMessage.Labels { + labels[i] = label.Name + } + ch := core.NewChooser(d).SetEditable(true).SetAllowNew(true) + ch.OnChange(func(e events.Event) { + labels = append(labels, ch.CurrentItem.Value.(string)) + }) + core.NewList(d).SetSlice(&labels) d.AddBottomBar(func(bar *core.Frame) { d.AddCancel(bar) d.AddOK(bar).SetText("Save") From 4da7d8686557f0540879cdc8698c50959c6e5386 Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Tue, 17 Sep 2024 14:50:12 -0700 Subject: [PATCH 75/75] update view tags to display in marbles --- marbles/graph.go | 16 ++++++++-------- marbles/settings.go | 6 +++--- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/marbles/graph.go b/marbles/graph.go index d0239ab9..32ff1304 100644 --- a/marbles/graph.go +++ b/marbles/graph.go @@ -84,28 +84,28 @@ type Params struct { //types:add MarbleStartY Expr // Starting horizontal velocity of the marbles - StartVelocityY Param `view:"inline" label:"Starting velocity y"` + StartVelocityY Param `display:"inline" label:"Starting velocity y"` // Starting vertical velocity of the marbles - StartVelocityX Param `view:"inline" label:"Starting velocity x"` + StartVelocityX Param `display:"inline" label:"Starting velocity x"` // how fast to move along velocity vector -- lower = smoother, more slow-mo - UpdateRate Param `view:"inline"` + UpdateRate Param `display:"inline"` // how fast time increases - TimeStep Param `view:"inline"` + TimeStep Param `display:"inline"` // how fast it accelerates down - YForce Param `view:"inline" label:"Y force (Gravity)"` + YForce Param `display:"inline" label:"Y force (Gravity)"` // how fast the marbles move side to side without collisions, set to 0 for no movement - XForce Param `view:"inline" label:"X force (Wind)"` + XForce Param `display:"inline" label:"X force (Wind)"` // the center point of the graph, x - CenterX Param `view:"inline" label:"Graph center x"` + CenterX Param `display:"inline" label:"Graph center x"` // the center point of the graph, y - CenterY Param `view:"inline" label:"Graph center y"` + CenterY Param `display:"inline" label:"Graph center y"` TrackingSettings TrackingSettings } diff --git a/marbles/settings.go b/marbles/settings.go index 75c69d49..8cd2904c 100644 --- a/marbles/settings.go +++ b/marbles/settings.go @@ -10,11 +10,11 @@ import ( // Settings are the settings the app has type Settings struct { - LineDefaults LineDefaults `view:"no-inline" label:"Line Defaults"` + LineDefaults LineDefaults `display:"no-inline" label:"Line Defaults"` - GraphDefaults Params `view:"no-inline" label:"Graph Param Defaults"` + GraphDefaults Params `display:"no-inline" label:"Graph Param Defaults"` - MarbleSettings MarbleSettings `view:"inline" label:"Marble Settings"` + MarbleSettings MarbleSettings `display:"inline" label:"Marble Settings"` GraphSize int `label:"Graph Size" min:"100" max:"800"`