You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
232 lines
6.1 KiB
232 lines
6.1 KiB
// Copyright (c) 2020 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 ( |
|
"encoding/json" |
|
"fmt" |
|
|
|
"github.com/ProtonMail/proton-bridge/pkg/pmapi" |
|
"github.com/pkg/errors" |
|
bolt "go.etcd.io/bbolt" |
|
) |
|
|
|
// GetAddress returns the store address by given ID. |
|
func (store *Store) GetAddress(addressID string) (*Address, error) { |
|
store.lock.RLock() |
|
defer store.lock.RUnlock() |
|
|
|
storeAddress, ok := store.addresses[addressID] |
|
if !ok { |
|
return nil, fmt.Errorf("addressID %v does not exist", addressID) |
|
} |
|
|
|
return storeAddress, nil |
|
} |
|
|
|
// RebuildMailboxes truncates all mailbox buckets and recreates them from the metadata bucket again. |
|
func (store *Store) RebuildMailboxes() (err error) { |
|
store.lock.Lock() |
|
defer store.lock.Unlock() |
|
|
|
log.WithField("user", store.UserID()).Trace("Truncating mailboxes") |
|
|
|
if err = store.truncateMailboxesBucket(); err != nil { |
|
log.WithError(err).Error("Could not truncate mailboxes bucket") |
|
return |
|
} |
|
|
|
if err = store.truncateAddressInfoBucket(); err != nil { |
|
log.WithError(err).Error("Could not truncate address info bucket") |
|
return |
|
} |
|
|
|
if err = store.init(false); err != nil { |
|
log.WithError(err).Error("Could not init store") |
|
return |
|
} |
|
|
|
if err := store.increaseMailboxesVersion(); err != nil { |
|
log.WithError(err).Error("Could not increase structure version") |
|
// Do not return here. The truncation was already done and mode |
|
// was changed in DB so we need to sync so that users start to see |
|
// messages and not block other operations. |
|
} |
|
|
|
log.WithField("user", store.UserID()).Trace("Rebuilding mailboxes") |
|
return store.initMailboxesBucket() |
|
} |
|
|
|
// createOrDeleteAddressesEvent creates address objects in the store for each necessary address |
|
// and deletes any address objects that shouldn't be there. |
|
// It doesn't do anything to addresses that are rightfully there. |
|
// It should only be called from the event loop. |
|
func (store *Store) createOrDeleteAddressesEvent() (err error) { |
|
labels, err := store.initCounts() |
|
if err != nil { |
|
return errors.Wrap(err, "failed to initialise label counts") |
|
} |
|
|
|
addrInfo, err := store.GetAddressInfo() |
|
if err != nil { |
|
return errors.Wrap(err, "failed to get addresses and address IDs") |
|
} |
|
|
|
// We need at least one address to continue. |
|
if len(addrInfo) < 1 { |
|
return errors.New("no addresses to initialise") |
|
} |
|
|
|
// If in combined mode, we only need the user's primary address. |
|
if store.addressMode == combinedMode { |
|
addrInfo = addrInfo[:1] |
|
} |
|
|
|
// Go through all addresses that *should* be there. |
|
for _, addr := range addrInfo { |
|
if _, ok := store.addresses[addr.AddressID]; ok { |
|
continue |
|
} |
|
|
|
// This address is missing so we create it. |
|
if err = store.addAddress(addr.Address, addr.AddressID, labels); err != nil { |
|
return errors.Wrap(err, "failed to add address to store") |
|
} |
|
} |
|
|
|
// Go through all addresses that *should not* be there. |
|
for _, addr := range store.addresses { |
|
belongs := false |
|
|
|
for _, a := range addrInfo { |
|
if addr.addressID == a.AddressID { |
|
belongs = true |
|
break |
|
} |
|
} |
|
|
|
if belongs { |
|
continue |
|
} |
|
|
|
delete(store.addresses, addr.addressID) |
|
} |
|
|
|
return err |
|
} |
|
|
|
// truncateAddressInfoBucket removes the address info bucket. |
|
func (store *Store) truncateAddressInfoBucket() (err error) { |
|
log.Trace("Truncating address info bucket") |
|
|
|
tx := func(tx *bolt.Tx) (err error) { |
|
if err = tx.DeleteBucket(addressInfoBucket); err != nil { |
|
return |
|
} |
|
|
|
if _, err = tx.CreateBucketIfNotExists(addressInfoBucket); err != nil { |
|
return |
|
} |
|
|
|
return |
|
} |
|
|
|
return store.db.Update(tx) |
|
} |
|
|
|
// truncateMailboxesBucket removes the mailboxes bucket. |
|
func (store *Store) truncateMailboxesBucket() (err error) { |
|
log.Trace("Truncating mailboxes bucket") |
|
|
|
store.addresses = nil |
|
|
|
tx := func(tx *bolt.Tx) (err error) { |
|
mbs := tx.Bucket(mailboxesBucket) |
|
|
|
return mbs.ForEach(func(addrIDMailbox, _ []byte) (err error) { |
|
addr := mbs.Bucket(addrIDMailbox) |
|
|
|
if err = addr.DeleteBucket(imapIDsBucket); err != nil { |
|
return |
|
} |
|
|
|
if _, err = addr.CreateBucketIfNotExists(imapIDsBucket); err != nil { |
|
return |
|
} |
|
|
|
if err = addr.DeleteBucket(apiIDsBucket); err != nil { |
|
return |
|
} |
|
|
|
if _, err = addr.CreateBucketIfNotExists(apiIDsBucket); err != nil { |
|
return |
|
} |
|
|
|
return |
|
}) |
|
} |
|
|
|
return store.db.Update(tx) |
|
} |
|
|
|
// initMailboxesBucket recreates the mailboxes bucket from the metadata bucket. |
|
func (store *Store) initMailboxesBucket() error { |
|
return store.db.Update(func(tx *bolt.Tx) error { |
|
i := 0 |
|
msgs := []*pmapi.Message{} |
|
|
|
err := tx.Bucket(metadataBucket).ForEach(func(k, v []byte) error { |
|
msg := &pmapi.Message{} |
|
|
|
if err := json.Unmarshal(v, msg); err != nil { |
|
return err |
|
} |
|
msgs = append(msgs, msg) |
|
|
|
// Calling txCreateOrUpdateMessages does some overhead by iterating |
|
// all mailboxes, accessing buckets and so on. It's better to do in |
|
// batches instead of one by one (seconds vs hours for huge accounts). |
|
// Average size of metadata is 1k bytes, sometimes up to 2k bytes. |
|
// 10k messages will take about 20 MB of memory. |
|
i++ |
|
if i%10000 == 0 { |
|
store.log.WithField("i", i).Debug("Init mboxes heartbeat") |
|
|
|
for _, a := range store.addresses { |
|
if err := a.txCreateOrUpdateMessages(tx, msgs); err != nil { |
|
return err |
|
} |
|
} |
|
msgs = []*pmapi.Message{} |
|
} |
|
|
|
return nil |
|
}) |
|
if err != nil { |
|
return err |
|
} |
|
|
|
for _, a := range store.addresses { |
|
if err := a.txCreateOrUpdateMessages(tx, msgs); err != nil { |
|
return err |
|
} |
|
} |
|
|
|
return nil |
|
}) |
|
}
|
|
|