mirror of
https://github.com/ProtonMail/proton-bridge.git
synced 2025-12-10 12:46:46 +00:00
259 lines
7.6 KiB
Go
259 lines
7.6 KiB
Go
// Copyright (c) 2021 Proton Technologies AG
|
|
//
|
|
// This file is part of ProtonMail Bridge.
|
|
//
|
|
// ProtonMail Bridge is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// ProtonMail Bridge is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
package store
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strconv"
|
|
|
|
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
|
"github.com/pkg/errors"
|
|
"github.com/sirupsen/logrus"
|
|
bolt "go.etcd.io/bbolt"
|
|
)
|
|
|
|
const syncFinishTimeKey = "sync_state" // The original key was sync_state and we want to keep compatibility.
|
|
const syncIDRangesKey = "id_ranges"
|
|
const syncIDsToBeDeletedKey = "ids_to_be_deleted"
|
|
|
|
// updateCountsFromServer will download and set the counts.
|
|
func (store *Store) updateCountsFromServer() error {
|
|
counts, err := store.client().CountMessages(context.Background(), "")
|
|
if err != nil {
|
|
return errors.Wrap(err, "cannot update counts from server")
|
|
}
|
|
|
|
return store.createOrUpdateOnAPICounts(counts)
|
|
}
|
|
|
|
// isSynced checks whether DB counts are synced with provided counts from API.
|
|
func (store *Store) isSynced(countsOnAPI []*pmapi.MessagesCount) (bool, error) {
|
|
store.log.WithField("apiCounts", countsOnAPI).Debug("Checking whether store is synced")
|
|
|
|
// IMPORTANT: The countsOnAPI can contain duplicates due to event merge
|
|
// (ie one label can be present multiple times). It is important to
|
|
// process all counts before checking whether they are synced.
|
|
if err := store.createOrUpdateOnAPICounts(countsOnAPI); err != nil {
|
|
store.log.WithError(err).Error("Cannot update counts before check sync")
|
|
return false, err
|
|
}
|
|
|
|
allCounts, err := store.getOnAPICounts()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
store.lock.Lock()
|
|
defer store.lock.Unlock()
|
|
|
|
countsAreOK := true
|
|
for _, counts := range allCounts {
|
|
total, unread := uint(0), uint(0)
|
|
for _, address := range store.addresses {
|
|
mbox, err := address.getMailboxByID(counts.LabelID)
|
|
if err != nil {
|
|
return false, errors.Wrapf(
|
|
err,
|
|
"cannot find mailbox for address %q",
|
|
address.addressID,
|
|
)
|
|
}
|
|
|
|
mboxTot, mboxUnread, _, err := mbox.GetCounts()
|
|
if err != nil {
|
|
errW := errors.Wrap(err, "cannot count messages")
|
|
store.log.
|
|
WithError(errW).
|
|
WithField("label", counts.LabelID).
|
|
WithField("address", address.addressID).
|
|
Error("IsSynced failed")
|
|
return false, err
|
|
}
|
|
total += mboxTot
|
|
unread += mboxUnread
|
|
}
|
|
|
|
if total != counts.TotalOnAPI || unread != counts.UnreadOnAPI {
|
|
store.log.WithFields(logrus.Fields{
|
|
"label": counts.LabelID,
|
|
"db-total": total,
|
|
"db-unread": unread,
|
|
"api-total": counts.TotalOnAPI,
|
|
"api-unread": counts.UnreadOnAPI,
|
|
}).Warning("counts differ")
|
|
countsAreOK = false
|
|
}
|
|
}
|
|
|
|
return countsAreOK, nil
|
|
}
|
|
|
|
// triggerSync starts a sync of complete user by syncing All Mail mailbox.
|
|
// All Mail mailbox contains all messages, so we download all meta data needed
|
|
// to generate any address/mailbox IMAP UIDs.
|
|
// Sync state can be in three states:
|
|
// * Nothing in database. For example when user logs in for the first time.
|
|
// `triggerSync` will start full sync.
|
|
// * Database has syncIDRangesKey and syncIDsToBeDeletedKey keys with data.
|
|
// Sync is in progress or was interrupted. In later case when, `triggerSync`
|
|
// will continue where it left off.
|
|
// * Database has only syncStateKey with time when database was last synced.
|
|
// `triggerSync` will reset it and start full sync again.
|
|
func (store *Store) triggerSync() {
|
|
syncState := store.loadSyncState()
|
|
|
|
// We first clear the last sync state in case this sync fails.
|
|
syncState.clearFinishTime()
|
|
|
|
// We don't want sync to block.
|
|
go func() {
|
|
defer store.panicHandler.HandlePanic()
|
|
|
|
store.log.Debug("Store sync triggered")
|
|
|
|
store.lock.Lock()
|
|
|
|
if store.isSyncRunning {
|
|
store.lock.Unlock()
|
|
store.log.Info("Store sync is already ongoing")
|
|
return
|
|
}
|
|
|
|
if store.syncCooldown.isTooSoon() {
|
|
store.lock.Unlock()
|
|
store.log.Info("Skipping sync: store tries to resync too often")
|
|
return
|
|
}
|
|
|
|
store.isSyncRunning = true
|
|
store.lock.Unlock()
|
|
|
|
defer func() {
|
|
store.lock.Lock()
|
|
store.isSyncRunning = false
|
|
store.lock.Unlock()
|
|
}()
|
|
|
|
store.log.WithField("isIncomplete", syncState.isIncomplete()).Info("Store sync started")
|
|
|
|
err := syncAllMail(store.panicHandler, store, store.client(), syncState)
|
|
if err != nil {
|
|
log.WithError(err).Error("Store sync failed")
|
|
store.syncCooldown.increaseWaitTime()
|
|
return
|
|
}
|
|
|
|
store.syncCooldown.reset()
|
|
syncState.setFinishTime()
|
|
}()
|
|
}
|
|
|
|
// isSyncFinished returns whether the database has finished a sync.
|
|
func (store *Store) isSyncFinished() (isSynced bool) {
|
|
return store.loadSyncState().isFinished()
|
|
}
|
|
|
|
// loadSyncState loads information about sync from database.
|
|
// See `triggerSync` to learn more about possible states.
|
|
func (store *Store) loadSyncState() *syncState {
|
|
finishTime := int64(0)
|
|
idRanges := []*syncIDRange{}
|
|
idsToBeDeleted := []string{}
|
|
|
|
err := store.db.View(func(tx *bolt.Tx) (err error) {
|
|
b := tx.Bucket(syncStateBucket)
|
|
|
|
finishTimeByte := b.Get([]byte(syncFinishTimeKey))
|
|
if finishTimeByte != nil {
|
|
finishTime, err = strconv.ParseInt(string(finishTimeByte), 10, 64)
|
|
if err != nil {
|
|
store.log.WithError(err).Error("Failed to unmarshal sync IDs ranges")
|
|
}
|
|
}
|
|
|
|
idRangesData := b.Get([]byte(syncIDRangesKey))
|
|
if idRangesData != nil {
|
|
if err := json.Unmarshal(idRangesData, &idRanges); err != nil {
|
|
store.log.WithError(err).Error("Failed to unmarshal sync IDs ranges")
|
|
}
|
|
}
|
|
|
|
idsToBeDeletedData := b.Get([]byte(syncIDsToBeDeletedKey))
|
|
if idsToBeDeletedData != nil {
|
|
if err := json.Unmarshal(idsToBeDeletedData, &idsToBeDeleted); err != nil {
|
|
store.log.WithError(err).Error("Failed to unmarshal sync IDs to be deleted")
|
|
}
|
|
}
|
|
|
|
return
|
|
})
|
|
|
|
if err != nil {
|
|
store.log.WithError(err).Error("Failed to load sync state")
|
|
}
|
|
|
|
return newSyncState(store, finishTime, idRanges, idsToBeDeleted)
|
|
}
|
|
|
|
// saveSyncState saves information about sync to database.
|
|
// See `triggerSync` to learn more about possible states.
|
|
func (store *Store) saveSyncState(finishTime int64, idRanges []*syncIDRange, idsToBeDeleted []string) {
|
|
idRangesData, err := json.Marshal(idRanges)
|
|
if err != nil {
|
|
store.log.WithError(err).Error("Failed to marshall sync IDs ranges")
|
|
}
|
|
|
|
idsToBeDeletedData, err := json.Marshal(idsToBeDeleted)
|
|
if err != nil {
|
|
store.log.WithError(err).Error("Failed to marshall sync IDs to be deleted")
|
|
}
|
|
|
|
err = store.db.Update(func(tx *bolt.Tx) (err error) {
|
|
b := tx.Bucket(syncStateBucket)
|
|
if finishTime != 0 {
|
|
curTime := []byte(fmt.Sprintf("%v", finishTime))
|
|
if err := b.Put([]byte(syncFinishTimeKey), curTime); err != nil {
|
|
return err
|
|
}
|
|
if err := b.Delete([]byte(syncIDRangesKey)); err != nil {
|
|
return err
|
|
}
|
|
if err := b.Delete([]byte(syncIDsToBeDeletedKey)); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
if err := b.Delete([]byte(syncFinishTimeKey)); err != nil {
|
|
return err
|
|
}
|
|
if err := b.Put([]byte(syncIDRangesKey), idRangesData); err != nil {
|
|
return err
|
|
}
|
|
if err := b.Put([]byte(syncIDsToBeDeletedKey), idsToBeDeletedData); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
store.log.WithError(err).Error("Failed to set sync state")
|
|
}
|
|
}
|