Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

More Cogent Mail updates #357

Merged
merged 84 commits into from
Oct 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
84 commits
Select commit Hold shift + click to select a range
c165d01
start on tableLabel approach
kkoreilly Sep 18, 2024
67e7526
populate cached available labels directly in caching
kkoreilly Sep 18, 2024
bbf1396
use cached labels as chooser items for label dialog
kkoreilly Sep 18, 2024
c31d0e1
get mail label table updating working better
kkoreilly Sep 18, 2024
a7411c6
improve mail label text
kkoreilly Sep 18, 2024
83e5872
more friendly labeling for mail label dialog
kkoreilly Sep 18, 2024
983e746
get automatic chooser resetting after label adding working
kkoreilly Sep 18, 2024
e5b5633
more mail label dialog work
kkoreilly Sep 18, 2024
7d260c0
finish new label logic in mail
kkoreilly Sep 18, 2024
d052c33
start on label updating contains logic
kkoreilly Sep 18, 2024
114d80f
only create mail caching files in CacheMessagesForAccount instead of …
kkoreilly Sep 19, 2024
9cc6ea3
don't add new label in mail dialog if it is blank
kkoreilly Sep 19, 2024
85ee5c5
implement mail labeling copy conditions
kkoreilly Sep 20, 2024
5c44d6d
get message copying to new labels working in mail
kkoreilly Sep 20, 2024
a4d2ca6
implement expunge for old labels in mail label
kkoreilly Sep 20, 2024
2b8a7aa
update cache based on mail labeling
kkoreilly Sep 20, 2024
a36f0d9
try using OnClick in mail tree
kkoreilly Sep 20, 2024
0f21170
clean up old mail move code
kkoreilly Sep 20, 2024
84f15ba
rename CacheData to CacheMessage; much clearer
kkoreilly Sep 20, 2024
be4ec9d
use a separate resultantLabels copy of a.readMessage.Labels to preven…
kkoreilly Sep 20, 2024
b4c66e6
add after func for mail actionLabels to fix concurrency for resultant…
kkoreilly Sep 20, 2024
c64a988
add error if no labels are specified in mail
kkoreilly Sep 20, 2024
92d4165
start on text for current label name and number of messages
kkoreilly Sep 20, 2024
70359bc
rename cd to cm for CacheMessage local variables in mail
kkoreilly Sep 20, 2024
dea98a1
add unread messages count to mail
kkoreilly Sep 20, 2024
30b50d0
get initial mail attachment sending logic working
kkoreilly Sep 20, 2024
29e1886
use filepath.Base for attachment filename
kkoreilly Sep 20, 2024
9fa7f35
set Content-Type header for mail attachment; now can be viewed in oth…
kkoreilly Sep 21, 2024
53f87ef
start on attachment reading in mail
kkoreilly Sep 21, 2024
7012cc6
clarify read/display message naming
kkoreilly Sep 21, 2024
91fa685
add readMessageParsed to reduce ambiguity and clutter in mail
kkoreilly Sep 21, 2024
d2fb706
gather attachments list
kkoreilly Sep 21, 2024
d1cdf22
start on displaying mail attachments in message reading
kkoreilly Sep 21, 2024
21c9aec
start on AttachmentButton
kkoreilly Sep 21, 2024
a7db025
update mail generated code
kkoreilly Sep 21, 2024
45e7a17
improve AttachmentButton formatting
kkoreilly Sep 21, 2024
a9bd637
start on attachment data reading
kkoreilly Sep 21, 2024
60157a0
implement initial attachment downloading
kkoreilly Sep 21, 2024
e702cb8
use SoloFuncButton to prompt user for attachment download location in…
kkoreilly Sep 21, 2024
023bf8e
set default read mail download location to downloads folder with atta…
kkoreilly Sep 21, 2024
1a5d0d0
use temporary file and add AsyncLock protection for cache saving in m…
kkoreilly Sep 21, 2024
041ef54
make a backup of the old cache file in mail first for extra safety
kkoreilly Sep 21, 2024
a203910
Revert "make a backup of the old cache file in mail first for extra s…
kkoreilly Sep 21, 2024
ee8376b
add uniqute messageFilename logic
kkoreilly Sep 21, 2024
7d8a0b4
use unique filename for storing cached mail and save it in the cache …
kkoreilly Sep 21, 2024
35f6847
only display attachments in display message if they exist
kkoreilly Sep 21, 2024
1e04b24
only add criteria if there are UIDs; multi-mailbox mail download is f…
kkoreilly Sep 21, 2024
bd73ecd
use OnSelect for mailbox tree; now fully working with recent core cha…
kkoreilly Sep 21, 2024
b6c9404
stop doing Logout for now
kkoreilly Sep 22, 2024
82c3be8
move actual mail labeling logic into a separate function
kkoreilly Sep 22, 2024
0d4deaa
add Delete button to mail
kkoreilly Sep 22, 2024
18bd00a
remove gmail prefix from mail labels
kkoreilly Sep 22, 2024
a66e3b8
add temporary skipLabels for mail
kkoreilly Sep 22, 2024
33272e4
improve sent label formatting
kkoreilly Sep 22, 2024
4791072
implement initial mailbox tree icons
kkoreilly Sep 22, 2024
850c82f
use IconLeaf for label icons in mail tree
kkoreilly Sep 22, 2024
45b26e1
initial implementation of label tree nesting in mail
kkoreilly Sep 23, 2024
8358525
clean up makeLabelTree in mail
kkoreilly Sep 23, 2024
ca7dec7
recompute the friendly labels in every Updater/Maker in case they hav…
kkoreilly Sep 23, 2024
021d0c3
implement a cache cleaning function that clears removed message label…
kkoreilly Sep 25, 2024
3fa477b
unexport mail cache functions
kkoreilly Sep 25, 2024
8341180
use Tree.Icon instead of Tree.IconLeaf with new improved styling
kkoreilly Sep 25, 2024
3254377
cache mail messages in baches of the last 100 UIDs per mailbox
kkoreilly Sep 26, 2024
b49b76d
start on repeated mail caching
kkoreilly Sep 26, 2024
aa4f5bf
improve mutex protection for mail caching
kkoreilly Sep 26, 2024
3d26b60
unexport more things in mail
kkoreilly Sep 26, 2024
657c64d
start on mail syncFlags
kkoreilly Sep 26, 2024
4dec34c
get basic mail syncFlags working
kkoreilly Sep 27, 2024
a76af1d
add total messages text in mail
kkoreilly Sep 27, 2024
726a81b
use guard statement for label checking in mail listCache updater
kkoreilly Sep 27, 2024
0b5f9c0
only show downloaded messages text if not everything downloaded
kkoreilly Sep 27, 2024
ada4445
improve format for downloaded messages text
kkoreilly Sep 27, 2024
0aada19
start on conversation combination / threading in mail
kkoreilly Sep 27, 2024
c6e10dd
only add message to replies if it isn't already there in mail threading
kkoreilly Sep 27, 2024
0d10bfb
add 1 to number of replies in mail
kkoreilly Sep 27, 2024
e5c92ec
start on more extensible DisplayMessageFrame paradigm in mail
kkoreilly Sep 28, 2024
67878fd
get initial display of multiple DisplayMessageFrame widgets working f…
kkoreilly Sep 28, 2024
61bc402
get initial display of actual message contents of thread working
kkoreilly Sep 28, 2024
adbad2c
add separators between conversation messages
kkoreilly Sep 28, 2024
dbbc8bc
get better conversation thread determining working in Cogent Mail wit…
kkoreilly Sep 28, 2024
1bac5e9
add mail.CacheMessage.latestDate to sort messages in the list cache b…
kkoreilly Oct 2, 2024
73d3ad4
add isRead function such that a message is treated as unread if it or…
kkoreilly Oct 2, 2024
1d9a3ed
fold readMessageParsed into CacheMessage
kkoreilly Oct 2, 2024
620c9a7
use correct message for parsed info in DisplayMessageFrame in mail
kkoreilly Oct 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 129 additions & 37 deletions mail/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
package mail

import (
"fmt"
"slices"
"strings"

"cogentcore.org/core/base/iox/jsonx"
"cogentcore.org/core/core"
"cogentcore.org/core/events"
"github.com/emersion/go-imap/v2"
Expand All @@ -19,14 +19,16 @@ 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)) {
// It calls the function for the current a.readMessage and all of its replies.
func (a *App) action(f func(c *imapclient.Client) error) {
// 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(a.imapClient[a.currentEmail])
err := jsonx.Save(a.cache[a.currentEmail], a.cacheFilename(a.currentEmail))
err := f(a.imapClient[a.currentEmail])
core.ErrorSnackbar(a, err, "Error performing action")
err = a.saveCacheFile(a.cache[a.currentEmail], a.currentEmail)
core.ErrorSnackbar(a, err, "Error saving cache")
mu.Unlock()
a.AsyncLock()
Expand All @@ -36,45 +38,137 @@ 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, label Label)) {
a.action(func(c *imapclient.Client) {
// selecting the mailbox for each one first. It does so in a goroutine with proper
// locking. It takes an optional function to call while still in the protected
// goroutine after all of the labels have been processed.
func (a *App) actionLabels(f func(c *imapclient.Client, label Label) error, after ...func()) {
a.action(func(c *imapclient.Client) error {
for _, label := range a.readMessage.Labels {
err := a.selectMailbox(c, a.currentEmail, label.Name)
if err != nil {
core.ErrorSnackbar(a, err)
return
return err
}
err = f(c, label)
if err != nil {
return err
}
f(c, label)
}
if len(after) > 0 {
after[0]()
}
return nil
})
}

// tableLabel is used for displaying labels in a table
// for user selection.
type tableLabel struct {
name string // the true underlying name
On bool `display:"checkbox"`
Label string `edit:"-"` // the friendly label name
}

// Label opens a dialog for changing the labels (mailboxes) of the current message.
func (a *App) Label() { //types:add
d := core.NewBody("Label")
labels := make([]string, len(a.readMessage.Labels))
labels := make([]tableLabel, len(a.readMessage.Labels))
for i, label := range a.readMessage.Labels {
labels[i] = label.Name
labels[i] = tableLabel{name: label.Name, On: label.Name != "INBOX", Label: friendlyLabelName(label.Name)}
}
var tb *core.Table
ch := core.NewChooser(d).SetEditable(true).SetAllowNew(true)
for _, label := range a.labels[a.currentEmail] {
ch.Items = append(ch.Items, core.ChooserItem{Value: label, Text: friendlyLabelName(label)})
}
ch.OnChange(func(e events.Event) {
labels = append(labels, ch.CurrentItem.Value.(string))
labels = append(labels, tableLabel{name: ch.CurrentItem.Value.(string), On: true, Label: ch.CurrentItem.Text})
tb.Update()
})
core.NewList(d).SetSlice(&labels)
ch.OnFinal(events.Change, func(e events.Event) {
if ch.CurrentItem.Text == "" {
return
}
ch.CurrentItem = core.ChooserItem{}
ch.SetCurrentValue("")
})
tb = core.NewTable(d).SetSlice(&labels)
d.AddBottomBar(func(bar *core.Frame) {
d.AddCancel(bar)
d.AddOK(bar).SetText("Save")
d.AddOK(bar).SetText("Save").OnClick(func(e events.Event) {
newLabels := []string{}
for _, label := range labels {
if label.On {
newLabels = append(newLabels, label.name)
}
}
if len(newLabels) == 0 {
core.ErrorSnackbar(a, fmt.Errorf("specify at least one label"))
return
}
a.label(newLabels)
})
})
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")
// })
}

// label changes the labels of the current message to the given labels.
// newLabels are the labels we want to end up with, in contrast
// to the old labels we started with, which are a.readMessage.Labels.
func (a *App) label(newLabels []string) {
// resultantLabels are the labels we apply to a.readMessage.Labels after
// the process is over. This needs to be a copy of a.readMessage.Labels
// since we can't modify it while looping over it and checking it.
resultantLabels := make([]Label, len(a.readMessage.Labels))
copy(resultantLabels, a.readMessage.Labels)
first := true
a.actionLabels(func(c *imapclient.Client, label Label) error {
// We copy the existing message to all of the new labels.
if first {
first = false
for _, newLabel := range newLabels {
if slices.ContainsFunc(a.readMessage.Labels, func(label Label) bool {
return label.Name == newLabel
}) {
continue // Already have this label.
}
cd, err := c.Copy(label.UIDSet(), newLabel).Wait()
if err != nil {
return err
}
// Add this new label to the cache.
resultantLabels = append(resultantLabels, Label{newLabel, cd.DestUIDs[0].Start})
}
}
// We remove the existing message from each old label.
if slices.Contains(newLabels, label.Name) {
return nil // Still have this label.
}
err := c.Store(label.UIDSet(), &imap.StoreFlags{
Op: imap.StoreFlagsAdd,
Silent: true,
Flags: []imap.Flag{imap.FlagDeleted},
}, nil).Wait()
if err != nil {
return err
}
err = c.UIDExpunge(label.UIDSet()).Wait()
if err != nil {
return err
}
// Remove this old label from the cache.
resultantLabels = slices.DeleteFunc(resultantLabels, func(l Label) bool {
return l == label
})
return nil
}, func() {
// Now that we are done, we can save resultantLabels to the cache.
a.readMessage.Labels = resultantLabels
})
}

// Delete moves the current message to the trash.
func (a *App) Delete() { //types:add
a.label([]string{"[Gmail]/Trash"}) // TODO: support other trash mailboxes
}

// Reply opens a dialog to reply to the current message.
Expand Down Expand Up @@ -125,7 +219,7 @@ func (a *App) reply(title string, forward bool) {
a.composeMessage.Subject = prefix + a.composeMessage.Subject
}
a.composeMessage.inReplyTo = a.readMessage.MessageID
a.composeMessage.references = append(a.readMessageReferences, a.readMessage.MessageID)
a.composeMessage.references = append(a.readMessage.parsed.references, a.readMessage.MessageID)
from := IMAPToMailAddresses(a.readMessage.From)[0].String()
date := a.readMessage.Date.Format("Mon, Jan 2, 2006 at 3:04 PM")
if forward {
Expand All @@ -143,7 +237,7 @@ func (a *App) reply(title string, forward bool) {
a.composeMessage.body = "\n\n> On " + date + ", " + from + " wrote:"
}
a.composeMessage.body += "\n>\n> "
a.composeMessage.body += strings.ReplaceAll(a.readMessagePlain, "\n", "\n> ")
a.composeMessage.body += strings.ReplaceAll(a.readMessage.parsed.plain, "\n", "\n> ")
a.compose(title)
}

Expand All @@ -159,34 +253,32 @@ func (a *App) MarkAsUnread() { //types:add

// 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 {
if a.readMessage.isRead() == seen {
// Already set correctly.
return
}
a.actionLabels(func(c *imapclient.Client, label Label) {
uidset := imap.UIDSet{}
uidset.AddNum(label.UID)
a.actionLabels(func(c *imapclient.Client, label Label) error {
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()
err := c.Store(label.UIDSet(), &imap.StoreFlags{
Op: op,
Silent: true,
Flags: []imap.Flag{imap.FlagSeen},
}, nil).Wait()
if err != nil {
core.ErrorSnackbar(a, err, "Error marking message as read")
return
return err
}
// Also directly update the cache:
flags := &a.cache[a.currentEmail][a.readMessage.MessageID].Flags
flags := &a.readMessage.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
})
}
return nil
})
}
Loading
Loading