forked from Silverfish/proton-bridge
This will download the missing messages into a temporary directory and decrypt them along with the metadata so we can attempt analyze them once submitted to see what is going wrong.
269 lines
7.1 KiB
Go
269 lines
7.1 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"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"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"
|
|
)
|
|
|
|
type CheckClientStateResult struct {
|
|
MissingMessages map[string]map[string]user.DiagMailboxMessage
|
|
}
|
|
|
|
func (c *CheckClientStateResult) AddMissingMessage(userID string, message user.DiagMailboxMessage) {
|
|
v, ok := c.MissingMessages[userID]
|
|
if !ok {
|
|
c.MissingMessages[userID] = map[string]user.DiagMailboxMessage{message.ID: message}
|
|
} else {
|
|
v[message.ID] = message
|
|
}
|
|
}
|
|
|
|
// 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)) (CheckClientStateResult, error) {
|
|
bridge.usersLock.RLock()
|
|
defer bridge.usersLock.RUnlock()
|
|
|
|
users := maps.Values(bridge.users)
|
|
|
|
result := CheckClientStateResult{
|
|
MissingMessages: make(map[string]map[string]user.DiagMailboxMessage),
|
|
}
|
|
|
|
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 result, 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 result, err
|
|
}
|
|
|
|
info, err := bridge.GetUserInfo(usr.ID())
|
|
if err != nil {
|
|
log.WithError(err).Error("Failed to get user info")
|
|
return result, 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)
|
|
}
|
|
|
|
result.AddMissingMessage(msg.UserID, msg)
|
|
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 result, err
|
|
}
|
|
}
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func (bridge *Bridge) DebugDownloadFailedMessages(
|
|
ctx context.Context,
|
|
result CheckClientStateResult,
|
|
exportPath string,
|
|
progressCB func(string, int, int),
|
|
) error {
|
|
bridge.usersLock.RLock()
|
|
defer bridge.usersLock.RUnlock()
|
|
|
|
for userID, messages := range result.MissingMessages {
|
|
usr, ok := bridge.users[userID]
|
|
if !ok {
|
|
return fmt.Errorf("failed to find user with id %v", userID)
|
|
}
|
|
|
|
userDir := filepath.Join(exportPath, userID)
|
|
if err := os.MkdirAll(userDir, 0o700); err != nil {
|
|
return fmt.Errorf("failed to create directory '%v': %w", userDir, err)
|
|
}
|
|
|
|
if err := usr.DebugDownloadMessages(ctx, userDir, messages, progressCB); 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
|
|
}
|