Files
proton-bridge/internal/imap/mailbox.go
James Houlahan 6bd0739013 GODT-1158: Store full messages bodies on disk
- GODT-1158: simple on-disk cache in store
- GODT-1158: better member naming in event loop
- GODT-1158: create on-disk cache during bridge setup
- GODT-1158: better job options
- GODT-1158: rename GetLiteral to GetRFC822
- GODT-1158: rename events -> currentEvents
- GODT-1158: unlock cache per-user
- GODT-1158: clean up cache after logout
- GODT-1158: randomized encrypted cache passphrase
- GODT-1158: Opt out of on-disk cache in settings
- GODT-1158: free space in cache
- GODT-1158: make tests compile
- GODT-1158: optional compression
- GODT-1158: cache custom location
- GODT-1158: basic capacity checker
- GODT-1158: cache free space config
- GODT-1158: only unlock cache if pmapi client is unlocked as well
- GODT-1158: simple background sync worker
- GODT-1158: set size/bodystructure when caching message
- GODT-1158: limit store db update blocking with semaphore
- GODT-1158: dumb 10-semaphore
- GODT-1158: properly handle delete; remove bad bodystructure handling
- GODT-1158: hacky fix for caching after logout... baaaaad
- GODT-1158: cache worker
- GODT-1158: compute body structure lazily
- GODT-1158: cache size in store
- GODT-1158: notify cacher when adding to store
- GODT-1158: 15 second store cache watcher
- GODT-1158: enable cacher
- GODT-1158: better cache worker starting/stopping
- GODT-1158: limit cacher to less concurrency than disk cache
- GODT-1158: message builder prio + pchan pkg
- GODT-1158: fix pchan, use in message builder
- GODT-1158: no sem in cacher (rely on message builder prio)
- GODT-1158: raise priority of existing jobs when requested
- GODT-1158: pending messages in on-disk cache
- GODT-1158: WIP just a note about deleting messages from disk cache
- GODT-1158: pending wait when trying to write
- GODT-1158: pending.add to return bool
- GODT-1225: Headers in bodystructure are stored as bytes.
- GODT-1158: fixing header caching
- GODT-1158: don't cache in background
- GODT-1158: all concurrency set in settings
- GODT-1158: worker pools inside message builder
- GODT-1158: fix linter issues
- GODT-1158: remove completed builds from builder
- GODT-1158: remove builder pool
- GODT-1158: cacher defer job done properly
- GODT-1158: fix linter
- GODT-1299: Continue with bodystructure build if deserialization failed
- GODT-1324: Delete messages from the cache when they are deleted on the server
- GODT-1158: refactor cache tests
- GODT-1158: move builder to app/bridge
- GODT-1306: Migrate cache on disk when location is changed (and delete when disabled)
2021-11-30 10:12:36 +01:00

244 lines
8.2 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 imap
import (
"strings"
"time"
"github.com/ProtonMail/proton-bridge/pkg/message"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/emersion/go-imap"
"github.com/sirupsen/logrus"
)
type imapMailbox struct {
panicHandler panicHandler
user *imapUser
name string
log *logrus.Entry
storeUser storeUserProvider
storeAddress storeAddressProvider
storeMailbox storeMailboxProvider
}
// newIMAPMailbox returns struct implementing go-imap/mailbox interface.
func newIMAPMailbox(panicHandler panicHandler, user *imapUser, storeMailbox storeMailboxProvider) *imapMailbox {
return &imapMailbox{
panicHandler: panicHandler,
user: user,
name: storeMailbox.Name(),
log: log.
WithField("addressID", user.storeAddress.AddressID()).
WithField("userID", user.storeUser.UserID()).
WithField("labelID", storeMailbox.LabelID()),
storeUser: user.storeUser,
storeAddress: user.storeAddress,
storeMailbox: storeMailbox,
}
}
// logCommand is helper to log commands requested by IMAP client with their
// params, result, and duration, but without private data.
// It's logged as INFO so it's logged for every user by default. This should
// help devs to find out reasons why clients, mostly Apple Mail, does re-sync.
// FETCH, APPEND, STORE, COPY, MOVE, and EXPUNGE should be using this helper.
func (im *imapMailbox) logCommand(callback func() error, cmd string, params ...interface{}) error {
start := time.Now()
err := callback()
// Not using im.log to not include addressID which is not needed in this case.
log.WithFields(logrus.Fields{
"userID": im.storeUser.UserID(),
"labelID": im.storeMailbox.LabelID(),
"duration": time.Since(start),
"err": err,
"params": params,
}).Info(cmd)
return err
}
// Name returns this mailbox name.
func (im *imapMailbox) Name() string {
// Called from go-imap in goroutines - we need to handle panics for each function.
defer im.panicHandler.HandlePanic()
return im.name
}
// Info returns this mailbox info.
func (im *imapMailbox) Info() (*imap.MailboxInfo, error) {
// Called from go-imap in goroutines - we need to handle panics for each function.
defer im.panicHandler.HandlePanic()
info := &imap.MailboxInfo{
Attributes: im.getFlags(),
Delimiter: im.storeMailbox.GetDelimiter(),
Name: im.name,
}
return info, nil
}
func (im *imapMailbox) getFlags() []string {
flags := []string{}
if !im.storeMailbox.IsFolder() || im.storeMailbox.IsSystem() {
flags = append(flags, imap.NoInferiorsAttr) // Subfolders are not supported for System or Label
}
switch im.storeMailbox.LabelID() {
case pmapi.SentLabel:
flags = append(flags, imap.SentAttr)
case pmapi.TrashLabel:
flags = append(flags, imap.TrashAttr)
case pmapi.SpamLabel:
flags = append(flags, imap.JunkAttr)
case pmapi.ArchiveLabel:
flags = append(flags, imap.ArchiveAttr)
case pmapi.AllMailLabel:
flags = append(flags, imap.AllAttr)
case pmapi.DraftLabel:
flags = append(flags, imap.DraftsAttr)
}
return flags
}
// Status returns this mailbox status. The fields Name, Flags and
// PermanentFlags in the returned MailboxStatus must be always populated. This
// function does not affect the state of any messages in the mailbox. See RFC
// 3501 section 6.3.10 for a list of items that can be requested.
//
// It always returns the state of DB (which could be different to server status).
// Additionally it checks that all stored numbers are same as in DB and polls events if needed.
func (im *imapMailbox) Status(items []imap.StatusItem) (*imap.MailboxStatus, error) {
// Called from go-imap in goroutines - we need to handle panics for each function.
defer im.panicHandler.HandlePanic()
l := log.WithField("status-label", im.storeMailbox.LabelID())
l.Data["user"] = im.storeUser.UserID()
l.Data["address"] = im.storeAddress.AddressID()
status := imap.NewMailboxStatus(im.name, items)
status.UidValidity = im.storeMailbox.UIDValidity()
status.Flags = []string{
imap.SeenFlag, strings.ToUpper(imap.SeenFlag),
imap.FlaggedFlag, strings.ToUpper(imap.FlaggedFlag),
imap.DeletedFlag, strings.ToUpper(imap.DeletedFlag),
imap.DraftFlag, strings.ToUpper(imap.DraftFlag),
message.AppleMailJunkFlag,
message.ThunderbirdJunkFlag,
message.ThunderbirdNonJunkFlag,
}
status.PermanentFlags = append([]string{}, status.Flags...)
dbTotal, dbUnread, dbUnreadSeqNum, err := im.storeMailbox.GetCounts()
l.WithFields(logrus.Fields{
"total": dbTotal,
"unread": dbUnread,
"unreadSeqNum": dbUnreadSeqNum,
"err": err,
}).Debug("DB counts")
if err == nil {
status.Messages = uint32(dbTotal)
status.Unseen = uint32(dbUnread)
status.UnseenSeqNum = uint32(dbUnreadSeqNum)
}
if status.UidNext, err = im.storeMailbox.GetNextUID(); err != nil {
return nil, err
}
return status, nil
}
// SetSubscribed adds or removes the mailbox to the server's set of "active"
// or "subscribed" mailboxes.
func (im *imapMailbox) SetSubscribed(subscribed bool) error {
// Called from go-imap in goroutines - we need to handle panics for each function.
defer im.panicHandler.HandlePanic()
label := im.storeMailbox.LabelID()
if subscribed && !im.user.isSubscribed(label) {
im.user.removeFromCache(SubscriptionException, label)
}
if !subscribed && im.user.isSubscribed(label) {
im.user.addToCache(SubscriptionException, label)
}
return nil
}
// Check requests a checkpoint of the currently selected mailbox. A checkpoint
// refers to any implementation-dependent housekeeping associated with the
// mailbox (e.g., resolving the server's in-memory state of the mailbox with
// the state on its disk). A checkpoint MAY take a non-instantaneous amount of
// real time to complete. If a server implementation has no such housekeeping
// considerations, CHECK is equivalent to NOOP.
func (im *imapMailbox) Check() error {
return nil
}
// Expunge permanently removes all messages that have the \Deleted flag set
// from the currently selected mailbox.
func (im *imapMailbox) Expunge() error {
// See comment of appendExpungeLock.
if im.storeMailbox.LabelID() == pmapi.TrashLabel || im.storeMailbox.LabelID() == pmapi.SpamLabel {
im.user.appendExpungeLock.Lock()
defer im.user.appendExpungeLock.Unlock()
}
return im.logCommand(im.expunge, "EXPUNGE")
}
func (im *imapMailbox) expunge() error {
im.user.backend.updates.block(im.user.currentAddressLowercase, im.name, operationDeleteMessage)
defer im.user.backend.updates.unblock(im.user.currentAddressLowercase, im.name, operationDeleteMessage)
return im.storeMailbox.RemoveDeleted(nil)
}
// UIDExpunge permanently removes messages that have the \Deleted flag set
// and UID passed from SeqSet from the currently selected mailbox.
func (im *imapMailbox) UIDExpunge(seqSet *imap.SeqSet) error {
return im.logCommand(func() error {
return im.uidExpunge(seqSet)
}, "UID EXPUNGE", seqSet)
}
func (im *imapMailbox) uidExpunge(seqSet *imap.SeqSet) error {
// See comment of appendExpungeLock.
if im.storeMailbox.LabelID() == pmapi.TrashLabel || im.storeMailbox.LabelID() == pmapi.SpamLabel {
im.user.appendExpungeLock.Lock()
defer im.user.appendExpungeLock.Unlock()
}
im.user.backend.updates.block(im.user.currentAddressLowercase, im.name, operationDeleteMessage)
defer im.user.backend.updates.unblock(im.user.currentAddressLowercase, im.name, operationDeleteMessage)
messageIDs, err := im.apiIDsFromSeqSet(true, seqSet)
if err != nil || len(messageIDs) == 0 {
return err
}
return im.storeMailbox.RemoveDeleted(messageIDs)
}
func (im *imapMailbox) ListQuotas() ([]string, error) {
return []string{""}, nil
}