mirror of
https://github.com/ProtonMail/proton-bridge.git
synced 2025-12-10 04:36:43 +00:00
chore: Merge remote-tracking branch 'origin/devel' into release/trift
This commit is contained in:
@ -36,6 +36,14 @@ issues:
|
||||
- gosec
|
||||
- goconst
|
||||
- dogsled
|
||||
- path: utils/smtp-send
|
||||
linters:
|
||||
- dupl
|
||||
- gochecknoglobals
|
||||
- gochecknoinits
|
||||
- gosec
|
||||
- goconst
|
||||
- dogsled
|
||||
|
||||
linters-settings:
|
||||
godox:
|
||||
|
||||
59
Changelog.md
59
Changelog.md
@ -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
2
go.mod
@ -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
2
go.sum
@ -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=
|
||||
|
||||
@ -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
297
internal/bridge/debug.go
Normal 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
|
||||
}
|
||||
@ -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())
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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{
|
||||
|
||||
74
internal/frontend/cli/debug.go
Normal file
74
internal/frontend/cli/debug.go
Normal 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))
|
||||
}
|
||||
@ -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() {
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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())
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
419
internal/user/debug.go
Normal 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
|
||||
}
|
||||
@ -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),
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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
91
utils/smtp-send/main.go
Normal 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user