chore: Merge remote-tracking branch 'origin/devel' into release/trift

This commit is contained in:
Romain LE JEUNE
2023-07-06 16:13:25 +02:00
27 changed files with 1035 additions and 118 deletions

View File

@ -36,6 +36,14 @@ issues:
- gosec
- goconst
- dogsled
- path: utils/smtp-send
linters:
- dupl
- gochecknoglobals
- gochecknoinits
- gosec
- goconst
- dogsled
linters-settings:
godox:

View File

@ -3,39 +3,23 @@
Changelog [format](http://keepachangelog.com/en/1.0.0/)
## Trift Bridge 3.4.0
## [Bridge 3.4.0] Trift changelog
### Added
### Changed
* Remove gRPC auto-generated C++ source files.
* GODT-2709: Remove the config status file when user is removed.
* GODT-2749: Manual test-windows again.
* GODT-2712: Feed config_status with user action while pending.
* GODT-2715: Add Unitary test for configStatus event.
* GODT-2715: Add Functional test for configStatus telemetry event.
* GODT-2714: Apply PR comments.
* GODT-2714: Set Configuration Status to Failure and send Recovery event when issue is solved.
* GODT-2713: Send config_progress event once a day if the configuration is stucked in pending for more than a day.
* GODT-2711: Send config_abort event on User removal.
* GODT-2710: Send config success on IMAP/SMTP connection..
* GODT-2716: Make Configuration Statistics persistent.
* GODT-2709: Init Configuration status.
* GODT-2750: Disable raise on main window when a notification is clicked on Linux.
* GODT-2748: Log calls that cause main window to show, with reason.
* Test: Force all unit test to use minimum sync spec.
* Revert "feat(GODT-2749): manual windows-test.".
* Test: Force sync limits to minimum with env variable.
* GODT-2749: Manual windows-test.
* GODT-2691: Close logrus output file on exit.
* GODT-2522: New Gluon database layout.
* GODT-2728: Remove the sentry report for gRPC event stream interruptions in bridge-gui.
* GODT-2678: When internet is off, do not display status dot icon for the user in the context menu.
* GODT-2686: Change the orientation of the expand/collapse arrow for Advanced settings.
* Test(GODT-2636): Add step for sending from EML.
* GODT-2707: Set bridge-gui default log level to 'debug'.
* Log failed message ids during sync.
* GODT-2705: Added log entries for focus service on client and server sides.
* GODT-2510: Remove Ent.
* Test(GODT-2600): Changing state (read/unread, starred/unstarred) of a message in integration tests.
* GODT-2703: Got rid of account details dialog with Apple Mail autoconf.
@ -43,13 +27,10 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
* GODT-2690: Update sentry reporting in GUI for new log file naming.
* GODT-2668: Implemented new log retention policy.
* Test(GODT-2683): Save Draft without "Date" & "From" in headers.
* Merge branch release/stone to devel.
* GODT-2666: Feat(GODT-2667): introduce sessionID in bridge.
* Fix linter errors.
* GODT-2653: Log API error details on Message import and send.
* GODT-2674: Add more logs to failed update.
* GODT-2660: Calculate bridge coverage and refactor CI yaml file.
* GODT-2674: Add more logs during update failed.
* Fix dependency_license script to handle dot formated version.
* Add error logs when messages fail to build during sync.
* GODT-2673: Use NoClient as UserAgent without any client connected and...
@ -57,18 +38,48 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
### Fixed
* GODT-2758: Fix panic in SetFlagsOnMessages.
* GODT-2708: Fix dimensions event format + handling of ReportClicked event.
* GODT-2578: Refresh literals appended to Sent folder.
* GODT-2756: Fix for 'Settings' context menu opening the 'Help' page.
* GODT-2753: Vault test now check that value auto-assigned is first available port.
* GODT-2522: Handle migration with unreferenced db values.
* GODT-2693: Allow missing whitespace after header field colon.
* GODT-2726: Fix Parsing of Details field in GPA error message.
* GODT-2653: Only log when err is not nil.
* GODT-2680: Fix for C++ debugger not working on ARM64 because of OpenSSL 3.1.
* GODT-2675: Update GPA to applye togin-gonic/gin patch + update COPYING_NOTES.
* GODT-2672: Fix context cancelled when IMAP/SMTP parameters change is in progress.
## [Bridge 3.3.1] Stone changelog
### Added
### Changed
* GODT-2707: Set bridge-gui default log level to 'debug'.
* GODT-2674: Add more logs during update failed.
* GODT-2750: Disable raise on main window when a notification is clicked on Linux.
* GODT-2709: Remove the config status file when user is removed.
* GODT-2748: Log calls that cause main window to show, with reason.
* GODT-2705: Added log entries for focus service on client and server sides.
* GODT-2712: Feed config_status with user action while pending.
* GODT-2728: Remove the sentry report for gRPC event stream interruptions in bridge-gui.
* GODT-2715: Add Unitary test for configStatus event.
* GODT-2715: Add Functional test for configStatus telemetry event.
* Disable windows runner.
* GODT-2714: Apply PR comments.
* GODT-2714: Set Configuration Status to Failure and send Recovery event when issue is solved.
* GODT-2713: Send config_progress event once a day if the configuration is stucked in pending for more than a day.
* GODT-2711: Send config_abort event on User removal.
* GODT-2710: Send config success on IMAP/SMTP connection..
* GODT-2716: Make Configuration Statistics persistent.
* GODT-2709: Init Configuration status.
* Log errors on failed message Downloads.
### Fixed
* GODT-2726: Fix Parsing of Details field in GPA error message.
* GODT-2708: Fix dimensions event format + handling of ReportClicked event.
* GODT-2756: Fix for 'Settings' context menu opening the 'Help' page.
## Stone Bridge 3.3.0
### Changed

2
go.mod
View File

@ -5,7 +5,7 @@ go 1.20
require (
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557
github.com/Masterminds/semver/v3 v3.2.0
github.com/ProtonMail/gluon v0.16.1-0.20230705085038-02a8a9f2a454
github.com/ProtonMail/gluon v0.16.1-0.20230706110757-a9327fb18611
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a
github.com/ProtonMail/go-proton-api v0.4.1-0.20230704060229-a77a437ec052
github.com/ProtonMail/gopenpgp/v2 v2.7.1-proton

2
go.sum
View File

@ -27,6 +27,8 @@ github.com/ProtonMail/gluon v0.16.1-0.20230704083024-d901d16834de h1:th289W4w6aE
github.com/ProtonMail/gluon v0.16.1-0.20230704083024-d901d16834de/go.mod h1:Og5/Dz1MiGpCJn51XujZwxiLG7WzvvjE5PRpZBQmAHo=
github.com/ProtonMail/gluon v0.16.1-0.20230705085038-02a8a9f2a454 h1:dyvL9eYmjGo06CasWfGDVzlbunqKaQASMcLLT1D2irI=
github.com/ProtonMail/gluon v0.16.1-0.20230705085038-02a8a9f2a454/go.mod h1:Og5/Dz1MiGpCJn51XujZwxiLG7WzvvjE5PRpZBQmAHo=
github.com/ProtonMail/gluon v0.16.1-0.20230706110757-a9327fb18611 h1:QVydPr/+pgz5xihc2ujNNV+qnq3oTidIXvF0PgkcY6U=
github.com/ProtonMail/gluon v0.16.1-0.20230706110757-a9327fb18611/go.mod h1:Og5/Dz1MiGpCJn51XujZwxiLG7WzvvjE5PRpZBQmAHo=
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a h1:D+aZah+k14Gn6kmL7eKxoo/4Dr/lK3ChBcwce2+SQP4=
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=

View File

@ -25,6 +25,7 @@ import (
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"testing"
"time"
@ -300,8 +301,11 @@ func TestBridge_UserAgentFromSMTPClient(t *testing.T) {
string(info.BridgePass)),
))
currentUserAgent = b.GetCurrentUserAgent()
require.Contains(t, currentUserAgent, "UnknownClient/0.0.1")
require.Eventually(t, func() bool {
currentUserAgent = b.GetCurrentUserAgent()
return strings.Contains(currentUserAgent, "UnknownClient/0.0.1")
}, time.Minute, 5*time.Second)
})
})
}

297
internal/bridge/debug.go Normal file
View File

@ -0,0 +1,297 @@
// 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/go-proton-api"
"github.com/ProtonMail/proton-bridge/v3/internal/user"
"github.com/bradenaw/juniper/iterator"
"github.com/bradenaw/juniper/xslices"
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
}
}
// Check for orphaned messages (only present in All Mail)
if progressCB != nil {
progressCB(fmt.Sprintf("Checking user %v for orphans", usr.Name()))
}
log.Debugf("Checking for orphans")
for _, m := range meta.Metadata {
filteredLabels := xslices.Filter(m.LabelIDs, func(t string) bool {
switch t {
case proton.AllMailLabel:
return false
case proton.AllSentLabel:
return false
case proton.AllDraftsLabel:
return false
case proton.OutboxLabel:
return false
default:
return true
}
})
if len(filteredLabels) == 0 {
log.Warnf("Message %v is only present in All Mail (Subject=%v)", m.ID, m.Subject)
}
}
}
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
}

View File

@ -32,7 +32,7 @@ import (
const HeartbeatCheckInterval = time.Hour
func (bridge *Bridge) IsTelemetryAvailable() bool {
func (bridge *Bridge) IsTelemetryAvailable(ctx context.Context) bool {
var flag = true
if bridge.GetTelemetryDisabled() {
return false
@ -40,14 +40,14 @@ func (bridge *Bridge) IsTelemetryAvailable() bool {
safe.RLock(func() {
for _, user := range bridge.users {
flag = flag && user.IsTelemetryEnabled(context.Background())
flag = flag && user.IsTelemetryEnabled(ctx)
}
}, bridge.usersLock)
return flag
}
func (bridge *Bridge) SendHeartbeat(heartbeat *telemetry.HeartbeatData) bool {
func (bridge *Bridge) SendHeartbeat(ctx context.Context, heartbeat *telemetry.HeartbeatData) bool {
data, err := json.Marshal(heartbeat)
if err != nil {
if err := bridge.reporter.ReportMessageWithContext("Cannot parse heartbeat data.", reporter.Context{
@ -62,7 +62,7 @@ func (bridge *Bridge) SendHeartbeat(heartbeat *telemetry.HeartbeatData) bool {
safe.RLock(func() {
for _, user := range bridge.users {
if err := user.SendTelemetry(context.Background(), data); err == nil {
if err := user.SendTelemetry(ctx, data); err == nil {
sent = true
break
}
@ -87,7 +87,7 @@ func (bridge *Bridge) StartHeartbeat(manager telemetry.HeartbeatManager) {
bridge.goHeartbeat = bridge.tasks.PeriodicOrTrigger(HeartbeatCheckInterval, 0, func(ctx context.Context) {
logrus.Debug("Checking for heartbeat")
bridge.heartbeat.TrySending()
bridge.heartbeat.TrySending(ctx)
})
bridge.heartbeat.SetRollout(bridge.GetUpdateRollout())

View File

@ -50,7 +50,7 @@ func NewMocks(tb testing.TB, version, minAuto *semver.Version) *Mocks {
mocks.CrashHandler.EXPECT().HandlePanic(gomock.Any()).AnyTimes()
// this is called at start of heartbeat process.
mocks.Heartbeat.EXPECT().IsTelemetryAvailable().AnyTimes()
mocks.Heartbeat.EXPECT().IsTelemetryAvailable(gomock.Any()).AnyTimes()
return mocks
}

View File

@ -5,6 +5,7 @@
package mocks
import (
context "context"
reflect "reflect"
time "time"
@ -50,31 +51,31 @@ func (mr *MockHeartbeatManagerMockRecorder) GetLastHeartbeatSent() *gomock.Call
}
// IsTelemetryAvailable mocks base method.
func (m *MockHeartbeatManager) IsTelemetryAvailable() bool {
func (m *MockHeartbeatManager) IsTelemetryAvailable(arg0 context.Context) bool {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "IsTelemetryAvailable")
ret := m.ctrl.Call(m, "IsTelemetryAvailable", arg0)
ret0, _ := ret[0].(bool)
return ret0
}
// IsTelemetryAvailable indicates an expected call of IsTelemetryAvailable.
func (mr *MockHeartbeatManagerMockRecorder) IsTelemetryAvailable() *gomock.Call {
func (mr *MockHeartbeatManagerMockRecorder) IsTelemetryAvailable(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsTelemetryAvailable", reflect.TypeOf((*MockHeartbeatManager)(nil).IsTelemetryAvailable))
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsTelemetryAvailable", reflect.TypeOf((*MockHeartbeatManager)(nil).IsTelemetryAvailable), arg0)
}
// SendHeartbeat mocks base method.
func (m *MockHeartbeatManager) SendHeartbeat(arg0 *telemetry.HeartbeatData) bool {
func (m *MockHeartbeatManager) SendHeartbeat(arg0 context.Context, arg1 *telemetry.HeartbeatData) bool {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SendHeartbeat", arg0)
ret := m.ctrl.Call(m, "SendHeartbeat", arg0, arg1)
ret0, _ := ret[0].(bool)
return ret0
}
// SendHeartbeat indicates an expected call of SendHeartbeat.
func (mr *MockHeartbeatManagerMockRecorder) SendHeartbeat(arg0 interface{}) *gomock.Call {
func (mr *MockHeartbeatManagerMockRecorder) SendHeartbeat(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendHeartbeat", reflect.TypeOf((*MockHeartbeatManager)(nil).SendHeartbeat), arg0)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendHeartbeat", reflect.TypeOf((*MockHeartbeatManager)(nil).SendHeartbeat), arg0, arg1)
}
// SetLastHeartbeatSent mocks base method.

View File

@ -297,8 +297,7 @@ func (bridge *Bridge) SetColorScheme(colorScheme string) error {
// Note: it does not clear the keychain. The only entry in the keychain is the vault password,
// which we need at next startup to decrypt the vault.
func (bridge *Bridge) FactoryReset(ctx context.Context) {
useTelemetry := bridge.IsTelemetryAvailable()
useTelemetry := !bridge.GetTelemetryDisabled()
// Delete all the users.
safe.Lock(func() {
for _, user := range bridge.users {

View File

@ -18,6 +18,7 @@
package bridge
import (
"context"
"fmt"
"io"
"strings"
@ -61,7 +62,7 @@ func (s *smtpSession) AuthPlain(username, password string) error {
s.Bridge.setUserAgent(useragent.UnknownClient, useragent.DefaultVersion)
}
user.SendConfigStatusSuccess()
user.SendConfigStatusSuccess(context.Background())
return nil
}

View File

@ -243,15 +243,13 @@ func (bridge *Bridge) LogoutUser(ctx context.Context, userID string) error {
func (bridge *Bridge) DeleteUser(ctx context.Context, userID string) error {
logrus.WithField("userID", userID).Info("Deleting user")
useTelemetry := bridge.IsTelemetryAvailable()
return safe.LockRet(func() error {
if !bridge.vault.HasUser(userID) {
return ErrNoSuchUser
}
if user, ok := bridge.users[userID]; ok {
bridge.logoutUser(ctx, user, true, true, useTelemetry)
bridge.logoutUser(ctx, user, true, true, !bridge.GetTelemetryDisabled())
}
if err := bridge.vault.DeleteUser(userID); err != nil {
@ -602,7 +600,7 @@ func (bridge *Bridge) logoutUser(ctx context.Context, user *user.User, withAPI,
// if this is actually a remove account
if withData && withAPI {
user.SendConfigStatusAbort(withTelemetry)
user.SendConfigStatusAbort(ctx, withTelemetry)
}
logrus.WithFields(logrus.Fields{

View File

@ -0,0 +1,74 @@
// 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 cli
import (
"context"
"os"
"github.com/abiosoft/ishell"
)
func (f *frontendCLI) debugMailboxState(c *ishell.Context) {
f.ShowPrompt(false)
defer f.ShowPrompt(true)
checkFlags := f.yesNoQuestion("Also check message flags")
c.Println("Starting state check. Note that depending on your message count this may take a while.")
result, err := f.bridge.CheckClientState(context.Background(), checkFlags, func(s string) {
c.Println(s)
})
if err != nil {
c.Printf("State check failed : %v", err)
return
}
c.Println("State check finished, see log for more details.")
if len(result.MissingMessages) == 0 {
return
}
f.Println("\n\nSome missing messages were detected. Bridge can download these messages for you")
f.Println("in a directory which you can later send to the developers for analysis.\n")
f.Println(bold("Note that the Messages will be stored unencrypted on disk.") + " If you do not wish")
f.Println("to continue, input no in the prompt below.\n")
if !f.yesNoQuestion("Would you like to proceed") {
return
}
location, err := os.MkdirTemp("", "debug-state-check-*")
if err != nil {
f.Printf("Failed to create temporary directory: %v\n", err)
return
}
c.Printf("Messages will be downloaded to: %v\n\n", bold(location))
if err := f.bridge.DebugDownloadFailedMessages(context.Background(), result, location, func(s string, i int, i2 int) {
f.Printf("[%v] Retrieving message %v of %v\n", s, i, i2)
}); err != nil {
f.Println(err)
return
}
c.Printf("\nMessage download finished. Data is available at %v\n", bold(location))
}

View File

@ -312,6 +312,19 @@ func New(
})
fe.AddCmd(telemetryCmd)
dbgCmd := &ishell.Cmd{
Name: "debug",
Help: "Debug diagnostics ",
}
dbgCmd.AddCmd(&ishell.Cmd{
Name: "mailbox-state",
Help: "Verify local mailbox state against proton server state",
Func: fe.debugMailboxState,
})
fe.AddCmd(dbgCmd)
go fe.watchEvents(eventCh)
go func() {

View File

@ -18,6 +18,7 @@
package telemetry
import (
"context"
"strconv"
"time"
@ -149,12 +150,12 @@ func (heartbeat *Heartbeat) SetPrevVersion(val string) {
heartbeat.metrics.Dimensions.PrevVersion = val
}
func (heartbeat *Heartbeat) TrySending() {
if heartbeat.manager.IsTelemetryAvailable() {
func (heartbeat *Heartbeat) TrySending(ctx context.Context) {
if heartbeat.manager.IsTelemetryAvailable(ctx) {
lastSent := heartbeat.manager.GetLastHeartbeatSent()
now := time.Now()
if now.Year() > lastSent.Year() || (now.Year() == lastSent.Year() && now.YearDay() > lastSent.YearDay()) {
if !heartbeat.manager.SendHeartbeat(&heartbeat.metrics) {
if !heartbeat.manager.SendHeartbeat(ctx, &heartbeat.metrics) {
heartbeat.log.WithFields(logrus.Fields{
"metrics": heartbeat.metrics,
}).Error("Failed to send heartbeat")

View File

@ -18,6 +18,7 @@
package telemetry_test
import (
"context"
"testing"
"time"
@ -52,21 +53,21 @@ func TestHeartbeat_default_heartbeat(t *testing.T) {
},
}
mock.EXPECT().IsTelemetryAvailable().Return(true)
mock.EXPECT().IsTelemetryAvailable(context.Background()).Return(true)
mock.EXPECT().GetLastHeartbeatSent().Return(time.Date(2022, 6, 4, 0, 0, 0, 0, time.UTC))
mock.EXPECT().SendHeartbeat(&data).Return(true)
mock.EXPECT().SendHeartbeat(context.Background(), &data).Return(true)
mock.EXPECT().SetLastHeartbeatSent(gomock.Any()).Return(nil)
hb.TrySending()
hb.TrySending(context.Background())
})
}
func TestHeartbeat_already_sent_heartbeat(t *testing.T) {
withHeartbeat(t, 1143, 1025, "/tmp", "defaultKeychain", func(hb *telemetry.Heartbeat, mock *mocks.MockHeartbeatManager) {
mock.EXPECT().IsTelemetryAvailable().Return(true)
mock.EXPECT().IsTelemetryAvailable(context.Background()).Return(true)
mock.EXPECT().GetLastHeartbeatSent().Return(time.Now().Truncate(24 * time.Hour))
hb.TrySending()
hb.TrySending(context.Background())
})
}

View File

@ -5,6 +5,7 @@
package mocks
import (
context "context"
reflect "reflect"
time "time"
@ -50,31 +51,31 @@ func (mr *MockHeartbeatManagerMockRecorder) GetLastHeartbeatSent() *gomock.Call
}
// IsTelemetryAvailable mocks base method.
func (m *MockHeartbeatManager) IsTelemetryAvailable() bool {
func (m *MockHeartbeatManager) IsTelemetryAvailable(arg0 context.Context) bool {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "IsTelemetryAvailable")
ret := m.ctrl.Call(m, "IsTelemetryAvailable", arg0)
ret0, _ := ret[0].(bool)
return ret0
}
// IsTelemetryAvailable indicates an expected call of IsTelemetryAvailable.
func (mr *MockHeartbeatManagerMockRecorder) IsTelemetryAvailable() *gomock.Call {
func (mr *MockHeartbeatManagerMockRecorder) IsTelemetryAvailable(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsTelemetryAvailable", reflect.TypeOf((*MockHeartbeatManager)(nil).IsTelemetryAvailable))
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsTelemetryAvailable", reflect.TypeOf((*MockHeartbeatManager)(nil).IsTelemetryAvailable), arg0)
}
// SendHeartbeat mocks base method.
func (m *MockHeartbeatManager) SendHeartbeat(arg0 *telemetry.HeartbeatData) bool {
func (m *MockHeartbeatManager) SendHeartbeat(arg0 context.Context, arg1 *telemetry.HeartbeatData) bool {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SendHeartbeat", arg0)
ret := m.ctrl.Call(m, "SendHeartbeat", arg0, arg1)
ret0, _ := ret[0].(bool)
return ret0
}
// SendHeartbeat indicates an expected call of SendHeartbeat.
func (mr *MockHeartbeatManagerMockRecorder) SendHeartbeat(arg0 interface{}) *gomock.Call {
func (mr *MockHeartbeatManagerMockRecorder) SendHeartbeat(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendHeartbeat", reflect.TypeOf((*MockHeartbeatManager)(nil).SendHeartbeat), arg0)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendHeartbeat", reflect.TypeOf((*MockHeartbeatManager)(nil).SendHeartbeat), arg0, arg1)
}
// SetLastHeartbeatSent mocks base method.

View File

@ -18,6 +18,7 @@
package telemetry
import (
"context"
"time"
"github.com/sirupsen/logrus"
@ -33,12 +34,12 @@ const (
)
type Availability interface {
IsTelemetryAvailable() bool
IsTelemetryAvailable(ctx context.Context) bool
}
type HeartbeatManager interface {
Availability
SendHeartbeat(heartbeat *HeartbeatData) bool
SendHeartbeat(ctx context.Context, heartbeat *HeartbeatData) bool
GetLastHeartbeatSent() time.Time
SetLastHeartbeatSent(time.Time) error
}

View File

@ -25,12 +25,12 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/configstatus"
)
func (user *User) SendConfigStatusSuccess() {
func (user *User) SendConfigStatusSuccess(ctx context.Context) {
if user.configStatus.IsFromFailure() {
user.SendConfigStatusRecovery()
user.SendConfigStatusRecovery(ctx)
return
}
if !user.telemetryManager.IsTelemetryAvailable() {
if !user.IsTelemetryEnabled(ctx) {
return
}
if !user.configStatus.IsPending() {
@ -49,7 +49,7 @@ func (user *User) SendConfigStatusSuccess() {
return
}
if err := user.SendTelemetry(context.Background(), data); err == nil {
if err := user.SendTelemetry(ctx, data); err == nil {
user.log.Info("Configuration Status Success event sent.")
if err := user.configStatus.ApplySuccess(); err != nil {
user.log.WithError(err).Error("Failed to ApplySuccess on config_status.")
@ -57,7 +57,7 @@ func (user *User) SendConfigStatusSuccess() {
}
}
func (user *User) SendConfigStatusAbort(withTelemetry bool) {
func (user *User) SendConfigStatusAbort(ctx context.Context, withTelemetry bool) {
if err := user.configStatus.Remove(); err != nil {
user.log.WithError(err).Error("Failed to remove config_status file.")
}
@ -65,7 +65,7 @@ func (user *User) SendConfigStatusAbort(withTelemetry bool) {
if !user.configStatus.IsPending() {
return
}
if !withTelemetry {
if !withTelemetry || !user.IsTelemetryEnabled(ctx) {
return
}
var builder configstatus.ConfigAbortBuilder
@ -80,17 +80,17 @@ func (user *User) SendConfigStatusAbort(withTelemetry bool) {
return
}
if err := user.SendTelemetry(context.Background(), data); err == nil {
if err := user.SendTelemetry(ctx, data); err == nil {
user.log.Info("Configuration Status Abort event sent.")
}
}
func (user *User) SendConfigStatusRecovery() {
func (user *User) SendConfigStatusRecovery(ctx context.Context) {
if !user.configStatus.IsFromFailure() {
user.SendConfigStatusSuccess()
user.SendConfigStatusSuccess(ctx)
return
}
if !user.telemetryManager.IsTelemetryAvailable() {
if !user.IsTelemetryEnabled(ctx) {
return
}
if !user.configStatus.IsPending() {
@ -109,7 +109,7 @@ func (user *User) SendConfigStatusRecovery() {
return
}
if err := user.SendTelemetry(context.Background(), data); err == nil {
if err := user.SendTelemetry(ctx, data); err == nil {
user.log.Info("Configuration Status Recovery event sent.")
if err := user.configStatus.ApplySuccess(); err != nil {
user.log.WithError(err).Error("Failed to ApplySuccess on config_status.")
@ -117,8 +117,8 @@ func (user *User) SendConfigStatusRecovery() {
}
}
func (user *User) SendConfigStatusProgress() {
if !user.telemetryManager.IsTelemetryAvailable() {
func (user *User) SendConfigStatusProgress(ctx context.Context) {
if !user.IsTelemetryEnabled(ctx) {
return
}
if !user.configStatus.IsPending() {
@ -143,7 +143,7 @@ func (user *User) SendConfigStatusProgress() {
return
}
if err := user.SendTelemetry(context.Background(), data); err == nil {
if err := user.SendTelemetry(ctx, data); err == nil {
user.log.Info("Configuration Status Progress event sent.")
if err := user.configStatus.ApplyProgress(); err != nil {
user.log.WithError(err).Error("Failed to ApplyProgress on config_status.")

419
internal/user/debug.go Normal file
View File

@ -0,0 +1,419 @@
// 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 user
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"mime"
"os"
"path/filepath"
"strings"
"github.com/ProtonMail/gluon/imap"
"github.com/ProtonMail/gluon/rfc822"
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/gopenpgp/v2/constants"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/bradenaw/juniper/xmaps"
"github.com/bradenaw/juniper/xslices"
"github.com/emersion/go-message"
"github.com/sirupsen/logrus"
)
type DiagnosticMetadata struct {
MessageIDs []string
Metadata []proton.MessageMetadata
FailedMessageIDs xmaps.Set[string]
}
type AccountMailboxMap map[string][]DiagMailboxMessage
type DiagMailboxMessage struct {
AddressID string
UserID string
ID string
Flags imap.FlagSet
}
func (apm DiagnosticMetadata) BuildMailboxToMessageMap(user *User) (map[string]AccountMailboxMap, error) {
return safe.RLockRetErr(func() (map[string]AccountMailboxMap, error) {
result := make(map[string]AccountMailboxMap)
mode := user.GetAddressMode()
primaryAddrID, err := getPrimaryAddr(user.apiAddrs)
if err != nil {
return nil, fmt.Errorf("failed to get primary addr for user: %w", err)
}
getAccount := func(addrID string) (AccountMailboxMap, bool) {
if mode == vault.CombinedMode {
addrID = primaryAddrID.ID
}
addr := user.apiAddrs[addrID]
if addr.Status != proton.AddressStatusEnabled {
return nil, false
}
v, ok := result[addr.Email]
if !ok {
result[addr.Email] = make(AccountMailboxMap)
v = result[addr.Email]
}
return v, true
}
for _, metadata := range apm.Metadata {
for _, label := range metadata.LabelIDs {
details, ok := user.apiLabels[label]
if !ok {
logrus.Warnf("User %v has message with unknown label '%v'", user.Name(), label)
continue
}
if !wantLabel(details) {
continue
}
account, enabled := getAccount(metadata.AddressID)
if !enabled {
continue
}
var mboxName string
if details.Type == proton.LabelTypeSystem {
mboxName = details.Name
} else {
mboxName = strings.Join(getMailboxName(details), "/")
}
mboxMessage := DiagMailboxMessage{
UserID: user.ID(),
ID: metadata.ID,
AddressID: metadata.AddressID,
Flags: buildFlagSetFromMessageMetadata(metadata),
}
if v, ok := account[mboxName]; ok {
account[mboxName] = append(v, mboxMessage)
} else {
account[mboxName] = []DiagMailboxMessage{mboxMessage}
}
}
}
return result, nil
}, user.apiAddrsLock, user.apiLabelsLock)
}
func (user *User) GetDiagnosticMetadata(ctx context.Context) (DiagnosticMetadata, error) {
failedMessages := xmaps.SetFromSlice(user.vault.SyncStatus().FailedMessageIDs)
messageIDs, err := user.client.GetMessageIDs(ctx, "")
if err != nil {
return DiagnosticMetadata{}, err
}
meta := make([]proton.MessageMetadata, 0, len(messageIDs))
for _, m := range xslices.Chunk(messageIDs, 100) {
metadata, err := user.client.GetMessageMetadataPage(ctx, 0, len(m), proton.MessageFilter{ID: m})
if err != nil {
return DiagnosticMetadata{}, err
}
meta = append(meta, metadata...)
}
return DiagnosticMetadata{
MessageIDs: messageIDs,
Metadata: meta,
FailedMessageIDs: failedMessages,
}, nil
}
func (user *User) DebugDownloadMessages(
ctx context.Context,
path string,
msgs map[string]DiagMailboxMessage,
progressCB func(string, int, int),
) error {
var err error
safe.RLock(func() {
err = func() error {
total := len(msgs)
userID := user.ID()
counter := 1
for _, msg := range msgs {
if progressCB != nil {
progressCB(userID, counter, total)
counter++
}
msgDir := filepath.Join(path, msg.ID)
if err := os.MkdirAll(msgDir, 0o700); err != nil {
return fmt.Errorf("failed to create directory '%v':%w", msgDir, err)
}
message, err := user.client.GetFullMessage(ctx, msg.ID, newProtonAPIScheduler(user.panicHandler), proton.NewDefaultAttachmentAllocator())
if err != nil {
return fmt.Errorf("failed to download message '%v':%w", msg.ID, err)
}
if err := writeMetadata(msgDir, message.Message); err != nil {
return err
}
if err := withAddrKR(user.apiUser, user.apiAddrs[msg.AddressID], user.vault.KeyPass(), func(_, addrKR *crypto.KeyRing) error {
switch {
case len(message.Attachments) > 0:
return decodeMultipartMessage(msgDir, addrKR, message.Message, message.AttData)
case message.MIMEType == "multipart/mixed":
return decodePGPMessage(msgDir, addrKR, message.Message)
default:
return decodeSimpleMessage(msgDir, addrKR, message.Message)
}
}); err != nil {
return err
}
}
return nil
}()
}, user.apiAddrsLock, user.apiUserLock)
return err
}
func getBodyName(path string) string {
return filepath.Join(path, "body.txt")
}
func getBodyNameFailed(path string) string {
return filepath.Join(path, "body_failed.txt")
}
func getBodyNamePGP(path string) string {
return filepath.Join(path, "body.pgp")
}
func getMetadataPath(path string) string {
return filepath.Join(path, "metadata.json")
}
func getAttachmentPathSuccess(path, id, name string) string {
return filepath.Join(path, fmt.Sprintf("attachment_%v_%v", id, name))
}
func getAttachmentPathFailure(path, id string) string {
return filepath.Join(path, fmt.Sprintf("attachment_%v_failed.pgp", id))
}
func decodeMultipartMessage(outPath string, kr *crypto.KeyRing, msg proton.Message, attData [][]byte) error {
for idx, attachment := range msg.Attachments {
if err := decodeAttachment(outPath, kr, attachment, attData[idx]); err != nil {
return fmt.Errorf("failed to decode attachment %v of message %v: %w", attachment.ID, msg.ID, err)
}
}
return decodeSimpleMessage(outPath, kr, msg)
}
func decodePGPMessage(outPath string, kr *crypto.KeyRing, msg proton.Message) error {
var decrypted bytes.Buffer
decrypted.Grow(len(msg.Body))
if err := msg.DecryptInto(kr, &decrypted); err != nil {
logrus.Warnf("Failed to decrypt pgp message %v, storing as is: %v", msg.ID, err)
bodyPath := getBodyNamePGP(outPath)
if err := os.WriteFile(bodyPath, []byte(msg.Body), 0o600); err != nil {
return fmt.Errorf("failed to write pgp body to '%v': %w", bodyPath, err)
}
}
bodyPath := getBodyName(outPath)
if err := os.WriteFile(bodyPath, decrypted.Bytes(), 0o600); err != nil {
return fmt.Errorf("failed to write pgp body to '%v': %w", bodyPath, err)
}
return nil
}
func decodeSimpleMessage(outPath string, kr *crypto.KeyRing, msg proton.Message) error {
var decrypted bytes.Buffer
decrypted.Grow(len(msg.Body))
if err := msg.DecryptInto(kr, &decrypted); err != nil {
logrus.Warnf("Failed to decrypt simple message %v, will try again as attachment : %v", msg.ID, err)
return writeCustomTextPart(getBodyNameFailed(outPath), msg, err)
}
bodyPath := getBodyName(outPath)
if err := os.WriteFile(bodyPath, decrypted.Bytes(), 0o600); err != nil {
return fmt.Errorf("failed to write simple body to '%v': %w", bodyPath, err)
}
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
}
metadata := CustomMetadata{
MessageMetadata: msg.MessageMetadata,
Header: msg.Header,
ParsedHeaders: msg.ParsedHeaders,
MIMEType: msg.MIMEType,
Attachments: msg.Attachments,
}
j, err := json.MarshalIndent(metadata, "", " ")
if err != nil {
return fmt.Errorf("failed to encode json for message %v: %w", msg.ID, err)
}
metaPath := getMetadataPath(outPath)
if err := os.WriteFile(metaPath, j, 0o600); err != nil {
return fmt.Errorf("failed to write metadata to '%v': %w", metaPath, err)
}
return nil
}
func decodeAttachment(outPath string, kr *crypto.KeyRing,
att proton.Attachment,
attData []byte) error {
kps, err := base64.StdEncoding.DecodeString(att.KeyPackets)
if err != nil {
return err
}
// Use io.Multi
attachmentReader := io.MultiReader(bytes.NewReader(kps), bytes.NewReader(attData))
stream, err := kr.DecryptStream(attachmentReader, nil, crypto.GetUnixTime())
if err != nil {
logrus.
WithField("attID", att.ID).
WithError(err).
Warn("Attachment decryption failed - construct")
var pgpMessageBuffer bytes.Buffer
pgpMessageBuffer.Grow(len(kps) + len(attData))
pgpMessageBuffer.Write(kps)
pgpMessageBuffer.Write(attData)
return writeCustomAttachmentPart(getAttachmentPathFailure(outPath, att.ID), att, &crypto.PGPMessage{Data: pgpMessageBuffer.Bytes()}, err)
}
var decryptBuffer bytes.Buffer
decryptBuffer.Grow(len(kps) + len(attData))
if _, err := decryptBuffer.ReadFrom(stream); err != nil {
logrus.
WithField("attID", att.ID).
WithError(err).
Warn("Attachment decryption failed - stream")
var pgpMessageBuffer bytes.Buffer
pgpMessageBuffer.Grow(len(kps) + len(attData))
pgpMessageBuffer.Write(kps)
pgpMessageBuffer.Write(attData)
return writeCustomAttachmentPart(getAttachmentPathFailure(outPath, att.ID), att, &crypto.PGPMessage{Data: pgpMessageBuffer.Bytes()}, err)
}
attachmentPath := getAttachmentPathSuccess(outPath, att.ID, att.Name)
if err := os.WriteFile(attachmentPath, decryptBuffer.Bytes(), 0o600); err != nil {
return fmt.Errorf("failed to write attachment %v to '%v': %w", att.ID, attachmentPath, err)
}
return nil
}
func writeCustomTextPart(
outPath string,
msg proton.Message,
decError error,
) error {
enc, err := crypto.NewPGPMessageFromArmored(msg.Body)
if err != nil {
return err
}
arm, err := enc.GetArmoredWithCustomHeaders(
fmt.Sprintf("This message could not be decrypted: %v", decError),
constants.ArmorHeaderVersion,
)
if err != nil {
return err
}
if err := os.WriteFile(outPath, []byte(arm), 0o600); err != nil {
return fmt.Errorf("failed to write custom message %v data to '%v': %w", msg.ID, outPath, err)
}
return nil
}
// writeCustomAttachmentPart writes an armored-PGP data part for an attachment that couldn't be decrypted.
func writeCustomAttachmentPart(
outPath string,
att proton.Attachment,
msg *crypto.PGPMessage,
decError error,
) error {
arm, err := msg.GetArmoredWithCustomHeaders(
fmt.Sprintf("This attachment could not be decrypted: %v", decError),
constants.ArmorHeaderVersion,
)
if err != nil {
return err
}
filename := mime.QEncoding.Encode("utf-8", att.Name+".pgp")
var hdr message.Header
hdr.SetContentType("application/octet-stream", map[string]string{"name": filename})
hdr.SetContentDisposition(string(att.Disposition), map[string]string{"filename": filename})
if err := os.WriteFile(outPath, []byte(arm), 0o600); err != nil {
return fmt.Errorf("failed to write custom attachment %v part to '%v': %w", att.ID, outPath, err)
}
return nil
}

View File

@ -648,23 +648,7 @@ func (user *User) handleUpdateMessageEvent(_ context.Context, message proton.Mes
"subject": logging.Sensitive(message.Subject),
}).Info("Handling message updated event")
flags := imap.NewFlagSet()
if message.Seen() {
flags.AddToSelf(imap.FlagSeen)
}
if message.Starred() {
flags.AddToSelf(imap.FlagFlagged)
}
if message.IsDraft() {
flags.AddToSelf(imap.FlagDraft)
}
if message.IsRepliedAll == true || message.IsReplied == true { //nolint: gosimple
flags.AddToSelf(imap.FlagAnswered)
}
flags := buildFlagSetFromMessageMetadata(message)
update := imap.NewMessageMailboxesUpdated(
imap.MessageID(message.ID),

View File

@ -76,7 +76,7 @@ func newIMAPConnector(user *User, addrID string) *imapConnector {
}
// Authorize returns whether the given username/password combination are valid for this connector.
func (conn *imapConnector) Authorize(username string, password []byte) bool {
func (conn *imapConnector) Authorize(ctx context.Context, username string, password []byte) bool {
addrID, err := conn.CheckAuth(username, password)
if err != nil {
return false
@ -86,7 +86,7 @@ func (conn *imapConnector) Authorize(username string, password []byte) bool {
return false
}
conn.User.SendConfigStatusSuccess()
conn.User.SendConfigStatusSuccess(ctx)
return true
}
@ -608,19 +608,7 @@ func (conn *imapConnector) importMessage(
}
func toIMAPMessage(message proton.MessageMetadata) imap.Message {
flags := imap.NewFlagSet()
if !message.Unread {
flags = flags.Add(imap.FlagSeen)
}
if slices.Contains(message.LabelIDs, proton.StarredLabel) {
flags = flags.Add(imap.FlagFlagged)
}
if slices.Contains(message.LabelIDs, proton.DraftsLabel) {
flags = flags.Add(imap.FlagDraft)
}
flags := buildFlagSetFromMessageMetadata(message)
var date time.Time
@ -713,3 +701,25 @@ func toIMAPMailbox(label proton.Label, flags, permFlags, attrs imap.FlagSet) ima
func isAllMailOrScheduled(mailboxID imap.MailboxID) bool {
return (mailboxID == proton.AllMailLabel) || (mailboxID == proton.AllScheduledLabel)
}
func buildFlagSetFromMessageMetadata(message proton.MessageMetadata) imap.FlagSet {
flags := imap.NewFlagSet()
if message.Seen() {
flags.AddToSelf(imap.FlagSeen)
}
if message.Starred() {
flags.AddToSelf(imap.FlagFlagged)
}
if message.IsDraft() {
flags.AddToSelf(imap.FlagDraft)
}
if message.IsRepliedAll == true || message.IsReplied == true { //nolint: gosimple
flags.AddToSelf(imap.FlagAnswered)
}
return flags
}

View File

@ -180,7 +180,7 @@ func New(
// Check for status_progress when triggered.
user.goStatusProgress = user.tasks.PeriodicOrTrigger(configstatus.ProgressCheckInterval, 0, func(ctx context.Context) {
user.SendConfigStatusProgress()
user.SendConfigStatusProgress(ctx)
})
defer user.goStatusProgress()

View File

@ -148,7 +148,7 @@ func withUser(tb testing.TB, ctx context.Context, _ *server.Server, m *proton.Ma
ctl := gomock.NewController(tb)
defer ctl.Finish()
manager := mocks.NewMockHeartbeatManager(ctl)
manager.EXPECT().IsTelemetryAvailable().AnyTimes()
manager.EXPECT().IsTelemetryAvailable(context.Background()).AnyTimes()
user, err := New(ctx, vaultUser, client, nil, apiUser, nil, true, vault.DefaultMaxSyncMemory, tb.TempDir(), manager)
require.NoError(tb, err)
defer user.Close()

View File

@ -314,7 +314,7 @@ func (s *scenario) bridgeTelemetryFeatureDisabled() error {
}
func (s *scenario) checkTelemetry(expect bool) error {
res := s.t.bridge.IsTelemetryAvailable()
res := s.t.bridge.IsTelemetryAvailable(context.Background())
if res != expect {
return fmt.Errorf("expected telemetry feature %v but got %v ", expect, res)
}

View File

@ -18,6 +18,7 @@
package tests
import (
"context"
"errors"
"testing"
"time"
@ -54,14 +55,14 @@ func (hb *heartbeatRecorder) GetLastHeartbeatSent() time.Time {
return hb.bridge.GetLastHeartbeatSent()
}
func (hb *heartbeatRecorder) IsTelemetryAvailable() bool {
func (hb *heartbeatRecorder) IsTelemetryAvailable(ctx context.Context) bool {
if hb.bridge == nil {
return false
}
return hb.bridge.IsTelemetryAvailable()
return hb.bridge.IsTelemetryAvailable(ctx)
}
func (hb *heartbeatRecorder) SendHeartbeat(metrics *telemetry.HeartbeatData) bool {
func (hb *heartbeatRecorder) SendHeartbeat(_ context.Context, metrics *telemetry.HeartbeatData) bool {
if hb.bridge == nil {
return false
}

91
utils/smtp-send/main.go Normal file
View File

@ -0,0 +1,91 @@
// 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 main
import (
"bytes"
"crypto/tls"
"flag"
"fmt"
"os"
"github.com/emersion/go-sasl"
"github.com/emersion/go-smtp"
)
var (
serverURL = flag.String("server", "127.0.0.1:1025", "SMTP server address:port")
userName = flag.String("user-name", "user", "SMTP user name")
userPassword = flag.String("user-pwd", "password", "SMTP user password")
toAddr = flag.String("toAddr", "", "Address toAddr whom toAddr send the message")
)
func main() {
flag.Parse()
flag.Usage = func() {
fmt.Printf("Usage %v [options] file0 ... fileN\n", os.Args[0])
fmt.Printf("\nOptions:\n")
flag.PrintDefaults()
}
if len(*toAddr) == 0 {
panic(fmt.Errorf("to flag can't be empty"))
}
args := flag.Args()
if len(args) == 0 {
flag.Usage()
return
}
smtpClient, err := smtp.Dial(*serverURL)
if err != nil {
panic(fmt.Errorf("failed to connect to server: %w", err))
}
defer func() { _ = smtpClient.Close() }()
// Upgrade to TLS.
if err := smtpClient.StartTLS(&tls.Config{InsecureSkipVerify: true}); err != nil {
panic(fmt.Errorf("failed to starttls: %w", err))
}
// Authorize with SASL PLAIN.
if err := smtpClient.Auth(sasl.NewPlainClient(
*userName,
*userName,
*userPassword,
)); err != nil {
panic(fmt.Errorf("failed to login: %w", err))
}
for idx, v := range args {
fileData, err := os.ReadFile(v)
if err != nil {
panic(fmt.Errorf("failed to read file:%v - %w", v, err))
}
// Send the message.
if err := smtpClient.SendMail(
*userName,
[]string{*toAddr},
bytes.NewReader(fileData),
); err != nil {
panic(fmt.Errorf("failed to send msg %v: %w", idx, err))
}
}
}