Skip to content

Commit 95a57a2

Browse files
hkdblorduskordus
andauthored
v0.2.3 (#162)
* Add Czech (cs) translation (#143) * (#143) docs update + bump to v0.2.3-dev * Update zh and translation rules * composer del/backspace guard * Fixed detached composer system theme detection - #153 * Fixed launch flow - #154 * drag-n-drop * Added cross account copy/move mail - #108 * Auto-commit and draggable recipients #111 #85 * Fixed dark theme rendering - #155 * Unread count update after background sync * Bumped to v0.2.3 --------- Co-authored-by: lorduskordus <136916485+lorduskordus@users.noreply.github.com>
1 parent 8808a1d commit 95a57a2

54 files changed

Lines changed: 1873 additions & 182 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,21 @@
11
# CHANGELOG
22

3+
4+
**v0.2.3 - 05-14-2026**
5+
---
6+
7+
- Added Czech translation
8+
- Added drag-and-drop to move messages to another folder
9+
- Added cross account copy/move mail - [#108](https://github.com/hkdb/aerion/issues/108)
10+
- Added draggable recipients in composer - [#111](https://github.com/hkdb/aerion/issues/111)
11+
- Added auto-commit recipient on lost focus - [#85](https://github.com/hkdb/aerion/issues/85)
12+
- Added composer del/backspace guard to prevent accidental message delete
13+
- Fixed detached composer system theme detection - [#153](https://github.com/hkdb/aerion/issues/153)
14+
- Fixed launch flow - [#154](https://github.com/hkdb/aerion/issues/154)
15+
- Fixed dark theme rendering - [#155](https://github.com/hkdb/aerion/issues/155)
16+
- Added unread count update after background sync to ensure accuracy
17+
18+
319
**v0.2.2 - 05-09-2026**
420
---
521

README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -403,12 +403,19 @@ Special thanks to translation contributors for making Aerion more accessible:
403403

404404
<table>
405405
<tr>
406+
<td align="center">
407+
<a href="https://github.com/lorduskordus">
408+
<img src="https://github.com/lorduskordus.png" width="80"><br>
409+
<sub><b>lorduskordus</b></sub>
410+
</a><br>
411+
<sub>Čeština (cs)</sub>
412+
</td>
406413
<td align="center">
407414
<a href="https://github.com/freemans32">
408415
<img src="https://github.com/freemans32.png" width="80"><br>
409416
<sub><b>freemans32</b></sub>
410417
</a><br>
411-
<sub>French (fr)</sub>
418+
<sub>Français (fr)</sub>
412419
</td>
413420
</tr>
414421
</table>

app/actions.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,24 @@ func (a *App) MoveToFolder(messageIDs []string, destFolderID string) error {
342342
return fmt.Errorf("destination folder not found: %s", destFolderID)
343343
}
344344

345+
// Cross-account move: APPEND raw bytes to destination first, then route source
346+
// cleanup through the existing Trash() pipeline (which handles local DB, IMAP
347+
// COPY+EXPUNGE within source, folder counts, events, undo, Gmail labels).
348+
// Strict ordering: APPEND completes synchronously before Trash runs — if
349+
// APPEND fails, source stays untouched.
350+
if messages[0].AccountID != destFolder.AccountID {
351+
if err := a.copyMessagesAcrossAccounts(messages, destFolder); err != nil {
352+
return fmt.Errorf("cross-account move: append failed: %w", err)
353+
}
354+
_, trashErr := a.Trash(messageIDs)
355+
// Sync destination so appended messages get correct UIDs locally.
356+
go func() {
357+
defer recoverPanic("app.actions", "cross-account move dest sync")
358+
_ = a.SyncFolder(destFolder.AccountID, destFolder.ID)
359+
}()
360+
return trashErr
361+
}
362+
345363
// Group by source folder
346364
byFolder := make(map[string][]*message.Message)
347365
for _, m := range messages {
@@ -610,6 +628,28 @@ func (a *App) CopyToFolder(messageIDs []string, destFolderID string) error {
610628
return fmt.Errorf("destination folder not found: %s", destFolderID)
611629
}
612630

631+
// Cross-account copy: APPEND raw bytes to destination. Fire-and-forget to
632+
// match intra-account CopyToFolder's existing async behavior.
633+
if messages[0].AccountID != destFolder.AccountID {
634+
go func() {
635+
defer recoverPanic("app.actions", "cross-account copy")
636+
if err := a.copyMessagesAcrossAccounts(messages, destFolder); err != nil {
637+
log.Error().Err(err).
638+
Str("sourceAccountID", messages[0].AccountID).
639+
Str("destAccountID", destFolder.AccountID).
640+
Str("destFolderID", destFolder.ID).
641+
Msg("Cross-account copy failed")
642+
return
643+
}
644+
_ = a.SyncFolder(destFolder.AccountID, destFolder.ID)
645+
wailsRuntime.EventsEmit(a.ctx, "messages:copied", map[string]interface{}{
646+
"messageIds": messageIDs,
647+
"destFolderId": destFolder.ID,
648+
})
649+
}()
650+
return nil
651+
}
652+
613653
// Group by source folder
614654
byFolder := make(map[string][]*message.Message)
615655
for _, m := range messages {
@@ -681,6 +721,67 @@ func (a *App) copyMessagesToIMAP(messages []*message.Message, sourceFolderID str
681721
})
682722
}
683723

724+
// flagsForAppend maps a local message's flag fields to the IMAP flag slice
725+
// used by AppendMessage on cross-account copy/move.
726+
func flagsForAppend(m *message.Message) []goImap.Flag {
727+
flags := []goImap.Flag{}
728+
if m.IsRead {
729+
flags = append(flags, goImap.FlagSeen)
730+
}
731+
if m.IsStarred {
732+
flags = append(flags, goImap.FlagFlagged)
733+
}
734+
if m.IsAnswered {
735+
flags = append(flags, goImap.FlagAnswered)
736+
}
737+
if m.IsDraft {
738+
flags = append(flags, goImap.FlagDraft)
739+
}
740+
return flags
741+
}
742+
743+
// copyMessagesAcrossAccounts fetches each message's raw RFC822 bytes from the
744+
// source account's IMAP and APPENDs them to the destination account's IMAP.
745+
// Used by the cross-account branches of MoveToFolder and CopyToFolder.
746+
//
747+
// Synchronous: returns when every APPEND has succeeded. Caller is expected to
748+
// short-circuit on error so the source side is never touched after a partial
749+
// success/failure.
750+
func (a *App) copyMessagesAcrossAccounts(messages []*message.Message, destFolder *folder.Folder) error {
751+
log := logging.WithComponent("app.copyMessagesAcrossAccounts")
752+
753+
if len(messages) == 0 {
754+
return nil
755+
}
756+
757+
log.Info().
758+
Str("sourceAccountID", messages[0].AccountID).
759+
Str("destAccountID", destFolder.AccountID).
760+
Str("destFolder", destFolder.Path).
761+
Int("count", len(messages)).
762+
Msg("Starting cross-account append")
763+
764+
return a.withIMAPRetry(destFolder.AccountID, func(conn *imap.Client) error {
765+
if _, err := conn.SelectMailbox(a.ctx, destFolder.Path); err != nil {
766+
return fmt.Errorf("failed to select destination mailbox: %w", err)
767+
}
768+
for _, m := range messages {
769+
raw, err := a.syncEngine.FetchRawMessage(a.ctx, m.AccountID, m.FolderID, m.UID)
770+
if err != nil {
771+
return fmt.Errorf("failed to fetch raw message from source: %w", err)
772+
}
773+
if _, err := conn.AppendMessage(destFolder.Path, flagsForAppend(m), m.Date, raw); err != nil {
774+
return fmt.Errorf("failed to append to destination: %w", err)
775+
}
776+
}
777+
log.Info().
778+
Str("destFolder", destFolder.Path).
779+
Int("count", len(messages)).
780+
Msg("Cross-account append completed")
781+
return nil
782+
})
783+
}
784+
684785
// Archive moves messages to the Archive folder
685786
func (a *App) Archive(messageIDs []string) error {
686787
if len(messageIDs) == 0 {

app/app.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -635,6 +635,13 @@ func (a *App) BeforeClose(ctx context.Context) bool {
635635
return true
636636
}
637637

638+
// NotifyStartupComplete signals the desktop environment that startup is done.
639+
// Called from the frontend after WindowShow() so KDE/Plasma sees the placeholder
640+
// → real window handoff cleanly (avoiding the taskbar-icon flash from #154).
641+
func (a *App) NotifyStartupComplete() {
642+
platform.NotifyStartupComplete()
643+
}
644+
638645
// ShowWindow brings the window to the foreground from hidden/minimized state.
639646
// Used by single-instance activation, notification clicks, etc.
640647
func (a *App) ShowWindow() {

app/background.go

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,18 @@ func (a *App) initBackgroundSync(ctx context.Context) {
3939
"folderId": folderID,
4040
"error": err.Error(),
4141
})
42-
} else {
43-
wailsRuntime.EventsEmit(a.ctx, "folder:synced", map[string]interface{}{
44-
"accountId": accountID,
45-
"folderId": folderID,
42+
return
43+
}
44+
wailsRuntime.EventsEmit(a.ctx, "folder:synced", map[string]interface{}{
45+
"accountId": accountID,
46+
"folderId": folderID,
47+
})
48+
// Mirror the unread count to the sidebar so badges refresh after a
49+
// scheduled sync (the manual SyncFolder path emits this at
50+
// app/sync.go:110; the scheduler path was missing it).
51+
if folderObj, ferr := a.folderStore.Get(folderID); ferr == nil && folderObj != nil {
52+
wailsRuntime.EventsEmit(a.ctx, "folders:countsChanged", map[string]int{
53+
folderID: folderObj.UnreadCount,
4654
})
4755
}
4856
})

app/detached_composer.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,13 @@ func (c *ComposerApp) IsFlatpak() bool {
576576
return platform.IsFlatpak()
577577
}
578578

579+
// NotifyStartupComplete signals the desktop environment that startup is done.
580+
// Called from the frontend after WindowShow() so KDE/Plasma sees the placeholder
581+
// → real window handoff cleanly (avoiding the taskbar-icon flash from #154).
582+
func (c *ComposerApp) NotifyStartupComplete() {
583+
platform.NotifyStartupComplete()
584+
}
585+
579586
// GetOriginalMessage returns the original message for reply/forward.
580587
func (c *ComposerApp) GetOriginalMessage() (*message.Message, error) {
581588
if c.originalMessage != nil {

app/state.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ type AppInfo struct {
3636
func (a *App) GetAppInfo() AppInfo {
3737
return AppInfo{
3838
Name: "Aerion",
39-
Version: "0.2.2",
39+
Version: "0.2.3",
4040
Description: "An Open Source Lightweight E-Mail Client",
4141
Website: "https://github.com/hkdb/aerion",
4242
License: "Apache 2.0",

0 commit comments

Comments
 (0)