Files
proton-bridge/internal/store/user_address.go
2020-04-09 14:03:43 +02:00

233 lines
6.1 KiB
Go

// 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
})
}