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

Major progress on Cogent Mail #354

Merged
merged 75 commits into from
Sep 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
11905a6
start on using core.List for mail messages
kkoreilly Sep 7, 2024
3d0d721
start on MessageListItem
kkoreilly Sep 7, 2024
58014d9
more attempts at getting message list working
kkoreilly Sep 7, 2024
6bd5d41
forgot that you need to use AddChild for core.Value Init child adding
kkoreilly Sep 7, 2024
3517ab5
add theApp
kkoreilly Sep 7, 2024
e35224d
improve mail list updating
kkoreilly Sep 7, 2024
3c1e25c
improve mail currentCache handling and improve naming
kkoreilly Sep 7, 2024
6472a60
move mailbox tree logic to maker
kkoreilly Sep 7, 2024
3bc0f27
start moving updateReadMessage logic to Updater
kkoreilly Sep 7, 2024
e6f62f0
only updateReadMessage when needed
kkoreilly Sep 7, 2024
ea44f54
remove old and unnecessary names for mail widgets
kkoreilly Sep 7, 2024
7a24e62
add mail list item context menu to list context menu for now
kkoreilly Sep 7, 2024
c255b7f
start on imapMu
kkoreilly Sep 7, 2024
d4a172d
add more imapMu coverage
kkoreilly Sep 7, 2024
9d59067
use a goroutine in mail MoveMessage to prevent deadlock; with that an…
kkoreilly Sep 7, 2024
74ad9f3
disable text wrap for mail.MessageListItem; prevents https://github.c…
kkoreilly Sep 8, 2024
39016f6
move MoveMessage to mail toolbar
kkoreilly Sep 8, 2024
df13b39
improve move icon and make message body grow
kkoreilly Sep 8, 2024
ecc7c80
support changing address in AddressTextField
kkoreilly Sep 8, 2024
ba0eadc
skip gmail mailboxes for now
kkoreilly Sep 9, 2024
101b2db
remove unnecessary constant SetSlice calling
kkoreilly Sep 9, 2024
a8d3e8a
start adding mail shortcuts
kkoreilly Sep 9, 2024
68d700e
add mail.App.compose
kkoreilly Sep 9, 2024
9db9cb4
start on mail reply
kkoreilly Sep 9, 2024
b1b18d4
set default To for reply in mail to From of original message
kkoreilly Sep 9, 2024
eb2ee8a
add support for In-Reply-To header
kkoreilly Sep 9, 2024
6297365
add support for message references
kkoreilly Sep 9, 2024
2ac2f5f
add Re: to reply subject in mail
kkoreilly Sep 10, 2024
0fcee7b
add mail.App.reply
kkoreilly Sep 10, 2024
7f37ce8
add mail ReplyAll
kkoreilly Sep 10, 2024
05a71b6
add readMessageReferences
kkoreilly Sep 10, 2024
f97eec7
start on original message quoting in mail reply
kkoreilly Sep 11, 2024
016eba6
add readMessagePlain to get closer to full reply quoting
kkoreilly Sep 11, 2024
27f3de8
more reply quoting work
kkoreilly Sep 11, 2024
a020752
add more message quoting for mail reply
kkoreilly Sep 12, 2024
68f832f
add more reply quoting in mail
kkoreilly Sep 12, 2024
4ccb9e6
start fetching and caching flags in mail
kkoreilly Sep 12, 2024
c3000ff
add indicator for unseen messages
kkoreilly Sep 12, 2024
5eafc33
set Peek to true to prevent the messages from being marked as read au…
kkoreilly Sep 12, 2024
75448e7
add MarkAsread and MarkAsUnread in mail; mark as read when clicking o…
kkoreilly Sep 12, 2024
20c15d5
start on mail forwarding
kkoreilly Sep 12, 2024
2a91629
if original message is from me, reply to original receiver, not me
kkoreilly Sep 13, 2024
30f6c61
finish logic for excluding ourself from mail reply receiver
kkoreilly Sep 13, 2024
af7deec
fully finish mail reply to ourself handling
kkoreilly Sep 13, 2024
430c39f
fix original sender fetching in mail reply
kkoreilly Sep 13, 2024
a204eff
use correct Fwd: prefix for forwarding in mail
kkoreilly Sep 13, 2024
c72387f
add correct front matter for mail forwarding
kkoreilly Sep 13, 2024
e5c1670
fix plain message handling logic in mail
kkoreilly Sep 13, 2024
2d8c13f
use 2 spaces to create markdown newlines for mail forwarding front ma…
kkoreilly Sep 13, 2024
b9fd126
fix the mail message writer Close() calls; this was causing the unexp…
kkoreilly Sep 13, 2024
028d210
store cached mail per account as a map to allow for easy many-to-many…
kkoreilly Sep 14, 2024
29a1bb9
use base32 MessageID with .eml extension for cached mail filename
kkoreilly Sep 14, 2024
850e536
add mail message labels
kkoreilly Sep 14, 2024
f7afc90
add more label logic and TODOs
kkoreilly Sep 14, 2024
3ecd314
fix cached map initialization
kkoreilly Sep 14, 2024
f246f22
clean up listCache naming
kkoreilly Sep 15, 2024
947f4e0
only save message content to a file if it is not already cached
kkoreilly Sep 15, 2024
7211b6a
store a UID for each label/mailbox a message is in
kkoreilly Sep 15, 2024
49e55af
start on App.action helper function in mail
kkoreilly Sep 15, 2024
25801c4
add actionLabels in mail
kkoreilly Sep 15, 2024
507d813
clean up mail label naming and start on better label listing
kkoreilly Sep 15, 2024
9fc0785
get labels tree working
kkoreilly Sep 15, 2024
86220c7
add initial friendlyLabelName to mail
kkoreilly Sep 15, 2024
cf381f9
update the cache in markSeen for mail in addition to IMAP
kkoreilly Sep 15, 2024
445142c
minor comment cleanup
kkoreilly Sep 15, 2024
22efacd
update cached mail labels to be stored per email account
kkoreilly Sep 17, 2024
29d243b
add mail selectMailbox with selectedMailbox cache to avoid unnecessar…
kkoreilly Sep 17, 2024
0f5caac
fix nil map panic
kkoreilly Sep 17, 2024
a10e77d
reselect the mailbox for each caching round in case it has been chang…
kkoreilly Sep 17, 2024
5904a38
add cacheFilename function for mail
kkoreilly Sep 17, 2024
4bfd741
automatically save the mail cache after each action is completed
kkoreilly Sep 17, 2024
1bf1b9d
start on new label dialog
kkoreilly Sep 17, 2024
30d980d
more work on mail labeling dialog
kkoreilly Sep 17, 2024
24f47a2
more label list gui work
kkoreilly Sep 17, 2024
4da7d86
update view tags to display in marbles
kkoreilly Sep 17, 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
192 changes: 180 additions & 12 deletions mail/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,188 @@
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"
"github.com/emersion/go-imap/v2/imapclient"
"github.com/emersion/go-message/mail"
)

// MoveMessage moves the current message to the given mailbox.
func (a *App) MoveMessage(mailbox string) error { //types:add
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)
return err
// 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.
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))
core.ErrorSnackbar(a, err, "Error saving cache")
mu.Unlock()
a.AsyncLock()
a.Update()
a.AsyncUnlock()
}()
}

// 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) {
for _, label := range a.readMessage.Labels {
err := a.selectMailbox(c, a.currentEmail, label.Name)
if err != nil {
core.ErrorSnackbar(a, err)
return
}
f(c, label)
}
})
}

// 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))
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")
})
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")
// })
}

// Reply opens a dialog to reply to the current message.
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)
}
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", 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", true)
}

// reply is the implementation of the email reply dialog,
// 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 {
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
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 := 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
// 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, ", ")
} 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)
}

// 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
}
a.actionLabels(func(c *imapclient.Client, label Label) {
uidset := imap.UIDSet{}
uidset.AddNum(label.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()
if err != nil {
core.ErrorSnackbar(a, err, "Error marking message as read")
return
}
// 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)
} else if !seen {
*flags = slices.DeleteFunc(*flags, func(flag imap.Flag) bool {
return flag == imap.FlagSeen
})
}
})
}
Loading
Loading