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.
236 lines
7.1 KiB
236 lines
7.1 KiB
// Copyright (c) 2022 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 ( |
|
"errors" |
|
"strings" |
|
"sync" |
|
|
|
imapquota "github.com/emersion/go-imap-quota" |
|
goIMAPBackend "github.com/emersion/go-imap/backend" |
|
"github.com/ljanyst/peroxide/pkg/pmapi" |
|
"github.com/ljanyst/peroxide/pkg/store" |
|
"github.com/ljanyst/peroxide/pkg/users" |
|
) |
|
|
|
var ( |
|
errNoSuchMailbox = errors.New("no such mailbox") //nolint[gochecknoglobals] |
|
) |
|
|
|
type imapUser struct { |
|
backend *imapBackend |
|
user *users.User |
|
|
|
storeUser *store.Store |
|
storeAddress *store.Address |
|
|
|
currentAddressLowercase string |
|
|
|
// Some clients, for example Outlook, do MOVE by STORE \Deleted, APPEND, |
|
// EXPUNGE where APPEN and EXPUNGE can go in parallel. Usual IMAP servers |
|
// do not deduplicate messages and this it's not an issue, but for APPEND |
|
// for PM means just assigning label. That would cause to assign label and |
|
// then delete the message, or in other words cause data loss. |
|
// go-imap does not call CreateMessage till it gets the whole message from |
|
// IMAP client, therefore with big message, simple wait for APPEND before |
|
// performing EXPUNGE is not enough. There has to be two-way lock. Only |
|
// that way even if EXPUNGE is called few ms before APPEND and message |
|
// is deleted, APPEND will not just assing label but creates the message |
|
// again. |
|
// The issue is only when moving message from folder which is causing |
|
// real removal, so Trash and Spam. Those only need to use the lock to |
|
// not cause huge slow down as EXPUNGE is implicitly called also after |
|
// UNSELECT, CLOSE, or LOGOUT. |
|
appendExpungeLock sync.Mutex |
|
} |
|
|
|
// newIMAPUser returns struct implementing go-imap/user interface. |
|
func newIMAPUser( |
|
backend *imapBackend, |
|
user *users.User, |
|
addressID, address string, |
|
) (*imapUser, error) { |
|
log.WithField("address", addressID).Debug("Creating new IMAP user") |
|
|
|
storeUser := user.GetStore() |
|
if storeUser == nil { |
|
return nil, errors.New("user database is not initialized") |
|
} |
|
|
|
storeAddress, err := storeUser.GetAddress(addressID) |
|
if err != nil { |
|
log.WithField("address", addressID).Debug("Could not get store user address") |
|
return nil, err |
|
} |
|
|
|
return &imapUser{ |
|
backend: backend, |
|
user: user, |
|
|
|
storeUser: storeUser, |
|
storeAddress: storeAddress, |
|
|
|
currentAddressLowercase: strings.ToLower(address), |
|
}, err |
|
} |
|
|
|
// This method should eventually no longer be necessary. Everything should go via store. |
|
func (iu *imapUser) client() pmapi.Client { |
|
return iu.user.GetClient() |
|
} |
|
|
|
func (iu *imapUser) isSubscribed(labelID string) bool { |
|
subscriptionExceptions := iu.backend.getCacheList(iu.storeUser.UserID(), SubscriptionException) |
|
exceptions := strings.Split(subscriptionExceptions, ";") |
|
|
|
for _, exception := range exceptions { |
|
if exception == labelID { |
|
return false |
|
} |
|
} |
|
return true |
|
} |
|
|
|
func (iu *imapUser) removeFromCache(label, value string) { |
|
iu.backend.removeFromCache(iu.storeUser.UserID(), label, value) |
|
} |
|
|
|
func (iu *imapUser) addToCache(label, value string) { |
|
iu.backend.addToCache(iu.storeUser.UserID(), label, value) |
|
} |
|
|
|
// Username returns this user's username. |
|
func (iu *imapUser) Username() string { |
|
return iu.storeAddress.AddressString() |
|
} |
|
|
|
// ListMailboxes returns a list of mailboxes belonging to this user. |
|
// If subscribed is set to true, returns only subscribed mailboxes. |
|
func (iu *imapUser) ListMailboxes(showOnlySubcribed bool) ([]goIMAPBackend.Mailbox, error) { |
|
mailboxes := []goIMAPBackend.Mailbox{} |
|
for _, storeMailbox := range iu.storeAddress.ListMailboxes() { |
|
if showOnlySubcribed && !iu.isSubscribed(storeMailbox.LabelID()) { |
|
continue |
|
} |
|
mailbox := newIMAPMailbox(iu, storeMailbox) |
|
mailboxes = append(mailboxes, mailbox) |
|
} |
|
|
|
mailboxes = append(mailboxes, newLabelsRootMailbox()) |
|
mailboxes = append(mailboxes, newFoldersRootMailbox()) |
|
|
|
log.WithField("mailboxes", mailboxes).Trace("Listing mailboxes") |
|
|
|
return mailboxes, nil |
|
} |
|
|
|
// GetMailbox returns a mailbox. If it doesn't exist, it returns ErrNoSuchMailbox. |
|
func (iu *imapUser) GetMailbox(name string) (mb goIMAPBackend.Mailbox, err error) { |
|
storeMailbox, err := iu.storeAddress.GetMailbox(name) |
|
if err != nil { |
|
logMsg := log.WithField("name", name).WithError(err) |
|
|
|
// GODT-97: some clients perform SELECT "" in order to unselect. |
|
// We don't want to fill the logs with errors in this case. |
|
if name != "" { |
|
logMsg.Error("Could not get mailbox") |
|
} else { |
|
logMsg.Debug("Failed attempt to get mailbox with empty name") |
|
} |
|
|
|
return |
|
} |
|
|
|
return newIMAPMailbox(iu, storeMailbox), nil |
|
} |
|
|
|
// CreateMailbox creates a new mailbox. |
|
func (iu *imapUser) CreateMailbox(name string) error { |
|
return iu.storeAddress.CreateMailbox(name) |
|
} |
|
|
|
// DeleteMailbox permanently removes the mailbox with the given name. |
|
func (iu *imapUser) DeleteMailbox(name string) (err error) { |
|
storeMailbox, err := iu.storeAddress.GetMailbox(name) |
|
if err != nil { |
|
log.WithField("name", name).WithError(err).Error("Could not get mailbox") |
|
return |
|
} |
|
|
|
return storeMailbox.Delete() |
|
} |
|
|
|
// RenameMailbox changes the name of a mailbox. It is an error to attempt to |
|
// rename a mailbox that does not exist or to rename a mailbox to a name that |
|
// already exists. |
|
func (iu *imapUser) RenameMailbox(oldName, newName string) (err error) { |
|
storeMailbox, err := iu.storeAddress.GetMailbox(oldName) |
|
if err != nil { |
|
log.WithField("name", oldName).WithError(err).Error("Could not get mailbox") |
|
return |
|
} |
|
|
|
return storeMailbox.Rename(newName) |
|
} |
|
|
|
// Logout is called when this User will no longer be used, likely because the |
|
// client closed the connection. |
|
func (iu *imapUser) Logout() (err error) { |
|
log.Debug("IMAP client logged out address ", iu.storeAddress.AddressID()) |
|
|
|
iu.backend.deleteUser(iu.currentAddressLowercase) |
|
|
|
return nil |
|
} |
|
|
|
func (iu *imapUser) GetQuota(name string) (*imapquota.Status, error) { |
|
usedSpace, maxSpace, err := iu.storeUser.GetSpaceKB() |
|
if err != nil { |
|
log.Error("Failed getting quota: ", err) |
|
return nil, err |
|
} |
|
|
|
resources := make(map[string][2]uint32) |
|
var list [2]uint32 |
|
list[0] = usedSpace |
|
list[1] = maxSpace |
|
resources[imapquota.ResourceStorage] = list |
|
status := &imapquota.Status{ |
|
Name: "", |
|
Resources: resources, |
|
} |
|
|
|
return status, nil |
|
} |
|
|
|
func (iu *imapUser) SetQuota(name string, resources map[string]uint32) error { |
|
return errors.New("quota cannot be set") |
|
} |
|
|
|
func (iu *imapUser) CreateMessageLimit() *uint32 { |
|
maxUpload, err := iu.storeUser.GetMaxUpload() |
|
if err != nil { |
|
log.Error("Failed getting current user for message limit: ", err) |
|
zero := uint32(0) |
|
return &zero |
|
} |
|
|
|
upload := uint32(maxUpload) |
|
return &upload |
|
}
|
|
|