forked from Silverfish/proton-bridge
Add debug commands to CLI to diagnose potential bride problems. Currently we only have a command which validates whether the state of all the mailboxes reported by IMAP matches what is currently available on the proton servers.
221 lines
5.8 KiB
Go
221 lines
5.8 KiB
Go
// Copyright (c) 2023 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 bridge
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
|
|
"github.com/ProtonMail/gluon/imap"
|
|
"github.com/ProtonMail/gluon/rfc822"
|
|
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
|
"github.com/bradenaw/juniper/iterator"
|
|
goimap "github.com/emersion/go-imap"
|
|
goimapclient "github.com/emersion/go-imap/client"
|
|
"github.com/sirupsen/logrus"
|
|
"golang.org/x/exp/maps"
|
|
)
|
|
|
|
// CheckClientState checks the current IMAP client reported state against the proton server state and reports
|
|
// anything that is out of place.
|
|
func (bridge *Bridge) CheckClientState(ctx context.Context, checkFlags bool, progressCB func(string)) error {
|
|
bridge.usersLock.RLock()
|
|
defer bridge.usersLock.RUnlock()
|
|
|
|
users := maps.Values(bridge.users)
|
|
|
|
for _, usr := range users {
|
|
if progressCB != nil {
|
|
progressCB(fmt.Sprintf("Checking state for user %v", usr.Name()))
|
|
}
|
|
log := logrus.WithField("user", usr.Name()).WithField("diag", "state-check")
|
|
log.Debug("Retrieving all server metadata")
|
|
meta, err := usr.GetDiagnosticMetadata(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
success := true
|
|
|
|
if len(meta.Metadata) != len(meta.MessageIDs) {
|
|
log.Errorf("Metadata (%v) and message(%v) list sizes do not match", len(meta.Metadata), len(meta.MessageIDs))
|
|
}
|
|
|
|
log.Debug("Building state")
|
|
state, err := meta.BuildMailboxToMessageMap(usr)
|
|
if err != nil {
|
|
log.WithError(err).Error("Failed to build state")
|
|
return err
|
|
}
|
|
|
|
info, err := bridge.GetUserInfo(usr.ID())
|
|
if err != nil {
|
|
log.WithError(err).Error("Failed to get user info")
|
|
return err
|
|
}
|
|
|
|
addr := fmt.Sprintf("127.0.0.1:%v", bridge.GetIMAPPort())
|
|
|
|
for account, mboxMap := range state {
|
|
if progressCB != nil {
|
|
progressCB(fmt.Sprintf("Checking state for user %v's account '%v'", usr.Name(), account))
|
|
}
|
|
if err := func(account string, mboxMap user.AccountMailboxMap) error {
|
|
client, err := goimapclient.Dial(addr)
|
|
if err != nil {
|
|
log.WithError(err).Error("Failed to connect to imap client")
|
|
return err
|
|
}
|
|
|
|
defer func() {
|
|
_ = client.Logout()
|
|
}()
|
|
|
|
if err := client.Login(account, string(info.BridgePass)); err != nil {
|
|
return fmt.Errorf("failed to login for user %v:%w", usr.Name(), err)
|
|
}
|
|
|
|
log := log.WithField("account", account)
|
|
for mboxName, messageList := range mboxMap {
|
|
log := log.WithField("mbox", mboxName)
|
|
status, err := client.Select(mboxName, true)
|
|
if err != nil {
|
|
log.WithError(err).Errorf("Failed to select mailbox %v", messageList)
|
|
return fmt.Errorf("failed to select '%v':%w", mboxName, err)
|
|
}
|
|
|
|
log.Debug("Checking message count")
|
|
|
|
if int(status.Messages) != len(messageList) {
|
|
success = false
|
|
log.Errorf("Message count doesn't match, got '%v' expected '%v'", status.Messages, len(messageList))
|
|
}
|
|
|
|
ids, err := clientGetMessageIDs(client, mboxName)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get message ids for mbox '%v': %w", mboxName, err)
|
|
}
|
|
|
|
for _, msg := range messageList {
|
|
imapFlags, ok := ids[msg.ID]
|
|
if !ok {
|
|
if meta.FailedMessageIDs.Contains(msg.ID) {
|
|
log.Warningf("Missing message '%v', but it is part of failed message set", msg.ID)
|
|
} else {
|
|
log.Errorf("Missing message '%v'", msg.ID)
|
|
}
|
|
|
|
continue
|
|
}
|
|
|
|
if checkFlags {
|
|
if !imapFlags.Equals(msg.Flags) {
|
|
log.Errorf("Message '%v' flags do mot match, got=%v, expected=%v",
|
|
msg.ID,
|
|
imapFlags.ToSlice(),
|
|
msg.Flags.ToSlice(),
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if !success {
|
|
log.Errorf("State does not match")
|
|
} else {
|
|
log.Info("State matches")
|
|
}
|
|
|
|
return nil
|
|
}(account, mboxMap); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func clientGetMessageIDs(client *goimapclient.Client, mailbox string) (map[string]imap.FlagSet, error) {
|
|
status, err := client.Select(mailbox, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if status.Messages == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
resCh := make(chan *goimap.Message)
|
|
|
|
section, err := goimap.ParseBodySectionName("BODY[HEADER]")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
fetchItems := []goimap.FetchItem{"BODY[HEADER]", goimap.FetchFlags}
|
|
|
|
seq, err := goimap.ParseSeqSet("1:*")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
go func() {
|
|
if err := client.Fetch(
|
|
seq,
|
|
fetchItems,
|
|
resCh,
|
|
); err != nil {
|
|
panic(err)
|
|
}
|
|
}()
|
|
|
|
messages := iterator.Collect(iterator.Chan(resCh))
|
|
|
|
ids := make(map[string]imap.FlagSet, len(messages))
|
|
|
|
for i, m := range messages {
|
|
literal, err := io.ReadAll(m.GetBody(section))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
header, err := rfc822.NewHeader(literal)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse header for msg %v: %w", i, err)
|
|
}
|
|
|
|
internalID, ok := header.GetChecked("X-Pm-Internal-Id")
|
|
if !ok {
|
|
logrus.Errorf("Message %v does not have internal id", internalID)
|
|
continue
|
|
}
|
|
|
|
messageFlags := imap.NewFlagSet(m.Flags...)
|
|
|
|
// Recent and Deleted are not part of the proton flag set.
|
|
messageFlags.RemoveFromSelf("\\Recent")
|
|
messageFlags.RemoveFromSelf("\\Deleted")
|
|
|
|
ids[internalID] = messageFlags
|
|
}
|
|
|
|
return ids, nil
|
|
}
|