diff --git a/internal/user/events.go b/internal/user/events.go index 760d678d..b848a98e 100644 --- a/internal/user/events.go +++ b/internal/user/events.go @@ -217,7 +217,7 @@ func (user *User) handleCreateAddressEvent(ctx context.Context, event proton.Add // If the address is enabled, we need to hook it up to the update channels. switch user.vault.AddressMode() { case vault.CombinedMode: - primAddr, err := getAddrIdx(user.apiAddrs, 0) + primAddr, err := getPrimaryAddr(user.apiAddrs) if err != nil { return fmt.Errorf("failed to get primary address: %w", err) } @@ -276,7 +276,7 @@ func (user *User) handleUpdateAddressEvent(_ context.Context, event proton.Addre case oldAddr.Status != proton.AddressStatusEnabled && event.Address.Status == proton.AddressStatusEnabled: switch user.vault.AddressMode() { case vault.CombinedMode: - primAddr, err := getAddrIdx(user.apiAddrs, 0) + primAddr, err := getPrimaryAddr(user.apiAddrs) if err != nil { return fmt.Errorf("failed to get primary address: %w", err) } @@ -628,7 +628,14 @@ func (user *User) handleCreateMessageEvent(ctx context.Context, message proton.M } update = imap.NewMessagesCreated(false, res.update) - user.updateCh[full.AddressID].Enqueue(update) + didPublish, err := safePublishMessageUpdate(user, full.AddressID, update) + if err != nil { + return err + } + + if !didPublish { + update = nil + } return nil }); err != nil { @@ -674,7 +681,14 @@ func (user *User) handleUpdateMessageEvent(_ context.Context, message proton.Mes flags, ) - user.updateCh[message.AddressID].Enqueue(update) + didPublish, err := safePublishMessageUpdate(user, message.AddressID, update) + if err != nil { + return nil, err + } + + if !didPublish { + return nil, nil + } return []imap.Update{update}, nil }, user.apiLabelsLock, user.updateChLock) @@ -743,13 +757,24 @@ func (user *User) handleUpdateDraftEvent(ctx context.Context, event proton.Messa true, // Is the message doesn't exist, silently create it. ) - user.updateCh[full.AddressID].Enqueue(update) + didPublish, err := safePublishMessageUpdate(user, full.AddressID, update) + if err != nil { + return err + } + + if !didPublish { + update = nil + } return nil }); err != nil { return nil, err } + if update == nil { + return nil, nil + } + return []imap.Update{update}, nil }, user.apiUserLock, user.apiAddrsLock, user.apiLabelsLock, user.updateChLock) } @@ -816,3 +841,37 @@ func (user *User) reportErrorNoContextCancel(title string, err error, reportCont } } } + +// safePublishMessageUpdate handles the rare case where the address' update channel may have been deleted in the same +// event. This rare case can take place if in the same event fetch request there is an update for delete address and +// create/update message. +// If the user is in combined mode, we simply push the update to the primary address. If the user is in split mode +// we do not publish the update as the address no longer exists. +func safePublishMessageUpdate(user *User, addressID string, update imap.Update) (bool, error) { + v, ok := user.updateCh[addressID] + if !ok { + if user.GetAddressMode() == vault.CombinedMode { + primAddr, err := getPrimaryAddr(user.apiAddrs) + if err != nil { + return false, fmt.Errorf("failed to get primary address: %w", err) + } + primaryCh, ok := user.updateCh[primAddr.ID] + if !ok { + return false, fmt.Errorf("primary address channel is not available") + } + + primaryCh.Enqueue(update) + + return true, nil + } + + logrus.Warnf("Update channel not found for address %v, it may have been already deleted", addressID) + _ = user.reporter.ReportMessage("Message Update channel does not exist") + + return false, nil + } + + v.Enqueue(update) + + return true, nil +} diff --git a/internal/user/types.go b/internal/user/types.go index fc6a2286..67fbdf04 100644 --- a/internal/user/types.go +++ b/internal/user/types.go @@ -83,6 +83,18 @@ func getAddrIdx(apiAddrs map[string]proton.Address, idx int) (proton.Address, er return sorted[idx], nil } +func getPrimaryAddr(apiAddrs map[string]proton.Address) (proton.Address, error) { + sorted := sortSlice(maps.Values(apiAddrs), func(a, b proton.Address) bool { + return a.Order < b.Order + }) + + if len(sorted) == 0 { + return proton.Address{}, fmt.Errorf("no addresses available") + } + + return sorted[0], nil +} + // sortSlice returns the given slice sorted by the given comparator. func sortSlice[Item any](items []Item, less func(Item, Item) bool) []Item { sorted := make([]Item, len(items))