Compare commits

..

46 Commits

Author SHA1 Message Date
6ff4c8a738 Other: Bridge James 1.8.0 2021-05-07 15:56:11 +02:00
dd66b7f8d0 GODT-1159 SMTP server not restarting after restored internet
- [x] write tests to check that IMAP and SMTP servers are closed when there
  is no internet
- [x] always create new go-smtp instance during listenAndServe(int)
2021-05-07 10:34:14 +00:00
0b95ed4dea GODT-1146: Refactor header filtering 2021-05-03 15:53:46 +02:00
ce64aeb05f Other: avoid API jail 2021-05-03 07:05:15 +02:00
27cfda680d GODT-1152: Correctly resolve wildcard sequence/UID set 2021-04-30 10:35:34 +00:00
323303a98b GODT-1089 Explicitly open system preferences window on BigSur. 2021-04-30 09:10:14 +00:00
8109831c07 GODT-35: Finish all details and make tests pass 2021-04-30 05:41:39 +02:00
2284e9ede1 GODT-35: New pmapi client and manager using resty 2021-04-30 05:34:36 +02:00
1d538e8540 GODT-876 Set default from if empty for importing draft 2021-04-29 14:07:20 +00:00
8ccaac8090 GODT-1056 Check encrypted size of the message before upload 2021-04-29 12:40:29 +00:00
22bf8f62ce GODT-1143 Turn off SMTP server while no connection 2021-04-28 11:23:41 +02:00
fed031ebaa Other: early release notes 1.7.1 2021-04-28 10:43:42 +02:00
7a15ebbd54 Other: update go.mod with qt docs 2021-04-27 17:11:23 +02:00
94b5799ba7 Other refactor: clean old builder 2021-04-27 08:12:50 +00:00
286f51a4e7 Other: Bridge Iron 1.7.1 2021-04-23 11:29:03 +02:00
ee961ae4a8 GODT-1141 Use attachment name from content type if not specified in content disposition 2021-04-23 07:18:41 +00:00
4038752a9a Other: preserve message header in PGP/MIME passthrough message 2021-04-22 16:30:29 +02:00
ebf724412b Other: fix custom message on decryption error for externally encrypted message 2021-04-22 12:28:54 +00:00
14d42b5e76 GODT-1081 Return newline after headers with every fetch 2021-04-21 13:02:23 +00:00
2b8d92e82d Other: fix release notes 2021-04-21 12:11:52 +02:00
11b1e3acf5 Other: Release notes Iron early 1.7.0 2021-04-21 10:47:46 +02:00
c5eb660315 Other: fix live test: API sanitize timestamp 2021-04-16 08:32:51 +02:00
5ad23715ec Other: Release Bridge Iron v1.7.0 2021-04-15 13:27:05 +02:00
8ab05a000c GODT-1136 DB Cache header from builder and test 2021-04-15 09:51:08 +00:00
454d248819 GODT-213: Preserve contenttype for undecryptable message body 2021-04-15 09:51:08 +00:00
6c8e5f7cd3 GODT-213: Use application/octet-stream for encrypted parts 2021-04-15 09:51:08 +00:00
f5aba717b2 GODT-213: Force no transfer encoding for embedded message/rfc822 parts 2021-04-15 09:51:08 +00:00
1359c39bc0 GODT-213: Remove dead code GetRelatedHeader/GetRelatedBoundary 2021-04-15 09:51:08 +00:00
4850681f1d GODT-213: correctly expect text/plain in custom message text parts 2021-04-15 09:51:08 +00:00
aa55c69307 Other: fix linter 2021-04-15 09:51:08 +00:00
1f19d4df75 GODT-213: Force text/plain for custom message text part 2021-04-15 09:51:08 +00:00
c0f6af9eb5 GODT-213: Complex external encrypted tests (multipart/alternative, message/rfc822 attachment) 2021-04-15 09:51:08 +00:00
ef6a3d4999 GODT-213: Add comments for newly added code 2021-04-15 09:51:08 +00:00
50550d42b4 GODT-213: Message Builder 2021-04-15 09:51:08 +00:00
8db89a1a6c GODT-1113: Fix tray icon size on macOS Big Sur.
Add patched libqcocoa based on Qt 5.13.0
2021-04-15 09:08:19 +00:00
ba1dfb1bf4 GODT-947 Force colors in logs 2021-04-15 07:20:53 +00:00
d243880753 Other: stop rejecting old TLS versions 2021-04-14 09:28:31 +02:00
cccaaa3d82 Other: turn off bad login in live test 2021-04-12 06:16:34 +02:00
2d95f21567 Other: add straightforward linters 2021-04-08 16:09:40 +02:00
7d0af7624c Other: Bump linter 2021-04-07 10:54:09 +02:00
2f35c453a1 Other: Release notes stable 2021-04-01 08:05:04 +02:00
05dd137bc8 Other: Release notes 2021-03-31 06:52:00 +02:00
767628946f Other: Bridge HZM 1.6.9 2021-03-29 12:08:46 +02:00
d4efa7131f GODT-1121 Initial value of silent updates toggle button 2021-03-29 06:15:33 +02:00
144cf6e40c Other: Bridge HZM 1.6.8 & Import-Export Farg 1.3.3 2021-03-26 11:17:01 +01:00
a205d8c046 GODT-1120 hotfix: use Info level in internal/app logs 2021-03-25 11:33:32 +01:00
308 changed files with 12012 additions and 10917 deletions

View File

@ -1,3 +1,4 @@
---
run: run:
timeout: 10m timeout: 10m
build-tags: build-tags:
@ -8,9 +9,11 @@ run:
issues: issues:
exclude-use-default: false exclude-use-default: false
exclude: exclude:
- Using the variable on range scope `tt` in function literal - Using the variable on range scope `tt` in function literal
- should have comment (\([^)]+\) )?or be unexported # For now we are missing a lot of comments. # For now we are missing a lot of comments.
- at least one file in a package should have a package comment # For now we are missing a lot of comments. - should have comment (\([^)]+\) )?or be unexported
# For now we are missing a lot of comments.
- at least one file in a package should have a package comment
exclude-rules: exclude-rules:
- path: _test\.go - path: _test\.go
@ -30,7 +33,7 @@ linters-settings:
linters: linters:
# setting disable-all will make only explicitly enabled linters run # setting disable-all will make only explicitly enabled linters run
disable-all: true disable-all: true
enable: enable:
- deadcode # Finds unused code [fast: true, auto-fix: false] - deadcode # Finds unused code [fast: true, auto-fix: false]
- errcheck # Errcheck is a program for checking for unchecked errors in go programs. These unchecked errors can be critical bugs in some cases [fast: true, auto-fix: false] - errcheck # Errcheck is a program for checking for unchecked errors in go programs. These unchecked errors can be critical bugs in some cases [fast: true, auto-fix: false]
@ -49,7 +52,6 @@ linters:
- funlen # Tool for detection of long functions [fast: true, auto-fix: false] - funlen # Tool for detection of long functions [fast: true, auto-fix: false]
- gochecknoglobals # Checks that no globals are present in Go code [fast: true, auto-fix: false] - gochecknoglobals # Checks that no globals are present in Go code [fast: true, auto-fix: false]
- gochecknoinits # Checks that no init functions are present in Go code [fast: true, auto-fix: false] - gochecknoinits # Checks that no init functions are present in Go code [fast: true, auto-fix: false]
#- gocognit # Computes and checks the cognitive complexity of functions [fast: true, auto-fix: false]
- goconst # Finds repeated strings that could be replaced by a constant [fast: true, auto-fix: false] - goconst # Finds repeated strings that could be replaced by a constant [fast: true, auto-fix: false]
- gocritic # The most opinionated Go source code linter [fast: true, auto-fix: false] - gocritic # The most opinionated Go source code linter [fast: true, auto-fix: false]
- gocyclo # Computes and checks the cyclomatic complexity of functions [fast: true, auto-fix: false] - gocyclo # Computes and checks the cyclomatic complexity of functions [fast: true, auto-fix: false]
@ -58,15 +60,52 @@ linters:
- goimports # Goimports does everything that gofmt does. Additionally it checks unused imports [fast: true, auto-fix: true] - goimports # Goimports does everything that gofmt does. Additionally it checks unused imports [fast: true, auto-fix: true]
- golint # Golint differs from gofmt. Gofmt reformats Go source code, whereas golint prints out style mistakes [fast: true, auto-fix: false] - golint # Golint differs from gofmt. Gofmt reformats Go source code, whereas golint prints out style mistakes [fast: true, auto-fix: false]
- gosec # Inspects source code for security problems [fast: true, auto-fix: false] - gosec # Inspects source code for security problems [fast: true, auto-fix: false]
- interfacer # Linter that suggests narrower interface types [fast: true, auto-fix: false]
- maligned # Tool to detect Go structs that would take less memory if their fields were sorted [fast: true, auto-fix: false]
- misspell # Finds commonly misspelled English words in comments [fast: true, auto-fix: true] - misspell # Finds commonly misspelled English words in comments [fast: true, auto-fix: true]
- nakedret # Finds naked returns in functions greater than a specified function length [fast: true, auto-fix: false] - nakedret # Finds naked returns in functions greater than a specified function length [fast: true, auto-fix: false]
- prealloc # Finds slice declarations that could potentially be preallocated [fast: true, auto-fix: false] - prealloc # Finds slice declarations that could potentially be preallocated [fast: true, auto-fix: false]
- scopelint # Scopelint checks for unpinned variables in go programs [fast: true, auto-fix: false]
- stylecheck # Stylecheck is a replacement for golint [fast: true, auto-fix: false] - stylecheck # Stylecheck is a replacement for golint [fast: true, auto-fix: false]
- unconvert # Remove unnecessary type conversions [fast: true, auto-fix: false] - unconvert # Remove unnecessary type conversions [fast: true, auto-fix: false]
- unparam # Reports unused function parameters [fast: true, auto-fix: false] - unparam # Reports unused function parameters [fast: true, auto-fix: false]
- whitespace # Tool for detection of leading and trailing whitespace [fast: true, auto-fix: true] - whitespace # Tool for detection of leading and trailing whitespace [fast: true, auto-fix: true]
#- wsl # Whitespace Linter - Forces you to use empty lines! [fast: true, auto-fix: false] - asciicheck # Simple linter to check that your code does not contain non-ASCII identifiers [fast: true, auto-fix: false]
#- lll # Reports long lines [fast: true, auto-fix: false] - durationcheck # check for two durations multiplied together [fast: false, auto-fix: false]
- exhaustive # check exhaustiveness of enum switch statements [fast: false, auto-fix: false]
- exportloopref # checks for pointers to enclosing loop variables [fast: false, auto-fix: false]
- forcetypeassert # finds forced type assertions [fast: true, auto-fix: false]
- godot # Check if comments end in a period [fast: true, auto-fix: true]
- goheader # Checks is file header matches to pattern [fast: true, auto-fix: false]
- gomodguard # Allow and block list linter for direct Go module dependencies. This is different from depguard where there are different block types for example version constraints and module recommendations. [fast: true, auto-fix: false]
- goprintffuncname # Checks that printf-like functions are named with `f` at the end [fast: true, auto-fix: false]
- importas # Enforces consistent import aliases [fast: false, auto-fix: false]
- makezero # Finds slice declarations with non-zero initial length [fast: false, auto-fix: false]
- nilerr # Finds the code that returns nil even if it checks that the error is not nil. [fast: false, auto-fix: false]
- predeclared # find code that shadows one of Go's predeclared identifiers [fast: true, auto-fix: false]
- revive # Fast, configurable, extensible, flexible, and beautiful linter for Go. Drop-in replacement of golint. [fast: false, auto-fix: false]
- rowserrcheck # checks whether Err of rows is checked successfully [fast: false, auto-fix: false]
- sqlclosecheck # Checks that sql.Rows and sql.Stmt are closed. [fast: false, auto-fix: false]
- tparallel # tparallel detects inappropriate usage of t.Parallel() method in your Go test codes [fast: false, auto-fix: false]
- wastedassign # wastedassign finds wasted assignment statements. [fast: false, auto-fix: false]
# - wsl # Whitespace Linter - Forces you to use empty lines! [fast: true, auto-fix: false]
# - lll # Reports long lines [fast: true, auto-fix: false]
# Consider to include:
# - gocognit # Computes and checks the cognitive complexity of functions [fast: true, auto-fix: false]
# - cyclop # checks function and package cyclomatic complexity [fast: false, auto-fix: false]
# - errorlint # go-errorlint is a source code linter for Go software that can be used to find code that will cause problems with the error wrapping scheme introduced in Go 1.13. [fast: false, auto-fix: false]
# - exhaustivestruct # Checks if all struct's fields are initialized [fast: false, auto-fix: false]
# - forbidigo # Forbids identifiers [fast: true, auto-fix: false]
# - gci # Gci control golang package import order and make it always deterministic. [fast: true, auto-fix: true]
# - gocognit # Computes and checks the cognitive complexity of functions [fast: true, auto-fix: false]
# - goerr113 # Golang linter to check the errors handling expressions [fast: false, auto-fix: false]
# - gofumpt # Gofumpt checks whether code was gofumpt-ed. [fast: true, auto-fix: true]
# - gomnd # An analyzer to detect magic numbers. [fast: true, auto-fix: false]
# - gomoddirectives # Manage the use of 'replace', 'retract', and 'excludes' directives in go.mod. [fast: true, auto-fix: false]
# - ifshort # Checks that your code uses short syntax for if-statements whenever possible [fast: true, auto-fix: false]
# - nestif # Reports deeply nested if statements [fast: true, auto-fix: false]
# - nlreturn # nlreturn checks for a new line before return and branch statements to increase code clarity [fast: true, auto-fix: false]
# - noctx # noctx finds sending http request without context.Context [fast: false, auto-fix: false]
# - nolintlint # Reports ill-formed or insufficient nolint directives [fast: true, auto-fix: false]
# - paralleltest # paralleltest detects missing usage of t.Parallel() method in your Go test [fast: true, auto-fix: false]
# - testpackage # linter that makes you use a separate _test package [fast: true, auto-fix: false]
# - thelper # thelper detects golang test helpers without t.Helper() call and checks the consistency of test helpers [fast: false, auto-fix: false]
# - wrapcheck # Checks that errors returned from external packages are wrapped [fast: false, auto-fix: false]

View File

@ -2,6 +2,64 @@
Changelog [format](http://keepachangelog.com/en/1.0.0/) Changelog [format](http://keepachangelog.com/en/1.0.0/)
## [Bridge 1.8.0] James
### Added
* GODT-1056 Check encrypted size of the message before upload.
* GODT-1143 Turn off SMTP server while no connection.
* GODT-1089 Explicitly open system preferences window on BigSur.
### Fixed
* GODT-1159 SMTP server not restarting after restored internet.
* GODT-1146 Refactor handling of fetching BODY[HEADER] (and similar) regarding trailing newline.
* GODT-1152 Correctly resolve wildcard sequence/UID set.
* Other: Avoid API jail.
## [Bridge 1.7.1] Iron
### Fixed
* GODT-1081 Properly return newlines when returning headers.
* GODT-1150 Externally encrypted messages with missing private key would not be built with custom message.
* GODT-1141 Attachment is named as attachment.bin in some cases.
## [Bridge 1.7.0] Iron
### Added
* GODT-213 New message builder:
* Preserve Content-Type for undecryptable message body.
* Use application/octet-stream for encrypted parts.
* Force no transfer encoding for embedded message/rfc822 parts.
* Remove dead code GetRelatedHeader/GetRelatedBoundary.
* Correctly expect text/plain in custom message text parts.
* Force text/plain for custom message text part.
* Complex external encrypted tests (multipart/alternative, message/rfc822 attachment).
### Fixed
* GODT-1136 DB Cache header from builder and test.
* GODT-1113 Fix tray icon size on macOS Big Sur.
* GODT-947 Force colors in logs.
## [Bridge 1.6.9] HZM
### Fixed
* GODT-1121 'Keep the application up to date' switches off after restarting Bridge.
## [Bridge 1.6.8] HZM
### Fixed
* GODT-1120 Use Info level in internal/app logs.
## [IE 1.3.3] Farg
### Fixed
* GODT-1120 Use Info level in internal/app logs.
## [Bridge 1.6.7] HZM ## [Bridge 1.6.7] HZM
### Added ### Added

View File

@ -10,8 +10,8 @@ TARGET_OS?=${GOOS}
.PHONY: build build-ie build-nogui build-ie-nogui build-launcher build-launcher-ie versioner hasher .PHONY: build build-ie build-nogui build-ie-nogui build-launcher build-launcher-ie versioner hasher
# Keep version hardcoded so app build works also without Git repository. # Keep version hardcoded so app build works also without Git repository.
BRIDGE_APP_VERSION?=1.6.7+git BRIDGE_APP_VERSION?=1.8.0+git
IE_APP_VERSION?=1.3.2+git IE_APP_VERSION?=1.3.3+git
APP_VERSION:=${BRIDGE_APP_VERSION} APP_VERSION:=${BRIDGE_APP_VERSION}
SRC_ICO:=logo.ico SRC_ICO:=logo.ico
SRC_ICNS:=Bridge.icns SRC_ICNS:=Bridge.icns
@ -165,6 +165,7 @@ THERECIPE_ENV:=github.com/therecipe/env_${TARGET_OS}_amd64_513
# therecipe/env in order to download it only once # therecipe/env in order to download it only once
vendor-cache/${THERECIPE_ENV}: vendor-cache/${THERECIPE_ENV}:
git clone https://${THERECIPE_ENV}.git vendor-cache/${THERECIPE_ENV} git clone https://${THERECIPE_ENV}.git vendor-cache/${THERECIPE_ENV}
if [ "${TARGET_OS}" == "darwin" ]; then cp -f "./utils/QTBUG-88600/libqcocoa.dylib" "./vendor-cache/${THERECIPE_ENV}/5.13.0/clang_64/plugins/platforms/"; fi;
# The command used to make symlinks is different on windows. # The command used to make symlinks is different on windows.
# So if the GOOS is windows and we aren't crossbuilding (in which case the host os would still be *nix) # So if the GOOS is windows and we aren't crossbuilding (in which case the host os would still be *nix)
@ -188,7 +189,7 @@ update-qt-docs:
## Dev dependencies ## Dev dependencies
.PHONY: install-devel-tools install-linter install-go-mod-outdated install-git-hooks .PHONY: install-devel-tools install-linter install-go-mod-outdated install-git-hooks
LINTVER:="v1.29.0" LINTVER:="v1.39.0"
LINTSRC:="https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh" LINTSRC:="https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh"
install-dev-dependencies: install-devel-tools install-linter install-go-mod-outdated install-dev-dependencies: install-devel-tools install-linter install-go-mod-outdated
@ -252,12 +253,17 @@ bench:
coverage: test coverage: test
go tool cover -html=/tmp/coverage.out -o=coverage.html go tool cover -html=/tmp/coverage.out -o=coverage.html
integration-test-bridge:
${MAKE} -C test test-bridge
mocks: mocks:
mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/users Locator,PanicHandler,ClientManager,CredentialsStorer,StoreMaker > internal/users/mocks/mocks.go mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/users Locator,PanicHandler,CredentialsStorer,StoreMaker > internal/users/mocks/mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/transfer PanicHandler,ClientManager,IMAPClientProvider > internal/transfer/mocks/mocks.go mockgen --package mocks github.com/ProtonMail/proton-bridge/pkg/listener Listener > internal/users/mocks/listener_mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/store PanicHandler,ClientManager,BridgeUser,ChangeNotifier > internal/store/mocks/mocks.go mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/transfer PanicHandler,IMAPClientProvider > internal/transfer/mocks/mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/store PanicHandler,BridgeUser,ChangeNotifier > internal/store/mocks/mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/pkg/listener Listener > internal/store/mocks/utils_mocks.go mockgen --package mocks github.com/ProtonMail/proton-bridge/pkg/listener Listener > internal/store/mocks/utils_mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/pkg/pmapi Client > pkg/pmapi/mocks/mocks.go mockgen --package mocks github.com/ProtonMail/proton-bridge/pkg/pmapi Client,Manager > pkg/pmapi/mocks/mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/pkg/message Fetcher > pkg/message/mocks/mocks.go
lint: gofiles lint-golang lint-license lint-changelog lint: gofiles lint-golang lint-license lint-changelog
@ -354,4 +360,4 @@ generate:
go generate ./... go generate ./...
$(MAKE) add-license $(MAKE) add-license
.FORCE: .FORCE:

6
go.mod
View File

@ -40,7 +40,7 @@ require (
github.com/fatih/color v1.9.0 github.com/fatih/color v1.9.0
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
github.com/getsentry/sentry-go v0.8.0 github.com/getsentry/sentry-go v0.8.0
github.com/go-resty/resty/v2 v2.3.0 github.com/go-resty/resty/v2 v2.6.0
github.com/golang/mock v1.4.4 github.com/golang/mock v1.4.4
github.com/google/go-cmp v0.5.1 github.com/google/go-cmp v0.5.1
github.com/google/uuid v1.1.1 github.com/google/uuid v1.1.1
@ -50,7 +50,7 @@ require (
github.com/keybase/go-keychain v0.0.0-20200502122510-cda31fe0c86d github.com/keybase/go-keychain v0.0.0-20200502122510-cda31fe0c86d
github.com/logrusorgru/aurora v2.0.3+incompatible github.com/logrusorgru/aurora v2.0.3+incompatible
github.com/mattn/go-runewidth v0.0.9 // indirect github.com/mattn/go-runewidth v0.0.9 // indirect
github.com/miekg/dns v1.1.30 github.com/miekg/dns v1.1.41
github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce
github.com/olekukonko/tablewriter v0.0.4 // indirect github.com/olekukonko/tablewriter v0.0.4 // indirect
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
@ -64,7 +64,7 @@ require (
github.com/urfave/cli/v2 v2.2.0 github.com/urfave/cli/v2 v2.2.0
github.com/vmihailenco/msgpack/v5 v5.1.3 github.com/vmihailenco/msgpack/v5 v5.1.3
go.etcd.io/bbolt v1.3.5 go.etcd.io/bbolt v1.3.5
golang.org/x/net v0.0.0-20200707034311-ab3426394381 golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4
golang.org/x/text v0.3.5-0.20201125200606-c27b9fd57aec golang.org/x/text v0.3.5-0.20201125200606-c27b9fd57aec
) )

31
go.sum
View File

@ -113,8 +113,8 @@ github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclK
github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8= github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8=
github.com/go-resty/resty/v2 v2.3.0 h1:JOOeAvjSlapTT92p8xiS19Zxev1neGikoHsXJeOq8So= github.com/go-resty/resty/v2 v2.6.0 h1:joIR5PNLM2EFqqESUjCMGXrWmXNHEU9CEiK813oKYS4=
github.com/go-resty/resty/v2 v2.3.0/go.mod h1:UpN9CgLZNsv4e9XG50UU8xdI0F43UQ4HmxLBDwaroHU= github.com/go-resty/resty/v2 v2.6.0/go.mod h1:PwvJS6hvaPkjtjNg9ph+VrSD92bi5Zq73w/BIH7cC3Q=
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
@ -195,8 +195,8 @@ github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m
github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
github.com/mediocregopher/radix/v3 v3.4.2/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8= github.com/mediocregopher/radix/v3 v3.4.2/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8=
github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc= github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc=
github.com/miekg/dns v1.1.30 h1:Qww6FseFn8PRfw07jueqIXqodm0JKiiKuK0DeXSqfyo= github.com/miekg/dns v1.1.41 h1:WMszZWJG0XmzbK9FEmzH2TVcqYzFesusSIB41b8KHxY=
github.com/miekg/dns v1.1.30/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@ -263,7 +263,6 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/therecipe/qt v0.0.0-20200701200531-7f61353ee73e h1:G0DQ/TRQyrEZjtLlLwevFjaRiG8eeCMlq9WXQ2OO2bk= github.com/therecipe/qt v0.0.0-20200701200531-7f61353ee73e h1:G0DQ/TRQyrEZjtLlLwevFjaRiG8eeCMlq9WXQ2OO2bk=
github.com/therecipe/qt v0.0.0-20200701200531-7f61353ee73e/go.mod h1:SUUR2j3aE1z6/g76SdD6NwACEpvCxb3fvG82eKbD6us= github.com/therecipe/qt v0.0.0-20200701200531-7f61353ee73e/go.mod h1:SUUR2j3aE1z6/g76SdD6NwACEpvCxb3fvG82eKbD6us=
github.com/therecipe/qt v0.0.0-20200904063919-c0c124a5770d h1:T+d8FnaLSvM/1BdlDXhW4d5dr2F07bAbB+LpgzMxx+o= github.com/therecipe/qt v0.0.0-20200904063919-c0c124a5770d h1:T+d8FnaLSvM/1BdlDXhW4d5dr2F07bAbB+LpgzMxx+o=
github.com/therecipe/qt/internal/binding/files/docs v0.0.0-20191019224306-1097424d656c h1:/VhcwU7WuFEVgDHZ9V8PIYAyYqQ6KNxFUjBMOf2aFZM=
github.com/therecipe/qt/internal/binding/files/docs/5.12.0 v0.0.0-20200904063919-c0c124a5770d h1:hAZyEG2swPRWjF0kqqdGERXUazYnRJdAk4a58f14z7Y= github.com/therecipe/qt/internal/binding/files/docs/5.12.0 v0.0.0-20200904063919-c0c124a5770d h1:hAZyEG2swPRWjF0kqqdGERXUazYnRJdAk4a58f14z7Y=
github.com/therecipe/qt/internal/binding/files/docs/5.12.0 v0.0.0-20200904063919-c0c124a5770d/go.mod h1:7m8PDYDEtEVqfjoUQc2UrFqhG0CDmoVJjRlQxexndFc= github.com/therecipe/qt/internal/binding/files/docs/5.12.0 v0.0.0-20200904063919-c0c124a5770d/go.mod h1:7m8PDYDEtEVqfjoUQc2UrFqhG0CDmoVJjRlQxexndFc=
github.com/therecipe/qt/internal/binding/files/docs/5.13.0 v0.0.0-20200904063919-c0c124a5770d h1:AJRoBel/g9cDS+yE8BcN3E+TDD/xNAguG21aoR8DAIE= github.com/therecipe/qt/internal/binding/files/docs/5.13.0 v0.0.0-20200904063919-c0c124a5770d h1:AJRoBel/g9cDS+yE8BcN3E+TDD/xNAguG21aoR8DAIE=
@ -311,16 +310,18 @@ golang.org/x/net v0.0.0-20190420063019-afa5a82059c6/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw=
golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -331,14 +332,19 @@ golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04 h1:cEhElsAv9LUt9ZUUocxzWe05oFLVd+AA2nstydTeI8g=
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44 h1:Bli41pIlzTzf3KEY06n+xnzK/BESIg2ze4Pgfh/aI8c=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5-0.20201125200606-c27b9fd57aec h1:A1qYjneJuzBZZ2gIB8rd6zrfq6l7SoEMJ8EsSilNK/U= golang.org/x/text v0.3.5-0.20201125200606-c27b9fd57aec h1:A1qYjneJuzBZZ2gIB8rd6zrfq6l7SoEMJ8EsSilNK/U=
golang.org/x/text v0.3.5-0.20201125200606-c27b9fd57aec/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5-0.20201125200606-c27b9fd57aec/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@ -349,7 +355,6 @@ golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3
golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69 h1:yBHHx+XZqXJBm6Exke3N7V9gnlsyXxoCPEb1yVenjfk= golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69 h1:yBHHx+XZqXJBm6Exke3N7V9gnlsyXxoCPEb1yVenjfk=
golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@ -23,7 +23,7 @@
// - persistent settings // - persistent settings
// - event listener // - event listener
// - credentials store // - credentials store
// - pmapi ClientManager // - pmapi Manager
// In addition, the base initialises logging and reacts to command line arguments // In addition, the base initialises logging and reacts to command line arguments
// which control the log verbosity and enable cpu/memory profiling. // which control the log verbosity and enable cpu/memory profiling.
package base package base
@ -69,7 +69,7 @@ const (
flagMemProfileShort = "m" flagMemProfileShort = "m"
flagLogLevel = "log-level" flagLogLevel = "log-level"
flagLogLevelShort = "l" flagLogLevelShort = "l"
// FlagCLI indicate to start with command line interface // FlagCLI indicate to start with command line interface.
FlagCLI = "cli" FlagCLI = "cli"
flagCLIShort = "c" flagCLIShort = "c"
flagRestart = "restart" flagRestart = "restart"
@ -85,7 +85,7 @@ type Base struct {
Cache *cache.Cache Cache *cache.Cache
Listener listener.Listener Listener listener.Listener
Creds *credentials.Store Creds *credentials.Store
CM *pmapi.ClientManager CM pmapi.Manager
CookieJar *cookies.Jar CookieJar *cookies.Jar
UserAgent *useragent.UserAgent UserAgent *useragent.UserAgent
Updater *updater.Updater Updater *updater.Updater
@ -136,7 +136,6 @@ func New( // nolint[funlen]
if err := logging.Init(logsPath); err != nil { if err := logging.Init(logsPath); err != nil {
return nil, err return nil, err
} }
logging.SetLevel("debug") // Proper level is set later in run.
crashHandler.AddRecoveryAction(logging.DumpStackTrace(logsPath)) crashHandler.AddRecoveryAction(logging.DumpStackTrace(logsPath))
if err := migrateFiles(configName); err != nil { if err := migrateFiles(configName); err != nil {
@ -182,13 +181,23 @@ func New( // nolint[funlen]
kc = keychain.NewMissingKeychain() kc = keychain.NewMissingKeychain()
} }
cfg := pmapi.NewConfig(configName, constants.Version)
cfg.GetUserAgent = userAgent.String
cfg.UpgradeApplicationHandler = func() { listener.Emit(events.UpgradeApplicationEvent, "") }
cfg.TLSIssueHandler = func() { listener.Emit(events.TLSCertIssue, "") }
cm := pmapi.New(cfg)
cm.AddConnectionObserver(pmapi.NewConnectionObserver(
func() { listener.Emit(events.InternetOffEvent, "") },
func() { listener.Emit(events.InternetOnEvent, "") },
))
jar, err := cookies.NewCookieJar(settingsObj) jar, err := cookies.NewCookieJar(settingsObj)
if err != nil { if err != nil {
return nil, err return nil, err
} }
cm := pmapi.NewClientManager(getAPIConfig(configName, listener), userAgent)
cm.SetRoundTripper(pmapi.GetRoundTripper(cm, listener))
cm.SetCookieJar(jar) cm.SetCookieJar(jar)
key, err := crypto.NewKeyFromArmored(updater.DefaultPublicKey) key, err := crypto.NewKeyFromArmored(updater.DefaultPublicKey)
@ -329,6 +338,7 @@ func (b *Base) run(appMainLoop func(*Base, *cli.Context) error) cli.ActionFunc {
} }
logging.SetLevel(c.String(flagLogLevel)) logging.SetLevel(c.String(flagLogLevel))
b.CM.SetLogging(logrus.WithField("pkg", "pmapi"), logrus.GetLevel() == logrus.TraceLevel)
logrus. logrus.
WithField("appName", b.Name). WithField("appName", b.Name).
@ -376,13 +386,3 @@ func (b *Base) doTeardown() error {
return nil return nil
} }
func getAPIConfig(configName string, listener listener.Listener) *pmapi.ClientConfig {
apiConfig := pmapi.GetAPIConfig(configName, constants.Version)
apiConfig.ConnectionOffHandler = func() { listener.Emit(events.InternetOffEvent, "") }
apiConfig.ConnectionOnHandler = func() { listener.Emit(events.InternetOnEvent, "") }
apiConfig.UpgradeApplicationHandler = func() { listener.Emit(events.UpgradeApplicationEvent, "") }
return apiConfig
}

View File

@ -34,7 +34,7 @@ import (
// | prefs | ~/.cache/protonmail/<app>/c11/prefs.json | ~/.config/protonmail/<app>/prefs.json | // | prefs | ~/.cache/protonmail/<app>/c11/prefs.json | ~/.config/protonmail/<app>/prefs.json |
// | c11 1.5.x | ~/.cache/protonmail/<app>/c11 | ~/.cache/protonmail/<app>/cache/c11 | // | c11 1.5.x | ~/.cache/protonmail/<app>/c11 | ~/.cache/protonmail/<app>/cache/c11 |
// | c11 1.6.x | ~/.cache/protonmail/<app>/cache/c11 | ~/.config/protonmail/<app>/cache/c11 | // | c11 1.6.x | ~/.cache/protonmail/<app>/cache/c11 | ~/.config/protonmail/<app>/cache/c11 |
// | updates | ~/.cache/protonmail/<app>/updates | ~/.config/protonmail/<app>/updates | // | updates | ~/.cache/protonmail/<app>/updates | ~/.config/protonmail/<app>/updates |.
func migrateFiles(configName string) error { func migrateFiles(configName string) error {
locationsProvider, err := locations.NewDefaultProvider(filepath.Join(constants.VendorName, configName)) locationsProvider, err := locations.NewDefaultProvider(filepath.Join(constants.VendorName, configName))
if err != nil { if err != nil {
@ -50,7 +50,7 @@ func migrateFiles(configName string) error {
if err := migrateCacheFromBoth15xAnd16x(locations, userCacheDir); err != nil { if err := migrateCacheFromBoth15xAnd16x(locations, userCacheDir); err != nil {
return err return err
} }
if err := migrateUpdatesFrom16x(configName, locations); err != nil { if err := migrateUpdatesFrom16x(configName, locations); err != nil { //nolint[revive] It is more clear to structure this way
return err return err
} }
return nil return nil
@ -111,7 +111,7 @@ func moveIfExists(source, destination string) error {
l := logrus.WithField("source", source).WithField("destination", destination) l := logrus.WithField("source", source).WithField("destination", destination)
if _, err := os.Stat(source); os.IsNotExist(err) { if _, err := os.Stat(source); os.IsNotExist(err) {
l.Debug("No need to migrate file, source doesn't exist") l.Info("No need to migrate file, source doesn't exist")
return nil return nil
} }

View File

@ -95,6 +95,7 @@ func run(b *base.Base, c *cli.Context) error { // nolint[funlen]
smtpPort := b.Settings.GetInt(settings.SMTPPortKey) smtpPort := b.Settings.GetInt(settings.SMTPPortKey)
useSSL := b.Settings.GetBool(settings.SMTPSSLKey) useSSL := b.Settings.GetBool(settings.SMTPSSLKey)
smtp.NewSMTPServer( smtp.NewSMTPServer(
b.CrashHandler,
c.Bool(flagLogSMTP), c.Bool(flagLogSMTP),
smtpPort, useSSL, tlsConfig, smtpBackend, b.Listener).ListenAndServe() smtpPort, useSSL, tlsConfig, smtpBackend, b.Listener).ListenAndServe()
}() }()
@ -189,9 +190,10 @@ func generateTLSCerts(b *base.Base) error {
} }
func checkAndHandleUpdate(u types.Updater, f frontend.Frontend, autoUpdate bool) { func checkAndHandleUpdate(u types.Updater, f frontend.Frontend, autoUpdate bool) {
log := logrus.WithField("pkg", "app/bridge")
version, err := u.Check() version, err := u.Check()
if err != nil { if err != nil {
logrus.WithError(err).Error("An error occurred while checking for updates") log.WithError(err).Error("An error occurred while checking for updates")
return return
} }
@ -201,11 +203,11 @@ func checkAndHandleUpdate(u types.Updater, f frontend.Frontend, autoUpdate bool)
f.SetVersion(version) f.SetVersion(version)
if !u.IsUpdateApplicable(version) { if !u.IsUpdateApplicable(version) {
logrus.Debug("No need to update") log.Info("No need to update")
return return
} }
logrus.WithField("version", version.Version).Info("An update is available") log.WithField("version", version.Version).Info("An update is available")
if !autoUpdate { if !autoUpdate {
f.NotifyManualUpdate(version, u.CanInstall(version)) f.NotifyManualUpdate(version, u.CanInstall(version))
@ -213,16 +215,16 @@ func checkAndHandleUpdate(u types.Updater, f frontend.Frontend, autoUpdate bool)
} }
if !u.CanInstall(version) { if !u.CanInstall(version) {
logrus.Info("A manual update is required") log.Info("A manual update is required")
f.NotifySilentUpdateError(updater.ErrManualUpdateRequired) f.NotifySilentUpdateError(updater.ErrManualUpdateRequired)
return return
} }
if err := u.InstallUpdate(version); err != nil { if err := u.InstallUpdate(version); err != nil {
if errors.Cause(err) == updater.ErrDownloadVerify { if errors.Cause(err) == updater.ErrDownloadVerify {
logrus.WithError(err).Warning("Skipping update installation due to temporary error") log.WithError(err).Warning("Skipping update installation due to temporary error")
} else { } else {
logrus.WithError(err).Error("The update couldn't be installed") log.WithError(err).Error("The update couldn't be installed")
f.NotifySilentUpdateError(err) f.NotifySilentUpdateError(err)
} }

View File

@ -87,9 +87,10 @@ func run(b *base.Base, c *cli.Context) error {
} }
func checkAndHandleUpdate(u types.Updater, f frontend.Frontend, autoUpdate bool) { //nolint[unparam] func checkAndHandleUpdate(u types.Updater, f frontend.Frontend, autoUpdate bool) { //nolint[unparam]
log := logrus.WithField("pkg", "app/ie")
version, err := u.Check() version, err := u.Check()
if err != nil { if err != nil {
logrus.WithError(err).Error("An error occurred while checking for updates") log.WithError(err).Error("An error occurred while checking for updates")
return return
} }
@ -99,11 +100,11 @@ func checkAndHandleUpdate(u types.Updater, f frontend.Frontend, autoUpdate bool)
f.SetVersion(version) f.SetVersion(version)
if !u.IsUpdateApplicable(version) { if !u.IsUpdateApplicable(version) {
logrus.Debug("No need to update") log.Info("No need to update")
return return
} }
logrus.WithField("version", version.Version).Info("An update is available") log.WithField("version", version.Version).Info("An update is available")
f.NotifyManualUpdate(version, u.CanInstall(version)) f.NotifyManualUpdate(version, u.CanInstall(version))
} }

View File

@ -19,6 +19,7 @@
package bridge package bridge
import ( import (
"context"
"fmt" "fmt"
"strconv" "strconv"
"time" "time"
@ -44,7 +45,7 @@ type Bridge struct {
locations Locator locations Locator
settings SettingsProvider settings SettingsProvider
clientManager users.ClientManager clientManager pmapi.Manager
updater Updater updater Updater
versioner Versioner versioner Versioner
} }
@ -56,7 +57,7 @@ func New(
sentryReporter *sentry.Reporter, sentryReporter *sentry.Reporter,
panicHandler users.PanicHandler, panicHandler users.PanicHandler,
eventListener listener.Listener, eventListener listener.Listener,
clientManager users.ClientManager, clientManager pmapi.Manager,
credStorer users.CredentialsStorer, credStorer users.CredentialsStorer,
updater Updater, updater Updater,
versioner Versioner, versioner Versioner,
@ -67,7 +68,7 @@ func New(
clientManager.AllowProxy() clientManager.AllowProxy()
} }
storeFactory := newStoreFactory(cache, sentryReporter, panicHandler, clientManager, eventListener) storeFactory := newStoreFactory(cache, sentryReporter, panicHandler, eventListener)
u := users.New(locations, panicHandler, eventListener, clientManager, credStorer, storeFactory, true) u := users.New(locations, panicHandler, eventListener, clientManager, credStorer, storeFactory, true)
b := &Bridge{ b := &Bridge{
Users: u, Users: u,
@ -118,28 +119,15 @@ func (b *Bridge) heartbeat() {
// ReportBug reports a new bug from the user. // ReportBug reports a new bug from the user.
func (b *Bridge) ReportBug(osType, osVersion, description, accountName, address, emailClient string) error { func (b *Bridge) ReportBug(osType, osVersion, description, accountName, address, emailClient string) error {
c := b.clientManager.GetAnonymousClient() return b.clientManager.ReportBug(context.Background(), pmapi.ReportBugReq{
defer c.Logout()
title := "[Bridge] Bug"
report := pmapi.ReportReq{
OS: osType, OS: osType,
OSVersion: osVersion, OSVersion: osVersion,
Browser: emailClient, Browser: emailClient,
Title: title, Title: "[Bridge] Bug",
Description: description, Description: description,
Username: accountName, Username: accountName,
Email: address, Email: address,
} })
if err := c.Report(report); err != nil {
log.Error("Reporting bug failed: ", err)
return err
}
log.Info("Bug successfully reported")
return nil
} }
// GetUpdateChannel returns currently set update channel. // GetUpdateChannel returns currently set update channel.

View File

@ -31,7 +31,6 @@ type storeFactory struct {
cache Cacher cache Cacher
sentryReporter *sentry.Reporter sentryReporter *sentry.Reporter
panicHandler users.PanicHandler panicHandler users.PanicHandler
clientManager users.ClientManager
eventListener listener.Listener eventListener listener.Listener
storeCache *store.Cache storeCache *store.Cache
} }
@ -40,14 +39,12 @@ func newStoreFactory(
cache Cacher, cache Cacher,
sentryReporter *sentry.Reporter, sentryReporter *sentry.Reporter,
panicHandler users.PanicHandler, panicHandler users.PanicHandler,
clientManager users.ClientManager,
eventListener listener.Listener, eventListener listener.Listener,
) *storeFactory { ) *storeFactory {
return &storeFactory{ return &storeFactory{
cache: cache, cache: cache,
sentryReporter: sentryReporter, sentryReporter: sentryReporter,
panicHandler: panicHandler, panicHandler: panicHandler,
clientManager: clientManager,
eventListener: eventListener, eventListener: eventListener,
storeCache: store.NewCache(cache.GetIMAPCachePath()), storeCache: store.NewCache(cache.GetIMAPCachePath()),
} }
@ -56,7 +53,7 @@ func newStoreFactory(
// New creates new store for given user. // New creates new store for given user.
func (f *storeFactory) New(user store.BridgeUser) (*store.Store, error) { func (f *storeFactory) New(user store.BridgeUser) (*store.Store, error) {
storePath := getUserStorePath(f.cache.GetDBDir(), user.ID()) storePath := getUserStorePath(f.cache.GetDBDir(), user.ID())
return store.New(f.sentryReporter, f.panicHandler, user, f.clientManager, f.eventListener, storePath, f.storeCache) return store.New(f.sentryReporter, f.panicHandler, user, f.eventListener, storePath, f.storeCache)
} }
// Remove removes all store files for given user. // Remove removes all store files for given user.

View File

@ -78,7 +78,7 @@ func (s *Settings) setDefaultValues() {
s.setDefault(ReportOutgoingNoEncKey, "false") s.setDefault(ReportOutgoingNoEncKey, "false")
s.setDefault(LastVersionKey, "") s.setDefault(LastVersionKey, "")
s.setDefault(UpdateChannelKey, "") s.setDefault(UpdateChannelKey, "")
s.setDefault(RolloutKey, fmt.Sprintf("%v", rand.Float64())) s.setDefault(RolloutKey, fmt.Sprintf("%v", rand.Float64())) //nolint[gosec] G404 It is OK to use weak random number generator here
s.setDefault(PreferredKeychainKey, "") s.setDefault(PreferredKeychainKey, "")
s.setDefault(APIPortKey, DefaultAPIPort) s.setDefault(APIPortKey, DefaultAPIPort)

View File

@ -122,11 +122,7 @@ func (t *TLS) GenerateCerts(template *x509.Certificate) error {
} }
defer keyOut.Close() // nolint[errcheck] defer keyOut.Close() // nolint[errcheck]
if err := pem.Encode(keyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)}); err != nil { return pem.Encode(keyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})
return err
}
return nil
} }
// GetConfig tries to load TLS config or generate new one which is then returned. // GetConfig tries to load TLS config or generate new one which is then returned.
@ -148,6 +144,7 @@ func (t *TLS) GetConfig() (*tls.Config, error) {
caCertPool := x509.NewCertPool() caCertPool := x509.NewCertPool()
caCertPool.AddCert(c.Leaf) caCertPool.AddCert(c.Leaf)
// nolint[gosec]: We need to support older TLS versions for AppleMail and Outlook.
return &tls.Config{ return &tls.Config{
Certificates: []tls.Certificate{c}, Certificates: []tls.Certificate{c},
ServerName: "127.0.0.1", ServerName: "127.0.0.1",

View File

@ -25,8 +25,20 @@ import (
"github.com/Masterminds/semver/v3" "github.com/Masterminds/semver/v3"
) )
// IsCatalinaOrNewer checks whether host is MacOS Catalina 10.15.x or higher. // IsCatalinaOrNewer checks whether the host is MacOS Catalina 10.15.x or higher.
func IsCatalinaOrNewer() bool { func IsCatalinaOrNewer() bool {
return isThisDarwinNewerOrEqual(getMinCatalina())
}
// IsBigSurOrNewer checks whether the host is MacOS BigSur 10.16.x or higher.
func IsBigSurOrNewer() bool {
return isThisDarwinNewerOrEqual(getMinBigSur())
}
func getMinCatalina() *semver.Version { return semver.MustParse("10.15.0") }
func getMinBigSur() *semver.Version { return semver.MustParse("10.16.0") }
func isThisDarwinNewerOrEqual(minVersion *semver.Version) bool {
if runtime.GOOS != "darwin" { if runtime.GOOS != "darwin" {
return false return false
} }
@ -36,16 +48,14 @@ func IsCatalinaOrNewer() bool {
return false return false
} }
return isVersionCatalinaOrNewer(strings.TrimSpace(string(rawVersion))) return isVersionEqualOrNewer(minVersion, strings.TrimSpace(string(rawVersion)))
} }
func isVersionCatalinaOrNewer(rawVersion string) bool { // isVersionEqualOrNewer is separated to be able to run test on other than darwin.
func isVersionEqualOrNewer(minVersion *semver.Version, rawVersion string) bool {
semVersion, err := semver.NewVersion(rawVersion) semVersion, err := semver.NewVersion(rawVersion)
if err != nil { if err != nil {
return false return false
} }
minVersion := semver.MustParse("10.15.0")
return semVersion.GreaterThan(minVersion) || semVersion.Equal(minVersion) return semVersion.GreaterThan(minVersion) || semVersion.Equal(minVersion)
} }

View File

@ -38,7 +38,27 @@ func TestIsVersionCatalinaOrNewer(t *testing.T) {
} }
for args, exp := range testData { for args, exp := range testData {
got := isVersionCatalinaOrNewer(args.version) got := isVersionEqualOrNewer(getMinCatalina(), args.version)
assert.Equal(t, exp, got, "version %v", args.version)
}
}
func TestIsVersionBigSurOrNewer(t *testing.T) {
testData := map[struct{ version string }]bool{
{""}: false,
{"9.0.0"}: false,
{"9.15.0"}: false,
{"10.13.0"}: false,
{"10.14.0"}: false,
{"10.14.99"}: false,
{"10.15.0"}: false,
{"10.16.0"}: true,
{"11.0.0"}: true,
{"11.1"}: true,
}
for args, exp := range testData {
got := isVersionEqualOrNewer(getMinBigSur(), args.version)
assert.Equal(t, exp, got, "version %v", args.version) assert.Equal(t, exp, got, "version %v", args.version)
} }
} }

View File

@ -29,10 +29,15 @@ import (
"time" "time"
"github.com/ProtonMail/proton-bridge/internal/bridge" "github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/config/useragent"
"github.com/ProtonMail/proton-bridge/internal/frontend/types" "github.com/ProtonMail/proton-bridge/internal/frontend/types"
"github.com/ProtonMail/proton-bridge/pkg/mobileconfig" "github.com/ProtonMail/proton-bridge/pkg/mobileconfig"
) )
const (
bigSurPreferncesPane = "/System/Library/PreferencePanes/Profiles.prefPane"
)
func init() { //nolint[gochecknoinit] func init() { //nolint[gochecknoinit]
available = append(available, &appleMail{}) available = append(available, &appleMail{})
} }
@ -43,7 +48,22 @@ func (c *appleMail) Name() string {
return "Apple Mail" return "Apple Mail"
} }
func (c *appleMail) Configure(imapPort, smtpPort int, imapSSL, smtpSSL bool, user types.User, addressIndex int) error { //nolint[funlen] func (c *appleMail) Configure(imapPort, smtpPort int, imapSSL, smtpSSL bool, user types.User, addressIndex int) error {
mc := prepareMobileConfig(imapPort, smtpPort, imapSSL, smtpSSL, user, addressIndex)
confPath, err := saveConfigTemporarily(mc)
if err != nil {
return err
}
if useragent.IsBigSurOrNewer() {
return exec.Command("open", bigSurPreferncesPane, confPath).Run() //nolint[gosec] G204: open command is safe, mobileconfig is generated by us
}
return exec.Command("open", confPath).Run() //nolint[gosec] G204: open command is safe, mobileconfig is generated by us
}
func prepareMobileConfig(imapPort, smtpPort int, imapSSL, smtpSSL bool, user types.User, addressIndex int) *mobileconfig.Config {
var addresses string var addresses string
var displayName string var displayName string
@ -62,7 +82,7 @@ func (c *appleMail) Configure(imapPort, smtpPort int, imapSSL, smtpSSL bool, use
timestamp := strconv.FormatInt(time.Now().Unix(), 10) timestamp := strconv.FormatInt(time.Now().Unix(), 10)
mc := &mobileconfig.Config{ return &mobileconfig.Config{
EmailAddress: addresses, EmailAddress: addresses,
DisplayName: displayName, DisplayName: displayName,
Identifier: "protonmail " + displayName + timestamp, Identifier: "protonmail " + displayName + timestamp,
@ -80,10 +100,12 @@ func (c *appleMail) Configure(imapPort, smtpPort int, imapSSL, smtpSSL bool, use
Username: displayName, Username: displayName,
}, },
} }
}
func saveConfigTemporarily(mc *mobileconfig.Config) (fname string, err error) {
dir, err := ioutil.TempDir("", "protonmail-autoconfig") dir, err := ioutil.TempDir("", "protonmail-autoconfig")
if err != nil { if err != nil {
return err return
} }
// Make sure the temporary file is deleted. // Make sure the temporary file is deleted.
@ -93,16 +115,17 @@ func (c *appleMail) Configure(imapPort, smtpPort int, imapSSL, smtpSSL bool, use
})() })()
// Make sure the file is only readable for the current user. // Make sure the file is only readable for the current user.
f, err := os.OpenFile(filepath.Join(dir, "protonmail.mobileconfig"), os.O_RDWR|os.O_CREATE, 0600) fname = filepath.Clean(filepath.Join(dir, "protonmail.mobileconfig"))
f, err := os.OpenFile(fname, os.O_RDWR|os.O_CREATE, 0600)
if err != nil { if err != nil {
return err return
} }
if err := mc.WriteOut(f); err != nil { if err = mc.WriteOut(f); err != nil {
_ = f.Close() _ = f.Close()
return err return
} }
_ = f.Close() _ = f.Close()
return exec.Command("open", f.Name()).Run() // nolint[gosec] return
} }

View File

@ -18,6 +18,7 @@
package cliie package cliie
import ( import (
"context"
"strings" "strings"
"github.com/abiosoft/ishell" "github.com/abiosoft/ishell"
@ -25,7 +26,7 @@ import (
func (f *frontendCLI) listAccounts(c *ishell.Context) { func (f *frontendCLI) listAccounts(c *ishell.Context) {
spacing := "%-2d: %-20s (%-15s, %-15s)\n" spacing := "%-2d: %-20s (%-15s, %-15s)\n"
f.Printf(bold(strings.Replace(spacing, "d", "s", -1)), "#", "account", "status", "address mode") f.Printf(bold(strings.ReplaceAll(spacing, "d", "s")), "#", "account", "status", "address mode")
for idx, user := range f.ie.GetUsers() { for idx, user := range f.ie.GetUsers() {
connected := "disconnected" connected := "disconnected"
if user.IsConnected() { if user.IsConnected() {
@ -79,7 +80,7 @@ func (f *frontendCLI) loginAccount(c *ishell.Context) { // nolint[funlen]
return return
} }
err = client.Auth2FA(twoFactor, auth) err = client.Auth2FA(context.Background(), twoFactor)
if err != nil { if err != nil {
f.processAPIError(err) f.processAPIError(err)
return return

View File

@ -84,11 +84,6 @@ func New( //nolint[funlen]
Aliases: []string{"u", "version", "v"}, Aliases: []string{"u", "version", "v"},
Func: fe.checkUpdates, Func: fe.checkUpdates,
}) })
checkCmd.AddCmd(&ishell.Cmd{Name: "internet",
Help: "check internet connection. (aliases: i, conn, connection)",
Aliases: []string{"i", "con", "connection"},
Func: fe.checkInternetConnection,
})
fe.AddCmd(checkCmd) fe.AddCmd(checkCmd)
// Print info commands. // Print info commands.
@ -177,13 +172,13 @@ func New( //nolint[funlen]
} }
func (f *frontendCLI) watchEvents() { func (f *frontendCLI) watchEvents() {
errorCh := f.getEventChannel(events.ErrorEvent) errorCh := f.eventListener.ProvideChannel(events.ErrorEvent)
credentialsErrorCh := f.getEventChannel(events.CredentialsErrorEvent) credentialsErrorCh := f.eventListener.ProvideChannel(events.CredentialsErrorEvent)
internetOffCh := f.getEventChannel(events.InternetOffEvent) internetOffCh := f.eventListener.ProvideChannel(events.InternetOffEvent)
internetOnCh := f.getEventChannel(events.InternetOnEvent) internetOnCh := f.eventListener.ProvideChannel(events.InternetOnEvent)
addressChangedLogoutCh := f.getEventChannel(events.AddressChangedLogoutEvent) addressChangedLogoutCh := f.eventListener.ProvideChannel(events.AddressChangedLogoutEvent)
logoutCh := f.getEventChannel(events.LogoutEvent) logoutCh := f.eventListener.ProvideChannel(events.LogoutEvent)
certIssue := f.getEventChannel(events.TLSCertIssue) certIssue := f.eventListener.ProvideChannel(events.TLSCertIssue)
for { for {
select { select {
case errorDetails := <-errorCh: case errorDetails := <-errorCh:
@ -208,13 +203,6 @@ func (f *frontendCLI) watchEvents() {
} }
} }
func (f *frontendCLI) getEventChannel(event string) <-chan string {
ch := make(chan string)
f.eventListener.Add(event, ch)
f.eventListener.RetryEmit(event)
return ch
}
// Loop starts the frontend loop with an interactive shell. // Loop starts the frontend loop with an interactive shell.
func (f *frontendCLI) Loop() error { func (f *frontendCLI) Loop() error {
f.Print(` f.Print(`

View File

@ -29,14 +29,6 @@ func (f *frontendCLI) restart(c *ishell.Context) {
} }
} }
func (f *frontendCLI) checkInternetConnection(c *ishell.Context) {
if f.ie.CheckConnection() == nil {
f.Println("Internet connection is available.")
} else {
f.Println("Can not contact the server, please check your internet connection.")
}
}
func (f *frontendCLI) printLogDir(c *ishell.Context) { func (f *frontendCLI) printLogDir(c *ishell.Context) {
if path, err := f.locations.ProvideLogsPath(); err != nil { if path, err := f.locations.ProvideLogsPath(); err != nil {
f.Println("Failed to determine location of log files") f.Println("Failed to determine location of log files")

View File

@ -20,7 +20,7 @@ package cliie
import ( import (
"strings" "strings"
pmapi "github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/fatih/color" "github.com/fatih/color"
) )
@ -71,7 +71,7 @@ func (f *frontendCLI) printAndLogError(args ...interface{}) {
func (f *frontendCLI) processAPIError(err error) { func (f *frontendCLI) processAPIError(err error) {
log.Warn("API error: ", err) log.Warn("API error: ", err)
switch err { switch err {
case pmapi.ErrAPINotReachable: case pmapi.ErrNoConnection:
f.notifyInternetOff() f.notifyInternetOff()
case pmapi.ErrUpgradeApplication: case pmapi.ErrUpgradeApplication:
f.notifyNeedUpgrade() f.notifyNeedUpgrade()

View File

@ -18,6 +18,7 @@
package cli package cli
import ( import (
"context"
"strings" "strings"
"github.com/ProtonMail/proton-bridge/internal/bridge" "github.com/ProtonMail/proton-bridge/internal/bridge"
@ -28,7 +29,7 @@ import (
func (f *frontendCLI) listAccounts(c *ishell.Context) { func (f *frontendCLI) listAccounts(c *ishell.Context) {
spacing := "%-2d: %-20s (%-15s, %-15s)\n" spacing := "%-2d: %-20s (%-15s, %-15s)\n"
f.Printf(bold(strings.Replace(spacing, "d", "s", -1)), "#", "account", "status", "address mode") f.Printf(bold(strings.ReplaceAll(spacing, "d", "s")), "#", "account", "status", "address mode")
for idx, user := range f.bridge.GetUsers() { for idx, user := range f.bridge.GetUsers() {
connected := "disconnected" connected := "disconnected"
if user.IsConnected() { if user.IsConnected() {
@ -126,7 +127,7 @@ func (f *frontendCLI) loginAccount(c *ishell.Context) { // nolint[funlen]
return return
} }
err = client.Auth2FA(twoFactor, auth) err = client.Auth2FA(context.Background(), twoFactor)
if err != nil { if err != nil {
f.processAPIError(err) f.processAPIError(err)
return return

View File

@ -157,15 +157,6 @@ func New( //nolint[funlen]
}) })
fe.AddCmd(updatesCmd) fe.AddCmd(updatesCmd)
// Check commands.
checkCmd := &ishell.Cmd{Name: "check", Help: "check internet connection or new version."}
checkCmd.AddCmd(&ishell.Cmd{Name: "internet",
Help: "check internet connection. (aliases: i, conn, connection)",
Aliases: []string{"i", "con", "connection"},
Func: fe.checkInternetConnection,
})
fe.AddCmd(checkCmd)
// Print info commands. // Print info commands.
fe.AddCmd(&ishell.Cmd{Name: "log-dir", fe.AddCmd(&ishell.Cmd{Name: "log-dir",
Help: "print path to directory with logs. (aliases: log, logs)", Help: "print path to directory with logs. (aliases: log, logs)",
@ -228,14 +219,14 @@ func New( //nolint[funlen]
} }
func (f *frontendCLI) watchEvents() { func (f *frontendCLI) watchEvents() {
errorCh := f.getEventChannel(events.ErrorEvent) errorCh := f.eventListener.ProvideChannel(events.ErrorEvent)
credentialsErrorCh := f.getEventChannel(events.CredentialsErrorEvent) credentialsErrorCh := f.eventListener.ProvideChannel(events.CredentialsErrorEvent)
internetOffCh := f.getEventChannel(events.InternetOffEvent) internetOffCh := f.eventListener.ProvideChannel(events.InternetOffEvent)
internetOnCh := f.getEventChannel(events.InternetOnEvent) internetOnCh := f.eventListener.ProvideChannel(events.InternetOnEvent)
addressChangedCh := f.getEventChannel(events.AddressChangedEvent) addressChangedCh := f.eventListener.ProvideChannel(events.AddressChangedEvent)
addressChangedLogoutCh := f.getEventChannel(events.AddressChangedLogoutEvent) addressChangedLogoutCh := f.eventListener.ProvideChannel(events.AddressChangedLogoutEvent)
logoutCh := f.getEventChannel(events.LogoutEvent) logoutCh := f.eventListener.ProvideChannel(events.LogoutEvent)
certIssue := f.getEventChannel(events.TLSCertIssue) certIssue := f.eventListener.ProvideChannel(events.TLSCertIssue)
for { for {
select { select {
case errorDetails := <-errorCh: case errorDetails := <-errorCh:
@ -262,13 +253,6 @@ func (f *frontendCLI) watchEvents() {
} }
} }
func (f *frontendCLI) getEventChannel(event string) <-chan string {
ch := make(chan string)
f.eventListener.Add(event, ch)
f.eventListener.RetryEmit(event)
return ch
}
// Loop starts the frontend loop with an interactive shell. // Loop starts the frontend loop with an interactive shell.
func (f *frontendCLI) Loop() error { func (f *frontendCLI) Loop() error {
f.Print(` f.Print(`

View File

@ -39,14 +39,6 @@ func (f *frontendCLI) restart(c *ishell.Context) {
} }
} }
func (f *frontendCLI) checkInternetConnection(c *ishell.Context) {
if f.bridge.CheckConnection() == nil {
f.Println("Internet connection is available.")
} else {
f.Println("Can not contact the server, please check your internet connection.")
}
}
func (f *frontendCLI) printLogDir(c *ishell.Context) { func (f *frontendCLI) printLogDir(c *ishell.Context) {
if path, err := f.locations.ProvideLogsPath(); err != nil { if path, err := f.locations.ProvideLogsPath(); err != nil {
f.Println("Failed to determine location of log files") f.Println("Failed to determine location of log files")
@ -161,7 +153,7 @@ func (f *frontendCLI) disallowProxy(c *ishell.Context) {
} }
func (f *frontendCLI) isPortFree(port string) bool { func (f *frontendCLI) isPortFree(port string) bool {
port = strings.Replace(port, ":", "", -1) port = strings.ReplaceAll(port, ":", "")
if port == "" || port == currentPort { if port == "" || port == currentPort {
return true return true
} }

View File

@ -71,7 +71,7 @@ func (f *frontendCLI) printAndLogError(args ...interface{}) {
func (f *frontendCLI) processAPIError(err error) { func (f *frontendCLI) processAPIError(err error) {
log.Warn("API error: ", err) log.Warn("API error: ", err)
switch err { switch err {
case pmapi.ErrAPINotReachable: case pmapi.ErrNoConnection:
f.notifyInternetOff() f.notifyInternetOff()
case pmapi.ErrUpgradeApplication: case pmapi.ErrUpgradeApplication:
f.notifyNeedUpgrade() f.notifyNeedUpgrade()

View File

@ -409,7 +409,6 @@ Dialog {
onShow: { onShow: {
if (winMain.updateState==gui.enums.statusNoInternet) { if (winMain.updateState==gui.enums.statusNoInternet) {
go.checkInternet()
if (winMain.updateState==gui.enums.statusNoInternet) { if (winMain.updateState==gui.enums.statusNoInternet) {
go.notifyError(gui.enums.errNoInternet) go.notifyError(gui.enums.errNoInternet)
root.hide() root.hide()

View File

@ -857,14 +857,12 @@ Dialog {
inputPort . checkIsANumber() inputPort . checkIsANumber()
//emailProvider . currentIndex!=0 //emailProvider . currentIndex!=0
)) isOK = false )) isOK = false
go.checkInternet()
if (winMain.updateState == gui.enums.statusNoInternet) { // todo: use main error dialog for this if (winMain.updateState == gui.enums.statusNoInternet) { // todo: use main error dialog for this
errorPopup.show(qsTr("Please check your internet connection.")) errorPopup.show(qsTr("Please check your internet connection."))
return false return false
} }
break break
case 2: // loading structure case 2: // loading structure
go.checkInternet()
if (winMain.updateState == gui.enums.statusNoInternet) { if (winMain.updateState == gui.enums.statusNoInternet) {
errorPopup.show(qsTr("Please check your internet connection.")) errorPopup.show(qsTr("Please check your internet connection."))
return false return false
@ -949,7 +947,6 @@ Dialog {
onShow : { onShow : {
root.clear() root.clear()
if (winMain.updateState==gui.enums.statusNoInternet) { if (winMain.updateState==gui.enums.statusNoInternet) {
go.checkInternet()
if (winMain.updateState==gui.enums.statusNoInternet) { if (winMain.updateState==gui.enums.statusNoInternet) {
winMain.popupMessage.show(go.canNotReachAPI) winMain.popupMessage.show(go.canNotReachAPI)
root.hide() root.hide()

View File

@ -25,33 +25,12 @@ import ProtonUI 1.0
Rectangle { Rectangle {
id: root id: root
property var iTry: 0 property var iTry: 0
property var secLeft: 0
property var second: 1000 // convert millisecond to second property var second: 1000 // convert millisecond to second
property var checkInterval: [ 5, 10, 30, 60, 120, 300, 600 ] // seconds
property bool isVisible: true property bool isVisible: true
property var fontSize : 1.2 * Style.main.fontSize property var fontSize : 1.2 * Style.main.fontSize
color : "black" color : "black"
state: "upToDate" state: "upToDate"
Timer {
id: retryInternet
interval: second
triggeredOnStart: false
repeat: true
onTriggered : {
secLeft--
if (secLeft <= 0) {
retryInternet.stop()
go.checkInternet()
if (iTry < checkInterval.length-1) {
iTry++
}
secLeft=checkInterval[iTry]
retryInternet.start()
}
}
}
Row { Row {
id: messageRow id: messageRow
anchors.centerIn: root anchors.centerIn: root
@ -110,16 +89,12 @@ Rectangle {
case "internetCheck": case "internetCheck":
break; break;
case "noInternet" : case "noInternet" :
retryInternet.start()
secLeft=checkInterval[iTry]
break; break;
case "oldVersion": case "oldVersion":
break; break;
case "forceUpdate": case "forceUpdate":
break; break;
case "upToDate": case "upToDate":
iTry = 0
secLeft=checkInterval[iTry]
break; break;
case "updateRestart": case "updateRestart":
break; break;
@ -128,24 +103,6 @@ Rectangle {
default : default :
break; break;
} }
if (root.state!="noInternet") {
retryInternet.stop()
}
}
function timeToRetry() {
if (secLeft==1){
return qsTr("a second", "time to wait till internet connection is retried")
} else if (secLeft<60){
return secLeft + " " + qsTr("seconds", "time to wait till internet connection is retried")
} else {
var leading = ""+secLeft%60
if (leading.length < 2) {
leading = "0" + leading
}
return Math.floor(secLeft/60) + ":" + leading
}
} }
states: [ states: [
@ -194,23 +151,15 @@ Rectangle {
PropertyChanges { PropertyChanges {
target: message target: message
color: Style.main.line color: Style.main.line
text: qsTr("Cannot contact server. Retrying in ", "displayed when the app is disconnected from the internet or server has problems")+timeToRetry()+"." text: qsTr("Cannot contact server. Please wait...", "displayed when the app is disconnected from the internet or server has problems")
} }
PropertyChanges { PropertyChanges {
target: linkText target: linkText
visible: false visible: false
} }
PropertyChanges {
target: actionText
visible: true
text: qsTr("Retry now", "click to try to connect to the internet when the app is disconnected from the internet")
onClicked: {
go.checkInternet()
}
}
PropertyChanges { PropertyChanges {
target: separatorText target: separatorText
visible: true visible: false
text: "|" text: "|"
} }
PropertyChanges { PropertyChanges {

View File

@ -1331,10 +1331,6 @@ Window {
return (fname!="fail") return (fname!="fail")
} }
function checkInternet() {
// nothing to do
}
function loadImportReports(fname) { function loadImportReports(fname) {
console.log("load import reports for ", fname) console.log("load import reports for ", fname)
} }

View File

@ -20,6 +20,7 @@
package qtcommon package qtcommon
import ( import (
"context"
"fmt" "fmt"
"strings" "strings"
"sync" "sync"
@ -164,7 +165,7 @@ func (a *Accounts) showLoginError(err error, scope string) bool {
return false return false
} }
log.Warnf("%s: %v", scope, err) log.Warnf("%s: %v", scope, err)
if err == pmapi.ErrAPINotReachable { if err == pmapi.ErrNoConnection {
a.qml.SetConnectionStatus(false) a.qml.SetConnectionStatus(false)
SendNotification(a.qml, TabAccount, a.qml.CanNotReachAPI()) SendNotification(a.qml, TabAccount, a.qml.CanNotReachAPI())
a.qml.ProcessFinished() a.qml.ProcessFinished()
@ -207,7 +208,7 @@ func (a *Accounts) Auth2FA(twoFacAuth string) int {
if a.auth == nil || a.authClient == nil { if a.auth == nil || a.authClient == nil {
err = fmt.Errorf("missing authentication in auth2FA %p %p", a.auth, a.authClient) err = fmt.Errorf("missing authentication in auth2FA %p %p", a.auth, a.authClient)
} else { } else {
err = a.authClient.Auth2FA(twoFacAuth, a.auth) err = a.authClient.Auth2FA(context.Background(), twoFacAuth)
} }
if a.showLoginError(err, "auth2FA") { if a.showLoginError(err, "auth2FA") {

View File

@ -113,10 +113,3 @@ type Listener interface {
Add(string, chan<- string) Add(string, chan<- string)
RetryEmit(string) RetryEmit(string)
} }
func MakeAndRegisterEvent(eventListener Listener, event string) <-chan string {
ch := make(chan string)
eventListener.Add(event, ch)
eventListener.RetryEmit(event)
return ch
}

View File

@ -143,16 +143,16 @@ func (f *FrontendQt) NotifySilentUpdateError(err error) {
} }
func (f *FrontendQt) watchEvents() { func (f *FrontendQt) watchEvents() {
credentialsErrorCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.CredentialsErrorEvent) credentialsErrorCh := f.eventListener.ProvideChannel(events.CredentialsErrorEvent)
internetOffCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.InternetOffEvent) internetOffCh := f.eventListener.ProvideChannel(events.InternetOffEvent)
internetOnCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.InternetOnEvent) internetOnCh := f.eventListener.ProvideChannel(events.InternetOnEvent)
secondInstanceCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.SecondInstanceEvent) secondInstanceCh := f.eventListener.ProvideChannel(events.SecondInstanceEvent)
restartBridgeCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.RestartBridgeEvent) restartBridgeCh := f.eventListener.ProvideChannel(events.RestartBridgeEvent)
addressChangedCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.AddressChangedEvent) addressChangedCh := f.eventListener.ProvideChannel(events.AddressChangedEvent)
addressChangedLogoutCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.AddressChangedLogoutEvent) addressChangedLogoutCh := f.eventListener.ProvideChannel(events.AddressChangedLogoutEvent)
logoutCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.LogoutEvent) logoutCh := f.eventListener.ProvideChannel(events.LogoutEvent)
updateApplicationCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.UpgradeApplicationEvent) updateApplicationCh := f.eventListener.ProvideChannel(events.UpgradeApplicationEvent)
newUserCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.UserRefreshEvent) newUserCh := f.eventListener.ProvideChannel(events.UserRefreshEvent)
for { for {
select { select {
case <-credentialsErrorCh: case <-credentialsErrorCh:
@ -351,11 +351,6 @@ func (f *FrontendQt) sendBug(description, emailClient, address string) bool {
// } // }
//} //}
// checkInternet is almost idetical to bridge
func (f *FrontendQt) checkInternet() {
f.Qml.SetConnectionStatus(f.ie.CheckConnection() == nil)
}
func (f *FrontendQt) showError(code int, err error) { func (f *FrontendQt) showError(code int, err error) {
f.Qml.SetErrorDescription(err.Error()) f.Qml.SetErrorDescription(err.Error())
log.WithField("code", code).Errorln(err.Error()) log.WithField("code", code).Errorln(err.Error())

View File

@ -78,7 +78,6 @@ type GoQMLInterface struct {
_ string `property:"versionCheckFailed"` _ string `property:"versionCheckFailed"`
// //
_ func(isAvailable bool) `signal:"setConnectionStatus"` _ func(isAvailable bool) `signal:"setConnectionStatus"`
_ func() `slot:"checkInternet"`
_ func() `slot:"setToRestart"` _ func() `slot:"setToRestart"`
@ -189,8 +188,6 @@ func (s *GoQMLInterface) SetFrontend(f *FrontendQt) {
return f.programVersion return f.programVersion
}) })
s.ConnectCheckInternet(f.checkInternet)
s.ConnectSetToRestart(f.restarter.SetToRestart) s.ConnectSetToRestart(f.restarter.SetToRestart)
s.ConnectLoadStructureForExport(f.LoadStructureForExport) s.ConnectLoadStructureForExport(f.LoadStructureForExport)

View File

@ -20,6 +20,7 @@
package qt package qt
import ( import (
"context"
"fmt" "fmt"
"strings" "strings"
@ -130,7 +131,7 @@ func (s *FrontendQt) showLoginError(err error, scope string) bool {
return false return false
} }
log.Warnf("%s: %v", scope, err) log.Warnf("%s: %v", scope, err)
if err == pmapi.ErrAPINotReachable { if err == pmapi.ErrNoConnection {
s.Qml.SetConnectionStatus(false) s.Qml.SetConnectionStatus(false)
s.SendNotification(TabAccount, s.Qml.CanNotReachAPI()) s.SendNotification(TabAccount, s.Qml.CanNotReachAPI())
s.Qml.ProcessFinished() s.Qml.ProcessFinished()
@ -173,7 +174,7 @@ func (s *FrontendQt) auth2FA(twoFacAuth string) int {
if s.auth == nil || s.authClient == nil { if s.auth == nil || s.authClient == nil {
err = fmt.Errorf("missing authentication in auth2FA %p %p", s.auth, s.authClient) err = fmt.Errorf("missing authentication in auth2FA %p %p", s.auth, s.authClient)
} else { } else {
err = s.authClient.Auth2FA(twoFacAuth, s.auth) err = s.authClient.Auth2FA(context.Background(), twoFacAuth)
} }
if s.showLoginError(err, "auth2FA") { if s.showLoginError(err, "auth2FA") {

View File

@ -191,20 +191,20 @@ func (s *FrontendQt) NotifySilentUpdateError(err error) {
func (s *FrontendQt) watchEvents() { func (s *FrontendQt) watchEvents() {
s.WaitUntilFrontendIsReady() s.WaitUntilFrontendIsReady()
errorCh := s.getEventChannel(events.ErrorEvent) errorCh := s.eventListener.ProvideChannel(events.ErrorEvent)
credentialsErrorCh := s.getEventChannel(events.CredentialsErrorEvent) credentialsErrorCh := s.eventListener.ProvideChannel(events.CredentialsErrorEvent)
outgoingNoEncCh := s.getEventChannel(events.OutgoingNoEncEvent) outgoingNoEncCh := s.eventListener.ProvideChannel(events.OutgoingNoEncEvent)
noActiveKeyForRecipientCh := s.getEventChannel(events.NoActiveKeyForRecipientEvent) noActiveKeyForRecipientCh := s.eventListener.ProvideChannel(events.NoActiveKeyForRecipientEvent)
internetOffCh := s.getEventChannel(events.InternetOffEvent) internetOffCh := s.eventListener.ProvideChannel(events.InternetOffEvent)
internetOnCh := s.getEventChannel(events.InternetOnEvent) internetOnCh := s.eventListener.ProvideChannel(events.InternetOnEvent)
secondInstanceCh := s.getEventChannel(events.SecondInstanceEvent) secondInstanceCh := s.eventListener.ProvideChannel(events.SecondInstanceEvent)
restartBridgeCh := s.getEventChannel(events.RestartBridgeEvent) restartBridgeCh := s.eventListener.ProvideChannel(events.RestartBridgeEvent)
addressChangedCh := s.getEventChannel(events.AddressChangedEvent) addressChangedCh := s.eventListener.ProvideChannel(events.AddressChangedEvent)
addressChangedLogoutCh := s.getEventChannel(events.AddressChangedLogoutEvent) addressChangedLogoutCh := s.eventListener.ProvideChannel(events.AddressChangedLogoutEvent)
logoutCh := s.getEventChannel(events.LogoutEvent) logoutCh := s.eventListener.ProvideChannel(events.LogoutEvent)
updateApplicationCh := s.getEventChannel(events.UpgradeApplicationEvent) updateApplicationCh := s.eventListener.ProvideChannel(events.UpgradeApplicationEvent)
newUserCh := s.getEventChannel(events.UserRefreshEvent) newUserCh := s.eventListener.ProvideChannel(events.UserRefreshEvent)
certIssue := s.getEventChannel(events.TLSCertIssue) certIssue := s.eventListener.ProvideChannel(events.TLSCertIssue)
for { for {
select { select {
case errorDetails := <-errorCh: case errorDetails := <-errorCh:
@ -254,13 +254,6 @@ func (s *FrontendQt) watchEvents() {
} }
} }
func (s *FrontendQt) getEventChannel(event string) <-chan string {
ch := make(chan string)
s.eventListener.Add(event, ch)
s.eventListener.RetryEmit(event)
return ch
}
// Loop function for tests. // Loop function for tests.
// //
// It runs QtExecute in new thread with function returning itself after setup. // It runs QtExecute in new thread with function returning itself after setup.
@ -370,24 +363,15 @@ func (s *FrontendQt) qtExecute(Procedure func(*FrontendQt) error) error {
} }
s.Qml.SetIsAutoStart(s.autostart.IsEnabled()) s.Qml.SetIsAutoStart(s.autostart.IsEnabled())
if s.settings.GetBool(settings.AllowProxyKey) { s.Qml.SetIsAutoUpdate(s.settings.GetBool(settings.AutoUpdateKey))
s.Qml.SetIsProxyAllowed(true) s.Qml.SetIsProxyAllowed(s.settings.GetBool(settings.AllowProxyKey))
} else { s.Qml.SetIsEarlyAccess(updater.UpdateChannel(s.settings.Get(settings.UpdateChannelKey)) == updater.EarlyChannel)
s.Qml.SetIsProxyAllowed(false)
}
if updater.UpdateChannel(s.settings.Get(settings.UpdateChannelKey)) == updater.EarlyChannel {
s.Qml.SetIsEarlyAccess(true)
} else {
s.Qml.SetIsEarlyAccess(false)
}
availableKeychain := []string{} availableKeychain := []string{}
for chain := range keychain.Helpers { for chain := range keychain.Helpers {
availableKeychain = append(availableKeychain, chain) availableKeychain = append(availableKeychain, chain)
} }
s.Qml.SetAvailableKeychain(availableKeychain) s.Qml.SetAvailableKeychain(availableKeychain)
s.Qml.SetSelectedKeychain(s.settings.Get(settings.PreferredKeychainKey)) s.Qml.SetSelectedKeychain(s.settings.Get(settings.PreferredKeychainKey))
// Set reporting of outgoing email without encryption. // Set reporting of outgoing email without encryption.
@ -662,10 +646,6 @@ func (s *FrontendQt) isSMTPSTARTTLS() bool {
return !s.settings.GetBool(settings.SMTPSSLKey) return !s.settings.GetBool(settings.SMTPSSLKey)
} }
func (s *FrontendQt) checkInternet() {
s.Qml.SetConnectionStatus(s.bridge.CheckConnection() == nil)
}
func (s *FrontendQt) switchAddressModeUser(iAccount int) { func (s *FrontendQt) switchAddressModeUser(iAccount int) {
defer s.Qml.ProcessFinished() defer s.Qml.ProcessFinished()
userID := s.Accounts.get(iAccount).UserID() userID := s.Accounts.get(iAccount).UserID()

View File

@ -84,7 +84,6 @@ type GoQMLInterface struct {
_ string `property:"progressDescription"` _ string `property:"progressDescription"`
_ func(isAvailable bool) `signal:"setConnectionStatus"` _ func(isAvailable bool) `signal:"setConnectionStatus"`
_ func() `slot:"checkInternet"`
_ func() `slot:"setToRestart"` _ func() `slot:"setToRestart"`
@ -205,8 +204,6 @@ func (s *GoQMLInterface) SetFrontend(f *FrontendQt) {
return f.programVer return f.programVer
}) })
s.ConnectCheckInternet(f.checkInternet)
s.ConnectSetToRestart(f.restarter.SetToRestart) s.ConnectSetToRestart(f.restarter.SetToRestart)
s.ConnectToggleIsReportingOutgoingNoEnc(f.toggleIsReportingOutgoingNoEnc) s.ConnectToggleIsReportingOutgoingNoEnc(f.toggleIsReportingOutgoingNoEnc)

View File

@ -55,7 +55,6 @@ type UserManager interface {
GetUser(query string) (User, error) GetUser(query string) (User, error)
DeleteUser(userID string, clearCache bool) error DeleteUser(userID string, clearCache bool) error
ClearData() error ClearData() error
CheckConnection() error
} }
// User is an interface of user needed by frontend. // User is an interface of user needed by frontend.

View File

@ -16,6 +16,19 @@
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// Package imap provides IMAP server of the Bridge. // Package imap provides IMAP server of the Bridge.
//
// Methods are called by the go-imap library in parallel.
// Additional parallelism is achieved while handling each IMAP request.
//
// For example, ListMessages internally uses `fetchWorkers` workers to resolve each requested item.
// When IMAP clients request message literals (or parts thereof), we sometimes need to build RFC822 message literals.
// To do this, we pass build jobs to the message builder, which internally manages its own parallelism.
// Summary:
// - each IMAP fetch request is handled in parallel,
// - within each IMAP fetch request, individual items are handled by a pool of `fetchWorkers` workers,
// - within each worker, build jobs are posted to the message builder,
// - the message builder handles build jobs using its own, independent worker pool,
// The builder will handle jobs in parallel up to its own internal limit. This prevents it from overwhelming API.
package imap package imap
import ( import (
@ -26,10 +39,19 @@ import (
"github.com/ProtonMail/proton-bridge/internal/bridge" "github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/events" "github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/pkg/listener" "github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/ProtonMail/proton-bridge/pkg/message"
"github.com/emersion/go-imap" "github.com/emersion/go-imap"
goIMAPBackend "github.com/emersion/go-imap/backend" goIMAPBackend "github.com/emersion/go-imap/backend"
) )
const (
// NOTE: Each fetch worker has its own set of attach workers so there can be up to 20*5=100 API requests at once.
// This is a reasonable limit to not overwhelm API while still maintaining as much parallelism as possible.
fetchWorkers = 20 // In how many workers to fetch message (group list on IMAP).
attachWorkers = 5 // In how many workers to fetch attachments (for one message).
buildWorkers = 20 // In how many workers to build messages.
)
type panicHandler interface { type panicHandler interface {
HandlePanic() HandlePanic()
} }
@ -43,6 +65,8 @@ type imapBackend struct {
users map[string]*imapUser users map[string]*imapUser
usersLocker sync.Locker usersLocker sync.Locker
builder *message.Builder
imapCache map[string]map[string]string imapCache map[string]map[string]string
imapCachePath string imapCachePath string
imapCacheLock *sync.RWMutex imapCacheLock *sync.RWMutex
@ -78,6 +102,8 @@ func newIMAPBackend(
users: map[string]*imapUser{}, users: map[string]*imapUser{},
usersLocker: &sync.Mutex{}, usersLocker: &sync.Mutex{},
builder: message.NewBuilder(fetchWorkers, attachWorkers, buildWorkers),
imapCachePath: cache.GetIMAPCachePath(), imapCachePath: cache.GetIMAPCachePath(),
imapCacheLock: &sync.RWMutex{}, imapCacheLock: &sync.RWMutex{},
} }

View File

@ -38,11 +38,10 @@ type bridgeUser interface {
IsCombinedAddressMode() bool IsCombinedAddressMode() bool
GetAddressID(address string) (string, error) GetAddressID(address string) (string, error)
GetPrimaryAddress() string GetPrimaryAddress() string
UpdateUser() error
Logout() error Logout() error
CloseConnection(address string) CloseConnection(address string)
GetStore() storeUserProvider GetStore() storeUserProvider
GetTemporaryPMAPIClient() pmapi.Client GetClient() pmapi.Client
} }
type bridgeWrap struct { type bridgeWrap struct {
@ -61,7 +60,7 @@ func (b *bridgeWrap) GetUser(query string) (bridgeUser, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
return newBridgeUserWrap(user), nil return newBridgeUserWrap(user), nil //nolint[typecheck] missing methods are inherited
} }
type bridgeUserWrap struct { type bridgeUserWrap struct {
@ -77,5 +76,5 @@ func (u *bridgeUserWrap) GetStore() storeUserProvider {
if store == nil { if store == nil {
return nil return nil
} }
return newStoreUserWrap(store) return newStoreUserWrap(store) //nolint[typecheck] missing methods are inherited
} }

View File

@ -26,7 +26,7 @@ type currentClientSetter interface {
SetClient(name, version string) SetClient(name, version string)
} }
// Extension for IMAP server // Extension for IMAP server.
type extension struct { type extension struct {
extID imapserver.ConnExtension extID imapserver.ConnExtension
clientSetter currentClientSetter clientSetter currentClientSetter

View File

@ -19,11 +19,4 @@ package imap
import "github.com/sirupsen/logrus" import "github.com/sirupsen/logrus"
const ( var log = logrus.WithField("pkg", "imap") //nolint[gochecknoglobals]
fetchMessagesWorkers = 5 // In how many workers to fetch message (group list on IMAP).
fetchAttachmentsWorkers = 5 // In how many workers to fetch attachments (for one message).
)
var (
log = logrus.WithField("pkg", "imap") //nolint[gochecknoglobals]
)

View File

@ -37,10 +37,12 @@ type imapMailbox struct {
storeUser storeUserProvider storeUser storeUserProvider
storeAddress storeAddressProvider storeAddress storeAddressProvider
storeMailbox storeMailboxProvider storeMailbox storeMailboxProvider
builder *message.Builder
} }
// newIMAPMailbox returns struct implementing go-imap/mailbox interface. // newIMAPMailbox returns struct implementing go-imap/mailbox interface.
func newIMAPMailbox(panicHandler panicHandler, user *imapUser, storeMailbox storeMailboxProvider) *imapMailbox { func newIMAPMailbox(panicHandler panicHandler, user *imapUser, storeMailbox storeMailboxProvider, builder *message.Builder) *imapMailbox {
return &imapMailbox{ return &imapMailbox{
panicHandler: panicHandler, panicHandler: panicHandler,
user: user, user: user,
@ -54,6 +56,8 @@ func newIMAPMailbox(panicHandler panicHandler, user *imapUser, storeMailbox stor
storeUser: user.storeUser, storeUser: user.storeUser,
storeAddress: user.storeAddress, storeAddress: user.storeAddress,
storeMailbox: storeMailbox, storeMailbox: storeMailbox,
builder: builder,
} }
} }

View File

@ -0,0 +1,198 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail 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.
//
// ProtonMail 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 ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package imap
import (
"io"
"net/mail"
"strings"
"time"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/ProtonMail/proton-bridge/internal/imap/uidplus"
"github.com/ProtonMail/proton-bridge/pkg/message"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/emersion/go-imap"
"github.com/pkg/errors"
)
// CreateMessage appends a new message to this mailbox. The \Recent flag will
// be added regardless of whether flags is empty or not. If date is nil, the
// current time will be used.
//
// If the Backend implements Updater, it must notify the client immediately
// via a mailbox update.
func (im *imapMailbox) CreateMessage(flags []string, date time.Time, body imap.Literal) error {
return im.logCommand(func() error {
return im.createMessage(flags, date, body)
}, "APPEND", flags, date)
}
func (im *imapMailbox) createMessage(flags []string, date time.Time, body imap.Literal) error { //nolint[funlen]
// Called from go-imap in goroutines - we need to handle panics for each function.
defer im.panicHandler.HandlePanic()
m, _, _, readers, err := message.Parse(body)
if err != nil {
return err
}
addr := im.storeAddress.APIAddress()
if addr == nil {
return errors.New("no available address for encryption")
}
m.AddressID = addr.ID
kr, err := im.user.client().KeyRingForAddressID(addr.ID)
if err != nil {
return err
}
// Handle imported messages which have no "Sender" address.
// This sometimes occurs with outlook which reports errors as imported emails or for drafts.
if m.Sender == nil {
im.log.Warning("Append: Missing email sender. Will use main address")
m.Sender = &mail.Address{
Name: "",
Address: addr.Email,
}
}
// "Drafts" needs to call special API routes.
// Clients always append the whole message again and remove the old one.
if im.storeMailbox.LabelID() == pmapi.DraftLabel {
// Sender address needs to be sanitised (drafts need to match cases exactly).
m.Sender.Address = pmapi.ConstructAddress(m.Sender.Address, addr.Email)
draft, _, err := im.user.storeUser.CreateDraft(kr, m, readers, "", "", "")
if err != nil {
return errors.Wrap(err, "failed to create draft")
}
targetSeq := im.storeMailbox.GetUIDList([]string{draft.ID})
return uidplus.AppendResponse(im.storeMailbox.UIDValidity(), targetSeq)
}
// We need to make sure this is an import, and not a sent message from this account
// (sent messages from the account will be added by the event loop).
if im.storeMailbox.LabelID() == pmapi.SentLabel {
sanitizedSender := pmapi.SanitizeEmail(m.Sender.Address)
// Check whether this message was sent by a bridge user.
user, err := im.user.backend.bridge.GetUser(sanitizedSender)
if err == nil && user.ID() == im.storeUser.UserID() {
logEntry := im.log.WithField("addr", sanitizedSender).WithField("extID", m.Header.Get("Message-Id"))
// If we find the message in the store already, we can skip importing it.
if foundUID := im.storeMailbox.GetUIDByHeader(&m.Header); foundUID != uint32(0) {
logEntry.Info("Ignoring APPEND of duplicate to Sent folder")
return uidplus.AppendResponse(im.storeMailbox.UIDValidity(), &uidplus.OrderedSeq{foundUID})
}
// We didn't find the message in the store, so we are currently sending it.
logEntry.WithField("time", date).Info("No matching UID, continuing APPEND to Sent")
}
}
message.ParseFlags(m, flags)
if !date.IsZero() {
m.Time = date.Unix()
}
internalID := m.Header.Get("X-Pm-Internal-Id")
references := m.Header.Get("References")
referenceList := strings.Fields(references)
// In case there is a mail client which corrupts headers, try
// "References" too.
if internalID == "" && len(referenceList) > 0 {
lastReference := referenceList[len(referenceList)-1]
match := pmapi.RxInternalReferenceFormat.FindStringSubmatch(lastReference)
if len(match) == 2 {
internalID = match[1]
}
}
im.user.appendExpungeLock.Lock()
defer im.user.appendExpungeLock.Unlock()
// Avoid appending a message which is already on the server. Apply the
// new label instead. This always happens with Outlook (it uses APPEND
// instead of COPY).
if internalID != "" {
// Check to see if this belongs to a different address in split mode or another ProtonMail account.
msg, err := im.storeMailbox.GetMessage(internalID)
if err == nil && (im.user.user.IsCombinedAddressMode() || (im.storeAddress.AddressID() == msg.Message().AddressID)) {
IDs := []string{internalID}
// See the comment bellow.
if msg.IsMarkedDeleted() {
if err := im.storeMailbox.MarkMessagesUndeleted(IDs); err != nil {
log.WithError(err).Error("Failed to undelete re-imported internal message")
}
}
err = im.storeMailbox.LabelMessages(IDs)
if err != nil {
return err
}
targetSeq := im.storeMailbox.GetUIDList(IDs)
return uidplus.AppendResponse(im.storeMailbox.UIDValidity(), targetSeq)
}
}
im.log.Info("Importing external message")
if err := im.importMessage(m, readers, kr); err != nil {
im.log.Error("Import failed: ", err)
return err
}
// IMAP clients can move message to local folder (setting \Deleted flag)
// and then move it back (IMAP client does not remember the message,
// so instead removing the flag it imports duplicate message).
// Regular IMAP server would keep the message twice and later EXPUNGE would
// not delete the message (EXPUNGE would delete the original message and
// the new duplicate one would stay). API detects duplicates; therefore
// we need to remove \Deleted flag if IMAP client re-imports.
msg, err := im.storeMailbox.GetMessage(m.ID)
if err == nil && msg.IsMarkedDeleted() {
if err := im.storeMailbox.MarkMessagesUndeleted([]string{m.ID}); err != nil {
log.WithError(err).Error("Failed to undelete re-imported message")
}
}
targetSeq := im.storeMailbox.GetUIDList([]string{m.ID})
return uidplus.AppendResponse(im.storeMailbox.UIDValidity(), targetSeq)
}
func (im *imapMailbox) importMessage(m *pmapi.Message, readers []io.Reader, kr *crypto.KeyRing) (err error) {
body, err := message.BuildEncrypted(m, readers, kr)
if err != nil {
return err
}
labels := []string{}
for _, l := range m.LabelIDs {
if l == pmapi.StarredLabel {
labels = append(labels, pmapi.StarredLabel)
}
}
return im.storeMailbox.ImportMessage(m, body, labels)
}

View File

@ -0,0 +1,322 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail 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.
//
// ProtonMail 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 ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package imap
import (
"bytes"
"context"
"github.com/ProtonMail/proton-bridge/internal/imap/cache"
"github.com/ProtonMail/proton-bridge/pkg/message"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/emersion/go-imap"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
func (im *imapMailbox) getMessage(
storeMessage storeMessageProvider,
items []imap.FetchItem,
msgBuildCountHistogram *msgBuildCountHistogram,
) (msg *imap.Message, err error) {
msglog := im.log.WithField("msgID", storeMessage.ID())
msglog.Trace("Getting message")
seqNum, err := storeMessage.SequenceNumber()
if err != nil {
return
}
m := storeMessage.Message()
msg = imap.NewMessage(seqNum, items)
for _, item := range items {
switch item {
case imap.FetchEnvelope:
// No need to check IsFullHeaderCached here. API header
// contain enough information to build the envelope.
msg.Envelope = message.GetEnvelope(m, storeMessage.GetMIMEHeader())
case imap.FetchBody, imap.FetchBodyStructure:
structure, err := im.getBodyStructure(storeMessage)
if err != nil {
return nil, err
}
if msg.BodyStructure, err = structure.IMAPBodyStructure([]int{}); err != nil {
return nil, err
}
case imap.FetchFlags:
msg.Flags = message.GetFlags(m)
if storeMessage.IsMarkedDeleted() {
msg.Flags = append(msg.Flags, imap.DeletedFlag)
}
case imap.FetchInternalDate:
// Apple Mail crashes fetching messages with date older than 1970.
// There is no point having message older than RFC itself, it's not possible.
msg.InternalDate = message.SanitizeMessageDate(m.Time)
case imap.FetchRFC822Size:
if msg.Size, err = im.getSize(storeMessage); err != nil {
return nil, err
}
case imap.FetchUid:
if msg.Uid, err = storeMessage.UID(); err != nil {
return nil, err
}
case imap.FetchAll, imap.FetchFast, imap.FetchFull, imap.FetchRFC822, imap.FetchRFC822Header, imap.FetchRFC822Text:
fallthrough // this is list of defined items by go-imap, but items can be also sections generated from requests
default:
if err = im.getLiteralForSection(item, msg, storeMessage, msgBuildCountHistogram); err != nil {
return
}
}
}
return msg, err
}
// getSize returns cached size or it will build the message, save the size in
// DB and then returns the size after build.
//
// We are storing size in DB as part of pmapi messages metada. The size
// attribute on the server represents size of encrypted body. The value is
// cleared in Bridge and the final decrypted size (including header, attachment
// and MIME structure) is computed after building the message.
func (im *imapMailbox) getSize(storeMessage storeMessageProvider) (uint32, error) {
m := storeMessage.Message()
if m.Size <= 0 {
im.log.WithField("msgID", m.ID).Debug("Size unknown - downloading body")
// We are sure the size is not a problem right now. Clients
// might not first check sizes of all messages so we couldn't
// be sure if seeing 1st or 2nd sync is all right or not.
// Therefore, it's better to exclude getting size from the
// counting and see build count as real message build.
if _, _, err := im.getBodyAndStructure(storeMessage, nil); err != nil {
return 0, err
}
}
return uint32(m.Size), nil
}
func (im *imapMailbox) getLiteralForSection(
itemSection imap.FetchItem,
msg *imap.Message,
storeMessage storeMessageProvider,
msgBuildCountHistogram *msgBuildCountHistogram,
) error {
section, err := imap.ParseBodySectionName(itemSection)
if err != nil {
log.WithError(err).Warn("Failed to parse body section name; part will be skipped")
return nil //nolint[nilerr] ignore error
}
var literal imap.Literal
if literal, err = im.getMessageBodySection(storeMessage, section, msgBuildCountHistogram); err != nil {
return err
}
msg.Body[section] = literal
return nil
}
// getBodyStructure returns the cached body structure or it will build the message,
// save the structure in DB and then returns the structure after build.
//
// Apple Mail requests body structure for all messages irregularly. We cache
// bodystructure in local database in order to not re-download all messages
// from server.
func (im *imapMailbox) getBodyStructure(storeMessage storeMessageProvider) (bs *message.BodyStructure, err error) {
bs, err = storeMessage.GetBodyStructure()
if err != nil {
im.log.WithError(err).Debug("Fail to retrieve bodystructure from database")
}
if bs == nil {
// We are sure the body structure is not a problem right now.
// Clients might do first fetch body structure so we couldn't
// be sure if seeing 1st or 2nd sync is all right or not.
// Therefore, it's better to exclude first body structure fetch
// from the counting and see build count as real message build.
if bs, _, err = im.getBodyAndStructure(storeMessage, nil); err != nil {
return
}
}
return
}
func (im *imapMailbox) getBodyAndStructure(
storeMessage storeMessageProvider, msgBuildCountHistogram *msgBuildCountHistogram,
) (
structure *message.BodyStructure, bodyReader *bytes.Reader, err error,
) {
m := storeMessage.Message()
id := im.storeUser.UserID() + m.ID
cache.BuildLock(id)
defer cache.BuildUnlock(id)
bodyReader, structure = cache.LoadMail(id)
// return the message which was found in cache
if bodyReader.Len() != 0 && structure != nil {
return structure, bodyReader, nil
}
structure, body, err := im.buildMessage(m)
bodyReader = bytes.NewReader(body)
size := int64(len(body))
l := im.log.WithField("newSize", size).WithField("msgID", m.ID)
if err != nil || structure == nil || size == 0 {
l.WithField("hasStructure", structure != nil).Warn("Failed to build message")
return structure, bodyReader, err
}
// Save the size, body structure and header even for messages which
// were unable to decrypt. Hence they doesn't have to be computed every
// time.
m.Size = size
cacheMessageInStore(storeMessage, structure, body, l)
if msgBuildCountHistogram != nil {
times, errCount := storeMessage.IncreaseBuildCount()
if errCount != nil {
l.WithError(errCount).Warn("Cannot increase build count")
}
msgBuildCountHistogram.add(times)
}
// Drafts can change therefore we don't want to cache them.
if !isMessageInDraftFolder(m) {
cache.SaveMail(id, body, structure)
}
return structure, bodyReader, err
}
func cacheMessageInStore(storeMessage storeMessageProvider, structure *message.BodyStructure, body []byte, l *logrus.Entry) {
m := storeMessage.Message()
if errSize := storeMessage.SetSize(m.Size); errSize != nil {
l.WithError(errSize).Warn("Cannot update size while building")
}
if structure != nil && !isMessageInDraftFolder(m) {
if errStruct := storeMessage.SetBodyStructure(structure); errStruct != nil {
l.WithError(errStruct).Warn("Cannot update bodystructure while building")
}
}
header, errHead := structure.GetMailHeaderBytes(bytes.NewReader(body))
if errHead == nil && len(header) != 0 {
if errStore := storeMessage.SetHeader(header); errStore != nil {
l.WithError(errStore).Warn("Cannot update header in store")
}
} else {
l.WithError(errHead).Warn("Cannot get header bytes from structure")
}
}
func isMessageInDraftFolder(m *pmapi.Message) bool {
for _, labelID := range m.LabelIDs {
if labelID == pmapi.DraftLabel {
return true
}
}
return false
}
// This will download message (or read from cache) and pick up the section,
// extract data (header,body, both) and trim the output if needed.
//
// In order to speed up (avoid download and decryptions) we
// cache the header. If a mail header was requested and DB
// contains full header (it means it was already built once)
// the DB header can be used without downloading and decrypting.
// Otherwise header is incomplete and clients would have issues
// e.g. AppleMail expects `text/plain` in HTML mails.
//
// For all other cases it is necessary to download and decrypt the message
// and drop the header which was obtained from cache. The header will
// will be stored in DB once successfully built. Check `getBodyAndStructure`.
func (im *imapMailbox) getMessageBodySection(
storeMessage storeMessageProvider,
section *imap.BodySectionName,
msgBuildCountHistogram *msgBuildCountHistogram,
) (imap.Literal, error) {
var header []byte
var response []byte
im.log.WithField("msgID", storeMessage.ID()).Trace("Getting message body")
isMainHeaderRequested := len(section.Path) == 0 && section.Specifier == imap.HeaderSpecifier
if isMainHeaderRequested && storeMessage.IsFullHeaderCached() {
header = storeMessage.GetHeader()
} else {
structure, bodyReader, err := im.getBodyAndStructure(storeMessage, msgBuildCountHistogram)
if err != nil {
return nil, err
}
switch {
case section.Specifier == imap.EntireSpecifier && len(section.Path) == 0:
// An empty section specification refers to the entire message, including the header.
response, err = structure.GetSection(bodyReader, section.Path)
case section.Specifier == imap.TextSpecifier || (section.Specifier == imap.EntireSpecifier && len(section.Path) != 0):
// The TEXT specifier refers to the content of the message (or section), omitting the [RFC-2822] header.
// Non-empty section with no specifier (imap.EntireSpecifier) refers to section content without header.
response, err = structure.GetSectionContent(bodyReader, section.Path)
case section.Specifier == imap.MIMESpecifier: // The MIME part specifier refers to the [MIME-IMB] header for this part.
fallthrough
case section.Specifier == imap.HeaderSpecifier:
header, err = structure.GetSectionHeaderBytes(bodyReader, section.Path)
default:
err = errors.New("Unknown specifier " + string(section.Specifier))
}
if err != nil {
return nil, err
}
}
if header != nil {
response = filterHeader(header, section)
}
// Trim any output if requested.
return bytes.NewBuffer(section.ExtractPartial(response)), nil
}
// buildMessage from PM to IMAP.
func (im *imapMailbox) buildMessage(m *pmapi.Message) (*message.BodyStructure, []byte, error) {
body, err := im.builder.NewJobWithOptions(
context.Background(),
im.user.client(),
m.ID,
message.JobOptions{
IgnoreDecryptionErrors: true, // Whether to ignore decryption errors and create a "custom message" instead.
SanitizeDate: true, // Whether to replace all dates before 1970 with RFC822's birthdate.
AddInternalID: true, // Whether to include MessageID as X-Pm-Internal-Id.
AddExternalID: true, // Whether to include ExternalID as X-Pm-External-Id.
AddMessageDate: true, // Whether to include message time as X-Pm-Date.
AddMessageIDReference: true, // Whether to include the MessageID in References.
},
).GetResult()
if err != nil {
return nil, nil, err
}
structure, err := message.NewBodyStructure(bytes.NewReader(body))
if err != nil {
return nil, nil, err
}
return structure, body, nil
}

View File

@ -0,0 +1,67 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail 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.
//
// ProtonMail 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 ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package imap
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestFilterHeader(t *testing.T) {
const header = "To: somebody\r\nFrom: somebody else\r\nSubject: this is\r\n\ta multiline field\r\n\r\n"
assert.Equal(t, "To: somebody\r\n\r\n", string(filterHeaderLines([]byte(header), func(field string) bool {
return strings.EqualFold(field, "To")
})))
assert.Equal(t, "From: somebody else\r\n\r\n", string(filterHeaderLines([]byte(header), func(field string) bool {
return strings.EqualFold(field, "From")
})))
assert.Equal(t, "To: somebody\r\nFrom: somebody else\r\n\r\n", string(filterHeaderLines([]byte(header), func(field string) bool {
return strings.EqualFold(field, "To") || strings.EqualFold(field, "From")
})))
assert.Equal(t, "Subject: this is\r\n\ta multiline field\r\n\r\n", string(filterHeaderLines([]byte(header), func(field string) bool {
return strings.EqualFold(field, "Subject")
})))
}
// TestFilterHeaderNoNewline tests that we don't include a trailing newline when filtering
// if the original header also lacks one (which it can legally do if there is no body).
func TestFilterHeaderNoNewline(t *testing.T) {
const header = "To: somebody\r\nFrom: somebody else\r\nSubject: this is\r\n\ta multiline field\r\n"
assert.Equal(t, "To: somebody\r\n", string(filterHeaderLines([]byte(header), func(field string) bool {
return strings.EqualFold(field, "To")
})))
assert.Equal(t, "From: somebody else\r\n", string(filterHeaderLines([]byte(header), func(field string) bool {
return strings.EqualFold(field, "From")
})))
assert.Equal(t, "To: somebody\r\nFrom: somebody else\r\n", string(filterHeaderLines([]byte(header), func(field string) bool {
return strings.EqualFold(field, "To") || strings.EqualFold(field, "From")
})))
assert.Equal(t, "Subject: this is\r\n\ta multiline field\r\n", string(filterHeaderLines([]byte(header), func(field string) bool {
return strings.EqualFold(field, "Subject")
})))
}

View File

@ -0,0 +1,104 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail 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.
//
// ProtonMail 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 ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package imap
import (
"bufio"
"bytes"
"io"
"strings"
"github.com/emersion/go-imap"
"github.com/pkg/errors"
)
func filterHeader(header []byte, section *imap.BodySectionName) []byte {
// Empty section.Fields means BODY[HEADER] was requested so we should return the full header.
if len(section.Fields) == 0 {
return header
}
fieldMap := make(map[string]struct{})
for _, field := range section.Fields {
fieldMap[strings.ToLower(field)] = struct{}{}
}
return filterHeaderLines(header, func(field string) bool {
_, ok := fieldMap[strings.ToLower(field)]
if section.NotFields {
ok = !ok
}
return ok
})
}
func filterHeaderLines(header []byte, wantField func(string) bool) []byte {
var res []byte
for _, line := range headerLines(header) {
if len(bytes.TrimSpace(line)) == 0 {
res = append(res, line...)
} else {
split := bytes.SplitN(line, []byte(": "), 2)
if len(split) != 2 {
continue
}
if wantField(string(bytes.ToLower(split[0]))) {
res = append(res, line...)
}
}
}
return res
}
// NOTE: This sucks because we trim and split stuff here already, only to do it again when we use this function!
func headerLines(header []byte) [][]byte {
var lines [][]byte
r := bufio.NewReader(bytes.NewReader(header))
for {
b, err := r.ReadBytes('\n')
if err != nil {
if err != io.EOF {
panic(errors.Wrap(err, "failed to read header line"))
}
break
}
switch {
case len(bytes.TrimSpace(b)) == 0:
lines = append(lines, b)
case len(bytes.SplitN(b, []byte(": "), 2)) != 2:
lines[len(lines)-1] = append(lines[len(lines)-1], b...)
default:
lines = append(lines, b)
}
}
return lines
}

View File

@ -1,809 +0,0 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail 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.
//
// ProtonMail 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 ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package imap
import (
"bytes"
"fmt"
"io"
"mime/multipart"
"net/mail"
"net/textproto"
"sort"
"strings"
"time"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/ProtonMail/proton-bridge/internal/imap/cache"
"github.com/ProtonMail/proton-bridge/internal/imap/uidplus"
"github.com/ProtonMail/proton-bridge/pkg/message"
"github.com/ProtonMail/proton-bridge/pkg/parallel"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/emersion/go-imap"
"github.com/hashicorp/go-multierror"
"github.com/pkg/errors"
openpgperrors "golang.org/x/crypto/openpgp/errors"
)
var (
rfc822Birthday = time.Date(1982, 8, 13, 0, 0, 0, 0, time.UTC) //nolint[gochecknoglobals]
)
type doNotCacheError struct{ e error }
func (dnc *doNotCacheError) Error() string { return dnc.e.Error() }
func (dnc *doNotCacheError) add(err error) { dnc.e = multierror.Append(dnc.e, err) }
func (dnc *doNotCacheError) errorOrNil() error {
if dnc == nil {
return nil
}
if dnc.e != nil {
return dnc
}
return nil
}
// CreateMessage appends a new message to this mailbox. The \Recent flag will
// be added regardless of whether flags is empty or not. If date is nil, the
// current time will be used.
//
// If the Backend implements Updater, it must notify the client immediately
// via a mailbox update.
func (im *imapMailbox) CreateMessage(flags []string, date time.Time, body imap.Literal) error {
return im.logCommand(func() error {
return im.createMessage(flags, date, body)
}, "APPEND", flags, date)
}
func (im *imapMailbox) createMessage(flags []string, date time.Time, body imap.Literal) error { // nolint[funlen]
// Called from go-imap in goroutines - we need to handle panics for each function.
defer im.panicHandler.HandlePanic()
m, _, _, readers, err := message.Parse(body)
if err != nil {
return err
}
addr := im.storeAddress.APIAddress()
if addr == nil {
return errors.New("no available address for encryption")
}
m.AddressID = addr.ID
kr, err := im.user.client().KeyRingForAddressID(addr.ID)
if err != nil {
return err
}
// Handle imported messages which have no "Sender" address.
// This sometimes occurs with outlook which reports errors as imported emails or for drafts.
if m.Sender == nil {
im.log.Warning("Append: Missing email sender. Will use main address")
m.Sender = &mail.Address{
Name: "",
Address: addr.Email,
}
}
// "Drafts" needs to call special API routes.
// Clients always append the whole message again and remove the old one.
if im.storeMailbox.LabelID() == pmapi.DraftLabel {
// Sender address needs to be sanitised (drafts need to match cases exactly).
m.Sender.Address = pmapi.ConstructAddress(m.Sender.Address, addr.Email)
draft, _, err := im.user.storeUser.CreateDraft(kr, m, readers, "", "", "")
if err != nil {
return errors.Wrap(err, "failed to create draft")
}
targetSeq := im.storeMailbox.GetUIDList([]string{draft.ID})
return uidplus.AppendResponse(im.storeMailbox.UIDValidity(), targetSeq)
}
// We need to make sure this is an import, and not a sent message from this account
// (sent messages from the account will be added by the event loop).
if im.storeMailbox.LabelID() == pmapi.SentLabel {
sanitizedSender := pmapi.SanitizeEmail(m.Sender.Address)
// Check whether this message was sent by a bridge user.
user, err := im.user.backend.bridge.GetUser(sanitizedSender)
if err == nil && user.ID() == im.storeUser.UserID() {
logEntry := im.log.WithField("addr", sanitizedSender).WithField("extID", m.Header.Get("Message-Id"))
// If we find the message in the store already, we can skip importing it.
if foundUID := im.storeMailbox.GetUIDByHeader(&m.Header); foundUID != uint32(0) {
logEntry.Info("Ignoring APPEND of duplicate to Sent folder")
return uidplus.AppendResponse(im.storeMailbox.UIDValidity(), &uidplus.OrderedSeq{foundUID})
}
// We didn't find the message in the store, so we are currently sending it.
logEntry.WithField("time", date).Info("No matching UID, continuing APPEND to Sent")
}
}
message.ParseFlags(m, flags)
if !date.IsZero() {
m.Time = date.Unix()
}
internalID := m.Header.Get("X-Pm-Internal-Id")
references := m.Header.Get("References")
referenceList := strings.Fields(references)
// In case there is a mail client which corrupts headers, try
// "References" too.
if internalID == "" && len(referenceList) > 0 {
lastReference := referenceList[len(referenceList)-1]
match := pmapi.RxInternalReferenceFormat.FindStringSubmatch(lastReference)
if len(match) == 2 {
internalID = match[1]
}
}
im.user.appendExpungeLock.Lock()
defer im.user.appendExpungeLock.Unlock()
// Avoid appending a message which is already on the server. Apply the
// new label instead. This always happens with Outlook (it uses APPEND
// instead of COPY).
if internalID != "" {
// Check to see if this belongs to a different address in split mode or another ProtonMail account.
msg, err := im.storeMailbox.GetMessage(internalID)
if err == nil && (im.user.user.IsCombinedAddressMode() || (im.storeAddress.AddressID() == msg.Message().AddressID)) {
IDs := []string{internalID}
// See the comment bellow.
if msg.IsMarkedDeleted() {
if err := im.storeMailbox.MarkMessagesUndeleted(IDs); err != nil {
log.WithError(err).Error("Failed to undelete re-imported internal message")
}
}
err = im.storeMailbox.LabelMessages(IDs)
if err != nil {
return err
}
targetSeq := im.storeMailbox.GetUIDList(IDs)
return uidplus.AppendResponse(im.storeMailbox.UIDValidity(), targetSeq)
}
}
im.log.Info("Importing external message")
if err := im.importMessage(m, readers, kr); err != nil {
im.log.Error("Import failed: ", err)
return err
}
// IMAP clients can move message to local folder (setting \Deleted flag)
// and then move it back (IMAP client does not remember the message,
// so instead removing the flag it imports duplicate message).
// Regular IMAP server would keep the message twice and later EXPUNGE would
// not delete the message (EXPUNGE would delete the original message and
// the new duplicate one would stay). API detects duplicates; therefore
// we need to remove \Deleted flag if IMAP client re-imports.
msg, err := im.storeMailbox.GetMessage(m.ID)
if err == nil && msg.IsMarkedDeleted() {
if err := im.storeMailbox.MarkMessagesUndeleted([]string{m.ID}); err != nil {
log.WithError(err).Error("Failed to undelete re-imported message")
}
}
targetSeq := im.storeMailbox.GetUIDList([]string{m.ID})
return uidplus.AppendResponse(im.storeMailbox.UIDValidity(), targetSeq)
}
func (im *imapMailbox) importMessage(m *pmapi.Message, readers []io.Reader, kr *crypto.KeyRing) (err error) { // nolint[funlen]
body, err := message.BuildEncrypted(m, readers, kr)
if err != nil {
return err
}
labels := []string{}
for _, l := range m.LabelIDs {
if l == pmapi.StarredLabel {
labels = append(labels, pmapi.StarredLabel)
}
}
return im.storeMailbox.ImportMessage(m, body, labels)
}
func (im *imapMailbox) getMessage(storeMessage storeMessageProvider, items []imap.FetchItem, msgBuildCountHistogram *msgBuildCountHistogram) (msg *imap.Message, err error) { //nolint[funlen]
im.log.WithField("msgID", storeMessage.ID()).Trace("Getting message")
seqNum, err := storeMessage.SequenceNumber()
if err != nil {
return
}
m := storeMessage.Message()
msg = imap.NewMessage(seqNum, items)
for _, item := range items {
switch item {
case imap.FetchEnvelope:
msg.Envelope = message.GetEnvelope(m)
case imap.FetchBody, imap.FetchBodyStructure:
var structure *message.BodyStructure
structure, err = im.getBodyStructure(storeMessage)
if err != nil {
return
}
if msg.BodyStructure, err = structure.IMAPBodyStructure([]int{}); err != nil {
return
}
case imap.FetchFlags:
msg.Flags = message.GetFlags(m)
if storeMessage.IsMarkedDeleted() {
msg.Flags = append(msg.Flags, imap.DeletedFlag)
}
case imap.FetchInternalDate:
msg.InternalDate = time.Unix(m.Time, 0)
// Apple Mail crashes fetching messages with date older than 1970.
// There is no point having message older than RFC itself, it's not possible.
if msg.InternalDate.Before(rfc822Birthday) {
msg.InternalDate = rfc822Birthday
}
case imap.FetchRFC822Size:
// Size attribute on the server counts encrypted data. The value is cleared
// on our part and we need to compute "real" size of decrypted data.
if m.Size <= 0 {
im.log.WithField("msgID", storeMessage.ID()).Trace("Size unknown - downloading body")
// We are sure the size is not a problem right now. Clients
// might not first check sizes of all messages so we couldn't
// be sure if seeing 1st or 2nd sync is all right or not.
// Therefore, it's better to exclude getting size from the
// counting and see build count as real message build.
if _, _, err = im.getBodyAndStructure(storeMessage, nil); err != nil {
return
}
}
msg.Size = uint32(m.Size)
case imap.FetchUid:
msg.Uid, err = storeMessage.UID()
if err != nil {
return nil, err
}
default:
if err = im.getLiteralForSection(item, msg, storeMessage, msgBuildCountHistogram); err != nil {
return
}
}
}
return msg, err
}
func (im *imapMailbox) getLiteralForSection(itemSection imap.FetchItem, msg *imap.Message, storeMessage storeMessageProvider, msgBuildCountHistogram *msgBuildCountHistogram) error {
section, err := imap.ParseBodySectionName(itemSection)
if err != nil { // Ignore error
return nil
}
var literal imap.Literal
if literal, err = im.getMessageBodySection(storeMessage, section, msgBuildCountHistogram); err != nil {
return err
}
msg.Body[section] = literal
return nil
}
func (im *imapMailbox) getBodyStructure(storeMessage storeMessageProvider) (bs *message.BodyStructure, err error) {
// Apple Mail requests body structure for all
// messages irregularly. We cache bodystructure in
// local database in order to not re-download all
// messages from server.
bs, err = storeMessage.GetBodyStructure()
if err != nil {
im.log.WithError(err).Debug("Fail to retrieve bodystructure from database")
}
if bs == nil {
// We are sure the body structure is not a problem right now.
// Clients might do first fetch body structure so we couldn't
// be sure if seeing 1st or 2nd sync is all right or not.
// Therefore, it's better to exclude first body structure fetch
// from the counting and see build count as real message build.
if bs, _, err = im.getBodyAndStructure(storeMessage, nil); err != nil {
return
}
}
return
}
func (im *imapMailbox) getBodyAndStructure(storeMessage storeMessageProvider, msgBuildCountHistogram *msgBuildCountHistogram) (
structure *message.BodyStructure,
bodyReader *bytes.Reader, err error,
) {
m := storeMessage.Message()
id := im.storeUser.UserID() + m.ID
cache.BuildLock(id)
if bodyReader, structure = cache.LoadMail(id); bodyReader.Len() == 0 || structure == nil {
var body []byte
structure, body, err = im.buildMessage(m)
m.Size = int64(len(body))
// Save size and body structure even for messages unable to decrypt
// so the size or body structure doesn't have to be computed every time.
if err := storeMessage.SetSize(m.Size); err != nil {
im.log.WithError(err).
WithField("newSize", m.Size).
WithField("msgID", m.ID).
Warn("Cannot update size while building")
}
if structure != nil && !isMessageInDraftFolder(m) {
if err := storeMessage.SetBodyStructure(structure); err != nil {
im.log.WithError(err).
WithField("msgID", m.ID).
Warn("Cannot update bodystructure while building")
}
}
if err == nil && structure != nil && len(body) > 0 {
if err := storeMessage.SetContentTypeAndHeader(m.MIMEType, m.Header); err != nil {
im.log.WithError(err).
WithField("msgID", m.ID).
Warn("Cannot update header while building")
}
if msgBuildCountHistogram != nil {
times, err := storeMessage.IncreaseBuildCount()
if err != nil {
im.log.WithError(err).
WithField("msgID", m.ID).
Warn("Cannot increase build count")
}
msgBuildCountHistogram.add(times)
}
// Drafts can change and we don't want to cache them.
if !isMessageInDraftFolder(m) {
cache.SaveMail(id, body, structure)
}
bodyReader = bytes.NewReader(body)
}
if _, ok := err.(*doNotCacheError); ok {
im.log.WithField("msgID", m.ID).Errorf("do not cache message: %v", err)
err = nil
bodyReader = bytes.NewReader(body)
}
}
cache.BuildUnlock(id)
return structure, bodyReader, err
}
func isMessageInDraftFolder(m *pmapi.Message) bool {
for _, labelID := range m.LabelIDs {
if labelID == pmapi.DraftLabel {
return true
}
}
return false
}
// This will download message (or read from cache) and pick up the section,
// extract data (header,body, both) and trim the output if needed.
func (im *imapMailbox) getMessageBodySection(storeMessage storeMessageProvider, section *imap.BodySectionName, msgBuildCountHistogram *msgBuildCountHistogram) (literal imap.Literal, err error) { // nolint[funlen]
var (
structure *message.BodyStructure
bodyReader *bytes.Reader
header textproto.MIMEHeader
response []byte
)
im.log.WithField("msgID", storeMessage.ID()).Trace("Getting message body")
m := storeMessage.Message()
if len(section.Path) == 0 && section.Specifier == imap.HeaderSpecifier {
// We can extract message header without decrypting.
header = message.GetHeader(m)
// We need to ensure we use the correct content-type,
// otherwise AppleMail expects `text/plain` in HTML mails.
if header.Get("Content-Type") == "" {
if err = im.fetchMessage(m); err != nil {
return
}
if _, err = im.setMessageContentType(m); err != nil {
return
}
if err = storeMessage.SetContentTypeAndHeader(m.MIMEType, m.Header); err != nil {
return
}
header = message.GetHeader(m)
}
} else {
// The rest of cases need download and decrypt.
structure, bodyReader, err = im.getBodyAndStructure(storeMessage, msgBuildCountHistogram)
if err != nil {
return
}
switch {
case section.Specifier == imap.EntireSpecifier && len(section.Path) == 0:
// An empty section specification refers to the entire message, including the header.
response, err = structure.GetSection(bodyReader, section.Path)
case section.Specifier == imap.TextSpecifier || (section.Specifier == imap.EntireSpecifier && len(section.Path) != 0):
// The TEXT specifier refers to the content of the message (or section), omitting the [RFC-2822] header.
// Non-empty section with no specifier (imap.EntireSpecifier) refers to section content without header.
response, err = structure.GetSectionContent(bodyReader, section.Path)
case section.Specifier == imap.MIMESpecifier:
// The MIME part specifier refers to the [MIME-IMB] header for this part.
fallthrough
case section.Specifier == imap.HeaderSpecifier:
header, err = structure.GetSectionHeader(section.Path)
default:
err = errors.New("Unknown specifier " + string(section.Specifier))
}
}
if err != nil {
return
}
// Filter header. Options are: all fields, only selected fields, all fields except selected.
if header != nil {
// remove fields
if len(section.Fields) != 0 && section.NotFields {
for _, field := range section.Fields {
header.Del(field)
}
}
fields := make([]string, 0, len(header))
if len(section.Fields) == 0 || section.NotFields { // add all and sort
for f := range header {
fields = append(fields, f)
}
sort.Strings(fields)
} else { // add only requested (in requested order)
for _, f := range section.Fields {
fields = append(fields, textproto.CanonicalMIMEHeaderKey(f))
}
}
headerBuf := &bytes.Buffer{}
for _, canonical := range fields {
if values, ok := header[canonical]; !ok {
continue
} else {
for _, val := range values {
fmt.Fprintf(headerBuf, "%s: %s\r\n", canonical, val)
}
}
}
response = headerBuf.Bytes()
}
// Trim any output if requested.
literal = bytes.NewBuffer(section.ExtractPartial(response))
return literal, nil
}
func (im *imapMailbox) fetchMessage(m *pmapi.Message) (err error) {
im.log.Trace("Fetching message")
complete, err := im.storeMailbox.FetchMessage(m.ID)
if err != nil {
im.log.WithError(err).Error("Could not get message from store")
return
}
*m = *complete.Message()
return
}
func (im *imapMailbox) writeMessageBody(w io.Writer, m *pmapi.Message) (err error) {
im.log.Trace("Writing message body")
if m.Body == "" {
im.log.Trace("While writing message body, noticed message body is null, need to fetch")
if err = im.fetchMessage(m); err != nil {
return
}
}
kr, err := im.user.client().KeyRingForAddressID(m.AddressID)
if err != nil {
return errors.Wrap(err, "failed to get keyring for address ID")
}
err = message.WriteBody(w, kr, m)
if err != nil {
if customMessageErr := message.CustomMessage(m, err, true); customMessageErr != nil {
im.log.WithError(customMessageErr).Warn("Failed to make custom message")
}
_, _ = io.WriteString(w, m.Body)
err = nil
}
return
}
func (im *imapMailbox) writeAttachmentBody(w io.Writer, m *pmapi.Message, att *pmapi.Attachment) (err error) {
// Retrieve encrypted attachment.
r, err := im.user.client().GetAttachment(att.ID)
if err != nil {
return
}
defer r.Close() //nolint[errcheck]
kr, err := im.user.client().KeyRingForAddressID(m.AddressID)
if err != nil {
return errors.Wrap(err, "failed to get keyring for address ID")
}
if err = message.WriteAttachmentBody(w, kr, m, att, r); err != nil {
// Returning an error here makes certain mail clients behave badly,
// trying to retrieve the message again and again.
im.log.Warn("Cannot write attachment body: ", err)
err = nil
}
return
}
func (im *imapMailbox) writeRelatedPart(p io.Writer, m *pmapi.Message, inlines []*pmapi.Attachment) (err error) {
related := multipart.NewWriter(p)
_ = related.SetBoundary(message.GetRelatedBoundary(m))
buf := &bytes.Buffer{}
if err = im.writeMessageBody(buf, m); err != nil {
return
}
// Write the body part.
h := message.GetBodyHeader(m)
if p, err = related.CreatePart(h); err != nil {
return
}
_, _ = buf.WriteTo(p)
for _, inline := range inlines {
buf = &bytes.Buffer{}
if err = im.writeAttachmentBody(buf, m, inline); err != nil {
return
}
h := message.GetAttachmentHeader(inline, true)
if p, err = related.CreatePart(h); err != nil {
return
}
_, _ = buf.WriteTo(p)
}
_ = related.Close()
return nil
}
const (
noMultipart = iota // only body
simpleMultipart // body + attachment or inline
complexMultipart // mixed, rfc822, alternatives, ...
)
func (im *imapMailbox) setMessageContentType(m *pmapi.Message) (multipartType int, err error) {
if m.MIMEType == "" {
err = fmt.Errorf("trying to set Content-Type without MIME TYPE")
return
}
// message.MIMEType can have just three values from our server:
// * `text/html` (refers to body type, but might contain attachments and inlines)
// * `text/plain` (refers to body type, but might contain attachments and inlines)
// * `multipart/mixed` (refers to external message with multipart structure)
// The proper header content fields must be set and saved to DB based MIMEType and content.
multipartType = noMultipart
if m.MIMEType == pmapi.ContentTypeMultipartMixed {
multipartType = complexMultipart
} else if m.NumAttachments != 0 {
multipartType = simpleMultipart
}
h := textproto.MIMEHeader(m.Header)
if multipartType == noMultipart {
message.SetBodyContentFields(&h, m)
} else {
h.Set("Content-Type",
fmt.Sprintf("%s; boundary=%s", "multipart/mixed", message.GetBoundary(m)),
)
}
m.Header = mail.Header(h)
return
}
// buildMessage from PM to IMAP.
func (im *imapMailbox) buildMessage(m *pmapi.Message) (structure *message.BodyStructure, msgBody []byte, err error) {
im.log.Trace("Building message")
var errNoCache doNotCacheError
// If fetch or decryption fails we need to change the MIMEType (in customMessage).
err = im.fetchMessage(m)
if err != nil {
return
}
kr, err := im.user.client().KeyRingForAddressID(m.AddressID)
if err != nil {
err = errors.Wrap(err, "failed to get keyring for address ID")
return
}
errDecrypt := m.Decrypt(kr)
if errDecrypt != nil && errDecrypt != openpgperrors.ErrSignatureExpired {
errNoCache.add(errDecrypt)
if customMessageErr := message.CustomMessage(m, errDecrypt, true); customMessageErr != nil {
im.log.WithError(customMessageErr).Warn("Failed to make custom message")
}
}
// Inner function can fail even when message is decrypted.
// #1048 For example we have problem with double-encrypted messages
// which seems as still encrypted and we try them to decrypt again
// and that fails. For any building error is better to return custom
// message than error because it will not be fixed and users would
// get error message all the time and could not see some messages.
structure, msgBody, err = im.buildMessageInner(m, kr)
if err == pmapi.ErrAPINotReachable || err == pmapi.ErrInvalidToken || err == pmapi.ErrUpgradeApplication {
return nil, nil, err
} else if err != nil {
errNoCache.add(err)
if customMessageErr := message.CustomMessage(m, err, true); customMessageErr != nil {
im.log.WithError(customMessageErr).Warn("Failed to make custom message")
}
structure, msgBody, err = im.buildMessageInner(m, kr)
if err != nil {
return nil, nil, err
}
}
err = errNoCache.errorOrNil()
return structure, msgBody, err
}
func (im *imapMailbox) buildMessageInner(m *pmapi.Message, kr *crypto.KeyRing) (structure *message.BodyStructure, msgBody []byte, err error) { // nolint[funlen]
multipartType, err := im.setMessageContentType(m)
if err != nil {
return
}
tmpBuf := &bytes.Buffer{}
mainHeader := buildHeader(m)
if err = writeHeader(tmpBuf, mainHeader); err != nil {
return
}
_, _ = io.WriteString(tmpBuf, "\r\n")
switch multipartType {
case noMultipart:
err = message.WriteBody(tmpBuf, kr, m)
if err != nil {
return
}
case complexMultipart:
_, _ = io.WriteString(tmpBuf, "\r\n--"+message.GetBoundary(m)+"\r\n")
err = message.WriteBody(tmpBuf, kr, m)
if err != nil {
return
}
_, _ = io.WriteString(tmpBuf, "\r\n--"+message.GetBoundary(m)+"--\r\n")
case simpleMultipart:
atts, inlines := message.SeparateInlineAttachments(m)
mw := multipart.NewWriter(tmpBuf)
_ = mw.SetBoundary(message.GetBoundary(m))
var partWriter io.Writer
if len(inlines) > 0 {
relatedHeader := message.GetRelatedHeader(m)
if partWriter, err = mw.CreatePart(relatedHeader); err != nil {
return
}
_ = im.writeRelatedPart(partWriter, m, inlines)
} else {
buf := &bytes.Buffer{}
if err = im.writeMessageBody(buf, m); err != nil {
return
}
// Write the body part.
bodyHeader := message.GetBodyHeader(m)
if partWriter, err = mw.CreatePart(bodyHeader); err != nil {
return
}
_, _ = buf.WriteTo(partWriter)
}
// Write the attachments parts.
input := make([]interface{}, len(atts))
for i, att := range atts {
input[i] = att
}
processCallback := func(value interface{}) (interface{}, error) {
att := value.(*pmapi.Attachment)
buf := &bytes.Buffer{}
if err = im.writeAttachmentBody(buf, m, att); err != nil {
return nil, err
}
return buf, nil
}
collectCallback := func(idx int, value interface{}) error {
buf := value.(*bytes.Buffer)
defer buf.Reset()
att := atts[idx]
attachmentHeader := message.GetAttachmentHeader(att, true)
if partWriter, err = mw.CreatePart(attachmentHeader); err != nil {
return err
}
_, _ = buf.WriteTo(partWriter)
return nil
}
err = parallel.RunParallel(fetchAttachmentsWorkers, input, processCallback, collectCallback)
if err != nil {
return
}
_ = mw.Close()
default:
fmt.Fprintf(tmpBuf, "\r\n\r\nUknown multipart type: %d\r\n\r\n", multipartType)
}
// We need to copy buffer before building body structure.
msgBody = tmpBuf.Bytes()
structure, err = message.NewBodyStructure(tmpBuf)
if err != nil {
// NOTE: We need to set structure if it fails and is empty.
if structure == nil {
structure = &message.BodyStructure{}
}
}
return structure, msgBody, err
}
func buildHeader(msg *pmapi.Message) textproto.MIMEHeader {
header := message.GetHeader(msg)
msgTime := time.Unix(msg.Time, 0)
// Apple Mail crashes fetching messages with date older than 1970.
// There is no point having message older than RFC itself, it's not possible.
d, err := msg.Header.Date()
if err != nil || d.Before(rfc822Birthday) || msgTime.Before(rfc822Birthday) {
if err != nil || d.IsZero() {
header.Set("X-Original-Date", msgTime.Format(time.RFC1123Z))
} else {
header.Set("X-Original-Date", d.Format(time.RFC1123Z))
}
header.Set("Date", rfc822Birthday.Format(time.RFC1123Z))
}
return header
}

View File

@ -18,7 +18,6 @@
package imap package imap
import ( import (
"errors"
"fmt" "fmt"
"net/mail" "net/mail"
"strings" "strings"
@ -30,6 +29,7 @@ import (
"github.com/ProtonMail/proton-bridge/pkg/parallel" "github.com/ProtonMail/proton-bridge/pkg/parallel"
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/emersion/go-imap" "github.com/emersion/go-imap"
"github.com/pkg/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@ -141,7 +141,7 @@ func (im *imapMailbox) addOrRemoveFlags(operation imap.FlagsOp, messageIDs, flag
for _, f := range flags { for _, f := range flags {
switch f { switch f {
case imap.SeenFlag: case imap.SeenFlag:
switch operation { switch operation { //nolint[exhaustive] imap.SetFlags is processed by im.setFlags
case imap.AddFlags: case imap.AddFlags:
if err := im.storeMailbox.MarkMessagesRead(messageIDs); err != nil { if err := im.storeMailbox.MarkMessagesRead(messageIDs); err != nil {
return err return err
@ -152,7 +152,7 @@ func (im *imapMailbox) addOrRemoveFlags(operation imap.FlagsOp, messageIDs, flag
} }
} }
case imap.FlaggedFlag: case imap.FlaggedFlag:
switch operation { switch operation { //nolint[exhaustive] imap.SetFlag is processed by im.setFlags
case imap.AddFlags: case imap.AddFlags:
if err := im.storeMailbox.MarkMessagesStarred(messageIDs); err != nil { if err := im.storeMailbox.MarkMessagesStarred(messageIDs); err != nil {
return err return err
@ -163,7 +163,7 @@ func (im *imapMailbox) addOrRemoveFlags(operation imap.FlagsOp, messageIDs, flag
} }
} }
case imap.DeletedFlag: case imap.DeletedFlag:
switch operation { switch operation { //nolint[exhaustive] imap.SetFlag is processed by im.setFlags
case imap.AddFlags: case imap.AddFlags:
if err := im.storeMailbox.MarkMessagesDeleted(messageIDs); err != nil { if err := im.storeMailbox.MarkMessagesDeleted(messageIDs); err != nil {
return err return err
@ -182,7 +182,7 @@ func (im *imapMailbox) addOrRemoveFlags(operation imap.FlagsOp, messageIDs, flag
} }
// Handle custom junk flags for Apple Mail and Thunderbird. // Handle custom junk flags for Apple Mail and Thunderbird.
switch operation { switch operation { //nolint[exhaustive] imap.SetFlag is processed by im.setFlags
// No label removal is necessary because Spam and Inbox are both exclusive labels so the backend // No label removal is necessary because Spam and Inbox are both exclusive labels so the backend
// will automatically take care of label removal. // will automatically take care of label removal.
case imap.AddFlags: case imap.AddFlags:
@ -358,23 +358,28 @@ func (im *imapMailbox) SearchMessages(isUID bool, criteria *imap.SearchCriteria)
continue continue
} }
} }
// In order to speed up search it is not needed to check if IsFullHeaderCached.
header := storeMessage.GetMIMEHeader()
if !criteria.SentBefore.IsZero() || !criteria.SentSince.IsZero() { if !criteria.SentBefore.IsZero() || !criteria.SentSince.IsZero() {
if t, err := m.Header.Date(); err == nil && !t.IsZero() { t, err := mail.Header(header).Date()
if !criteria.SentBefore.IsZero() { if err != nil || t.IsZero() {
if truncated := criteria.SentBefore.Truncate(24 * time.Hour); t.Unix() > truncated.Unix() { t = time.Unix(m.Time, 0)
continue }
} if !criteria.SentBefore.IsZero() {
if truncated := criteria.SentBefore.Truncate(24 * time.Hour); t.Unix() > truncated.Unix() {
continue
} }
if !criteria.SentSince.IsZero() { }
if truncated := criteria.SentSince.Truncate(24 * time.Hour); t.Unix() < truncated.Unix() { if !criteria.SentSince.IsZero() {
continue if truncated := criteria.SentSince.Truncate(24 * time.Hour); t.Unix() < truncated.Unix() {
} continue
} }
} }
} }
// Filter by headers. // Filter by headers.
header := message.GetHeader(m)
headerMatch := true headerMatch := true
for criteriaKey, criteriaValues := range criteria.Header { for criteriaKey, criteriaValues := range criteria.Header {
for _, criteriaValue := range criteriaValues { for _, criteriaValue := range criteriaValues {
@ -382,6 +387,8 @@ func (im *imapMailbox) SearchMessages(isUID bool, criteria *imap.SearchCriteria)
continue continue
} }
switch criteriaKey { switch criteriaKey {
case "Subject":
headerMatch = strings.Contains(strings.ToLower(m.Subject), strings.ToLower(criteriaValue))
case "From": case "From":
headerMatch = addressMatch([]*mail.Address{m.Sender}, criteriaValue) headerMatch = addressMatch([]*mail.Address{m.Sender}, criteriaValue)
case "To": case "To":
@ -414,7 +421,7 @@ func (im *imapMailbox) SearchMessages(isUID bool, criteria *imap.SearchCriteria)
if isStringInList(m.LabelIDs, pmapi.StarredLabel) { if isStringInList(m.LabelIDs, pmapi.StarredLabel) {
messageFlagsMap[imap.FlaggedFlag] = true messageFlagsMap[imap.FlaggedFlag] = true
} }
if m.Unread == 0 { if !m.Unread {
messageFlagsMap[imap.SeenFlag] = true messageFlagsMap[imap.SeenFlag] = true
} }
if m.Has(pmapi.FlagReplied) || m.Has(pmapi.FlagRepliedAll) { if m.Has(pmapi.FlagReplied) || m.Has(pmapi.FlagRepliedAll) {
@ -518,25 +525,13 @@ func (im *imapMailbox) listMessages(isUID bool, seqSet *imap.SeqSet, items []ima
return err return err
} }
// From RFC: UID range of 559:* always includes the UID of the last message
// in the mailbox, even if 559 is higher than any assigned UID value.
// See: https://tools.ietf.org/html/rfc3501#page-61
if isUID && seqSet.Dynamic() && len(apiIDs) == 0 {
l.Debug("Requesting empty UID dynamic fetch, adding latest message")
apiID, err := im.storeMailbox.GetLatestAPIID()
if err != nil {
return nil
}
apiIDs = []string{apiID}
}
input := make([]interface{}, len(apiIDs)) input := make([]interface{}, len(apiIDs))
for i, apiID := range apiIDs { for i, apiID := range apiIDs {
input[i] = apiID input[i] = apiID
} }
processCallback := func(value interface{}) (interface{}, error) { processCallback := func(value interface{}) (interface{}, error) {
apiID := value.(string) apiID := value.(string) //nolint[forcetypeassert] we want to panic here
storeMessage, err := im.storeMailbox.GetMessage(apiID) storeMessage, err := im.storeMailbox.GetMessage(apiID)
if err != nil { if err != nil {
@ -552,7 +547,7 @@ func (im *imapMailbox) listMessages(isUID bool, seqSet *imap.SeqSet, items []ima
return nil, err return nil, err
} }
if storeMessage.Message().Unread == 1 { if storeMessage.Message().Unread {
for section := range msg.Body { for section := range msg.Body {
// Peek means get messages without marking them as read. // Peek means get messages without marking them as read.
// If client does not only ask for peek, we have to mark them as read. // If client does not only ask for peek, we have to mark them as read.
@ -570,12 +565,12 @@ func (im *imapMailbox) listMessages(isUID bool, seqSet *imap.SeqSet, items []ima
} }
collectCallback := func(idx int, value interface{}) error { collectCallback := func(idx int, value interface{}) error {
msg := value.(*imap.Message) msg := value.(*imap.Message) //nolint[forcetypeassert] we want to panic here
msgResponse <- msg msgResponse <- msg
return nil return nil
} }
err = parallel.RunParallel(fetchMessagesWorkers, input, processCallback, collectCallback) err = parallel.RunParallel(fetchWorkers, input, processCallback, collectCallback)
if err != nil { if err != nil {
return err return err
} }

View File

@ -32,7 +32,7 @@ import (
// - 100 messages were downloaded first time // - 100 messages were downloaded first time
// - 100 messages were downloaded second time // - 100 messages were downloaded second time
// - 99 messages were downloaded 10th times // - 99 messages were downloaded 10th times
// - 1 messages were downloaded 100th times // - 1 messages were downloaded 100th times.
type msgBuildCountHistogram struct { type msgBuildCountHistogram struct {
// Key represents how many times message was build. // Key represents how many times message was build.
// Value stores how many messages are build X times based on the key. // Value stores how many messages are build X times based on the key.

View File

@ -32,8 +32,8 @@ import (
"github.com/ProtonMail/proton-bridge/internal/events" "github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/imap/id" "github.com/ProtonMail/proton-bridge/internal/imap/id"
"github.com/ProtonMail/proton-bridge/internal/imap/uidplus" "github.com/ProtonMail/proton-bridge/internal/imap/uidplus"
"github.com/ProtonMail/proton-bridge/internal/serverutil"
"github.com/ProtonMail/proton-bridge/pkg/listener" "github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/ProtonMail/proton-bridge/pkg/ports"
"github.com/emersion/go-imap" "github.com/emersion/go-imap"
imapappendlimit "github.com/emersion/go-imap-appendlimit" imapappendlimit "github.com/emersion/go-imap-appendlimit"
imapidle "github.com/emersion/go-imap-idle" imapidle "github.com/emersion/go-imap-idle"
@ -116,60 +116,63 @@ func NewIMAPServer(panicHandler panicHandler, debugClient, debugServer bool, por
return server return server
} }
// Starts the server. func (s *imapServer) HandlePanic() { s.panicHandler.HandlePanic() }
func (s *imapServer) ListenAndServe() { func (s *imapServer) IsRunning() bool { return s.isRunning.Load().(bool) }
go s.monitorDisconnectedUsers() func (s *imapServer) Port() int { return s.port }
go s.monitorInternetConnection()
// When starting the Bridge, we don't want to retry to notify user // ListenAndServe starts the server and keeps it on based on internet
// quickly about the issue. Very probably retry will not help anyway. // availability.
s.listenAndServe(0) func (s *imapServer) ListenAndServe() {
serverutil.ListenAndServe(s, s.eventListener)
} }
func (s *imapServer) listenAndServe(retries int) { // ListenRetryAndServe will start listener. If port is occupied it will try
if s.isRunning.Load().(bool) { // again after coolDown time. Once listener is OK it will serve.
func (s *imapServer) ListenRetryAndServe(retries int, retryAfter time.Duration) {
if s.IsRunning() {
return return
} }
s.isRunning.Store(true) s.isRunning.Store(true)
log.Info("IMAP server listening at ", s.server.Addr) l := log.WithField("address", s.server.Addr)
l, err := net.Listen("tcp", s.server.Addr) l.Info("IMAP server is starting")
listener, err := net.Listen("tcp", s.server.Addr)
if err != nil { if err != nil {
s.isRunning.Store(false) s.isRunning.Store(false)
if retries > 0 { if retries > 0 {
log.WithError(err).WithField("retries", retries).Warn("IMAP listener failed") l.WithError(err).WithField("retries", retries).Warn("IMAP listener failed")
time.Sleep(15 * time.Second) time.Sleep(retryAfter)
s.listenAndServe(retries - 1) s.ListenRetryAndServe(retries-1, retryAfter)
return return
} }
log.WithError(err).Error("IMAP listener failed") l.WithError(err).Error("IMAP listener failed")
s.eventListener.Emit(events.ErrorEvent, "IMAP failed: "+err.Error()) s.eventListener.Emit(events.ErrorEvent, "IMAP failed: "+err.Error())
return return
} }
err = s.server.Serve(&connListener{ err = s.server.Serve(&connListener{
Listener: l, Listener: listener,
server: s, server: s,
userAgent: s.userAgent, userAgent: s.userAgent,
}) })
// Serve returns error every time, even after closing the server. // Serve returns error every time, even after closing the server.
// User shouldn't be notified about error if server shouldn't be running, // User shouldn't be notified about error if server shouldn't be running,
// but it should in case it was not closed by `s.Close()`. // but it should in case it was not closed by `s.Close()`.
if err != nil && s.isRunning.Load().(bool) { if err != nil && s.IsRunning() {
s.isRunning.Store(false) s.isRunning.Store(false)
log.WithError(err).Error("IMAP server failed") l.WithError(err).Error("IMAP server failed")
s.eventListener.Emit(events.ErrorEvent, "IMAP failed: "+err.Error()) s.eventListener.Emit(events.ErrorEvent, "IMAP failed: "+err.Error())
return return
} }
defer s.server.Close() //nolint[errcheck] defer s.server.Close() //nolint[errcheck]
log.Info("IMAP server stopped") l.Info("IMAP server stopped")
} }
// Stops the server. // Stops the server.
func (s *imapServer) Close() { func (s *imapServer) Close() {
if !s.isRunning.Load().(bool) { if !s.IsRunning() {
return return
} }
s.isRunning.Store(false) s.isRunning.Store(false)
@ -180,62 +183,16 @@ func (s *imapServer) Close() {
} }
} }
func (s *imapServer) monitorInternetConnection() { func (s *imapServer) DisconnectUser(address string) {
on := make(chan string) log.Info("Disconnecting all open IMAP connections for ", address)
s.eventListener.Add(events.InternetOnEvent, on) s.server.ForEachConn(func(conn imapserver.Conn) {
off := make(chan string) connUser := conn.Context().User
s.eventListener.Add(events.InternetOffEvent, off) if connUser != nil && strings.EqualFold(connUser.Username(), address) {
if err := conn.Close(); err != nil {
for { log.WithError(err).Error("Failed to close the connection")
var expectedIsPortFree bool
select {
case <-on:
go func() {
defer s.panicHandler.HandlePanic()
// We had issues on Mac that from time to time something
// blocked our port for a bit after we closed IMAP server
// due to connection issues.
// Restart always helped, so we do retry to not bother user.
s.listenAndServe(10)
}()
expectedIsPortFree = false
case <-off:
s.Close()
expectedIsPortFree = true
}
start := time.Now()
for {
if ports.IsPortFree(s.port) == expectedIsPortFree {
break
}
// Safety stop if something went wrong.
if time.Since(start) > 15*time.Second {
log.WithField("expectedIsPortFree", expectedIsPortFree).Warn("Server start/stop check timeouted")
break
}
time.Sleep(100 * time.Millisecond)
}
}
}
func (s *imapServer) monitorDisconnectedUsers() {
ch := make(chan string)
s.eventListener.Add(events.CloseConnectionEvent, ch)
for address := range ch {
address := address
log.Info("Disconnecting all open IMAP connections for ", address)
disconnectUser := func(conn imapserver.Conn) {
connUser := conn.Context().User
if connUser != nil && strings.EqualFold(connUser.Username(), address) {
if err := conn.Close(); err != nil {
log.WithError(err).Error("Failed to close the connection")
}
} }
} }
s.server.ForEachConn(disconnectUser) })
}
} }
// connListener sets debug loggers on server containing fields with local // connListener sets debug loggers on server containing fields with local

View File

@ -20,48 +20,33 @@ package imap
import ( import (
"fmt" "fmt"
"testing" "testing"
"time"
"github.com/ProtonMail/proton-bridge/internal/bridge" "github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/config/useragent" "github.com/ProtonMail/proton-bridge/internal/config/useragent"
"github.com/ProtonMail/proton-bridge/internal/events" "github.com/ProtonMail/proton-bridge/internal/serverutil/mocks"
"github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/ProtonMail/proton-bridge/pkg/ports"
imapserver "github.com/emersion/go-imap/server" imapserver "github.com/emersion/go-imap/server"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
type testPanicHandler struct{}
func (ph *testPanicHandler) HandlePanic() {}
func TestIMAPServerTurnOffAndOnAgain(t *testing.T) { func TestIMAPServerTurnOffAndOnAgain(t *testing.T) {
panicHandler := &testPanicHandler{} r := require.New(t)
ts := mocks.NewTestServer(12345)
eventListener := listener.New()
port := ports.FindFreePortFrom(12345)
server := imapserver.New(nil) server := imapserver.New(nil)
server.Addr = fmt.Sprintf("%v:%v", bridge.Host, port) server.Addr = fmt.Sprintf("%v:%v", bridge.Host, ts.WantPort)
s := &imapServer{ s := &imapServer{
panicHandler: panicHandler, panicHandler: ts.PanicHandler,
server: server, server: server,
eventListener: eventListener, port: ts.WantPort,
eventListener: ts.EventListener,
userAgent: useragent.New(), userAgent: useragent.New(),
} }
s.isRunning.Store(false) s.isRunning.Store(false)
r.True(ts.IsPortFree())
go s.ListenAndServe() go s.ListenAndServe()
time.Sleep(5 * time.Second) ts.RunServerTests(r)
require.False(t, ports.IsPortFree(port))
eventListener.Emit(events.InternetOffEvent, "")
time.Sleep(10 * time.Second)
require.True(t, ports.IsPortFree(port))
eventListener.Emit(events.InternetOnEvent, "")
time.Sleep(10 * time.Second)
require.False(t, ports.IsPortFree(port))
} }

View File

@ -20,6 +20,7 @@ package imap
import ( import (
"io" "io"
"net/mail" "net/mail"
"net/textproto"
"github.com/ProtonMail/gopenpgp/v2/crypto" "github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/ProtonMail/proton-bridge/internal/imap/uidplus" "github.com/ProtonMail/proton-bridge/internal/imap/uidplus"
@ -100,7 +101,10 @@ type storeMessageProvider interface {
IsMarkedDeleted() bool IsMarkedDeleted() bool
SetSize(int64) error SetSize(int64) error
SetContentTypeAndHeader(string, mail.Header) error SetHeader([]byte) error
GetHeader() []byte
GetMIMEHeader() textproto.MIMEHeader
IsFullHeaderCached() bool
SetBodyStructure(*pkgMsg.BodyStructure) error SetBodyStructure(*pkgMsg.BodyStructure) error
GetBodyStructure() (*pkgMsg.BodyStructure, error) GetBodyStructure() (*pkgMsg.BodyStructure, error)
IncreaseBuildCount() (uint32, error) IncreaseBuildCount() (uint32, error)
@ -123,7 +127,7 @@ func (s *storeUserWrap) GetAddress(addressID string) (storeAddressProvider, erro
if err != nil { if err != nil {
return nil, err return nil, err
} }
return newStoreAddressWrap(address), nil return newStoreAddressWrap(address), nil //nolint[typecheck] missing methods are inherited
} }
type storeAddressWrap struct { type storeAddressWrap struct {
@ -137,7 +141,7 @@ func newStoreAddressWrap(address *store.Address) *storeAddressWrap {
func (s *storeAddressWrap) ListMailboxes() []storeMailboxProvider { func (s *storeAddressWrap) ListMailboxes() []storeMailboxProvider {
mailboxes := []storeMailboxProvider{} mailboxes := []storeMailboxProvider{}
for _, mailbox := range s.Address.ListMailboxes() { for _, mailbox := range s.Address.ListMailboxes() {
mailboxes = append(mailboxes, newStoreMailboxWrap(mailbox)) mailboxes = append(mailboxes, newStoreMailboxWrap(mailbox)) //nolint[typecheck] missing methods are inherited
} }
return mailboxes return mailboxes
} }
@ -147,7 +151,7 @@ func (s *storeAddressWrap) GetMailbox(name string) (storeMailboxProvider, error)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return newStoreMailboxWrap(mailbox), nil return newStoreMailboxWrap(mailbox), nil //nolint[typecheck] missing methods are inherited
} }
type storeMailboxWrap struct { type storeMailboxWrap struct {

View File

@ -33,7 +33,7 @@ import (
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
// Capability extension identifier // Capability extension identifier.
const Capability = "UIDPLUS" const Capability = "UIDPLUS"
const ( const (
@ -228,7 +228,9 @@ func getStatusResponseCopy(uidValidity uint32, sourceSeq, targetSeq *OrderedSeq)
// CopyResponse prepares OK response with extended UID information about copied message. // CopyResponse prepares OK response with extended UID information about copied message.
func CopyResponse(uidValidity uint32, sourceSeq, targetSeq *OrderedSeq) error { func CopyResponse(uidValidity uint32, sourceSeq, targetSeq *OrderedSeq) error {
return server.ErrStatusResp(getStatusResponseCopy(uidValidity, sourceSeq, targetSeq)) return &imap.ErrStatusResp{
Resp: getStatusResponseCopy(uidValidity, sourceSeq, targetSeq),
}
} }
func getStatusResponseAppend(uidValidity uint32, targetSeq *OrderedSeq) *imap.StatusResp { func getStatusResponseAppend(uidValidity uint32, targetSeq *OrderedSeq) *imap.StatusResp {
@ -250,5 +252,7 @@ func getStatusResponseAppend(uidValidity uint32, targetSeq *OrderedSeq) *imap.St
// AppendResponse prepares OK response with extended UID information about appended message. // AppendResponse prepares OK response with extended UID information about appended message.
func AppendResponse(uidValidity uint32, targetSeq *OrderedSeq) error { func AppendResponse(uidValidity uint32, targetSeq *OrderedSeq) error {
return server.ErrStatusResp(getStatusResponseAppend(uidValidity, targetSeq)) return &imap.ErrStatusResp{
Resp: getStatusResponseAppend(uidValidity, targetSeq),
}
} }

View File

@ -93,7 +93,7 @@ func newIMAPUser(
// This method should eventually no longer be necessary. Everything should go via store. // This method should eventually no longer be necessary. Everything should go via store.
func (iu *imapUser) client() pmapi.Client { func (iu *imapUser) client() pmapi.Client {
return iu.user.GetTemporaryPMAPIClient() return iu.user.GetClient()
} }
func (iu *imapUser) isSubscribed(labelID string) bool { func (iu *imapUser) isSubscribed(labelID string) bool {
@ -135,7 +135,7 @@ func (iu *imapUser) ListMailboxes(showOnlySubcribed bool) ([]goIMAPBackend.Mailb
if showOnlySubcribed && !iu.isSubscribed(storeMailbox.LabelID()) { if showOnlySubcribed && !iu.isSubscribed(storeMailbox.LabelID()) {
continue continue
} }
mailbox := newIMAPMailbox(iu.panicHandler, iu, storeMailbox) mailbox := newIMAPMailbox(iu.panicHandler, iu, storeMailbox, iu.backend.builder)
mailboxes = append(mailboxes, mailbox) mailboxes = append(mailboxes, mailbox)
} }
@ -167,7 +167,7 @@ func (iu *imapUser) GetMailbox(name string) (mb goIMAPBackend.Mailbox, err error
return return
} }
return newIMAPMailbox(iu.panicHandler, iu, storeMailbox), nil return newIMAPMailbox(iu.panicHandler, iu, storeMailbox, iu.backend.builder), nil
} }
// CreateMailbox creates a new mailbox. // CreateMailbox creates a new mailbox.

View File

@ -20,7 +20,9 @@ package importexport
import ( import (
"bytes" "bytes"
"context"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/transfer" "github.com/ProtonMail/proton-bridge/internal/transfer"
"github.com/ProtonMail/proton-bridge/internal/users" "github.com/ProtonMail/proton-bridge/internal/users"
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
@ -39,7 +41,8 @@ type ImportExport struct {
locations Locator locations Locator
cache Cacher cache Cacher
panicHandler users.PanicHandler panicHandler users.PanicHandler
clientManager users.ClientManager eventListener listener.Listener
clientManager pmapi.Manager
} }
func New( func New(
@ -47,7 +50,7 @@ func New(
cache Cacher, cache Cacher,
panicHandler users.PanicHandler, panicHandler users.PanicHandler,
eventListener listener.Listener, eventListener listener.Listener,
clientManager users.ClientManager, clientManager pmapi.Manager,
credStorer users.CredentialsStorer, credStorer users.CredentialsStorer,
) *ImportExport { ) *ImportExport {
u := users.New(locations, panicHandler, eventListener, clientManager, credStorer, &storeFactory{}, false) u := users.New(locations, panicHandler, eventListener, clientManager, credStorer, &storeFactory{}, false)
@ -58,63 +61,38 @@ func New(
locations: locations, locations: locations,
cache: cache, cache: cache,
panicHandler: panicHandler, panicHandler: panicHandler,
eventListener: eventListener,
clientManager: clientManager, clientManager: clientManager,
} }
} }
// ReportBug reports a new bug from the user. // ReportBug reports a new bug from the user.
func (ie *ImportExport) ReportBug(osType, osVersion, description, accountName, address, emailClient string) error { func (ie *ImportExport) ReportBug(osType, osVersion, description, accountName, address, emailClient string) error {
c := ie.clientManager.GetAnonymousClient() return ie.clientManager.ReportBug(context.Background(), pmapi.ReportBugReq{
defer c.Logout()
title := "[Import-Export] Bug"
report := pmapi.ReportReq{
OS: osType, OS: osType,
OSVersion: osVersion, OSVersion: osVersion,
Browser: emailClient, Browser: emailClient,
Title: title, Title: "[Import-Export] Bug",
Description: description, Description: description,
Username: accountName, Username: accountName,
Email: address, Email: address,
} })
if err := c.Report(report); err != nil {
log.Error("Reporting bug failed: ", err)
return err
}
log.Info("Bug successfully reported")
return nil
} }
// ReportFile submits import report file // ReportFile submits import report file.
func (ie *ImportExport) ReportFile(osType, osVersion, accountName, address string, logdata []byte) error { func (ie *ImportExport) ReportFile(osType, osVersion, accountName, address string, logdata []byte) error {
c := ie.clientManager.GetAnonymousClient() report := pmapi.ReportBugReq{
defer c.Logout()
title := "[Import-Export] report file"
description := "An Import-Export report from the user swam down the river."
report := pmapi.ReportReq{
OS: osType, OS: osType,
OSVersion: osVersion, OSVersion: osVersion,
Description: description, Description: "An Import-Export report from the user swam down the river.",
Title: title, Title: "[Import-Export] report file",
Username: accountName, Username: accountName,
Email: address, Email: address,
} }
report.AddAttachment("log", "report.log", bytes.NewReader(logdata)) report.AddAttachment("log", "report.log", bytes.NewReader(logdata))
if err := c.Report(report); err != nil { return ie.clientManager.ReportBug(context.Background(), report)
log.Error("Sending report failed: ", err)
return err
}
log.Info("Report successfully sent")
return nil
} }
// GetLocalImporter returns transferrer from local EML or MBOX structure to ProtonMail account. // GetLocalImporter returns transferrer from local EML or MBOX structure to ProtonMail account.
@ -187,5 +165,23 @@ func (ie *ImportExport) getPMAPIProvider(username, address string) (*transfer.PM
log.WithError(err).Info("Address does not exist, using all addresses") log.WithError(err).Info("Address does not exist, using all addresses")
} }
return transfer.NewPMAPIProvider(ie.clientManager, user.ID(), addressID) provider, err := transfer.NewPMAPIProvider(user.GetClient(), user.ID(), addressID)
if err != nil {
return nil, err
}
go func() {
internetOffCh := ie.eventListener.ProvideChannel(events.InternetOffEvent)
internetOnCh := ie.eventListener.ProvideChannel(events.InternetOnEvent)
for {
select {
case <-internetOffCh:
provider.SetConnectionDown()
case <-internetOnCh:
provider.SetConnectionUp()
}
}
}()
return provider, nil
} }

View File

@ -34,7 +34,7 @@ import (
// - logs: ~/.cache/protonmail/<app>/logs // - logs: ~/.cache/protonmail/<app>/logs
// - cache: ~/.config/protonmail/<app>/cache // - cache: ~/.config/protonmail/<app>/cache
// - updates: ~/.config/protonmail/<app>/updates // - updates: ~/.config/protonmail/<app>/updates
// - lockfile: ~/.cache/protonmail/<app>/<app>.lock // - lockfile: ~/.cache/protonmail/<app>/<app>.lock .
type Locations struct { type Locations struct {
userConfig, userCache string userConfig, userCache string
configName string configName string

View File

@ -34,7 +34,7 @@ func DumpStackTrace(logsPath string) crash.RecoveryAction {
return func(r interface{}) error { return func(r interface{}) error {
file := filepath.Join(logsPath, getStackTraceName(constants.Version, constants.Revision)) file := filepath.Join(logsPath, getStackTraceName(constants.Version, constants.Revision))
f, err := os.OpenFile(file, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0600) f, err := os.OpenFile(filepath.Clean(file), os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0600)
if err != nil { if err != nil {
return err return err
} }

View File

@ -42,6 +42,7 @@ const (
func Init(logsPath string) error { func Init(logsPath string) error {
logrus.SetFormatter(&logrus.TextFormatter{ logrus.SetFormatter(&logrus.TextFormatter{
ForceColors: true,
FullTimestamp: true, FullTimestamp: true,
TimestampFormat: time.StampMilli, TimestampFormat: time.StampMilli,
}) })
@ -69,6 +70,10 @@ func Init(logsPath string) error {
return nil return nil
} }
// SetLevel will change the level of logging and in case of Debug or Trace
// level it will also prevent from writing to file. Setting level to Info or
// higher will not set writing to file again if it was previously cancelled by
// Debug or Trace.
func SetLevel(level string) { func SetLevel(level string) {
if lvl, err := logrus.ParseLevel(level); err == nil { if lvl, err := logrus.ParseLevel(level); err == nil {
logrus.SetLevel(lvl) logrus.SetLevel(lvl)

View File

@ -0,0 +1,150 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail 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.
//
// ProtonMail 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 ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package mocks
import (
"fmt"
"net/http"
"sync/atomic"
"time"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/ProtonMail/proton-bridge/pkg/ports"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/require"
)
type DummyPanicHandler struct{}
func (ph *DummyPanicHandler) HandlePanic() {}
type TestServer struct {
PanicHandler *DummyPanicHandler
WantPort int
EventListener listener.Listener
isRunning atomic.Value
srv *http.Server
}
func NewTestServer(port int) *TestServer {
s := &TestServer{
PanicHandler: &DummyPanicHandler{},
EventListener: listener.New(),
WantPort: ports.FindFreePortFrom(port),
}
s.isRunning.Store(false)
return s
}
func (s *TestServer) IsPortFree() bool {
return true
}
func (s *TestServer) IsPortOccupied() bool {
return true
}
func (s *TestServer) Emit(event string, try, iEvt int) int {
// Emit has separate go routine so it is needed to wait here to
// prevent event race condition.
time.Sleep(100 * time.Millisecond)
iEvt++
s.EventListener.Emit(event, fmt.Sprintf("%d:%d", try, iEvt))
return iEvt
}
func (s *TestServer) HandlePanic() {}
func (s *TestServer) DisconnectUser(string) {}
func (s *TestServer) Port() int { return s.WantPort }
func (s *TestServer) IsRunning() bool { return s.isRunning.Load().(bool) }
func (s *TestServer) ListenRetryAndServe(retries int, retryAfter time.Duration) {
if s.isRunning.Load().(bool) {
return
}
s.isRunning.Store(true)
// There can be delay when starting server
time.Sleep(200 * time.Millisecond)
s.srv = &http.Server{
Addr: fmt.Sprintf("127.0.0.1:%d", s.WantPort),
}
err := s.srv.ListenAndServe()
if err != nil {
s.isRunning.Store(false)
if retries > 0 {
time.Sleep(retryAfter)
s.ListenRetryAndServe(retries-1, retryAfter)
}
}
if s.IsRunning() {
logrus.Error("Not serving but isRunning is true")
s.isRunning.Store(false)
}
}
func (s *TestServer) Close() {
if !s.isRunning.Load().(bool) {
return
}
s.isRunning.Store(false)
// There can be delay when stopping server
time.Sleep(200 * time.Millisecond)
if err := s.srv.Close(); err != nil {
logrus.WithError(err).Error("Closing dummy server")
}
}
func (s *TestServer) RunServerTests(r *require.Assertions) {
// NOTE About choosing tick durations:
// In order to avoid ticks to synchronise and cause occasional race
// condition we choose the tick duration around 100ms but not exactly
// to have large common multiple.
r.Eventually(s.IsPortOccupied, 5*time.Second, 97*time.Millisecond)
// There was an issue where second time we were not able to restore server.
for try := 0; try < 3; try++ {
i := s.Emit(events.InternetOffEvent, try, 0)
r.Eventually(s.IsPortFree, 10*time.Second, 99*time.Millisecond, "signal off try %d : %d", try, i)
i = s.Emit(events.InternetOnEvent, try, i)
i = s.Emit(events.InternetOffEvent, try, i)
i = s.Emit(events.InternetOffEvent, try, i)
i = s.Emit(events.InternetOffEvent, try, i)
i = s.Emit(events.InternetOffEvent, try, i)
i = s.Emit(events.InternetOnEvent, try, i)
i = s.Emit(events.InternetOnEvent, try, i)
i = s.Emit(events.InternetOffEvent, try, i)
// Wait a bit longer if needed to process all events
r.Eventually(s.IsPortFree, 20*time.Second, 101*time.Millisecond, "again signal off number %d : %d", try, i)
i = s.Emit(events.InternetOnEvent, try, i)
r.Eventually(s.IsPortOccupied, 10*time.Second, 103*time.Millisecond, "signal on number %d : %d", try, i)
i = s.Emit(events.InternetOffEvent, try, i)
i = s.Emit(events.InternetOnEvent, try, i)
i = s.Emit(events.InternetOnEvent, try, i)
r.Eventually(s.IsPortOccupied, 10*time.Second, 107*time.Millisecond, "again signal on number %d : %d", try, i)
}
}

View File

@ -0,0 +1,132 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail 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.
//
// ProtonMail 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 ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package serverutil
import (
"time"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/ProtonMail/proton-bridge/pkg/ports"
"github.com/sirupsen/logrus"
)
// Server which can handle disconnected users and lost internet connection.
type Server interface {
HandlePanic()
DisconnectUser(string)
ListenRetryAndServe(int, time.Duration)
Close()
Port() int
IsRunning() bool
}
func monitorDisconnectedUsers(s Server, l listener.Listener) {
ch := make(chan string)
l.Add(events.CloseConnectionEvent, ch)
for address := range ch {
s.DisconnectUser(address)
}
}
func redirectInternetEventsToOneChannel(l listener.Listener) (isInternetOn chan bool) {
on := make(chan string)
l.Add(events.InternetOnEvent, on)
off := make(chan string)
l.Add(events.InternetOffEvent, off)
// Redirect two channels into one. When select was used the algorithm
// first read all on channels and then read all off channels.
isInternetOn = make(chan bool, 20)
go func() {
for {
logrus.WithField("try", <-on).Trace("Internet ON")
isInternetOn <- true
}
}()
go func() {
for {
logrus.WithField("try", <-off).Trace("Internet OFF")
isInternetOn <- false
}
}()
return
}
const (
recheckPortAfter = 50 * time.Millisecond
stopPortChecksAfter = 15 * time.Second
retryListnerAfter = 5 * time.Second
)
func monitorInternetConnection(s Server, l listener.Listener) {
isInternetOn := redirectInternetEventsToOneChannel(l)
for {
var expectedIsPortFree bool
if <-isInternetOn {
if s.IsRunning() {
continue
}
go func() {
defer s.HandlePanic()
// We had issues on Mac that from time to time something
// blocked our port for a bit after we closed IMAP server
// due to connection issues.
// Restart always helped, so we do retry to not bother user.
s.ListenRetryAndServe(10, retryListnerAfter)
}()
expectedIsPortFree = false
} else {
if !s.IsRunning() {
continue
}
s.Close()
expectedIsPortFree = true
}
start := time.Now()
for {
isPortFree := ports.IsPortFree(s.Port())
logrus.
WithField("port", s.Port()).
WithField("isFree", isPortFree).
WithField("wantToBeFree", expectedIsPortFree).
Trace("Check port")
if isPortFree == expectedIsPortFree {
break
}
// Safety stop if something went wrong.
if time.Since(start) > stopPortChecksAfter {
logrus.WithField("expectedIsPortFree", expectedIsPortFree).Warn("Server start/stop check timeouted")
break
}
time.Sleep(recheckPortAfter)
}
}
}
// ListenAndServe starts the server and keeps it on based on internet
// availability. It also monitors and disconnect users if requested.
func ListenAndServe(s Server, l listener.Listener) {
go monitorDisconnectedUsers(s, l)
go monitorInternetConnection(s, l)
// When starting the Bridge, we don't want to retry to notify user
// quickly about the issue. Very probably retry will not help anyway.
s.ListenRetryAndServe(0, 0)
}

View File

@ -15,9 +15,21 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package users package serverutil
// IsAuthorized returns whether the user has received an Auth from the API yet. import (
func (u *User) IsAuthorized() bool { "testing"
return u.isAuthorized
"github.com/ProtonMail/proton-bridge/internal/serverutil/mocks"
"github.com/stretchr/testify/require"
)
func TestServerTurnOffAndOnAgain(t *testing.T) {
r := require.New(t)
s := mocks.NewTestServer(12321)
r.True(s.IsPortFree())
go ListenAndServe(s, s.EventListener)
s.RunServerTests(r)
} }

View File

@ -31,7 +31,7 @@ type bridgeUser interface {
CheckBridgeLogin(password string) error CheckBridgeLogin(password string) error
IsCombinedAddressMode() bool IsCombinedAddressMode() bool
GetAddressID(address string) (string, error) GetAddressID(address string) (string, error)
GetTemporaryPMAPIClient() pmapi.Client GetClient() pmapi.Client
GetStore() storeUserProvider GetStore() storeUserProvider
} }
@ -51,7 +51,7 @@ func (b *bridgeWrap) GetUser(query string) (bridgeUser, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
return newBridgeUserWrap(user), nil return newBridgeUserWrap(user), nil //nolint[typecheck] missing methods are inherited
} }
type bridgeUserWrap struct { type bridgeUserWrap struct {

View File

@ -173,7 +173,7 @@ func (b *sendPreferencesBuilder) withPublicKey(v *crypto.KeyRing) {
// | 16 (PGP/MIME), // | 16 (PGP/MIME),
// mimeType: 'text/html' | 'text/plain' | 'multipart/mixed', // mimeType: 'text/html' | 'text/plain' | 'multipart/mixed',
// publicKey: OpenPGPKey | undefined/null // publicKey: OpenPGPKey | undefined/null
// } // }.
func (b *sendPreferencesBuilder) build() (p SendPreferences) { func (b *sendPreferencesBuilder) build() (p SendPreferences) {
p.Encrypt = b.shouldEncrypt() p.Encrypt = b.shouldEncrypt()
p.Sign = b.shouldSign() p.Sign = b.shouldSign()
@ -492,6 +492,8 @@ func (b *sendPreferencesBuilder) setEncryptionPreferences(mailSettings pmapi.Mai
b.withSchemeDefault(pgpInline) b.withSchemeDefault(pgpInline)
case pmapi.PGPMIMEPackage: case pmapi.PGPMIMEPackage:
b.withSchemeDefault(pgpMIME) b.withSchemeDefault(pgpMIME)
case pmapi.ClearMIMEPackage, pmapi.ClearPackage, pmapi.EncryptedOutsidePackage, pmapi.InternalPackage:
// nothing to set
} }
// Its value is constrained by the sign flag and the PGP scheme: // Its value is constrained by the sign flag and the PGP scheme:

View File

@ -18,6 +18,7 @@
package smtp package smtp
import ( import (
"context"
"crypto/sha256" "crypto/sha256"
"fmt" "fmt"
"strings" "strings"
@ -28,7 +29,7 @@ import (
) )
type messageGetter interface { type messageGetter interface {
GetMessage(string) (*pmapi.Message, error) GetMessage(context.Context, string) (*pmapi.Message, error)
} }
type sendRecorderValue struct { type sendRecorderValue struct {
@ -126,7 +127,7 @@ func (q *sendRecorder) isSendingOrSent(client messageGetter, hash string) (isSen
return true, false return true, false
} }
message, err := client.GetMessage(value.messageID) message, err := client.GetMessage(context.TODO(), value.messageID)
// Message could be deleted or there could be an internet issue or whatever, // Message could be deleted or there could be an internet issue or whatever,
// so let's assume the message was not sent. // so let's assume the message was not sent.
if err != nil { if err != nil {

View File

@ -18,6 +18,7 @@
package smtp package smtp
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"net/mail" "net/mail"
@ -33,7 +34,7 @@ type testSendRecorderGetMessageMock struct {
err error err error
} }
func (m *testSendRecorderGetMessageMock) GetMessage(messageID string) (*pmapi.Message, error) { func (m *testSendRecorderGetMessageMock) GetMessage(_ context.Context, messageID string) (*pmapi.Message, error) {
return m.message, m.err return m.message, m.err
} }

View File

@ -20,30 +20,34 @@ package smtp
import ( import (
"crypto/tls" "crypto/tls"
"fmt" "fmt"
"net"
"sync/atomic"
"time"
"github.com/ProtonMail/proton-bridge/internal/bridge" "github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/events" "github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/serverutil"
"github.com/ProtonMail/proton-bridge/pkg/listener" "github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/emersion/go-sasl" "github.com/emersion/go-sasl"
goSMTP "github.com/emersion/go-smtp" goSMTP "github.com/emersion/go-smtp"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
type smtpServer struct { // Server is Bridge SMTP server implementation.
type Server struct {
panicHandler panicHandler
backend goSMTP.Backend
server *goSMTP.Server server *goSMTP.Server
eventListener listener.Listener eventListener listener.Listener
debug bool
useSSL bool useSSL bool
port int
tls *tls.Config
isRunning atomic.Value
} }
// NewSMTPServer returns an SMTP server configured with the given options. // NewSMTPServer returns an SMTP server configured with the given options.
func NewSMTPServer(debug bool, port int, useSSL bool, tls *tls.Config, smtpBackend goSMTP.Backend, eventListener listener.Listener) *smtpServer { //nolint[golint] func NewSMTPServer(panicHandler panicHandler, debug bool, port int, useSSL bool, tls *tls.Config, smtpBackend goSMTP.Backend, eventListener listener.Listener) *Server {
s := goSMTP.NewServer(smtpBackend)
s.Addr = fmt.Sprintf("%v:%v", bridge.Host, port)
s.TLSConfig = tls
s.Domain = bridge.Host
s.AllowInsecureAuth = true
s.MaxLineLength = 2 << 16
if debug { if debug {
fmt.Println("THE LOG WILL CONTAIN **DECRYPTED** MESSAGE DATA") fmt.Println("THE LOG WILL CONTAIN **DECRYPTED** MESSAGE DATA")
log.Warning("================================================") log.Warning("================================================")
@ -51,13 +55,38 @@ func NewSMTPServer(debug bool, port int, useSSL bool, tls *tls.Config, smtpBacke
log.Warning("================================================") log.Warning("================================================")
} }
server := &Server{
panicHandler: panicHandler,
backend: smtpBackend,
eventListener: eventListener,
debug: debug,
useSSL: useSSL,
port: port,
tls: tls,
}
server.isRunning.Store(false)
return server
}
func (s *Server) HandlePanic() { s.panicHandler.HandlePanic() }
func (s *Server) IsRunning() bool { return s.isRunning.Load().(bool) }
func (s *Server) Port() int { return s.port }
func newGoSMTPServer(debug bool, smtpBackend goSMTP.Backend, port int, tls *tls.Config) *goSMTP.Server {
newSMTP := goSMTP.NewServer(smtpBackend)
newSMTP.Addr = fmt.Sprintf("%v:%v", bridge.Host, port)
newSMTP.TLSConfig = tls
newSMTP.Domain = bridge.Host
newSMTP.AllowInsecureAuth = true
newSMTP.MaxLineLength = 1 << 16
if debug { if debug {
s.Debug = logrus. newSMTP.Debug = logrus.
WithField("pkg", "smtp/server"). WithField("pkg", "smtp/server").
WriterLevel(logrus.DebugLevel) WriterLevel(logrus.DebugLevel)
} }
s.EnableAuth(sasl.Login, func(conn *goSMTP.Conn) sasl.Server { newSMTP.EnableAuth(sasl.Login, func(conn *goSMTP.Conn) sasl.Server {
return sasl.NewLoginServer(func(address, password string) error { return sasl.NewLoginServer(func(address, password string) error {
user, err := conn.Server().Backend.Login(nil, address, password) user, err := conn.Server().Backend.Login(nil, address, password)
if err != nil { if err != nil {
@ -68,57 +97,92 @@ func NewSMTPServer(debug bool, port int, useSSL bool, tls *tls.Config, smtpBacke
return nil return nil
}) })
}) })
return newSMTP
return &smtpServer{
server: s,
eventListener: eventListener,
useSSL: useSSL,
}
} }
// Starts the server. // ListenAndServe starts the server and keeps it on based on internet
func (s *smtpServer) ListenAndServe() { // availability.
go s.monitorDisconnectedUsers() func (s *Server) ListenAndServe() {
l := log.WithField("useSSL", s.useSSL).WithField("address", s.server.Addr) serverutil.ListenAndServe(s, s.eventListener)
}
l.Info("SMTP server is starting") func (s *Server) ListenRetryAndServe(retries int, retryAfter time.Duration) {
var err error if s.IsRunning() {
if s.useSSL {
err = s.server.ListenAndServeTLS()
} else {
err = s.server.ListenAndServe()
}
if err != nil {
s.eventListener.Emit(events.ErrorEvent, "SMTP failed: "+err.Error())
l.Error("SMTP failed: ", err)
return return
} }
defer s.server.Close() //nolint[errcheck] s.isRunning.Store(true)
l.Info("SMTP server stopped") s.server = newGoSMTPServer(s.debug, s.backend, s.port, s.tls)
l := log.WithField("useSSL", s.useSSL).WithField("address", s.server.Addr)
l.Info("SMTP server is starting")
var listener net.Listener
var err error
if s.useSSL {
listener, err = tls.Listen("tcp", s.server.Addr, s.server.TLSConfig)
} else {
listener, err = net.Listen("tcp", s.server.Addr)
}
l.WithError(err).Debug("Listener for SMTP created")
if err != nil {
s.isRunning.Store(false)
if retries > 0 {
l.WithError(err).WithField("retries", retries).Warn("SMTP listener failed")
time.Sleep(retryAfter)
s.ListenRetryAndServe(retries-1, retryAfter)
return
}
l.WithError(err).Error("SMTP listener failed")
s.eventListener.Emit(events.ErrorEvent, "SMTP failed: "+err.Error())
return
}
err = s.server.Serve(listener)
l.WithError(err).Debug("GoSMTP not serving")
// Serve returns error every time, even after closing the server.
// User shouldn't be notified about error if server shouldn't be running,
// but it should in case it was not closed by `s.Close()`.
if err != nil && s.IsRunning() {
s.isRunning.Store(false)
l.WithError(err).Error("SMTP server failed")
s.eventListener.Emit(events.ErrorEvent, "SMTP failed: "+err.Error())
return
}
defer func() {
// Go SMTP server instance can be closed only once. Otherwise
// it returns an error. The error is not export therefore we
// will check the string value.
err := s.server.Close()
if err == nil || err.Error() != "smtp: server already closed" {
l.WithError(err).Warn("Server was not closed")
}
}()
l.Info("SMTP server closed")
} }
// Stops the server. // Close stops the server.
func (s *smtpServer) Close() { func (s *Server) Close() {
if !s.IsRunning() {
return
}
s.isRunning.Store(false)
if err := s.server.Close(); err != nil { if err := s.server.Close(); err != nil {
log.WithError(err).Error("Failed to close the connection") log.WithError(err).Error("Cannot close the server")
} }
} }
func (s *smtpServer) monitorDisconnectedUsers() { func (s *Server) DisconnectUser(address string) {
ch := make(chan string) log.Info("Disconnecting all open SMTP connections for ", address)
s.eventListener.Add(events.CloseConnectionEvent, ch) s.server.ForEachConn(func(conn *goSMTP.Conn) {
connUser := conn.Session()
for address := range ch { if connUser != nil {
log.Info("Disconnecting all open SMTP connections for ", address) if err := conn.Close(); err != nil {
disconnectUser := func(conn *goSMTP.Conn) { log.WithError(err).Error("Failed to close the connection")
connUser := conn.Session()
if connUser != nil {
if err := conn.Close(); err != nil {
log.WithError(err).Error("Failed to close the connection")
}
} }
} }
s.server.ForEachConn(disconnectUser) })
}
} }

View File

@ -15,29 +15,29 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package pmapi package smtp
import ( import (
"net/url" "testing"
"github.com/ProtonMail/proton-bridge/internal/serverutil/mocks"
"github.com/stretchr/testify/require"
) )
// SendSimpleMetric makes a simple GET request to send a simple metrics report. func TestSMTPServerTurnOffAndOnAgain(t *testing.T) {
func (c *client) SendSimpleMetric(category, action, label string) (err error) { r := require.New(t)
v := url.Values{} ts := mocks.NewTestServer(12342)
v.Set("Category", category)
v.Set("Action", action)
v.Set("Label", label)
req, err := c.NewRequest("GET", "/metrics?"+v.Encode(), nil) s := &Server{
if err != nil { panicHandler: ts.PanicHandler,
return port: ts.WantPort,
eventListener: ts.EventListener,
} }
s.isRunning.Store(false)
var res Res r.True(ts.IsPortFree())
if err = c.DoJSON(req, &res); err != nil {
return
}
err = res.Err() go s.ListenAndServe()
return ts.RunServerTests(r)
} }

View File

@ -21,10 +21,10 @@ package smtp
import ( import (
"bytes" "bytes"
"context"
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"mime" "mime"
"net/mail" "net/mail"
"strings" "strings"
@ -81,7 +81,7 @@ func newSMTPUser(
// This method should eventually no longer be necessary. Everything should go via store. // This method should eventually no longer be necessary. Everything should go via store.
func (su *smtpUser) client() pmapi.Client { func (su *smtpUser) client() pmapi.Client {
return su.user.GetTemporaryPMAPIClient() return su.user.GetClient()
} }
// Send sends an email from the given address to the given addresses with the given body. // Send sends an email from the given address to the given addresses with the given body.
@ -123,7 +123,7 @@ func (su *smtpUser) getSendPreferences(
} }
func (su *smtpUser) getContactVCardData(recipient string) (meta *ContactMetadata, err error) { func (su *smtpUser) getContactVCardData(recipient string) (meta *ContactMetadata, err error) {
emails, err := su.client().GetContactEmailByEmail(recipient, 0, 1000) emails, err := su.client().GetContactEmailByEmail(context.TODO(), recipient, 0, 1000)
if err != nil { if err != nil {
return return
} }
@ -135,7 +135,7 @@ func (su *smtpUser) getContactVCardData(recipient string) (meta *ContactMetadata
} }
var contact pmapi.Contact var contact pmapi.Contact
if contact, err = su.client().GetContactByID(email.ContactID); err != nil { if contact, err = su.client().GetContactByID(context.TODO(), email.ContactID); err != nil {
return return
} }
@ -151,7 +151,7 @@ func (su *smtpUser) getContactVCardData(recipient string) (meta *ContactMetadata
} }
func (su *smtpUser) getAPIKeyData(recipient string) (apiKeys []pmapi.PublicKey, isInternal bool, err error) { func (su *smtpUser) getAPIKeyData(recipient string) (apiKeys []pmapi.PublicKey, isInternal bool, err error) {
return su.client().GetPublicKeysForEmail(recipient) return su.client().GetPublicKeysForEmail(context.TODO(), recipient)
} }
// Discard currently processed message. // Discard currently processed message.
@ -219,7 +219,7 @@ func (su *smtpUser) Send(returnPath string, to []string, messageReader io.Reader
messageReader = io.TeeReader(messageReader, b) messageReader = io.TeeReader(messageReader, b)
mailSettings, err := su.client().GetMailSettings() mailSettings, err := su.client().GetMailSettings(context.TODO())
if err != nil { if err != nil {
return err return err
} }
@ -325,12 +325,6 @@ func (su *smtpUser) Send(returnPath string, to []string, messageReader io.Reader
return nil return nil
} }
if ok, err := su.isTotalSizeOkay(message, attReaders); err != nil {
return err
} else if !ok {
return errors.New("message is too large")
}
su.backend.sendRecorder.addMessage(sendRecorderMessageHash) su.backend.sendRecorder.addMessage(sendRecorderMessageHash)
message, atts, err := su.storeUser.CreateDraft(kr, message, attReaders, attachedPublicKey, attachedPublicKeyName, parentID) message, atts, err := su.storeUser.CreateDraft(kr, message, attReaders, attachedPublicKey, attachedPublicKeyName, parentID)
if err != nil { if err != nil {
@ -346,7 +340,7 @@ func (su *smtpUser) Send(returnPath string, to []string, messageReader io.Reader
// can lead to sending the wrong message. Also clients do not necessarily // can lead to sending the wrong message. Also clients do not necessarily
// delete the old draft. // delete the old draft.
if draftID != "" { if draftID != "" {
if err := su.client().DeleteMessages([]string{draftID}); err != nil { if err := su.client().DeleteMessages(context.TODO(), []string{draftID}); err != nil {
log.WithError(err).WithField("draftID", draftID).Warn("Original draft cannot be deleted") log.WithError(err).WithField("draftID", draftID).Warn("Original draft cannot be deleted")
} }
} }
@ -400,7 +394,7 @@ func (su *smtpUser) Send(returnPath string, to []string, messageReader io.Reader
return errors.New("error decoding subject message " + message.Header.Get("Subject")) return errors.New("error decoding subject message " + message.Header.Get("Subject"))
} }
if !su.continueSendingUnencryptedMail(subject) { if !su.continueSendingUnencryptedMail(subject) {
if err := su.client().DeleteMessages([]string{message.ID}); err != nil { if err := su.client().DeleteMessages(context.TODO(), []string{message.ID}); err != nil {
log.WithError(err).Warn("Failed to delete canceled messages") log.WithError(err).Warn("Failed to delete canceled messages")
} }
return errors.New("sending was canceled by user") return errors.New("sending was canceled by user")
@ -429,7 +423,7 @@ func (su *smtpUser) handleReferencesHeader(m *pmapi.Message) (draftID, parentID
if su.addressID != "" { if su.addressID != "" {
filter.AddressID = su.addressID filter.AddressID = su.addressID
} }
metadata, _, _ := su.client().ListMessages(filter) metadata, _, _ := su.client().ListMessages(context.TODO(), filter)
for _, m := range metadata { for _, m := range metadata {
if m.IsDraft() { if m.IsDraft() {
draftID = m.ID draftID = m.ID
@ -449,7 +443,7 @@ func (su *smtpUser) handleReferencesHeader(m *pmapi.Message) (draftID, parentID
if su.addressID != "" { if su.addressID != "" {
filter.AddressID = su.addressID filter.AddressID = su.addressID
} }
metadata, _, _ := su.client().ListMessages(filter) metadata, _, _ := su.client().ListMessages(context.TODO(), filter)
// There can be two or messages with the same external ID and then we cannot // There can be two or messages with the same external ID and then we cannot
// be sure which message should be parent. Better to not choose any. // be sure which message should be parent. Better to not choose any.
if len(metadata) == 1 { if len(metadata) == 1 {
@ -541,24 +535,3 @@ func (su *smtpUser) Logout() error {
log.Debug("SMTP client logged out user ", su.addressID) log.Debug("SMTP client logged out user ", su.addressID)
return nil return nil
} }
func (su *smtpUser) isTotalSizeOkay(message *pmapi.Message, attReaders []io.Reader) (bool, error) {
maxUpload, err := su.storeUser.GetMaxUpload()
if err != nil {
return false, err
}
var attSize int64
for i := range attReaders {
b, err := ioutil.ReadAll(attReaders[i])
if err != nil {
return false, err
}
attSize += int64(len(b))
attReaders[i] = bytes.NewBuffer(b)
}
return message.Size+attSize <= maxUpload, nil
}

View File

@ -90,7 +90,7 @@ func getLabelPrefix(l *pmapi.Label) string {
switch { switch {
case pmapi.IsSystemLabel(l.ID): case pmapi.IsSystemLabel(l.ID):
return "" return ""
case l.Exclusive == 1: case bool(l.Exclusive):
return UserFoldersPrefix return UserFoldersPrefix
default: default:
return UserLabelsPrefix return UserLabelsPrefix

View File

@ -37,8 +37,8 @@ func TestNotifyChangeCreateOrUpdateMessage(t *testing.T) {
m.newStoreNoEvents(true) m.newStoreNoEvents(true)
m.store.SetChangeNotifier(m.changeNotifier) m.store.SetChangeNotifier(m.changeNotifier)
insertMessage(t, m, "msg1", "Test message 1", addrID1, 0, []string{pmapi.AllMailLabel}) insertMessage(t, m, "msg1", "Test message 1", addrID1, false, []string{pmapi.AllMailLabel})
insertMessage(t, m, "msg2", "Test message 2", addrID1, 0, []string{pmapi.AllMailLabel}) insertMessage(t, m, "msg2", "Test message 2", addrID1, false, []string{pmapi.AllMailLabel})
} }
func TestNotifyChangeCreateOrUpdateMessages(t *testing.T) { func TestNotifyChangeCreateOrUpdateMessages(t *testing.T) {
@ -52,8 +52,8 @@ func TestNotifyChangeCreateOrUpdateMessages(t *testing.T) {
m.newStoreNoEvents(true) m.newStoreNoEvents(true)
m.store.SetChangeNotifier(m.changeNotifier) m.store.SetChangeNotifier(m.changeNotifier)
msg1 := getTestMessage("msg1", "Test message 1", addrID1, 0, []string{pmapi.AllMailLabel}) msg1 := getTestMessage("msg1", "Test message 1", addrID1, false, []string{pmapi.AllMailLabel})
msg2 := getTestMessage("msg2", "Test message 2", addrID1, 0, []string{pmapi.AllMailLabel}) msg2 := getTestMessage("msg2", "Test message 2", addrID1, false, []string{pmapi.AllMailLabel})
require.Nil(t, m.store.createOrUpdateMessagesEvent([]*pmapi.Message{msg1, msg2})) require.Nil(t, m.store.createOrUpdateMessagesEvent([]*pmapi.Message{msg1, msg2}))
} }
@ -63,8 +63,8 @@ func TestNotifyChangeDeleteMessage(t *testing.T) {
m.newStoreNoEvents(true) m.newStoreNoEvents(true)
insertMessage(t, m, "msg1", "Test message 1", addrID1, 0, []string{pmapi.AllMailLabel}) insertMessage(t, m, "msg1", "Test message 1", addrID1, false, []string{pmapi.AllMailLabel})
insertMessage(t, m, "msg2", "Test message 2", addrID1, 0, []string{pmapi.AllMailLabel}) insertMessage(t, m, "msg2", "Test message 2", addrID1, false, []string{pmapi.AllMailLabel})
m.changeNotifier.EXPECT().DeleteMessage(addr1, "All Mail", uint32(2)) m.changeNotifier.EXPECT().DeleteMessage(addr1, "All Mail", uint32(2))
m.changeNotifier.EXPECT().DeleteMessage(addr1, "All Mail", uint32(1)) m.changeNotifier.EXPECT().DeleteMessage(addr1, "All Mail", uint32(1))

View File

@ -18,6 +18,7 @@
package store package store
import ( import (
"context"
"math/rand" "math/rand"
"time" "time"
@ -80,7 +81,7 @@ func (loop *eventLoop) client() pmapi.Client {
func (loop *eventLoop) setFirstEventID() (err error) { func (loop *eventLoop) setFirstEventID() (err error) {
loop.log.Info("Setting first event ID") loop.log.Info("Setting first event ID")
event, err := loop.client().GetEvent("") event, err := loop.client().GetEvent(context.Background(), "")
if err != nil { if err != nil {
loop.log.WithError(err).Error("Could not get latest event ID") loop.log.WithError(err).Error("Could not get latest event ID")
return return
@ -99,6 +100,11 @@ func (loop *eventLoop) setFirstEventID() (err error) {
// pollNow starts polling events right away and waits till the events are // pollNow starts polling events right away and waits till the events are
// processed so we are sure updates are propagated to the database. // processed so we are sure updates are propagated to the database.
func (loop *eventLoop) pollNow() { func (loop *eventLoop) pollNow() {
// When event loop is not running, it would cause infinite wait.
if !loop.isRunning {
return
}
eventProcessedCh := make(chan struct{}) eventProcessedCh := make(chan struct{})
loop.pollCh <- eventProcessedCh loop.pollCh <- eventProcessedCh
<-eventProcessedCh <-eventProcessedCh
@ -156,6 +162,7 @@ func (loop *eventLoop) loop() {
return return
case <-t.C: case <-t.C:
// Randomise periodic calls within range pollInterval ± pollSpread to reduces potential load spikes on API. // Randomise periodic calls within range pollInterval ± pollSpread to reduces potential load spikes on API.
//nolint[gosec] It is OK to use weaker random number generator here
time.Sleep(time.Duration(rand.Intn(2*int(pollIntervalSpread.Milliseconds()))) * time.Millisecond) time.Sleep(time.Duration(rand.Intn(2*int(pollIntervalSpread.Milliseconds()))) * time.Millisecond)
case eventProcessedCh = <-loop.pollCh: case eventProcessedCh = <-loop.pollCh:
// We don't want to wait here. Polling should happen instantly. // We don't want to wait here. Polling should happen instantly.
@ -215,7 +222,7 @@ func (loop *eventLoop) processNextEvent() (more bool, err error) { // nolint[fun
// We only want to consider invalid tokens as real errors because all other errors might fix themselves eventually // We only want to consider invalid tokens as real errors because all other errors might fix themselves eventually
// (e.g. no internet, ulimit reached etc.) // (e.g. no internet, ulimit reached etc.)
defer func() { defer func() {
if errors.Cause(err) == pmapi.ErrAPINotReachable { if errors.Cause(err) == pmapi.ErrNoConnection {
l.Warn("Internet unavailable") l.Warn("Internet unavailable")
err = nil err = nil
} }
@ -231,13 +238,12 @@ func (loop *eventLoop) processNextEvent() (more bool, err error) { // nolint[fun
err = nil err = nil
} }
_, errUnauthorized := errors.Cause(err).(*pmapi.ErrUnauthorized)
if err == nil { if err == nil {
loop.errCounter = 0 loop.errCounter = 0
} }
// All errors except Invalid Token (which is not possible to recover from) are ignored.
if err != nil && !errUnauthorized && errors.Cause(err) != pmapi.ErrInvalidToken { // All errors except ErrUnauthorized (which is not possible to recover from) are ignored.
if err != nil && errors.Cause(err) != pmapi.ErrUnauthorized {
l.WithError(err).WithField("errors", loop.errCounter).Error("Error skipped") l.WithError(err).WithField("errors", loop.errCounter).Error("Error skipped")
loop.errCounter++ loop.errCounter++
if loop.errCounter == errMaxSentry { if loop.errCounter == errMaxSentry {
@ -258,7 +264,7 @@ func (loop *eventLoop) processNextEvent() (more bool, err error) { // nolint[fun
loop.pollCounter++ loop.pollCounter++
var event *pmapi.Event var event *pmapi.Event
if event, err = loop.client().GetEvent(loop.currentEventID); err != nil { if event, err = loop.client().GetEvent(context.Background(), loop.currentEventID); err != nil {
return false, errors.Wrap(err, "failed to get event") return false, errors.Wrap(err, "failed to get event")
} }
@ -285,7 +291,7 @@ func (loop *eventLoop) processNextEvent() (more bool, err error) { // nolint[fun
} }
} }
return event.More == 1, err return bool(event.More), err
} }
func (loop *eventLoop) processEvent(event *pmapi.Event) (err error) { func (loop *eventLoop) processEvent(event *pmapi.Event) (err error) {
@ -344,7 +350,7 @@ func (loop *eventLoop) processAddresses(log *logrus.Entry, addressEvents []*pmap
// Get old addresses for comparisons before updating user. // Get old addresses for comparisons before updating user.
oldList := loop.client().Addresses() oldList := loop.client().Addresses()
if err = loop.user.UpdateUser(); err != nil { if err = loop.user.UpdateUser(context.Background()); err != nil {
if logoutErr := loop.user.Logout(); logoutErr != nil { if logoutErr := loop.user.Logout(); logoutErr != nil {
log.WithError(logoutErr).Error("Failed to logout user after failed update") log.WithError(logoutErr).Error("Failed to logout user after failed update")
} }
@ -381,6 +387,8 @@ func (loop *eventLoop) processAddresses(log *logrus.Entry, addressEvents []*pmap
log.WithField("email", email).Debug("Address was deleted") log.WithField("email", email).Debug("Address was deleted")
loop.user.CloseConnection(email) loop.user.CloseConnection(email)
loop.events.Emit(bridgeEvents.AddressChangedLogoutEvent, email) loop.events.Emit(bridgeEvents.AddressChangedLogoutEvent, email)
case pmapi.EventUpdateFlags:
log.Error("EventUpdateFlags for address event is uknown operation")
} }
} }
@ -409,6 +417,8 @@ func (loop *eventLoop) processLabels(eventLog *logrus.Entry, labels []*pmapi.Eve
if err := loop.store.deleteMailboxEvent(eventLabel.ID); err != nil { if err := loop.store.deleteMailboxEvent(eventLabel.ID); err != nil {
return errors.Wrap(err, "failed to delete label") return errors.Wrap(err, "failed to delete label")
} }
case pmapi.EventUpdateFlags:
log.Error("EventUpdateFlags for label event is uknown operation")
} }
} }
@ -451,8 +461,8 @@ func (loop *eventLoop) processMessages(eventLog *logrus.Entry, messages []*pmapi
msgLog.WithError(err).Warning("Message was not present in DB. Trying fetch...") msgLog.WithError(err).Warning("Message was not present in DB. Trying fetch...")
if msg, err = loop.client().GetMessage(message.ID); err != nil { if msg, err = loop.client().GetMessage(context.Background(), message.ID); err != nil {
if _, ok := err.(*pmapi.ErrUnprocessableEntity); ok { if _, ok := err.(pmapi.ErrUnprocessableEntity); ok {
msgLog.WithError(err).Warn("Skipping message update because message exists neither in local DB nor on API") msgLog.WithError(err).Warn("Skipping message update because message exists neither in local DB nor on API")
err = nil err = nil
continue continue

View File

@ -18,6 +18,7 @@
package store package store
import ( import (
"context"
"net/mail" "net/mail"
"testing" "testing"
"time" "time"
@ -39,17 +40,17 @@ func TestEventLoopProcessMoreEvents(t *testing.T) {
// Doesn't matter which IDs are used. // Doesn't matter which IDs are used.
// This test is trying to see whether event loop will immediately process // This test is trying to see whether event loop will immediately process
// next event if there is `More` of them. // next event if there is `More` of them.
m.client.EXPECT().GetEvent("latestEventID").Return(&pmapi.Event{ m.client.EXPECT().GetEvent(gomock.Any(), "latestEventID").Return(&pmapi.Event{
EventID: "event50", EventID: "event50",
More: 1, More: true,
}, nil), }, nil),
m.client.EXPECT().GetEvent("event50").Return(&pmapi.Event{ m.client.EXPECT().GetEvent(gomock.Any(), "event50").Return(&pmapi.Event{
EventID: "event70", EventID: "event70",
More: 0, More: false,
}, nil), }, nil),
m.client.EXPECT().GetEvent("event70").Return(&pmapi.Event{ m.client.EXPECT().GetEvent(gomock.Any(), "event70").Return(&pmapi.Event{
EventID: "event71", EventID: "event71",
More: 0, More: false,
}, nil), }, nil),
) )
m.newStoreNoEvents(true) m.newStoreNoEvents(true)
@ -165,7 +166,7 @@ func TestEventLoopDeletionPaused(t *testing.T) {
func testEvent(t *testing.T, m *mocksForStore, event *pmapi.Event) { func testEvent(t *testing.T, m *mocksForStore, event *pmapi.Event) {
eventReceived := make(chan struct{}) eventReceived := make(chan struct{})
m.client.EXPECT().GetEvent("latestEventID").DoAndReturn(func(eventID string) (*pmapi.Event, error) { m.client.EXPECT().GetEvent(gomock.Any(), "latestEventID").DoAndReturn(func(_ context.Context, eventID string) (*pmapi.Event, error) {
defer close(eventReceived) defer close(eventReceived)
return event, nil return event, nil
}) })
@ -187,7 +188,7 @@ func TestEventLoopUpdateMessage(t *testing.T) {
msg := &pmapi.Message{ msg := &pmapi.Message{
ID: "msg1", ID: "msg1",
Subject: "old", Subject: "old",
Unread: 0, Unread: false,
Flags: 10, Flags: 10,
Sender: address1, Sender: address1,
ToList: []*mail.Address{address2}, ToList: []*mail.Address{address2},
@ -199,7 +200,7 @@ func TestEventLoopUpdateMessage(t *testing.T) {
newMsg := &pmapi.Message{ newMsg := &pmapi.Message{
ID: "msg1", ID: "msg1",
Subject: "new", Subject: "new",
Unread: 1, Unread: true,
Flags: 11, Flags: 11,
Sender: address2, Sender: address2,
ToList: []*mail.Address{address1}, ToList: []*mail.Address{address1},

View File

@ -254,7 +254,7 @@ func (storeMailbox *Mailbox) txGetAPIIDsBucket(tx *bolt.Tx) *bolt.Bucket {
return storeMailbox.txGetBucket(tx).Bucket(apiIDsBucket) return storeMailbox.txGetBucket(tx).Bucket(apiIDsBucket)
} }
// txGetDeletedIDsBucket returns the bucket with messagesID marked as deleted // txGetDeletedIDsBucket returns the bucket with messagesID marked as deleted.
func (storeMailbox *Mailbox) txGetDeletedIDsBucket(tx *bolt.Tx) *bolt.Bucket { func (storeMailbox *Mailbox) txGetDeletedIDsBucket(tx *bolt.Tx) *bolt.Bucket {
return storeMailbox.txGetBucket(tx).Bucket(deletedIDsBucket) return storeMailbox.txGetBucket(tx).Bucket(deletedIDsBucket)
} }

View File

@ -129,17 +129,10 @@ func (mc *mailboxCounts) getPMLabel() *pmapi.Label {
Color: mc.Color, Color: mc.Color,
Order: mc.Order, Order: mc.Order,
Type: pmapi.LabelTypeMailbox, Type: pmapi.LabelTypeMailbox,
Exclusive: mc.isExclusive(), Exclusive: pmapi.Boolean(mc.IsFolder),
} }
} }
func (mc *mailboxCounts) isExclusive() int {
if mc.IsFolder {
return 1
}
return 0
}
// createOrUpdateMailboxCountsBuckets will not change the on-API-counts. // createOrUpdateMailboxCountsBuckets will not change the on-API-counts.
func (store *Store) createOrUpdateMailboxCountsBuckets(labels []*pmapi.Label) error { func (store *Store) createOrUpdateMailboxCountsBuckets(labels []*pmapi.Label) error {
// Don't forget about system folders. // Don't forget about system folders.
@ -162,7 +155,7 @@ func (store *Store) createOrUpdateMailboxCountsBuckets(labels []*pmapi.Label) er
mailbox.LabelName = label.Path mailbox.LabelName = label.Path
mailbox.Color = label.Color mailbox.Color = label.Color
mailbox.Order = label.Order mailbox.Order = label.Order
mailbox.IsFolder = label.Exclusive == 1 mailbox.IsFolder = bool(label.Exclusive)
// Write. // Write.
if err = mailbox.txWriteToBucket(countsBkt); err != nil { if err = mailbox.txWriteToBucket(countsBkt); err != nil {

View File

@ -75,7 +75,7 @@ func TestMailboxNames(t *testing.T) {
newLabel(100, "labelID1", "Label1"), newLabel(100, "labelID1", "Label1"),
newLabel(1000, "folderID1", "Folder1"), newLabel(1000, "folderID1", "Folder1"),
} }
foldersAndLabels[1].Exclusive = 1 foldersAndLabels[1].Exclusive = true
for _, counts := range getSystemFolders() { for _, counts := range getSystemFolders() {
foldersAndLabels = append(foldersAndLabels, counts.getPMLabel()) foldersAndLabels = append(foldersAndLabels, counts.getPMLabel())

View File

@ -36,23 +36,36 @@ import (
func (storeMailbox *Mailbox) GetAPIIDsFromUIDRange(start, stop uint32) (apiIDs []string, err error) { func (storeMailbox *Mailbox) GetAPIIDsFromUIDRange(start, stop uint32) (apiIDs []string, err error) {
err = storeMailbox.db().View(func(tx *bolt.Tx) error { err = storeMailbox.db().View(func(tx *bolt.Tx) error {
b := storeMailbox.txGetIMAPIDsBucket(tx) b := storeMailbox.txGetIMAPIDsBucket(tx)
c := b.Cursor()
// If the start range is a wildcard, the range can only refer to the last message in the mailbox.
if start == 0 {
_, apiID := c.Last()
apiIDs = append(apiIDs, string(apiID))
return nil
}
// Resolve the stop value to be the final UID in the mailbox.
if stop == 0 { if stop == 0 {
// A null stop means no stop. stop = storeMailbox.txGetFinalUID(b)
stop = ^uint32(0) }
// After resolving the stop value, it might be less than start so we sort it.
if start > stop {
start, stop = stop, start
} }
startb := itob(start) startb := itob(start)
stopb := itob(stop) stopb := itob(stop)
c := b.Cursor()
for k, v := c.Seek(startb); k != nil && bytes.Compare(k, stopb) <= 0; k, v = c.Next() { for k, v := c.Seek(startb); k != nil && bytes.Compare(k, stopb) <= 0; k, v = c.Next() {
apiIDs = append(apiIDs, string(v)) apiIDs = append(apiIDs, string(v))
} }
return nil return nil
}) })
return
return apiIDs, err
} }
// GetAPIIDsFromSequenceRange returns API IDs by IMAP sequence number range. // GetAPIIDsFromSequenceRange returns API IDs by IMAP sequence number range.
@ -60,28 +73,47 @@ func (storeMailbox *Mailbox) GetAPIIDsFromSequenceRange(start, stop uint32) (api
err = storeMailbox.db().View(func(tx *bolt.Tx) error { err = storeMailbox.db().View(func(tx *bolt.Tx) error {
b := storeMailbox.txGetIMAPIDsBucket(tx) b := storeMailbox.txGetIMAPIDsBucket(tx)
c := b.Cursor() c := b.Cursor()
// If the start range is a wildcard, the range can only refer to the last message in the mailbox.
if start == 0 {
_, apiID := c.Last()
apiIDs = append(apiIDs, string(apiID))
return nil
}
var i uint32 var i uint32
for k, v := c.First(); k != nil; k, v = c.Next() { for k, v := c.First(); k != nil; k, v = c.Next() {
i++ i++
if i < start { if i < start {
continue continue
} }
if stop > 0 && i > stop { if stop > 0 && i > stop {
break break
} }
apiIDs = append(apiIDs, string(v)) apiIDs = append(apiIDs, string(v))
} }
if stop == 0 && len(apiIDs) == 0 {
if _, apiID := c.Last(); len(apiID) > 0 {
apiIDs = append(apiIDs, string(apiID))
}
}
return nil return nil
}) })
return
return apiIDs, err
} }
// GetLatestAPIID returns the latest message API ID which still exists. // GetLatestAPIID returns the latest message API ID which still exists.
// Info: not the latest IMAP UID which can be already removed. // Info: not the latest IMAP UID which can be already removed.
func (storeMailbox *Mailbox) GetLatestAPIID() (apiID string, err error) { func (storeMailbox *Mailbox) GetLatestAPIID() (apiID string, err error) {
err = storeMailbox.db().View(func(tx *bolt.Tx) error { err = storeMailbox.db().View(func(tx *bolt.Tx) error {
b := storeMailbox.txGetAPIIDsBucket(tx) c := storeMailbox.txGetAPIIDsBucket(tx).Cursor()
c := b.Cursor()
lastAPIID, _ := c.Last() lastAPIID, _ := c.Last()
apiID = string(lastAPIID) apiID = string(lastAPIID)
if apiID == "" { if apiID == "" {
@ -283,3 +315,8 @@ func (storeMailbox *Mailbox) GetUIDByHeader(header *mail.Header) (foundUID uint3
return foundUID return foundUID
} }
func (storeMailbox *Mailbox) txGetFinalUID(b *bolt.Bucket) uint32 {
uid, _ := b.Cursor().Last()
return btoi(uid)
}

View File

@ -37,10 +37,10 @@ func TestGetSequenceNumberAndGetUID(t *testing.T) {
m.newStoreNoEvents(true) m.newStoreNoEvents(true)
insertMessage(t, m, "msg1", "Test message 1", addrID1, 0, []string{pmapi.AllMailLabel, pmapi.InboxLabel}) insertMessage(t, m, "msg1", "Test message 1", addrID1, false, []string{pmapi.AllMailLabel, pmapi.InboxLabel})
insertMessage(t, m, "msg2", "Test message 2", addrID1, 0, []string{pmapi.AllMailLabel, pmapi.ArchiveLabel}) insertMessage(t, m, "msg2", "Test message 2", addrID1, false, []string{pmapi.AllMailLabel, pmapi.ArchiveLabel})
insertMessage(t, m, "msg3", "Test message 3", addrID1, 0, []string{pmapi.AllMailLabel, pmapi.InboxLabel}) insertMessage(t, m, "msg3", "Test message 3", addrID1, false, []string{pmapi.AllMailLabel, pmapi.InboxLabel})
insertMessage(t, m, "msg4", "Test message 4", addrID1, 0, []string{pmapi.AllMailLabel}) insertMessage(t, m, "msg4", "Test message 4", addrID1, false, []string{pmapi.AllMailLabel})
checkAllMessageIDs(t, m, []string{"msg1", "msg2", "msg3", "msg4"}) checkAllMessageIDs(t, m, []string{"msg1", "msg2", "msg3", "msg4"})
@ -56,7 +56,7 @@ func checkMailboxMessageIDs(t *testing.T, m *mocksForStore, mailboxLabel string,
storeAddress := m.store.addresses[addrID1] storeAddress := m.store.addresses[addrID1]
storeMailbox := storeAddress.mailboxes[mailboxLabel] storeMailbox := storeAddress.mailboxes[mailboxLabel]
ids, err := storeMailbox.GetAPIIDsFromSequenceRange(0, uint32(len(wantIDs))) ids, err := storeMailbox.GetAPIIDsFromSequenceRange(1, uint32(len(wantIDs)))
require.Nil(t, err) require.Nil(t, err)
idx := 0 idx := 0
@ -82,20 +82,20 @@ func TestGetUIDByHeader(t *testing.T) { //nolint[funlen]
m.newStoreNoEvents(true) m.newStoreNoEvents(true)
tstMsg := getTestMessage("msg1", "Without external ID", addrID1, 0, []string{pmapi.AllMailLabel, pmapi.SentLabel}) tstMsg := getTestMessage("msg1", "Without external ID", addrID1, false, []string{pmapi.AllMailLabel, pmapi.SentLabel})
require.Nil(t, m.store.createOrUpdateMessageEvent(tstMsg)) require.Nil(t, m.store.createOrUpdateMessageEvent(tstMsg))
tstMsg = getTestMessage("msg2", "External ID with spaces", addrID1, 0, []string{pmapi.AllMailLabel, pmapi.SentLabel}) tstMsg = getTestMessage("msg2", "External ID with spaces", addrID1, false, []string{pmapi.AllMailLabel, pmapi.SentLabel})
tstMsg.ExternalID = " externalID-non-pm-com " tstMsg.ExternalID = " externalID-non-pm-com "
require.Nil(t, m.store.createOrUpdateMessageEvent(tstMsg)) require.Nil(t, m.store.createOrUpdateMessageEvent(tstMsg))
tstMsg = getTestMessage("msg3", "External ID with <>", addrID1, 0, []string{pmapi.AllMailLabel, pmapi.SentLabel}) tstMsg = getTestMessage("msg3", "External ID with <>", addrID1, false, []string{pmapi.AllMailLabel, pmapi.SentLabel})
tstMsg.ExternalID = "<externalID@pm.me>" tstMsg.ExternalID = "<externalID@pm.me>"
tstMsg.Header = mail.Header{"References": []string{"wrongID", "externalID-non-pm-com", "msg2"}} tstMsg.Header = mail.Header{"References": []string{"wrongID", "externalID-non-pm-com", "msg2"}}
require.Nil(t, m.store.createOrUpdateMessageEvent(tstMsg)) require.Nil(t, m.store.createOrUpdateMessageEvent(tstMsg))
// Not sure if this is a real-world scenario but we should be able to address this properly. // Not sure if this is a real-world scenario but we should be able to address this properly.
tstMsg = getTestMessage("msg4", "External ID with <> and spaces and special characters", addrID1, 0, []string{pmapi.AllMailLabel, pmapi.SentLabel}) tstMsg = getTestMessage("msg4", "External ID with <> and spaces and special characters", addrID1, false, []string{pmapi.AllMailLabel, pmapi.SentLabel})
tstMsg.ExternalID = " < external.()+*[]ID@another.pm.me > " tstMsg.ExternalID = " < external.()+*[]ID@another.pm.me > "
require.Nil(t, m.store.createOrUpdateMessageEvent(tstMsg)) require.Nil(t, m.store.createOrUpdateMessageEvent(tstMsg))

View File

@ -25,7 +25,7 @@ import (
) )
// ErrAllMailOpNotAllowed is error user when user tries to do unsupported // ErrAllMailOpNotAllowed is error user when user tries to do unsupported
// operation on All Mail folder // operation on All Mail folder.
var ErrAllMailOpNotAllowed = errors.New("operation not allowed for 'All Mail' folder") var ErrAllMailOpNotAllowed = errors.New("operation not allowed for 'All Mail' folder")
// GetMessage returns the `pmapi.Message` struct wrapped in `StoreMessage` // GetMessage returns the `pmapi.Message` struct wrapped in `StoreMessage`
@ -41,7 +41,7 @@ func (storeMailbox *Mailbox) GetMessage(apiID string) (*Message, error) {
// FetchMessage fetches the message with the given `apiID`, stores it in the database, and returns a new store message // FetchMessage fetches the message with the given `apiID`, stores it in the database, and returns a new store message
// wrapping it. // wrapping it.
func (storeMailbox *Mailbox) FetchMessage(apiID string) (*Message, error) { func (storeMailbox *Mailbox) FetchMessage(apiID string) (*Message, error) {
msg, err := storeMailbox.client().GetMessage(apiID) msg, err := storeMailbox.client().GetMessage(exposeContextForIMAP(), apiID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -58,15 +58,17 @@ func (storeMailbox *Mailbox) ImportMessage(msg *pmapi.Message, body []byte, labe
} }
importReqs := &pmapi.ImportMsgReq{ importReqs := &pmapi.ImportMsgReq{
AddressID: msg.AddressID, Metadata: &pmapi.ImportMetadata{
Body: body, AddressID: msg.AddressID,
Unread: msg.Unread, Unread: msg.Unread,
Flags: msg.Flags, Flags: msg.Flags,
Time: msg.Time, Time: msg.Time,
LabelIDs: labelIDs, LabelIDs: labelIDs,
},
Message: body,
} }
res, err := storeMailbox.client().Import([]*pmapi.ImportMsgReq{importReqs}) res, err := storeMailbox.client().Import(exposeContextForIMAP(), pmapi.ImportMsgReqs{importReqs})
if err != nil { if err != nil {
return err return err
} }
@ -95,7 +97,7 @@ func (storeMailbox *Mailbox) LabelMessages(apiIDs []string) error {
return ErrAllMailOpNotAllowed return ErrAllMailOpNotAllowed
} }
defer storeMailbox.pollNow() defer storeMailbox.pollNow()
return storeMailbox.client().LabelMessages(apiIDs, storeMailbox.labelID) return storeMailbox.client().LabelMessages(exposeContextForIMAP(), apiIDs, storeMailbox.labelID)
} }
// UnlabelMessages removes the label by calling an API. // UnlabelMessages removes the label by calling an API.
@ -108,7 +110,7 @@ func (storeMailbox *Mailbox) UnlabelMessages(apiIDs []string) error {
return ErrAllMailOpNotAllowed return ErrAllMailOpNotAllowed
} }
defer storeMailbox.pollNow() defer storeMailbox.pollNow()
return storeMailbox.client().UnlabelMessages(apiIDs, storeMailbox.labelID) return storeMailbox.client().UnlabelMessages(exposeContextForIMAP(), apiIDs, storeMailbox.labelID)
} }
// MarkMessagesRead marks the message read by calling an API. // MarkMessagesRead marks the message read by calling an API.
@ -128,14 +130,14 @@ func (storeMailbox *Mailbox) MarkMessagesRead(apiIDs []string) error {
// Therefore we do not issue API update if the message is already read. // Therefore we do not issue API update if the message is already read.
ids := []string{} ids := []string{}
for _, apiID := range apiIDs { for _, apiID := range apiIDs {
if message, _ := storeMailbox.store.getMessageFromDB(apiID); message == nil || message.Unread == 1 { if message, _ := storeMailbox.store.getMessageFromDB(apiID); message == nil || message.Unread {
ids = append(ids, apiID) ids = append(ids, apiID)
} }
} }
if len(ids) == 0 { if len(ids) == 0 {
return nil return nil
} }
return storeMailbox.client().MarkMessagesRead(ids) return storeMailbox.client().MarkMessagesRead(exposeContextForIMAP(), ids)
} }
// MarkMessagesUnread marks the message unread by calling an API. // MarkMessagesUnread marks the message unread by calling an API.
@ -147,7 +149,7 @@ func (storeMailbox *Mailbox) MarkMessagesUnread(apiIDs []string) error {
"mailbox": storeMailbox.Name, "mailbox": storeMailbox.Name,
}).Trace("Marking messages as unread") }).Trace("Marking messages as unread")
defer storeMailbox.pollNow() defer storeMailbox.pollNow()
return storeMailbox.client().MarkMessagesUnread(apiIDs) return storeMailbox.client().MarkMessagesUnread(exposeContextForIMAP(), apiIDs)
} }
// MarkMessagesStarred adds the Starred label by calling an API. // MarkMessagesStarred adds the Starred label by calling an API.
@ -160,7 +162,7 @@ func (storeMailbox *Mailbox) MarkMessagesStarred(apiIDs []string) error {
"mailbox": storeMailbox.Name, "mailbox": storeMailbox.Name,
}).Trace("Marking messages as starred") }).Trace("Marking messages as starred")
defer storeMailbox.pollNow() defer storeMailbox.pollNow()
return storeMailbox.client().LabelMessages(apiIDs, pmapi.StarredLabel) return storeMailbox.client().LabelMessages(exposeContextForIMAP(), apiIDs, pmapi.StarredLabel)
} }
// MarkMessagesUnstarred removes the Starred label by calling an API. // MarkMessagesUnstarred removes the Starred label by calling an API.
@ -173,11 +175,11 @@ func (storeMailbox *Mailbox) MarkMessagesUnstarred(apiIDs []string) error {
"mailbox": storeMailbox.Name, "mailbox": storeMailbox.Name,
}).Trace("Marking messages as unstarred") }).Trace("Marking messages as unstarred")
defer storeMailbox.pollNow() defer storeMailbox.pollNow()
return storeMailbox.client().UnlabelMessages(apiIDs, pmapi.StarredLabel) return storeMailbox.client().UnlabelMessages(exposeContextForIMAP(), apiIDs, pmapi.StarredLabel)
} }
// MarkMessagesDeleted adds local flag \Deleted. This is not propagated to API // MarkMessagesDeleted adds local flag \Deleted. This is not propagated to API
// until RemoveDeleted is called // until RemoveDeleted is called.
func (storeMailbox *Mailbox) MarkMessagesDeleted(apiIDs []string) error { func (storeMailbox *Mailbox) MarkMessagesDeleted(apiIDs []string) error {
log.WithFields(logrus.Fields{ log.WithFields(logrus.Fields{
"messages": apiIDs, "messages": apiIDs,
@ -257,11 +259,11 @@ func (storeMailbox *Mailbox) RemoveDeleted(apiIDs []string) error {
} }
case pmapi.DraftLabel: case pmapi.DraftLabel:
storeMailbox.log.WithField("ids", apiIDs).Warn("Deleting drafts") storeMailbox.log.WithField("ids", apiIDs).Warn("Deleting drafts")
if err := storeMailbox.client().DeleteMessages(apiIDs); err != nil { if err := storeMailbox.client().DeleteMessages(exposeContextForIMAP(), apiIDs); err != nil {
return err return err
} }
default: default:
if err := storeMailbox.client().UnlabelMessages(apiIDs, storeMailbox.labelID); err != nil { if err := storeMailbox.client().UnlabelMessages(exposeContextForIMAP(), apiIDs, storeMailbox.labelID); err != nil {
return err return err
} }
} }
@ -299,13 +301,13 @@ func (storeMailbox *Mailbox) deleteFromTrashOrSpam(apiIDs []string) error {
} }
} }
if len(messageIDsToUnlabel) > 0 { if len(messageIDsToUnlabel) > 0 {
if err := storeMailbox.client().UnlabelMessages(messageIDsToUnlabel, storeMailbox.labelID); err != nil { if err := storeMailbox.client().UnlabelMessages(exposeContextForIMAP(), messageIDsToUnlabel, storeMailbox.labelID); err != nil {
l.WithError(err).Warning("Cannot unlabel before deleting") l.WithError(err).Warning("Cannot unlabel before deleting")
} }
} }
if len(messageIDsToDelete) > 0 { if len(messageIDsToDelete) > 0 {
storeMailbox.log.WithField("ids", messageIDsToDelete).Warn("Deleting messages") storeMailbox.log.WithField("ids", messageIDsToDelete).Warn("Deleting messages")
if err := storeMailbox.client().DeleteMessages(messageIDsToDelete); err != nil { if err := storeMailbox.client().DeleteMessages(exposeContextForIMAP(), messageIDsToDelete); err != nil {
return err return err
} }
} }

View File

@ -18,10 +18,14 @@
package store package store
import ( import (
"bufio"
"bytes"
"net/mail" "net/mail"
"net/textproto"
pkgMsg "github.com/ProtonMail/proton-bridge/pkg/message" pkgMsg "github.com/ProtonMail/proton-bridge/pkg/message"
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/pkg/errors"
bolt "go.etcd.io/bbolt" bolt "go.etcd.io/bbolt"
) )
@ -64,7 +68,7 @@ func (message *Message) Message() *pmapi.Message {
} }
// IsMarkedDeleted returns true if message is marked as deleted for specific // IsMarkedDeleted returns true if message is marked as deleted for specific
// mailbox // mailbox.
func (message *Message) IsMarkedDeleted() bool { func (message *Message) IsMarkedDeleted() bool {
isMarkedAsDeleted := false isMarkedAsDeleted := false
err := message.storeMailbox.db().View(func(tx *bolt.Tx) error { err := message.storeMailbox.db().View(func(tx *bolt.Tx) error {
@ -103,6 +107,8 @@ func (message *Message) SetSize(size int64) error {
// header of decrypted message. This should not trigger any IMAP update. // header of decrypted message. This should not trigger any IMAP update.
// NOTE: Content type depends on details of decrypted message which we want to // NOTE: Content type depends on details of decrypted message which we want to
// cache. // cache.
//
// Deprecated: Use SetHeader instead.
func (message *Message) SetContentTypeAndHeader(mimeType string, header mail.Header) error { func (message *Message) SetContentTypeAndHeader(mimeType string, header mail.Header) error {
message.msg.MIMEType = mimeType message.msg.MIMEType = mimeType
message.msg.Header = header message.msg.Header = header
@ -121,6 +127,57 @@ func (message *Message) SetContentTypeAndHeader(mimeType string, header mail.Hea
return message.store.db.Update(txUpdate) return message.store.db.Update(txUpdate)
} }
// SetHeader checks header can be parsed and if yes it stores header bytes in
// database.
func (message *Message) SetHeader(header []byte) error {
_, err := textproto.NewReader(bufio.NewReader(bytes.NewReader(header))).ReadMIMEHeader()
if err != nil {
return err
}
return message.store.db.Update(func(tx *bolt.Tx) error {
return tx.Bucket(headersBucket).Put([]byte(message.ID()), header)
})
}
// IsFullHeaderCached will check that valid full header is stored in DB.
func (message *Message) IsFullHeaderCached() bool {
header, err := message.getRawHeader()
return err == nil && header != nil
}
func (message *Message) getRawHeader() (raw []byte, err error) {
err = message.store.db.View(func(tx *bolt.Tx) error {
raw = tx.Bucket(headersBucket).Get([]byte(message.ID()))
return nil
})
return
}
// GetHeader will return cached header from DB.
func (message *Message) GetHeader() []byte {
raw, err := message.getRawHeader()
if err != nil {
panic(errors.Wrap(err, "failed to get raw message header"))
}
return raw
}
// GetMIMEHeader will return cached header from DB, parsed as a textproto.MIMEHeader.
func (message *Message) GetMIMEHeader() textproto.MIMEHeader {
raw, err := message.getRawHeader()
if err != nil {
panic(errors.Wrap(err, "failed to get raw message header"))
}
header, err := textproto.NewReader(bufio.NewReader(bytes.NewReader(raw))).ReadMIMEHeader()
if err != nil {
return textproto.MIMEHeader(message.msg.Header)
}
return header
}
// SetBodyStructure stores serialized body structure in database. // SetBodyStructure stores serialized body structure in database.
func (message *Message) SetBodyStructure(bs *pkgMsg.BodyStructure) error { func (message *Message) SetBodyStructure(bs *pkgMsg.BodyStructure) error {
txUpdate := func(tx *bolt.Tx) error { txUpdate := func(tx *bolt.Tx) error {

View File

@ -1,10 +1,11 @@
// Code generated by MockGen. DO NOT EDIT. // Code generated by MockGen. DO NOT EDIT.
// Source: github.com/ProtonMail/proton-bridge/internal/store (interfaces: PanicHandler,ClientManager,BridgeUser,ChangeNotifier) // Source: github.com/ProtonMail/proton-bridge/internal/store (interfaces: PanicHandler,BridgeUser,ChangeNotifier)
// Package mocks is a generated GoMock package. // Package mocks is a generated GoMock package.
package mocks package mocks
import ( import (
context "context"
reflect "reflect" reflect "reflect"
pmapi "github.com/ProtonMail/proton-bridge/pkg/pmapi" pmapi "github.com/ProtonMail/proton-bridge/pkg/pmapi"
@ -46,43 +47,6 @@ func (mr *MockPanicHandlerMockRecorder) HandlePanic() *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HandlePanic", reflect.TypeOf((*MockPanicHandler)(nil).HandlePanic)) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HandlePanic", reflect.TypeOf((*MockPanicHandler)(nil).HandlePanic))
} }
// MockClientManager is a mock of ClientManager interface
type MockClientManager struct {
ctrl *gomock.Controller
recorder *MockClientManagerMockRecorder
}
// MockClientManagerMockRecorder is the mock recorder for MockClientManager
type MockClientManagerMockRecorder struct {
mock *MockClientManager
}
// NewMockClientManager creates a new mock instance
func NewMockClientManager(ctrl *gomock.Controller) *MockClientManager {
mock := &MockClientManager{ctrl: ctrl}
mock.recorder = &MockClientManagerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockClientManager) EXPECT() *MockClientManagerMockRecorder {
return m.recorder
}
// GetClient mocks base method
func (m *MockClientManager) GetClient(arg0 string) pmapi.Client {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetClient", arg0)
ret0, _ := ret[0].(pmapi.Client)
return ret0
}
// GetClient indicates an expected call of GetClient
func (mr *MockClientManagerMockRecorder) GetClient(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClient", reflect.TypeOf((*MockClientManager)(nil).GetClient), arg0)
}
// MockBridgeUser is a mock of BridgeUser interface // MockBridgeUser is a mock of BridgeUser interface
type MockBridgeUser struct { type MockBridgeUser struct {
ctrl *gomock.Controller ctrl *gomock.Controller
@ -145,6 +109,20 @@ func (mr *MockBridgeUserMockRecorder) GetAddressID(arg0 interface{}) *gomock.Cal
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAddressID", reflect.TypeOf((*MockBridgeUser)(nil).GetAddressID), arg0) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAddressID", reflect.TypeOf((*MockBridgeUser)(nil).GetAddressID), arg0)
} }
// GetClient mocks base method
func (m *MockBridgeUser) GetClient() pmapi.Client {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetClient")
ret0, _ := ret[0].(pmapi.Client)
return ret0
}
// GetClient indicates an expected call of GetClient
func (mr *MockBridgeUserMockRecorder) GetClient() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClient", reflect.TypeOf((*MockBridgeUser)(nil).GetClient))
}
// GetPrimaryAddress mocks base method // GetPrimaryAddress mocks base method
func (m *MockBridgeUser) GetPrimaryAddress() string { func (m *MockBridgeUser) GetPrimaryAddress() string {
m.ctrl.T.Helper() m.ctrl.T.Helper()
@ -230,17 +208,17 @@ func (mr *MockBridgeUserMockRecorder) Logout() *gomock.Call {
} }
// UpdateUser mocks base method // UpdateUser mocks base method
func (m *MockBridgeUser) UpdateUser() error { func (m *MockBridgeUser) UpdateUser(arg0 context.Context) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateUser") ret := m.ctrl.Call(m, "UpdateUser", arg0)
ret0, _ := ret[0].(error) ret0, _ := ret[0].(error)
return ret0 return ret0
} }
// UpdateUser indicates an expected call of UpdateUser // UpdateUser indicates an expected call of UpdateUser
func (mr *MockBridgeUserMockRecorder) UpdateUser() *gomock.Call { func (mr *MockBridgeUserMockRecorder) UpdateUser(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUser", reflect.TypeOf((*MockBridgeUser)(nil).UpdateUser)) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUser", reflect.TypeOf((*MockBridgeUser)(nil).UpdateUser), arg0)
} }
// MockChangeNotifier is a mock of ChangeNotifier interface // MockChangeNotifier is a mock of ChangeNotifier interface

View File

@ -58,6 +58,20 @@ func (mr *MockListenerMockRecorder) Emit(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Emit", reflect.TypeOf((*MockListener)(nil).Emit), arg0, arg1) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Emit", reflect.TypeOf((*MockListener)(nil).Emit), arg0, arg1)
} }
// ProvideChannel mocks base method
func (m *MockListener) ProvideChannel(arg0 string) <-chan string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ProvideChannel", arg0)
ret0, _ := ret[0].(<-chan string)
return ret0
}
// ProvideChannel indicates an expected call of ProvideChannel
func (mr *MockListenerMockRecorder) ProvideChannel(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProvideChannel", reflect.TypeOf((*MockListener)(nil).ProvideChannel), arg0)
}
// Remove mocks base method // Remove mocks base method
func (m *MockListener) Remove(arg0 string, arg1 chan<- string) { func (m *MockListener) Remove(arg0 string, arg1 chan<- string) {
m.ctrl.T.Helper() m.ctrl.T.Helper()

View File

@ -19,6 +19,7 @@
package store package store
import ( import (
"context"
"fmt" "fmt"
"os" "os"
"sync" "sync"
@ -34,15 +35,15 @@ import (
) )
const ( const (
// PathDelimiter for IMAP // PathDelimiter for IMAP.
PathDelimiter = "/" PathDelimiter = "/"
// UserLabelsMailboxName for IMAP // UserLabelsMailboxName for IMAP.
UserLabelsMailboxName = "Labels" UserLabelsMailboxName = "Labels"
// UserLabelsPrefix contains name with delimiter for IMAP // UserLabelsPrefix contains name with delimiter for IMAP.
UserLabelsPrefix = UserLabelsMailboxName + PathDelimiter UserLabelsPrefix = UserLabelsMailboxName + PathDelimiter
// UserFoldersMailboxName for IMAP // UserFoldersMailboxName for IMAP.
UserFoldersMailboxName = "Folders" UserFoldersMailboxName = "Folders"
// UserFoldersPrefix contains name with delimiter for IMAP // UserFoldersPrefix contains name with delimiter for IMAP.
UserFoldersPrefix = UserFoldersMailboxName + PathDelimiter UserFoldersPrefix = UserFoldersMailboxName + PathDelimiter
) )
@ -51,7 +52,9 @@ var (
// Database structure: // Database structure:
// * metadata // * metadata
// * {messageID} -> message data (subject, from, to, time, headers, body size, ...) // * {messageID} -> message data (subject, from, to, time, body size, ...)
// * headers
// * {messageID} -> header bytes
// * bodystructure // * bodystructure
// * {messageID} -> message body structure // * {messageID} -> message body structure
// * msgbuildcount // * msgbuildcount
@ -77,6 +80,7 @@ var (
// * deleted_ids (can be missing or have no keys) // * deleted_ids (can be missing or have no keys)
// * {messageID} -> true // * {messageID} -> true
metadataBucket = []byte("metadata") //nolint[gochecknoglobals] metadataBucket = []byte("metadata") //nolint[gochecknoglobals]
headersBucket = []byte("headers") //nolint[gochecknoglobals]
bodystructureBucket = []byte("bodystructure") //nolint[gochecknoglobals] bodystructureBucket = []byte("bodystructure") //nolint[gochecknoglobals]
msgBuildCountBucket = []byte("msgbuildcount") //nolint[gochecknoglobals] msgBuildCountBucket = []byte("msgbuildcount") //nolint[gochecknoglobals]
countsBucket = []byte("counts") //nolint[gochecknoglobals] countsBucket = []byte("counts") //nolint[gochecknoglobals]
@ -97,13 +101,24 @@ var (
ErrNoSuchSeqNum = errors.New("no such sequence number") //nolint[gochecknoglobals] ErrNoSuchSeqNum = errors.New("no such sequence number") //nolint[gochecknoglobals]
) )
// exposeContextForIMAP should be replaced once with context passed
// as an argument from IMAP package and IMAP library should cancel
// context when IMAP client cancels the request.
func exposeContextForIMAP() context.Context {
return context.TODO()
}
// exposeContextForSMTP is the same as above but for SMTP.
func exposeContextForSMTP() context.Context {
return context.TODO()
}
// Store is local user storage, which handles the synchronization between IMAP and PM API. // Store is local user storage, which handles the synchronization between IMAP and PM API.
type Store struct { type Store struct {
sentryReporter *sentry.Reporter sentryReporter *sentry.Reporter
panicHandler PanicHandler panicHandler PanicHandler
eventLoop *eventLoop eventLoop *eventLoop
user BridgeUser user BridgeUser
clientManager ClientManager
log *logrus.Entry log *logrus.Entry
@ -124,13 +139,12 @@ func New( // nolint[funlen]
sentryReporter *sentry.Reporter, sentryReporter *sentry.Reporter,
panicHandler PanicHandler, panicHandler PanicHandler,
user BridgeUser, user BridgeUser,
clientManager ClientManager,
events listener.Listener, events listener.Listener,
path string, path string,
cache *Cache, cache *Cache,
) (store *Store, err error) { ) (store *Store, err error) {
if user == nil || clientManager == nil || events == nil || cache == nil { if user == nil || events == nil || cache == nil {
return nil, fmt.Errorf("missing parameters - user: %v, api: %v, events: %v, cache: %v", user, clientManager, events, cache) return nil, fmt.Errorf("missing parameters - user: %v, events: %v, cache: %v", user, events, cache)
} }
l := log.WithField("user", user.ID()) l := log.WithField("user", user.ID())
@ -153,7 +167,6 @@ func New( // nolint[funlen]
store = &Store{ store = &Store{
sentryReporter: sentryReporter, sentryReporter: sentryReporter,
panicHandler: panicHandler, panicHandler: panicHandler,
clientManager: clientManager,
user: user, user: user,
cache: cache, cache: cache,
filePath: path, filePath: path,
@ -199,40 +212,24 @@ func openBoltDatabase(filePath string) (db *bolt.DB, err error) {
} }
tx := func(tx *bolt.Tx) (err error) { tx := func(tx *bolt.Tx) (err error) {
if _, err = tx.CreateBucketIfNotExists(metadataBucket); err != nil { buckets := [][]byte{
return metadataBucket,
headersBucket,
bodystructureBucket,
msgBuildCountBucket,
countsBucket,
addressInfoBucket,
addressModeBucket,
syncStateBucket,
mailboxesBucket,
mboxVersionBucket,
} }
if _, err = tx.CreateBucketIfNotExists(bodystructureBucket); err != nil { for _, bucket := range buckets {
return if _, err = tx.CreateBucketIfNotExists(bucket); err != nil {
} err = errors.Wrap(err, string(bucket))
return
if _, err = tx.CreateBucketIfNotExists(msgBuildCountBucket); err != nil { }
return
}
if _, err = tx.CreateBucketIfNotExists(countsBucket); err != nil {
return
}
if _, err = tx.CreateBucketIfNotExists(addressInfoBucket); err != nil {
return
}
if _, err = tx.CreateBucketIfNotExists(addressModeBucket); err != nil {
return
}
if _, err = tx.CreateBucketIfNotExists(syncStateBucket); err != nil {
return
}
if _, err = tx.CreateBucketIfNotExists(mailboxesBucket); err != nil {
return
}
if _, err = tx.CreateBucketIfNotExists(mboxVersionBucket); err != nil {
return
} }
return return
@ -287,13 +284,13 @@ func (store *Store) init(firstInit bool) (err error) {
} }
func (store *Store) client() pmapi.Client { func (store *Store) client() pmapi.Client {
return store.clientManager.GetClient(store.UserID()) return store.user.GetClient()
} }
// initCounts initialises the counts for each label. It tries to use the API first to fetch the labels but if // initCounts initialises the counts for each label. It tries to use the API first to fetch the labels but if
// the API is unavailable for whatever reason it tries to fetch the labels locally. // the API is unavailable for whatever reason it tries to fetch the labels locally.
func (store *Store) initCounts() (labels []*pmapi.Label, err error) { func (store *Store) initCounts() (labels []*pmapi.Label, err error) {
if labels, err = store.client().ListLabels(); err != nil { if labels, err = store.client().ListLabels(context.Background()); err != nil {
store.log.WithError(err).Warn("Could not list API labels. Trying with local labels.") store.log.WithError(err).Warn("Could not list API labels. Trying with local labels.")
if labels, err = store.getLabelsFromLocalStorage(); err != nil { if labels, err = store.getLabelsFromLocalStorage(); err != nil {
store.log.WithError(err).Error("Cannot list local labels") store.log.WithError(err).Error("Cannot list local labels")

View File

@ -18,6 +18,7 @@
package store package store
import ( import (
"context"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os" "os"
@ -25,6 +26,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/ProtonMail/gopenpgp/v2/crypto"
storemocks "github.com/ProtonMail/proton-bridge/internal/store/mocks" storemocks "github.com/ProtonMail/proton-bridge/internal/store/mocks"
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
pmapimocks "github.com/ProtonMail/proton-bridge/pkg/pmapi/mocks" pmapimocks "github.com/ProtonMail/proton-bridge/pkg/pmapi/mocks"
@ -39,8 +41,92 @@ const (
addr2 = "jamesandmichalarecool@pm.me" addr2 = "jamesandmichalarecool@pm.me"
addrID2 = "jamesandmichalarecool" addrID2 = "jamesandmichalarecool"
testPrivateKeyPassword = "apple"
testPrivateKey = `-----BEGIN PGP PRIVATE KEY BLOCK-----
Version: OpenPGP.js v0.7.1
Comment: http://openpgpjs.org
xcMGBFRJbc0BCAC0mMLZPDBbtSCWvxwmOfXfJkE2+ssM3ux21LhD/bPiWefE
WSHlCjJ8PqPHy7snSiUuxuj3f9AvXPvg+mjGLBwu1/QsnSP24sl3qD2onl39
vPiLJXUqZs20ZRgnvX70gjkgEzMFBxINiy2MTIG+4RU8QA7y8KzWev0btqKi
MeVa+GLEHhgZ2KPOn4Jv1q4bI9hV0C9NUe2tTXS6/Vv3vbCY7lRR0kbJ65T5
c8CmpqJuASIJNrSXM/Q3NnnsY4kBYH0s5d2FgbASQvzrjuC2rngUg0EoPsrb
DEVRA2/BCJonw7aASiNCrSP92lkZdtYlax/pcoE/mQ4WSwySFmcFT7yFABEB
AAH+CQMIvzcDReuJkc9gnxAkfgmnkBFwRQrqT/4UAPOF8WGVo0uNvDo7Snlk
qWsJS+54+/Xx6Jur/PdBWeEu+6+6GnppYuvsaT0D0nFdFhF6pjng+02IOxfG
qlYXYcW4hRru3BfvJlSvU2LL/Z/ooBnw3T5vqd0eFHKrvabUuwf0x3+K/sru
Fp24rl2PU+bzQlUgKpWzKDmO+0RdKQ6KVCyCDMIXaAkALwNffAvYxI0wnb2y
WAV/bGn1ODnszOYPk3pEMR6kKSxLLaO69kYx4eTERFyJ+1puAxEPCk3Cfeif
yDWi4rU03YB16XH7hQLSFl61SKeIYlkKmkO5Hk1ybi/BhvOGBPVeGGbxWnwI
46G8DfBHW0+uvD5cAQtk2d/q3Ge1I+DIyvuRCcSu0XSBNv/Bkpp4IbAUPBaW
TIvf5p9oxw+AjrMtTtcdSiee1S6CvMMaHhVD7SI6qGA8GqwaXueeLuEXa0Ok
BWlehx8wibMi4a9fLcQZtzJkmGhR1WzXcJfiEg32srILwIzPQYxuFdZZ2elb
gYp/bMEIp4LKhi43IyM6peCDHDzEba8NuOSd0heEqFIm0vlXujMhkyMUvDBv
H0V5On4aMuw/aSEKcAdbazppOru/W1ndyFa5ZHQIC19g72ZaDVyYjPyvNgOV
AFqO4o3IbC5z31zMlTtMbAq2RG9svwUVejn0tmF6UPluTe0U1NuXFpLK6TCH
wqocLz4ecptfJQulpYjClVLgzaYGDuKwQpIwPWg5G/DtKSCGNtEkfqB3aemH
V5xmoYm1v5CQZAEvvsrLA6jxCk9lzqYV8QMivWNXUG+mneIEM35G0HOPzXca
LLyB+N8Zxioc9DPGfdbcxXuVgOKRepbkq4xv1pUpMQ4BUmlkejDRSP+5SIR3
iEthg+FU6GRSQbORE6nhrKjGBk8fpNpozQZVc2VySUTCwHIEEAEIACYFAlRJ
bc8GCwkIBwMCCRA+tiWe3yHfJAQVCAIKAxYCAQIbAwIeAQAA9J0H/RLR/Uwt
CakrPKtfeGaNuOI45SRTNxM8TklC6tM28sJSzkX8qKPzvI1PxyLhs/i0/fCQ
7Z5bU6n41oLuqUt2S9vy+ABlChKAeziOqCHUcMzHOtbKiPkKW88aO687nx+A
ol2XOnMTkVIC+edMUgnKp6tKtZnbO4ea6Cg88TFuli4hLHNXTfCECswuxHOc
AO1OKDRrCd08iPI5CLNCIV60QnduitE1vF6ehgrH25Vl6LEdd8vPVlTYAvsa
6ySk2RIrHNLUZZ3iII3MBFL8HyINp/XA1BQP+QbH801uSLq8agxM4iFT9C+O
D147SawUGhjD5RG7T+YtqItzgA1V9l277EXHwwYEVEltzwEIAJD57uX6bOc4
Tgf3utfL/4hdyoqIMVHkYQOvE27wPsZxX08QsdlaNeGji9Ap2ifIDuckUqn6
Ji9jtZDKtOzdTBm6rnG5nPmkn6BJXPhnecQRP8N0XBISnAGmE4t+bxtts5Wb
qeMdxJYqMiGqzrLBRJEIDTcg3+QF2Y3RywOqlcXqgG/xX++PsvR1Jiz0rEVP
TcBc7ytyb/Av7mx1S802HRYGJHOFtVLoPTrtPCvv+DRDK8JzxQW2XSQLlI0M
9s1tmYhCogYIIqKx9qOTd5mFJ1hJlL6i9xDkvE21qPFASFtww5tiYmUfFaxI
LwbXPZlQ1I/8fuaUdOxctQ+g40ZgHPcAEQEAAf4JAwgdUg8ubE2BT2DITBD+
XFgjrnUlQBilbN8/do/36KHuImSPO/GGLzKh4+oXxrvLc5fQLjeO+bzeen4u
COCBRO0hG7KpJPhQ6+T02uEF6LegE1sEz5hp6BpKUdPZ1+8799Rylb5kubC5
IKnLqqpGDbH3hIsmSV3CG/ESkaGMLc/K0ZPt1JRWtUQ9GesXT0v6fdM5GB/L
cZWFdDoYgZAw5BtymE44knIodfDAYJ4DHnPCh/oilWe1qVTQcNMdtkpBgkuo
THecqEmiODQz5EX8pVmS596XsnPO299Lo3TbaHUQo7EC6Au1Au9+b5hC1pDa
FVCLcproi/Cgch0B/NOCFkVLYmp6BEljRj2dSZRWbO0vgl9kFmJEeiiH41+k
EAI6PASSKZs3BYLFc2I8mBkcvt90kg4MTBjreuk0uWf1hdH2Rv8zprH4h5Uh
gjx5nUDX8WXyeLxTU5EBKry+A2DIe0Gm0/waxp6lBlUl+7ra28KYEoHm8Nq/
N9FCuEhFkFgw6EwUp7jsrFcqBKvmni6jyplm+mJXi3CK+IiNcqub4XPnBI97
lR19fupB/Y6M7yEaxIM8fTQXmP+x/fe8zRphdo+7o+pJQ3hk5LrrNPK8GEZ6
DLDOHjZzROhOgBvWtbxRktHk+f5YpuQL+xWd33IV1xYSSHuoAm0Zwt0QJxBs
oFBwJEq1NWM4FxXJBogvzV7KFhl/hXgtvx+GaMv3y8gucj+gE89xVv0XBXjl
5dy5/PgCI0Id+KAFHyKpJA0N0h8O4xdJoNyIBAwDZ8LHt0vlnLGwcJFR9X7/
PfWe0PFtC3d7cYY3RopDhnRP7MZs1Wo9nZ4IvlXoEsE2nPkWcns+Wv5Yaewr
s2ra9ZIK7IIJhqKKgmQtCeiXyFwTq+kfunDnxeCavuWL3HuLKIOZf7P9vXXt
XgEir9rCwF8EGAEIABMFAlRJbdIJED62JZ7fId8kAhsMAAD+LAf+KT1EpkwH
0ivTHmYako+6qG6DCtzd3TibWw51cmbY20Ph13NIS/MfBo828S9SXm/sVUzN
/r7qZgZYfI0/j57tG3BguVGm53qya4bINKyi1RjK6aKo/rrzRkh5ZVD5rVNO
E2zzvyYAnLUWG9AV1OYDxcgLrXqEMWlqZAo+Wmg7VrTBmdCGs/BPvscNgQRr
6Gpjgmv9ru6LjRL7vFhEcov/tkBLj+CtaWWFTd1s2vBLOs4rCsD9TT/23vfw
CnokvvVjKYN5oviy61yhpqF1rWlOsxZ4+2sKW3Pq7JLBtmzsZegTONfcQAf7
qqGRQm3MxoTdgQUShAwbNwNNQR9cInfMnA==
=2wIY
-----END PGP PRIVATE KEY BLOCK-----
`
) )
var testPrivateKeyRing *crypto.KeyRing
func init() {
privKey, err := crypto.NewKeyFromArmored(testPrivateKey)
if err != nil {
panic(err)
}
privKeyUnlocked, err := privKey.Unlock([]byte(testPrivateKeyPassword))
if err != nil {
panic(err)
}
if testPrivateKeyRing, err = crypto.NewKeyRing(privKeyUnlocked); err != nil {
panic(err)
}
}
type mocksForStore struct { type mocksForStore struct {
tb testing.TB tb testing.TB
@ -48,7 +134,6 @@ type mocksForStore struct {
events *storemocks.MockListener events *storemocks.MockListener
user *storemocks.MockBridgeUser user *storemocks.MockBridgeUser
client *pmapimocks.MockClient client *pmapimocks.MockClient
clientManager *storemocks.MockClientManager
panicHandler *storemocks.MockPanicHandler panicHandler *storemocks.MockPanicHandler
changeNotifier *storemocks.MockChangeNotifier changeNotifier *storemocks.MockChangeNotifier
store *Store store *Store
@ -65,7 +150,6 @@ func initMocks(tb testing.TB) (*mocksForStore, func()) {
events: storemocks.NewMockListener(ctrl), events: storemocks.NewMockListener(ctrl),
user: storemocks.NewMockBridgeUser(ctrl), user: storemocks.NewMockBridgeUser(ctrl),
client: pmapimocks.NewMockClient(ctrl), client: pmapimocks.NewMockClient(ctrl),
clientManager: storemocks.NewMockClientManager(ctrl),
panicHandler: storemocks.NewMockPanicHandler(ctrl), panicHandler: storemocks.NewMockPanicHandler(ctrl),
changeNotifier: storemocks.NewMockChangeNotifier(ctrl), changeNotifier: storemocks.NewMockChangeNotifier(ctrl),
} }
@ -97,30 +181,30 @@ func (mocks *mocksForStore) newStoreNoEvents(combinedMode bool, msgs ...*pmapi.M
mocks.user.EXPECT().IsConnected().Return(true) mocks.user.EXPECT().IsConnected().Return(true)
mocks.user.EXPECT().IsCombinedAddressMode().Return(combinedMode) mocks.user.EXPECT().IsCombinedAddressMode().Return(combinedMode)
mocks.clientManager.EXPECT().GetClient("userID").AnyTimes().Return(mocks.client) mocks.user.EXPECT().GetClient().AnyTimes().Return(mocks.client)
mocks.client.EXPECT().Addresses().Return(pmapi.AddressList{ mocks.client.EXPECT().Addresses().Return(pmapi.AddressList{
{ID: addrID1, Email: addr1, Type: pmapi.OriginalAddress, Receive: pmapi.CanReceive}, {ID: addrID1, Email: addr1, Type: pmapi.OriginalAddress, Receive: true},
{ID: addrID2, Email: addr2, Type: pmapi.AliasAddress, Receive: pmapi.CanReceive}, {ID: addrID2, Email: addr2, Type: pmapi.AliasAddress, Receive: true},
}) })
mocks.client.EXPECT().ListLabels().AnyTimes() mocks.client.EXPECT().ListLabels(gomock.Any()).AnyTimes()
mocks.client.EXPECT().CountMessages("") mocks.client.EXPECT().CountMessages(gomock.Any(), "")
// Call to get latest event ID and then to process first event. // Call to get latest event ID and then to process first event.
eventAfterSyncRequested := make(chan struct{}) eventAfterSyncRequested := make(chan struct{})
mocks.client.EXPECT().GetEvent("").Return(&pmapi.Event{ mocks.client.EXPECT().GetEvent(gomock.Any(), "").Return(&pmapi.Event{
EventID: "firstEventID", EventID: "firstEventID",
}, nil) }, nil)
mocks.client.EXPECT().GetEvent("firstEventID").DoAndReturn(func(_ string) (*pmapi.Event, error) { mocks.client.EXPECT().GetEvent(gomock.Any(), "firstEventID").DoAndReturn(func(_ context.Context, _ string) (*pmapi.Event, error) {
close(eventAfterSyncRequested) close(eventAfterSyncRequested)
return &pmapi.Event{ return &pmapi.Event{
EventID: "latestEventID", EventID: "latestEventID",
}, nil }, nil
}) })
mocks.client.EXPECT().ListMessages(gomock.Any()).Return(msgs, len(msgs), nil).AnyTimes() mocks.client.EXPECT().ListMessages(gomock.Any(), gomock.Any()).Return(msgs, len(msgs), nil).AnyTimes()
for _, msg := range msgs { for _, msg := range msgs {
mocks.client.EXPECT().GetMessage(msg.ID).Return(msg, nil).AnyTimes() mocks.client.EXPECT().GetMessage(gomock.Any(), msg.ID).Return(msg, nil).AnyTimes()
} }
var err error var err error
@ -128,7 +212,6 @@ func (mocks *mocksForStore) newStoreNoEvents(combinedMode bool, msgs ...*pmapi.M
nil, // Sentry reporter is not used under unit tests. nil, // Sentry reporter is not used under unit tests.
mocks.panicHandler, mocks.panicHandler,
mocks.user, mocks.user,
mocks.clientManager,
mocks.events, mocks.events,
filepath.Join(mocks.tmpDir, "mailbox-test.db"), filepath.Join(mocks.tmpDir, "mailbox-test.db"),
mocks.cache, mocks.cache,

View File

@ -90,10 +90,7 @@ func (store *Store) TestDumpDB(tb assert.TestingT) {
return err return err
} }
} }
if err := txMails(tx); err != nil { return txMails(tx)
return err
}
return nil
} }
assert.NoError(tb, store.db.View(txDump)) assert.NoError(tb, store.db.View(txDump))

View File

@ -18,6 +18,7 @@
package store package store
import ( import (
"context"
"math" "math"
"sync" "sync"
@ -39,10 +40,10 @@ type storeSynchronizer interface {
} }
type messageLister interface { type messageLister interface {
ListMessages(*pmapi.MessagesFilter) ([]*pmapi.Message, int, error) ListMessages(context.Context, *pmapi.MessagesFilter) ([]*pmapi.Message, int, error)
} }
func syncAllMail(panicHandler PanicHandler, store storeSynchronizer, api func() messageLister, syncState *syncState) error { func syncAllMail(panicHandler PanicHandler, store storeSynchronizer, api messageLister, syncState *syncState) error {
labelID := pmapi.AllMailLabel labelID := pmapi.AllMailLabel
// When the full sync starts (i.e. is not already in progress), we need to load // When the full sync starts (i.e. is not already in progress), we need to load
@ -53,7 +54,7 @@ func syncAllMail(panicHandler PanicHandler, store storeSynchronizer, api func()
return errors.Wrap(err, "failed to load message IDs") return errors.Wrap(err, "failed to load message IDs")
} }
if err := findIDRanges(labelID, api(), syncState); err != nil { if err := findIDRanges(labelID, api, syncState); err != nil {
return errors.Wrap(err, "failed to load IDs ranges") return errors.Wrap(err, "failed to load IDs ranges")
} }
syncState.save() syncState.save()
@ -71,7 +72,7 @@ func syncAllMail(panicHandler PanicHandler, store storeSynchronizer, api func()
defer panicHandler.HandlePanic() defer panicHandler.HandlePanic()
defer wg.Done() defer wg.Done()
err := syncBatch(labelID, store, api(), syncState, idRange, &shouldStop) err := syncBatch(labelID, store, api, syncState, idRange, &shouldStop)
if err != nil { if err != nil {
shouldStop = 1 shouldStop = 1
resultError = errors.Wrap(err, "failed to sync group") resultError = errors.Wrap(err, "failed to sync group")
@ -147,7 +148,7 @@ func getSplitIDAndCount(labelID string, api messageLister, page int) (string, in
Limit: 1, Limit: 1,
} }
// If the page does not exist, an empty page instead of an error is returned. // If the page does not exist, an empty page instead of an error is returned.
messages, total, err := api.ListMessages(filter) messages, total, err := api.ListMessages(context.Background(), filter)
if err != nil { if err != nil {
return "", 0, errors.Wrap(err, "failed to list messages") return "", 0, errors.Wrap(err, "failed to list messages")
} }
@ -189,7 +190,7 @@ func syncBatch( //nolint[funlen]
log.WithField("begin", filter.BeginID).WithField("end", filter.EndID).Debug("Fetching page") log.WithField("begin", filter.BeginID).WithField("end", filter.EndID).Debug("Fetching page")
messages, _, err := api.ListMessages(filter) messages, _, err := api.ListMessages(context.Background(), filter)
if err != nil { if err != nil {
return errors.Wrap(err, "failed to list messages") return errors.Wrap(err, "failed to list messages")
} }

View File

@ -18,6 +18,7 @@
package store package store
import ( import (
"context"
"sort" "sort"
"strconv" "strconv"
"sync" "sync"
@ -34,7 +35,7 @@ type mockLister struct {
messageIDs []string messageIDs []string
} }
func (m *mockLister) ListMessages(filter *pmapi.MessagesFilter) (msgs []*pmapi.Message, total int, err error) { func (m *mockLister) ListMessages(_ context.Context, filter *pmapi.MessagesFilter) (msgs []*pmapi.Message, total int, err error) {
if m.err != nil { if m.err != nil {
return nil, 0, m.err return nil, 0, m.err
} }
@ -197,7 +198,7 @@ func TestSyncAllMail(t *testing.T) { //nolint[funlen]
syncState := newSyncState(store, 0, tc.idRanges, tc.idsToBeDeleted) syncState := newSyncState(store, 0, tc.idRanges, tc.idsToBeDeleted)
err := syncAllMail(m.panicHandler, store, func() messageLister { return api }, syncState) err := syncAllMail(m.panicHandler, store, api, syncState)
require.Nil(t, err) require.Nil(t, err)
// Check all messages were created or updated. // Check all messages were created or updated.
@ -245,7 +246,7 @@ func TestSyncAllMail_FailedListing(t *testing.T) {
} }
syncState := newTestSyncState(store) syncState := newTestSyncState(store)
err := syncAllMail(m.panicHandler, store, func() messageLister { return api }, syncState) err := syncAllMail(m.panicHandler, store, api, syncState)
require.EqualError(t, err, "failed to sync group: failed to list messages: error") require.EqualError(t, err, "failed to sync group: failed to list messages: error")
} }
@ -264,7 +265,7 @@ func TestSyncAllMail_FailedCreateOrUpdateMessage(t *testing.T) {
} }
syncState := newTestSyncState(store) syncState := newTestSyncState(store)
err := syncAllMail(m.panicHandler, store, func() messageLister { return api }, syncState) err := syncAllMail(m.panicHandler, store, api, syncState)
require.EqualError(t, err, "failed to sync group: failed to create or update messages: error") require.EqualError(t, err, "failed to sync group: failed to create or update messages: error")
} }

View File

@ -17,16 +17,16 @@
package store package store
import "github.com/ProtonMail/proton-bridge/pkg/pmapi" import (
"context"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
)
type PanicHandler interface { type PanicHandler interface {
HandlePanic() HandlePanic()
} }
type ClientManager interface {
GetClient(userID string) pmapi.Client
}
// BridgeUser is subset of bridge.User for use by the Store. // BridgeUser is subset of bridge.User for use by the Store.
type BridgeUser interface { type BridgeUser interface {
ID() string ID() string
@ -35,7 +35,8 @@ type BridgeUser interface {
IsCombinedAddressMode() bool IsCombinedAddressMode() bool
GetPrimaryAddress() string GetPrimaryAddress() string
GetStoreAddresses() []string GetStoreAddresses() []string
UpdateUser() error GetClient() pmapi.Client
UpdateUser(context.Context) error
CloseAllConnections() CloseAllConnections()
CloseConnection(string) CloseConnection(string)
Logout() error Logout() error

View File

@ -24,7 +24,7 @@ func (store *Store) UserID() string {
// GetSpace returns used and total space in bytes. // GetSpace returns used and total space in bytes.
func (store *Store) GetSpace() (usedSpace, maxSpace uint, err error) { func (store *Store) GetSpace() (usedSpace, maxSpace uint, err error) {
apiUser, err := store.client().CurrentUser() apiUser, err := store.client().CurrentUser(exposeContextForIMAP())
if err != nil { if err != nil {
return 0, 0, err return 0, 0, err
} }
@ -33,7 +33,7 @@ func (store *Store) GetSpace() (usedSpace, maxSpace uint, err error) {
// GetMaxUpload returns max size of message + all attachments in bytes. // GetMaxUpload returns max size of message + all attachments in bytes.
func (store *Store) GetMaxUpload() (int64, error) { func (store *Store) GetMaxUpload() (int64, error) {
apiUser, err := store.client().CurrentUser() apiUser, err := store.client().CurrentUser(exposeContextForIMAP())
if err != nil { if err != nil {
return 0, err return 0, err
} }

View File

@ -147,7 +147,7 @@ func (store *Store) createOrUpdateAddressInfo(addressList pmapi.AddressList) (er
// filterAddresses filters out inactive addresses and ensures the original address is listed first. // filterAddresses filters out inactive addresses and ensures the original address is listed first.
func filterAddresses(addressList pmapi.AddressList) (filteredList pmapi.AddressList) { func filterAddresses(addressList pmapi.AddressList) (filteredList pmapi.AddressList) {
for _, address := range addressList { for _, address := range addressList {
if address.Receive != pmapi.CanReceive { if !address.Receive {
continue continue
} }

View File

@ -38,14 +38,14 @@ func (store *Store) createMailbox(name string) error {
color := store.leastUsedColor() color := store.leastUsedColor()
var exclusive int var exclusive bool
switch { switch {
case strings.HasPrefix(name, UserLabelsPrefix): case strings.HasPrefix(name, UserLabelsPrefix):
name = strings.TrimPrefix(name, UserLabelsPrefix) name = strings.TrimPrefix(name, UserLabelsPrefix)
exclusive = 0 exclusive = false
case strings.HasPrefix(name, UserFoldersPrefix): case strings.HasPrefix(name, UserFoldersPrefix):
name = strings.TrimPrefix(name, UserFoldersPrefix) name = strings.TrimPrefix(name, UserFoldersPrefix)
exclusive = 1 exclusive = true
default: default:
// Ideally we would throw an error here, but then Outlook for // Ideally we would throw an error here, but then Outlook for
// macOS keeps trying to make an IMAP Drafts folder and popping // macOS keeps trying to make an IMAP Drafts folder and popping
@ -55,10 +55,10 @@ func (store *Store) createMailbox(name string) error {
return nil return nil
} }
_, err := store.client().CreateLabel(&pmapi.Label{ _, err := store.client().CreateLabel(exposeContextForIMAP(), &pmapi.Label{
Name: name, Name: name,
Color: color, Color: color,
Exclusive: exclusive, Exclusive: pmapi.Boolean(exclusive),
Type: pmapi.LabelTypeMailbox, Type: pmapi.LabelTypeMailbox,
}) })
return err return err
@ -125,7 +125,7 @@ func (store *Store) leastUsedColor() string {
func (store *Store) updateMailbox(labelID, newName, color string) error { func (store *Store) updateMailbox(labelID, newName, color string) error {
defer store.eventLoop.pollNow() defer store.eventLoop.pollNow()
_, err := store.client().UpdateLabel(&pmapi.Label{ _, err := store.client().UpdateLabel(exposeContextForIMAP(), &pmapi.Label{
ID: labelID, ID: labelID,
Name: newName, Name: newName,
Color: color, Color: color,
@ -142,15 +142,15 @@ func (store *Store) deleteMailbox(labelID, addressID string) error {
var err error var err error
switch labelID { switch labelID {
case pmapi.SpamLabel: case pmapi.SpamLabel:
err = store.client().EmptyFolder(pmapi.SpamLabel, addressID) err = store.client().EmptyFolder(exposeContextForIMAP(), pmapi.SpamLabel, addressID)
case pmapi.TrashLabel: case pmapi.TrashLabel:
err = store.client().EmptyFolder(pmapi.TrashLabel, addressID) err = store.client().EmptyFolder(exposeContextForIMAP(), pmapi.TrashLabel, addressID)
default: default:
err = fmt.Errorf("cannot empty mailbox %v", labelID) err = fmt.Errorf("cannot empty mailbox %v", labelID)
} }
return err return err
} }
return store.client().DeleteLabel(labelID) return store.client().DeleteLabel(exposeContextForIMAP(), labelID)
} }
func (store *Store) createLabelsIfMissing(affectedLabelIDs map[string]bool) error { func (store *Store) createLabelsIfMissing(affectedLabelIDs map[string]bool) error {
@ -165,7 +165,7 @@ func (store *Store) createLabelsIfMissing(affectedLabelIDs map[string]bool) erro
return nil return nil
} }
labels, err := store.client().ListLabels() labels, err := store.client().ListLabels(exposeContextForIMAP())
if err != nil { if err != nil {
return err return err
} }

View File

@ -44,45 +44,132 @@ func (store *Store) CreateDraft(
attachedPublicKey, attachedPublicKey,
attachedPublicKeyName string, attachedPublicKeyName string,
parentID string) (*pmapi.Message, []*pmapi.Attachment, error) { parentID string) (*pmapi.Message, []*pmapi.Attachment, error) {
defer store.eventLoop.pollNow() attachments := store.prepareDraftAttachments(message, attachmentReaders, attachedPublicKey, attachedPublicKeyName)
// Since this is a draft, we don't need to sign it. if err := encryptDraft(kr, message, attachments); err != nil {
if err := message.Encrypt(kr, nil); err != nil {
return nil, nil, errors.Wrap(err, "failed to encrypt draft") return nil, nil, errors.Wrap(err, "failed to encrypt draft")
} }
attachments := message.Attachments if ok, err := store.checkDraftTotalSize(message, attachments); err != nil {
message.Attachments = nil return nil, nil, err
} else if !ok {
return nil, nil, errors.New("message is too large")
}
draftAction := store.getDraftAction(message) draftAction := store.getDraftAction(message)
draft, err := store.client().CreateDraft(message, parentID, draftAction) draft, err := store.client().CreateDraft(exposeContextForSMTP(), message, parentID, draftAction)
if err != nil { if err != nil {
return nil, nil, errors.Wrap(err, "failed to create draft") return nil, nil, errors.Wrap(err, "failed to create draft")
} }
// Do poll only when call to API succeeded.
defer store.eventLoop.pollNow()
createdAttachments := []*pmapi.Attachment{}
for _, att := range attachments {
att.attachment.MessageID = draft.ID
createdAttachment, err := store.client().CreateAttachment(exposeContextForSMTP(), att.attachment, att.encReader, att.sigReader)
if err != nil {
return nil, nil, errors.Wrap(err, "failed to create attachment")
}
createdAttachments = append(createdAttachments, createdAttachment)
}
return draft, createdAttachments, nil
}
type draftAttachment struct {
attachment *pmapi.Attachment
reader io.Reader
sigReader io.Reader
encReader io.Reader
}
func (store *Store) prepareDraftAttachments(
message *pmapi.Message,
attachmentReaders []io.Reader,
attachedPublicKey,
attachedPublicKeyName string) []*draftAttachment {
attachments := []*draftAttachment{}
for idx, attachment := range message.Attachments {
attachments = append(attachments, &draftAttachment{
attachment: attachment,
reader: attachmentReaders[idx],
})
}
message.Attachments = nil
if attachedPublicKey != "" { if attachedPublicKey != "" {
attachmentReaders = append(attachmentReaders, strings.NewReader(attachedPublicKey))
publicKeyAttachment := &pmapi.Attachment{ publicKeyAttachment := &pmapi.Attachment{
Name: attachedPublicKeyName + ".asc", Name: attachedPublicKeyName + ".asc",
MIMEType: "application/pgp-keys", MIMEType: "application/pgp-keys",
Header: textproto.MIMEHeader{}, Header: textproto.MIMEHeader{},
} }
attachments = append(attachments, publicKeyAttachment) attachments = append(attachments, &draftAttachment{
attachment: publicKeyAttachment,
reader: strings.NewReader(attachedPublicKey),
})
} }
for idx, attachment := range attachments { return attachments
attachment.MessageID = draft.ID }
attachmentBody, _ := ioutil.ReadAll(attachmentReaders[idx])
createdAttachment, err := store.createAttachment(kr, attachment, attachmentBody) func encryptDraft(kr *crypto.KeyRing, message *pmapi.Message, attachments []*draftAttachment) error {
// Since this is a draft, we don't need to sign it.
if err := message.Encrypt(kr, nil); err != nil {
return errors.Wrap(err, "failed to encrypt message")
}
for _, att := range attachments {
attachment := att.attachment
attachmentBody, err := ioutil.ReadAll(att.reader)
if err != nil { if err != nil {
return nil, nil, errors.Wrap(err, "failed to create attachment for draft") return errors.Wrap(err, "failed to read attachment")
} }
attachments[idx] = createdAttachment r := bytes.NewReader(attachmentBody)
sigReader, err := attachment.DetachedSign(kr, r)
if err != nil {
return errors.Wrap(err, "failed to sign attachment")
}
att.sigReader = sigReader
r = bytes.NewReader(attachmentBody)
encReader, err := attachment.Encrypt(kr, r)
if err != nil {
return errors.Wrap(err, "failed to encrypt attachment")
}
att.encReader = encReader
att.reader = nil
}
return nil
}
func (store *Store) checkDraftTotalSize(message *pmapi.Message, attachments []*draftAttachment) (bool, error) {
maxUpload, err := store.GetMaxUpload()
if err != nil {
return false, err
} }
return draft, attachments, nil msgSize := message.Size
if msgSize == 0 {
msgSize = int64(len(message.Body))
}
var attSize int64
for _, att := range attachments {
b, err := ioutil.ReadAll(att.encReader)
if err != nil {
return false, err
}
attSize += int64(len(b))
att.encReader = bytes.NewBuffer(b)
}
return msgSize+attSize <= maxUpload, nil
} }
func (store *Store) getDraftAction(message *pmapi.Message) int { func (store *Store) getDraftAction(message *pmapi.Message) int {
@ -93,31 +180,10 @@ func (store *Store) getDraftAction(message *pmapi.Message) int {
return pmapi.DraftActionReply return pmapi.DraftActionReply
} }
func (store *Store) createAttachment(kr *crypto.KeyRing, attachment *pmapi.Attachment, attachmentBody []byte) (*pmapi.Attachment, error) {
r := bytes.NewReader(attachmentBody)
sigReader, err := attachment.DetachedSign(kr, r)
if err != nil {
return nil, errors.Wrap(err, "failed to sign attachment")
}
r = bytes.NewReader(attachmentBody)
encReader, err := attachment.Encrypt(kr, r)
if err != nil {
return nil, errors.Wrap(err, "failed to encrypt attachment")
}
createdAttachment, err := store.client().CreateAttachment(attachment, encReader, sigReader)
if err != nil {
return nil, errors.Wrap(err, "failed to create attachment")
}
return createdAttachment, nil
}
// SendMessage sends the message. // SendMessage sends the message.
func (store *Store) SendMessage(messageID string, req *pmapi.SendMessageReq) error { func (store *Store) SendMessage(messageID string, req *pmapi.SendMessageReq) error {
defer store.eventLoop.pollNow() defer store.eventLoop.pollNow()
_, _, err := store.client().SendMessage(messageID, req) _, _, err := store.client().SendMessage(exposeContextForSMTP(), messageID, req)
return err return err
} }
@ -289,7 +355,7 @@ func clearNonMetadata(onlyMeta *pmapi.Message) {
// If there is stored message in metaBucket the size, header and MIMEType are // If there is stored message in metaBucket the size, header and MIMEType are
// not changed if already set. To change these: // not changed if already set. To change these:
// * size must be updated by Message.SetSize // * size must be updated by Message.SetSize
// * contentType and header must be updated by Message.SetContentTypeAndHeader // * contentType and header must be updated by Message.SetContentTypeAndHeader.
func txUpdateMetadaFromDB(metaBucket *bolt.Bucket, onlyMeta *pmapi.Message, log *logrus.Entry) { func txUpdateMetadaFromDB(metaBucket *bolt.Bucket, onlyMeta *pmapi.Message, log *logrus.Entry) {
// Size attribute on the server is counting encrypted data. We need to compute // Size attribute on the server is counting encrypted data. We need to compute
// "real" size of decrypted data. Negative values will be processed during fetch. // "real" size of decrypted data. Negative values will be processed during fetch.

View File

@ -18,10 +18,13 @@
package store package store
import ( import (
"io"
"net/mail" "net/mail"
"strings"
"testing" "testing"
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/golang/mock/gomock"
a "github.com/stretchr/testify/assert" a "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -32,10 +35,10 @@ func TestGetAllMessageIDs(t *testing.T) {
m.newStoreNoEvents(true) m.newStoreNoEvents(true)
insertMessage(t, m, "msg1", "Test message 1", addrID1, 0, []string{pmapi.AllMailLabel, pmapi.InboxLabel}) insertMessage(t, m, "msg1", "Test message 1", addrID1, false, []string{pmapi.AllMailLabel, pmapi.InboxLabel})
insertMessage(t, m, "msg2", "Test message 2", addrID1, 0, []string{pmapi.AllMailLabel, pmapi.ArchiveLabel}) insertMessage(t, m, "msg2", "Test message 2", addrID1, false, []string{pmapi.AllMailLabel, pmapi.ArchiveLabel})
insertMessage(t, m, "msg3", "Test message 3", addrID1, 0, []string{pmapi.AllMailLabel, pmapi.InboxLabel}) insertMessage(t, m, "msg3", "Test message 3", addrID1, false, []string{pmapi.AllMailLabel, pmapi.InboxLabel})
insertMessage(t, m, "msg4", "Test message 4", addrID1, 0, []string{}) insertMessage(t, m, "msg4", "Test message 4", addrID1, false, []string{})
checkAllMessageIDs(t, m, []string{"msg1", "msg2", "msg3", "msg4"}) checkAllMessageIDs(t, m, []string{"msg1", "msg2", "msg3", "msg4"})
} }
@ -45,7 +48,7 @@ func TestGetMessageFromDB(t *testing.T) {
defer clear() defer clear()
m.newStoreNoEvents(true) m.newStoreNoEvents(true)
insertMessage(t, m, "msg1", "Test message 1", addrID1, 0, []string{pmapi.AllMailLabel}) insertMessage(t, m, "msg1", "Test message 1", addrID1, false, []string{pmapi.AllMailLabel})
tests := []struct{ msgID, wantErr string }{ tests := []struct{ msgID, wantErr string }{
{"msg1", ""}, {"msg1", ""},
@ -70,7 +73,7 @@ func TestCreateOrUpdateMessageMetadata(t *testing.T) {
defer clear() defer clear()
m.newStoreNoEvents(true) m.newStoreNoEvents(true)
insertMessage(t, m, "msg1", "Test message 1", addrID1, 0, []string{pmapi.AllMailLabel}) insertMessage(t, m, "msg1", "Test message 1", addrID1, false, []string{pmapi.AllMailLabel})
msg, err := m.store.getMessageFromDB("msg1") msg, err := m.store.getMessageFromDB("msg1")
require.Nil(t, err) require.Nil(t, err)
@ -102,7 +105,7 @@ func TestCreateOrUpdateMessageMetadata(t *testing.T) {
a.Equal(t, wantHeader, msg.Header) a.Equal(t, wantHeader, msg.Header)
// Check calculated data are not overridden by reinsert. // Check calculated data are not overridden by reinsert.
insertMessage(t, m, "msg1", "Test message 1", addrID1, 0, []string{pmapi.AllMailLabel}) insertMessage(t, m, "msg1", "Test message 1", addrID1, false, []string{pmapi.AllMailLabel})
msg, err = m.store.getMessageFromDB("msg1") msg, err = m.store.getMessageFromDB("msg1")
require.Nil(t, err) require.Nil(t, err)
@ -116,8 +119,8 @@ func TestDeleteMessage(t *testing.T) {
defer clear() defer clear()
m.newStoreNoEvents(true) m.newStoreNoEvents(true)
insertMessage(t, m, "msg1", "Test message 1", addrID1, 0, []string{pmapi.AllMailLabel}) insertMessage(t, m, "msg1", "Test message 1", addrID1, false, []string{pmapi.AllMailLabel})
insertMessage(t, m, "msg2", "Test message 2", addrID1, 0, []string{pmapi.AllMailLabel}) insertMessage(t, m, "msg2", "Test message 2", addrID1, false, []string{pmapi.AllMailLabel})
require.Nil(t, m.store.deleteMessageEvent("msg1")) require.Nil(t, m.store.deleteMessageEvent("msg1"))
@ -125,17 +128,17 @@ func TestDeleteMessage(t *testing.T) {
checkMailboxMessageIDs(t, m, pmapi.AllMailLabel, []wantID{{"msg2", 2}}) checkMailboxMessageIDs(t, m, pmapi.AllMailLabel, []wantID{{"msg2", 2}})
} }
func insertMessage(t *testing.T, m *mocksForStore, id, subject, sender string, unread int, labelIDs []string) { //nolint[unparam] func insertMessage(t *testing.T, m *mocksForStore, id, subject, sender string, unread bool, labelIDs []string) { //nolint[unparam]
msg := getTestMessage(id, subject, sender, unread, labelIDs) msg := getTestMessage(id, subject, sender, unread, labelIDs)
require.Nil(t, m.store.createOrUpdateMessageEvent(msg)) require.Nil(t, m.store.createOrUpdateMessageEvent(msg))
} }
func getTestMessage(id, subject, sender string, unread int, labelIDs []string) *pmapi.Message { func getTestMessage(id, subject, sender string, unread bool, labelIDs []string) *pmapi.Message {
address := &mail.Address{Address: sender} address := &mail.Address{Address: sender}
return &pmapi.Message{ return &pmapi.Message{
ID: id, ID: id,
Subject: subject, Subject: subject,
Unread: unread, Unread: pmapi.Boolean(unread),
Sender: address, Sender: address,
ToList: []*mail.Address{address}, ToList: []*mail.Address{address},
LabelIDs: labelIDs, LabelIDs: labelIDs,
@ -154,3 +157,47 @@ func checkAllMessageIDs(t *testing.T, m *mocksForStore, wantIDs []string) {
require.Nil(t, allErr) require.Nil(t, allErr)
require.Equal(t, wantIDs, allIds) require.Equal(t, wantIDs, allIds)
} }
func TestCreateDraftCheckMessageSize(t *testing.T) {
m, clear := initMocks(t)
defer clear()
m.newStoreNoEvents(false)
m.client.EXPECT().CurrentUser(gomock.Any()).Return(&pmapi.User{
MaxUpload: 100, // Decrypted message 5 chars, encrypted 500+.
}, nil)
// Even small body is bloated to at least about 500 chars of basic pgp message.
message := &pmapi.Message{
Body: strings.Repeat("a", 5),
}
attachmentReaders := []io.Reader{}
_, _, err := m.store.CreateDraft(testPrivateKeyRing, message, attachmentReaders, "", "", "")
require.EqualError(t, err, "message is too large")
}
func TestCreateDraftCheckMessageWithAttachmentSize(t *testing.T) {
m, clear := initMocks(t)
defer clear()
m.newStoreNoEvents(false)
m.client.EXPECT().CurrentUser(gomock.Any()).Return(&pmapi.User{
MaxUpload: 800, // Decrypted message 5 chars + 5 chars of attachment, encrypted 500+ + 300+.
}, nil)
// Even small body is bloated to at least about 500 chars of basic pgp message.
message := &pmapi.Message{
Body: strings.Repeat("a", 5),
Attachments: []*pmapi.Attachment{
{Name: "name"},
},
}
// Even small attachment is bloated to about 300 chars of encrypted text.
attachmentReaders := []io.Reader{
strings.NewReader(strings.Repeat("b", 5)),
}
_, _, err := m.store.CreateDraft(testPrivateKeyRing, message, attachmentReaders, "", "", "")
require.EqualError(t, err, "message is too large")
}

View File

@ -18,6 +18,7 @@
package store package store
import ( import (
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"strconv" "strconv"
@ -34,7 +35,7 @@ const syncIDsToBeDeletedKey = "ids_to_be_deleted"
// updateCountsFromServer will download and set the counts. // updateCountsFromServer will download and set the counts.
func (store *Store) updateCountsFromServer() error { func (store *Store) updateCountsFromServer() error {
counts, err := store.client().CountMessages("") counts, err := store.client().CountMessages(context.Background(), "")
if err != nil { if err != nil {
return errors.Wrap(err, "cannot update counts from server") return errors.Wrap(err, "cannot update counts from server")
} }
@ -152,7 +153,7 @@ func (store *Store) triggerSync() {
store.log.WithField("isIncomplete", syncState.isIncomplete()).Info("Store sync started") store.log.WithField("isIncomplete", syncState.isIncomplete()).Info("Store sync started")
err := syncAllMail(store.panicHandler, store, func() messageLister { return store.client() }, syncState) err := syncAllMail(store.panicHandler, store, store.client(), syncState)
if err != nil { if err != nil {
log.WithError(err).Error("Store sync failed") log.WithError(err).Error("Store sync failed")
store.syncCooldown.increaseWaitTime() store.syncCooldown.increaseWaitTime()

View File

@ -31,8 +31,8 @@ func TestLoadSaveSyncState(t *testing.T) {
defer clear() defer clear()
m.newStoreNoEvents(true) m.newStoreNoEvents(true)
insertMessage(t, m, "msg1", "Test message 1", addrID1, 0, []string{pmapi.AllMailLabel, pmapi.InboxLabel}) insertMessage(t, m, "msg1", "Test message 1", addrID1, false, []string{pmapi.AllMailLabel, pmapi.InboxLabel})
insertMessage(t, m, "msg2", "Test message 2", addrID1, 0, []string{pmapi.AllMailLabel, pmapi.InboxLabel}) insertMessage(t, m, "msg2", "Test message 2", addrID1, false, []string{pmapi.AllMailLabel, pmapi.InboxLabel})
// Clear everything. // Clear everything.

View File

@ -35,7 +35,7 @@ var systemFolderMapping = map[string]string{ //nolint[gochecknoglobals]
// Add more translations. // Add more translations.
} }
// LeastUsedColor is intended to return color for creating a new inbox or label // LeastUsedColor is intended to return color for creating a new inbox or label.
func LeastUsedColor(mailboxes []Mailbox) string { func LeastUsedColor(mailboxes []Mailbox) string {
usedColors := []string{} usedColors := []string{}
for _, m := range mailboxes { for _, m := range mailboxes {

View File

@ -1,5 +1,5 @@
// Code generated by MockGen. DO NOT EDIT. // Code generated by MockGen. DO NOT EDIT.
// Source: github.com/ProtonMail/proton-bridge/internal/transfer (interfaces: PanicHandler,ClientManager,IMAPClientProvider) // Source: github.com/ProtonMail/proton-bridge/internal/transfer (interfaces: PanicHandler,IMAPClientProvider)
// Package mocks is a generated GoMock package. // Package mocks is a generated GoMock package.
package mocks package mocks
@ -7,7 +7,6 @@ package mocks
import ( import (
reflect "reflect" reflect "reflect"
pmapi "github.com/ProtonMail/proton-bridge/pkg/pmapi"
imap "github.com/emersion/go-imap" imap "github.com/emersion/go-imap"
sasl "github.com/emersion/go-sasl" sasl "github.com/emersion/go-sasl"
gomock "github.com/golang/mock/gomock" gomock "github.com/golang/mock/gomock"
@ -48,57 +47,6 @@ func (mr *MockPanicHandlerMockRecorder) HandlePanic() *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HandlePanic", reflect.TypeOf((*MockPanicHandler)(nil).HandlePanic)) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HandlePanic", reflect.TypeOf((*MockPanicHandler)(nil).HandlePanic))
} }
// MockClientManager is a mock of ClientManager interface
type MockClientManager struct {
ctrl *gomock.Controller
recorder *MockClientManagerMockRecorder
}
// MockClientManagerMockRecorder is the mock recorder for MockClientManager
type MockClientManagerMockRecorder struct {
mock *MockClientManager
}
// NewMockClientManager creates a new mock instance
func NewMockClientManager(ctrl *gomock.Controller) *MockClientManager {
mock := &MockClientManager{ctrl: ctrl}
mock.recorder = &MockClientManagerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockClientManager) EXPECT() *MockClientManagerMockRecorder {
return m.recorder
}
// CheckConnection mocks base method
func (m *MockClientManager) CheckConnection() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CheckConnection")
ret0, _ := ret[0].(error)
return ret0
}
// CheckConnection indicates an expected call of CheckConnection
func (mr *MockClientManagerMockRecorder) CheckConnection() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckConnection", reflect.TypeOf((*MockClientManager)(nil).CheckConnection))
}
// GetClient mocks base method
func (m *MockClientManager) GetClient(arg0 string) pmapi.Client {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetClient", arg0)
ret0, _ := ret[0].(pmapi.Client)
return ret0
}
// GetClient indicates an expected call of GetClient
func (mr *MockClientManagerMockRecorder) GetClient(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClient", reflect.TypeOf((*MockClientManager)(nil).GetClient), arg0)
}
// MockIMAPClientProvider is a mock of IMAPClientProvider interface // MockIMAPClientProvider is a mock of IMAPClientProvider interface
type MockIMAPClientProvider struct { type MockIMAPClientProvider struct {
ctrl *gomock.Controller ctrl *gomock.Controller

View File

@ -27,7 +27,7 @@ import (
type IMAPClientProvider interface { type IMAPClientProvider interface {
Capability() (map[string]bool, error) Capability() (map[string]bool, error)
Support(cap string) (bool, error) Support(capability string) (bool, error)
State() imap.ConnState State() imap.ConnState
SupportAuth(mech string) (bool, error) SupportAuth(mech string) (bool, error)
Authenticate(auth sasl.Client) error Authenticate(auth sasl.Client) error

View File

@ -19,13 +19,14 @@ package transfer
import ( import (
"crypto/tls" "crypto/tls"
"fmt"
"net" "net"
"net/http"
"strings" "strings"
"time" "time"
imapID "github.com/ProtonMail/go-imap-id" imapID "github.com/ProtonMail/go-imap-id"
"github.com/ProtonMail/proton-bridge/internal/constants" "github.com/ProtonMail/proton-bridge/internal/constants"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/emersion/go-imap" "github.com/emersion/go-imap"
imapClient "github.com/emersion/go-imap/client" imapClient "github.com/emersion/go-imap/client"
"github.com/emersion/go-sasl" "github.com/emersion/go-sasl"
@ -38,6 +39,8 @@ const (
imapRetries = 10 imapRetries = 10
imapReconnectTimeout = 30 * time.Minute imapReconnectTimeout = 30 * time.Minute
imapReconnectSleep = time.Minute imapReconnectSleep = time.Minute
protonStatusURL = "http://protonstatus.com/vpn_status"
) )
type imapErrorLogger struct { type imapErrorLogger struct {
@ -62,10 +65,10 @@ func imapClientDial(addr string) (IMAPClientProvider, error) {
client.ErrorLog = &imapErrorLogger{logrus.WithField("pkg", "imap-client")} client.ErrorLog = &imapErrorLogger{logrus.WithField("pkg", "imap-client")}
// Logrus `WriterLevel` fails for big messages because of bufio.MaxScanTokenSize limit. // Logrus `WriterLevel` fails for big messages because of bufio.MaxScanTokenSize limit.
// Also, this spams a lot, uncomment once needed during development. // Also, this spams a lot, uncomment once needed during development.
//client.SetDebug(imap.NewDebugWriter( // client.SetDebug(imap.NewDebugWriter(
// logrus.WithField("pkg", "imap/client").WriterLevel(logrus.TraceLevel), // logrus.WithField("pkg", "imap/client").WriterLevel(logrus.TraceLevel),
// logrus.WithField("pkg", "imap/server").WriterLevel(logrus.TraceLevel), // logrus.WithField("pkg", "imap/server").WriterLevel(logrus.TraceLevel),
//)) // ))
} }
return client, err return client, err
} }
@ -84,7 +87,7 @@ func imapClientDialHelper(addr string) (*imapClient.Client, error) {
var tlsConf *tls.Config var tlsConf *tls.Config
if strings.Contains(strings.ToLower(host), "yahoo") { if strings.Contains(strings.ToLower(host), "yahoo") {
log.Warning("Yahoo server detected: limiting maximal TLS version to 1.2.") log.Warning("Yahoo server detected: limiting maximal TLS version to 1.2.")
tlsConf = &tls.Config{MaxVersion: tls.VersionTLS12} tlsConf = &tls.Config{MaxVersion: tls.VersionTLS12} //nolint[gosec] G402
} }
return imapClient.DialTLS(addr, tlsConf) return imapClient.DialTLS(addr, tlsConf)
} }
@ -118,7 +121,7 @@ func (p *IMAPProvider) tryReconnect(ensureSelectedIn string) error {
return previousErr return previousErr
} }
err := pmapi.CheckConnection() err := checkConnection()
log.WithError(err).Debug("Connection check") log.WithError(err).Debug("Connection check")
if err != nil { if err != nil {
time.Sleep(imapReconnectSleep) time.Sleep(imapReconnectSleep)
@ -286,3 +289,23 @@ func (p *IMAPProvider) fetchHelper(uid bool, ensureSelectedIn string, seqSet *im
return err return err
}, ensureSelectedIn) }, ensureSelectedIn)
} }
// checkConnection returns an error if there is no internet connection.
// Note we don't want to use client manager because it only reports connection
// issues with API; we are only interested here whether we can reach
// third-party IMAP servers.
func checkConnection() error {
client := &http.Client{Timeout: time.Second * 10}
resp, err := client.Get(protonStatusURL)
if err != nil {
return err
}
_ = resp.Body.Close()
if resp.StatusCode != 200 {
return fmt.Errorf("HTTP status code %d", resp.StatusCode)
}
return nil
}

View File

@ -63,7 +63,7 @@ func (p *MBOXProvider) writeMessage(msg Message) error {
} }
mboxPath := filepath.Join(p.root, mboxName) mboxPath := filepath.Join(p.root, mboxName)
mboxFile, err := os.OpenFile(mboxPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600) mboxFile, err := os.OpenFile(filepath.Clean(mboxPath), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
if err != nil { if err != nil {
multiErr = multierror.Append(multiErr, err) multiErr = multierror.Append(multiErr, err)
continue continue

View File

@ -18,32 +18,44 @@
package transfer package transfer
import ( import (
"context"
"sort" "sort"
"github.com/ProtonMail/gopenpgp/v2/crypto" "github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/ProtonMail/proton-bridge/pkg/message"
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
const (
fetchWorkers = 20 // In how many workers to fetch message (group list on IMAP).
attachWorkers = 5 // In how many workers to fetch attachments (for one message).
buildWorkers = 20 // In how many workers to build messages.
)
// PMAPIProvider implements import and export to/from ProtonMail server. // PMAPIProvider implements import and export to/from ProtonMail server.
type PMAPIProvider struct { type PMAPIProvider struct {
clientManager ClientManager client pmapi.Client
userID string userID string
addressID string addressID string
keyRing *crypto.KeyRing keyRing *crypto.KeyRing
builder *message.Builder
nextImportRequests map[string]*pmapi.ImportMsgReq // Key is msg transfer ID. nextImportRequests map[string]*pmapi.ImportMsgReq // Key is msg transfer ID.
nextImportRequestsSize int nextImportRequestsSize int
timeIt *timeIt timeIt *timeIt
connection bool
} }
// NewPMAPIProvider returns new PMAPIProvider. // NewPMAPIProvider returns new PMAPIProvider.
func NewPMAPIProvider(clientManager ClientManager, userID, addressID string) (*PMAPIProvider, error) { func NewPMAPIProvider(client pmapi.Client, userID, addressID string) (*PMAPIProvider, error) {
provider := &PMAPIProvider{ provider := &PMAPIProvider{
clientManager: clientManager, client: client,
userID: userID, userID: userID,
addressID: addressID, addressID: addressID,
builder: message.NewBuilder(fetchWorkers, attachWorkers, buildWorkers),
nextImportRequests: map[string]*pmapi.ImportMsgReq{}, nextImportRequests: map[string]*pmapi.ImportMsgReq{},
nextImportRequestsSize: 0, nextImportRequestsSize: 0,
@ -52,7 +64,7 @@ func NewPMAPIProvider(clientManager ClientManager, userID, addressID string) (*P
} }
if addressID != "" { if addressID != "" {
keyRing, err := clientManager.GetClient(userID).KeyRingForAddressID(addressID) keyRing, err := client.KeyRingForAddressID(addressID)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "failed to get key ring") return nil, errors.Wrap(err, "failed to get key ring")
} }
@ -62,10 +74,6 @@ func NewPMAPIProvider(clientManager ClientManager, userID, addressID string) (*P
return provider, nil return provider, nil
} }
func (p *PMAPIProvider) client() pmapi.Client {
return p.clientManager.GetClient(p.userID)
}
// ID returns identifier of current setup of PMAPI provider. // ID returns identifier of current setup of PMAPI provider.
// Identification is unique per user. // Identification is unique per user.
func (p *PMAPIProvider) ID() string { func (p *PMAPIProvider) ID() string {
@ -74,7 +82,7 @@ func (p *PMAPIProvider) ID() string {
// Mailboxes returns all available labels in ProtonMail account. // Mailboxes returns all available labels in ProtonMail account.
func (p *PMAPIProvider) Mailboxes(includeEmpty, includeAllMail bool) ([]Mailbox, error) { func (p *PMAPIProvider) Mailboxes(includeEmpty, includeAllMail bool) ([]Mailbox, error) {
labels, err := p.client().ListLabels() labels, err := p.client.ListLabels(context.Background())
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -83,7 +91,7 @@ func (p *PMAPIProvider) Mailboxes(includeEmpty, includeAllMail bool) ([]Mailbox,
emptyLabelsMap := map[string]bool{} emptyLabelsMap := map[string]bool{}
if !includeEmpty { if !includeEmpty {
messagesCounts, err := p.client().CountMessages(p.addressID) messagesCounts, err := p.client.CountMessages(context.Background(), p.addressID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -111,7 +119,7 @@ func (p *PMAPIProvider) Mailboxes(includeEmpty, includeAllMail bool) ([]Mailbox,
ID: label.ID, ID: label.ID,
Name: label.Name, Name: label.Name,
Color: label.Color, Color: label.Color,
IsExclusive: label.Exclusive == 1, IsExclusive: bool(label.Exclusive),
}) })
} }
return mailboxes, nil return mailboxes, nil
@ -151,10 +159,10 @@ func (l byFoldersLabels) Swap(i, j int) {
// Less sorts first folders, then labels, by user order. // Less sorts first folders, then labels, by user order.
func (l byFoldersLabels) Less(i, j int) bool { func (l byFoldersLabels) Less(i, j int) bool {
if l[i].Exclusive == 1 && l[j].Exclusive == 0 { if l[i].Exclusive && !l[j].Exclusive {
return true return true
} }
if l[i].Exclusive == 0 && l[j].Exclusive == 1 { if !l[i].Exclusive && l[j].Exclusive {
return false return false
} }
return l[i].Order < l[j].Order return l[i].Order < l[j].Order

View File

@ -18,12 +18,13 @@
package transfer package transfer
import ( import (
"context"
"errors"
"fmt" "fmt"
"sync" "sync"
pkgMsg "github.com/ProtonMail/proton-bridge/pkg/message" "github.com/ProtonMail/proton-bridge/pkg/message"
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/pkg/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@ -144,6 +145,7 @@ func (p *PMAPIProvider) transferTo(rule *Rule, progress *Progress, ch chan<- Mes
func (p *PMAPIProvider) exportMessage(rule *Rule, progress *Progress, pmapiMsgID, msgID string, skipEncryptedMessages bool) (Message, error) { func (p *PMAPIProvider) exportMessage(rule *Rule, progress *Progress, pmapiMsgID, msgID string, skipEncryptedMessages bool) (Message, error) {
var msg *pmapi.Message var msg *pmapi.Message
progress.callWrap(func() error { progress.callWrap(func() error {
var err error var err error
msg, err = p.getMessage(pmapiMsgID) msg, err = p.getMessage(pmapiMsgID)
@ -153,29 +155,23 @@ func (p *PMAPIProvider) exportMessage(rule *Rule, progress *Progress, pmapiMsgID
p.timeIt.start("build", msgID) p.timeIt.start("build", msgID)
defer p.timeIt.stop("build", msgID) defer p.timeIt.stop("build", msgID)
msgBuilder := pkgMsg.NewBuilder(p.client(), msg) body, err := p.builder.NewJobWithOptions(
msgBuilder.EncryptedToHTML = false context.Background(),
_, body, err := msgBuilder.BuildMessage() p.client,
msg.ID,
message.JobOptions{IgnoreDecryptionErrors: !skipEncryptedMessages},
).GetResult()
if err != nil { if err != nil {
return Message{ if errors.Is(err, message.ErrDecryptionFailed) && skipEncryptedMessages {
Body: body, // Keep body to show details about the message to user. err = errors.New("skipping encrypted message")
}, errors.Wrap(err, "failed to build message") }
}
if !msgBuilder.SuccessfullyDecrypted() && skipEncryptedMessages { return Message{Body: []byte(msg.Body)}, err
return Message{
Body: body, // Keep body to show details about the message to user.
}, errors.New("skipping encrypted message")
}
unread := false
if msg.Unread == 1 {
unread = true
} }
return Message{ return Message{
ID: msgID, ID: msgID,
Unread: unread, Unread: bool(msg.Unread),
Body: body, Body: body,
Sources: []Mailbox{rule.SourceMailbox}, Sources: []Mailbox{rule.SourceMailbox},
Targets: rule.TargetMailboxes, Targets: rule.TargetMailboxes,

Some files were not shown because too many files have changed in this diff Show More