1
0

We build too many walls and not enough bridges

This commit is contained in:
Jakub
2020-04-08 12:59:16 +02:00
commit 17f4d6097a
494 changed files with 62753 additions and 0 deletions

220
internal/imap/backend.go Normal file
View File

@ -0,0 +1,220 @@
// 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 imap provides IMAP server of the Bridge.
package imap
import (
"strings"
"sync"
"time"
imapid "github.com/ProtonMail/go-imap-id"
"github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/pkg/listener"
goIMAPBackend "github.com/emersion/go-imap/backend"
)
type panicHandler interface {
HandlePanic()
}
type imapBackend struct {
panicHandler panicHandler
bridge bridger
updates chan interface{}
eventListener listener.Listener
users map[string]*imapUser
usersLocker sync.Locker
lastMailClient imapid.ID
lastMailClientLocker sync.Locker
imapCache map[string]map[string]string
imapCachePath string
imapCacheLock *sync.RWMutex
}
// NewIMAPBackend returns struct implementing go-imap/backend interface.
func NewIMAPBackend(
panicHandler panicHandler,
eventListener listener.Listener,
cfg configProvider,
bridge *bridge.Bridge,
) *imapBackend { //nolint[golint]
bridgeWrap := newBridgeWrap(bridge)
backend := newIMAPBackend(panicHandler, cfg, bridgeWrap, eventListener)
// We want idle updates coming from bridge's updates channel (which in turn come
// from the bridge users' stores) to be sent to the imap backend's update channel.
backend.updates = bridge.GetIMAPUpdatesChannel()
go backend.monitorDisconnectedUsers()
return backend
}
func newIMAPBackend(
panicHandler panicHandler,
cfg configProvider,
bridge bridger,
eventListener listener.Listener,
) *imapBackend {
return &imapBackend{
panicHandler: panicHandler,
bridge: bridge,
updates: make(chan interface{}),
eventListener: eventListener,
users: map[string]*imapUser{},
usersLocker: &sync.Mutex{},
lastMailClient: imapid.ID{imapid.FieldName: clientNone},
lastMailClientLocker: &sync.Mutex{},
imapCachePath: cfg.GetIMAPCachePath(),
imapCacheLock: &sync.RWMutex{},
}
}
func (ib *imapBackend) getUser(address string) (*imapUser, error) {
ib.usersLocker.Lock()
defer ib.usersLocker.Unlock()
address = strings.ToLower(address)
imapUser, ok := ib.users[address]
if ok {
return imapUser, nil
}
return ib.createUser(address)
}
// createUser require that address MUST be in lowercase.
func (ib *imapBackend) createUser(address string) (*imapUser, error) {
log.WithField("address", address).Debug("Creating new IMAP user")
user, err := ib.bridge.GetUser(address)
if err != nil {
return nil, err
}
// Make sure you return the same user for all valid addresses when in combined mode.
if user.IsCombinedAddressMode() {
address = strings.ToLower(user.GetPrimaryAddress())
if combinedUser, ok := ib.users[address]; ok {
return combinedUser, nil
}
}
// Client can log in only using address so we can properly close all IMAP connections.
var addressID string
if addressID, err = user.GetAddressID(address); err != nil {
return nil, err
}
newUser, err := newIMAPUser(ib.panicHandler, ib, user, addressID, address)
if err != nil {
return nil, err
}
ib.users[address] = newUser
return newUser, nil
}
// deleteUser removes a user from the users map.
// This is a safe operation even if the user doesn't exist so it is no problem if it is done twice.
func (ib *imapBackend) deleteUser(address string) {
log.WithField("address", address).Debug("Deleting IMAP user")
ib.usersLocker.Lock()
defer ib.usersLocker.Unlock()
delete(ib.users, strings.ToLower(address))
}
// Login authenticates a user.
func (ib *imapBackend) Login(username, password string) (goIMAPBackend.User, error) {
// Called from go-imap in goroutines - we need to handle panics for each function.
defer ib.panicHandler.HandlePanic()
imapUser, err := ib.getUser(username)
if err != nil {
log.WithError(err).Warn("Cannot get user")
return nil, err
}
if err := imapUser.user.CheckBridgeLogin(password); err != nil {
log.WithError(err).Error("Could not check bridge password")
_ = imapUser.Logout()
// Apple Mail sometimes generates a lot of requests very quickly.
// It's therefore good to have a timeout after a bad login so that we can slow
// those requests down a little bit.
time.Sleep(10 * time.Second)
return nil, err
}
// The update channel should be nil until we try to login to IMAP for the first time
// so that it doesn't make bridge slow for users who are only using bridge for SMTP
// (otherwise the store will be locked for 1 sec per email during synchronization).
imapUser.user.SetIMAPIdleUpdateChannel()
return imapUser, nil
}
// Updates returns a channel of updates for IMAP IDLE extension.
func (ib *imapBackend) Updates() <-chan interface{} {
// Called from go-imap in goroutines - we need to handle panics for each function.
defer ib.panicHandler.HandlePanic()
return ib.updates
}
func (ib *imapBackend) CreateMessageLimit() *uint32 {
return nil
}
func (ib *imapBackend) setLastMailClient(id imapid.ID) {
ib.lastMailClientLocker.Lock()
defer ib.lastMailClientLocker.Unlock()
if name, ok := id[imapid.FieldName]; ok && ib.lastMailClient[imapid.FieldName] != name {
ib.lastMailClient = imapid.ID{}
for k, v := range id {
ib.lastMailClient[k] = v
}
log.Warn("Mail Client ID changed to ", ib.lastMailClient)
ib.bridge.SetCurrentClient(
ib.lastMailClient[imapid.FieldName],
ib.lastMailClient[imapid.FieldVersion],
)
}
}
// monitorDisconnectedUsers removes users when it receives a close connection event for them.
func (ib *imapBackend) monitorDisconnectedUsers() {
ch := make(chan string)
ib.eventListener.Add(events.CloseConnectionEvent, ch)
for address := range ch {
// delete the user to ensure future imap login attempts use the latest bridge user
// (bridge user might be removed-readded so we want to use the new bridge user object).
ib.deleteUser(address)
}
}

View File

@ -0,0 +1,136 @@
// 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 imap
import (
"encoding/json"
"errors"
"os"
"strings"
)
// Cache keys.
const (
SubscriptionException = "subscription_exceptions"
)
// addToCache adds item to existing item list.
// Starting from following structure:
// {
// "username": {"label": "item1;item2"}
// }
//
// After calling addToCache("username", "label", "newItem") we get:
// {
// "username": {"label": "item1;item2;newItem"}
// }
//
func (ib *imapBackend) addToCache(userID, label, toAdd string) {
list := ib.getCacheList(userID, label)
if list != "" {
list = list + ";" + toAdd
} else {
list = toAdd
}
ib.imapCacheLock.Lock()
ib.imapCache[userID][label] = list
ib.imapCacheLock.Unlock()
if err := ib.saveIMAPCache(); err != nil {
log.Info("Backend/userinfo: could not save cache: ", err)
}
}
func (ib *imapBackend) removeFromCache(userID, label, toRemove string) {
list := ib.getCacheList(userID, label)
split := strings.Split(list, ";")
for i, item := range split {
if item == toRemove {
split = append(split[:i], split[i+1:]...)
}
}
ib.imapCacheLock.Lock()
ib.imapCache[userID][label] = strings.Join(split, ";")
ib.imapCacheLock.Unlock()
if err := ib.saveIMAPCache(); err != nil {
log.Info("Backend/userinfo: could not save cache: ", err)
}
}
func (ib *imapBackend) getCacheList(userID, label string) (list string) {
if err := ib.loadIMAPCache(); err != nil {
log.Warn("Could not load cache: ", err)
}
ib.imapCacheLock.Lock()
if ib.imapCache == nil {
ib.imapCache = map[string]map[string]string{}
}
if ib.imapCache[userID] == nil {
ib.imapCache[userID] = map[string]string{}
ib.imapCache[userID][SubscriptionException] = ""
}
list = ib.imapCache[userID][label]
ib.imapCacheLock.Unlock()
_ = ib.saveIMAPCache()
return
}
func (ib *imapBackend) loadIMAPCache() error {
if ib.imapCache != nil {
return nil
}
ib.imapCacheLock.Lock()
defer ib.imapCacheLock.Unlock()
f, err := os.Open(ib.imapCachePath)
if err != nil {
return err
}
defer f.Close() //nolint[errcheck]
return json.NewDecoder(f).Decode(&ib.imapCache)
}
func (ib *imapBackend) saveIMAPCache() error {
if ib.imapCache == nil {
return errors.New("cannot save cache: cache is nil")
}
ib.imapCacheLock.Lock()
defer ib.imapCacheLock.Unlock()
f, err := os.Create(ib.imapCachePath)
if err != nil {
return err
}
defer f.Close() //nolint[errcheck]
return json.NewEncoder(f).Encode(ib.imapCache)
}

78
internal/imap/bridge.go Normal file
View File

@ -0,0 +1,78 @@
// 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 imap
import (
"github.com/ProtonMail/proton-bridge/internal/bridge"
)
type configProvider interface {
GetEventsPath() string
GetDBDir() string
GetIMAPCachePath() string
}
type bridger interface {
SetCurrentClient(clientName, clientVersion string)
GetUser(query string) (bridgeUser, error)
}
type bridgeUser interface {
ID() string
CheckBridgeLogin(password string) error
IsCombinedAddressMode() bool
GetAddressID(address string) (string, error)
GetPrimaryAddress() string
SetIMAPIdleUpdateChannel()
UpdateUser() error
Logout() error
CloseConnection(address string)
GetStore() storeUserProvider
GetTemporaryPMAPIClient() bridge.PMAPIProvider
}
type bridgeWrap struct {
*bridge.Bridge
}
// newBridgeWrap wraps bridge struct into local bridgeWrap to implement local
// interface. Problem is that bridge is returning package bridge's User type,
// so every method that returns User has to be overridden to fulfill the interface.
func newBridgeWrap(bridge *bridge.Bridge) *bridgeWrap {
return &bridgeWrap{Bridge: bridge}
}
func (b *bridgeWrap) GetUser(query string) (bridgeUser, error) {
user, err := b.Bridge.GetUser(query)
if err != nil {
return nil, err
}
return newBridgeUserWrap(user), nil
}
type bridgeUserWrap struct {
*bridge.User
}
func newBridgeUserWrap(bridgeUser *bridge.User) *bridgeUserWrap {
return &bridgeUserWrap{User: bridgeUser}
}
func (u *bridgeUserWrap) GetStore() storeUserProvider {
return newStoreUserWrap(u.User.GetStore())
}

151
internal/imap/cache/cache.go vendored Normal file
View File

@ -0,0 +1,151 @@
// 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 cache
import (
"bytes"
"sort"
"sync"
"time"
backendMessage "github.com/ProtonMail/proton-bridge/pkg/message"
)
type key struct {
ID string
Timestamp int64
Size int
}
type oldestFirst []key
func (s oldestFirst) Len() int { return len(s) }
func (s oldestFirst) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
func (s oldestFirst) Less(i, j int) bool { return s[i].Timestamp < s[j].Timestamp }
type cachedMessage struct {
key
data []byte
structure backendMessage.BodyStructure
}
//nolint[gochecknoglobals]
var (
cacheTimeLimit = int64(1 * 60 * 60 * 1000) // milliseconds
cacheSizeLimit = 100 * 1000 * 1000 // B - MUST be larger than email max size limit (~ 25 MB)
mailCache = make(map[string]cachedMessage)
// cacheMutex takes care of one single operation, whereas buildMutex takes
// care of the whole action doing multiple operations. buildMutex will protect
// you from asking server or decrypting or building the same message more
// than once. When first request to build the message comes, it will block
// all other build requests. When the first one is done, all others are
// handled by cache, not doing anything twice. With cacheMutex we are safe
// only to not mess up with the cache, but we could end up downloading and
// building message twice.
cacheMutex = &sync.Mutex{}
buildMutex = &sync.Mutex{}
buildLocks = map[string]interface{}{}
)
func (m *cachedMessage) isValidOrDel() bool {
if m.key.Timestamp+cacheTimeLimit < timestamp() {
delete(mailCache, m.key.ID)
return false
}
return true
}
func timestamp() int64 {
return time.Now().UnixNano() / int64(time.Millisecond)
}
func Clear() {
mailCache = make(map[string]cachedMessage)
}
// BuildLock locks per message level, not on global level.
// Multiple different messages can be building at once.
func BuildLock(messageID string) {
for {
buildMutex.Lock()
if _, ok := buildLocks[messageID]; ok { // if locked, wait
buildMutex.Unlock()
time.Sleep(10 * time.Millisecond)
} else { // if unlocked, lock it
buildLocks[messageID] = struct{}{}
buildMutex.Unlock()
return
}
}
}
func BuildUnlock(messageID string) {
buildMutex.Lock()
defer buildMutex.Unlock()
delete(buildLocks, messageID)
}
func LoadMail(mID string) (reader *bytes.Reader, structure *backendMessage.BodyStructure) {
reader = &bytes.Reader{}
cacheMutex.Lock()
defer cacheMutex.Unlock()
if message, ok := mailCache[mID]; ok && message.isValidOrDel() {
reader = bytes.NewReader(message.data)
structure = &message.structure
// Update timestamp to keep emails which are used often.
message.Timestamp = timestamp()
}
return
}
func SaveMail(mID string, msg []byte, structure *backendMessage.BodyStructure) {
cacheMutex.Lock()
defer cacheMutex.Unlock()
newMessage := cachedMessage{
key: key{
ID: mID,
Timestamp: timestamp(),
Size: len(msg),
},
data: msg,
structure: *structure,
}
// Remove old and reduce size.
totalSize := 0
messageList := []key{}
for _, message := range mailCache {
if message.isValidOrDel() {
messageList = append(messageList, message.key)
totalSize += message.key.Size
}
}
sort.Sort(oldestFirst(messageList))
var oldest key
for totalSize+newMessage.key.Size >= cacheSizeLimit {
oldest, messageList = messageList[0], messageList[1:]
delete(mailCache, oldest.ID)
totalSize -= oldest.Size
}
// Write new.
mailCache[mID] = newMessage
}

99
internal/imap/cache/cache_test.go vendored Normal file
View File

@ -0,0 +1,99 @@
// 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 cache
import (
"bytes"
"fmt"
"testing"
"time"
bckMsg "github.com/ProtonMail/proton-bridge/pkg/message"
"github.com/stretchr/testify/require"
)
var bs = &bckMsg.BodyStructure{} //nolint[gochecknoglobals]
const testUID = "testmsg"
func TestSaveAndLoad(t *testing.T) {
msg := []byte("Test message")
SaveMail(testUID, msg, bs)
require.Equal(t, mailCache[testUID].data, msg)
reader, _ := LoadMail(testUID)
require.Equal(t, reader.Len(), len(msg))
stored := make([]byte, len(msg))
_, _ = reader.Read(stored)
require.Equal(t, stored, msg)
}
func TestMissing(t *testing.T) {
reader, _ := LoadMail("non-existing")
require.Equal(t, reader.Len(), 0)
}
func TestClearOld(t *testing.T) {
cacheTimeLimit = 10
msg := []byte("Test message")
SaveMail(testUID, msg, bs)
time.Sleep(100 * time.Millisecond)
reader, _ := LoadMail(testUID)
require.Equal(t, reader.Len(), 0)
}
func TestClearBig(t *testing.T) {
msg := []byte("Test message")
nSize := 3
cacheSizeLimit = nSize*len(msg) + 1
cacheTimeLimit = int64(nSize * nSize * 2) // be sure the message will survive
// It should have more than nSize items.
for i := 0; i < nSize*nSize; i++ {
time.Sleep(1 * time.Millisecond)
SaveMail(fmt.Sprintf("%s%d", testUID, i), msg, bs)
if len(mailCache) > nSize {
t.Error("Number of items in cache should not be more than", nSize)
}
}
// Check that the oldest are deleted first.
for i := 0; i < nSize*nSize; i++ {
iUID := fmt.Sprintf("%s%d", testUID, i)
reader, _ := LoadMail(iUID)
if i < nSize*(nSize-1) && reader.Len() != 0 {
mail := mailCache[iUID]
t.Error("LoadMail should return empty but have:", mail.data, iUID, mail.key.Timestamp)
}
stored := make([]byte, len(msg))
_, _ = reader.Read(stored)
if i >= nSize*(nSize-1) && !bytes.Equal(stored, msg) {
t.Error("LoadMail returned wrong message:", stored, iUID)
}
}
}
func TestConcurency(t *testing.T) {
msg := []byte("Test message")
for i := 0; i < 10; i++ {
go SaveMail(fmt.Sprintf("%s%d", testUID, i), msg, bs)
}
}

35
internal/imap/imap.go Normal file
View File

@ -0,0 +1,35 @@
// 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 imap
import "github.com/ProtonMail/proton-bridge/pkg/config"
const (
fetchMessagesWorkers = 5 // In how many workers to fetch message (group list on IMAP).
fetchAttachmentsWorkers = 5 // In how many workers to fetch attachments (for one message).
clientAppleMail = "Mac OS X Mail" //nolint[deadcode]
clientThunderbird = "Thunderbird" //nolint[deadcode]
clientOutlookMac = "Microsoft Outlook for Mac" //nolint[deadcode]
clientOutlookWin = "Microsoft Outlook" //nolint[deadcode]
clientNone = ""
)
var (
log = config.GetLogEntry("imap") //nolint[gochecknoglobals]
)

187
internal/imap/mailbox.go Normal file
View File

@ -0,0 +1,187 @@
// 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 imap
import (
"strings"
"github.com/ProtonMail/proton-bridge/pkg/message"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/emersion/go-imap"
specialuse "github.com/emersion/go-imap-specialuse"
"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,
}
}
// 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{imap.NoInferiorsAttr} // Subfolders are not yet supported by API.
switch im.storeMailbox.LabelID() {
case pmapi.SentLabel:
flags = append(flags, specialuse.Sent)
case pmapi.TrashLabel:
flags = append(flags, specialuse.Trash)
case pmapi.SpamLabel:
flags = append(flags, specialuse.Junk)
case pmapi.ArchiveLabel:
flags = append(flags, specialuse.Archive)
case pmapi.AllMailLabel:
flags = append(flags, specialuse.All)
case pmapi.DraftLabel:
flags = append(flags, specialuse.Drafts)
}
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 []string) (*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.PermanentFlags = []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,
}
dbTotal, dbUnread, err := im.storeMailbox.GetCounts()
l.Debugln("DB: total", dbTotal, "unread", dbUnread, "err", err)
if err == nil {
status.Messages = uint32(dbTotal)
status.Unseen = uint32(dbUnread)
}
if status.UidNext, err = im.storeMailbox.GetNextUID(); err != nil {
return nil, err
}
return status, nil
}
// Subscribe adds the mailbox to the server's set of "active" or "subscribed" mailboxes.
func (im *imapMailbox) Subscribe() error {
// Called from go-imap in goroutines - we need to handle panics for each function.
defer im.panicHandler.HandlePanic()
label := im.storeMailbox.LabelID()
if !im.user.isSubscribed(label) {
im.user.removeFromCache(SubscriptionException, label)
}
return nil
}
// Unsubscribe removes the mailbox to the server's set of "active" or "subscribed" mailboxes.
func (im *imapMailbox) Unsubscribe() error {
// Called from go-imap in goroutines - we need to handle panics for each function.
defer im.panicHandler.HandlePanic()
label := im.storeMailbox.LabelID()
if 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.
// Our messages do not have \Deleted flag, nothing to do here.
func (im *imapMailbox) Expunge() error {
return nil
}
func (im *imapMailbox) ListQuotas() ([]string, error) {
return []string{""}, nil
}

View File

@ -0,0 +1,784 @@
// 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 imap
import (
"bytes"
"encoding/base64"
"fmt"
"io"
"io/ioutil"
"mime/multipart"
"net/mail"
"net/textproto"
"regexp"
"sort"
"strings"
"time"
pmcrypto "github.com/ProtonMail/gopenpgp/crypto"
"github.com/ProtonMail/proton-bridge/internal/imap/cache"
"github.com/ProtonMail/proton-bridge/internal/imap/uidplus"
"github.com/ProtonMail/proton-bridge/pkg/message"
"github.com/ProtonMail/proton-bridge/pkg/parallel"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/emersion/go-imap"
"github.com/emersion/go-textwrapper"
"github.com/hashicorp/go-multierror"
enmime "github.com/jhillyerd/enmime"
"github.com/pkg/errors"
openpgperrors "golang.org/x/crypto/openpgp/errors"
)
type doNotCacheError struct{ e error }
func (dnc *doNotCacheError) Error() string { return dnc.e.Error() }
func (dnc *doNotCacheError) add(err error) { dnc.e = multierror.Append(dnc.e, err) }
func (dnc *doNotCacheError) errorOrNil() error {
if dnc == nil {
return nil
}
if dnc.e != nil {
return dnc
}
return nil
}
// CreateMessage appends a new message to this mailbox. The \Recent flag will
// be added regardless of whether flags is empty or not. If date is nil, the
// current time will be used.
//
// If the Backend implements Updater, it must notify the client immediately
// via a mailbox update.
func (im *imapMailbox) CreateMessage(flags []string, date time.Time, body imap.Literal) error { // nolint[funlen]
// Called from go-imap in goroutines - we need to handle panics for each function.
defer im.panicHandler.HandlePanic()
m, _, _, readers, err := message.Parse(body, "", "")
if err != nil {
return err
}
addr := im.storeAddress.APIAddress()
if addr == nil {
return errors.New("no available address for encryption")
}
m.AddressID = addr.ID
kr := addr.KeyRing()
// Handle imported messages which have no "Sender" address.
// This sometimes occurs with outlook which reports errors as imported emails or for drafts.
if m.Sender == nil {
im.log.Warning("Append: Missing email sender. Will use main address")
m.Sender = &mail.Address{
Name: "",
Address: addr.Email,
}
}
// "Drafts" needs to call special API routes.
// Clients always append the whole message again and remove the old one.
if im.storeMailbox.LabelID() == pmapi.DraftLabel {
// Sender address needs to be sanitised (drafts need to match cases exactly).
m.Sender.Address = pmapi.ConstructAddress(m.Sender.Address, addr.Email)
draft, _, err := im.user.storeUser.CreateDraft(kr, m, readers, "", "", "")
if err != nil {
return errors.Wrap(err, "failed to create draft")
}
targetSeq := im.storeMailbox.GetUIDList([]string{draft.ID})
return uidplus.AppendResponse(im.storeMailbox.UIDValidity(), targetSeq)
}
// We need to make sure this is an import, and not a sent message from this account
// (sent messages from the account will be added by the event loop).
if im.storeMailbox.LabelID() == pmapi.SentLabel {
sanitizedSender := pmapi.SanitizeEmail(m.Sender.Address)
// Check whether this message was sent by a bridge user.
user, err := im.user.backend.bridge.GetUser(sanitizedSender)
if err == nil && user.ID() == im.storeUser.UserID() {
logEntry := im.log.WithField("addr", sanitizedSender).WithField("extID", m.Header.Get("Message-Id"))
// If we find the message in the store already, we can skip importing it.
if foundUID := im.storeMailbox.GetUIDByHeader(&m.Header); foundUID != uint32(0) {
logEntry.Info("Ignoring APPEND of duplicate to Sent folder")
return uidplus.AppendResponse(im.storeMailbox.UIDValidity(), &uidplus.OrderedSeq{foundUID})
}
// We didn't find the message in the store, so we are currently sending it.
logEntry.WithField("time", date).Info("No matching UID, continuing APPEND to Sent")
// For now we don't import user's own messages to Sent because GetUIDByHeader is not smart enough.
// This will be fixed in GODT-143.
return nil
}
// This is an APPEND to the Sent folder, so we will set the sent flag
m.Flags |= pmapi.FlagSent
}
message.ParseFlags(m, flags)
if !date.IsZero() {
m.Time = date.Unix()
}
internalID := m.Header.Get("X-Pm-Internal-Id")
references := m.Header.Get("References")
referenceList := strings.Fields(references)
if len(referenceList) > 0 {
lastReference := referenceList[len(referenceList)-1]
// In case we are using a mail client which corrupts headers, try "References" too.
re := regexp.MustCompile("<[a-zA-Z0-9-_=]*@protonmail.internalid>")
match := re.FindString(lastReference)
if match != "" {
internalID = match[1 : len(match)-len("@protonmail.internalid>")]
}
}
// Avoid appending a message which is already on the server. Apply the new
// label instead. This sometimes happens which Outlook (it uses APPEND instead of COPY).
if internalID != "" {
// Check to see if this belongs to a different address in split mode or another ProtonMail account.
msg, err := im.storeMailbox.GetMessage(internalID)
if err == nil && (im.user.user.IsCombinedAddressMode() || (im.storeAddress.AddressID() == msg.Message().AddressID)) {
IDs := []string{internalID}
err = im.storeMailbox.LabelMessages(IDs)
if err != nil {
return err
}
targetSeq := im.storeMailbox.GetUIDList([]string{m.ID})
return uidplus.AppendResponse(im.storeMailbox.UIDValidity(), targetSeq)
}
}
im.log.Info("Importing external message")
if err := im.importMessage(m, readers, kr); err != nil {
im.log.Error("Import failed: ", err)
return err
}
targetSeq := im.storeMailbox.GetUIDList([]string{m.ID})
return uidplus.AppendResponse(im.storeMailbox.UIDValidity(), targetSeq)
}
func (im *imapMailbox) importMessage(m *pmapi.Message, readers []io.Reader, kr *pmcrypto.KeyRing) (err error) { // nolint[funlen]
b := &bytes.Buffer{}
// Overwrite content for main header for import.
// Even if message has just simple body we should upload as multipart/mixed.
// Each part has encrypted body and header reflects the original header.
mainHeader := message.GetHeader(m)
mainHeader.Set("Content-Type", "multipart/mixed; boundary="+message.GetBoundary(m))
mainHeader.Del("Content-Disposition")
mainHeader.Del("Content-Transfer-Encoding")
if err = writeHeader(b, mainHeader); err != nil {
return
}
mw := multipart.NewWriter(b)
if err = mw.SetBoundary(message.GetBoundary(m)); err != nil {
return
}
// Write the body part.
bodyHeader := make(textproto.MIMEHeader)
bodyHeader.Set("Content-Type", m.MIMEType+"; charset=utf-8")
bodyHeader.Set("Content-Disposition", "inline")
bodyHeader.Set("Content-Transfer-Encoding", "7bit")
var p io.Writer
if p, err = mw.CreatePart(bodyHeader); err != nil {
return
}
// First, encrypt the message body.
if err = m.Encrypt(kr, kr); err != nil {
return err
}
if _, err := io.WriteString(p, m.Body); err != nil {
return err
}
// Write the attachments parts.
for i := 0; i < len(m.Attachments); i++ {
att := m.Attachments[i]
r := readers[i]
h := message.GetAttachmentHeader(att)
if p, err = mw.CreatePart(h); err != nil {
return
}
// Create line wrapper writer.
ww := textwrapper.NewRFC822(p)
// Create base64 writer.
bw := base64.NewEncoder(base64.StdEncoding, ww)
data, err := ioutil.ReadAll(r)
if err != nil {
return err
}
// Create encrypted writer.
pgpMessage, err := kr.Encrypt(pmcrypto.NewPlainMessage(data), nil)
if err != nil {
return err
}
if _, err := bw.Write(pgpMessage.GetBinary()); err != nil {
return err
}
if err := bw.Close(); err != nil {
return err
}
}
if err := mw.Close(); err != nil {
return err
}
labels := []string{}
for _, l := range m.LabelIDs {
if l == pmapi.StarredLabel {
labels = append(labels, pmapi.StarredLabel)
}
}
return im.storeMailbox.ImportMessage(m, b.Bytes(), labels)
}
func (im *imapMailbox) getMessage(storeMessage storeMessageProvider, items []string) (msg *imap.Message, err error) {
im.log.WithField("msgID", storeMessage.ID()).Trace("Getting message")
seqNum, err := storeMessage.SequenceNumber()
if err != nil {
return
}
m := storeMessage.Message()
msg = imap.NewMessage(seqNum, items)
for _, item := range items {
switch item {
case imap.EnvelopeMsgAttr:
msg.Envelope = message.GetEnvelope(m)
case imap.BodyMsgAttr, imap.BodyStructureMsgAttr:
var structure *message.BodyStructure
if structure, _, err = im.getBodyStructure(storeMessage); err != nil {
return
}
if msg.BodyStructure, err = structure.IMAPBodyStructure([]int{}); err != nil {
return
}
case imap.FlagsMsgAttr:
msg.Flags = message.GetFlags(m)
case imap.InternalDateMsgAttr:
msg.InternalDate = time.Unix(m.Time, 0)
case imap.SizeMsgAttr:
// Size attribute on the server counts encrypted data. The value is cleared
// on our part and we need to compute "real" size of decrypted data.
if m.Size <= 0 {
if _, _, err = im.getBodyStructure(storeMessage); err != nil {
return
}
}
msg.Size = uint32(m.Size)
case imap.UidMsgAttr:
msg.Uid, err = storeMessage.UID()
if err != nil {
return nil, err
}
default:
s := item
var section *imap.BodySectionName
if section, err = imap.NewBodySectionName(s); err != nil {
err = nil // Ignore error
break
}
var literal imap.Literal
if literal, err = im.getMessageBodySection(storeMessage, section); err != nil {
return
}
msg.Body[section] = literal
}
}
return msg, err
}
func (im *imapMailbox) getBodyStructure(storeMessage storeMessageProvider) (
structure *message.BodyStructure,
bodyReader *bytes.Reader, err error,
) {
m := storeMessage.Message()
id := im.storeUser.UserID() + m.ID
cache.BuildLock(id)
if bodyReader, structure = cache.LoadMail(id); bodyReader.Len() == 0 || structure == nil {
var body []byte
structure, body, err = im.buildMessage(m)
if err == nil && structure != nil && len(body) > 0 {
m.Size = int64(len(body))
if err := storeMessage.SetSize(m.Size); err != nil {
im.log.WithError(err).
WithField("newSize", m.Size).
WithField("msgID", m.ID).
Warn("Cannot update size while building")
}
if err := storeMessage.SetContentTypeAndHeader(m.MIMEType, m.Header); err != nil {
im.log.WithError(err).
WithField("msgID", m.ID).
Warn("Cannot update header while building")
}
// Drafts can change and we don't want to cache them.
if !isMessageInDraftFolder(m) {
cache.SaveMail(id, body, structure)
}
bodyReader = bytes.NewReader(body)
}
if _, ok := err.(*doNotCacheError); ok {
im.log.WithField("msgID", m.ID).Errorf("do not cache message: %v", err)
err = nil
bodyReader = bytes.NewReader(body)
}
}
cache.BuildUnlock(id)
return structure, bodyReader, err
}
func isMessageInDraftFolder(m *pmapi.Message) bool {
for _, labelID := range m.LabelIDs {
if labelID == pmapi.DraftLabel {
return true
}
}
return false
}
// This will download message (or read from cache) and pick up the section,
// extract data (header,body, both) and trim the output if needed.
func (im *imapMailbox) getMessageBodySection(storeMessage storeMessageProvider, section *imap.BodySectionName) (literal imap.Literal, err error) { // nolint[funlen]
var (
structure *message.BodyStructure
bodyReader *bytes.Reader
header textproto.MIMEHeader
response []byte
)
im.log.WithField("msgID", storeMessage.ID()).Trace("Getting message body")
m := storeMessage.Message()
if len(section.Path) == 0 && section.Specifier == imap.HeaderSpecifier {
// We can extract message header without decrypting.
header = message.GetHeader(m)
// We need to ensure we use the correct content-type,
// otherwise AppleMail expects `text/plain` in HTML mails.
if header.Get("Content-Type") == "" {
if err = im.fetchMessage(m); err != nil {
return
}
if _, err = im.setMessageContentType(m); err != nil {
return
}
if err = storeMessage.SetContentTypeAndHeader(m.MIMEType, m.Header); err != nil {
return
}
header = message.GetHeader(m)
}
} else {
// The rest of cases need download and decrypt.
structure, bodyReader, err = im.getBodyStructure(storeMessage)
if err != nil {
return
}
switch {
case section.Specifier == imap.EntireSpecifier && len(section.Path) == 0:
// An empty section specification refers to the entire message, including the header.
response, err = structure.GetSection(bodyReader, section.Path)
case section.Specifier == imap.TextSpecifier || (section.Specifier == imap.EntireSpecifier && len(section.Path) != 0):
// The TEXT specifier refers to the content of the message (or section), omitting the [RFC-2822] header.
// Non-empty section with no specifier (imap.EntireSpecifier) refers to section content without header.
response, err = structure.GetSectionContent(bodyReader, section.Path)
case section.Specifier == imap.MimeSpecifier:
// The MIME part specifier refers to the [MIME-IMB] header for this part.
fallthrough
case section.Specifier == imap.HeaderSpecifier:
header, err = structure.GetSectionHeader(section.Path)
default:
err = errors.New("Unknown specifier " + section.Specifier)
}
}
if err != nil {
return
}
// Filter header. Options are: all fields, only selected fields, all fields except selected.
if header != nil {
// remove fields
if len(section.Fields) != 0 && section.NotFields {
for _, field := range section.Fields {
header.Del(field)
}
}
fields := make([]string, 0, len(header))
if len(section.Fields) == 0 || section.NotFields { // add all and sort
for f := range header {
fields = append(fields, f)
}
sort.Strings(fields)
} else { // add only requested (in requested order)
for _, f := range section.Fields {
fields = append(fields, textproto.CanonicalMIMEHeaderKey(f))
}
}
headerBuf := &bytes.Buffer{}
for _, canonical := range fields {
if values, ok := header[canonical]; !ok {
continue
} else {
for _, val := range values {
fmt.Fprintf(headerBuf, "%s: %s\r\n", canonical, val)
}
}
}
response = headerBuf.Bytes()
}
// Trim any output if requested.
literal = bytes.NewBuffer(section.ExtractPartial(response))
return literal, nil
}
func (im *imapMailbox) fetchMessage(m *pmapi.Message) (err error) {
im.log.Trace("Fetching message")
complete, err := im.storeMailbox.FetchMessage(m.ID)
if err != nil {
im.log.WithError(err).Error("Could not get message from store")
return
}
*m = *complete.Message()
return
}
func (im *imapMailbox) customMessage(m *pmapi.Message, err error, attachBody bool) {
// Assuming quoted-printable.
origBody := strings.Replace(m.Body, "=", "=3D", -1)
m.Body = "Content-Type: text/html\r\n"
m.Body = "\n<html><head></head><body style=3D\"font-family: Arial,'Helvetica Neue',Helvetica,sans-serif; font-size: 14px; \">\n"
m.Body += "<div style=3D\"color:#555; background-color:#cf9696; padding:20px; border-radius: 4px;\" >\n<strong>Decryption error</strong><br/>Decryption of this message's encrypted content failed.<pre>\n"
m.Body += err.Error()
m.Body += "\n</pre></div>\n"
if attachBody {
m.Body += "<div style=3D\"color:#333; background-color:#f4f4f4; border: 1px solid #acb0bf; border-radius: 2px; padding:1rem; margin:1rem 0; font-family:monospace; font-size: 1em;\" ><pre>\n"
m.Body += origBody
m.Body += "\n</pre></div>\n"
}
m.Body += "</body></html>"
m.MIMEType = "text/html"
// NOTE: we need to set header in custom message header, so we check that is non-nil.
if m.Header == nil {
m.Header = make(mail.Header)
}
}
func (im *imapMailbox) writeMessageBody(w io.Writer, m *pmapi.Message) (err error) {
im.log.Trace("Writing message body")
if m.Body == "" {
im.log.Trace("While writing message body, noticed message body is null, need to fetch")
if err = im.fetchMessage(m); err != nil {
return
}
}
kr := im.user.client.KeyRingForAddressID(m.AddressID)
err = message.WriteBody(w, kr, m)
if err != nil {
im.customMessage(m, err, true)
_, _ = io.WriteString(w, m.Body)
err = nil
}
return
}
func (im *imapMailbox) writeAndParseMIMEBody(m *pmapi.Message) (mime *enmime.Envelope, err error) { //nolint[unused]
b := &bytes.Buffer{}
if err = im.writeMessageBody(b, m); err != nil {
return
}
mime, err = enmime.ReadEnvelope(b)
return
}
func (im *imapMailbox) writeAttachmentBody(w io.Writer, m *pmapi.Message, att *pmapi.Attachment) (err error) {
// Retrieve encrypted attachment.
r, err := im.user.client.GetAttachment(att.ID)
if err != nil {
return
}
defer r.Close() //nolint[errcheck]
kr := im.user.client.KeyRingForAddressID(m.AddressID)
if err = message.WriteAttachmentBody(w, kr, m, att, r); err != nil {
// Returning an error here makes certain mail clients behave badly,
// trying to retrieve the message again and again.
im.log.Warn("Cannot write attachment body: ", err)
err = nil
}
return
}
func (im *imapMailbox) writeRelatedPart(p io.Writer, m *pmapi.Message, inlines []*pmapi.Attachment) (err error) {
related := multipart.NewWriter(p)
_ = related.SetBoundary(message.GetRelatedBoundary(m))
buf := &bytes.Buffer{}
if err = im.writeMessageBody(buf, m); err != nil {
return
}
// Write the body part.
h := message.GetBodyHeader(m)
if p, err = related.CreatePart(h); err != nil {
return
}
_, _ = buf.WriteTo(p)
for _, inline := range inlines {
buf = &bytes.Buffer{}
if err = im.writeAttachmentBody(buf, m, inline); err != nil {
return
}
h := message.GetAttachmentHeader(inline)
if p, err = related.CreatePart(h); err != nil {
return
}
_, _ = buf.WriteTo(p)
}
_ = related.Close()
return nil
}
const (
noMultipart = iota // only body
simpleMultipart // body + attachment or inline
complexMultipart // mixed, rfc822, alternatives, ...
)
func (im *imapMailbox) setMessageContentType(m *pmapi.Message) (multipartType int, err error) {
if m.MIMEType == "" {
err = fmt.Errorf("trying to set Content-Type without MIME TYPE")
return
}
// message.MIMEType can have just three values from our server:
// * `text/html` (refers to body type, but might contain attachments and inlines)
// * `text/plain` (refers to body type, but might contain attachments and inlines)
// * `multipart/mixed` (refers to external message with multipart structure)
// The proper header content fields must be set and saved to DB based MIMEType and content.
multipartType = noMultipart
if m.MIMEType == pmapi.ContentTypeMultipartMixed {
multipartType = complexMultipart
} else if m.NumAttachments != 0 {
multipartType = simpleMultipart
}
h := textproto.MIMEHeader(m.Header)
if multipartType == noMultipart {
message.SetBodyContentFields(&h, m)
} else {
h.Set("Content-Type",
fmt.Sprintf("%s; boundary=%s", "multipart/mixed", message.GetBoundary(m)),
)
}
m.Header = mail.Header(h)
return
}
// buildMessage from PM to IMAP.
func (im *imapMailbox) buildMessage(m *pmapi.Message) (structure *message.BodyStructure, msgBody []byte, err error) {
im.log.Trace("Building message")
var errNoCache doNotCacheError
// If fetch or decryption fails we need to change the MIMEType (in customMessage).
err = im.fetchMessage(m)
if err != nil {
return
}
kr := im.user.client.KeyRingForAddressID(m.AddressID)
errDecrypt := m.Decrypt(kr)
if errDecrypt != nil && errDecrypt != openpgperrors.ErrSignatureExpired {
errNoCache.add(errDecrypt)
im.customMessage(m, errDecrypt, true)
}
// Inner function can fail even when message is decrypted.
// #1048 For example we have problem with double-encrypted messages
// which seems as still encrypted and we try them to decrypt again
// and that fails. For any building error is better to return custom
// message than error because it will not be fixed and users would
// get error message all the time and could not see some messages.
structure, msgBody, err = im.buildMessageInner(m, kr)
if err == pmapi.ErrAPINotReachable || err == pmapi.ErrInvalidToken || err == pmapi.ErrUpgradeApplication {
return nil, nil, err
} else if err != nil {
errNoCache.add(err)
im.customMessage(m, err, true)
structure, msgBody, err = im.buildMessageInner(m, kr)
if err != nil {
return nil, nil, err
}
}
err = errNoCache.errorOrNil()
return structure, msgBody, err
}
func (im *imapMailbox) buildMessageInner(m *pmapi.Message, kr *pmcrypto.KeyRing) (structure *message.BodyStructure, msgBody []byte, err error) { // nolint[funlen]
multipartType, err := im.setMessageContentType(m)
if err != nil {
return
}
tmpBuf := &bytes.Buffer{}
mainHeader := message.GetHeader(m)
if err = writeHeader(tmpBuf, mainHeader); err != nil {
return
}
_, _ = io.WriteString(tmpBuf, "\r\n")
switch multipartType {
case noMultipart:
err = message.WriteBody(tmpBuf, kr, m)
if err != nil {
return
}
case complexMultipart:
_, _ = io.WriteString(tmpBuf, "\r\n--"+message.GetBoundary(m)+"\r\n")
err = message.WriteBody(tmpBuf, kr, m)
if err != nil {
return
}
_, _ = io.WriteString(tmpBuf, "\r\n--"+message.GetBoundary(m)+"--\r\n")
case simpleMultipart:
atts, inlines := message.SeparateInlineAttachments(m)
mw := multipart.NewWriter(tmpBuf)
_ = mw.SetBoundary(message.GetBoundary(m))
var partWriter io.Writer
if len(inlines) > 0 {
relatedHeader := message.GetRelatedHeader(m)
if partWriter, err = mw.CreatePart(relatedHeader); err != nil {
return
}
_ = im.writeRelatedPart(partWriter, m, inlines)
} else {
buf := &bytes.Buffer{}
if err = im.writeMessageBody(buf, m); err != nil {
return
}
// Write the body part.
bodyHeader := message.GetBodyHeader(m)
if partWriter, err = mw.CreatePart(bodyHeader); err != nil {
return
}
_, _ = buf.WriteTo(partWriter)
}
// Write the attachments parts.
input := make([]interface{}, len(atts))
for i, att := range atts {
input[i] = att
}
processCallback := func(value interface{}) (interface{}, error) {
att := value.(*pmapi.Attachment)
buf := &bytes.Buffer{}
if err = im.writeAttachmentBody(buf, m, att); err != nil {
return nil, err
}
return buf, nil
}
collectCallback := func(idx int, value interface{}) error {
buf := value.(*bytes.Buffer)
defer buf.Reset()
att := atts[idx]
attachmentHeader := message.GetAttachmentHeader(att)
if partWriter, err = mw.CreatePart(attachmentHeader); err != nil {
return err
}
_, _ = buf.WriteTo(partWriter)
return nil
}
err = parallel.RunParallel(fetchAttachmentsWorkers, input, processCallback, collectCallback)
if err != nil {
return
}
_ = mw.Close()
default:
fmt.Fprintf(tmpBuf, "\r\n\r\nUknown multipart type: %d\r\n\r\n", multipartType)
}
// We need to copy buffer before building body structure.
msgBody = tmpBuf.Bytes()
structure, err = message.NewBodyStructure(tmpBuf)
if err != nil {
// NOTE: We need to set structure if it fails and is empty.
if structure == nil {
structure = &message.BodyStructure{}
}
}
return structure, msgBody, err
}

View File

@ -0,0 +1,41 @@
// 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 imap
import (
"errors"
"testing"
"github.com/stretchr/testify/require"
)
func TestDoNotCache(t *testing.T) {
var dnc doNotCacheError
require.NoError(t, dnc.errorOrNil())
_, ok := dnc.errorOrNil().(*doNotCacheError)
require.True(t, !ok, "should not be type doNotCacheError")
dnc.add(errors.New("first"))
require.True(t, dnc.errorOrNil() != nil, "should be error")
_, ok = dnc.errorOrNil().(*doNotCacheError)
require.True(t, ok, "should be type doNotCacheError")
dnc.add(errors.New("second"))
dnc.add(errors.New("third"))
t.Log(dnc.errorOrNil())
}

View File

@ -0,0 +1,490 @@
// 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 imap
import (
"errors"
"fmt"
"net/mail"
"strings"
"sync"
"time"
"github.com/ProtonMail/proton-bridge/internal/imap/uidplus"
"github.com/ProtonMail/proton-bridge/pkg/message"
"github.com/ProtonMail/proton-bridge/pkg/parallel"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/emersion/go-imap"
"github.com/sirupsen/logrus"
)
// UpdateMessagesFlags alters flags for the specified message(s).
//
// If the Backend implements Updater, it must notify the client immediately
// via a message update.
func (im *imapMailbox) UpdateMessagesFlags(uid bool, seqSet *imap.SeqSet, operation imap.FlagsOp, flags []string) error {
log.WithFields(logrus.Fields{
"flags": flags,
"operation": operation,
}).Debug("Updating message flags")
// Called from go-imap in goroutines - we need to handle panics for each function.
defer im.panicHandler.HandlePanic()
messageIDs, err := im.apiIDsFromSeqSet(uid, seqSet)
if err != nil || len(messageIDs) == 0 {
return err
}
for _, f := range flags {
switch f {
case imap.SeenFlag:
switch operation {
case imap.SetFlags, imap.AddFlags:
_ = im.storeMailbox.MarkMessagesRead(messageIDs)
case imap.RemoveFlags:
_ = im.storeMailbox.MarkMessagesUnread(messageIDs)
}
case imap.FlaggedFlag:
switch operation {
case imap.SetFlags, imap.AddFlags:
_ = im.storeMailbox.MarkMessagesStarred(messageIDs)
case imap.RemoveFlags:
_ = im.storeMailbox.MarkMessagesUnstarred(messageIDs)
}
case imap.DeletedFlag:
if operation == imap.RemoveFlags {
break // Nothing to do, no message has the \Deleted flag.
}
_ = im.storeMailbox.DeleteMessages(messageIDs)
case imap.AnsweredFlag, imap.DraftFlag, imap.RecentFlag:
// Not supported.
case message.AppleMailJunkFlag, message.ThunderbirdJunkFlag:
storeMailbox, err := im.storeAddress.GetMailbox(pmapi.SpamLabel)
if err != nil {
return err
}
// Handle custom junk flags for Apple Mail and Thunderbird.
switch operation {
// No label removal is necessary because Spam and Inbox are both exclusive labels so the backend
// will automatically take care of label removal.
case imap.SetFlags, imap.AddFlags:
_ = storeMailbox.LabelMessages(messageIDs)
case imap.RemoveFlags:
_ = storeMailbox.UnlabelMessages(messageIDs)
}
}
}
return nil
}
// CopyMessages copies the specified message(s) to the end of the specified
// destination mailbox. The flags and internal date of the message(s) SHOULD
// be preserved, and the Recent flag SHOULD be set, in the copy.
func (im *imapMailbox) CopyMessages(uid bool, seqSet *imap.SeqSet, targetLabel string) error {
// Called from go-imap in goroutines - we need to handle panics for each function.
defer im.panicHandler.HandlePanic()
messageIDs, err := im.apiIDsFromSeqSet(uid, seqSet)
if err != nil || len(messageIDs) == 0 {
return err
}
// It is needed to get UID list before LabelingMessages because
// messages can be removed from source during labeling (e.g. folder1 -> folder2).
sourceSeqSet := im.storeMailbox.GetUIDList(messageIDs)
targetStoreMBX, err := im.storeAddress.GetMailbox(targetLabel)
if err != nil {
return err
}
if err = targetStoreMBX.LabelMessages(messageIDs); err != nil {
return err
}
targetSeqSet := targetStoreMBX.GetUIDList(messageIDs)
return uidplus.CopyResponse(im.storeMailbox.UIDValidity(), sourceSeqSet, targetSeqSet)
}
// MoveMessages adds dest's label and removes this mailbox' label from each message.
//
// This should not be used until MOVE extension has option to send UIDPLUS
// responses.
func (im *imapMailbox) MoveMessages(uid bool, seqSet *imap.SeqSet, newLabel string) error {
// Called from go-imap in goroutines - we need to handle panics for each function.
defer im.panicHandler.HandlePanic()
messageIDs, err := im.apiIDsFromSeqSet(uid, seqSet)
if err != nil || len(messageIDs) == 0 {
return err
}
storeMailbox, err := im.storeAddress.GetMailbox(newLabel)
if err != nil {
return err
}
// Label messages first to not loss them. If message is only in trash and we unlabel
// it, it will be removed completely and we cannot label it back.
if err := storeMailbox.LabelMessages(messageIDs); err != nil {
return err
}
return im.storeMailbox.UnlabelMessages(messageIDs)
}
// SearchMessages searches messages. The returned list must contain UIDs if
// uid is set to true, or sequence numbers otherwise.
func (im *imapMailbox) SearchMessages(isUID bool, criteria *imap.SearchCriteria) (ids []uint32, err error) { //nolint[gocyclo]
// Called from go-imap in goroutines - we need to handle panics for each function.
defer im.panicHandler.HandlePanic()
if criteria.Not != nil || criteria.Or[0] != nil {
return nil, errors.New("unsupported search query")
}
if criteria.Body != "" || criteria.Text != "" {
log.Warn("Body and Text criteria not applied.")
}
var apiIDs []string
if criteria.SeqSet != nil {
apiIDs, err = im.apiIDsFromSeqSet(false, criteria.SeqSet)
} else {
apiIDs, err = im.storeMailbox.GetAPIIDsFromSequenceRange(1, 0)
}
if err != nil {
return nil, err
}
var apiIDsFromUID []string
if criteria.Uid != nil {
if apiIDs, err := im.apiIDsFromSeqSet(true, criteria.Uid); err == nil {
apiIDsFromUID = append(apiIDsFromUID, apiIDs...)
}
}
// Apply filters.
for _, apiID := range apiIDs {
// Filter on UIDs.
if len(apiIDsFromUID) > 0 && !isStringInList(apiIDsFromUID, apiID) {
continue
}
// Get message.
storeMessage, err := im.storeMailbox.GetMessage(apiID)
if err != nil {
log.Warnf("search messages: cannot get message %q from db: %v", apiID, err)
continue
}
m := storeMessage.Message()
// Filter addresses.
if criteria.From != "" && !addressMatch([]*mail.Address{m.Sender}, criteria.From) {
continue
}
if criteria.To != "" && !addressMatch(m.ToList, criteria.To) {
continue
}
if criteria.Cc != "" && !addressMatch(m.CCList, criteria.Cc) {
continue
}
if criteria.Bcc != "" && !addressMatch(m.BCCList, criteria.Bcc) {
continue
}
// Filter strings.
if criteria.Subject != "" && !strings.Contains(strings.ToLower(m.Subject), strings.ToLower(criteria.Subject)) {
continue
}
if criteria.Keyword != "" && !hasKeyword(m, criteria.Keyword) {
continue
}
if criteria.Unkeyword != "" && hasKeyword(m, criteria.Unkeyword) {
continue
}
if criteria.Header[0] != "" {
h := message.GetHeader(m)
if val := h.Get(criteria.Header[0]); val == "" {
continue // Field is not in header.
} else if criteria.Header[1] != "" && !strings.Contains(strings.ToLower(val), strings.ToLower(criteria.Header[1])) {
continue // Field is in header, second criteria is non-zero and field value not matched (case insensitive).
}
}
// Filter flags.
if criteria.Flagged && !isStringInList(m.LabelIDs, pmapi.StarredLabel) {
continue
}
if criteria.Unflagged && isStringInList(m.LabelIDs, pmapi.StarredLabel) {
continue
}
if criteria.Seen && m.Unread == 1 {
continue
}
if criteria.Unseen && m.Unread == 0 {
continue
}
if criteria.Deleted {
continue
}
// if criteria.Undeleted { // All messages matches this criteria }
if criteria.Draft && (m.Has(pmapi.FlagSent) || m.Has(pmapi.FlagReceived)) {
continue
}
if criteria.Undraft && !(m.Has(pmapi.FlagSent) || m.Has(pmapi.FlagReceived)) {
continue
}
if criteria.Answered && !(m.Has(pmapi.FlagReplied) || m.Has(pmapi.FlagRepliedAll)) {
continue
}
if criteria.Unanswered && (m.Has(pmapi.FlagReplied) || m.Has(pmapi.FlagRepliedAll)) {
continue
}
if criteria.Recent && m.Has(pmapi.FlagOpened) { // opened means not recent
continue
}
if criteria.Old && !m.Has(pmapi.FlagOpened) {
continue
}
if criteria.New && !(!m.Has(pmapi.FlagOpened) && m.Unread == 1) {
continue
}
// Filter internal date.
if !criteria.Before.IsZero() {
if truncated := criteria.Before.Truncate(24 * time.Hour); m.Time > truncated.Unix() {
continue
}
}
if !criteria.Since.IsZero() {
if truncated := criteria.Since.Truncate(24 * time.Hour); m.Time < truncated.Unix() {
continue
}
}
if !criteria.On.IsZero() {
truncated := criteria.On.Truncate(24 * time.Hour)
if m.Time < truncated.Unix() || m.Time > truncated.Add(24*time.Hour).Unix() {
continue
}
}
if !(criteria.SentBefore.IsZero() && criteria.SentSince.IsZero() && criteria.SentOn.IsZero()) {
if t, err := m.Header.Date(); err == nil && !t.IsZero() {
// Filter header date.
if !criteria.SentBefore.IsZero() {
if truncated := criteria.SentBefore.Truncate(24 * time.Hour); t.Unix() > truncated.Unix() {
continue
}
}
if !criteria.SentSince.IsZero() {
if truncated := criteria.SentSince.Truncate(24 * time.Hour); t.Unix() < truncated.Unix() {
continue
}
}
if !criteria.SentOn.IsZero() {
truncated := criteria.SentOn.Truncate(24 * time.Hour)
if t.Unix() < truncated.Unix() || t.Unix() > truncated.Add(24*time.Hour).Unix() {
continue
}
}
}
}
// Filter size (only if size was already calculated).
if m.Size > 0 {
if criteria.Larger != 0 && m.Size <= int64(criteria.Larger) {
continue
}
if criteria.Smaller != 0 && m.Size >= int64(criteria.Smaller) {
continue
}
}
// Add the ID to response.
var id uint32
if isUID {
id, err = storeMessage.UID()
if err != nil {
return nil, err
}
} else {
id, err = storeMessage.SequenceNumber()
if err != nil {
return nil, err
}
}
ids = append(ids, id)
}
return ids, nil
}
// ListMessages returns a list of messages. seqset must be interpreted as UIDs
// if uid is set to true and as message sequence numbers otherwise. See RFC
// 3501 section 6.4.5 for a list of items that can be requested.
//
// Messages must be sent to msgResponse. When the function returns, msgResponse must be closed.
func (im *imapMailbox) ListMessages(isUID bool, seqSet *imap.SeqSet, items []string, msgResponse chan<- *imap.Message) (err error) { //nolint[funlen]
defer func() {
close(msgResponse)
if err != nil {
log.Errorf("cannot list messages (%v, %v, %v): %v", isUID, seqSet, items, err)
}
// Called from go-imap in goroutines - we need to handle panics for each function.
im.panicHandler.HandlePanic()
}()
var markAsReadIDs []string
markAsReadMutex := &sync.Mutex{}
l := log.WithField("cmd", "ListMessages")
apiIDs, err := im.apiIDsFromSeqSet(isUID, seqSet)
if err != nil {
err = fmt.Errorf("list messages seq: %v", err)
l.WithField("seq", seqSet).Error(err)
return err
}
// From RFC: UID range of 559:* always includes the UID of the last message
// in the mailbox, even if 559 is higher than any assigned UID value.
// See: https://tools.ietf.org/html/rfc3501#page-61
if isUID && seqSet.Dynamic() && len(apiIDs) == 0 {
l.Debug("Requesting empty UID dynamic fetch, adding latest message")
apiID, err := im.storeMailbox.GetLatestAPIID()
if err != nil {
return nil
}
apiIDs = []string{apiID}
}
input := make([]interface{}, len(apiIDs))
for i, apiID := range apiIDs {
input[i] = apiID
}
processCallback := func(value interface{}) (interface{}, error) {
apiID := value.(string)
storeMessage, err := im.storeMailbox.GetMessage(apiID)
if err != nil {
err = fmt.Errorf("list message from db: %v", err)
l.WithField("apiID", apiID).Error(err)
return nil, err
}
msg, err := im.getMessage(storeMessage, items)
if err != nil {
err = fmt.Errorf("list message build: %v", err)
l.WithField("metaID", storeMessage.ID()).Error(err)
return nil, err
}
if storeMessage.Message().Unread == 1 {
for section := range msg.Body {
// Peek means get messages without marking them as read.
// If client does not only ask for peek, we have to mark them as read.
if !section.Peek {
markAsReadMutex.Lock()
markAsReadIDs = append(markAsReadIDs, storeMessage.ID())
markAsReadMutex.Unlock()
msg.Flags = append(msg.Flags, imap.SeenFlag)
break
}
}
}
return msg, nil
}
collectCallback := func(idx int, value interface{}) error {
msg := value.(*imap.Message)
msgResponse <- msg
return nil
}
err = parallel.RunParallel(fetchMessagesWorkers, input, processCallback, collectCallback)
if err != nil {
return err
}
if len(markAsReadIDs) > 0 {
if err := im.storeMailbox.MarkMessagesRead(markAsReadIDs); err != nil {
l.Warnf("Cannot mark messages as read: %v", err)
}
}
return nil
}
// apiIDsFromSeqSet takes an IMAP sequence set (which can contain either
// sequence numbers or UIDs) and returns all known API IDs in this range.
func (im *imapMailbox) apiIDsFromSeqSet(uid bool, seqSet *imap.SeqSet) ([]string, error) {
apiIDs := []string{}
for _, seq := range seqSet.Set {
var newAPIIDs []string
var err error
if uid {
newAPIIDs, err = im.storeMailbox.GetAPIIDsFromUIDRange(seq.Start, seq.Stop)
} else {
newAPIIDs, err = im.storeMailbox.GetAPIIDsFromSequenceRange(seq.Start, seq.Stop)
}
if err != nil {
return []string{}, err
}
apiIDs = append(apiIDs, newAPIIDs...)
}
if len(apiIDs) == 0 {
log.Debugf("Requested empty message list: %v %v", uid, seqSet)
}
return apiIDs, nil
}
func isAddressInList(addrs []*mail.Address, query string) bool { //nolint[deadcode]
for _, addr := range addrs {
if strings.Contains(addr.Address, query) || strings.Contains(addr.Name, query) {
return true
}
}
return false
}
func isStringInList(list []string, s string) bool {
for _, v := range list {
if v == s {
return true
}
}
return false
}
func addressMatch(addresses []*mail.Address, criteria string) bool {
for _, addr := range addresses {
if strings.Contains(strings.ToLower(addr.String()), strings.ToLower(criteria)) {
return true
}
}
return false
}
func hasKeyword(m *pmapi.Message, keyword string) bool {
for _, v := range message.GetHeader(m) {
if strings.Contains(strings.ToLower(strings.Join(v, " ")), strings.ToLower(keyword)) {
return true
}
}
return false
}

View File

@ -0,0 +1,120 @@
// 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 imap
import (
"errors"
"time"
"github.com/ProtonMail/proton-bridge/internal/store"
imap "github.com/emersion/go-imap"
)
// The mailbox containing all custom folders or labels.
// The purpose of this mailbox is to see "Folders" and "Labels"
// at the root of the mailbox tree, e.g.:
//
// Folders << this
// Folders/Family
//
// Labels << this
// Labels/Security
//
// This mailbox cannot be modified or read in any way.
type imapRootMailbox struct {
isFolder bool
}
func newFoldersRootMailbox() *imapRootMailbox {
return &imapRootMailbox{isFolder: true}
}
func newLabelsRootMailbox() *imapRootMailbox {
return &imapRootMailbox{isFolder: false}
}
func (m *imapRootMailbox) Name() string {
if m.isFolder {
return store.UserFoldersMailboxName
}
return store.UserLabelsMailboxName
}
func (m *imapRootMailbox) Info() (info *imap.MailboxInfo, err error) {
info = &imap.MailboxInfo{
Attributes: []string{imap.NoSelectAttr},
Delimiter: store.PathDelimiter,
}
if m.isFolder {
info.Name = store.UserFoldersMailboxName
} else {
info.Name = store.UserLabelsMailboxName
}
return
}
func (m *imapRootMailbox) Status(items []string) (status *imap.MailboxStatus, err error) {
status = &imap.MailboxStatus{}
if m.isFolder {
status.Name = store.UserFoldersMailboxName
} else {
status.Name = store.UserLabelsMailboxName
}
return
}
func (m *imapRootMailbox) Subscribe() error {
return errors.New("cannot subscribe to Labels or Folders mailboxes")
}
func (m *imapRootMailbox) Unsubscribe() error {
return errors.New("cannot unsubscribe from Labels or Folders mailboxes")
}
func (m *imapRootMailbox) Check() error {
return nil
}
func (m *imapRootMailbox) ListMessages(uid bool, seqset *imap.SeqSet, items []string, ch chan<- *imap.Message) error {
close(ch)
return nil
}
func (m *imapRootMailbox) SearchMessages(uid bool, criteria *imap.SearchCriteria) (ids []uint32, err error) {
return
}
func (m *imapRootMailbox) CreateMessage(flags []string, t time.Time, body imap.Literal) error {
return errors.New("cannot create a message in this mailbox")
}
func (m *imapRootMailbox) UpdateMessagesFlags(uid bool, seqset *imap.SeqSet, op imap.FlagsOp, flags []string) (err error) {
return errors.New("cannot update message flags in this mailbox")
}
func (m *imapRootMailbox) CopyMessages(uid bool, seqset *imap.SeqSet, dest string) error {
return nil
}
// Expunge is not used by Bridge. We delete the message once it is flagged as \Deleted.
func (m *imapRootMailbox) Expunge() error {
return nil
}

188
internal/imap/server.go Normal file
View File

@ -0,0 +1,188 @@
// 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 imap
import (
"crypto/tls"
"fmt"
"io"
"strings"
"time"
imapid "github.com/ProtonMail/go-imap-id"
"github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/imap/uidplus"
"github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/emersion/go-imap"
imapappendlimit "github.com/emersion/go-imap-appendlimit"
imapidle "github.com/emersion/go-imap-idle"
imapquota "github.com/emersion/go-imap-quota"
imapspecialuse "github.com/emersion/go-imap-specialuse"
imapserver "github.com/emersion/go-imap/server"
"github.com/emersion/go-sasl"
"github.com/sirupsen/logrus"
)
type imapServer struct {
server *imapserver.Server
eventListener listener.Listener
}
// NewIMAPServer constructs a new IMAP server configured with the given options.
func NewIMAPServer(debugClient, debugServer bool, port int, tls *tls.Config, imapBackend *imapBackend, eventListener listener.Listener) *imapServer { //nolint[golint]
s := imapserver.New(imapBackend)
s.Addr = fmt.Sprintf("%v:%v", bridge.Host, port)
s.TLSConfig = tls
s.AllowInsecureAuth = true
s.ErrorLog = newServerErrorLogger("server-imap")
s.AutoLogout = 30 * time.Minute
if debugClient || debugServer {
var localDebug, remoteDebug imap.WriterWithFields
if debugClient {
remoteDebug = &logWithFields{log: log.WithField("pkg", "imap/client"), fields: logrus.Fields{}}
}
if debugServer {
localDebug = &logWithFields{log: log.WithField("pkg", "imap/server"), fields: logrus.Fields{}}
}
s.Debug = imap.NewDebugWithFields(localDebug, remoteDebug)
}
serverID := imapid.ID{
imapid.FieldName: "ProtonMail",
imapid.FieldVendor: "Proton Technologies AG",
imapid.FieldSupportURL: "https://protonmail.com/support",
}
s.EnableAuth(sasl.Login, func(conn imapserver.Conn) sasl.Server {
conn.Server().ForEachConn(func(candidate imapserver.Conn) {
if id, ok := candidate.(imapid.Conn); ok {
if conn.Context() == candidate.Context() {
imapBackend.setLastMailClient(id.ID())
return
}
}
})
return sasl.NewLoginServer(func(address, password string) error {
user, err := conn.Server().Backend.Login(address, password)
if err != nil {
return err
}
ctx := conn.Context()
ctx.State = imap.AuthenticatedState
ctx.User = user
return nil
})
})
s.Enable(
imapidle.NewExtension(),
//imapmove.NewExtension(), // extension is not fully implemented: if UIDPLUS exists it MUST return COPYUID and EXPUNGE continuous responses
imapspecialuse.NewExtension(),
imapid.NewExtension(serverID),
imapquota.NewExtension(),
imapappendlimit.NewExtension(),
uidplus.NewExtension(),
)
return &imapServer{
server: s,
eventListener: eventListener,
}
}
// Starts the server.
func (s *imapServer) ListenAndServe() {
go s.monitorDisconnectedUsers()
log.Info("IMAP server listening at ", s.server.Addr)
err := s.server.ListenAndServe()
if err != nil {
s.eventListener.Emit(events.ErrorEvent, "IMAP failed: "+err.Error())
log.Error("IMAP failed: ", err)
return
}
defer s.server.Close() //nolint[errcheck]
log.Info("IMAP server stopped")
}
// Stops the server.
func (s *imapServer) Close() {
_ = s.server.Close()
}
func (s *imapServer) monitorDisconnectedUsers() {
ch := make(chan string)
s.eventListener.Add(events.CloseConnectionEvent, ch)
for address := range ch {
address := address
log.Info("Disconnecting all open IMAP connections for ", address)
disconnectUser := func(conn imapserver.Conn) {
connUser := conn.Context().User
if connUser != nil && strings.EqualFold(connUser.Username(), address) {
_ = conn.Close()
}
}
s.server.ForEachConn(disconnectUser)
}
}
// logWithFields is used for debuging with additional field.
type logWithFields struct {
log *logrus.Entry
fields logrus.Fields
}
func (lf *logWithFields) Writer() io.Writer {
w := lf.log.WithFields(lf.fields).WriterLevel(logrus.DebugLevel)
lf.fields = logrus.Fields{}
return w
}
func (lf *logWithFields) SetField(key, value string) {
lf.fields[key] = value
}
// serverErrorLogger implements go-imap/logger interface.
type serverErrorLogger struct {
tag string
}
func newServerErrorLogger(tag string) *serverErrorLogger {
return &serverErrorLogger{tag}
}
func (s *serverErrorLogger) CheckErrorForReport(serverErr string) {
}
func (s *serverErrorLogger) Printf(format string, args ...interface{}) {
err := fmt.Sprintf(format, args...)
s.CheckErrorForReport(err)
log.WithField("pkg", s.tag).Error(err)
}
func (s *serverErrorLogger) Println(args ...interface{}) {
err := fmt.Sprintln(args...)
s.CheckErrorForReport(err)
log.WithField("pkg", s.tag).Error(err)
}

156
internal/imap/store.go Normal file
View File

@ -0,0 +1,156 @@
// 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 imap
import (
"io"
"net/mail"
pmcrypto "github.com/ProtonMail/gopenpgp/crypto"
"github.com/ProtonMail/proton-bridge/internal/imap/uidplus"
"github.com/ProtonMail/proton-bridge/internal/store"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
)
type storeUserProvider interface {
UserID() string
GetSpace() (usedSpace, maxSpace uint, err error)
GetMaxUpload() (uint, error)
GetAddress(addressID string) (storeAddressProvider, error)
CreateDraft(
kr *pmcrypto.KeyRing,
message *pmapi.Message,
attachmentReaders []io.Reader,
attachedPublicKey,
attachedPublicKeyName string,
parentID string) (*pmapi.Message, []*pmapi.Attachment, error)
}
type storeAddressProvider interface {
AddressString() string
AddressID() string
APIAddress() *pmapi.Address
CreateMailbox(name string) error
ListMailboxes() []storeMailboxProvider
GetMailbox(name string) (storeMailboxProvider, error)
}
type storeMailboxProvider interface {
LabelID() string
Name() string
Color() string
IsSystem() bool
IsFolder() bool
UIDValidity() uint32
Rename(newName string) error
Delete() error
GetAPIIDsFromUIDRange(start, stop uint32) ([]string, error)
GetAPIIDsFromSequenceRange(start, stop uint32) ([]string, error)
GetLatestAPIID() (string, error)
GetNextUID() (uint32, error)
GetCounts() (dbTotal, dbUnread uint, err error)
GetUIDList(apiIDs []string) *uidplus.OrderedSeq
GetUIDByHeader(header *mail.Header) uint32
GetDelimiter() string
GetMessage(apiID string) (storeMessageProvider, error)
FetchMessage(apiID string) (storeMessageProvider, error)
LabelMessages(apiID []string) error
UnlabelMessages(apiID []string) error
MarkMessagesRead(apiID []string) error
MarkMessagesUnread(apiID []string) error
MarkMessagesStarred(apiID []string) error
MarkMessagesUnstarred(apiID []string) error
ImportMessage(msg *pmapi.Message, body []byte, labelIDs []string) error
DeleteMessages(apiID []string) error
}
type storeMessageProvider interface {
ID() string
UID() (uint32, error)
SequenceNumber() (uint32, error)
Message() *pmapi.Message
SetSize(int64) error
SetContentTypeAndHeader(string, mail.Header) error
}
type storeUserWrap struct {
*store.Store
}
// newStoreUserWrap wraps store struct into local storeUserWrap to implement local
// interface. The problem is that store returns the store package's Address type, so
// every method that returns an address has to be overridden to fulfill the interface.
// The same is true for other store structs i.e. storeAddress or storeMailbox.
func newStoreUserWrap(store *store.Store) *storeUserWrap {
return &storeUserWrap{Store: store}
}
func (s *storeUserWrap) GetAddress(addressID string) (storeAddressProvider, error) {
address, err := s.Store.GetAddress(addressID)
if err != nil {
return nil, err
}
return newStoreAddressWrap(address), nil
}
type storeAddressWrap struct {
*store.Address
}
func newStoreAddressWrap(address *store.Address) *storeAddressWrap {
return &storeAddressWrap{Address: address}
}
func (s *storeAddressWrap) ListMailboxes() []storeMailboxProvider {
mailboxes := []storeMailboxProvider{}
for _, mailbox := range s.Address.ListMailboxes() {
mailboxes = append(mailboxes, newStoreMailboxWrap(mailbox))
}
return mailboxes
}
func (s *storeAddressWrap) GetMailbox(name string) (storeMailboxProvider, error) {
mailbox, err := s.Address.GetMailbox(name)
if err != nil {
return nil, err
}
return newStoreMailboxWrap(mailbox), nil
}
type storeMailboxWrap struct {
*store.Mailbox
}
func newStoreMailboxWrap(mailbox *store.Mailbox) *storeMailboxWrap {
return &storeMailboxWrap{Mailbox: mailbox}
}
func (s *storeMailboxWrap) GetMessage(apiID string) (storeMessageProvider, error) {
return s.Mailbox.GetMessage(apiID)
}
func (s *storeMailboxWrap) FetchMessage(apiID string) (storeMessageProvider, error) {
return s.Mailbox.FetchMessage(apiID)
}

View File

@ -0,0 +1,198 @@
// 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 uidplus DOES NOT implement full RFC4315!
//
// Excluded parts are:
// * Response `UIDNOTSTICKY`: All mailboxes of Bridge support stable
// UIDVALIDITY so it would never return this response
//
// Otherwise the standard RFC4315 is followed.
package uidplus
import (
"fmt"
"github.com/emersion/go-imap"
"github.com/emersion/go-imap/server"
"github.com/sirupsen/logrus"
)
// Capability extension identifier
const Capability = "UIDPLUS"
const (
copyuid = "COPYUID"
appenduid = "APPENDUID"
copySuccess = "COPY successful"
appendSucess = "APPEND successful"
)
var log = logrus.WithField("pkg", "impa/uidplus") //nolint[gochecknoglobals]
// OrderedSeq to remember Seq in order they are added.
// We didn't find any restriction in RFC that server must respond with ranges
// so we decided to always do explicit list. This makes sure that no dynamic
// ranges or out of the bound ranges are possible.
//
// NOTE: potential issue with response length
// * the user selects large number of messages to be copied and the
// response line will be long,
// * list of UIDs which high values
// which can create long response line. We didn't find a maximum length of one
// IMAP response line or maximum length of IMAP "response code" with parameters.
type OrderedSeq []uint32
// Len return number of added seq numbers.
func (os OrderedSeq) Len() int { return len(os) }
// Add number to sequence. Zero is not acceptable UID and it won't be added to list.
func (os *OrderedSeq) Add(num uint32) {
if num == 0 {
return
}
*os = append(*os, num)
}
func (os *OrderedSeq) String() string {
out := ""
if len(*os) == 0 {
return out
}
lastS := uint32(0)
isRangeOpened := false
for i, s := range *os {
// write first
if i == 0 {
out += fmt.Sprintf("%d", s)
isRangeOpened = false
lastS = s
continue
}
isLast := (i == len(*os)-1)
isContinuous := (lastS+1 == s)
if isContinuous {
isRangeOpened = true
lastS = s
if isLast {
out += fmt.Sprintf(":%d", s)
}
continue
}
if isRangeOpened && !isContinuous { // close range
out += fmt.Sprintf(":%d,%d", lastS, s)
isRangeOpened = false
lastS = s
continue
}
// Range is not opened and it is not continuous.
out += fmt.Sprintf(",%d", s)
isRangeOpened = false
lastS = s
}
return out
}
// UIDExpunge implements server.Handler but has no effect because Bridge is not
// using EXPUNGE at all. The message is deleted right after it was flagged as
// \Deleted Bridge should simply ignore this command with empty `OK` response.
//
// If not implemented it would cause harmless IMAP error.
//
// This overrides the standard EXPUNGE functionality.
type UIDExpunge struct{}
func (e *UIDExpunge) Parse(fields []interface{}) error { log.Traceln("parse", fields); return nil }
func (e *UIDExpunge) Handle(conn server.Conn) error { log.Traceln("handle"); return nil }
func (e *UIDExpunge) UidHandle(conn server.Conn) error { log.Traceln("uid handle"); return nil } //nolint[golint]
type extension struct{}
// NewExtension of UIDPLUS.
func NewExtension() server.Extension {
return &extension{}
}
func (ext *extension) Capabilities(c server.Conn) []string {
if c.Context().State&imap.AuthenticatedState != 0 {
return []string{Capability}
}
return nil
}
func (ext *extension) Command(name string) server.HandlerFactory {
if name == imap.Expunge {
return func() server.Handler {
return &UIDExpunge{}
}
}
return nil
}
func getStatusResponseCopy(uidValidity uint32, sourceSeq, targetSeq *OrderedSeq) *imap.StatusResp {
info := copySuccess
if sourceSeq.Len() != 0 && targetSeq.Len() != 0 &&
sourceSeq.Len() == targetSeq.Len() {
info = fmt.Sprintf("[%s %d %s %s] %s",
copyuid,
uidValidity,
sourceSeq.String(),
targetSeq.String(),
copySuccess,
)
}
return &imap.StatusResp{
Type: imap.StatusOk,
Info: info,
}
}
// CopyResponse prepares OK response with extended UID information about copied message.
func CopyResponse(uidValidity uint32, sourceSeq, targetSeq *OrderedSeq) error {
return server.ErrStatusResp(getStatusResponseCopy(uidValidity, sourceSeq, targetSeq))
}
func getStatusResponseAppend(uidValidity uint32, targetSeq *OrderedSeq) *imap.StatusResp {
info := appendSucess
if targetSeq.Len() > 0 {
info = fmt.Sprintf("[%s %d %s] %s",
appenduid,
uidValidity,
targetSeq.String(),
appendSucess,
)
}
return &imap.StatusResp{
Type: imap.StatusOk,
Info: info,
}
}
// AppendResponse prepares OK response with extended UID information about appended message.
func AppendResponse(uidValidity uint32, targetSeq *OrderedSeq) error {
return server.ErrStatusResp(getStatusResponseAppend(uidValidity, targetSeq))
}

View File

@ -0,0 +1,108 @@
// 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 uidplus
import (
"testing"
"github.com/stretchr/testify/assert"
)
// uidValidity is constant and global for bridge IMAP.
const uidValidity = 66
type testResponseData struct {
sourceList, targetList []int
expCopyInfo, expAppendInfo string
}
func (td *testResponseData) getOrdSeqFromList(seqList []int) *OrderedSeq {
set := &OrderedSeq{}
for _, seq := range seqList {
set.Add(uint32(seq))
}
return set
}
func (td *testResponseData) testCopyAndAppendResponses(tb testing.TB) {
sourceSeq := td.getOrdSeqFromList(td.sourceList)
targetSeq := td.getOrdSeqFromList(td.targetList)
gotCopyResp := getStatusResponseCopy(uidValidity, sourceSeq, targetSeq)
assert.Equal(tb, td.expCopyInfo, gotCopyResp.Info, "source: %v\ntarget: %v", td.sourceList, td.targetList)
gotAppendResp := getStatusResponseAppend(uidValidity, targetSeq)
assert.Equal(tb, td.expAppendInfo, gotAppendResp.Info, "source: %v\ntarget: %v", td.sourceList, td.targetList)
}
func TestStatusResponseInfo(t *testing.T) {
testData := []*testResponseData{
{ // Dynamic range must never be returned e.g 4:* (explicitly true if you OrderedSeq used instead of imap.SeqSet).
sourceList: []int{4, 5, 6},
targetList: []int{1, 2, 3},
expCopyInfo: "[" + copyuid + " 66 4:6 1:3] " + copySuccess,
expAppendInfo: "[" + appenduid + " 66 1:3] " + appendSucess,
},
{ // Ranges can be used only for consecutive strictly rising sequence.
sourceList: []int{6, 7, 8, 9, 10, 1, 3, 5, 10, 11, 20, 21, 30, 31},
targetList: []int{1, 2, 3, 4, 50, 8, 7, 6, 12, 13, 22, 23, 32, 33},
expCopyInfo: "[" + copyuid + " 66 6:10,1,3,5,10:11,20:21,30:31 1:4,50,8,7,6,12:13,22:23,32:33] " + copySuccess,
expAppendInfo: "[" + appenduid + " 66 1:4,50,8,7,6,12:13,22:23,32:33] " + appendSucess,
},
{ // Keep order (cannot use sequence set because 3,2,1 equals 1,2,3 equals 1:3 equals 3:1).
sourceList: []int{4, 5, 8},
targetList: []int{3, 2, 1},
expCopyInfo: "[" + copyuid + " 66 4:5,8 3,2,1] " + copySuccess,
expAppendInfo: "[" + appenduid + " 66 3,2,1] " + appendSucess,
},
{ // Incorrect count of source and target uids is wrong and we should not report it.
sourceList: []int{1},
targetList: []int{1, 2, 3},
expCopyInfo: copySuccess,
expAppendInfo: "[" + appenduid + " 66 1:3] " + appendSucess,
},
{
sourceList: []int{1, 2, 3},
targetList: []int{1},
expCopyInfo: copySuccess,
expAppendInfo: "[" + appenduid + " 66 1] " + appendSucess,
},
{ // One item should be always interpreted as one number (don't use imap.SeqSet because 1:1 means 1).
sourceList: []int{1},
targetList: []int{1},
expCopyInfo: "[" + copyuid + " 66 1 1] " + copySuccess,
expAppendInfo: "[" + appenduid + " 66 1] " + appendSucess,
},
{ // No UID is wrong we should not report it.
sourceList: []int{1},
targetList: []int{},
expCopyInfo: copySuccess,
expAppendInfo: appendSucess,
},
{ // Duplicates should be reported as list.
sourceList: []int{1, 1, 1},
targetList: []int{6, 6, 6},
expCopyInfo: "[" + copyuid + " 66 1,1,1 6,6,6] " + copySuccess,
expAppendInfo: "[" + appenduid + " 66 6,6,6] " + appendSucess,
},
}
for _, td := range testData {
td.testCopyAndAppendResponses(t)
}
}

239
internal/imap/user.go Normal file
View File

@ -0,0 +1,239 @@
// 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 imap
import (
"errors"
"strings"
"github.com/ProtonMail/proton-bridge/internal/bridge"
imapquota "github.com/emersion/go-imap-quota"
goIMAPBackend "github.com/emersion/go-imap/backend"
)
var (
errNoSuchMailbox = errors.New("no such mailbox") //nolint[gochecknoglobals]
)
type imapUser struct {
panicHandler panicHandler
backend *imapBackend
user bridgeUser
client bridge.PMAPIProvider
storeUser storeUserProvider
storeAddress storeAddressProvider
currentAddressLowercase string
}
// newIMAPUser returns struct implementing go-imap/user interface.
func newIMAPUser(
panicHandler panicHandler,
backend *imapBackend,
user bridgeUser,
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
}
client := user.GetTemporaryPMAPIClient()
return &imapUser{
panicHandler: panicHandler,
backend: backend,
user: user,
client: client,
storeUser: storeUser,
storeAddress: storeAddress,
currentAddressLowercase: strings.ToLower(address),
}, err
}
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 {
// Called from go-imap in goroutines - we need to handle panics for each function.
defer iu.panicHandler.HandlePanic()
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) {
// Called from go-imap in goroutines - we need to handle panics for each function.
defer iu.panicHandler.HandlePanic()
mailboxes := []goIMAPBackend.Mailbox{}
for _, storeMailbox := range iu.storeAddress.ListMailboxes() {
if showOnlySubcribed && !iu.isSubscribed(storeMailbox.LabelID()) {
continue
}
mailbox := newIMAPMailbox(iu.panicHandler, 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) {
// Called from go-imap in goroutines - we need to handle panics for each function.
defer iu.panicHandler.HandlePanic()
storeMailbox, err := iu.storeAddress.GetMailbox(name)
if err != nil {
log.WithField("name", name).WithError(err).Error("Could not get mailbox")
return
}
return newIMAPMailbox(iu.panicHandler, iu, storeMailbox), nil
}
// CreateMailbox creates a new mailbox.
func (iu *imapUser) CreateMailbox(name string) error {
// Called from go-imap in goroutines - we need to handle panics for each function.
defer iu.panicHandler.HandlePanic()
return iu.storeAddress.CreateMailbox(name)
}
// DeleteMailbox permanently removes the mailbox with the given name.
func (iu *imapUser) DeleteMailbox(name string) (err error) {
// Called from go-imap in goroutines - we need to handle panics for each function.
defer iu.panicHandler.HandlePanic()
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) {
// Called from go-imap in goroutines - we need to handle panics for each function.
defer iu.panicHandler.HandlePanic()
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) {
// Called from go-imap in goroutines - we need to handle panics for each function.
defer iu.panicHandler.HandlePanic()
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) {
// Called from go-imap in goroutines - we need to handle panics for each function.
defer iu.panicHandler.HandlePanic()
usedSpace, maxSpace, err := iu.storeUser.GetSpace()
if err != nil {
log.Error("Failed getting quota: ", err)
return nil, err
}
resources := make(map[string][2]uint32)
var list [2]uint32
list[0] = uint32(usedSpace / 1000)
list[1] = uint32(maxSpace / 1000)
resources[imapquota.ResourceStorage] = list
status := &imapquota.Status{
Name: "",
Resources: resources,
}
return status, nil
}
func (iu *imapUser) SetQuota(name string, resources map[string]uint32) error {
// Called from go-imap in goroutines - we need to handle panics for each function.
defer iu.panicHandler.HandlePanic()
return errors.New("quota cannot be set")
}
func (iu *imapUser) CreateMessageLimit() *uint32 {
// Called from go-imap in goroutines - we need to handle panics for each function.
defer iu.panicHandler.HandlePanic()
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
}

32
internal/imap/utils.go Normal file
View File

@ -0,0 +1,32 @@
// 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 imap
import (
"io"
"net/http"
"net/textproto"
)
func writeHeader(w io.Writer, h textproto.MIMEHeader) (err error) {
if err = http.Header(h).Write(w); err != nil {
return
}
_, err = io.WriteString(w, "\r\n")
return
}