proton-bridge/internal/services/imapservice/connector.go

853 lines
24 KiB
Go

// Copyright (c) 2024 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 imapservice
import (
"bytes"
"context"
"errors"
"fmt"
"net/mail"
"sync/atomic"
"time"
"github.com/ProtonMail/gluon/async"
"github.com/ProtonMail/gluon/connector"
"github.com/ProtonMail/gluon/imap"
"github.com/ProtonMail/gluon/rfc822"
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/ProtonMail/proton-bridge/v3/internal/services/sendrecorder"
"github.com/ProtonMail/proton-bridge/v3/internal/usertypes"
"github.com/ProtonMail/proton-bridge/v3/pkg/message"
"github.com/ProtonMail/proton-bridge/v3/pkg/message/parser"
"github.com/bradenaw/juniper/stream"
"github.com/bradenaw/juniper/xslices"
"github.com/sirupsen/logrus"
"golang.org/x/exp/slices"
)
// Connector contains all IMAP state required to satisfy sync and or imap queries.
type Connector struct {
addrID string
showAllMail uint32
flags imap.FlagSet
permFlags imap.FlagSet
attrs imap.FlagSet
identityState sharedIdentity
client APIClient
telemetry Telemetry
panicHandler async.PanicHandler
sendRecorder *sendrecorder.SendRecorder
addressMode usertypes.AddressMode
labels sharedLabels
updateCh *async.QueuedChannel[imap.Update]
log *logrus.Entry
sharedCache *SharedCache
syncState *SyncState
}
func NewConnector(
addrID string,
apiClient APIClient,
labels sharedLabels,
identityState sharedIdentity,
addressMode usertypes.AddressMode,
sendRecorder *sendrecorder.SendRecorder,
panicHandler async.PanicHandler,
telemetry Telemetry,
showAllMail bool,
syncState *SyncState,
) *Connector {
userID := identityState.UserID()
return &Connector{
identityState: identityState,
addrID: addrID,
showAllMail: b32(showAllMail),
flags: defaultMailboxFlags(),
permFlags: defaultMailboxPermanentFlags(),
attrs: defaultMailboxAttributes(),
client: apiClient,
telemetry: telemetry,
panicHandler: panicHandler,
sendRecorder: sendRecorder,
updateCh: async.NewQueuedChannel[imap.Update](
0,
0,
panicHandler,
fmt.Sprintf("connector-update-%v-%v", userID, addrID),
),
labels: labels,
addressMode: addressMode,
log: logrus.WithFields(logrus.Fields{
"gluon-connector": addressMode,
"addr-id": addrID,
"user-id": userID,
}),
sharedCache: NewSharedCached(),
syncState: syncState,
}
}
func (s *Connector) StateClose() {
s.log.Debug("Closing state")
s.updateCh.CloseAndDiscardQueued()
}
func (s *Connector) Init(ctx context.Context, cache connector.IMAPState) error {
s.sharedCache.Set(cache)
return cache.Write(ctx, func(ctx context.Context, write connector.IMAPStateWrite) error {
rd := s.labels.Read()
defer rd.Close()
mboxes, err := write.GetMailboxesWithoutAttrib(ctx)
if err != nil {
return err
}
// Attempt to fix bug when a vault got corrupted, but the sync state did not get reset leading to
// all labels being written to the root level. If we detect this happened, reset the sync state.
{
applied, err := fixGODT3003Labels(ctx, s.log, mboxes, rd, write)
if err != nil {
return err
}
if applied {
s.log.Debug("Patched folders/labels after GODT-3003 incident, resetting sync state.")
if err := s.syncState.ClearSyncStatus(ctx); err != nil {
return err
}
}
}
// Retroactively apply the forwarded flags to existing mailboxes so that the IMAP clients can recognize
// that they can store these flags now.
if err := write.AddFlagsToAllMailboxes(ctx, imap.ForwardFlagList...); err != nil {
return fmt.Errorf("failed to add \\Forward flag to all mailboxes:%w", err)
}
// Add forwarded flag as perm flags to all mailboxes.
if err := write.AddPermFlagsToAllMailboxes(ctx, imap.ForwardFlagList...); err != nil {
return fmt.Errorf("failed to add \\Forward permanent flag to all mailboxes:%w", err)
}
return nil
})
}
func (s *Connector) Authorize(ctx context.Context, username string, password []byte) bool {
addrID, err := s.identityState.CheckAuth(username, password)
if err != nil {
s.telemetry.ReportConfigStatusFailure("IMAP " + err.Error())
return false
}
if s.addressMode == usertypes.AddressModeSplit && addrID != s.addrID {
return false
}
s.telemetry.SendConfigStatusSuccess(ctx)
return true
}
func (s *Connector) CreateMailbox(ctx context.Context, _ connector.IMAPStateWrite, name []string) (imap.Mailbox, error) {
if len(name) < 2 {
return imap.Mailbox{}, fmt.Errorf("invalid mailbox name %q: %w", name, connector.ErrOperationNotAllowed)
}
switch name[0] {
case folderPrefix:
return s.createFolder(ctx, name[1:])
case labelPrefix:
return s.createLabel(ctx, name[1:])
default:
return imap.Mailbox{}, fmt.Errorf("invalid mailbox name %q: %w", name, connector.ErrOperationNotAllowed)
}
}
func (s *Connector) GetMessageLiteral(ctx context.Context, id imap.MessageID) ([]byte, error) {
msg, err := s.client.GetFullMessage(ctx, string(id), usertypes.NewProtonAPIScheduler(s.panicHandler), proton.NewDefaultAttachmentAllocator())
if err != nil {
return nil, err
}
var literal []byte
err = s.identityState.WithAddrKR(msg.AddressID, func(_, addrKR *crypto.KeyRing) error {
l, buildErr := message.DecryptAndBuildRFC822(addrKR, msg.Message, msg.AttData, defaultMessageJobOpts())
if buildErr != nil {
return buildErr
}
literal = l
return nil
})
return literal, err
}
func (s *Connector) GetMailboxVisibility(_ context.Context, mboxID imap.MailboxID) imap.MailboxVisibility {
switch mboxID {
case proton.AllMailLabel:
if atomic.LoadUint32(&s.showAllMail) != 0 {
return imap.Visible
}
return imap.Hidden
case proton.AllScheduledLabel:
return imap.HiddenIfEmpty
default:
return imap.Visible
}
}
func (s *Connector) UpdateMailboxName(ctx context.Context, _ connector.IMAPStateWrite, mboxID imap.MailboxID, name []string) error {
if len(name) < 2 {
return fmt.Errorf("invalid mailbox name %q: %w", name, connector.ErrOperationNotAllowed)
}
switch name[0] {
case folderPrefix:
return s.updateFolder(ctx, mboxID, name[1:])
case labelPrefix:
return s.updateLabel(ctx, mboxID, name[1:])
default:
return fmt.Errorf("invalid mailbox name %q: %w", name, connector.ErrOperationNotAllowed)
}
}
func (s *Connector) DeleteMailbox(ctx context.Context, _ connector.IMAPStateWrite, mboxID imap.MailboxID) error {
if err := s.client.DeleteLabel(ctx, string(mboxID)); err != nil {
return err
}
wLabels := s.labels.Write()
defer wLabels.Close()
wLabels.Delete(string(mboxID))
return nil
}
func (s *Connector) CreateMessage(ctx context.Context, _ connector.IMAPStateWrite, mailboxID imap.MailboxID, literal []byte, flags imap.FlagSet, _ time.Time) (imap.Message, []byte, error) {
if mailboxID == proton.AllMailLabel {
return imap.Message{}, nil, connector.ErrOperationNotAllowed
}
toList, err := getLiteralToList(literal)
if err != nil {
return imap.Message{}, nil, fmt.Errorf("failed to retrieve addresses from literal:%w", err)
}
// Compute the hash of the message (to match it against SMTP messages).
hash, err := sendrecorder.GetMessageHash(literal)
if err != nil {
return imap.Message{}, nil, err
}
// Check if we already tried to send this message recently.
if messageID, ok, err := s.sendRecorder.HasEntryWait(ctx, hash, time.Now().Add(90*time.Second), toList); err != nil {
return imap.Message{}, nil, fmt.Errorf("failed to check send hash: %w", err)
} else if ok {
s.log.WithField("messageID", messageID).Warn("Message already sent")
// Query the server-side message.
full, err := s.client.GetFullMessage(ctx, messageID, usertypes.NewProtonAPIScheduler(s.panicHandler), proton.NewDefaultAttachmentAllocator())
if err != nil {
return imap.Message{}, nil, fmt.Errorf("failed to fetch message: %w", err)
}
// Build the message as it is on the server.
if err := s.identityState.WithAddrKR(full.AddressID, func(_, addrKR *crypto.KeyRing) error {
var err error
if literal, err = message.DecryptAndBuildRFC822(addrKR, full.Message, full.AttData, defaultMessageJobOpts()); err != nil {
return err
}
return nil
}); err != nil {
return imap.Message{}, nil, fmt.Errorf("failed to build message: %w", err)
}
return toIMAPMessage(full.MessageMetadata), literal, nil
}
wantLabelIDs := []string{string(mailboxID)}
if flags.Contains(imap.FlagFlagged) {
wantLabelIDs = append(wantLabelIDs, proton.StarredLabel)
}
var wantFlags proton.MessageFlag
unread := !flags.Contains(imap.FlagSeen)
if mailboxID != proton.DraftsLabel {
header, err := rfc822.Parse(literal).ParseHeader()
if err != nil {
return imap.Message{}, nil, err
}
switch {
case mailboxID == proton.InboxLabel:
wantFlags = wantFlags.Add(proton.MessageFlagReceived)
case mailboxID == proton.SentLabel:
wantFlags = wantFlags.Add(proton.MessageFlagSent)
case header.Has("Received"):
wantFlags = wantFlags.Add(proton.MessageFlagReceived)
default:
wantFlags = wantFlags.Add(proton.MessageFlagSent)
}
} else {
unread = false
}
if flags.Contains(imap.FlagAnswered) {
wantFlags = wantFlags.Add(proton.MessageFlagReplied)
}
msg, literal, err := s.importMessage(ctx, literal, wantLabelIDs, wantFlags, unread)
if err != nil {
if errors.Is(err, proton.ErrImportSizeExceeded) {
// Remap error so that Gluon does not put this message in the recovery mailbox.
err = fmt.Errorf("%v: %w", err, connector.ErrMessageSizeExceedsLimits)
}
if apiErr := new(proton.APIError); errors.As(err, &apiErr) {
s.log.WithError(apiErr).WithField("Details", apiErr.DetailsToString()).Error("Failed to import message")
} else {
s.log.WithError(err).Error("Failed to import message")
}
}
return msg, literal, err
}
func (s *Connector) AddMessagesToMailbox(ctx context.Context, _ connector.IMAPStateWrite, messageIDs []imap.MessageID, mboxID imap.MailboxID) error {
if isAllMailOrScheduled(mboxID) {
return connector.ErrOperationNotAllowed
}
return s.client.LabelMessages(ctx, usertypes.MapTo[imap.MessageID, string](messageIDs), string(mboxID))
}
func (s *Connector) RemoveMessagesFromMailbox(ctx context.Context, _ connector.IMAPStateWrite, messageIDs []imap.MessageID, mboxID imap.MailboxID) error {
if isAllMailOrScheduled(mboxID) {
return connector.ErrOperationNotAllowed
}
msgIDs := usertypes.MapTo[imap.MessageID, string](messageIDs)
if err := s.client.UnlabelMessages(ctx, msgIDs, string(mboxID)); err != nil {
return err
}
if mboxID == proton.TrashLabel || mboxID == proton.DraftsLabel {
const ChunkSize = 150
var msgToPermaDelete []string
rdLabels := s.labels.Read()
defer rdLabels.Close()
// There's currently no limit on how many IDs we can filter on,
// but to be nice to API, let's chunk it by 150.
for _, messageIDs := range xslices.Chunk(messageIDs, ChunkSize) {
metadata, err := s.client.GetMessageMetadataPage(ctx, 0, ChunkSize, proton.MessageFilter{
ID: usertypes.MapTo[imap.MessageID, string](messageIDs),
})
if err != nil {
return err
}
// If a message is not preset in any other label other than AllMail, AllDrafts and AllSent, it can be
// permanently deleted.
for _, m := range metadata {
var remainingLabels []string
for _, id := range m.LabelIDs {
label, ok := rdLabels.GetLabel(id)
if !ok {
// Handle case where this label was newly introduced and we do not yet know about it.
logrus.WithField("labelID", id).Warnf("Unknown label found during expung from Trash, attempting to locate it")
label, err = s.client.GetLabel(ctx, id, proton.LabelTypeFolder, proton.LabelTypeSystem, proton.LabelTypeSystem)
if err != nil {
if errors.Is(err, proton.ErrNoSuchLabel) {
logrus.WithField("labelID", id).Warn("Label does not exist, ignoring")
continue
}
logrus.WithField("labelID", id).Errorf("Failed to resolve label: %v", err)
return fmt.Errorf("failed to resolve label: %w", err)
}
}
if !WantLabel(label) {
continue
}
if label.Type == proton.LabelTypeSystem && (id == proton.AllDraftsLabel ||
id == proton.AllMailLabel ||
id == proton.AllSentLabel ||
id == proton.AllScheduledLabel) {
continue
}
remainingLabels = append(remainingLabels, m.ID)
}
if len(remainingLabels) == 0 {
msgToPermaDelete = append(msgToPermaDelete, m.ID)
}
}
}
if len(msgToPermaDelete) != 0 {
logrus.Debugf("Following message(s) will be perma-deleted: %v", msgToPermaDelete)
if err := s.client.DeleteMessage(ctx, msgToPermaDelete...); err != nil {
return err
}
}
}
return nil
}
func (s *Connector) MoveMessages(ctx context.Context, _ connector.IMAPStateWrite, messageIDs []imap.MessageID, mboxFromID, mboxToID imap.MailboxID) (bool, error) {
if (mboxFromID == proton.InboxLabel && mboxToID == proton.SentLabel) ||
(mboxFromID == proton.SentLabel && mboxToID == proton.InboxLabel) ||
isAllMailOrScheduled(mboxFromID) ||
isAllMailOrScheduled(mboxToID) {
return false, connector.ErrOperationNotAllowed
}
shouldExpungeOldLocation := func() bool {
rdLabels := s.labels.Read()
defer rdLabels.Close()
var result bool
if v, ok := rdLabels.GetLabel(string(mboxFromID)); ok && v.Type == proton.LabelTypeLabel {
result = true
}
if v, ok := rdLabels.GetLabel(string(mboxToID)); ok && (v.Type == proton.LabelTypeFolder || v.Type == proton.LabelTypeSystem) {
result = true
}
return result
}()
if err := s.client.LabelMessages(ctx, usertypes.MapTo[imap.MessageID, string](messageIDs), string(mboxToID)); err != nil {
return false, fmt.Errorf("labeling messages: %w", err)
}
if shouldExpungeOldLocation {
if err := s.client.UnlabelMessages(ctx, usertypes.MapTo[imap.MessageID, string](messageIDs), string(mboxFromID)); err != nil {
return false, fmt.Errorf("unlabeling messages: %w", err)
}
}
return shouldExpungeOldLocation, nil
}
func (s *Connector) MarkMessagesSeen(ctx context.Context, _ connector.IMAPStateWrite, messageIDs []imap.MessageID, seen bool) error {
if seen {
return s.client.MarkMessagesRead(ctx, usertypes.MapTo[imap.MessageID, string](messageIDs)...)
}
return s.client.MarkMessagesUnread(ctx, usertypes.MapTo[imap.MessageID, string](messageIDs)...)
}
func (s *Connector) MarkMessagesFlagged(ctx context.Context, _ connector.IMAPStateWrite, messageIDs []imap.MessageID, flagged bool) error {
if flagged {
return s.client.LabelMessages(ctx, usertypes.MapTo[imap.MessageID, string](messageIDs), proton.StarredLabel)
}
return s.client.UnlabelMessages(ctx, usertypes.MapTo[imap.MessageID, string](messageIDs), proton.StarredLabel)
}
func (s *Connector) MarkMessagesForwarded(ctx context.Context, _ connector.IMAPStateWrite, messageIDs []imap.MessageID, flagged bool) error {
if flagged {
return s.client.MarkMessagesForwarded(ctx, usertypes.MapTo[imap.MessageID, string](messageIDs)...)
}
return s.client.MarkMessagesUnForwarded(ctx, usertypes.MapTo[imap.MessageID, string](messageIDs)...)
}
func (s *Connector) GetUpdates() <-chan imap.Update {
return s.updateCh.GetChannel()
}
func (s *Connector) Close(_ context.Context) error {
// Nothing to do
s.sharedCache.Close()
return nil
}
func (s *Connector) ShowAllMail(v bool) {
atomic.StoreUint32(&s.showAllMail, b32(v))
}
const (
folderPrefix = "Folders"
labelPrefix = "Labels"
)
// b32 returns a uint32 0 or 1 representing b.
func b32(b bool) uint32 {
if b {
return 1
}
return 0
}
func (s *Connector) createLabel(ctx context.Context, name []string) (imap.Mailbox, error) {
if len(name) != 1 {
return imap.Mailbox{}, fmt.Errorf("a label cannot have children: %w", connector.ErrOperationNotAllowed)
}
label, err := s.client.CreateLabel(ctx, proton.CreateLabelReq{
Name: name[0],
Color: "#f66",
Type: proton.LabelTypeLabel,
})
if err != nil {
return imap.Mailbox{}, err
}
wLabels := s.labels.Write()
defer wLabels.Close()
wLabels.SetLabel(label.ID, label)
return toIMAPMailbox(label, s.flags, s.permFlags, s.attrs), nil
}
func (s *Connector) createFolder(ctx context.Context, name []string) (imap.Mailbox, error) {
var parentID string
wLabels := s.labels.Write()
defer wLabels.Close()
if len(name) > 1 {
for _, label := range wLabels.GetLabels() {
if !slices.Equal(label.Path, name[:len(name)-1]) {
continue
}
parentID = label.ID
break
}
if parentID == "" {
return imap.Mailbox{}, fmt.Errorf("parent folder %q does not exist: %w", name[:len(name)-1], connector.ErrOperationNotAllowed)
}
}
label, err := s.client.CreateLabel(ctx, proton.CreateLabelReq{
Name: name[len(name)-1],
Color: "#f66",
Type: proton.LabelTypeFolder,
ParentID: parentID,
})
if err != nil {
return imap.Mailbox{}, err
}
// Add label to list so subsequent sub folder create requests work correct.
wLabels.SetLabel(label.ID, label)
return toIMAPMailbox(label, s.flags, s.permFlags, s.attrs), nil
}
func (s *Connector) updateLabel(ctx context.Context, labelID imap.MailboxID, name []string) error {
if len(name) != 1 {
return fmt.Errorf("a label cannot have children: %w", connector.ErrOperationNotAllowed)
}
label, err := s.client.GetLabel(ctx, string(labelID), proton.LabelTypeLabel)
if err != nil {
return err
}
update, err := s.client.UpdateLabel(ctx, label.ID, proton.UpdateLabelReq{
Name: name[0],
Color: label.Color,
})
if err != nil {
return err
}
wLabels := s.labels.Write()
defer wLabels.Close()
wLabels.SetLabel(label.ID, update)
return nil
}
func (s *Connector) updateFolder(ctx context.Context, labelID imap.MailboxID, name []string) error {
var parentID string
wLabels := s.labels.Write()
defer wLabels.Close()
if len(name) > 1 {
for _, label := range wLabels.GetLabels() {
if !slices.Equal(label.Path, name[:len(name)-1]) {
continue
}
parentID = label.ID
break
}
if parentID == "" {
return fmt.Errorf("parent folder %q does not exist: %w", name[:len(name)-1], connector.ErrOperationNotAllowed)
}
}
label, err := s.client.GetLabel(ctx, string(labelID), proton.LabelTypeFolder)
if err != nil {
return err
}
update, err := s.client.UpdateLabel(ctx, string(labelID), proton.UpdateLabelReq{
Name: name[len(name)-1],
Color: label.Color,
ParentID: parentID,
})
if err != nil {
return err
}
wLabels.SetLabel(label.ID, update)
return nil
}
func (s *Connector) importMessage(
ctx context.Context,
literal []byte,
labelIDs []string,
flags proton.MessageFlag,
unread bool,
) (imap.Message, []byte, error) {
var full proton.FullMessage
addr, ok := s.identityState.GetAddress(s.addrID)
if !ok {
return imap.Message{}, nil, fmt.Errorf("could not find address")
}
if err := s.identityState.WithAddrKR(s.addrID, func(_, addrKR *crypto.KeyRing) error {
primaryKey, errKey := addrKR.FirstKey()
if errKey != nil {
return fmt.Errorf("failed to get primary key for import: %w", errKey)
}
var messageID string
p, err2 := parser.New(bytes.NewReader(literal))
if err2 != nil {
return fmt.Errorf("failed to parse literal: %w", err2)
}
if slices.Contains(labelIDs, proton.DraftsLabel) {
msg, err := s.createDraftWithParser(ctx, p, primaryKey, addr)
if err != nil {
return fmt.Errorf("failed to create draft: %w", err)
}
// apply labels
messageID = msg.ID
} else {
// multipart body requires at least one text part to be properly encrypted.
if p.AttachEmptyTextPartIfNoneExists() {
buf := new(bytes.Buffer)
if err := p.NewWriter().Write(buf); err != nil {
return fmt.Errorf("failed build new MIMEBody: %w", err)
}
literal = buf.Bytes()
}
str, err := s.client.ImportMessages(ctx, primaryKey, 1, 1, []proton.ImportReq{{
Metadata: proton.ImportMetadata{
AddressID: s.addrID,
LabelIDs: labelIDs,
Unread: proton.Bool(unread),
Flags: flags,
},
Message: literal,
}}...)
if err != nil {
return fmt.Errorf("failed to prepare message for import: %w", err)
}
res, err := stream.Collect(ctx, str)
if err != nil {
return fmt.Errorf("failed to import message: %w", err)
}
messageID = res[0].MessageID
}
var err error
if full, err = s.client.GetFullMessage(ctx, messageID, usertypes.NewProtonAPIScheduler(s.panicHandler), proton.NewDefaultAttachmentAllocator()); err != nil {
return fmt.Errorf("failed to fetch message: %w", err)
}
if literal, err = message.DecryptAndBuildRFC822(primaryKey, full.Message, full.AttData, defaultMessageJobOpts()); err != nil {
return fmt.Errorf("failed to build message: %w", err)
}
return nil
}); err != nil {
return imap.Message{}, nil, err
}
return toIMAPMessage(full.MessageMetadata), literal, nil
}
func (s *Connector) createDraftWithParser(ctx context.Context, parser *parser.Parser, addrKR *crypto.KeyRing, sender proton.Address) (proton.Message, error) {
message, err := message.ParseWithParser(parser, true)
if err != nil {
return proton.Message{}, fmt.Errorf("failed to parse message: %w", err)
}
decBody := string(message.PlainBody)
if message.RichBody != "" {
decBody = string(message.RichBody)
}
draft, err := s.client.CreateDraft(ctx, addrKR, proton.CreateDraftReq{
Message: proton.DraftTemplate{
Subject: message.Subject,
Body: decBody,
MIMEType: message.MIMEType,
Sender: &mail.Address{Name: sender.DisplayName, Address: sender.Email},
ToList: message.ToList,
CCList: message.CCList,
BCCList: message.BCCList,
ExternalID: message.ExternalID,
},
})
if err != nil {
return proton.Message{}, fmt.Errorf("failed to create draft: %w", err)
}
for _, att := range message.Attachments {
disposition := proton.AttachmentDisposition
if att.Disposition == "inline" && att.ContentID != "" {
disposition = proton.InlineDisposition
}
if _, err := s.client.UploadAttachment(ctx, addrKR, proton.CreateAttachmentReq{
MessageID: draft.ID,
Filename: att.Name,
MIMEType: rfc822.MIMEType(att.MIMEType),
Disposition: disposition,
ContentID: att.ContentID,
Body: att.Data,
}); err != nil {
return proton.Message{}, fmt.Errorf("failed to add attachment to draft: %w", err)
}
}
return draft, nil
}
func (s *Connector) publishUpdate(_ context.Context, update imap.Update) {
s.updateCh.Enqueue(update)
}
func fixGODT3003Labels(
ctx context.Context,
log *logrus.Entry,
mboxes []imap.MailboxNoAttrib,
rd labelsRead,
write connector.IMAPStateWrite,
) (bool, error) {
var applied bool
for _, mbox := range mboxes {
lbl, ok := rd.GetLabel(string(mbox.ID))
if !ok {
continue
}
if lbl.Type == proton.LabelTypeFolder {
if mbox.Name[0] != folderPrefix {
log.WithField("labelID", mbox.ID.ShortID()).Debug("Found folder without prefix, patching")
if err := write.PatchMailboxHierarchyWithoutTransforms(ctx, mbox.ID, xslices.Insert(mbox.Name, 0, folderPrefix)); err != nil {
return false, fmt.Errorf("failed to update mailbox name: %w", err)
}
applied = true
}
} else if lbl.Type == proton.LabelTypeLabel {
if mbox.Name[0] != labelPrefix {
log.WithField("labelID", mbox.ID.ShortID()).Debug("Found label without prefix, patching")
if err := write.PatchMailboxHierarchyWithoutTransforms(ctx, mbox.ID, xslices.Insert(mbox.Name, 0, labelPrefix)); err != nil {
return false, fmt.Errorf("failed to update mailbox name: %w", err)
}
applied = true
}
}
}
return applied, nil
}
func defaultMailboxFlags() imap.FlagSet {
f := imap.NewFlagSet(imap.FlagSeen, imap.FlagFlagged, imap.FlagDeleted)
f.AddToSelf(imap.ForwardFlagList...)
return f
}
func defaultMailboxPermanentFlags() imap.FlagSet {
return defaultMailboxFlags()
}
func defaultMailboxAttributes() imap.FlagSet {
return imap.NewFlagSet()
}