diff --git a/internal/user/debug.go b/internal/user/debug.go index 8ab86bbc..0b298c0e 100644 --- a/internal/user/debug.go +++ b/internal/user/debug.go @@ -22,6 +22,7 @@ import ( "context" "encoding/base64" "encoding/json" + "errors" "fmt" "io" "mime" @@ -37,6 +38,7 @@ import ( imapservice "github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice" "github.com/ProtonMail/proton-bridge/v3/internal/usertypes" "github.com/ProtonMail/proton-bridge/v3/internal/vault" + bmessage "github.com/ProtonMail/proton-bridge/v3/pkg/message" "github.com/bradenaw/juniper/xmaps" "github.com/bradenaw/juniper/xslices" "github.com/emersion/go-message" @@ -224,6 +226,55 @@ func (user *User) DebugDownloadMessages( return nil } +func TryBuildDebugMessage(path string) error { + meta, err := loadDebugMetadata(path) + if err != nil { + return fmt.Errorf("failed to load metadata: %w", err) + } + + body, bodyDecrypted, err := loadDebugBody(path) + if err != nil { + return fmt.Errorf("failed to load body: %w", err) + } + + var da []bmessage.DecryptedAttachment + if len(meta.Attachments) != 0 { + d, err := loadAttachments(path, &meta) + if err != nil { + return err + } + da = d + } + + decryptedMessage := bmessage.DecryptedMessage{ + Msg: proton.Message{ + MessageMetadata: meta.MessageMetadata, + Header: meta.Header, + ParsedHeaders: meta.ParsedHeaders, + Body: "", + MIMEType: meta.MIMEType, + Attachments: nil, + }, + Body: bytes.Buffer{}, + BodyErr: nil, + Attachments: da, + } + + if bodyDecrypted { + decryptedMessage.Body.Write(body) + } else { + decryptedMessage.Msg.Body = string(body) + decryptedMessage.BodyErr = fmt.Errorf("body did not decrypt") + } + + var rfc822Message bytes.Buffer + if err := bmessage.BuildRFC822Into(nil, &decryptedMessage, defaultMessageJobOpts(), &rfc822Message); err != nil { + return fmt.Errorf("failed to build message: %w", err) + } + + return nil +} + func getBodyName(path string) string { return filepath.Join(path, "body.txt") } @@ -297,16 +348,16 @@ func decodeSimpleMessage(outPath string, kr *crypto.KeyRing, msg proton.Message) return nil } -func writeMetadata(outPath string, msg proton.Message) error { - type CustomMetadata struct { - proton.MessageMetadata - Header string - ParsedHeaders proton.Headers - MIMEType rfc822.MIMEType - Attachments []proton.Attachment - } +type DebugMetadata struct { + proton.MessageMetadata + Header string + ParsedHeaders proton.Headers + MIMEType rfc822.MIMEType + Attachments []proton.Attachment +} - metadata := CustomMetadata{ +func writeMetadata(outPath string, msg proton.Message) error { + metadata := DebugMetadata{ MessageMetadata: msg.MessageMetadata, Header: msg.Header, ParsedHeaders: msg.ParsedHeaders, @@ -433,3 +484,78 @@ func writeCustomAttachmentPart( return nil } + +func loadDebugMetadata(dir string) (DebugMetadata, error) { + metadataPath := getMetadataPath(dir) + b, err := os.ReadFile(metadataPath) //nolint:gosec + if err != nil { + return DebugMetadata{}, err + } + + var m DebugMetadata + + if err := json.Unmarshal(b, &m); err != nil { + return DebugMetadata{}, err + } + + return m, nil +} + +func loadDebugBody(dir string) ([]byte, bool, error) { + if b, err := os.ReadFile(getBodyName(dir)); err != nil { + if !errors.Is(err, os.ErrNotExist) { + return nil, false, err + } + } else { + return b, true, nil + } + + if b, err := os.ReadFile(getBodyNameFailed(dir)); err != nil { + if !errors.Is(err, os.ErrNotExist) { + return nil, false, err + } + } else { + return b, false, nil + } + + return nil, false, fmt.Errorf("body is either pgp message, which we can't handle or is missing") +} + +func loadAttachments(dir string, meta *DebugMetadata) ([]bmessage.DecryptedAttachment, error) { + attDecrypted := make([]bmessage.DecryptedAttachment, 0, len(meta.Attachments)) + + for _, a := range meta.Attachments { + data, err := os.ReadFile(getAttachmentPathSuccess(dir, a.ID, a.Name)) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return nil, fmt.Errorf("attachment (%v,%v) must have failed to decrypt, we can't do anything since we need the user's keyring", a.ID, a.Name) + } + + return nil, fmt.Errorf("failed to load attachment (%v,%v): %w", a.ID, a.Name, err) + } + + da := bmessage.DecryptedAttachment{ + Packet: nil, + Encrypted: nil, + Data: bytes.Buffer{}, + Err: nil, + } + + da.Data.Write(data) + + attDecrypted = append(attDecrypted, da) + } + + return attDecrypted, nil +} + +func defaultMessageJobOpts() bmessage.JobOptions { + return bmessage.JobOptions{ + IgnoreDecryptionErrors: true, // Whether to ignore decryption errors and create a "custom message" instead. + SanitizeDate: true, // Whether to replace all dates before 1970 with RFC822's birthdate. + AddInternalID: true, // Whether to include MessageID as X-Pm-Internal-Id. + AddExternalID: true, // Whether to include ExternalID as X-Pm-External-Id. + AddMessageDate: true, // Whether to include message time as X-Pm-Date. + AddMessageIDReference: true, // Whether to include the MessageID in References. + } +} diff --git a/utils/debug/debug_assemble.go b/utils/debug/debug_assemble.go new file mode 100644 index 00000000..0adb053f --- /dev/null +++ b/utils/debug/debug_assemble.go @@ -0,0 +1,37 @@ +// Copyright (c) 2023 Proton AG +// +// This file is part of Proton Mail Bridge.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 . + +package main + +import ( + "fmt" + "os" + + "github.com/ProtonMail/proton-bridge/v3/internal/user" +) + +func main() { + if len(os.Args) < 2 { + fmt.Printf("Usage: %v \n", os.Args[0]) + return + } + + if err := user.TryBuildDebugMessage(os.Args[1]); err != nil { + fmt.Printf("%v\n", err.Error()) + return + } +}