mirror of
https://github.com/ProtonMail/proton-bridge.git
synced 2025-12-10 12:46:46 +00:00
Compare commits
77 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7c41c8e23a | |||
| 36fdb88d96 | |||
| 885fb95454 | |||
| 629d6c5e4d | |||
| 4072205709 | |||
| 5d82c218ca | |||
| 6ff4c8a738 | |||
| dd66b7f8d0 | |||
| 0b95ed4dea | |||
| ce64aeb05f | |||
| 27cfda680d | |||
| 323303a98b | |||
| 8109831c07 | |||
| 2284e9ede1 | |||
| 1d538e8540 | |||
| 8ccaac8090 | |||
| 22bf8f62ce | |||
| fed031ebaa | |||
| 7a15ebbd54 | |||
| 94b5799ba7 | |||
| 286f51a4e7 | |||
| ee961ae4a8 | |||
| 4038752a9a | |||
| ebf724412b | |||
| 14d42b5e76 | |||
| 2b8d92e82d | |||
| 11b1e3acf5 | |||
| c5eb660315 | |||
| 5ad23715ec | |||
| 8ab05a000c | |||
| 454d248819 | |||
| 6c8e5f7cd3 | |||
| f5aba717b2 | |||
| 1359c39bc0 | |||
| 4850681f1d | |||
| aa55c69307 | |||
| 1f19d4df75 | |||
| c0f6af9eb5 | |||
| ef6a3d4999 | |||
| 50550d42b4 | |||
| 8db89a1a6c | |||
| ba1dfb1bf4 | |||
| d243880753 | |||
| cccaaa3d82 | |||
| 2d95f21567 | |||
| 7d0af7624c | |||
| 2f35c453a1 | |||
| 05dd137bc8 | |||
| 767628946f | |||
| d4efa7131f | |||
| 144cf6e40c | |||
| a205d8c046 | |||
| cccadaee42 | |||
| bbb365f8a5 | |||
| 1f18d9d917 | |||
| 59e0d63485 | |||
| 72fe5a636e | |||
| 45a83133ba | |||
| 215eb4d6eb | |||
| 479b951c50 | |||
| a94c8a943f | |||
| ea306f405e | |||
| 1b405506b8 | |||
| 38c6132f81 | |||
| b7351dfaf8 | |||
| 7e8f6943f2 | |||
| a0132e8440 | |||
| 27541784aa | |||
| 9e567f08b2 | |||
| bf274f984e | |||
| 3b60bbe13b | |||
| a73a1b623a | |||
| c0a8877018 | |||
| 904166c01c | |||
| 4761bc935a | |||
| 71301d891f | |||
| d47be3c4c0 |
@ -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]
|
||||||
|
|
||||||
|
|||||||
123
Changelog.md
123
Changelog.md
@ -2,6 +2,129 @@
|
|||||||
|
|
||||||
Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||||
|
|
||||||
|
## [Bridge 1.8.3] James
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* GODT-1182: Use correct contact route.
|
||||||
|
|
||||||
|
|
||||||
|
## [Bridge 1.8.2] James
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* GODT-1175: Bug reporting.
|
||||||
|
|
||||||
|
|
||||||
|
## [Bridge 1.8.1] James
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* GODT-1165: Handle UID FETCH with sequence range of empty mailbox.
|
||||||
|
|
||||||
|
|
||||||
|
## [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
|
||||||
|
|
||||||
|
### Added
|
||||||
|
* GODT-1111 Add correct metadata to Windows executables.
|
||||||
|
* GODT-1112 Add application to Windows Firewall exclusion list on install.
|
||||||
|
* GODT-1077 Track how many times message is built to help understand re-syncs.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* GODT-247 Revise all storage locations (cache, config, local etc).
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* GODT-948 Parser does not handle embedding of Content-Type: message/rfc822.
|
||||||
|
* GODT-1079 Correct 9001 error handling on login.
|
||||||
|
|
||||||
|
### Security
|
||||||
|
* GODT-1105 Dylib Hijacking security fix.
|
||||||
|
|
||||||
|
|
||||||
|
## [IE 1.3.2] Farg
|
||||||
|
|
||||||
|
### Added
|
||||||
|
* GODT-1111 Add correct metadata to Windows executables.
|
||||||
|
* GODT-1112 Add application to Windows Firewall exclusion list on install.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* GODT-247 Revise all storage locations (cache, config, local etc).
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* GODT-1079 Correct 9001 error handling on login.
|
||||||
|
|
||||||
|
### Security
|
||||||
|
* GODT-1105 Dylib Hijacking security fix.
|
||||||
|
|
||||||
|
|
||||||
|
## [IE 1.3.1] Farg
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* GODT-1047 No silent updates for Import-Export app.
|
||||||
|
* GODT-247 Cache and update files moved from user's cache to config.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* Other: include latest go.mod/go.sum changes.
|
||||||
|
* GODT-803 Fix import to wrong target address.
|
||||||
|
* GODT-948 Embedded messages.
|
||||||
|
* GODT-1043 Fix showing long login error in GUI dialog.
|
||||||
|
|
||||||
|
|
||||||
## [Bridge 1.6.6] HZM
|
## [Bridge 1.6.6] HZM
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
58
Makefile
58
Makefile
@ -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.6+git
|
BRIDGE_APP_VERSION?=1.8.3+git
|
||||||
IE_APP_VERSION?=1.3.0+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
|
||||||
@ -19,6 +19,7 @@ SRC_SVG:=logo.svg
|
|||||||
TGT_ICNS:=Bridge.icns
|
TGT_ICNS:=Bridge.icns
|
||||||
EXE_NAME:=proton-bridge
|
EXE_NAME:=proton-bridge
|
||||||
CONFIGNAME:=bridge
|
CONFIGNAME:=bridge
|
||||||
|
WINDRES_DEFINE:=BUILD_BRIDGE
|
||||||
ifeq "${TARGET_CMD}" "Import-Export"
|
ifeq "${TARGET_CMD}" "Import-Export"
|
||||||
APP_VERSION:=${IE_APP_VERSION}
|
APP_VERSION:=${IE_APP_VERSION}
|
||||||
SRC_ICO:=ie.ico
|
SRC_ICO:=ie.ico
|
||||||
@ -27,6 +28,7 @@ ifeq "${TARGET_CMD}" "Import-Export"
|
|||||||
TGT_ICNS:=ImportExport.icns
|
TGT_ICNS:=ImportExport.icns
|
||||||
EXE_NAME:=proton-ie
|
EXE_NAME:=proton-ie
|
||||||
CONFIGNAME:=importExport
|
CONFIGNAME:=importExport
|
||||||
|
WINDRES_DEFINE:=BUILD_IE
|
||||||
endif
|
endif
|
||||||
REVISION:=$(shell git rev-parse --short=10 HEAD)
|
REVISION:=$(shell git rev-parse --short=10 HEAD)
|
||||||
BUILD_TIME:=$(shell date +%FT%T%z)
|
BUILD_TIME:=$(shell date +%FT%T%z)
|
||||||
@ -56,7 +58,7 @@ EXE_QT:=${DIRNAME}
|
|||||||
ifeq "${TARGET_OS}" "windows"
|
ifeq "${TARGET_OS}" "windows"
|
||||||
EXE:=${EXE}.exe
|
EXE:=${EXE}.exe
|
||||||
EXE_QT:=${EXE_QT}.exe
|
EXE_QT:=${EXE_QT}.exe
|
||||||
ICO_FILES:=${SRC_ICO} icon.rc icon_windows.syso
|
RESOURCE_FILE:=resource.syso
|
||||||
endif
|
endif
|
||||||
ifeq "${TARGET_OS}" "darwin"
|
ifeq "${TARGET_OS}" "darwin"
|
||||||
DARWINAPP_CONTENTS:=${DEPLOY_DIR}/darwin/${EXE}.app/Contents
|
DARWINAPP_CONTENTS:=${DEPLOY_DIR}/darwin/${EXE}.app/Contents
|
||||||
@ -89,8 +91,14 @@ build-nogui: gofiles
|
|||||||
build-ie-nogui:
|
build-ie-nogui:
|
||||||
TARGET_CMD=Import-Export $(MAKE) build-nogui
|
TARGET_CMD=Import-Export $(MAKE) build-nogui
|
||||||
|
|
||||||
build-launcher:
|
ifeq "${GOOS}" "windows"
|
||||||
go build ${BUILD_FLAGS_LAUNCHER} -o launcher-${APP} cmd/launcher/main.go
|
PRERESOURCECMD:=cp ./resource.syso ./cmd/launcher/resource.syso
|
||||||
|
POSTRESOURCECMD:=rm -f ./cmd/launcher/resource.syso
|
||||||
|
endif
|
||||||
|
build-launcher: ${RESOURCE_FILE}
|
||||||
|
${PRERESOURCECMD}
|
||||||
|
go build ${BUILD_FLAGS_LAUNCHER} -o launcher-${EXE} ./cmd/launcher/
|
||||||
|
${POSTRESOURCECMD}
|
||||||
|
|
||||||
build-launcher-ie:
|
build-launcher-ie:
|
||||||
TARGET_CMD=Import-Export $(MAKE) build-launcher
|
TARGET_CMD=Import-Export $(MAKE) build-launcher
|
||||||
@ -134,7 +142,7 @@ ifneq "${GOOS}" "${TARGET_OS}"
|
|||||||
endif
|
endif
|
||||||
endif
|
endif
|
||||||
|
|
||||||
${EXE_TARGET}: check-has-go gofiles ${ICO_FILES} ${VENDOR_TARGET}
|
${EXE_TARGET}: check-has-go gofiles ${RESOURCE_FILE} ${VENDOR_TARGET}
|
||||||
rm -rf deploy ${TARGET_OS} ${DEPLOY_DIR}
|
rm -rf deploy ${TARGET_OS} ${DEPLOY_DIR}
|
||||||
cp cmd/${TARGET_CMD}/main.go .
|
cp cmd/${TARGET_CMD}/main.go .
|
||||||
qtdeploy ${BUILD_FLAGS_GUI} ${QT_BUILD_TARGET}
|
qtdeploy ${BUILD_FLAGS_GUI} ${QT_BUILD_TARGET}
|
||||||
@ -142,13 +150,12 @@ ${EXE_TARGET}: check-has-go gofiles ${ICO_FILES} ${VENDOR_TARGET}
|
|||||||
if [ "${EXE_QT_TARGET}" != "${EXE_TARGET}" ]; then mv ${EXE_QT_TARGET} ${EXE_TARGET}; fi
|
if [ "${EXE_QT_TARGET}" != "${EXE_TARGET}" ]; then mv ${EXE_QT_TARGET} ${EXE_TARGET}; fi
|
||||||
rm -rf ${TARGET_OS} main.go
|
rm -rf ${TARGET_OS} main.go
|
||||||
|
|
||||||
logo.ico ie.ico: ./internal/frontend/share/icons/${SRC_ICO}
|
|
||||||
cp $^ $@
|
|
||||||
icon.rc: ./internal/frontend/share/icon.rc
|
|
||||||
cp $^ .
|
|
||||||
icon_windows.syso: icon.rc logo.ico
|
|
||||||
windres --target=pe-x86-64 -o $@ $<
|
|
||||||
|
|
||||||
|
WINDRES_YEAR:=$(shell date +%Y)
|
||||||
|
APP_VERSION_COMMA:=$(shell echo "${APP_VERSION}" | sed -e 's/[^0-9,.]*//g' -e 's/\./,/g')
|
||||||
|
resource.syso: ./internal/frontend/share/info.rc ./internal/frontend/share/icons/${SRC_ICO} .FORCE
|
||||||
|
rm -f ./*.syso
|
||||||
|
windres --target=pe-x86-64 -I ./internal/frontend/share/icons/ -D ${WINDRES_DEFINE} -D ICO_FILE=${SRC_ICO} -D EXE_NAME="${EXE_NAME}" -D FILE_VERSION="${APP_VERSION}" -D ORIGINAL_FILE_NAME="${EXE}" -D PRODUCT_VERSION="${APP_VERSION}" -D FILE_VERSION_COMMA=${APP_VERSION_COMMA} -D YEAR=${WINDRES_YEAR} -o $@ $<
|
||||||
|
|
||||||
## Rules for therecipe/qt
|
## Rules for therecipe/qt
|
||||||
.PHONY: prepare-vendor update-vendor update-qt-docs
|
.PHONY: prepare-vendor update-vendor update-qt-docs
|
||||||
@ -158,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)
|
||||||
@ -181,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
|
||||||
@ -245,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
|
||||||
|
|
||||||
@ -294,6 +307,7 @@ LOG?=debug
|
|||||||
LOG_IMAP?=client # client/server/all, or empty to turn it off
|
LOG_IMAP?=client # client/server/all, or empty to turn it off
|
||||||
LOG_SMTP?=--log-smtp # empty to turn it off
|
LOG_SMTP?=--log-smtp # empty to turn it off
|
||||||
RUN_FLAGS?=-m -l=${LOG} --log-imap=${LOG_IMAP} ${LOG_SMTP}
|
RUN_FLAGS?=-m -l=${LOG} --log-imap=${LOG_IMAP} ${LOG_SMTP}
|
||||||
|
RUN_FLAGS_IE?=-m -l=${LOG}
|
||||||
|
|
||||||
run: run-nogui-cli
|
run: run-nogui-cli
|
||||||
|
|
||||||
@ -316,11 +330,11 @@ run-ie-qml-preview:
|
|||||||
$(MAKE) -C internal/frontend/qt-ie -f Makefile.local qmlpreview
|
$(MAKE) -C internal/frontend/qt-ie -f Makefile.local qmlpreview
|
||||||
|
|
||||||
run-ie:
|
run-ie:
|
||||||
TARGET_CMD=Import-Export $(MAKE) run
|
TARGET_CMD=Import-Export RUN_FLAGS="${RUN_FLAGS_IE}" $(MAKE) run
|
||||||
run-ie-qt:
|
run-ie-qt:
|
||||||
TARGET_CMD=Import-Export $(MAKE) run-qt
|
TARGET_CMD=Import-Export RUN_FLAGS="${RUN_FLAGS_IE}" $(MAKE) run-qt
|
||||||
run-ie-nogui:
|
run-ie-nogui:
|
||||||
TARGET_CMD=Import-Export $(MAKE) run-nogui
|
TARGET_CMD=Import-Export RUN_FLAGS="${RUN_FLAGS_IE}" $(MAKE) run-nogui
|
||||||
|
|
||||||
clean-frontend-qt:
|
clean-frontend-qt:
|
||||||
$(MAKE) -C internal/frontend/qt -f Makefile.local clean
|
$(MAKE) -C internal/frontend/qt -f Makefile.local clean
|
||||||
@ -337,7 +351,7 @@ clean: clean-vendor
|
|||||||
rm -rf cmd/Desktop-Bridge/deploy
|
rm -rf cmd/Desktop-Bridge/deploy
|
||||||
rm -rf cmd/Import-Export/deploy
|
rm -rf cmd/Import-Export/deploy
|
||||||
rm -f build last.log mem.pprof main.go
|
rm -f build last.log mem.pprof main.go
|
||||||
rm -rf logo.ico icon.rc icon_windows.syso internal/frontend/qt/icon_windows.syso
|
rm -f resource.syso
|
||||||
rm -f release-notes/bridge.html
|
rm -f release-notes/bridge.html
|
||||||
rm -f release-notes/import-export.html
|
rm -f release-notes/import-export.html
|
||||||
|
|
||||||
@ -345,3 +359,5 @@ clean: clean-vendor
|
|||||||
generate:
|
generate:
|
||||||
go generate ./...
|
go generate ./...
|
||||||
$(MAKE) add-license
|
$(MAKE) add-license
|
||||||
|
|
||||||
|
.FORCE:
|
||||||
|
|||||||
9
go.mod
9
go.mod
@ -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,20 +50,21 @@ 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
|
||||||
github.com/pkg/math v0.0.0-20141027224758-f2ed9e40e245
|
|
||||||
github.com/sirupsen/logrus v1.7.0
|
github.com/sirupsen/logrus v1.7.0
|
||||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
|
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
|
||||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
|
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
|
||||||
github.com/stretchr/testify v1.6.1
|
github.com/stretchr/testify v1.6.1
|
||||||
github.com/therecipe/qt v0.0.0-20200701200531-7f61353ee73e
|
github.com/therecipe/qt v0.0.0-20200701200531-7f61353ee73e
|
||||||
|
github.com/therecipe/qt/internal/binding/files/docs/5.12.0 v0.0.0-20200904063919-c0c124a5770d // indirect
|
||||||
|
github.com/therecipe/qt/internal/binding/files/docs/5.13.0 v0.0.0-20200904063919-c0c124a5770d // indirect
|
||||||
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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
37
go.sum
37
go.sum
@ -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=
|
||||||
@ -223,8 +223,6 @@ github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTw
|
|||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/math v0.0.0-20141027224758-f2ed9e40e245 h1:gk/AF9SGRj+RafNCoDcS3RRscb8S4BVbvqODOgWA7/8=
|
|
||||||
github.com/pkg/math v0.0.0-20141027224758-f2ed9e40e245/go.mod h1:2dhPPj2Li3DXrSY2U2ADdZy2B7sjQsT57lqENx1+FSE=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
|
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
|
||||||
@ -264,6 +262,11 @@ github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd
|
|||||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
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/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.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/go.mod h1:mH55Ek7AZcdns5KPp99O0bg+78el64YCYWHiQKrOdt4=
|
||||||
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
|
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
|
||||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||||
@ -307,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=
|
||||||
@ -327,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=
|
||||||
@ -345,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=
|
||||||
|
|||||||
@ -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
|
||||||
@ -181,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)
|
||||||
@ -328,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).
|
||||||
@ -375,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
|
|
||||||
}
|
|
||||||
|
|||||||
@ -29,10 +29,12 @@ import (
|
|||||||
// migrateFiles migrates files from their old (pre-refactor) locations to their new locations.
|
// migrateFiles migrates files from their old (pre-refactor) locations to their new locations.
|
||||||
// We can remove this eventually.
|
// We can remove this eventually.
|
||||||
//
|
//
|
||||||
// | entity | old location | new location |
|
// | entity | old location | new location |
|
||||||
// |--------|-------------------------------------------|----------------------------------------|
|
// |-----------|-------------------------------------------|----------------------------------------|
|
||||||
// | 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 | ~/.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 |
|
||||||
|
// | 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 {
|
||||||
@ -41,43 +43,89 @@ func migrateFiles(configName string) error {
|
|||||||
|
|
||||||
locations := locations.New(locationsProvider, configName)
|
locations := locations.New(locationsProvider, configName)
|
||||||
userCacheDir := locationsProvider.UserCache()
|
userCacheDir := locationsProvider.UserCache()
|
||||||
|
|
||||||
|
if err := migratePrefsFrom15x(locations, userCacheDir); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := migrateCacheFromBoth15xAnd16x(locations, userCacheDir); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := migrateUpdatesFrom16x(configName, locations); err != nil { //nolint[revive] It is more clear to structure this way
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func migratePrefsFrom15x(locations *locations.Locations, userCacheDir string) error {
|
||||||
newSettingsDir, err := locations.ProvideSettingsPath()
|
newSettingsDir, err := locations.ProvideSettingsPath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := moveIfExists(
|
return moveIfExists(
|
||||||
filepath.Join(userCacheDir, "c11", "prefs.json"),
|
filepath.Join(userCacheDir, "c11", "prefs.json"),
|
||||||
filepath.Join(newSettingsDir, "prefs.json"),
|
filepath.Join(newSettingsDir, "prefs.json"),
|
||||||
); err != nil {
|
)
|
||||||
return err
|
}
|
||||||
}
|
|
||||||
|
|
||||||
newCacheDir, err := locations.ProvideCachePath()
|
func migrateCacheFromBoth15xAnd16x(locations *locations.Locations, userCacheDir string) error {
|
||||||
|
olderCacheDir := userCacheDir
|
||||||
|
newerCacheDir := locations.GetOldCachePath()
|
||||||
|
latestCacheDir, err := locations.ProvideCachePath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Migration for versions before 1.6.x.
|
||||||
if err := moveIfExists(
|
if err := moveIfExists(
|
||||||
filepath.Join(userCacheDir, "c11"),
|
filepath.Join(olderCacheDir, "c11"),
|
||||||
filepath.Join(newCacheDir, "c11"),
|
filepath.Join(latestCacheDir, "c11"),
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
// Migration for versions 1.6.x.
|
||||||
|
return moveIfExists(
|
||||||
|
filepath.Join(newerCacheDir, "c11"),
|
||||||
|
filepath.Join(latestCacheDir, "c11"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func migrateUpdatesFrom16x(configName string, locations *locations.Locations) error {
|
||||||
|
// In order to properly update Bridge 1.6.X and higher we need to
|
||||||
|
// change the launcher first. Since this is not part of automatic
|
||||||
|
// updates the migration must wait until manual update. Until that
|
||||||
|
// we need to keep old path.
|
||||||
|
if configName == "bridge" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
oldUpdatesPath := locations.GetOldUpdatesPath()
|
||||||
|
// Do not use ProvideUpdatesPath, that creates dir right away.
|
||||||
|
newUpdatesPath := locations.GetUpdatesPath()
|
||||||
|
|
||||||
|
return moveIfExists(oldUpdatesPath, newUpdatesPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
func moveIfExists(source, destination string) error {
|
func moveIfExists(source, destination string) error {
|
||||||
|
l := logrus.WithField("source", source).WithField("destination", destination)
|
||||||
|
|
||||||
if _, err := os.Stat(source); os.IsNotExist(err) {
|
if _, err := os.Stat(source); os.IsNotExist(err) {
|
||||||
logrus.WithField("source", source).WithField("destination", destination).Debug("No need to migrate file")
|
l.Info("No need to migrate file, source doesn't exist")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := os.Stat(destination); !os.IsNotExist(err) {
|
if _, err := os.Stat(destination); !os.IsNotExist(err) {
|
||||||
logrus.WithField("source", source).WithField("destination", destination).Debug("No need to migrate file")
|
// Once migrated, files should not stay in source anymore. Therefore
|
||||||
|
// if some files are still in source location but target already exist,
|
||||||
|
// it's suspicious. Could happen by installing new version, then the
|
||||||
|
// old one because of some reason, and then the new one again.
|
||||||
|
// Good to see as warning because it could be a reason why Bridge is
|
||||||
|
// behaving weirdly, like wrong configuration, or db re-sync and so on.
|
||||||
|
l.Warn("No need to migrate file, target already exists")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
l.Info("Migrating files")
|
||||||
return os.Rename(source, destination)
|
return os.Rename(source, destination)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -28,8 +28,6 @@ import (
|
|||||||
"github.com/ProtonMail/proton-bridge/internal/frontend"
|
"github.com/ProtonMail/proton-bridge/internal/frontend"
|
||||||
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
|
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
|
||||||
"github.com/ProtonMail/proton-bridge/internal/importexport"
|
"github.com/ProtonMail/proton-bridge/internal/importexport"
|
||||||
"github.com/ProtonMail/proton-bridge/internal/updater"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
)
|
)
|
||||||
@ -88,10 +86,11 @@ func run(b *base.Base, c *cli.Context) error {
|
|||||||
return f.Loop()
|
return f.Loop()
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkAndHandleUpdate(u types.Updater, f frontend.Frontend, autoUpdate bool) {
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,33 +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")
|
||||||
|
|
||||||
if !autoUpdate {
|
f.NotifyManualUpdate(version, u.CanInstall(version))
|
||||||
f.NotifyManualUpdate(version, u.CanInstall(version))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !u.CanInstall(version) {
|
|
||||||
logrus.Info("A manual update is required")
|
|
||||||
f.NotifySilentUpdateError(updater.ErrManualUpdateRequired)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := u.InstallUpdate(version); err != nil {
|
|
||||||
if errors.Cause(err) == updater.ErrDownloadVerify {
|
|
||||||
logrus.WithError(err).Warning("Skipping update installation due to temporary error")
|
|
||||||
} else {
|
|
||||||
logrus.WithError(err).Error("The update couldn't be installed")
|
|
||||||
f.NotifySilentUpdateError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
f.NotifySilentUpdateInstalled()
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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(`
|
||||||
|
|||||||
@ -38,7 +38,7 @@ func (f *frontendCLI) importLocalMessages(c *ishell.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
t, err := f.ie.GetLocalImporter(user.GetPrimaryAddress(), path)
|
t, err := f.ie.GetLocalImporter(user.Username(), user.GetPrimaryAddress(), path)
|
||||||
f.transfer(t, err, false, true)
|
f.transfer(t, err, false, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,7 +68,7 @@ func (f *frontendCLI) importRemoteMessages(c *ishell.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
t, err := f.ie.GetRemoteImporter(user.GetPrimaryAddress(), username, password, host, port)
|
t, err := f.ie.GetRemoteImporter(user.Username(), user.GetPrimaryAddress(), username, password, host, port)
|
||||||
f.transfer(t, err, false, true)
|
f.transfer(t, err, false, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -81,7 +81,7 @@ func (f *frontendCLI) exportMessagesToEML(c *ishell.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
t, err := f.ie.GetEMLExporter(user.GetPrimaryAddress(), path)
|
t, err := f.ie.GetEMLExporter(user.Username(), user.GetPrimaryAddress(), path)
|
||||||
f.transfer(t, err, true, false)
|
f.transfer(t, err, true, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,7 +94,7 @@ func (f *frontendCLI) exportMessagesToMBOX(c *ishell.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
t, err := f.ie.GetMBOXExporter(user.GetPrimaryAddress(), path)
|
t, err := f.ie.GetMBOXExporter(user.Username(), user.GetPrimaryAddress(), path)
|
||||||
f.transfer(t, err, true, false)
|
f.transfer(t, err, true, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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")
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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(`
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -215,7 +215,7 @@ Item {
|
|||||||
}
|
}
|
||||||
go.updateState = "updateRestart"
|
go.updateState = "updateRestart"
|
||||||
winMain.dialogUpdate.finished(false)
|
winMain.dialogUpdate.finished(false)
|
||||||
|
|
||||||
// after manual update - just retart immidiatly
|
// after manual update - just retart immidiatly
|
||||||
go.setToRestart()
|
go.setToRestart()
|
||||||
Qt.quit()
|
Qt.quit()
|
||||||
@ -236,13 +236,13 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onNotifySilentUpdateRestartNeeded: {
|
//onNotifySilentUpdateRestartNeeded: {
|
||||||
go.updateState = "updateRestart"
|
// go.updateState = "updateRestart"
|
||||||
}
|
//}
|
||||||
|
//
|
||||||
onNotifySilentUpdateError: {
|
//onNotifySilentUpdateError: {
|
||||||
go.updateState = "updateError"
|
// go.updateState = "updateError"
|
||||||
}
|
//}
|
||||||
|
|
||||||
onNotifyLogout : {
|
onNotifyLogout : {
|
||||||
go.notifyBubble(0, qsTr("Account %1 has been disconnected. Please log in to continue to use the Import-Export app with this account.").arg(accname) )
|
go.notifyBubble(0, qsTr("Account %1 has been disconnected. Please log in to continue to use the Import-Export app with this account.").arg(accname) )
|
||||||
|
|||||||
@ -165,6 +165,7 @@ Column {
|
|||||||
textColor : Style.main.textBlue
|
textColor : Style.main.textBlue
|
||||||
onClicked: {
|
onClicked: {
|
||||||
dialogExport.currentIndex = 0
|
dialogExport.currentIndex = 0
|
||||||
|
dialogExport.account = account
|
||||||
dialogExport.address = account
|
dialogExport.address = account
|
||||||
dialogExport.show()
|
dialogExport.show()
|
||||||
}
|
}
|
||||||
@ -321,6 +322,7 @@ Column {
|
|||||||
textBold: true
|
textBold: true
|
||||||
textColor: Style.main.textBlue
|
textColor: Style.main.textBlue
|
||||||
onClicked: {
|
onClicked: {
|
||||||
|
dialogExport.account = account
|
||||||
dialogExport.address = listalias[index]
|
dialogExport.address = listalias[index]
|
||||||
dialogExport.show()
|
dialogExport.show()
|
||||||
}
|
}
|
||||||
@ -339,6 +341,7 @@ Column {
|
|||||||
textBold: true
|
textBold: true
|
||||||
textColor: enabled ? Style.main.textBlue : Style.main.textDisabled
|
textColor: enabled ? Style.main.textBlue : Style.main.textDisabled
|
||||||
onClicked: {
|
onClicked: {
|
||||||
|
dialogImport.account = account
|
||||||
dialogImport.address = listalias[index]
|
dialogImport.address = listalias[index]
|
||||||
dialogImport.show()
|
dialogImport.show()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -35,6 +35,7 @@ Dialog {
|
|||||||
|
|
||||||
title : set_title()
|
title : set_title()
|
||||||
|
|
||||||
|
property string account
|
||||||
property string address
|
property string address
|
||||||
property alias finish: finish
|
property alias finish: finish
|
||||||
|
|
||||||
@ -408,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()
|
||||||
@ -428,7 +428,7 @@ Dialog {
|
|||||||
onTriggered : {
|
onTriggered : {
|
||||||
switch (currentIndex) {
|
switch (currentIndex) {
|
||||||
case 0:
|
case 0:
|
||||||
go.loadStructureForExport(root.address)
|
go.loadStructureForExport(root.account, root.address)
|
||||||
sourceFoldersInput.hasItems = (transferRules.rowCount() > 0)
|
sourceFoldersInput.hasItems = (transferRules.rowCount() > 0)
|
||||||
break
|
break
|
||||||
case 2:
|
case 2:
|
||||||
|
|||||||
@ -34,6 +34,7 @@ Dialog {
|
|||||||
|
|
||||||
isDialogBusy: currentIndex==3 || currentIndex==4
|
isDialogBusy: currentIndex==3 || currentIndex==4
|
||||||
|
|
||||||
|
property string account
|
||||||
property string address
|
property string address
|
||||||
property string inputPath : ""
|
property string inputPath : ""
|
||||||
property bool isFromFile : inputEmail.text == "" && root.inputPath != ""
|
property bool isFromFile : inputEmail.text == "" && root.inputPath != ""
|
||||||
@ -856,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
|
||||||
@ -948,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()
|
||||||
@ -1032,6 +1030,7 @@ Dialog {
|
|||||||
root.isFromIMAP,
|
root.isFromIMAP,
|
||||||
root.inputPath,
|
root.inputPath,
|
||||||
inputEmail.text, inputPassword.text, inputServer.text, inputPort.text,
|
inputEmail.text, inputPassword.text, inputServer.text, inputPort.text,
|
||||||
|
root.account,
|
||||||
root.address
|
root.address
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
|
|||||||
@ -96,6 +96,8 @@ Item {
|
|||||||
onClicked: bugreportWin.show()
|
onClicked: bugreportWin.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
ButtonIconText {
|
ButtonIconText {
|
||||||
id: autoUpdates
|
id: autoUpdates
|
||||||
text: qsTr("Keep the application up to date", "label for toggle that activates and disables the automatic updates")
|
text: qsTr("Keep the application up to date", "label for toggle that activates and disables the automatic updates")
|
||||||
@ -115,8 +117,6 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
ButtonIconText {
|
ButtonIconText {
|
||||||
id: cacheClear
|
id: cacheClear
|
||||||
text: qsTr("Clear Cache")
|
text: qsTr("Clear Cache")
|
||||||
|
|||||||
@ -18,6 +18,7 @@
|
|||||||
// Dialog with adding new user
|
// Dialog with adding new user
|
||||||
|
|
||||||
import QtQuick 2.8
|
import QtQuick 2.8
|
||||||
|
import QtQuick.Controls 2.1
|
||||||
import QtQuick.Layouts 1.3
|
import QtQuick.Layouts 1.3
|
||||||
import ProtonUI 1.0
|
import ProtonUI 1.0
|
||||||
|
|
||||||
@ -83,6 +84,9 @@ StackLayout {
|
|||||||
text : ""
|
text : ""
|
||||||
color: Style.main.textBlue
|
color: Style.main.textBlue
|
||||||
visible: false
|
visible: false
|
||||||
|
width: root.width
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
}
|
}
|
||||||
|
|
||||||
// prevent any action below
|
// prevent any action below
|
||||||
|
|||||||
@ -70,7 +70,8 @@ Dialog {
|
|||||||
id: topSep
|
id: topSep
|
||||||
color : "transparent"
|
color : "transparent"
|
||||||
width : Style.main.dummy
|
width : Style.main.dummy
|
||||||
height : root.height/2 - (dialogNameAndPassword.heightInputs)/2
|
// Hacky hack: +10 is to make title of Dialog bigger so longer error can fit just fine.
|
||||||
|
height : root.height/2 + 10 - (dialogNameAndPassword.heightInputs)/2
|
||||||
}
|
}
|
||||||
|
|
||||||
InputField {
|
InputField {
|
||||||
|
|||||||
@ -107,7 +107,7 @@ Dialog {
|
|||||||
text: qsTr("Automatically update in the future", "Checkbox label for using autoupdates later on")
|
text: qsTr("Automatically update in the future", "Checkbox label for using autoupdates later on")
|
||||||
checked: go.isAutoUpdate
|
checked: go.isAutoUpdate
|
||||||
onToggled: go.toggleAutoUpdate()
|
onToggled: go.toggleAutoUpdate()
|
||||||
visible: !root.forceUpdate
|
visible: !root.forceUpdate && (go.isAutoUpdate != undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -23,13 +23,13 @@ import QtQuick.Window 2.2
|
|||||||
|
|
||||||
Window {
|
Window {
|
||||||
id : testroot
|
id : testroot
|
||||||
width : 100
|
width : 150
|
||||||
height : 600
|
height : 600
|
||||||
flags : Qt.Window | Qt.Dialog | Qt.FramelessWindowHint
|
flags : Qt.Window | Qt.Dialog | Qt.FramelessWindowHint
|
||||||
visible : true
|
visible : true
|
||||||
title : "GUI test Window"
|
title : "GUI test Window"
|
||||||
color : "transparent"
|
color : "transparent"
|
||||||
x : testgui.winMain.x - 120
|
x : testgui.winMain.x - 170
|
||||||
y : testgui.winMain.y
|
y : testgui.winMain.y
|
||||||
|
|
||||||
property bool newVersion : true
|
property bool newVersion : true
|
||||||
@ -110,8 +110,8 @@ Window {
|
|||||||
ListElement { title: "NotifyManualUpdateRestart" }
|
ListElement { title: "NotifyManualUpdateRestart" }
|
||||||
ListElement { title: "NotifyManualUpdateError" }
|
ListElement { title: "NotifyManualUpdateError" }
|
||||||
ListElement { title: "ForceUpdate" }
|
ListElement { title: "ForceUpdate" }
|
||||||
ListElement { title: "NotifySilentUpdateRestartNeeded" }
|
//ListElement { title: "NotifySilentUpdateRestartNeeded" }
|
||||||
ListElement { title: "NotifySilentUpdateError" }
|
//ListElement { title: "NotifySilentUpdateError" }
|
||||||
ListElement { title : "ImportStructure" }
|
ListElement { title : "ImportStructure" }
|
||||||
ListElement { title : "DraftImpFailed" }
|
ListElement { title : "DraftImpFailed" }
|
||||||
ListElement { title : "NoInterImp" }
|
ListElement { title : "NoInterImp" }
|
||||||
@ -183,12 +183,12 @@ Window {
|
|||||||
case "ForceUpdate" :
|
case "ForceUpdate" :
|
||||||
go.notifyForceUpdate()
|
go.notifyForceUpdate()
|
||||||
break;
|
break;
|
||||||
case "NotifySilentUpdateRestartNeeded" :
|
//case "NotifySilentUpdateRestartNeeded" :
|
||||||
go.notifySilentUpdateRestartNeeded()
|
//go.notifySilentUpdateRestartNeeded()
|
||||||
break;
|
//break;
|
||||||
case "NotifySilentUpdateError" :
|
//case "NotifySilentUpdateError" :
|
||||||
go.notifySilentUpdateError()
|
//go.notifySilentUpdateError()
|
||||||
break;
|
//break;
|
||||||
case "ImportStructure" :
|
case "ImportStructure" :
|
||||||
testgui.winMain.dialogImport.address = "cuto@pm.com"
|
testgui.winMain.dialogImport.address = "cuto@pm.com"
|
||||||
testgui.winMain.dialogImport.show()
|
testgui.winMain.dialogImport.show()
|
||||||
@ -836,7 +836,7 @@ Window {
|
|||||||
id: go
|
id: go
|
||||||
|
|
||||||
property int isAutoStart : 1
|
property int isAutoStart : 1
|
||||||
property bool isAutoUpdate : false
|
//property bool isAutoUpdate : false
|
||||||
property bool isFirstStart : false
|
property bool isFirstStart : false
|
||||||
property string currentAddress : "none"
|
property string currentAddress : "none"
|
||||||
//property string goos : "windows"
|
//property string goos : "windows"
|
||||||
@ -858,15 +858,15 @@ Window {
|
|||||||
|
|
||||||
property string updateState
|
property string updateState
|
||||||
property string updateVersion : "q0.1.0"
|
property string updateVersion : "q0.1.0"
|
||||||
property bool updateCanInstall: true
|
property bool updateCanInstall: false
|
||||||
property string updateLandingPage : "https://protonmail.com/import-export/download/"
|
property string updateLandingPage : "https://protonmail.com/import-export/download/"
|
||||||
property string updateReleaseNotesLink : "https://protonmail.com/download/ie/release_notes.html"
|
property string updateReleaseNotesLink : "https://protonmail.com/download/ie/release_notes.html"
|
||||||
signal notifyManualUpdate()
|
signal notifyManualUpdate()
|
||||||
signal notifyManualUpdateRestartNeeded()
|
signal notifyManualUpdateRestartNeeded()
|
||||||
signal notifyManualUpdateError()
|
signal notifyManualUpdateError()
|
||||||
signal notifyForceUpdate()
|
signal notifyForceUpdate()
|
||||||
signal notifySilentUpdateRestartNeeded()
|
//signal notifySilentUpdateRestartNeeded()
|
||||||
signal notifySilentUpdateError()
|
//signal notifySilentUpdateError()
|
||||||
function checkForUpdates() {
|
function checkForUpdates() {
|
||||||
console.log("checkForUpdates")
|
console.log("checkForUpdates")
|
||||||
go.notifyVersionIsTheLatest()
|
go.notifyVersionIsTheLatest()
|
||||||
@ -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)
|
||||||
}
|
}
|
||||||
@ -1355,10 +1351,10 @@ Window {
|
|||||||
return !fname.includes("fail")
|
return !fname.includes("fail")
|
||||||
}
|
}
|
||||||
|
|
||||||
onToggleAutoUpdate: {
|
//onToggleAutoUpdate: {
|
||||||
workAndClose()
|
// workAndClose()
|
||||||
isAutoUpdate = (isAutoUpdate!=false) ? false : true
|
// isAutoUpdate = (isAutoUpdate!=false) ? false : true
|
||||||
console.log (" Test: onToggleAutoUpdate "+isAutoUpdate)
|
// console.log (" Test: onToggleAutoUpdate "+isAutoUpdate)
|
||||||
}
|
//}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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") {
|
||||||
|
|||||||
@ -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
|
|
||||||
}
|
|
||||||
|
|||||||
@ -29,7 +29,7 @@ const (
|
|||||||
TypeMBOX = "MBOX"
|
TypeMBOX = "MBOX"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (f *FrontendQt) LoadStructureForExport(addressOrID string) {
|
func (f *FrontendQt) LoadStructureForExport(username, addressOrID string) {
|
||||||
errCode := errUnknownError
|
errCode := errUnknownError
|
||||||
var err error
|
var err error
|
||||||
defer func() {
|
defer func() {
|
||||||
@ -41,7 +41,7 @@ func (f *FrontendQt) LoadStructureForExport(addressOrID string) {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if f.transfer, err = f.ie.GetEMLExporter(addressOrID, ""); err != nil {
|
if f.transfer, err = f.ie.GetEMLExporter(username, addressOrID, ""); err != nil {
|
||||||
// The only error can be problem to load PM user and address.
|
// The only error can be problem to load PM user and address.
|
||||||
errCode = errPMLoadFailed
|
errCode = errPMLoadFailed
|
||||||
return
|
return
|
||||||
|
|||||||
@ -135,24 +135,24 @@ func (f *FrontendQt) SetVersion(version updater.VersionInfo) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *FrontendQt) NotifySilentUpdateInstalled() {
|
func (f *FrontendQt) NotifySilentUpdateInstalled() {
|
||||||
f.Qml.NotifySilentUpdateRestartNeeded()
|
//f.Qml.NotifySilentUpdateRestartNeeded()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *FrontendQt) NotifySilentUpdateError(err error) {
|
func (f *FrontendQt) NotifySilentUpdateError(err error) {
|
||||||
f.Qml.NotifySilentUpdateError()
|
//f.Qml.NotifySilentUpdateError()
|
||||||
}
|
}
|
||||||
|
|
||||||
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:
|
||||||
@ -245,11 +245,11 @@ func (f *FrontendQt) QtExecute(Procedure func(*FrontendQt) error) error {
|
|||||||
f.Qml.SetCredits(importexport.Credits)
|
f.Qml.SetCredits(importexport.Credits)
|
||||||
f.Qml.SetFullversion(f.buildVersion)
|
f.Qml.SetFullversion(f.buildVersion)
|
||||||
|
|
||||||
if f.settings.GetBool(settings.AutoUpdateKey) {
|
//if f.settings.GetBool(settings.AutoUpdateKey) {
|
||||||
f.Qml.SetIsAutoUpdate(true)
|
// f.Qml.SetIsAutoUpdate(true)
|
||||||
} else {
|
//} else {
|
||||||
f.Qml.SetIsAutoUpdate(false)
|
// f.Qml.SetIsAutoUpdate(false)
|
||||||
}
|
//}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
defer f.panicHandler.HandlePanic()
|
defer f.panicHandler.HandlePanic()
|
||||||
@ -339,22 +339,17 @@ func (f *FrontendQt) sendBug(description, emailClient, address string) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *FrontendQt) toggleAutoUpdate() {
|
//func (f *FrontendQt) toggleAutoUpdate() {
|
||||||
defer f.Qml.ProcessFinished()
|
// defer f.Qml.ProcessFinished()
|
||||||
|
//
|
||||||
if f.settings.GetBool(settings.AutoUpdateKey) {
|
// if f.settings.GetBool(settings.AutoUpdateKey) {
|
||||||
f.settings.SetBool(settings.AutoUpdateKey, false)
|
// f.settings.SetBool(settings.AutoUpdateKey, false)
|
||||||
f.Qml.SetIsAutoUpdate(false)
|
// f.Qml.SetIsAutoUpdate(false)
|
||||||
} else {
|
// } else {
|
||||||
f.settings.SetBool(settings.AutoUpdateKey, true)
|
// f.settings.SetBool(settings.AutoUpdateKey, true)
|
||||||
f.Qml.SetIsAutoUpdate(true)
|
// f.Qml.SetIsAutoUpdate(true)
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
|
|
||||||
// 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())
|
||||||
|
|||||||
@ -26,7 +26,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// wrapper for QML
|
// wrapper for QML
|
||||||
func (f *FrontendQt) setupAndLoadForImport(isFromIMAP bool, sourcePath, sourceEmail, sourcePassword, sourceServer, sourcePort, targetAddress string) {
|
func (f *FrontendQt) setupAndLoadForImport(isFromIMAP bool, sourcePath, sourceEmail, sourcePassword, sourceServer, sourcePort, targetUsername, targetAddress string) {
|
||||||
errCode := errUnknownError
|
errCode := errUnknownError
|
||||||
var err error
|
var err error
|
||||||
defer func() {
|
defer func() {
|
||||||
@ -39,7 +39,7 @@ func (f *FrontendQt) setupAndLoadForImport(isFromIMAP bool, sourcePath, sourceEm
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
if isFromIMAP {
|
if isFromIMAP {
|
||||||
f.transfer, err = f.ie.GetRemoteImporter(targetAddress, sourceEmail, sourcePassword, sourceServer, sourcePort)
|
f.transfer, err = f.ie.GetRemoteImporter(targetUsername, targetAddress, sourceEmail, sourcePassword, sourceServer, sourcePort)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, &transfer.ErrIMAPConnection{}):
|
case errors.Is(err, &transfer.ErrIMAPConnection{}):
|
||||||
@ -54,7 +54,7 @@ func (f *FrontendQt) setupAndLoadForImport(isFromIMAP bool, sourcePath, sourceEm
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
f.transfer, err = f.ie.GetLocalImporter(targetAddress, sourcePath)
|
f.transfer, err = f.ie.GetLocalImporter(targetUsername, targetAddress, sourcePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// The only error can be problem to load PM user and address.
|
// The only error can be problem to load PM user and address.
|
||||||
errCode = errPMLoadFailed
|
errCode = errPMLoadFailed
|
||||||
|
|||||||
@ -33,7 +33,7 @@ type GoQMLInterface struct {
|
|||||||
|
|
||||||
_ func() `constructor:"init"`
|
_ func() `constructor:"init"`
|
||||||
|
|
||||||
_ bool `property:"isAutoUpdate"`
|
//_ bool `property:"isAutoUpdate"`
|
||||||
_ string `property:"currentAddress"`
|
_ string `property:"currentAddress"`
|
||||||
_ string `property:"goos"`
|
_ string `property:"goos"`
|
||||||
_ string `property:"credits"`
|
_ string `property:"credits"`
|
||||||
@ -62,8 +62,8 @@ type GoQMLInterface struct {
|
|||||||
_ func() `signal:"notifyManualUpdateRestartNeeded"`
|
_ func() `signal:"notifyManualUpdateRestartNeeded"`
|
||||||
_ func() `signal:"notifyManualUpdateError"`
|
_ func() `signal:"notifyManualUpdateError"`
|
||||||
_ func() `signal:"notifyForceUpdate"`
|
_ func() `signal:"notifyForceUpdate"`
|
||||||
_ func() `signal:"notifySilentUpdateRestartNeeded"`
|
//_ func() `signal:"notifySilentUpdateRestartNeeded"`
|
||||||
_ func() `signal:"notifySilentUpdateError"`
|
//_ func() `signal:"notifySilentUpdateError"`
|
||||||
_ func() `slot:"checkForUpdates"`
|
_ func() `slot:"checkForUpdates"`
|
||||||
_ func() `slot:"checkAndOpenReleaseNotes"`
|
_ func() `slot:"checkAndOpenReleaseNotes"`
|
||||||
_ func() `signal:"openReleaseNotesExternally"`
|
_ func() `signal:"openReleaseNotesExternally"`
|
||||||
@ -77,8 +77,7 @@ type GoQMLInterface struct {
|
|||||||
_ string `property:"credentialsNotRemoved"`
|
_ string `property:"credentialsNotRemoved"`
|
||||||
_ 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"`
|
||||||
|
|
||||||
@ -93,7 +92,7 @@ type GoQMLInterface struct {
|
|||||||
|
|
||||||
_ func() `signal:"showWindow"`
|
_ func() `signal:"showWindow"`
|
||||||
|
|
||||||
_ func() `slot:"toggleAutoUpdate"`
|
//_ func() `slot:"toggleAutoUpdate"`
|
||||||
_ func() `slot:"quit"`
|
_ func() `slot:"quit"`
|
||||||
_ func() `slot:"loadAccounts"`
|
_ func() `slot:"loadAccounts"`
|
||||||
_ func() `slot:"openLogs"`
|
_ func() `slot:"openLogs"`
|
||||||
@ -108,14 +107,14 @@ type GoQMLInterface struct {
|
|||||||
|
|
||||||
_ func(description, client, address string) bool `slot:"sendBug"`
|
_ func(description, client, address string) bool `slot:"sendBug"`
|
||||||
_ func(address string) bool `slot:"sendImportReport"`
|
_ func(address string) bool `slot:"sendImportReport"`
|
||||||
_ func(address string) `slot:"loadStructureForExport"`
|
_ func(username, address string) `slot:"loadStructureForExport"`
|
||||||
_ func() string `slot:"leastUsedColor"`
|
_ func() string `slot:"leastUsedColor"`
|
||||||
_ func(username string, name string, color string, isLabel bool, sourceID string) bool `slot:"createLabelOrFolder"`
|
_ func(username string, name string, color string, isLabel bool, sourceID string) bool `slot:"createLabelOrFolder"`
|
||||||
_ func(fpath, address, fileType string, attachEncryptedBody bool) `slot:"startExport"`
|
_ func(fpath, address, fileType string, attachEncryptedBody bool) `slot:"startExport"`
|
||||||
_ func(email string, importEncrypted bool) `slot:"startImport"`
|
_ func(email string, importEncrypted bool) `slot:"startImport"`
|
||||||
_ func() `slot:"resetSource"`
|
_ func() `slot:"resetSource"`
|
||||||
|
|
||||||
_ func(isFromIMAP bool, sourcePath, sourceEmail, sourcePassword, sourceServe, sourcePort, targetAddress string) `slot:"setupAndLoadForImport"`
|
_ func(isFromIMAP bool, sourcePath, sourceEmail, sourcePassword, sourceServe, sourcePort, targetUsername, targetAddress string) `slot:"setupAndLoadForImport"`
|
||||||
|
|
||||||
_ string `property:"progressInit"`
|
_ string `property:"progressInit"`
|
||||||
|
|
||||||
@ -162,7 +161,7 @@ func (s *GoQMLInterface) init() {}
|
|||||||
func (s *GoQMLInterface) SetFrontend(f *FrontendQt) {
|
func (s *GoQMLInterface) SetFrontend(f *FrontendQt) {
|
||||||
s.ConnectQuit(f.App.Quit)
|
s.ConnectQuit(f.App.Quit)
|
||||||
|
|
||||||
s.ConnectToggleAutoUpdate(f.toggleAutoUpdate)
|
//s.ConnectToggleAutoUpdate(f.toggleAutoUpdate)
|
||||||
s.ConnectLoadAccounts(f.Accounts.LoadAccounts)
|
s.ConnectLoadAccounts(f.Accounts.LoadAccounts)
|
||||||
s.ConnectOpenLogs(f.openLogs)
|
s.ConnectOpenLogs(f.openLogs)
|
||||||
s.ConnectOpenDownloadLink(f.openDownloadLink)
|
s.ConnectOpenDownloadLink(f.openDownloadLink)
|
||||||
@ -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)
|
||||||
@ -207,4 +204,6 @@ func (s *GoQMLInterface) SetFrontend(f *FrontendQt) {
|
|||||||
s.ConnectCheckPathStatus(CheckPathStatus)
|
s.ConnectCheckPathStatus(CheckPathStatus)
|
||||||
|
|
||||||
s.ConnectEmitEvent(f.emitEvent)
|
s.ConnectEmitEvent(f.emitEvent)
|
||||||
|
|
||||||
|
s.ConnectStartManualUpdate(f.startManualUpdate)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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") {
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
IDI_ICON1 ICON DISCARDABLE "logo.ico"
|
|
||||||
45
internal/frontend/share/info.rc
Normal file
45
internal/frontend/share/info.rc
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
#define STRINGIZE_(x) #x
|
||||||
|
#define STRINGIZE(x) STRINGIZE_(x)
|
||||||
|
|
||||||
|
IDI_ICON1 ICON DISCARDABLE STRINGIZE(ICO_FILE)
|
||||||
|
|
||||||
|
#if defined BUILD_BRIDGE
|
||||||
|
#define FILE_COMMENTS "The Bridge is an application that runs on your computer in the background and seamlessly encrypts and decrypts your mail as it enters and leaves your computer."
|
||||||
|
#define FILE_DESCRIPTION "ProtonMail Bridge"
|
||||||
|
#define INTERNAL_NAME STRINGIZE(EXE_NAME)
|
||||||
|
#define PRODUCT_NAME "ProtonMail Bridge for Windows"
|
||||||
|
#elif defined BUILD_IE
|
||||||
|
#define FILE_COMMENTS "The Import-Export app helps you to migrate your emails from local files or remote IMAP servers to ProtonMail or simply export emails to local folder."
|
||||||
|
#define FILE_DESCRIPTION "ProtonMail Import-Export app"
|
||||||
|
#define INTERNAL_NAME STRINGIZE(EXE_NAME)
|
||||||
|
#define PRODUCT_NAME "ProtonMail Import-Export app for Windows"
|
||||||
|
#else
|
||||||
|
#error No target specified
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define LEGAL_COPYRIGHT "(C) " STRINGIZE(YEAR) " Proton Technologies AG"
|
||||||
|
|
||||||
|
1 VERSIONINFO
|
||||||
|
FILEVERSION FILE_VERSION_COMMA,0
|
||||||
|
PRODUCTVERSION FILE_VERSION_COMMA,0
|
||||||
|
BEGIN
|
||||||
|
BLOCK "StringFileInfo"
|
||||||
|
BEGIN
|
||||||
|
BLOCK "040904b0"
|
||||||
|
BEGIN
|
||||||
|
VALUE "Comments", FILE_COMMENTS
|
||||||
|
VALUE "CompanyName", "Proton Technologies AG"
|
||||||
|
VALUE "FileDescription", FILE_DESCRIPTION
|
||||||
|
VALUE "FileVersion", STRINGIZE(FILE_VERSION)
|
||||||
|
VALUE "InternalName", INTERNAL_NAME
|
||||||
|
VALUE "LegalCopyright", LEGAL_COPYRIGHT
|
||||||
|
VALUE "OriginalFilename", STRINGIZE(ORIGINAL_FILE_NAME)
|
||||||
|
VALUE "ProductName", PRODUCT_NAME
|
||||||
|
VALUE "ProductVersion", STRINGIZE(PRODUCT_VERSION)
|
||||||
|
END
|
||||||
|
END
|
||||||
|
BLOCK "VarFileInfo"
|
||||||
|
BEGIN
|
||||||
|
VALUE "Translation", 0x0409, 0x04B0
|
||||||
|
END
|
||||||
|
END
|
||||||
@ -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.
|
||||||
@ -114,10 +113,10 @@ func (b *bridgeWrap) GetUser(query string) (User, error) {
|
|||||||
type ImportExporter interface {
|
type ImportExporter interface {
|
||||||
UserManager
|
UserManager
|
||||||
|
|
||||||
GetLocalImporter(string, string) (*transfer.Transfer, error)
|
GetLocalImporter(string, string, string) (*transfer.Transfer, error)
|
||||||
GetRemoteImporter(string, string, string, string, string) (*transfer.Transfer, error)
|
GetRemoteImporter(string, string, string, string, string, string) (*transfer.Transfer, error)
|
||||||
GetEMLExporter(string, string) (*transfer.Transfer, error)
|
GetEMLExporter(string, string, string) (*transfer.Transfer, error)
|
||||||
GetMBOXExporter(string, string) (*transfer.Transfer, error)
|
GetMBOXExporter(string, string, string) (*transfer.Transfer, error)
|
||||||
ReportBug(osType, osVersion, description, accountName, address, emailClient string) error
|
ReportBug(osType, osVersion, description, accountName, address, emailClient string) error
|
||||||
ReportFile(osType, osVersion, accountName, address string, logdata []byte) error
|
ReportFile(osType, osVersion, accountName, address string, logdata []byte) error
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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{},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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]
|
|
||||||
)
|
|
||||||
|
|||||||
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
198
internal/imap/mailbox_append.go
Normal file
198
internal/imap/mailbox_append.go
Normal 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)
|
||||||
|
}
|
||||||
322
internal/imap/mailbox_fetch.go
Normal file
322
internal/imap/mailbox_fetch.go
Normal 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
|
||||||
|
}
|
||||||
67
internal/imap/mailbox_fetch_test.go
Normal file
67
internal/imap/mailbox_fetch_test.go
Normal 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")
|
||||||
|
})))
|
||||||
|
}
|
||||||
104
internal/imap/mailbox_header.go
Normal file
104
internal/imap/mailbox_header.go
Normal 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
|
||||||
|
}
|
||||||
@ -1,790 +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([]string{m.ID})
|
|
||||||
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) (msg *imap.Message, err error) {
|
|
||||||
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")
|
|
||||||
if _, _, err = im.getBodyAndStructure(storeMessage); 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); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return msg, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (im *imapMailbox) getLiteralForSection(itemSection imap.FetchItem, msg *imap.Message, storeMessage storeMessageProvider) error {
|
|
||||||
section, err := imap.ParseBodySectionName(itemSection)
|
|
||||||
if err != nil { // Ignore error
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var literal imap.Literal
|
|
||||||
if literal, err = im.getMessageBodySection(storeMessage, section); 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 {
|
|
||||||
if bs, _, err = im.getBodyAndStructure(storeMessage); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (im *imapMailbox) getBodyAndStructure(storeMessage storeMessageProvider) (
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
// 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) (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)
|
|
||||||
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)
|
|
||||||
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)
|
|
||||||
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
|
|
||||||
}
|
|
||||||
@ -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) {
|
||||||
@ -482,12 +489,13 @@ func (im *imapMailbox) SearchMessages(isUID bool, criteria *imap.SearchCriteria)
|
|||||||
//
|
//
|
||||||
// Messages must be sent to msgResponse. When the function returns, msgResponse must be closed.
|
// Messages must be sent to msgResponse. When the function returns, msgResponse must be closed.
|
||||||
func (im *imapMailbox) ListMessages(isUID bool, seqSet *imap.SeqSet, items []imap.FetchItem, msgResponse chan<- *imap.Message) error {
|
func (im *imapMailbox) ListMessages(isUID bool, seqSet *imap.SeqSet, items []imap.FetchItem, msgResponse chan<- *imap.Message) error {
|
||||||
|
msgBuildCountHistogram := newMsgBuildCountHistogram()
|
||||||
return im.logCommand(func() error {
|
return im.logCommand(func() error {
|
||||||
return im.listMessages(isUID, seqSet, items, msgResponse)
|
return im.listMessages(isUID, seqSet, items, msgResponse, msgBuildCountHistogram)
|
||||||
}, "FETCH", isUID, seqSet, items)
|
}, "FETCH", isUID, seqSet, items, msgBuildCountHistogram)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (im *imapMailbox) listMessages(isUID bool, seqSet *imap.SeqSet, items []imap.FetchItem, msgResponse chan<- *imap.Message) (err error) { //nolint[funlen]
|
func (im *imapMailbox) listMessages(isUID bool, seqSet *imap.SeqSet, items []imap.FetchItem, msgResponse chan<- *imap.Message, msgBuildCountHistogram *msgBuildCountHistogram) (err error) { //nolint[funlen]
|
||||||
defer func() {
|
defer func() {
|
||||||
close(msgResponse)
|
close(msgResponse)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -517,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 {
|
||||||
@ -544,14 +540,14 @@ func (im *imapMailbox) listMessages(isUID bool, seqSet *imap.SeqSet, items []ima
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
msg, err := im.getMessage(storeMessage, items)
|
msg, err := im.getMessage(storeMessage, items, msgBuildCountHistogram)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = fmt.Errorf("list message build: %v", err)
|
err = fmt.Errorf("list message build: %v", err)
|
||||||
l.WithField("metaID", storeMessage.ID()).Error(err)
|
l.WithField("metaID", storeMessage.ID()).Error(err)
|
||||||
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.
|
||||||
@ -569,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
|
||||||
}
|
}
|
||||||
|
|||||||
65
internal/imap/msg_build_counts.go
Normal file
65
internal/imap/msg_build_counts.go
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
// 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 (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// msgBuildCountHistogram is used to analyse and log the number of repetitive
|
||||||
|
// downloads of requested messages per one fetch. The number of builds per each
|
||||||
|
// messageID is stored in persistent database. The msgBuildCountHistogram will
|
||||||
|
// take this number for each message in ongoing fetch and create histogram of
|
||||||
|
// repeats.
|
||||||
|
//
|
||||||
|
// Example: During `fetch 1:300` there were
|
||||||
|
// - 100 messages were downloaded first time
|
||||||
|
// - 100 messages were downloaded second time
|
||||||
|
// - 99 messages were downloaded 10th times
|
||||||
|
// - 1 messages were downloaded 100th times.
|
||||||
|
type msgBuildCountHistogram struct {
|
||||||
|
// Key represents how many times message was build.
|
||||||
|
// Value stores how many messages are build X times based on the key.
|
||||||
|
counts map[uint32]uint32
|
||||||
|
lock sync.Locker
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMsgBuildCountHistogram() *msgBuildCountHistogram {
|
||||||
|
return &msgBuildCountHistogram{
|
||||||
|
counts: map[uint32]uint32{},
|
||||||
|
lock: &sync.Mutex{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *msgBuildCountHistogram) String() string {
|
||||||
|
res := ""
|
||||||
|
for nRebuild, counts := range c.counts {
|
||||||
|
if res != "" {
|
||||||
|
res += ", "
|
||||||
|
}
|
||||||
|
res += fmt.Sprintf("[%d]:%d", nRebuild, counts)
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *msgBuildCountHistogram) add(nRebuild uint32) {
|
||||||
|
c.lock.Lock()
|
||||||
|
defer c.lock.Unlock()
|
||||||
|
c.counts[nRebuild]++
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
|||||||
@ -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))
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,9 +101,13 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
type storeUserWrap struct {
|
type storeUserWrap struct {
|
||||||
@ -122,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 {
|
||||||
@ -136,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
|
||||||
}
|
}
|
||||||
@ -146,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 {
|
||||||
|
|||||||
@ -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),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
@ -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,69 +61,44 @@ 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.
|
||||||
func (ie *ImportExport) GetLocalImporter(address, path string) (*transfer.Transfer, error) {
|
func (ie *ImportExport) GetLocalImporter(username, address, path string) (*transfer.Transfer, error) {
|
||||||
source := transfer.NewLocalProvider(path)
|
source := transfer.NewLocalProvider(path)
|
||||||
target, err := ie.getPMAPIProvider(address)
|
target, err := ie.getPMAPIProvider(username, address)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -132,12 +110,12 @@ func (ie *ImportExport) GetLocalImporter(address, path string) (*transfer.Transf
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetRemoteImporter returns transferrer from remote IMAP to ProtonMail account.
|
// GetRemoteImporter returns transferrer from remote IMAP to ProtonMail account.
|
||||||
func (ie *ImportExport) GetRemoteImporter(address, username, password, host, port string) (*transfer.Transfer, error) {
|
func (ie *ImportExport) GetRemoteImporter(username, address, remoteUsername, remotePassword, host, port string) (*transfer.Transfer, error) {
|
||||||
source, err := transfer.NewIMAPProvider(username, password, host, port)
|
source, err := transfer.NewIMAPProvider(remoteUsername, remotePassword, host, port)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
target, err := ie.getPMAPIProvider(address)
|
target, err := ie.getPMAPIProvider(username, address)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -149,8 +127,8 @@ func (ie *ImportExport) GetRemoteImporter(address, username, password, host, por
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetEMLExporter returns transferrer from ProtonMail account to local EML structure.
|
// GetEMLExporter returns transferrer from ProtonMail account to local EML structure.
|
||||||
func (ie *ImportExport) GetEMLExporter(address, path string) (*transfer.Transfer, error) {
|
func (ie *ImportExport) GetEMLExporter(username, address, path string) (*transfer.Transfer, error) {
|
||||||
source, err := ie.getPMAPIProvider(address)
|
source, err := ie.getPMAPIProvider(username, address)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -163,8 +141,8 @@ func (ie *ImportExport) GetEMLExporter(address, path string) (*transfer.Transfer
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetMBOXExporter returns transferrer from ProtonMail account to local MBOX structure.
|
// GetMBOXExporter returns transferrer from ProtonMail account to local MBOX structure.
|
||||||
func (ie *ImportExport) GetMBOXExporter(address, path string) (*transfer.Transfer, error) {
|
func (ie *ImportExport) GetMBOXExporter(username, address, path string) (*transfer.Transfer, error) {
|
||||||
source, err := ie.getPMAPIProvider(address)
|
source, err := ie.getPMAPIProvider(username, address)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -176,8 +154,8 @@ func (ie *ImportExport) GetMBOXExporter(address, path string) (*transfer.Transfe
|
|||||||
return transfer.New(ie.panicHandler, newExportMetricsManager(ie), logsPath, ie.cache.GetTransferDir(), source, target)
|
return transfer.New(ie.panicHandler, newExportMetricsManager(ie), logsPath, ie.cache.GetTransferDir(), source, target)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ie *ImportExport) getPMAPIProvider(address string) (*transfer.PMAPIProvider, error) {
|
func (ie *ImportExport) getPMAPIProvider(username, address string) (*transfer.PMAPIProvider, error) {
|
||||||
user, err := ie.Users.GetUser(address)
|
user, err := ie.Users.GetUser(username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -187,5 +165,23 @@ func (ie *ImportExport) getPMAPIProvider(address string) (*transfer.PMAPIProvide
|
|||||||
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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,9 +32,9 @@ import (
|
|||||||
// On linux:
|
// On linux:
|
||||||
// - settings: ~/.config/protonmail/<app>
|
// - settings: ~/.config/protonmail/<app>
|
||||||
// - logs: ~/.cache/protonmail/<app>/logs
|
// - logs: ~/.cache/protonmail/<app>/logs
|
||||||
// - cache: ~/.cache/protonmail/<app>/cache
|
// - cache: ~/.config/protonmail/<app>/cache
|
||||||
// - updates: ~/.cache/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
|
||||||
@ -129,7 +129,7 @@ func (l *Locations) ProvideLogsPath() (string, error) {
|
|||||||
return l.getLogsPath(), nil
|
return l.getLogsPath(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProvideCachePath returns a location for user cache dirs (e.g. ~/.cache/<company>/<app>/cache).
|
// ProvideCachePath returns a location for user cache dirs (e.g. ~/.config/<company>/<app>/cache).
|
||||||
// It creates it if it doesn't already exist.
|
// It creates it if it doesn't already exist.
|
||||||
func (l *Locations) ProvideCachePath() (string, error) {
|
func (l *Locations) ProvideCachePath() (string, error) {
|
||||||
if err := os.MkdirAll(l.getCachePath(), 0700); err != nil {
|
if err := os.MkdirAll(l.getCachePath(), 0700); err != nil {
|
||||||
@ -139,6 +139,11 @@ func (l *Locations) ProvideCachePath() (string, error) {
|
|||||||
return l.getCachePath(), nil
|
return l.getCachePath(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetOldCachePath returns a former location for user cache dirs used for migration scripts only.
|
||||||
|
func (l *Locations) GetOldCachePath() string {
|
||||||
|
return filepath.Join(l.userCache, "cache")
|
||||||
|
}
|
||||||
|
|
||||||
// ProvideUpdatesPath returns a location for update files (e.g. ~/.cache/<company>/<app>/updates).
|
// ProvideUpdatesPath returns a location for update files (e.g. ~/.cache/<company>/<app>/updates).
|
||||||
// It creates it if it doesn't already exist.
|
// It creates it if it doesn't already exist.
|
||||||
func (l *Locations) ProvideUpdatesPath() (string, error) {
|
func (l *Locations) ProvideUpdatesPath() (string, error) {
|
||||||
@ -149,6 +154,16 @@ func (l *Locations) ProvideUpdatesPath() (string, error) {
|
|||||||
return l.getUpdatesPath(), nil
|
return l.getUpdatesPath(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetUpdatesPath returns a new location for update files used for migration scripts only.
|
||||||
|
func (l *Locations) GetUpdatesPath() string {
|
||||||
|
return l.getUpdatesPath()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOldUpdatesPath returns a former location for update files used for migration scripts only.
|
||||||
|
func (l *Locations) GetOldUpdatesPath() string {
|
||||||
|
return filepath.Join(l.userCache, "updates")
|
||||||
|
}
|
||||||
|
|
||||||
func (l *Locations) getSettingsPath() string {
|
func (l *Locations) getSettingsPath() string {
|
||||||
return l.userConfig
|
return l.userConfig
|
||||||
}
|
}
|
||||||
@ -158,11 +173,33 @@ func (l *Locations) getLogsPath() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (l *Locations) getCachePath() string {
|
func (l *Locations) getCachePath() string {
|
||||||
return filepath.Join(l.userCache, "cache")
|
// Bridge cache is not a typical cache which can be deleted with only
|
||||||
|
// downside that the app has to download everything again.
|
||||||
|
// Cache for bridge is database with IMAP UIDs and UIDVALIDITY, and also
|
||||||
|
// other IMAP setup. Deleting such data leads to either re-sync of client,
|
||||||
|
// or mix of headers and bodies. Both is caused because of need of re-sync
|
||||||
|
// between Bridge and API which will happen in different order than before.
|
||||||
|
// In the first case, UIDVALIDITY is also changed and causes the better
|
||||||
|
// outcome to "just" re-sync everything; in the later, UIDVALIDITY stays
|
||||||
|
// the same, causing the client to not re-sync but UIDs in the client does
|
||||||
|
// not match UIDs in Bridge.
|
||||||
|
// Because users might use tools to regularly clear caches, Bridge cache
|
||||||
|
// cannot be located in a standard cache folder.
|
||||||
|
return filepath.Join(l.userConfig, "cache")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Locations) getUpdatesPath() string {
|
func (l *Locations) getUpdatesPath() string {
|
||||||
return filepath.Join(l.userCache, "updates")
|
// In order to properly update Bridge 1.6.X and higher we need to
|
||||||
|
// change the launcher first. Since this is not part of automatic
|
||||||
|
// updates the migration must wait until manual update. Until that
|
||||||
|
// we need to keep old path.
|
||||||
|
if l.configName == "bridge" {
|
||||||
|
return l.GetOldUpdatesPath()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Users might use tools to regularly clear caches, which would mean always
|
||||||
|
// removing updates, therefore Bridge updates have to be somewhere else.
|
||||||
|
return filepath.Join(l.userConfig, "updates")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear removes everything except the lock and update files.
|
// Clear removes everything except the lock and update files.
|
||||||
|
|||||||
@ -45,7 +45,8 @@ func TestClearRemovesEverythingExceptLockAndUpdateFiles(t *testing.T) {
|
|||||||
assert.NoError(t, l.Clear())
|
assert.NoError(t, l.Clear())
|
||||||
|
|
||||||
assert.FileExists(t, l.GetLockFile())
|
assert.FileExists(t, l.GetLockFile())
|
||||||
assert.NoDirExists(t, l.getSettingsPath())
|
assert.DirExists(t, l.getSettingsPath())
|
||||||
|
assert.NoFileExists(t, filepath.Join(l.getSettingsPath(), "prefs.json"))
|
||||||
assert.NoDirExists(t, l.getLogsPath())
|
assert.NoDirExists(t, l.getLogsPath())
|
||||||
assert.NoDirExists(t, l.getCachePath())
|
assert.NoDirExists(t, l.getCachePath())
|
||||||
assert.DirExists(t, l.getUpdatesPath())
|
assert.DirExists(t, l.getUpdatesPath())
|
||||||
@ -58,6 +59,7 @@ func TestClearUpdateFiles(t *testing.T) {
|
|||||||
|
|
||||||
assert.FileExists(t, l.GetLockFile())
|
assert.FileExists(t, l.GetLockFile())
|
||||||
assert.DirExists(t, l.getSettingsPath())
|
assert.DirExists(t, l.getSettingsPath())
|
||||||
|
assert.FileExists(t, filepath.Join(l.getSettingsPath(), "prefs.json"))
|
||||||
assert.DirExists(t, l.getLogsPath())
|
assert.DirExists(t, l.getLogsPath())
|
||||||
assert.DirExists(t, l.getCachePath())
|
assert.DirExists(t, l.getCachePath())
|
||||||
assert.NoDirExists(t, l.getUpdatesPath())
|
assert.NoDirExists(t, l.getUpdatesPath())
|
||||||
@ -75,6 +77,7 @@ func TestCleanLeavesStandardLocationsUntouched(t *testing.T) {
|
|||||||
|
|
||||||
assert.FileExists(t, l.GetLockFile())
|
assert.FileExists(t, l.GetLockFile())
|
||||||
assert.DirExists(t, l.getSettingsPath())
|
assert.DirExists(t, l.getSettingsPath())
|
||||||
|
assert.FileExists(t, filepath.Join(l.getSettingsPath(), "prefs.json"))
|
||||||
assert.DirExists(t, l.getLogsPath())
|
assert.DirExists(t, l.getLogsPath())
|
||||||
assert.FileExists(t, filepath.Join(l.getLogsPath(), "log1.txt"))
|
assert.FileExists(t, filepath.Join(l.getLogsPath(), "log1.txt"))
|
||||||
assert.FileExists(t, filepath.Join(l.getLogsPath(), "log2.txt"))
|
assert.FileExists(t, filepath.Join(l.getLogsPath(), "log2.txt"))
|
||||||
@ -138,6 +141,9 @@ func newTestLocations(t *testing.T) *Locations {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.DirExists(t, settings)
|
require.DirExists(t, settings)
|
||||||
|
|
||||||
|
createFilesInDir(t, settings, "prefs.json")
|
||||||
|
require.FileExists(t, filepath.Join(settings, "prefs.json"))
|
||||||
|
|
||||||
logs, err := l.ProvideLogsPath()
|
logs, err := l.ProvideLogsPath()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.DirExists(t, logs)
|
require.DirExists(t, logs)
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
150
internal/serverutil/mocks/server.go
Normal file
150
internal/serverutil/mocks/server.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
132
internal/serverutil/server.go
Normal file
132
internal/serverutil/server.go
Normal 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)
|
||||||
|
}
|
||||||
@ -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)
|
||||||
}
|
}
|
||||||
@ -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 {
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -20,29 +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
|
|
||||||
|
|
||||||
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("================================================")
|
||||||
@ -50,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 {
|
||||||
@ -67,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)
|
})
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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))
|
||||||
|
|||||||
@ -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")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -268,8 +274,6 @@ func (loop *eventLoop) processNextEvent() (more bool, err error) { // nolint[fun
|
|||||||
return false, errors.New("received empty event")
|
return false, errors.New("received empty event")
|
||||||
}
|
}
|
||||||
|
|
||||||
l = l.WithField("newEventID", event.EventID)
|
|
||||||
|
|
||||||
if err = loop.processEvent(event); err != nil {
|
if err = loop.processEvent(event); err != nil {
|
||||||
return false, errors.Wrap(err, "failed to process event")
|
return false, errors.Wrap(err, "failed to process event")
|
||||||
}
|
}
|
||||||
@ -287,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) {
|
||||||
@ -346,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")
|
||||||
}
|
}
|
||||||
@ -383,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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -411,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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -453,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
|
||||||
|
|||||||
@ -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},
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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())
|
||||||
|
|||||||
@ -36,23 +36,41 @@ 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()
|
||||||
|
|
||||||
|
// GODT-1153 If the mailbox is empty we should reply BAD to client.
|
||||||
|
if uid, _ := c.Last(); uid == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 +78,52 @@ 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()
|
||||||
|
|
||||||
|
// GODT-1153 If the mailbox is empty we should reply BAD to client.
|
||||||
|
if uid, _ := c.Last(); uid == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 +325,13 @@ 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()
|
||||||
|
|
||||||
|
if uid == nil {
|
||||||
|
panic(errors.New("cannot get final UID of empty mailbox"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return btoi(uid)
|
||||||
|
}
|
||||||
|
|||||||
@ -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))
|
||||||
|
|
||||||
|
|||||||
@ -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,19 +58,25 @@ 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 && len(res) > 0 {
|
if err != nil {
|
||||||
msg.ID = res[0].MessageID
|
return err
|
||||||
}
|
}
|
||||||
return err
|
if len(res) == 0 {
|
||||||
|
return errors.New("no import response")
|
||||||
|
}
|
||||||
|
msg.ID = res[0].MessageID
|
||||||
|
return res[0].Error
|
||||||
}
|
}
|
||||||
|
|
||||||
// LabelMessages adds the label by calling an API.
|
// LabelMessages adds the label by calling an API.
|
||||||
@ -91,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.
|
||||||
@ -104,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.
|
||||||
@ -124,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.
|
||||||
@ -143,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.
|
||||||
@ -156,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.
|
||||||
@ -169,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,
|
||||||
@ -253,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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -295,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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 {
|
||||||
@ -148,3 +205,17 @@ func (message *Message) GetBodyStructure() (bs *pkgMsg.BodyStructure, err error)
|
|||||||
}
|
}
|
||||||
return bs, nil
|
return bs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (message *Message) IncreaseBuildCount() (times uint32, err error) {
|
||||||
|
txUpdate := func(tx *bolt.Tx) error {
|
||||||
|
times, err = message.store.txIncreaseMsgBuildCount(
|
||||||
|
tx.Bucket(msgBuildCountBucket),
|
||||||
|
message.ID(),
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = message.store.db.Update(txUpdate); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return times, nil
|
||||||
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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,9 +52,13 @@ 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
|
||||||
|
// * {messageID} -> uint32 number of message builds to track re-sync issues
|
||||||
// * counts
|
// * counts
|
||||||
// * {mailboxID} -> mailboxCounts: totalOnAPI, unreadOnAPI, labelName, labelColor, labelIsExclusive
|
// * {mailboxID} -> mailboxCounts: totalOnAPI, unreadOnAPI, labelName, labelColor, labelIsExclusive
|
||||||
// * address_info
|
// * address_info
|
||||||
@ -75,7 +80,9 @@ 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]
|
||||||
countsBucket = []byte("counts") //nolint[gochecknoglobals]
|
countsBucket = []byte("counts") //nolint[gochecknoglobals]
|
||||||
addressInfoBucket = []byte("address_info") //nolint[gochecknoglobals]
|
addressInfoBucket = []byte("address_info") //nolint[gochecknoglobals]
|
||||||
addressModeBucket = []byte("address_mode") //nolint[gochecknoglobals]
|
addressModeBucket = []byte("address_mode") //nolint[gochecknoglobals]
|
||||||
@ -94,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
|
||||||
|
|
||||||
@ -121,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())
|
||||||
@ -150,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,
|
||||||
@ -196,36 +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(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
|
||||||
@ -263,7 +267,7 @@ func (store *Store) init(firstInit bool) (err error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
store.log.WithField("mode", store.addressMode).Debug("Initialising store")
|
store.log.WithField("mode", store.addressMode).Info("Initialising store")
|
||||||
|
|
||||||
labels, err := store.initCounts()
|
labels, err := store.initCounts()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -280,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")
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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))
|
||||||
|
|||||||
@ -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")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user