// 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 . package bridge import ( "archive/zip" "bytes" "context" "io" "os" "path/filepath" "sort" "github.com/ProtonMail/go-proton-api" "github.com/ProtonMail/proton-bridge/v3/internal/constants" "github.com/ProtonMail/proton-bridge/v3/internal/logging" "github.com/ProtonMail/proton-bridge/v3/internal/vault" ) const ( MaxTotalAttachmentSize = 7 * (1 << 20) MaxCompressedFilesCount = 6 ) func (bridge *Bridge) ReportBug(ctx context.Context, osType, osVersion, description, username, email, client string, attachLogs bool) error { var account string if info, err := bridge.QueryUserInfo(username); err == nil { account = info.Username } else if userIDs := bridge.GetUserIDs(); len(userIDs) > 0 { if err := bridge.vault.GetUser(userIDs[0], func(user *vault.User) { account = user.Username() }); err != nil { return err } } var atts []proton.ReportBugAttachment if attachLogs { logs, err := getMatchingLogs(bridge.locator, func(filename string) bool { return logging.MatchBridgeLogName(filename) && !logging.MatchStackTraceName(filename) }) if err != nil { return err } crashes, err := getMatchingLogs(bridge.locator, func(filename string) bool { return logging.MatchBridgeLogName(filename) && logging.MatchStackTraceName(filename) }) if err != nil { return err } guiLogs, err := getMatchingLogs(bridge.locator, func(filename string) bool { return logging.MatchGUILogName(filename) && !logging.MatchStackTraceName(filename) }) if err != nil { return err } var matchFiles []string // Include bridge logs, up to a maximum amount. matchFiles = append(matchFiles, logs[max(0, len(logs)-(MaxCompressedFilesCount/2)):]...) // Include crash logs, up to a maximum amount. matchFiles = append(matchFiles, crashes[max(0, len(crashes)-(MaxCompressedFilesCount/2)):]...) // bridge-gui keeps just one small (~ 1kb) log file; we always include it. if len(guiLogs) > 0 { matchFiles = append(matchFiles, guiLogs[len(guiLogs)-1]) } archive, err := zipFiles(matchFiles) if err != nil { return err } body, err := io.ReadAll(archive) if err != nil { return err } atts = append(atts, proton.ReportBugAttachment{ Name: "logs.zip", Filename: "logs.zip", MIMEType: "application/zip", Body: body, }) } return bridge.api.ReportBug(ctx, proton.ReportBugReq{ OS: osType, OSVersion: osVersion, Title: "[Bridge] Bug", Description: description, Client: client, ClientType: proton.ClientTypeEmail, ClientVersion: constants.AppVersion(bridge.curVersion.Original()), Username: account, Email: email, }, atts...) } func max(a, b int) int { if a > b { return a } return b } func getMatchingLogs(locator Locator, filenameMatchFunc func(string) bool) (filenames []string, err error) { logsPath, err := locator.ProvideLogsPath() if err != nil { return nil, err } files, err := os.ReadDir(logsPath) if err != nil { return nil, err } var matchFiles []string for _, file := range files { if filenameMatchFunc(file.Name()) { matchFiles = append(matchFiles, filepath.Join(logsPath, file.Name())) } } sort.Strings(matchFiles) // Sorted by timestamp: oldest first. return matchFiles, nil } type limitedBuffer struct { capacity int buf *bytes.Buffer } func newLimitedBuffer(capacity int) *limitedBuffer { return &limitedBuffer{ capacity: capacity, buf: bytes.NewBuffer(make([]byte, 0, capacity)), } } func (b *limitedBuffer) Write(p []byte) (n int, err error) { if len(p)+b.buf.Len() > b.capacity { return 0, ErrSizeTooLarge } return b.buf.Write(p) } func (b *limitedBuffer) Read(p []byte) (n int, err error) { return b.buf.Read(p) } func zipFiles(filenames []string) (io.Reader, error) { if len(filenames) == 0 { return nil, nil } buf := newLimitedBuffer(MaxTotalAttachmentSize) w := zip.NewWriter(buf) defer w.Close() //nolint:errcheck for _, file := range filenames { if err := addFileToZip(file, w); err != nil { return nil, err } } if err := w.Close(); err != nil { return nil, err } return buf, nil } func addFileToZip(filename string, writer *zip.Writer) error { fileReader, err := os.Open(filepath.Clean(filename)) if err != nil { return err } defer fileReader.Close() //nolint:errcheck,gosec fileInfo, err := fileReader.Stat() if err != nil { return err } header, err := zip.FileInfoHeader(fileInfo) if err != nil { return err } header.Method = zip.Deflate header.Name = filepath.Base(filename) fileWriter, err := writer.CreateHeader(header) if err != nil { return err } if _, err := io.Copy(fileWriter, fileReader); err != nil { return err } return fileReader.Close() }