feat(GODT-2366): Handle failed message updates as creates

This handles the following case:
- event says message was created
- we try to fetch the message but API says the doesn’t exist yet — we skip applying the “message created” update
- event then says message was updated at some point in the future
- we try to handle it but fail because we don’t have the message — we should treat it as a creation
This commit is contained in:
James Houlahan
2023-02-21 14:15:16 +01:00
parent 2bd8f6938a
commit 038eb6d243
3 changed files with 39 additions and 23 deletions

2
go.mod
View File

@ -5,7 +5,7 @@ go 1.18
require ( require (
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557 github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557
github.com/Masterminds/semver/v3 v3.1.1 github.com/Masterminds/semver/v3 v3.1.1
github.com/ProtonMail/gluon v0.14.2-0.20230221114509-a4e1a32c42b9 github.com/ProtonMail/gluon v0.14.2-0.20230221141542-0259255ec9ff
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a
github.com/ProtonMail/go-proton-api v0.4.1-0.20230217130533-0af5d2f08497 github.com/ProtonMail/go-proton-api v0.4.1-0.20230217130533-0af5d2f08497
github.com/ProtonMail/go-rfc5322 v0.11.0 github.com/ProtonMail/go-rfc5322 v0.11.0

4
go.sum
View File

@ -28,8 +28,8 @@ github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo= github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo=
github.com/ProtonMail/docker-credential-helpers v1.1.0 h1:+kvUIpwWcbtP3WFv5sSvkFn/XLzSqPOB5AAthuk9xPk= github.com/ProtonMail/docker-credential-helpers v1.1.0 h1:+kvUIpwWcbtP3WFv5sSvkFn/XLzSqPOB5AAthuk9xPk=
github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g= github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g=
github.com/ProtonMail/gluon v0.14.2-0.20230221114509-a4e1a32c42b9 h1:QEkeqblKjA+pslf/1oj8+Uu348NZXTTURT8ski9+a5s= github.com/ProtonMail/gluon v0.14.2-0.20230221141542-0259255ec9ff h1:uOaWgCaR+0XmwPTaSx9Cv5Z1lCto27bkuvNVur4A4I4=
github.com/ProtonMail/gluon v0.14.2-0.20230221114509-a4e1a32c42b9/go.mod h1:HYHr7hG7LPWI1S50M8NfHRb1kYi5B+Yu4/N/H+y+JUY= github.com/ProtonMail/gluon v0.14.2-0.20230221141542-0259255ec9ff/go.mod h1:HYHr7hG7LPWI1S50M8NfHRb1kYi5B+Yu4/N/H+y+JUY=
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a h1:D+aZah+k14Gn6kmL7eKxoo/4Dr/lK3ChBcwce2+SQP4= github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a h1:D+aZah+k14Gn6kmL7eKxoo/4Dr/lK3ChBcwce2+SQP4=
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4= github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo= github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo=

View File

@ -24,6 +24,7 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"github.com/ProtonMail/gluon"
"github.com/ProtonMail/gluon/imap" "github.com/ProtonMail/gluon/imap"
"github.com/ProtonMail/gluon/queue" "github.com/ProtonMail/gluon/queue"
"github.com/ProtonMail/gluon/reporter" "github.com/ProtonMail/gluon/reporter"
@ -420,7 +421,7 @@ func (user *User) handleMessageEvents(ctx context.Context, messageEvents []proto
switch event.Action { switch event.Action {
case proton.EventCreate: case proton.EventCreate:
updates, err := user.handleCreateMessageEvent(logging.WithLogrusField(ctx, "action", "create message"), event) updates, err := user.handleCreateMessageEvent(logging.WithLogrusField(ctx, "action", "create message"), event.Message)
if err != nil { if err != nil {
if rerr := user.reporter.ReportMessageWithContext("Failed to apply create message event", reporter.Context{ if rerr := user.reporter.ReportMessageWithContext("Failed to apply create message event", reporter.Context{
"error": err, "error": err,
@ -449,6 +450,7 @@ func (user *User) handleMessageEvents(ctx context.Context, messageEvents []proto
}); rerr != nil { }); rerr != nil {
user.log.WithError(err).Error("Failed to report update draft message event error") user.log.WithError(err).Error("Failed to report update draft message event error")
} }
return fmt.Errorf("failed to handle update draft event: %w", err) return fmt.Errorf("failed to handle update draft event: %w", err)
} }
@ -465,7 +467,7 @@ func (user *User) handleMessageEvents(ctx context.Context, messageEvents []proto
// Issue regular update to handle mailboxes and flag changes. // Issue regular update to handle mailboxes and flag changes.
updates, err := user.handleUpdateMessageEvent( updates, err := user.handleUpdateMessageEvent(
logging.WithLogrusField(ctx, "action", "update message"), logging.WithLogrusField(ctx, "action", "update message"),
event, event.Message,
) )
if err != nil { if err != nil {
if rerr := user.reporter.ReportMessageWithContext("Failed to apply update message event", reporter.Context{ if rerr := user.reporter.ReportMessageWithContext("Failed to apply update message event", reporter.Context{
@ -473,12 +475,25 @@ func (user *User) handleMessageEvents(ctx context.Context, messageEvents []proto
}); rerr != nil { }); rerr != nil {
user.log.WithError(err).Error("Failed to report update message event error") user.log.WithError(err).Error("Failed to report update message event error")
} }
return fmt.Errorf("failed to handle update message event: %w", err) return fmt.Errorf("failed to handle update message event: %w", err)
} }
// If the update fails on the gluon side because it doesn't exist, we try to create the message instead.
if err := waitOnIMAPUpdates(ctx, updates); gluon.IsNoSuchMessage(err) {
user.log.WithError(err).Error("Failed to handle update message event in gluon, will try creating it")
updates, err := user.handleCreateMessageEvent(ctx, event.Message)
if err != nil {
return fmt.Errorf("failed to handle update message event as create: %w", err)
}
if err := waitOnIMAPUpdates(ctx, updates); err != nil { if err := waitOnIMAPUpdates(ctx, updates); err != nil {
return err return err
} }
} else if err != nil {
return err
}
case proton.EventDelete: case proton.EventDelete:
updates, err := user.handleDeleteMessageEvent( updates, err := user.handleDeleteMessageEvent(
@ -491,6 +506,7 @@ func (user *User) handleMessageEvents(ctx context.Context, messageEvents []proto
}); rerr != nil { }); rerr != nil {
user.log.WithError(err).Error("Failed to report delete message event error") user.log.WithError(err).Error("Failed to report delete message event error")
} }
return fmt.Errorf("failed to handle delete message event: %w", err) return fmt.Errorf("failed to handle delete message event: %w", err)
} }
@ -503,17 +519,17 @@ func (user *User) handleMessageEvents(ctx context.Context, messageEvents []proto
return nil return nil
} }
func (user *User) handleCreateMessageEvent(ctx context.Context, event proton.MessageEvent) ([]imap.Update, error) { func (user *User) handleCreateMessageEvent(ctx context.Context, message proton.MessageMetadata) ([]imap.Update, error) {
user.log.WithFields(logrus.Fields{ user.log.WithFields(logrus.Fields{
"messageID": event.ID, "messageID": message.ID,
"subject": logging.Sensitive(event.Message.Subject), "subject": logging.Sensitive(message.Subject),
}).Info("Handling message created event") }).Info("Handling message created event")
full, err := user.client.GetFullMessage(ctx, event.Message.ID, newProtonAPIScheduler(), proton.NewDefaultAttachmentAllocator()) full, err := user.client.GetFullMessage(ctx, message.ID, newProtonAPIScheduler(), proton.NewDefaultAttachmentAllocator())
if err != nil { if err != nil {
// If the message is not found, it means that it has been deleted before we could fetch it. // If the message is not found, it means that it has been deleted before we could fetch it.
if apiErr := new(proton.APIError); errors.As(err, &apiErr) && apiErr.Status == http.StatusUnprocessableEntity { if apiErr := new(proton.APIError); errors.As(err, &apiErr) && apiErr.Status == http.StatusUnprocessableEntity {
user.log.WithField("messageID", event.Message.ID).Warn("Cannot create new message: full message is missing on API") user.log.WithField("messageID", message.ID).Warn("Cannot create new message: full message is missing on API")
return nil, nil return nil, nil
} }
@ -523,13 +539,13 @@ func (user *User) handleCreateMessageEvent(ctx context.Context, event proton.Mes
return safe.RLockRetErr(func() ([]imap.Update, error) { return safe.RLockRetErr(func() ([]imap.Update, error) {
var update imap.Update var update imap.Update
if err := withAddrKR(user.apiUser, user.apiAddrs[event.Message.AddressID], user.vault.KeyPass(), func(_, addrKR *crypto.KeyRing) error { if err := withAddrKR(user.apiUser, user.apiAddrs[message.AddressID], user.vault.KeyPass(), func(_, addrKR *crypto.KeyRing) error {
res := buildRFC822(user.apiLabels, full, addrKR, new(bytes.Buffer)) res := buildRFC822(user.apiLabels, full, addrKR, new(bytes.Buffer))
if res.err != nil { if res.err != nil {
user.log.WithError(err).Error("Failed to build RFC822 message") user.log.WithError(err).Error("Failed to build RFC822 message")
if err := user.vault.AddFailedMessageID(event.ID); err != nil { if err := user.vault.AddFailedMessageID(message.ID); err != nil {
user.log.WithError(err).Error("Failed to add failed message ID to vault") user.log.WithError(err).Error("Failed to add failed message ID to vault")
} }
@ -543,7 +559,7 @@ func (user *User) handleCreateMessageEvent(ctx context.Context, event proton.Mes
return nil return nil
} }
if err := user.vault.RemFailedMessageID(event.ID); err != nil { if err := user.vault.RemFailedMessageID(message.ID); err != nil {
user.log.WithError(err).Error("Failed to remove failed message ID from vault") user.log.WithError(err).Error("Failed to remove failed message ID from vault")
} }
@ -559,22 +575,22 @@ func (user *User) handleCreateMessageEvent(ctx context.Context, event proton.Mes
}, user.apiUserLock, user.apiAddrsLock, user.apiLabelsLock, user.updateChLock) }, user.apiUserLock, user.apiAddrsLock, user.apiLabelsLock, user.updateChLock)
} }
func (user *User) handleUpdateMessageEvent(ctx context.Context, event proton.MessageEvent) ([]imap.Update, error) { //nolint:unparam func (user *User) handleUpdateMessageEvent(ctx context.Context, message proton.MessageMetadata) ([]imap.Update, error) { //nolint:unparam
return safe.RLockRetErr(func() ([]imap.Update, error) { return safe.RLockRetErr(func() ([]imap.Update, error) {
user.log.WithFields(logrus.Fields{ user.log.WithFields(logrus.Fields{
"messageID": event.ID, "messageID": message.ID,
"subject": logging.Sensitive(event.Message.Subject), "subject": logging.Sensitive(message.Subject),
}).Info("Handling message updated event") }).Info("Handling message updated event")
update := imap.NewMessageMailboxesUpdated( update := imap.NewMessageMailboxesUpdated(
imap.MessageID(event.ID), imap.MessageID(message.ID),
mapTo[string, imap.MailboxID](wantLabels(user.apiLabels, event.Message.LabelIDs)), mapTo[string, imap.MailboxID](wantLabels(user.apiLabels, message.LabelIDs)),
event.Message.Seen(), message.Seen(),
event.Message.Starred(), message.Starred(),
event.Message.IsDraft(), message.IsDraft(),
) )
user.updateCh[event.Message.AddressID].Enqueue(update) user.updateCh[message.AddressID].Enqueue(update)
return []imap.Update{update}, nil return []imap.Update{update}, nil
}, user.apiLabelsLock, user.updateChLock) }, user.apiLabelsLock, user.updateChLock)