all repos — emessage @ 54620e3a8954b11a15b400b7a6571158e4bfe37d

The EMessage email client

added idle cancel
Robert Ismo me@robertismo.com
Fri, 26 Jun 2026 15:31:45 -0500
commit

54620e3a8954b11a15b400b7a6571158e4bfe37d

parent

312f91c8b7e25ecd3446038beca95872642f3d45

M app.goapp.go

@@ -23,6 +23,8 @@ Config AppConfig

DB *gorm.DB UserContact Contact Updater *selfupdate.Updater + idleCtx context.Context + idleCancel context.CancelFunc } func NewApp(config AppConfig) *App {

@@ -44,6 +46,7 @@ Stdout()

defer app.Log("Startup finished."). Stdout() app.ctx = ctx + app.idleCtx, app.idleCancel = context.WithCancel(ctx) updater := &selfupdate.Updater{ CurrentVersion: app.GetVersion(), ApiURL: "https://updater.machinetele.com/",
M conversation.goconversation.go

@@ -376,9 +376,69 @@ Find(&messages).Error

if err != nil { return nil, err } - + + app.MarkConversationRead(conv) return &PaginatedMessages{ Messages: messages, Total: total, }, nil } + +func (app *App) MarkConversationRead(conv Conversation) error { + if err := app.DB.Preload("ServerRecipient").First(&conv, conv.ID).Error; err != nil { + return fmt.Errorf("conversation not found: %w", err) + } + + var unreadMessages []Message + if err := app.DB.Where("conversation_id = ? AND seen = ?", conv.ID, false).Find(&unreadMessages).Error; err != nil { + return fmt.Errorf("failed to query unread messages: %w", err) + } + + if len(unreadMessages) == 0 { + return app.DB.Model(&conv).Update("is_unread", false).Error + } + + connKey := conv.ServerRecipient.Value + conn, ok := mailConnections[connKey] + if !ok { + return fmt.Errorf("no active IMAP connection for %s", connKey) + } + + if _, err := conn.ImapClient.Select(AppMailbox, nil).Wait(); err != nil { + return fmt.Errorf("failed to select mailbox: %w", err) + } + + uids := make([]imap.UID, 0, len(unreadMessages)) + for _, msg := range unreadMessages { + uids = append(uids, imap.UID(msg.UID)) + } + uidSet := imap.UIDSetNum(uids...) + + if app.idleCancel != nil { + app.idleCancel() + } + ImapSyncMutex.Lock() + defer ImapSyncMutex.Unlock() + + storeFlags := &imap.StoreFlags{ + Op: imap.StoreFlagsAdd, + Flags: []imap.Flag{imap.FlagSeen}, + } + cmd := conn.ImapClient.Store(uidSet, storeFlags, &imap.StoreOptions{}) + + if _, err := cmd.Collect(); err != nil { + return fmt.Errorf("IMAP STORE failed: %w", err) + } + + for _, msg := range unreadMessages { + if err := app.DB.Model(&msg).Update("seen", true).Error; err != nil { + app.Log("Failed to mark message %d as seen: %v", msg.ID, err).Stdout() + } + } + + if err := app.DB.Model(&conv).Update("is_unread", false).Error; err != nil { + return fmt.Errorf("failed to update conversation read status: %w", err) + } + + return nil +}
M email.goemail.go

@@ -88,6 +88,9 @@ } else if err := conn.SmtpClient.SendMail(fromAddress.Value, toEmails, bytes.NewReader(msgBytes)); err != nil {

return nil, fmt.Errorf("SMTP send failed: %w", err) } + if app.idleCancel != nil { + app.idleCancel() + } ImapSyncMutex.Lock() defer ImapSyncMutex.Unlock() app.Log("Committing...").Stdout().StatusLine()
M frontend/src/lib/components/EnvelopeControl.sveltefrontend/src/lib/components/EnvelopeControl.svelte

@@ -13,7 +13,7 @@

const userContact = await GetUserContact(); const activeEmails = await GetActiveMailEmails(); - let recipients = $state(conversation.Recipients || []); + conversation.Recipients = conversation.Recipients || []; let recipientInfo = $state([]); $effect(async () => { if (global.conversation.Recipients) {

@@ -27,12 +27,12 @@ });

function add_recipient(email) { - if (recipients.some(r => r.ID === email.ID || r.Value === email.Value)) return; - recipients.push(email); + if (conversation.Recipients.some(r => r.ID === email.ID || r.Value === email.Value)) return; + conversation.Recipients.push(email); } function remove_recipient(index) { - recipients = recipients.filter((_, i) => i !== index); + conversation.Recipients = conversation.Recipients.filter((_, i) => i !== index); } function handle_from_select(event) {
M frontend/src/lib/wailsjs/go/main/App.d.tsfrontend/src/lib/wailsjs/go/main/App.d.ts

@@ -46,6 +46,8 @@ export function GetVersion():Promise<string>;

export function Log(arg1:string,arg2:Array<any>):Promise<main.LogMessage>; +export function MarkConversationRead(arg1:main.Conversation):Promise<void>; + export function NewContact():Promise<main.Contact>; export function NewConversation():Promise<main.Conversation>;
M frontend/src/lib/wailsjs/go/main/App.jsfrontend/src/lib/wailsjs/go/main/App.js

@@ -90,6 +90,10 @@ export function Log(arg1, arg2) {

return window['go']['main']['App']['Log'](arg1, arg2); } +export function MarkConversationRead(arg1) { + return window['go']['main']['App']['MarkConversationRead'](arg1); +} + export function NewContact() { return window['go']['main']['App']['NewContact'](); }
M mailaccount.gomailaccount.go

@@ -113,6 +113,9 @@ app.Updater.Update()

} func (app *App) EstablishMailConnections() { + if app.idleCancel != nil { + app.idleCancel() + } ImapSyncMutex.Lock() defer ImapSyncMutex.Unlock() _, _ = app.GetMailConnections()

@@ -416,11 +419,19 @@ }

} } } + func (app *App) watchMailbox(email string, conn MailAccountConnection) { mailbox := SourceMailboxes[0] for { + select { + case <-app.idleCtx.Done(): + app.Log("IDLE cancelled via context").Stdout() + return + default: + } + if !ImapSyncMutex.TryLock() { - time.Sleep(10 * time.Second) + time.Sleep(2 * time.Second) continue } if _, err := conn.ImapClient.Select(mailbox, nil).Wait(); err != nil {

@@ -434,10 +445,12 @@ app.Log("IDLE start failed: %v", err).Stdout()

ImapSyncMutex.Unlock() return } + waitChan := make(chan error, 1) go func() { waitChan <- idleCmd.Wait() }() + select { case err := <-waitChan: if err != nil {

@@ -445,12 +458,17 @@ app.Log("IDLE ended with error: %v", err).Stdout()

} idleCmd.Close() case <-time.After(1 * time.Minute): - app.Log("IDLE timeout – restarting", nil).Stdout() + app.Log("IDLE timeout – restarting").Stdout() + idleCmd.Close() + case <-app.idleCtx.Done(): + app.Log("IDLE forced stop").Stdout() idleCmd.Close() } + ImapSyncMutex.Unlock() if err := app.syncMessages(); err != nil { app.Log("Sync error: %v", err).Stdout() } - }} - + time.Sleep(2 * time.Second) + } +}
M releasesreleases

@@ -1,3 +1,4 @@

+prerelease.12 prerelease.11 prerelease.10 prerelease.9