mirror of
https://github.com/ProtonMail/proton-bridge.git
synced 2025-12-10 12:46:46 +00:00
251 lines
7.1 KiB
Go
251 lines
7.1 KiB
Go
// Copyright (c) 2022 Proton AG
|
|
//
|
|
// This file is part of Proton Mail Bridge.
|
|
//
|
|
// Proton Mail 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.
|
|
//
|
|
// Proton Mail 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 Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
package imap
|
|
|
|
import (
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/ProtonMail/proton-bridge/v2/internal/store"
|
|
"github.com/ProtonMail/proton-bridge/v2/pkg/algo"
|
|
"github.com/ProtonMail/proton-bridge/v2/pkg/message"
|
|
"github.com/ProtonMail/proton-bridge/v2/pkg/pmapi"
|
|
imap "github.com/emersion/go-imap"
|
|
goIMAPBackend "github.com/emersion/go-imap/backend"
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
type operation string
|
|
|
|
const (
|
|
operationUpdateMessage operation = "store"
|
|
operationDeleteMessage operation = "expunge"
|
|
)
|
|
|
|
type imapUpdates struct {
|
|
lock sync.Locker
|
|
blocking map[string]bool
|
|
delayedExpunges map[string][]chan struct{}
|
|
ch chan goIMAPBackend.Update
|
|
ib *imapBackend
|
|
}
|
|
|
|
func newIMAPUpdates(ib *imapBackend) *imapUpdates {
|
|
return &imapUpdates{
|
|
lock: &sync.Mutex{},
|
|
blocking: map[string]bool{},
|
|
delayedExpunges: map[string][]chan struct{}{},
|
|
ch: make(chan goIMAPBackend.Update),
|
|
ib: ib,
|
|
}
|
|
}
|
|
|
|
func (iu *imapUpdates) block(address, mailboxName string, op operation) {
|
|
iu.lock.Lock()
|
|
defer iu.lock.Unlock()
|
|
|
|
iu.blocking[getBlockingKey(address, mailboxName, op)] = true
|
|
}
|
|
|
|
func (iu *imapUpdates) unblock(address, mailboxName string, op operation) {
|
|
iu.lock.Lock()
|
|
defer iu.lock.Unlock()
|
|
|
|
delete(iu.blocking, getBlockingKey(address, mailboxName, op))
|
|
}
|
|
|
|
func (iu *imapUpdates) isBlocking(address, mailboxName string, op operation) bool {
|
|
iu.lock.Lock()
|
|
defer iu.lock.Unlock()
|
|
|
|
return iu.blocking[getBlockingKey(address, mailboxName, op)]
|
|
}
|
|
|
|
func getBlockingKey(address, mailboxName string, op operation) string {
|
|
return strings.ToLower(address + "_" + mailboxName + "_" + string(op))
|
|
}
|
|
|
|
func (iu *imapUpdates) forbidExpunge(mailboxID string) {
|
|
iu.lock.Lock()
|
|
defer iu.lock.Unlock()
|
|
|
|
iu.delayedExpunges[mailboxID] = []chan struct{}{}
|
|
}
|
|
|
|
func (iu *imapUpdates) allowExpunge(mailboxID string) {
|
|
iu.lock.Lock()
|
|
defer iu.lock.Unlock()
|
|
|
|
for _, ch := range iu.delayedExpunges[mailboxID] {
|
|
close(ch)
|
|
}
|
|
delete(iu.delayedExpunges, mailboxID)
|
|
}
|
|
|
|
func (iu *imapUpdates) CanDelete(mailboxID string) (bool, func()) {
|
|
iu.lock.Lock()
|
|
defer iu.lock.Unlock()
|
|
|
|
if iu.delayedExpunges[mailboxID] == nil {
|
|
return true, nil
|
|
}
|
|
|
|
ch := make(chan struct{})
|
|
iu.delayedExpunges[mailboxID] = append(iu.delayedExpunges[mailboxID], ch)
|
|
return false, func() {
|
|
log.WithField("mailbox", mailboxID).Debug("Expunge operations paused")
|
|
<-ch
|
|
log.WithField("mailbox", mailboxID).Debug("Expunge operations unpaused")
|
|
}
|
|
}
|
|
|
|
func (iu *imapUpdates) Notice(address, notice string) {
|
|
l := iu.updateLog(address, "")
|
|
l.Info("Notice")
|
|
update := new(goIMAPBackend.StatusUpdate)
|
|
update.Update = goIMAPBackend.NewUpdate(address, "")
|
|
update.StatusResp = &imap.StatusResp{
|
|
Type: imap.StatusRespOk,
|
|
Code: imap.CodeAlert,
|
|
Info: notice,
|
|
}
|
|
iu.sendIMAPUpdate(l, update, false)
|
|
}
|
|
|
|
func (iu *imapUpdates) UpdateMessage(
|
|
address, mailboxName string,
|
|
uid, sequenceNumber uint32,
|
|
msg *pmapi.Message, hasDeletedFlag bool,
|
|
) {
|
|
l := iu.updateLog(address, mailboxName).
|
|
WithFields(logrus.Fields{
|
|
"seqNum": sequenceNumber,
|
|
"uid": uid,
|
|
"flags": message.GetFlags(msg),
|
|
"deleted": hasDeletedFlag,
|
|
})
|
|
l.Info("IDLE update")
|
|
update := new(goIMAPBackend.MessageUpdate)
|
|
update.Update = goIMAPBackend.NewUpdate(address, mailboxName)
|
|
update.Message = imap.NewMessage(sequenceNumber, []imap.FetchItem{imap.FetchFlags, imap.FetchUid})
|
|
update.Message.Flags = message.GetFlags(msg)
|
|
if hasDeletedFlag {
|
|
update.Message.Flags = append(update.Message.Flags, imap.DeletedFlag)
|
|
}
|
|
update.Message.Uid = uid
|
|
iu.sendIMAPUpdate(l, update, iu.isBlocking(address, mailboxName, operationUpdateMessage))
|
|
}
|
|
|
|
func (iu *imapUpdates) DeleteMessage(address, mailboxName string, sequenceNumber uint32) {
|
|
l := iu.updateLog(address, mailboxName).
|
|
WithField("seqNum", sequenceNumber)
|
|
l.Info("IDLE delete")
|
|
update := new(goIMAPBackend.ExpungeUpdate)
|
|
update.Update = goIMAPBackend.NewUpdate(address, mailboxName)
|
|
update.SeqNum = sequenceNumber
|
|
iu.sendIMAPUpdate(l, update, iu.isBlocking(address, mailboxName, operationDeleteMessage))
|
|
}
|
|
|
|
func (iu *imapUpdates) MailboxCreated(address, mailboxName string) {
|
|
l := iu.updateLog(address, mailboxName)
|
|
l.Info("IDLE mailbox info")
|
|
update := new(goIMAPBackend.MailboxInfoUpdate)
|
|
update.Update = goIMAPBackend.NewUpdate(address, "")
|
|
update.MailboxInfo = &imap.MailboxInfo{
|
|
Attributes: []string{imap.NoInferiorsAttr},
|
|
Delimiter: store.PathDelimiter,
|
|
Name: mailboxName,
|
|
}
|
|
iu.sendIMAPUpdate(l, update, false)
|
|
}
|
|
|
|
func (iu *imapUpdates) MailboxStatus(address, mailboxName string, total, unread, unreadSeqNum uint32) {
|
|
l := iu.updateLog(address, mailboxName).
|
|
WithFields(logrus.Fields{
|
|
"total": total,
|
|
"unread": unread,
|
|
"unreadSeqNum": unreadSeqNum,
|
|
})
|
|
l.Info("IDLE status")
|
|
update := new(goIMAPBackend.MailboxUpdate)
|
|
update.Update = goIMAPBackend.NewUpdate(address, mailboxName)
|
|
update.MailboxStatus = imap.NewMailboxStatus(mailboxName, []imap.StatusItem{imap.StatusMessages, imap.StatusUnseen})
|
|
update.MailboxStatus.Messages = total
|
|
update.MailboxStatus.Unseen = unread
|
|
update.MailboxStatus.UnseenSeqNum = unreadSeqNum
|
|
iu.sendIMAPUpdate(l, update, true)
|
|
}
|
|
|
|
func (iu *imapUpdates) sendIMAPUpdate(updateLog *logrus.Entry, update goIMAPBackend.Update, isBlocking bool) {
|
|
l := updateLog.WithField("blocking", isBlocking)
|
|
if iu.ch == nil {
|
|
l.Info("IMAP IDLE unavailable")
|
|
return
|
|
}
|
|
|
|
done := update.Done()
|
|
go func() {
|
|
select {
|
|
case <-time.After(1 * time.Second):
|
|
l.Warn("IMAP update could not be sent (timeout)")
|
|
return
|
|
case iu.ch <- update:
|
|
}
|
|
}()
|
|
|
|
if !isBlocking {
|
|
return
|
|
}
|
|
|
|
select {
|
|
case <-done:
|
|
case <-time.After(1 * time.Second):
|
|
l.Warn("IMAP update could not be delivered (timeout)")
|
|
return
|
|
}
|
|
}
|
|
|
|
func (iu *imapUpdates) getIDs(address, mailboxName string) (addressID, mailboxID string) {
|
|
addressID = "unknown-" + algo.HashBase64SHA256(address)
|
|
mailboxID = "unknown-" + algo.HashBase64SHA256(mailboxName)
|
|
|
|
if iu == nil || iu.ib == nil {
|
|
return
|
|
}
|
|
|
|
user, err := iu.ib.getUser(address)
|
|
if err != nil || user == nil || user.storeAddress == nil {
|
|
return
|
|
}
|
|
addressID = user.addressID
|
|
|
|
if v := user.mailboxIDs.get(mailboxName); v != "" {
|
|
mailboxID = v
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func (iu *imapUpdates) updateLog(address, mailboxName string) *logrus.Entry {
|
|
addressID, mailboxID := iu.getIDs(address, mailboxName)
|
|
return log.
|
|
WithField("address", addressID).
|
|
WithField("mailbox", mailboxID)
|
|
}
|