Compare commits

...

79 Commits

Author SHA1 Message Date
5ad23715ec Other: Release Bridge Iron v1.7.0 2021-04-15 13:27:05 +02:00
8ab05a000c GODT-1136 DB Cache header from builder and test 2021-04-15 09:51:08 +00:00
454d248819 GODT-213: Preserve contenttype for undecryptable message body 2021-04-15 09:51:08 +00:00
6c8e5f7cd3 GODT-213: Use application/octet-stream for encrypted parts 2021-04-15 09:51:08 +00:00
f5aba717b2 GODT-213: Force no transfer encoding for embedded message/rfc822 parts 2021-04-15 09:51:08 +00:00
1359c39bc0 GODT-213: Remove dead code GetRelatedHeader/GetRelatedBoundary 2021-04-15 09:51:08 +00:00
4850681f1d GODT-213: correctly expect text/plain in custom message text parts 2021-04-15 09:51:08 +00:00
aa55c69307 Other: fix linter 2021-04-15 09:51:08 +00:00
1f19d4df75 GODT-213: Force text/plain for custom message text part 2021-04-15 09:51:08 +00:00
c0f6af9eb5 GODT-213: Complex external encrypted tests (multipart/alternative, message/rfc822 attachment) 2021-04-15 09:51:08 +00:00
ef6a3d4999 GODT-213: Add comments for newly added code 2021-04-15 09:51:08 +00:00
50550d42b4 GODT-213: Message Builder 2021-04-15 09:51:08 +00:00
8db89a1a6c GODT-1113: Fix tray icon size on macOS Big Sur.
Add patched libqcocoa based on Qt 5.13.0
2021-04-15 09:08:19 +00:00
ba1dfb1bf4 GODT-947 Force colors in logs 2021-04-15 07:20:53 +00:00
d243880753 Other: stop rejecting old TLS versions 2021-04-14 09:28:31 +02:00
cccaaa3d82 Other: turn off bad login in live test 2021-04-12 06:16:34 +02:00
2d95f21567 Other: add straightforward linters 2021-04-08 16:09:40 +02:00
7d0af7624c Other: Bump linter 2021-04-07 10:54:09 +02:00
2f35c453a1 Other: Release notes stable 2021-04-01 08:05:04 +02:00
05dd137bc8 Other: Release notes 2021-03-31 06:52:00 +02:00
767628946f Other: Bridge HZM 1.6.9 2021-03-29 12:08:46 +02:00
d4efa7131f GODT-1121 Initial value of silent updates toggle button 2021-03-29 06:15:33 +02:00
144cf6e40c Other: Bridge HZM 1.6.8 & Import-Export Farg 1.3.3 2021-03-26 11:17:01 +01:00
a205d8c046 GODT-1120 hotfix: use Info level in internal/app logs 2021-03-25 11:33:32 +01:00
cccadaee42 Other: Bridge HZM 1.6.7 & Import-Export Farg 1.3.2 2021-03-24 15:11:46 +01:00
bbb365f8a5 Merge branch 'release/farg' into devel 2021-03-24 14:55:55 +01:00
1f18d9d917 GODT-1117 Do not change updates location for Bridge now 2021-03-24 10:45:55 +01:00
59e0d63485 GODT-1105 Fix: Dylib hijack vulnerability found by https://objective-see.com/products/dhs.html 2021-03-24 08:37:30 +00:00
72fe5a636e GODT-1063: Add metainfo to launcher
Refactor metainfo file a bit
2021-03-24 07:04:28 +00:00
45a83133ba Other: increase SMTP line limit to 2^16 2021-03-17 11:45:54 +01:00
215eb4d6eb GODT-1085 Ignore live test of importing to sent and custom label 2021-03-17 08:10:50 +01:00
479b951c50 GODT-1076 Fix UIDPLUS response for importing existing message 2021-03-16 11:55:36 +00:00
a94c8a943f GODT-1077 IMAP sync counting 2021-03-16 12:35:36 +01:00
ea306f405e Other: print address mode in info level 2021-03-12 09:02:54 +01:00
1b405506b8 Merge remote-tracking branch 'remotes/origin/release-notes' into farg 2021-03-11 00:01:21 +01:00
38c6132f81 Other: Import-Export Farg 1.3.1 2021-03-11 00:00:40 +01:00
b7351dfaf8 Other 2021-03-10 21:52:52 +00:00
7e8f6943f2 Other 2021-03-10 21:35:31 +00:00
a0132e8440 GODT-1047 No silent updates for Import-Export app 2021-03-10 18:56:55 +00:00
27541784aa Merge master into devel 2021-03-10 14:52:45 +01:00
9e567f08b2 Other: release notes for 1.6.6 stable 2021-03-04 11:56:11 +00:00
bf274f984e Other: include latest go.mod/go.sum changes 2021-03-04 11:25:33 +00:00
3b60bbe13b Other 2021-03-04 09:50:29 +00:00
a73a1b623a GODT-803 Fix import to wrong target address 2021-03-02 16:02:23 +01:00
c0a8877018 Other: include latest go.mod/go.sum changes 2021-03-01 17:48:22 +01:00
904166c01c GODT-247 Cache and update files moved from user's cache to config 2021-03-01 14:06:58 +00:00
4761bc935a GODT-948 Embedded messages 2021-03-01 09:22:08 +00:00
71301d891f Other 2021-02-28 20:55:23 +01:00
d47be3c4c0 GODT-1043 Fix showing long login error in GUI dialog 2021-02-26 12:21:12 +00:00
199a4d1e3a Other: Release Bridge HZM 1.6.6 2021-02-25 16:28:17 +01:00
18668aafc9 GODT-1029: Fix tray icon not updating under certain conditions 2021-02-25 14:53:43 +00:00
fd73ec6861 GODT-1062: Fix lost notification bar when window is closed 2021-02-25 14:53:43 +00:00
feeb7179f5 GODT-1058 Install after chaning channel right away only in case of downgrade 2021-02-25 14:47:12 +00:00
0e5a45671f GODT-1073 Added: Re-write autostart link on every start if turned on in preferences. 2021-02-24 19:32:59 +00:00
2beb0d298e Other: QA build checks for update every 5 minutes 2021-02-24 20:34:13 +01:00
22a6fcd87f Other: add debug message dump when sending 2021-02-23 10:38:15 +00:00
f499252444 GODT-1055 Fix flaky empty trash test 2021-02-23 08:37:07 +00:00
b27e3fdb28 Merge release/hzm in devel 2021-02-22 17:36:31 +01:00
415e56d928 Other Update bridge_early.md 2021-02-22 15:42:30 +01:00
845074f421 Other: Bridge HZM 1.6.5 2021-02-19 13:00:01 +01:00
28f46deef9 Other: only choose pass if usable 2021-02-18 13:23:38 +01:00
2a078b76e6 GODT-1045 build without Qt by default 2021-02-18 09:45:18 +00:00
3428557b15 Other: Bridge HZM 1.6.4 2021-02-17 14:17:11 +01:00
1f25aeab31 GODT-980: placeholder for user agent 2021-02-17 13:49:51 +01:00
4e531d4524 GODT-1036 Event loop Sentry reporting of failures and refresh 2021-02-17 09:17:19 +00:00
7fc7083c76 GODT-957 Increase space to hide difference 2021-02-17 08:37:12 +00:00
0fe69d9de1 GODT-937: Add keychain switcher to frontend
GODT-1008: Fix transparent dialog under certain conditions
2021-02-17 07:35:59 +00:00
8b436186a4 GODT-1034 More tolerant connection speed detection 2021-02-17 06:13:15 +00:00
4d000c2376 GODT-1018 Pre-push git hook to check lints 2021-02-17 05:10:42 +00:00
56bce8e06f Other: Make all command line flags as const strings 2021-02-16 22:01:50 +00:00
6fd614595d Other: 1.6.3 release notes update 2021-02-16 19:39:43 +01:00
7bb7e1a518 GODT-1041 Log IMAP requests to debug Apple Mail re-sync issue 2021-02-16 14:15:37 +00:00
fb89fb7b31 Other: pretty print prefs.json 2021-02-15 11:31:42 +01:00
e6ae344f1f GODT-797 APPEND waits for EXPUNGE to prevent data loss when Outlook moves from Spam or Trash 2021-02-12 15:33:31 +01:00
bad8cad97d Other: fix nogui build 2021-02-12 09:34:10 +01:00
77cd2955f1 chore: remove credits 2021-02-11 15:10:53 +00:00
567b65df8d feat: autoupdates CLI commands 2021-02-11 08:40:51 +00:00
06b3ed9b85 GODT-317 Fix wrong total mailbox size in Apple Mail 2021-02-11 07:29:28 +00:00
565c0b6ddf Fixing changelog punctuation. 2021-02-10 16:09:47 +01:00
227 changed files with 6599 additions and 2074 deletions

3
.gitignore vendored
View File

@ -26,6 +26,9 @@ internal/frontend/qml/ProtonUI/images
internal/frontend/qml/ImportExportUI/images internal/frontend/qml/ImportExportUI/images
frontend/qml/*.qmlc frontend/qml/*.qmlc
# Credits files (generated).
internal/**/credits.go
# Build files # Build files
/launcher-* /launcher-*
/bridge_*_*.tgz /bridge_*_*.tgz

View File

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

View File

@ -2,6 +2,139 @@
Changelog [format](http://keepachangelog.com/en/1.0.0/) Changelog [format](http://keepachangelog.com/en/1.0.0/)
## [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
### Added
* Other: QA build checks for update every 5 minutes.
* Other: QA build adds debug message dump when sending.
### Changed
* GODT-1045 build without Qt by default.
### Fixed
* GODT-1029 Fix tray icon not updating under certain conditions.
* GODT-1062 Fix lost notification bar when window is closed.
* GODT-1058 Install version after chaning channel right away only in case of downgrade.
* GODT-1073 Re-write autostart link on every start if turned on in preferences.
* GODT-1055 Fix flaky empty trash test.
## [Bridge 1.6.5] HZM
### Changed
* GODT-1059 Check if keychain is usable on linux before using it by default.
## [Bridge 1.6.4] HZM
### Added
* Other: Autoupdates CLI commands.
### Removed
* Other: Remove credits.
### Changed
* GODT-980 Placeholder for user agent.
* GODT-1036 Event loop Sentry reporting of failures and refresh.
* GODT-957 Increase space to hide difference.
* GODT-937 Add keychain switcher to frontend.
* GODT-1008 Fix transparent dialog under certain conditions.
* GODT-1034 More tolerant connection speed detection.
* GODT-1018 Pre-push git hook to check lints.
* Other: Make all command line flags as const strings.
* GODT-1041 Log IMAP requests to debug Apple Mail re-sync issue.
* Other: Pretty print prefs.json.
### Fixed
* Other: Fix nogui build.
* GODT-317 Fix wrong total mailbox size in Apple Mail.
* Other: Fixing changelog punctuation.
* GODT-797 APPEND waits for EXPUNGE to prevent data loss when Outlook moves from Spam or Trash.
## [Bridge 1.6.3] HZM ## [Bridge 1.6.3] HZM
### Added ### Added
@ -10,15 +143,15 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
### Changed ### Changed
* GODT-885 Do not explicitly unlabel folders during move to match behaviour of other clients. * GODT-885 Do not explicitly unlabel folders during move to match behaviour of other clients.
* GODT-616 Better user message about wrong mailbox password. * GODT-616 Better user message about wrong mailbox password.
* GODT-1021 Do not allow copy Inbox->Sent or Sent->Inbox * GODT-1021 Do not allow copy Inbox->Sent or Sent->Inbox.
* GODT-976 Exclude updates from clearing cache and clear cache, including updates, while switching early access off * GODT-976 Exclude updates from clearing cache and clear cache, including updates, while switching early access off.
* GODT-1033 Retry starting IMAP server after connection was down * GODT-1033 Retry starting IMAP server after connection was down.
### Fixed ### Fixed
* GODT-1011 Stable integration test deleting many messages using UID EXPUNGE. * GODT-1011 Stable integration test deleting many messages using UID EXPUNGE.
* GODT-1015 Use lenient version parser to properly parse version provided by Mac. * GODT-1015 Use lenient version parser to properly parse version provided by Mac.
* GODT-919 Notify about update right after the start. * GODT-919 Notify about update right after the start.
* GODT-919 GODT-1022 Logs and signals * GODT-919 GODT-1022 Logs and signals.
## [IE 1.3.0] Farg ## [IE 1.3.0] Farg

View File

@ -10,8 +10,8 @@ TARGET_OS?=${GOOS}
.PHONY: build build-ie build-nogui build-ie-nogui build-launcher build-launcher-ie versioner hasher .PHONY: build build-ie build-nogui build-ie-nogui build-launcher build-launcher-ie versioner hasher
# Keep version hardcoded so app build works also without Git repository. # Keep version hardcoded so app build works also without Git repository.
BRIDGE_APP_VERSION?=1.6.3+git BRIDGE_APP_VERSION?=1.7.0+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,13 +28,14 @@ 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)
BUILD_FLAGS:=-tags='${BUILD_TAGS}' BUILD_FLAGS:=-tags='${BUILD_TAGS}'
BUILD_FLAGS_LAUNCHER:=${BUILD_FLAGS} BUILD_FLAGS_LAUNCHER:=${BUILD_FLAGS}
BUILD_FLAGS_NOGUI:=-tags='${BUILD_TAGS} nogui' BUILD_FLAGS_GUI:=-tags='${BUILD_TAGS} build_qt'
GO_LDFLAGS:=$(addprefix -X github.com/ProtonMail/proton-bridge/internal/constants.,Version=${APP_VERSION} Revision=${REVISION} BuildTime=${BUILD_TIME}) GO_LDFLAGS:=$(addprefix -X github.com/ProtonMail/proton-bridge/internal/constants.,Version=${APP_VERSION} Revision=${REVISION} BuildTime=${BUILD_TIME})
ifneq "${BUILD_LDFLAGS}" "" ifneq "${BUILD_LDFLAGS}" ""
GO_LDFLAGS+=${BUILD_LDFLAGS} GO_LDFLAGS+=${BUILD_LDFLAGS}
@ -45,7 +47,7 @@ ifeq "${TARGET_OS}" "windows"
endif endif
BUILD_FLAGS+=-ldflags '${GO_LDFLAGS}' BUILD_FLAGS+=-ldflags '${GO_LDFLAGS}'
BUILD_FLAGS_NOGUI+=-ldflags '${GO_LDFLAGS}' BUILD_FLAGS_GUI+=-ldflags '${GO_LDFLAGS}'
BUILD_FLAGS_LAUNCHER+=-ldflags '${GO_LDFLAGS_LAUNCHER}' BUILD_FLAGS_LAUNCHER+=-ldflags '${GO_LDFLAGS_LAUNCHER}'
DEPLOY_DIR:=cmd/${TARGET_CMD}/deploy DEPLOY_DIR:=cmd/${TARGET_CMD}/deploy
@ -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
@ -83,14 +85,20 @@ build: ${TGZ_TARGET}
build-ie: build-ie:
TARGET_CMD=Import-Export $(MAKE) build TARGET_CMD=Import-Export $(MAKE) build
build-nogui: build-nogui: gofiles
go build ${BUILD_FLAGS_NOGUI} -o ${EXE_NAME} cmd/${TARGET_CMD}/main.go go build ${BUILD_FLAGS} -o ${EXE_NAME} cmd/${TARGET_CMD}/main.go
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,21 +142,20 @@ 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} ${QT_BUILD_TARGET} qtdeploy ${BUILD_FLAGS_GUI} ${QT_BUILD_TARGET}
mv deploy cmd/${TARGET_CMD} mv deploy cmd/${TARGET_CMD}
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)
@ -180,8 +188,8 @@ update-qt-docs:
go get github.com/therecipe/qt/internal/binding/files/docs/$(QT_API) go get github.com/therecipe/qt/internal/binding/files/docs/$(QT_API)
## Dev dependencies ## Dev dependencies
.PHONY: install-devel-tools install-linter install-go-mod-outdated .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
@ -197,6 +205,9 @@ install-linter: check-has-go
install-go-mod-outdated: install-go-mod-outdated:
which go-mod-outdated || go get -u github.com/psampaz/go-mod-outdated which go-mod-outdated || go get -u github.com/psampaz/go-mod-outdated
install-git-hooks:
cp utils/githooks/* .git/hooks/
chmod +x .git/hooks/*
## Checks, mocks and docs ## Checks, mocks and docs
.PHONY: check-has-go add-license change-copyright-year test bench coverage mocks lint-license lint-golang lint updates doc release-notes .PHONY: check-has-go add-license change-copyright-year test bench coverage mocks lint-license lint-golang lint updates doc release-notes
@ -247,16 +258,16 @@ mocks:
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/internal/transfer PanicHandler,ClientManager,IMAPClientProvider > internal/transfer/mocks/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/store PanicHandler,ClientManager,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/message Fetcher > pkg/message/mocks/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 > pkg/pmapi/mocks/mocks.go
lint: lint-golang lint-license lint-changelog lint: gofiles lint-golang lint-license lint-changelog
lint-license: lint-license:
./utils/missing_license.sh check ./utils/missing_license.sh check
lint-changelog: lint-changelog:
./utils/changelog_linter.sh Changelog.md ./utils/changelog_linter.sh Changelog.md
./utils/changelog_linter.sh unreleased.md
lint-golang: lint-golang:
which golangci-lint || $(MAKE) install-linter which golangci-lint || $(MAKE) install-linter
@ -292,6 +303,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
@ -301,12 +313,12 @@ run-qt-cli: ${EXE_TARGET}
PROTONMAIL_ENV=dev ./$< ${RUN_FLAGS} -c PROTONMAIL_ENV=dev ./$< ${RUN_FLAGS} -c
run-nogui: clean-vendor gofiles run-nogui: clean-vendor gofiles
PROTONMAIL_ENV=dev go run ${BUILD_FLAGS_NOGUI} cmd/${TARGET_CMD}/main.go ${RUN_FLAGS} | tee last.log PROTONMAIL_ENV=dev go run ${BUILD_FLAGS} cmd/${TARGET_CMD}/main.go ${RUN_FLAGS} | tee last.log
run-nogui-cli: clean-vendor gofiles run-nogui-cli: clean-vendor gofiles
PROTONMAIL_ENV=dev go run ${BUILD_FLAGS_NOGUI} cmd/${TARGET_CMD}/main.go ${RUN_FLAGS} -c PROTONMAIL_ENV=dev go run ${BUILD_FLAGS} cmd/${TARGET_CMD}/main.go ${RUN_FLAGS} -c
run-debug: run-debug:
PROTONMAIL_ENV=dev dlv debug --build-flags "${BUILD_FLAGS_NOGUI}" cmd/${TARGET_CMD}/main.go -- ${RUN_FLAGS} PROTONMAIL_ENV=dev dlv debug --build-flags "${BUILD_FLAGS}" cmd/${TARGET_CMD}/main.go -- ${RUN_FLAGS}
run-qml-preview: run-qml-preview:
$(MAKE) -C internal/frontend/qt -f Makefile.local qmlpreview $(MAKE) -C internal/frontend/qt -f Makefile.local qmlpreview
@ -314,11 +326,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
@ -335,7 +347,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
@ -343,3 +355,5 @@ clean: clean-vendor
generate: generate:
go generate ./... go generate ./...
$(MAKE) add-license $(MAKE) add-license
.FORCE:

View File

@ -25,13 +25,14 @@ import (
"runtime" "runtime"
"github.com/ProtonMail/gopenpgp/v2/crypto" "github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/ProtonMail/proton-bridge/internal/config/useragent"
"github.com/ProtonMail/proton-bridge/internal/constants" "github.com/ProtonMail/proton-bridge/internal/constants"
"github.com/ProtonMail/proton-bridge/internal/crash" "github.com/ProtonMail/proton-bridge/internal/crash"
"github.com/ProtonMail/proton-bridge/internal/locations" "github.com/ProtonMail/proton-bridge/internal/locations"
"github.com/ProtonMail/proton-bridge/internal/logging" "github.com/ProtonMail/proton-bridge/internal/logging"
"github.com/ProtonMail/proton-bridge/internal/sentry"
"github.com/ProtonMail/proton-bridge/internal/updater" "github.com/ProtonMail/proton-bridge/internal/updater"
"github.com/ProtonMail/proton-bridge/internal/versioner" "github.com/ProtonMail/proton-bridge/internal/versioner"
"github.com/ProtonMail/proton-bridge/pkg/sentry"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@ -44,7 +45,7 @@ var (
) )
func main() { // nolint[funlen] func main() { // nolint[funlen]
reporter := sentry.NewReporter(appName, constants.Version) reporter := sentry.NewReporter(appName, constants.Version, useragent.New())
crashHandler := crash.NewHandler(reporter.ReportException) crashHandler := crash.NewHandler(reporter.ReportException)
defer crashHandler.HandlePanic() defer crashHandler.HandlePanic()

4
go.mod
View File

@ -6,7 +6,7 @@ go 1.13
// They are in a separate require block to highlight this. // They are in a separate require block to highlight this.
require ( require (
github.com/docker/docker-credential-helpers v0.6.3 github.com/docker/docker-credential-helpers v0.6.3
github.com/emersion/go-imap v1.0.6-0.20200708083111-011063d6c9df github.com/emersion/go-imap v1.0.6
github.com/jameskeane/bcrypt v0.0.0-20170924085257-7509ea014998 github.com/jameskeane/bcrypt v0.0.0-20170924085257-7509ea014998
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
) )
@ -29,7 +29,7 @@ require (
github.com/emersion/go-imap-appendlimit v0.0.0-20190308131241-25671c986a6a github.com/emersion/go-imap-appendlimit v0.0.0-20190308131241-25671c986a6a
github.com/emersion/go-imap-idle v0.0.0-20200601154248-f05f54664cc4 github.com/emersion/go-imap-idle v0.0.0-20200601154248-f05f54664cc4
github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342 github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342
github.com/emersion/go-imap-quota v0.0.0-20200423100218-dcfd1b7d2b41 github.com/emersion/go-imap-quota v0.0.0-20210203125329-619074823f3c
github.com/emersion/go-imap-unselect v0.0.0-20171113212723-b985794e5f26 github.com/emersion/go-imap-unselect v0.0.0-20171113212723-b985794e5f26
github.com/emersion/go-mbox v1.0.2 github.com/emersion/go-mbox v1.0.2
github.com/emersion/go-message v0.12.1-0.20201221184100-40c3f864532b github.com/emersion/go-message v0.12.1-0.20201221184100-40c3f864532b

4
go.sum
View File

@ -77,8 +77,8 @@ github.com/emersion/go-imap-idle v0.0.0-20200601154248-f05f54664cc4 h1:/JIALzmCd
github.com/emersion/go-imap-idle v0.0.0-20200601154248-f05f54664cc4/go.mod h1:o14zPKCmEH5WC1vU5SdPoZGgNvQx7zzKSnxPQlobo78= github.com/emersion/go-imap-idle v0.0.0-20200601154248-f05f54664cc4/go.mod h1:o14zPKCmEH5WC1vU5SdPoZGgNvQx7zzKSnxPQlobo78=
github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342 h1:5p1t3e1PomYgLWwEwhwEU5kVBwcyAcVrOpexv8AeZx0= github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342 h1:5p1t3e1PomYgLWwEwhwEU5kVBwcyAcVrOpexv8AeZx0=
github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342/go.mod h1:QuMaZcKFDVI0yCrnAbPLfbwllz1wtOrZH8/vZ5yzp4w= github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342/go.mod h1:QuMaZcKFDVI0yCrnAbPLfbwllz1wtOrZH8/vZ5yzp4w=
github.com/emersion/go-imap-quota v0.0.0-20200423100218-dcfd1b7d2b41 h1:z5lDGnSURauBEDdNLj3o0+HogVYKQCGeY3Anl/xyRfU= github.com/emersion/go-imap-quota v0.0.0-20210203125329-619074823f3c h1:khcEdu1yFiZjBgi7gGnQiLhpSgghJ0YTnKD0l4EUqqc=
github.com/emersion/go-imap-quota v0.0.0-20200423100218-dcfd1b7d2b41/go.mod h1:iApyhIQBiU4XFyr+3kdJyyGqle82TbQyuP2o+OZHrV0= github.com/emersion/go-imap-quota v0.0.0-20210203125329-619074823f3c/go.mod h1:iApyhIQBiU4XFyr+3kdJyyGqle82TbQyuP2o+OZHrV0=
github.com/emersion/go-imap-unselect v0.0.0-20171113212723-b985794e5f26 h1:FiSb8+XBQQSkcX3ubr+1tAtlRJBYaFmRZqOAweZ9Wy8= github.com/emersion/go-imap-unselect v0.0.0-20171113212723-b985794e5f26 h1:FiSb8+XBQQSkcX3ubr+1tAtlRJBYaFmRZqOAweZ9Wy8=
github.com/emersion/go-imap-unselect v0.0.0-20171113212723-b985794e5f26/go.mod h1:+gnnZx3Mg3MnCzZrv0eZdp5puxXQUgGT/6N6L7ShKfM= github.com/emersion/go-imap-unselect v0.0.0-20171113212723-b985794e5f26/go.mod h1:+gnnZx3Mg3MnCzZrv0eZdp5puxXQUgGT/6N6L7ShKfM=
github.com/emersion/go-mbox v1.0.2 h1:tE/rT+lEugK9y0myEymCCHnwlZN04hlXPrbKkxRBA5I= github.com/emersion/go-mbox v1.0.2 h1:tE/rT+lEugK9y0myEymCCHnwlZN04hlXPrbKkxRBA5I=

View File

@ -43,38 +43,55 @@ import (
"github.com/ProtonMail/proton-bridge/internal/config/cache" "github.com/ProtonMail/proton-bridge/internal/config/cache"
"github.com/ProtonMail/proton-bridge/internal/config/settings" "github.com/ProtonMail/proton-bridge/internal/config/settings"
"github.com/ProtonMail/proton-bridge/internal/config/tls" "github.com/ProtonMail/proton-bridge/internal/config/tls"
"github.com/ProtonMail/proton-bridge/internal/config/useragent"
"github.com/ProtonMail/proton-bridge/internal/constants" "github.com/ProtonMail/proton-bridge/internal/constants"
"github.com/ProtonMail/proton-bridge/internal/cookies" "github.com/ProtonMail/proton-bridge/internal/cookies"
"github.com/ProtonMail/proton-bridge/internal/crash" "github.com/ProtonMail/proton-bridge/internal/crash"
"github.com/ProtonMail/proton-bridge/internal/events" "github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/locations" "github.com/ProtonMail/proton-bridge/internal/locations"
"github.com/ProtonMail/proton-bridge/internal/logging" "github.com/ProtonMail/proton-bridge/internal/logging"
"github.com/ProtonMail/proton-bridge/internal/sentry"
"github.com/ProtonMail/proton-bridge/internal/updater" "github.com/ProtonMail/proton-bridge/internal/updater"
"github.com/ProtonMail/proton-bridge/internal/users/credentials" "github.com/ProtonMail/proton-bridge/internal/users/credentials"
"github.com/ProtonMail/proton-bridge/internal/versioner" "github.com/ProtonMail/proton-bridge/internal/versioner"
"github.com/ProtonMail/proton-bridge/pkg/keychain" "github.com/ProtonMail/proton-bridge/pkg/keychain"
"github.com/ProtonMail/proton-bridge/pkg/listener" "github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/ProtonMail/proton-bridge/pkg/sentry"
"github.com/allan-simon/go-singleinstance" "github.com/allan-simon/go-singleinstance"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
const (
flagCPUProfile = "cpu-prof"
flagCPUProfileShort = "p"
flagMemProfile = "mem-prof"
flagMemProfileShort = "m"
flagLogLevel = "log-level"
flagLogLevelShort = "l"
// FlagCLI indicate to start with command line interface.
FlagCLI = "cli"
flagCLIShort = "c"
flagRestart = "restart"
flagLauncher = "launcher"
)
type Base struct { type Base struct {
CrashHandler *crash.Handler SentryReporter *sentry.Reporter
Locations *locations.Locations CrashHandler *crash.Handler
Settings *settings.Settings Locations *locations.Locations
Lock *os.File Settings *settings.Settings
Cache *cache.Cache Lock *os.File
Listener listener.Listener Cache *cache.Cache
Creds *credentials.Store Listener listener.Listener
CM *pmapi.ClientManager Creds *credentials.Store
CookieJar *cookies.Jar CM *pmapi.ClientManager
Updater *updater.Updater CookieJar *cookies.Jar
Versioner *versioner.Versioner UserAgent *useragent.UserAgent
TLS *tls.TLS Updater *updater.Updater
Autostart *autostart.App Versioner *versioner.Versioner
TLS *tls.TLS
Autostart *autostart.App
Name string // the app's name Name string // the app's name
usage string // the app's usage description usage string // the app's usage description
@ -92,7 +109,10 @@ func New( // nolint[funlen]
keychainName, keychainName,
cacheVersion string, cacheVersion string,
) (*Base, error) { ) (*Base, error) {
sentryReporter := sentry.NewReporter(appName, constants.Version) userAgent := useragent.New()
sentryReporter := sentry.NewReporter(appName, constants.Version, userAgent)
crashHandler := crash.NewHandler( crashHandler := crash.NewHandler(
sentryReporter.ReportException, sentryReporter.ReportException,
crash.ShowErrorNotification(appName), crash.ShowErrorNotification(appName),
@ -166,20 +186,9 @@ func New( // nolint[funlen]
return nil, err return nil, err
} }
apiConfig := pmapi.GetAPIConfig(configName, constants.Version) cm := pmapi.NewClientManager(getAPIConfig(configName, listener), userAgent)
apiConfig.ConnectionOffHandler = func() {
listener.Emit(events.InternetOffEvent, "")
}
apiConfig.ConnectionOnHandler = func() {
listener.Emit(events.InternetOnEvent, "")
}
apiConfig.UpgradeApplicationHandler = func() {
listener.Emit(events.UpgradeApplicationEvent, "")
}
cm := pmapi.NewClientManager(apiConfig)
cm.SetRoundTripper(pmapi.GetRoundTripper(cm, listener)) cm.SetRoundTripper(pmapi.GetRoundTripper(cm, listener))
cm.SetCookieJar(jar) cm.SetCookieJar(jar)
sentryReporter.SetUserAgentProvider(cm)
key, err := crypto.NewKeyFromArmored(updater.DefaultPublicKey) key, err := crypto.NewKeyFromArmored(updater.DefaultPublicKey)
if err != nil { if err != nil {
@ -220,19 +229,21 @@ func New( // nolint[funlen]
} }
return &Base{ return &Base{
CrashHandler: crashHandler, SentryReporter: sentryReporter,
Locations: locations, CrashHandler: crashHandler,
Settings: settingsObj, Locations: locations,
Lock: lock, Settings: settingsObj,
Cache: cache, Lock: lock,
Listener: listener, Cache: cache,
Creds: credentials.NewStore(kc), Listener: listener,
CM: cm, Creds: credentials.NewStore(kc),
CookieJar: jar, CM: cm,
Updater: updater, CookieJar: jar,
Versioner: versioner, UserAgent: userAgent,
TLS: tls.New(settingsPath), Updater: updater,
Autostart: autostart, Versioner: versioner,
TLS: tls.New(settingsPath),
Autostart: autostart,
Name: appName, Name: appName,
usage: appUsage, usage: appUsage,
@ -252,32 +263,32 @@ func (b *Base) NewApp(action func(*Base, *cli.Context) error) *cli.App {
app.Action = b.run(action) app.Action = b.run(action)
app.Flags = []cli.Flag{ app.Flags = []cli.Flag{
&cli.BoolFlag{ &cli.BoolFlag{
Name: "cpu-prof", Name: flagCPUProfile,
Aliases: []string{"p"}, Aliases: []string{flagCPUProfileShort},
Usage: "Generate CPU profile", Usage: "Generate CPU profile",
}, },
&cli.BoolFlag{ &cli.BoolFlag{
Name: "mem-prof", Name: flagMemProfile,
Aliases: []string{"m"}, Aliases: []string{flagMemProfileShort},
Usage: "Generate memory profile", Usage: "Generate memory profile",
}, },
&cli.StringFlag{ &cli.StringFlag{
Name: "log-level", Name: flagLogLevel,
Aliases: []string{"l"}, Aliases: []string{flagLogLevelShort},
Usage: "Set the log level (one of panic, fatal, error, warn, info, debug)", Usage: "Set the log level (one of panic, fatal, error, warn, info, debug)",
}, },
&cli.BoolFlag{ &cli.BoolFlag{
Name: "cli", Name: FlagCLI,
Aliases: []string{"c"}, Aliases: []string{flagCLIShort},
Usage: "Use command line interface", Usage: "Use command line interface",
}, },
&cli.StringFlag{ &cli.StringFlag{
Name: "restart", Name: flagRestart,
Usage: "The number of times the application has already restarted", Usage: "The number of times the application has already restarted",
Hidden: true, Hidden: true,
}, },
&cli.StringFlag{ &cli.StringFlag{
Name: "launcher", Name: flagLauncher,
Usage: "The launcher to use to restart the application", Usage: "The launcher to use to restart the application",
Hidden: true, Hidden: true,
}, },
@ -302,21 +313,21 @@ func (b *Base) run(appMainLoop func(*Base, *cli.Context) error) cli.ActionFunc {
defer func() { _ = b.Lock.Close() }() defer func() { _ = b.Lock.Close() }()
// If launcher was used to start the app, use that for restart/autostart. // If launcher was used to start the app, use that for restart/autostart.
if launcher := c.String("launcher"); launcher != "" { if launcher := c.String(flagLauncher); launcher != "" {
b.Autostart.Exec = []string{launcher} b.Autostart.Exec = []string{launcher}
b.command = launcher b.command = launcher
} }
if doCPUProfile := c.Bool("cpu-prof"); doCPUProfile { if c.Bool(flagCPUProfile) {
startCPUProfile() startCPUProfile()
defer pprof.StopCPUProfile() defer pprof.StopCPUProfile()
} }
if doMemoryProfile := c.Bool("mem-prof"); doMemoryProfile { if c.Bool(flagMemProfile) {
defer makeMemoryProfile() defer makeMemoryProfile()
} }
logging.SetLevel(c.String("log-level")) logging.SetLevel(c.String(flagLogLevel))
logrus. logrus.
WithField("appName", b.Name). WithField("appName", b.Name).
@ -328,7 +339,7 @@ func (b *Base) run(appMainLoop func(*Base, *cli.Context) error) cli.ActionFunc {
Info("Run app") Info("Run app")
b.CrashHandler.AddRecoveryAction(func(interface{}) error { b.CrashHandler.AddRecoveryAction(func(interface{}) error {
if c.Int("restart") > maxAllowedRestarts { if c.Int(flagRestart) > maxAllowedRestarts {
logrus. logrus.
WithField("restart", c.Int("restart")). WithField("restart", c.Int("restart")).
Warn("Not restarting, already restarted too many times") Warn("Not restarting, already restarted too many times")
@ -364,3 +375,13 @@ func (b *Base) doTeardown() error {
return nil return nil
} }
func getAPIConfig(configName string, listener listener.Listener) *pmapi.ClientConfig {
apiConfig := pmapi.GetAPIConfig(configName, constants.Version)
apiConfig.ConnectionOffHandler = func() { listener.Emit(events.InternetOffEvent, "") }
apiConfig.ConnectionOnHandler = func() { listener.Emit(events.InternetOnEvent, "") }
apiConfig.UpgradeApplicationHandler = func() { listener.Emit(events.UpgradeApplicationEvent, "") }
return apiConfig
}

View File

@ -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)
} }

View File

@ -38,21 +38,28 @@ import (
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
const (
flagLogIMAP = "log-imap"
flagLogSMTP = "log-smtp"
flagNoWindow = "no-window"
flagNonInteractive = "noninteractive"
)
func New(base *base.Base) *cli.App { func New(base *base.Base) *cli.App {
app := base.NewApp(run) app := base.NewApp(run)
app.Flags = append(app.Flags, []cli.Flag{ app.Flags = append(app.Flags, []cli.Flag{
&cli.StringFlag{ &cli.StringFlag{
Name: "log-imap", Name: flagLogIMAP,
Usage: "Enable logging of IMAP communications (all|client|server) (may contain decrypted data!)"}, Usage: "Enable logging of IMAP communications (all|client|server) (may contain decrypted data!)"},
&cli.BoolFlag{ &cli.BoolFlag{
Name: "log-smtp", Name: flagLogSMTP,
Usage: "Enable logging of SMTP communications (may contain decrypted data!)"}, Usage: "Enable logging of SMTP communications (may contain decrypted data!)"},
&cli.BoolFlag{ &cli.BoolFlag{
Name: "no-window", Name: flagNoWindow,
Usage: "Don't show window after start"}, Usage: "Don't show window after start"},
&cli.BoolFlag{ &cli.BoolFlag{
Name: "noninteractive", Name: flagNonInteractive,
Usage: "Start Bridge entirely noninteractively"}, Usage: "Start Bridge entirely noninteractively"},
}...) }...)
@ -64,8 +71,7 @@ func run(b *base.Base, c *cli.Context) error { // nolint[funlen]
if err != nil { if err != nil {
logrus.WithError(err).Fatal("Failed to load TLS config") logrus.WithError(err).Fatal("Failed to load TLS config")
} }
bridge := bridge.New(b.Locations, b.Cache, b.Settings, b.SentryReporter, b.CrashHandler, b.Listener, b.CM, b.Creds, b.Updater, b.Versioner)
bridge := bridge.New(b.Locations, b.Cache, b.Settings, b.CrashHandler, b.Listener, b.CM, b.Creds, b.Updater, b.Versioner)
imapBackend := imap.NewIMAPBackend(b.CrashHandler, b.Listener, b.Cache, bridge) imapBackend := imap.NewIMAPBackend(b.CrashHandler, b.Listener, b.Cache, bridge)
smtpBackend := smtp.NewSMTPBackend(b.CrashHandler, b.Listener, b.Settings, bridge) smtpBackend := smtp.NewSMTPBackend(b.CrashHandler, b.Listener, b.Settings, bridge)
@ -79,9 +85,9 @@ func run(b *base.Base, c *cli.Context) error { // nolint[funlen]
imapPort := b.Settings.GetInt(settings.IMAPPortKey) imapPort := b.Settings.GetInt(settings.IMAPPortKey)
imap.NewIMAPServer( imap.NewIMAPServer(
b.CrashHandler, b.CrashHandler,
c.String("log-imap") == "client" || c.String("log-imap") == "all", c.String(flagLogIMAP) == "client" || c.String(flagLogIMAP) == "all",
c.String("log-imap") == "server" || c.String("log-imap") == "all", c.String(flagLogIMAP) == "server" || c.String(flagLogIMAP) == "all",
imapPort, tlsConfig, imapBackend, b.Listener).ListenAndServe() imapPort, tlsConfig, imapBackend, b.UserAgent, b.Listener).ListenAndServe()
}() }()
go func() { go func() {
@ -89,12 +95,12 @@ 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(
c.Bool("log-smtp"), c.Bool(flagLogSMTP),
smtpPort, useSSL, tlsConfig, smtpBackend, b.Listener).ListenAndServe() smtpPort, useSSL, tlsConfig, smtpBackend, b.Listener).ListenAndServe()
}() }()
// Bridge supports no-window option which we should use for autostart. // Bridge supports no-window option which we should use for autostart.
b.Autostart.Exec = append(b.Autostart.Exec, "--no-window") b.Autostart.Exec = append(b.Autostart.Exec, "--"+flagNoWindow)
// We want to remove old versions if the app exits successfully. // We want to remove old versions if the app exits successfully.
b.AddTeardownAction(b.Versioner.RemoveOldVersions) b.AddTeardownAction(b.Versioner.RemoveOldVersions)
@ -105,9 +111,9 @@ func run(b *base.Base, c *cli.Context) error { // nolint[funlen]
var frontendMode string var frontendMode string
switch { switch {
case c.Bool("cli"): case c.Bool(base.FlagCLI):
frontendMode = "cli" frontendMode = "cli"
case c.Bool("noninteractive"): case c.Bool(flagNonInteractive):
return <-(make(chan error)) // Block forever. return <-(make(chan error)) // Block forever.
default: default:
frontendMode = "qt" frontendMode = "qt"
@ -118,12 +124,13 @@ func run(b *base.Base, c *cli.Context) error { // nolint[funlen]
constants.BuildVersion, constants.BuildVersion,
b.Name, b.Name,
frontendMode, frontendMode,
!c.Bool("no-window"), !c.Bool(flagNoWindow),
b.CrashHandler, b.CrashHandler,
b.Locations, b.Locations,
b.Settings, b.Settings,
b.Listener, b.Listener,
b.Updater, b.Updater,
b.UserAgent,
bridge, bridge,
smtpBackend, smtpBackend,
b.Autostart, b.Autostart,
@ -132,7 +139,7 @@ func run(b *base.Base, c *cli.Context) error { // nolint[funlen]
// Watch for updates routine // Watch for updates routine
go func() { go func() {
ticker := time.NewTicker(time.Hour) ticker := time.NewTicker(constants.UpdateCheckInterval)
for { for {
checkAndHandleUpdate(b.Updater, f, b.Settings.GetBool(settings.AutoUpdateKey)) checkAndHandleUpdate(b.Updater, f, b.Settings.GetBool(settings.AutoUpdateKey))
@ -182,9 +189,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
} }
@ -194,11 +202,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))
@ -206,16 +214,16 @@ func checkAndHandleUpdate(u types.Updater, f frontend.Frontend, autoUpdate bool)
} }
if !u.CanInstall(version) { if !u.CanInstall(version) {
logrus.Info("A manual update is required") log.Info("A manual update is required")
f.NotifySilentUpdateError(updater.ErrManualUpdateRequired) f.NotifySilentUpdateError(updater.ErrManualUpdateRequired)
return return
} }
if err := u.InstallUpdate(version); err != nil { if err := u.InstallUpdate(version); err != nil {
if errors.Cause(err) == updater.ErrDownloadVerify { if errors.Cause(err) == updater.ErrDownloadVerify {
logrus.WithError(err).Warning("Skipping update installation due to temporary error") log.WithError(err).Warning("Skipping update installation due to temporary error")
} else { } else {
logrus.WithError(err).Error("The update couldn't be installed") log.WithError(err).Error("The update couldn't be installed")
f.NotifySilentUpdateError(err) f.NotifySilentUpdateError(err)
} }

View File

@ -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"
) )
@ -49,7 +47,7 @@ func run(b *base.Base, c *cli.Context) error {
var frontendMode string var frontendMode string
switch { switch {
case c.Bool("cli"): case c.Bool(base.FlagCLI):
frontendMode = "cli" frontendMode = "cli"
default: default:
frontendMode = "qt" frontendMode = "qt"
@ -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()
} }

View File

@ -26,6 +26,7 @@ import (
"github.com/ProtonMail/proton-bridge/internal/config/settings" "github.com/ProtonMail/proton-bridge/internal/config/settings"
"github.com/ProtonMail/proton-bridge/internal/constants" "github.com/ProtonMail/proton-bridge/internal/constants"
"github.com/ProtonMail/proton-bridge/internal/metrics" "github.com/ProtonMail/proton-bridge/internal/metrics"
"github.com/ProtonMail/proton-bridge/internal/sentry"
"github.com/ProtonMail/proton-bridge/internal/updater" "github.com/ProtonMail/proton-bridge/internal/updater"
"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"
@ -46,16 +47,13 @@ type Bridge struct {
clientManager users.ClientManager clientManager users.ClientManager
updater Updater updater Updater
versioner Versioner versioner Versioner
userAgentClientName string
userAgentClientVersion string
userAgentOS string
} }
func New( func New(
locations Locator, locations Locator,
cache Cacher, cache Cacher,
s SettingsProvider, s SettingsProvider,
sentryReporter *sentry.Reporter,
panicHandler users.PanicHandler, panicHandler users.PanicHandler,
eventListener listener.Listener, eventListener listener.Listener,
clientManager users.ClientManager, clientManager users.ClientManager,
@ -69,7 +67,7 @@ func New(
clientManager.AllowProxy() clientManager.AllowProxy()
} }
storeFactory := newStoreFactory(cache, panicHandler, clientManager, eventListener) storeFactory := newStoreFactory(cache, sentryReporter, panicHandler, clientManager, 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,40 +116,6 @@ func (b *Bridge) heartbeat() {
} }
} }
// GetCurrentClient returns currently connected client (e.g. Thunderbird).
func (b *Bridge) GetCurrentClient() string {
res := b.userAgentClientName
if b.userAgentClientVersion != "" {
res = res + " " + b.userAgentClientVersion
}
return res
}
// SetCurrentClient updates client info (e.g. Thunderbird) and sets the user agent
// on pmapi. By default no client is used, IMAP has to detect it on first login.
func (b *Bridge) SetCurrentClient(clientName, clientVersion string) {
b.userAgentClientName = clientName
b.userAgentClientVersion = clientVersion
b.updateUserAgent()
}
// SetCurrentOS updates OS and sets the user agent on pmapi. By default we use
// `runtime.GOOS`, but this can be overridden in case of better detection.
func (b *Bridge) SetCurrentOS(os string) {
b.userAgentOS = os
b.updateUserAgent()
}
func (b *Bridge) updateUserAgent() {
logrus.
WithField("clientName", b.userAgentClientName).
WithField("clientVersion", b.userAgentClientVersion).
WithField("OS", b.userAgentOS).
Info("Updating user agent")
b.clientManager.SetUserAgent(b.userAgentClientName, b.userAgentClientVersion, b.userAgentOS)
}
// 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() c := b.clientManager.GetAnonymousClient()
@ -187,26 +151,41 @@ func (b *Bridge) GetUpdateChannel() updater.UpdateChannel {
// Downgrading to previous version (by switching from early to stable, for example) // Downgrading to previous version (by switching from early to stable, for example)
// requires clearing all data including update files due to possibility of // requires clearing all data including update files due to possibility of
// inconsistency between versions and absence of backwards migration scripts. // inconsistency between versions and absence of backwards migration scripts.
func (b *Bridge) SetUpdateChannel(channel updater.UpdateChannel) error { func (b *Bridge) SetUpdateChannel(channel updater.UpdateChannel) (needRestart bool, err error) {
b.settings.Set(settings.UpdateChannelKey, string(channel)) b.settings.Set(settings.UpdateChannelKey, string(channel))
version, err := b.updater.Check() version, err := b.updater.Check()
if err != nil { if err != nil {
return err return false, err
} }
if b.updater.IsDowngrade(version) { // We have to deal right away only with downgrade - that action needs to
if err := b.Users.ClearData(); err != nil { // clear data and updates, and install bridge right away. But regular
log.WithError(err).Error("Failed to clear data while downgrading channel") // upgrade can be leaved out for periodic check.
} if !b.updater.IsDowngrade(version) {
if err := b.locations.ClearUpdates(); err != nil { return false, nil
log.WithError(err).Error("Failed to clear updates while downgrading channel") }
}
if err := b.Users.ClearData(); err != nil {
log.WithError(err).Error("Failed to clear data while downgrading channel")
}
if err := b.locations.ClearUpdates(); err != nil {
log.WithError(err).Error("Failed to clear updates while downgrading channel")
} }
if err := b.updater.InstallUpdate(version); err != nil { if err := b.updater.InstallUpdate(version); err != nil {
return err return false, err
} }
return b.versioner.RemoveOtherVersions(version.Version) return true, b.versioner.RemoveOtherVersions(version.Version)
}
// GetKeychainApp returns current keychain helper.
func (b *Bridge) GetKeychainApp() string {
return b.settings.Get(settings.PreferredKeychainKey)
}
// SetKeychainApp sets current keychain helper.
func (b *Bridge) SetKeychainApp(helper string) {
b.settings.Set(settings.PreferredKeychainKey, helper)
} }

View File

@ -1,22 +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/>.
// Code generated by ./credits.sh at Mon Feb 1 10:34:22 CET 2021. DO NOT EDIT.
package bridge
const Credits = "github.com/0xAX/notificator;github.com/Masterminds/semver/v3;github.com/ProtonMail/bcrypt;github.com/ProtonMail/crypto;github.com/ProtonMail/docker-credential-helpers;github.com/ProtonMail/go-autostart;github.com/ProtonMail/go-imap;github.com/ProtonMail/go-imap-id;github.com/ProtonMail/go-rfc5322;github.com/ProtonMail/go-vcard;github.com/ProtonMail/gopenpgp/v2;github.com/PuerkitoBio/goquery;github.com/abiosoft/ishell;github.com/abiosoft/readline;github.com/allan-simon/go-singleinstance;github.com/chzyer/logex;github.com/chzyer/test;github.com/cucumber/godog;github.com/docker/docker-credential-helpers;github.com/emersion/go-imap;github.com/emersion/go-imap-appendlimit;github.com/emersion/go-imap-idle;github.com/emersion/go-imap-move;github.com/emersion/go-imap-quota;github.com/emersion/go-imap-unselect;github.com/emersion/go-mbox;github.com/emersion/go-message;github.com/emersion/go-sasl;github.com/emersion/go-smtp;github.com/emersion/go-textwrapper;github.com/emersion/go-vcard;github.com/fatih/color;github.com/flynn-archive/go-shlex;github.com/getsentry/sentry-go;github.com/go-resty/resty/v2;github.com/golang/mock;github.com/google/go-cmp;github.com/google/uuid;github.com/gopherjs/gopherjs;github.com/hashicorp/go-multierror;github.com/jameskeane/bcrypt;github.com/jaytaylor/html2text;github.com/keybase/go-keychain;github.com/logrusorgru/aurora;github.com/mattn/go-runewidth;github.com/miekg/dns;github.com/nsf/jsondiff;github.com/olekukonko/tablewriter;github.com/pkg/errors;github.com/sirupsen/logrus;github.com/skratchdot/open-golang;github.com/ssor/bom;github.com/stretchr/testify;github.com/therecipe/qt;github.com/urfave/cli/v2;github.com/vmihailenco/msgpack/v5;go.etcd.io/bbolt;golang.org/x/crypto;golang.org/x/net;golang.org/x/text;;Font Awesome 4.7.0;;Qt 5.13 by Qt group;"

View File

@ -21,39 +21,42 @@ import (
"fmt" "fmt"
"path/filepath" "path/filepath"
"github.com/ProtonMail/proton-bridge/internal/sentry"
"github.com/ProtonMail/proton-bridge/internal/store" "github.com/ProtonMail/proton-bridge/internal/store"
"github.com/ProtonMail/proton-bridge/internal/users" "github.com/ProtonMail/proton-bridge/internal/users"
"github.com/ProtonMail/proton-bridge/pkg/listener" "github.com/ProtonMail/proton-bridge/pkg/listener"
) )
type storeFactory struct { type storeFactory struct {
cache Cacher cache Cacher
panicHandler users.PanicHandler sentryReporter *sentry.Reporter
clientManager users.ClientManager panicHandler users.PanicHandler
eventListener listener.Listener clientManager users.ClientManager
storeCache *store.Cache eventListener listener.Listener
storeCache *store.Cache
} }
func newStoreFactory( func newStoreFactory(
cache Cacher, cache Cacher,
sentryReporter *sentry.Reporter,
panicHandler users.PanicHandler, panicHandler users.PanicHandler,
clientManager users.ClientManager, clientManager users.ClientManager,
eventListener listener.Listener, eventListener listener.Listener,
) *storeFactory { ) *storeFactory {
return &storeFactory{ return &storeFactory{
cache: cache, cache: cache,
panicHandler: panicHandler, sentryReporter: sentryReporter,
clientManager: clientManager, panicHandler: panicHandler,
eventListener: eventListener, clientManager: clientManager,
storeCache: store.NewCache(cache.GetIMAPCachePath()), eventListener: eventListener,
storeCache: store.NewCache(cache.GetIMAPCachePath()),
} }
} }
// 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.panicHandler, user, f.clientManager, f.eventListener, storePath, f.storeCache) return store.New(f.sentryReporter, f.panicHandler, user, f.clientManager, f.eventListener, storePath, f.storeCache)
} }
// Remove removes all store files for given user. // Remove removes all store files for given user.

View File

@ -21,6 +21,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"io/ioutil"
"os" "os"
"strconv" "strconv"
"sync" "sync"
@ -73,13 +74,12 @@ func (p *keyValueStore) save() error {
p.lock.Lock() p.lock.Lock()
defer p.lock.Unlock() defer p.lock.Unlock()
f, err := os.Create(p.path) b, err := json.MarshalIndent(p.cache, "", "\t")
if err != nil { if err != nil {
return err return err
} }
defer f.Close() //nolint[errcheck]
return json.NewEncoder(f).Encode(p.cache) return ioutil.WriteFile(p.path, b, 0600)
} }
func (p *keyValueStore) setDefault(key, value string) { func (p *keyValueStore) setDefault(key, value string) {

View File

@ -72,20 +72,20 @@ func TestKeyValueStoreSetDefault(t *testing.T) {
func TestKeyValueStoreSet(t *testing.T) { func TestKeyValueStoreSet(t *testing.T) {
pref := newTestEmptyKeyValueStore(t) pref := newTestEmptyKeyValueStore(t)
pref.Set("str", "value") pref.Set("str", "value")
checkSavedKeyValueStore(t, "{\"str\":\"value\"}") checkSavedKeyValueStore(t, "{\n\t\"str\": \"value\"\n}")
} }
func TestKeyValueStoreSetInt(t *testing.T) { func TestKeyValueStoreSetInt(t *testing.T) {
pref := newTestEmptyKeyValueStore(t) pref := newTestEmptyKeyValueStore(t)
pref.SetInt("int", 42) pref.SetInt("int", 42)
checkSavedKeyValueStore(t, "{\"int\":\"42\"}") checkSavedKeyValueStore(t, "{\n\t\"int\": \"42\"\n}")
} }
func TestKeyValueStoreSetBool(t *testing.T) { func TestKeyValueStoreSetBool(t *testing.T) {
pref := newTestEmptyKeyValueStore(t) pref := newTestEmptyKeyValueStore(t)
pref.SetBool("trueBool", true) pref.SetBool("trueBool", true)
pref.SetBool("falseBool", false) pref.SetBool("falseBool", false)
checkSavedKeyValueStore(t, "{\"falseBool\":\"false\",\"trueBool\":\"true\"}") checkSavedKeyValueStore(t, "{\n\t\"falseBool\": \"false\",\n\t\"trueBool\": \"true\"\n}")
} }
func newTestEmptyKeyValueStore(t *testing.T) *keyValueStore { func newTestEmptyKeyValueStore(t *testing.T) *keyValueStore {
@ -101,5 +101,5 @@ func newTestKeyValueStore(t *testing.T) *keyValueStore {
func checkSavedKeyValueStore(t *testing.T, expected string) { func checkSavedKeyValueStore(t *testing.T, expected string) {
data, err := ioutil.ReadFile(testPrefFilePath) data, err := ioutil.ReadFile(testPrefFilePath)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, expected+"\n", string(data)) require.Equal(t, expected, string(data))
} }

View File

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

View File

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

View File

@ -25,29 +25,27 @@ import (
"github.com/Masterminds/semver/v3" "github.com/Masterminds/semver/v3"
) )
// IsCatalinaOrNewer checks that host is MacOS Catalina 10.15.x or higher. // IsCatalinaOrNewer checks whether host is MacOS Catalina 10.15.x or higher.
func IsCatalinaOrNewer() bool { func IsCatalinaOrNewer() bool {
if runtime.GOOS != "darwin" { if runtime.GOOS != "darwin" {
return false return false
} }
return isVersionCatalinaOrNewer(getMacVersion())
}
func getMacVersion() string { rawVersion, err := exec.Command("sw_vers", "-productVersion").Output()
out, err := exec.Command("sw_vers", "-productVersion").Output()
if err != nil {
return ""
}
return strings.TrimSpace(string(out))
}
func isVersionCatalinaOrNewer(version string) bool {
v, err := semver.NewVersion(version)
if err != nil { if err != nil {
return false return false
} }
catalina := semver.MustParse("10.15.0") return isVersionCatalinaOrNewer(strings.TrimSpace(string(rawVersion)))
return v.GreaterThan(catalina) || v.Equal(catalina) }
func isVersionCatalinaOrNewer(rawVersion string) bool {
semVersion, err := semver.NewVersion(rawVersion)
if err != nil {
return false
}
minVersion := semver.MustParse("10.15.0")
return semVersion.GreaterThan(minVersion) || semVersion.Equal(minVersion)
} }

View File

@ -15,38 +15,45 @@
// 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 useragent
import ( import (
"fmt" "fmt"
"regexp"
"runtime" "runtime"
"strings"
) )
// removeBrackets handle unwanted brackets in client identification string and join with given joinBy parameter. type UserAgent struct {
// Mac OS X Mail/13.0 (3601.0.4) -> Mac OS X Mail/13.0-3601.0.4 (joinBy = "-") client, platform string
func removeBrackets(s string, joinBy string) (r string) {
r = strings.ReplaceAll(s, " (", joinBy)
r = strings.ReplaceAll(r, "(", joinBy) // Should be faster than regex.
r = strings.ReplaceAll(r, ")", "")
return
} }
func formatUserAgent(clientName, clientVersion, os string) string { func New() *UserAgent {
client := "" return &UserAgent{
if clientName != "" { client: "",
client = removeBrackets(clientName, "-") platform: runtime.GOOS,
if clientVersion != "" {
client += "/" + removeBrackets(clientVersion, "-")
}
} }
}
if os == "" {
os = runtime.GOOS func (ua *UserAgent) SetClient(name, version string) {
} ua.client = fmt.Sprintf("%v/%v", name, regexp.MustCompile(`(.*) \((.*)\)`).ReplaceAllString(version, "$1-$2"))
}
os = removeBrackets(os, " ")
func (ua *UserAgent) HasClient() bool {
return fmt.Sprintf("%s (%s)", client, os) return ua.client != ""
}
func (ua *UserAgent) SetPlatform(platform string) {
ua.platform = platform
}
func (ua *UserAgent) String() string {
var client string
if ua.client != "" {
client = ua.client
} else {
client = "NoClient/0.0.1"
}
return fmt.Sprintf("%v (%v)", client, ua.platform)
} }

View File

@ -0,0 +1,86 @@
// 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 useragent
import (
"fmt"
"runtime"
"testing"
"github.com/stretchr/testify/assert"
)
func TestUserAgent(t *testing.T) {
tests := []struct {
name, version, platform string
want string
}{
// No name/version, no platform.
{
want: fmt.Sprintf("NoClient/0.0.1 (%v)", runtime.GOOS),
},
// No name/version, with platform.
{
platform: "macOS 10.15",
want: "NoClient/0.0.1 (macOS 10.15)",
},
// With name/version, with platform.
{
name: "Mac OS X Mail",
version: "1.0.0",
platform: "macOS 10.15",
want: "Mac OS X Mail/1.0.0 (macOS 10.15)",
},
// With name/version, with platform.
{
name: "Mac OS X Mail",
version: "13.4 (3608.120.23.2.4)",
platform: "macOS 10.15",
want: "Mac OS X Mail/13.4-3608.120.23.2.4 (macOS 10.15)",
},
// With name/version, with platform.
{
name: "Thunderbird",
version: "78.6.1",
platform: "Windows 10 (10.0)",
want: "Thunderbird/78.6.1 (Windows 10 (10.0))",
},
}
for _, test := range tests {
test := test
t.Run(test.want, func(t *testing.T) {
ua := New()
if test.name != "" && test.version != "" {
ua.SetClient(test.name, test.version)
}
if test.platform != "" {
ua.SetPlatform(test.platform)
}
assert.Equal(t, test.want, ua.String())
})
}
}

View File

@ -15,18 +15,14 @@
// 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 imap // +build !build_qa
import ( package constants
"io"
"net/http" import "time"
"net/textproto"
// nolint[gochecknoglobals]
var (
// UpdateCheckInterval defines how often we check for new version
UpdateCheckInterval = time.Hour //nolint[gochecknoglobals]
) )
func writeHeader(w io.Writer, h textproto.MIMEHeader) (err error) {
if err = http.Header(h).Write(w); err != nil {
return
}
_, err = io.WriteString(w, "\r\n")
return
}

View File

@ -0,0 +1,28 @@
// 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/>.
// +build build_qa
package constants
import "time"
// nolint[gochecknoglobals]
var (
// UpdateCheckInterval defines how often we check for new version
UpdateCheckInterval = time.Duration(5 * time.Minute)
)

View File

@ -19,7 +19,7 @@
package crash package crash
import ( import (
"github.com/ProtonMail/proton-bridge/pkg/sentry" "github.com/ProtonMail/proton-bridge/internal/sentry"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )

View File

@ -93,7 +93,7 @@ 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) f, err := os.OpenFile(filepath.Clean(filepath.Join(dir, "protonmail.mobileconfig")), os.O_RDWR|os.O_CREATE, 0600)
if err != nil { if err != nil {
return err return err
} }

View File

@ -25,7 +25,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() {

View File

@ -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)
} }

View File

@ -28,7 +28,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() {

View File

@ -102,10 +102,6 @@ func New( //nolint[funlen]
Aliases: []string{"p"}, Aliases: []string{"p"},
Func: fe.changePort, Func: fe.changePort,
}) })
changeCmd.AddCmd(&ishell.Cmd{Name: "proxy",
Help: "allow or disallow bridge to securely connect to proton via a third party when it is being blocked",
Func: fe.toggleAllowProxy,
})
changeCmd.AddCmd(&ishell.Cmd{Name: "smtp-security", changeCmd.AddCmd(&ishell.Cmd{Name: "smtp-security",
Help: "change port numbers of IMAP and SMTP servers.(alias: ssl, starttls)", Help: "change port numbers of IMAP and SMTP servers.(alias: ssl, starttls)",
Aliases: []string{"ssl", "starttls"}, Aliases: []string{"ssl", "starttls"},
@ -113,13 +109,56 @@ func New( //nolint[funlen]
}) })
fe.AddCmd(changeCmd) fe.AddCmd(changeCmd)
// DoH commands.
dohCmd := &ishell.Cmd{Name: "proxy",
Help: "allow or disallow bridge to securely connect to proton via a third party when it is being blocked",
}
dohCmd.AddCmd(&ishell.Cmd{Name: "allow",
Help: "allow bridge to securely connect to proton via a third party when it is being blocked",
Func: fe.allowProxy,
})
dohCmd.AddCmd(&ishell.Cmd{Name: "disallow",
Help: "disallow bridge to securely connect to proton via a third party when it is being blocked",
Func: fe.disallowProxy,
})
fe.AddCmd(dohCmd)
// Updates commands.
updatesCmd := &ishell.Cmd{Name: "updates",
Help: "manage bridge updates",
}
updatesCmd.AddCmd(&ishell.Cmd{Name: "check",
Help: "check for Bridge updates",
Func: fe.checkUpdates,
})
autoUpdatesCmd := &ishell.Cmd{Name: "autoupdates",
Help: "manage bridge updates",
}
updatesCmd.AddCmd(autoUpdatesCmd)
autoUpdatesCmd.AddCmd(&ishell.Cmd{Name: "enable",
Help: "automatically keep bridge up to date",
Func: fe.enableAutoUpdates,
})
autoUpdatesCmd.AddCmd(&ishell.Cmd{Name: "disable",
Help: "require bridge to be manually updated",
Func: fe.disableAutoUpdates,
})
updatesChannelCmd := &ishell.Cmd{Name: "channel",
Help: "switch updates channel",
}
updatesCmd.AddCmd(updatesChannelCmd)
updatesChannelCmd.AddCmd(&ishell.Cmd{Name: "early",
Help: "switch to the early-access updates channel",
Func: fe.selectEarlyChannel,
})
updatesChannelCmd.AddCmd(&ishell.Cmd{Name: "stable",
Help: "switch to the stable updates channel",
Func: fe.selectStableChannel,
})
fe.AddCmd(updatesCmd)
// Check commands. // Check commands.
checkCmd := &ishell.Cmd{Name: "check", Help: "check internet connection or new version."} checkCmd := &ishell.Cmd{Name: "check", Help: "check internet connection or new version."}
checkCmd.AddCmd(&ishell.Cmd{Name: "updates",
Help: "check for Bridge updates. (aliases: u, v, version)",
Aliases: []string{"u", "version", "v"},
Func: fe.checkUpdates,
})
checkCmd.AddCmd(&ishell.Cmd{Name: "internet", checkCmd.AddCmd(&ishell.Cmd{Name: "internet",
Help: "check internet connection. (aliases: i, conn, connection)", Help: "check internet connection. (aliases: i, conn, connection)",
Aliases: []string{"i", "con", "connection"}, Aliases: []string{"i", "con", "connection"},

View File

@ -132,24 +132,36 @@ func (f *frontendCLI) changePort(c *ishell.Context) {
} }
} }
func (f *frontendCLI) toggleAllowProxy(c *ishell.Context) { func (f *frontendCLI) allowProxy(c *ishell.Context) {
if f.settings.GetBool(settings.AllowProxyKey) { if f.settings.GetBool(settings.AllowProxyKey) {
f.Println("Bridge is currently set to use alternative routing to connect to Proton if it is being blocked.") f.Println("Bridge is already set to use alternative routing to connect to Proton if it is being blocked.")
if f.yesNoQuestion("Are you sure you want to stop bridge from doing this") { return
f.settings.SetBool(settings.AllowProxyKey, false) }
f.bridge.DisallowProxy()
} f.Println("Bridge is currently set to NOT use alternative routing to connect to Proton if it is being blocked.")
} else {
f.Println("Bridge is currently set to NOT use alternative routing to connect to Proton if it is being blocked.") if f.yesNoQuestion("Are you sure you want to allow bridge to do this") {
if f.yesNoQuestion("Are you sure you want to allow bridge to do this") { f.settings.SetBool(settings.AllowProxyKey, true)
f.settings.SetBool(settings.AllowProxyKey, true) f.bridge.AllowProxy()
f.bridge.AllowProxy() }
} }
func (f *frontendCLI) disallowProxy(c *ishell.Context) {
if !f.settings.GetBool(settings.AllowProxyKey) {
f.Println("Bridge is already set to NOT use alternative routing to connect to Proton if it is being blocked.")
return
}
f.Println("Bridge is currently set to use alternative routing to connect to Proton if it is being blocked.")
if f.yesNoQuestion("Are you sure you want to stop bridge from doing this") {
f.settings.SetBool(settings.AllowProxyKey, false)
f.bridge.DisallowProxy()
} }
} }
func (f *frontendCLI) isPortFree(port string) bool { func (f *frontendCLI) isPortFree(port string) bool {
port = strings.Replace(port, ":", "", -1) port = strings.ReplaceAll(port, ":", "")
if port == "" || port == currentPort { if port == "" || port == currentPort {
return true return true
} }

View File

@ -21,11 +21,23 @@ import (
"strings" "strings"
"github.com/ProtonMail/proton-bridge/internal/bridge" "github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/config/settings"
"github.com/ProtonMail/proton-bridge/internal/updater"
"github.com/abiosoft/ishell" "github.com/abiosoft/ishell"
) )
func (f *frontendCLI) checkUpdates(c *ishell.Context) { func (f *frontendCLI) checkUpdates(c *ishell.Context) {
f.Println("Your version is up to date.") version, err := f.updater.Check()
if err != nil {
f.Println("An error occurred while checking for updates.")
return
}
if f.updater.IsUpdateApplicable(version) {
f.Println("An update is available.")
} else {
f.Println("Your version is up to date.")
}
} }
func (f *frontendCLI) printCredits(c *ishell.Context) { func (f *frontendCLI) printCredits(c *ishell.Context) {
@ -33,3 +45,68 @@ func (f *frontendCLI) printCredits(c *ishell.Context) {
f.Println(pkg) f.Println(pkg)
} }
} }
func (f *frontendCLI) enableAutoUpdates(c *ishell.Context) {
if f.settings.GetBool(settings.AutoUpdateKey) {
f.Println("Bridge is already set to automatically install updates.")
return
}
f.Println("Bridge is currently set to NOT automatically install updates.")
if f.yesNoQuestion("Are you sure you want to allow bridge to do this") {
f.settings.SetBool(settings.AutoUpdateKey, true)
}
}
func (f *frontendCLI) disableAutoUpdates(c *ishell.Context) {
if !f.settings.GetBool(settings.AutoUpdateKey) {
f.Println("Bridge is already set to NOT automatically install updates.")
return
}
f.Println("Bridge is currently set to automatically install updates.")
if f.yesNoQuestion("Are you sure you want to stop bridge from doing this") {
f.settings.SetBool(settings.AutoUpdateKey, false)
}
}
func (f *frontendCLI) selectEarlyChannel(c *ishell.Context) {
if f.bridge.GetUpdateChannel() == updater.EarlyChannel {
f.Println("Bridge is already on the early-access update channel.")
return
}
f.Println("Bridge is currently on the stable update channel.")
if f.yesNoQuestion("Are you sure you want to switch to the early-access update channel") {
needRestart, err := f.bridge.SetUpdateChannel(updater.EarlyChannel)
if err != nil {
f.Println("There was a problem switching update channel.")
}
if needRestart {
f.restarter.SetToRestart()
}
}
}
func (f *frontendCLI) selectStableChannel(c *ishell.Context) {
if f.bridge.GetUpdateChannel() == updater.StableChannel {
f.Println("Bridge is already on the stable update channel.")
return
}
f.Println("Bridge is currently on the early-access update channel.")
f.Println("Switching to the stable channel may reset all data!")
if f.yesNoQuestion("Are you sure you want to switch to the stable update channel") {
needRestart, err := f.bridge.SetUpdateChannel(updater.StableChannel)
if err != nil {
f.Println("There was a problem switching update channel.")
}
if needRestart {
f.restarter.SetToRestart()
}
}
}

View File

@ -22,6 +22,7 @@ import (
"github.com/ProtonMail/go-autostart" "github.com/ProtonMail/go-autostart"
"github.com/ProtonMail/proton-bridge/internal/bridge" "github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/config/settings" "github.com/ProtonMail/proton-bridge/internal/config/settings"
"github.com/ProtonMail/proton-bridge/internal/config/useragent"
"github.com/ProtonMail/proton-bridge/internal/frontend/cli" "github.com/ProtonMail/proton-bridge/internal/frontend/cli"
cliie "github.com/ProtonMail/proton-bridge/internal/frontend/cli-ie" cliie "github.com/ProtonMail/proton-bridge/internal/frontend/cli-ie"
"github.com/ProtonMail/proton-bridge/internal/frontend/qt" "github.com/ProtonMail/proton-bridge/internal/frontend/qt"
@ -60,6 +61,7 @@ func New(
settings *settings.Settings, settings *settings.Settings,
eventListener listener.Listener, eventListener listener.Listener,
updater types.Updater, updater types.Updater,
userAgent *useragent.UserAgent,
bridge *bridge.Bridge, bridge *bridge.Bridge,
noEncConfirmator types.NoEncConfirmator, noEncConfirmator types.NoEncConfirmator,
autostart *autostart.App, autostart *autostart.App,
@ -77,6 +79,7 @@ func New(
settings, settings,
eventListener, eventListener,
updater, updater,
userAgent,
bridgeWrap, bridgeWrap,
noEncConfirmator, noEncConfirmator,
autostart, autostart,
@ -95,6 +98,7 @@ func newBridgeFrontend(
settings *settings.Settings, settings *settings.Settings,
eventListener listener.Listener, eventListener listener.Listener,
updater types.Updater, updater types.Updater,
userAgent *useragent.UserAgent,
bridge types.Bridger, bridge types.Bridger,
noEncConfirmator types.NoEncConfirmator, noEncConfirmator types.NoEncConfirmator,
autostart *autostart.App, autostart *autostart.App,
@ -122,6 +126,7 @@ func newBridgeFrontend(
settings, settings,
eventListener, eventListener,
updater, updater,
userAgent,
bridge, bridge,
noEncConfirmator, noEncConfirmator,
autostart, autostart,

View File

@ -0,0 +1,194 @@
// 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/>.
// Change default keychain dialog
import QtQuick 2.8
import BridgeUI 1.0
import ProtonUI 1.0
import QtQuick.Controls 2.2 as QC
import QtQuick.Layouts 1.0
Dialog {
id: root
title : "Change which keychain Bridge uses as default"
subtitle : "Select which keychain is used (Bridge will automatically restart)"
isDialogBusy: currentIndex==1
property var selectedKeychain
Connections {
target: go.selectedKeychain
onValueChanged: {
console.debug("go.selectedKeychain == ", go.selectedKeychain)
}
}
ColumnLayout {
Layout.fillHeight: true
Layout.fillWidth: true
Item {
Layout.fillWidth: true
Layout.minimumHeight: root.titleHeight + Style.dialog.heightSeparator
Layout.maximumHeight: root.titleHeight + Style.dialog.heightSeparator
}
Item {
Layout.fillHeight: true
Layout.fillWidth: true
ColumnLayout {
anchors.centerIn: parent
Repeater {
id: keychainRadioButtons
model: go.availableKeychain
QC.RadioButton {
id: radioDelegate
text: modelData
checked: go.selectedKeychain === modelData
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
spacing: Style.main.spacing
indicator: Text {
text : radioDelegate.checked ? Style.fa.check_circle : Style.fa.circle_o
color : radioDelegate.checked ? Style.main.textBlue : Style.main.textInactive
font {
pointSize: Style.dialog.iconSize * Style.pt
family: Style.fontawesome.name
}
}
contentItem: Text {
text: radioDelegate.text
color: Style.main.text
font {
pointSize: Style.dialog.fontSize * Style.pt
bold: checked
}
horizontalAlignment : Text.AlignHCenter
verticalAlignment : Text.AlignVCenter
leftPadding: Style.dialog.iconSize
}
onCheckedChanged: {
if (checked) {
root.selectedKeychain = modelData
}
}
}
}
Item {
Layout.fillWidth: true
Layout.minimumHeight: Style.dialog.heightSeparator
Layout.maximumHeight: Style.dialog.heightSeparator
}
Row {
id: buttonRow
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
spacing: Style.dialog.spacing
ButtonRounded {
id:buttonNo
color_main: Style.dialog.text
fa_icon: Style.fa.times
text: qsTr("Cancel", "dismisses current action")
onClicked : root.hide()
}
ButtonRounded {
id: buttonYes
color_main: Style.dialog.text
color_minor: Style.main.textBlue
isOpaque: true
fa_icon: Style.fa.check
text: qsTr("Okay", "confirms and dismisses a notification")
onClicked : root.confirmed()
}
}
}
}
}
ColumnLayout {
Layout.fillHeight: true
Layout.fillWidth: true
Item {
Layout.fillWidth: true
Layout.minimumHeight: root.titleHeight + Style.dialog.heightSeparator
Layout.maximumHeight: root.titleHeight + Style.dialog.heightSeparator
}
Item {
Layout.fillHeight: true
Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
Text {
id: answ
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
width : parent.width/2
color: Style.dialog.text
font {
pointSize : Style.dialog.fontSize * Style.pt
bold : true
}
text : "Default keychain is now set to " + root.selectedKeychain +
"\n\n" +
qsTr("Settings will be applied after the next start.", "notification about setting being applied after next start") +
"\n\n" +
qsTr("Bridge will now restart.", "notification about restarting")
wrapMode: Text.Wrap
horizontalAlignment: Text.AlignHCenter
}
}
}
Shortcut {
sequence: StandardKey.Cancel
onActivated: root.hide()
}
Shortcut {
sequence: "Enter"
onActivated: root.confirmed()
}
function confirmed() {
if (selectedKeychain === go.selectedKeychain) {
root.hide()
return
}
incrementCurrentIndex()
timer.start()
}
timer.interval : 5000
Connections {
target: timer
onTriggered: {
// This action triggers restart on the backend side.
go.selectedKeychain = selectedKeychain
}
}
}

View File

@ -229,7 +229,7 @@ Dialog {
currentIndex : 0 currentIndex : 0
title : qsTr("Clear cache", "title of page that displays during cache clearing") title : qsTr("Clear cache", "title of page that displays during cache clearing")
question : qsTr("Are you sure you want to clear your local cache?", "displays during cache clearing") question : qsTr("Are you sure you want to clear your local cache?", "displays during cache clearing")
note : qsTr("This will delete all of your stored preferences as well as cached email data for all accounts, temporarily slowing down the email download process significantly.", "displays during cache clearing") note : qsTr("This will delete all of your stored preferences as well as cached email data for all accounts, and requires you to reconfigure your client.", "displays during cache clearing")
answer : qsTr("Clearing the cache ...", "displays during cache clearing") answer : qsTr("Clearing the cache ...", "displays during cache clearing")
} }
}, },
@ -310,7 +310,7 @@ Dialog {
target: root target: root
currentIndex : 0 currentIndex : 0
question : qsTr("Are you sure you want to leave early access? Please keep in mind this operation clears the cache and restarts Bridge.") question : qsTr("Are you sure you want to leave early access? Please keep in mind this operation clears the cache and restarts Bridge.")
note : qsTr("This will delete all of your stored preferences as well as cached email data for all accounts, temporarily slowing down the email download process significantly.") note : qsTr("This will delete all of your stored preferences as well as cached email data for all accounts, and requires you to reconfigure your client.")
title : qsTr("Disable early access") title : qsTr("Disable early access")
answer : qsTr("Disabling early access...") answer : qsTr("Disabling early access...")
} }

View File

@ -303,6 +303,10 @@ Window {
id: dialogChangePort id: dialogChangePort
} }
DialogKeychainChange {
id: dialogChangeKeychain
}
DialogConnectionTroubleshoot { DialogConnectionTroubleshoot {
id: dialogConnectionTroubleshoot id: dialogConnectionTroubleshoot
} }

View File

@ -239,6 +239,25 @@ Item {
dialogGlobal.show() dialogGlobal.show()
} }
} }
ButtonIconText {
id: changeKeychain
visible: advancedSettings.isAdvanced && (go.availableKeychain.length > 1)
text: qsTr("Change keychain", "button to open dialog with default keychain selection")
leftIcon.text : Style.fa.key
rightIcon {
text : qsTr("Change", "clickable link next to change keychain button in settings")
color: Style.main.text
font {
family : changeKeychain.font.family // use default font, not font-awesome
pointSize : Style.settings.fontSize * Style.pt
underline : true
}
}
onClicked: {
dialogChangeKeychain.show()
}
}
} }
} }
} }

View File

@ -2,6 +2,7 @@ module BridgeUI
AccountDelegate 1.0 AccountDelegate.qml AccountDelegate 1.0 AccountDelegate.qml
Credits 1.0 Credits.qml Credits 1.0 Credits.qml
DialogFirstStart 1.0 DialogFirstStart.qml DialogFirstStart 1.0 DialogFirstStart.qml
DialogKeychainChange 1.0 DialogKeychainChange.qml
DialogPortChange 1.0 DialogPortChange.qml DialogPortChange 1.0 DialogPortChange.qml
DialogYesNo 1.0 DialogYesNo.qml DialogYesNo 1.0 DialogYesNo.qml
DialogTLSCertInfo 1.0 DialogTLSCertInfo.qml DialogTLSCertInfo 1.0 DialogTLSCertInfo.qml

View File

@ -107,17 +107,53 @@ Item {
gui.openMainWindow(false) gui.openMainWindow(false)
if (go.isConnectionOK) { if (go.isConnectionOK) {
if( winMain.updateState=="noInternet") { if( winMain.updateState=="noInternet") {
go.setUpdateState("upToDate") go.updateState = "upToDate"
} }
} else { } else {
go.setUpdateState("noInternet") go.updateState = "noInternet"
} }
} }
onSetUpdateState : { onUpdateStateChanged : {
// Update tray icon if needed
switch (go.updateState) {
case "internetCheck":
break;
case "noInternet" :
gui.warningFlags |= Style.warnInfoBar
break;
case "oldVersion":
gui.warningFlags |= Style.warnInfoBar
break;
case "forceUpdate":
// Force update should presist once it happened and never be overwritten.
// That means that tray icon should allways remain in error state.
// But since we have only two sources of error icon in tray (force update
// + installation fail) and both are unrecoverable and we do not ever remove
// error flag from gui.warningFlags - it is ok to rely on gui.warningFlags and
// not on winMain.updateState (which presist forceUpdate)
gui.warningFlags |= Style.errorInfoBar
break;
case "upToDate":
gui.warningFlags &= ~Style.warnInfoBar
break;
case "updateRestart":
gui.warningFlags |= Style.warnInfoBar
break;
case "updateError":
gui.warningFlags |= Style.errorInfoBar
break;
default :
break;
}
// if main window is closed - most probably it is destroyed (see closeMainWindow())
if (winMain == null) {
return
}
// once app is outdated prevent from state change // once app is outdated prevent from state change
if (winMain.updateState != "forceUpdate") { if (winMain.updateState != "forceUpdate") {
winMain.updateState = updateState winMain.updateState = go.updateState
} }
} }
@ -129,15 +165,14 @@ Item {
} }
onNotifyManualUpdate: { onNotifyManualUpdate: {
go.setUpdateState("oldVersion") go.updateState = "oldVersion"
} }
onNotifyManualUpdateRestartNeeded: { onNotifyManualUpdateRestartNeeded: {
if (!winMain.dialogUpdate.visible) { if (!winMain.dialogUpdate.visible) {
gui.openMainWindow(true)
winMain.dialogUpdate.show() winMain.dialogUpdate.show()
} }
go.setUpdateState("updateRestart") go.updateState = "updateRestart"
winMain.dialogUpdate.finished(false) winMain.dialogUpdate.finished(false)
// after manual update - just retart immidiatly // after manual update - just retart immidiatly
@ -147,28 +182,25 @@ Item {
onNotifyManualUpdateError: { onNotifyManualUpdateError: {
if (!winMain.dialogUpdate.visible) { if (!winMain.dialogUpdate.visible) {
gui.openMainWindow(true)
winMain.dialogUpdate.show() winMain.dialogUpdate.show()
} }
go.setUpdateState("updateError") go.updateState = "updateError"
winMain.dialogUpdate.finished(true) winMain.dialogUpdate.finished(true)
} }
onNotifyForceUpdate : { onNotifyForceUpdate : {
go.setUpdateState("forceUpdate") go.updateState = "forceUpdate"
if (!winMain.dialogUpdate.visible) { if (!winMain.dialogUpdate.visible) {
gui.openMainWindow(true)
winMain.dialogUpdate.show() winMain.dialogUpdate.show()
} }
} }
onNotifySilentUpdateRestartNeeded: { onNotifySilentUpdateRestartNeeded: {
go.setUpdateState("updateRestart") go.updateState = "updateRestart"
} }
onNotifySilentUpdateError: { onNotifySilentUpdateError: {
go.setUpdateState("updateError") go.updateState = "updateError"
gui.openMainWindow(true)
} }
onNotifyLogout : { onNotifyLogout : {
@ -287,9 +319,17 @@ Item {
if (showAndRise) { if (showAndRise) {
gui.winMain.showAndRise() gui.winMain.showAndRise()
} }
// restore update notification bar: trigger updateStateChanged
var tmp = go.updateState
go.updateState = ""
go.updateState = tmp
} }
function closeMainWindow () { function closeMainWindow () {
// Historical reasons: once upon a time there was a report about high GPU
// usage on MacOS while bridge is closed. Legends say that destroying
// MainWindow solved this.
gui.winMain.hide() gui.winMain.hide()
gui.winMain.destroy(5000) gui.winMain.destroy(5000)
gui.winMain = null gui.winMain = null

View File

@ -30,7 +30,6 @@ Item {
id: gui id: gui
property alias winMain: winMain property alias winMain: winMain
property bool isFirstWindow: true property bool isFirstWindow: true
property int warningFlags: 0
property var locale : Qt.locale("en_US") property var locale : Qt.locale("en_US")
property date netBday : new Date("1989-03-13T00:00:00") property date netBday : new Date("1989-03-13T00:00:00")
@ -96,17 +95,17 @@ Item {
go.isConnectionOK = isAvailable go.isConnectionOK = isAvailable
if (go.isConnectionOK) { if (go.isConnectionOK) {
if( winMain.updateState==gui.enums.statusNoInternet) { if( winMain.updateState==gui.enums.statusNoInternet) {
go.setUpdateState(gui.enums.statusUpToDate) go.updateState = gui.enums.statusUpToDate
} }
} else { } else {
go.setUpdateState(gui.enums.statusNoInternet) go.updateState = gui.enums.statusNoInternet
} }
} }
onSetUpdateState : { onUpdateStateChanged : {
// once app is outdated prevent from state change // once app is outdated prevent from state change
if (winMain.updateState != "forceUpdate") { if (winMain.updateState != "forceUpdate") {
winMain.updateState = updateState winMain.updateState = go.updateState
} }
} }
@ -207,16 +206,16 @@ Item {
} }
onNotifyManualUpdate: { onNotifyManualUpdate: {
go.setUpdateState("oldVersion") go.updateState = "oldVersion"
} }
onNotifyManualUpdateRestartNeeded: { onNotifyManualUpdateRestartNeeded: {
if (!winMain.dialogUpdate.visible) { if (!winMain.dialogUpdate.visible) {
winMain.dialogUpdate.show() winMain.dialogUpdate.show()
} }
go.setUpdateState("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()
@ -226,24 +225,24 @@ Item {
if (!winMain.dialogUpdate.visible) { if (!winMain.dialogUpdate.visible) {
winMain.dialogUpdate.show() winMain.dialogUpdate.show()
} }
go.setUpdateState("updateError") go.updateState = "updateError"
winMain.dialogUpdate.finished(true) winMain.dialogUpdate.finished(true)
} }
onNotifyForceUpdate : { onNotifyForceUpdate : {
go.setUpdateState("forceUpdate") go.updateState = "forceUpdate"
if (!winMain.dialogUpdate.visible) { if (!winMain.dialogUpdate.visible) {
winMain.dialogUpdate.show() winMain.dialogUpdate.show()
} }
} }
onNotifySilentUpdateRestartNeeded: { //onNotifySilentUpdateRestartNeeded: {
go.setUpdateState("updateRestart") // go.updateState = "updateRestart"
} //}
//
onNotifySilentUpdateError: { //onNotifySilentUpdateError: {
go.setUpdateState("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) )

View File

@ -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()
} }

View File

@ -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
@ -428,7 +429,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:

View File

@ -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 != ""
@ -1032,6 +1033,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

View File

@ -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")

View File

@ -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
@ -111,6 +115,11 @@ StackLayout {
Accessible.description: title Accessible.description: title
Accessible.focusable: true Accessible.focusable: true
onVisibleChanged: {
if (background.visible != visible) {
background.visible = visible
}
}
visible : false visible : false
anchors { anchors {

View File

@ -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 {

View File

@ -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 {

View File

@ -66,14 +66,15 @@ Rectangle {
ClickIconText { ClickIconText {
id: linkText id: linkText
anchors.verticalCenter : message.verticalCenter anchors.verticalCenter : message.verticalCenter
iconText : "" iconText : " "
fontSize : root.fontSize fontSize : root.fontSize
textUnderline: true
} }
ClickIconText { ClickIconText {
id: actionText id: actionText
anchors.verticalCenter : message.verticalCenter anchors.verticalCenter : message.verticalCenter
iconText : "" iconText : " "
fontSize : root.fontSize fontSize : root.fontSize
textUnderline: true textUnderline: true
} }
@ -107,31 +108,25 @@ Rectangle {
onStateChanged : { onStateChanged : {
switch (root.state) { switch (root.state) {
case "internetCheck": case "internetCheck":
break; break;
case "noInternet" : case "noInternet" :
gui.warningFlags |= Style.warnInfoBar retryInternet.start()
retryInternet.start() secLeft=checkInterval[iTry]
secLeft=checkInterval[iTry] break;
break;
case "oldVersion": case "oldVersion":
gui.warningFlags |= Style.warnInfoBar break;
break;
case "forceUpdate": case "forceUpdate":
gui.warningFlags |= Style.errorInfoBar break;
break;
case "upToDate": case "upToDate":
gui.warningFlags &= ~Style.warnInfoBar iTry = 0
iTry = 0 secLeft=checkInterval[iTry]
secLeft=checkInterval[iTry] break;
break;
case "updateRestart": case "updateRestart":
gui.warningFlags |= Style.warnInfoBar break;
break;
case "updateError": case "updateError":
gui.warningFlags |= Style.errorInfoBar break;
break;
default : default :
break; break;
} }
if (root.state!="noInternet") { if (root.state!="noInternet") {
@ -247,7 +242,7 @@ Rectangle {
PropertyChanges { PropertyChanges {
target: linkText target: linkText
visible: true visible: true
text: "(" + qsTr("view release notes", "display the release notes from the new version") + ")" text: qsTr("Release Notes", "display the release notes from the new version")
onClicked: gui.openReleaseNotes() onClicked: gui.openReleaseNotes()
} }
PropertyChanges { PropertyChanges {
@ -270,7 +265,7 @@ Rectangle {
target: closeSign target: closeSign
visible: true visible: true
onClicked: { onClicked: {
root.state = "upToDate" go.updateState = "upToDate"
} }
} }
}, },

View File

@ -24,7 +24,7 @@ import QtQuick.Window 2.2
Window { Window {
id: testroot id: testroot
width : 150 width : 250
height : 600 height : 600
flags : Qt.Window | Qt.Dialog | Qt.FramelessWindowHint flags : Qt.Window | Qt.Dialog | Qt.FramelessWindowHint
visible : true visible : true
@ -60,7 +60,7 @@ Window {
Text { Text {
id: systrText id: systrText
anchors { anchors {
right : test_systray.right horizontalCenter: parent.horizontalCenter
verticalCenter: test_systray.verticalCenter verticalCenter: test_systray.verticalCenter
} }
text: "unset" text: "unset"
@ -281,6 +281,9 @@ Window {
property bool hasNoKeychain : true property bool hasNoKeychain : true
property var availableKeychain: ["pass-app", "gnome-keyring"]
property var selectedKeychain: "gnome-keyring"
property string wrongCredentials property string wrongCredentials
property string wrongMailboxPassword property string wrongMailboxPassword
property string canNotReachAPI property string canNotReachAPI
@ -296,6 +299,7 @@ Window {
property string fullversion : "QA.1.0 (d9f8sdf9) 2020-02-19T10:57:23+01:00" property string fullversion : "QA.1.0 (d9f8sdf9) 2020-02-19T10:57:23+01:00"
property string downloadLink: "https://protonmail.com/download/beta/protonmail-bridge-1.1.5-1.x86_64.rpm;https://www.protonmail.com/downloads/beta/Desktop-Bridge-link1.exe;https://www.protonmail.com/downloads/beta/Desktop-Bridge-link1.exe;https://www.protonmail.com/downloads/beta/Desktop-Bridge-link1.exe;" property string downloadLink: "https://protonmail.com/download/beta/protonmail-bridge-1.1.5-1.x86_64.rpm;https://www.protonmail.com/downloads/beta/Desktop-Bridge-link1.exe;https://www.protonmail.com/downloads/beta/Desktop-Bridge-link1.exe;https://www.protonmail.com/downloads/beta/Desktop-Bridge-link1.exe;"
property string updateState
property string updateVersion : "QA.1.0" property string updateVersion : "QA.1.0"
property bool updateCanInstall: true property bool updateCanInstall: true
property string updateLandingPage : "https://protonmail.com/bridge/download/" property string updateLandingPage : "https://protonmail.com/bridge/download/"
@ -337,7 +341,6 @@ Window {
signal notifyPortIssue(bool busyPortIMAP, bool busyPortSMTP) signal notifyPortIssue(bool busyPortIMAP, bool busyPortSMTP)
signal notifyVersionIsTheLatest() signal notifyVersionIsTheLatest()
signal setUpdateState(string updateState)
signal notifyKeychainRebuild() signal notifyKeychainRebuild()
signal notifyHasNoKeychain() signal notifyHasNoKeychain()

View File

@ -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"
@ -856,16 +856,17 @@ Window {
property string fullversion : "QA.1.0 (d9f8sdf9) 2020-02-19T10:57:23+01:00" property string fullversion : "QA.1.0 (d9f8sdf9) 2020-02-19T10:57:23+01:00"
property string downloadLink: "https://protonmail.com/download/beta/protonmail-bridge-1.1.5-1.x86_64.rpm;https://www.protonmail.com/downloads/beta/Desktop-Bridge-link1.exe;https://www.protonmail.com/downloads/beta/Desktop-Bridge-link1.exe;https://www.protonmail.com/downloads/beta/Desktop-Bridge-link1.exe;" property string downloadLink: "https://protonmail.com/download/beta/protonmail-bridge-1.1.5-1.x86_64.rpm;https://www.protonmail.com/downloads/beta/Desktop-Bridge-link1.exe;https://www.protonmail.com/downloads/beta/Desktop-Bridge-link1.exe;https://www.protonmail.com/downloads/beta/Desktop-Bridge-link1.exe;"
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()
@ -900,7 +901,6 @@ Window {
signal showQuit() signal showQuit()
signal notifyVersionIsTheLatest() signal notifyVersionIsTheLatest()
signal setUpdateState(string updateState)
signal showMainWin() signal showMainWin()
signal hideMainWin() signal hideMainWin()
@ -1355,10 +1355,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)
} //}
} }
} }

View File

@ -15,7 +15,7 @@
// 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/>.
// +build !nogui // +build build_qt
package qtcommon package qtcommon

View File

@ -15,7 +15,7 @@
// 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/>.
// +build !nogui // +build build_qt
package qtcommon package qtcommon

View File

@ -1,4 +1,4 @@
// +build !nogui // +build build_qt
#include "common.h" #include "common.h"
#include "_cgo_export.h" #include "_cgo_export.h"

View File

@ -15,7 +15,7 @@
// 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/>.
// +build !nogui // +build build_qt
package qtcommon package qtcommon

View File

@ -15,7 +15,7 @@
// 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/>.
// +build !nogui // +build build_qt
package qtcommon package qtcommon

View File

@ -15,7 +15,7 @@
// 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/>.
// +build !nogui // +build build_qt
package qtie package qtie

View File

@ -15,7 +15,7 @@
// 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/>.
// +build !nogui // +build build_qt
package qtie package qtie

View File

@ -15,7 +15,7 @@
// 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/>.
// +build !nogui // +build build_qt
package qtie package qtie
@ -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

View File

@ -15,7 +15,7 @@
// 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/>.
// +build !nogui // +build build_qt
package qtie package qtie

View File

@ -15,7 +15,7 @@
// 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/>.
// +build !nogui // +build build_qt
package qtie package qtie

View File

@ -15,7 +15,7 @@
// 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/>.
// +build !nogui // +build build_qt
package qtie package qtie

View File

@ -15,7 +15,7 @@
// 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/>.
// +build !nogui // +build build_qt
package qtie package qtie
@ -135,11 +135,11 @@ 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() {
@ -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,17 +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 // checkInternet is almost idetical to bridge
func (f *FrontendQt) checkInternet() { func (f *FrontendQt) checkInternet() {

View File

@ -15,7 +15,7 @@
// 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/>.
// +build nogui // +build !build_qt
package qtie package qtie

View File

@ -15,7 +15,7 @@
// 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/>.
// +build !nogui // +build build_qt
package qtie package qtie
@ -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

View File

@ -16,7 +16,7 @@
// 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/>.
// +build !nogui // +build build_qt
package qtie package qtie

View File

@ -15,7 +15,7 @@
// 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/>.
// +build !nogui // +build build_qt
package qtie package qtie

View File

@ -15,7 +15,7 @@
// 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/>.
// +build !nogui // +build build_qt
package qtie package qtie

View File

@ -15,7 +15,7 @@
// 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/>.
// +build !nogui // +build build_qt
package qtie package qtie

View File

@ -15,7 +15,7 @@
// 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/>.
// +build !nogui // +build build_qt
package qtie package qtie
@ -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"`
@ -53,6 +53,7 @@ type GoQMLInterface struct {
_ string `property:"fullversion"` _ string `property:"fullversion"`
_ string `property:"downloadLink"` _ string `property:"downloadLink"`
_ string `property:"updateState"`
_ string `property:"updateVersion"` _ string `property:"updateVersion"`
_ bool `property:"updateCanInstall"` _ bool `property:"updateCanInstall"`
_ string `property:"updateLandingPage"` _ string `property:"updateLandingPage"`
@ -61,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"`
@ -76,9 +77,8 @@ 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(updateState string) `signal:"setUpdateState"` _ func() `slot:"checkInternet"`
_ func() `slot:"checkInternet"`
_ func() `slot:"setToRestart"` _ func() `slot:"setToRestart"`
@ -93,7 +93,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 +108,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 +162,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)
@ -207,4 +207,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)
} }

View File

@ -15,7 +15,7 @@
// 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/>.
// +build !nogui // +build build_qt
package qt package qt

View File

@ -15,7 +15,7 @@
// 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/>.
// +build !nogui // +build build_qt
package qt package qt
@ -84,7 +84,7 @@ func (s *FrontendQt) clearCache() {
channel := s.bridge.GetUpdateChannel() channel := s.bridge.GetUpdateChannel()
if channel == updater.EarlyChannel { if channel == updater.EarlyChannel {
if err := s.bridge.SetUpdateChannel(updater.StableChannel); err != nil { if _, err := s.bridge.SetUpdateChannel(updater.StableChannel); err != nil {
s.Qml.NotifyManualUpdateError() s.Qml.NotifyManualUpdateError()
return return
} }

View File

@ -15,7 +15,7 @@
// 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/>.
// +build !nogui // +build build_qt
// Package qt is the Qt User interface for Desktop bridge. // Package qt is the Qt User interface for Desktop bridge.
// //
@ -39,16 +39,17 @@ import (
"github.com/ProtonMail/go-autostart" "github.com/ProtonMail/go-autostart"
"github.com/ProtonMail/proton-bridge/internal/bridge" "github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/config/settings" "github.com/ProtonMail/proton-bridge/internal/config/settings"
"github.com/ProtonMail/proton-bridge/internal/config/useragent"
"github.com/ProtonMail/proton-bridge/internal/events" "github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/frontend/autoconfig" "github.com/ProtonMail/proton-bridge/internal/frontend/autoconfig"
qtcommon "github.com/ProtonMail/proton-bridge/internal/frontend/qt-common" qtcommon "github.com/ProtonMail/proton-bridge/internal/frontend/qt-common"
"github.com/ProtonMail/proton-bridge/internal/frontend/types" "github.com/ProtonMail/proton-bridge/internal/frontend/types"
"github.com/ProtonMail/proton-bridge/internal/locations" "github.com/ProtonMail/proton-bridge/internal/locations"
"github.com/ProtonMail/proton-bridge/internal/updater" "github.com/ProtonMail/proton-bridge/internal/updater"
"github.com/ProtonMail/proton-bridge/pkg/keychain"
"github.com/ProtonMail/proton-bridge/pkg/listener" "github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/ProtonMail/proton-bridge/pkg/ports" "github.com/ProtonMail/proton-bridge/pkg/ports"
"github.com/ProtonMail/proton-bridge/pkg/useragent"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/skratchdot/open-golang/open" "github.com/skratchdot/open-golang/open"
"github.com/therecipe/qt/core" "github.com/therecipe/qt/core"
@ -74,6 +75,7 @@ type FrontendQt struct {
settings *settings.Settings settings *settings.Settings
eventListener listener.Listener eventListener listener.Listener
updater types.Updater updater types.Updater
userAgent *useragent.UserAgent
bridge types.Bridger bridge types.Bridger
noEncConfirmator types.NoEncConfirmator noEncConfirmator types.NoEncConfirmator
@ -113,12 +115,15 @@ func New(
settings *settings.Settings, settings *settings.Settings,
eventListener listener.Listener, eventListener listener.Listener,
updater types.Updater, updater types.Updater,
userAgent *useragent.UserAgent,
bridge types.Bridger, bridge types.Bridger,
noEncConfirmator types.NoEncConfirmator, noEncConfirmator types.NoEncConfirmator,
autostart *autostart.App, autostart *autostart.App,
restarter types.Restarter, restarter types.Restarter,
) *FrontendQt { ) *FrontendQt {
tmp := &FrontendQt{ userAgent.SetPlatform(core.QSysInfo_PrettyProductName())
f := &FrontendQt{
version: version, version: version,
buildVersion: buildVersion, buildVersion: buildVersion,
programName: programName, programName: programName,
@ -128,6 +133,7 @@ func New(
settings: settings, settings: settings,
eventListener: eventListener, eventListener: eventListener,
updater: updater, updater: updater,
userAgent: userAgent,
bridge: bridge, bridge: bridge,
noEncConfirmator: noEncConfirmator, noEncConfirmator: noEncConfirmator,
programVer: "v" + version, programVer: "v" + version,
@ -137,13 +143,9 @@ func New(
// Initializing.Done is only called sync.Once. Please keep the increment // Initializing.Done is only called sync.Once. Please keep the increment
// set to 1 // set to 1
tmp.initializing.Add(1) f.initializing.Add(1)
// Nicer string for OS. return f
currentOS := core.QSysInfo_PrettyProductName()
bridge.SetCurrentOS(currentOS)
return tmp
} }
// InstanceExistAlert is a global warning window indicating an instance already exists. // InstanceExistAlert is a global warning window indicating an instance already exists.
@ -338,39 +340,46 @@ func (s *FrontendQt) qtExecute(Procedure func(*FrontendQt) error) error {
s.Qml.SetCredits(bridge.Credits) s.Qml.SetCredits(bridge.Credits)
s.Qml.SetFullversion(s.buildVersion) s.Qml.SetFullversion(s.buildVersion)
// Autostart. // Autostart: rewrite the current definition of autostart
if s.Qml.IsFirstStart() { // - when it is the first time
if s.autostart.IsEnabled() { // - when starting after clear cache
// - when there is already autostart file from past
//
// This will make sure that autostart will use the latest path to
// launcher or bridge.
isAutoStartEnabled := s.autostart.IsEnabled()
if s.Qml.IsFirstStart() || isAutoStartEnabled {
if isAutoStartEnabled {
if err := s.autostart.Disable(); err != nil { if err := s.autostart.Disable(); err != nil {
log.Error("First disable ", err) log.
WithField("first", s.Qml.IsFirstStart()).
WithField("wasEnabled", isAutoStartEnabled).
WithError(err).
Error("Disable on start failed.")
s.autostartError(err) s.autostartError(err)
} }
} }
s.toggleAutoStart() if err := s.autostart.Enable(); err != nil {
} log.
if s.autostart.IsEnabled() { WithField("first", s.Qml.IsFirstStart()).
s.Qml.SetIsAutoStart(true) WithField("wasEnabled", isAutoStartEnabled).
} else { WithError(err).
s.Qml.SetIsAutoStart(false) Error("Enable on start failed.")
s.autostartError(err)
}
} }
s.Qml.SetIsAutoStart(s.autostart.IsEnabled())
if s.settings.GetBool(settings.AutoUpdateKey) { s.Qml.SetIsAutoUpdate(s.settings.GetBool(settings.AutoUpdateKey))
s.Qml.SetIsAutoUpdate(true) s.Qml.SetIsProxyAllowed(s.settings.GetBool(settings.AllowProxyKey))
} else { s.Qml.SetIsEarlyAccess(updater.UpdateChannel(s.settings.Get(settings.UpdateChannelKey)) == updater.EarlyChannel)
s.Qml.SetIsAutoUpdate(false)
}
if s.settings.GetBool(settings.AllowProxyKey) { availableKeychain := []string{}
s.Qml.SetIsProxyAllowed(true) for chain := range keychain.Helpers {
} else { availableKeychain = append(availableKeychain, chain)
s.Qml.SetIsProxyAllowed(false)
}
if updater.UpdateChannel(s.settings.Get(settings.UpdateChannelKey)) == updater.EarlyChannel {
s.Qml.SetIsEarlyAccess(true)
} else {
s.Qml.SetIsEarlyAccess(false)
} }
s.Qml.SetAvailableKeychain(availableKeychain)
s.Qml.SetSelectedKeychain(s.settings.Get(settings.PreferredKeychainKey))
// Set reporting of outgoing email without encryption. // Set reporting of outgoing email without encryption.
s.Qml.SetIsReportingOutgoingNoEnc(s.settings.GetBool(settings.ReportOutgoingNoEncKey)) s.Qml.SetIsReportingOutgoingNoEnc(s.settings.GetBool(settings.ReportOutgoingNoEncKey))
@ -497,7 +506,7 @@ func (s *FrontendQt) sendBug(description, client, address string) (isOK bool) {
} }
func (s *FrontendQt) getLastMailClient() string { func (s *FrontendQt) getLastMailClient() string {
return s.bridge.GetCurrentClient() return s.userAgent.String()
} }
func (s *FrontendQt) configureAppleMail(iAccount, iAddress int) { func (s *FrontendQt) configureAppleMail(iAccount, iAddress int) {
@ -547,20 +556,22 @@ func (s *FrontendQt) configureAppleMail(iAccount, iAddress int) {
func (s *FrontendQt) toggleAutoStart() { func (s *FrontendQt) toggleAutoStart() {
defer s.Qml.ProcessFinished() defer s.Qml.ProcessFinished()
var err error var err error
if s.autostart.IsEnabled() { wasEnabled := s.autostart.IsEnabled()
if wasEnabled {
err = s.autostart.Disable() err = s.autostart.Disable()
} else { } else {
err = s.autostart.Enable() err = s.autostart.Enable()
} }
isEnabled := s.autostart.IsEnabled()
if err != nil { if err != nil {
log.Error("Enable autostart: ", err) log.
WithField("wasEnabled", wasEnabled).
WithField("isEnabled", isEnabled).
WithError(err).
Error("Autostart change failed.")
s.autostartError(err) s.autostartError(err)
} }
if s.autostart.IsEnabled() { s.Qml.SetIsAutoStart(isEnabled)
s.Qml.SetIsAutoStart(true)
} else {
s.Qml.SetIsAutoStart(false)
}
} }
func (s *FrontendQt) toggleAutoUpdate() { func (s *FrontendQt) toggleAutoUpdate() {
@ -585,14 +596,16 @@ func (s *FrontendQt) toggleEarlyAccess() {
channel = updater.EarlyChannel channel = updater.EarlyChannel
} }
err := s.bridge.SetUpdateChannel(channel) needRestart, err := s.bridge.SetUpdateChannel(channel)
s.Qml.SetIsEarlyAccess(channel == updater.EarlyChannel) s.Qml.SetIsEarlyAccess(channel == updater.EarlyChannel)
if err != nil { if err != nil {
s.Qml.NotifyManualUpdateError() s.Qml.NotifyManualUpdateError()
return return
} }
s.restarter.SetToRestart() if needRestart {
s.App.Quit() s.restarter.SetToRestart()
s.App.Quit()
}
} }
func (s *FrontendQt) toggleAllowProxy() { func (s *FrontendQt) toggleAllowProxy() {
@ -711,3 +724,16 @@ func (s *FrontendQt) setGUIIsReady() {
s.initializing.Done() s.initializing.Done()
}) })
} }
func (s *FrontendQt) getKeychain() string {
return s.bridge.GetKeychainApp()
}
func (s *FrontendQt) setKeychain(keychain string) {
if keychain != s.bridge.GetKeychainApp() {
s.bridge.SetKeychainApp(keychain)
s.restarter.SetToRestart()
s.App.Quit()
}
}

View File

@ -15,7 +15,7 @@
// 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/>.
// +build nogui // +build !build_qt
package qt package qt
@ -25,6 +25,7 @@ import (
"github.com/ProtonMail/go-autostart" "github.com/ProtonMail/go-autostart"
"github.com/ProtonMail/proton-bridge/internal/config/settings" "github.com/ProtonMail/proton-bridge/internal/config/settings"
"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/internal/locations" "github.com/ProtonMail/proton-bridge/internal/locations"
"github.com/ProtonMail/proton-bridge/internal/updater" "github.com/ProtonMail/proton-bridge/internal/updater"
@ -71,6 +72,7 @@ func New(
settings *settings.Settings, settings *settings.Settings,
eventListener listener.Listener, eventListener listener.Listener,
updater types.Updater, updater types.Updater,
userAgent *useragent.UserAgent,
bridge types.Bridger, bridge types.Bridger,
noEncConfirmator types.NoEncConfirmator, noEncConfirmator types.NoEncConfirmator,
autostart *autostart.App, autostart *autostart.App,

View File

@ -15,7 +15,7 @@
// 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/>.
// +build !nogui // +build build_qt
package qt package qt

View File

@ -15,7 +15,7 @@
// 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/>.
// +build !nogui // +build build_qt
package qt package qt

View File

@ -15,7 +15,7 @@
// 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/>.
// +build !nogui // +build build_qt
package qt package qt

View File

@ -15,7 +15,7 @@
// 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/>.
// +build !nogui // +build build_qt
package qt package qt
@ -50,6 +50,7 @@ type GoQMLInterface struct {
_ string `property:"fullversion"` _ string `property:"fullversion"`
_ string `property:"downloadLink"` _ string `property:"downloadLink"`
_ string `property:"updateState"`
_ string `property:"updateVersion"` _ string `property:"updateVersion"`
_ bool `property:"updateCanInstall"` _ bool `property:"updateCanInstall"`
_ string `property:"updateLandingPage"` _ string `property:"updateLandingPage"`
@ -66,6 +67,9 @@ type GoQMLInterface struct {
_ func() `slot:"startManualUpdate"` _ func() `slot:"startManualUpdate"`
_ func() `slot:"guiIsReady"` _ func() `slot:"guiIsReady"`
_ []string `property:"availableKeychain"`
_ string `property:"selectedKeychain"`
// Translations. // Translations.
_ string `property:"wrongCredentials"` _ string `property:"wrongCredentials"`
_ string `property:"wrongMailboxPassword"` _ string `property:"wrongMailboxPassword"`
@ -79,9 +83,8 @@ type GoQMLInterface struct {
_ float32 `property:"progress"` _ float32 `property:"progress"`
_ string `property:"progressDescription"` _ string `property:"progressDescription"`
_ func(isAvailable bool) `signal:"setConnectionStatus"` _ func(isAvailable bool) `signal:"setConnectionStatus"`
_ func(updateState string) `signal:"setUpdateState"` _ func() `slot:"checkInternet"`
_ func() `slot:"checkInternet"`
_ func() `slot:"setToRestart"` _ func() `slot:"setToRestart"`
@ -209,4 +212,7 @@ func (s *GoQMLInterface) SetFrontend(f *FrontendQt) {
s.ConnectToggleIsReportingOutgoingNoEnc(f.toggleIsReportingOutgoingNoEnc) s.ConnectToggleIsReportingOutgoingNoEnc(f.toggleIsReportingOutgoingNoEnc)
s.ConnectShouldSendAnswer(f.shouldSendAnswer) s.ConnectShouldSendAnswer(f.shouldSendAnswer)
s.ConnectSaveOutgoingNoEncPopupCoord(f.saveOutgoingNoEncPopupCoord) s.ConnectSaveOutgoingNoEncPopupCoord(f.saveOutgoingNoEncPopupCoord)
s.ConnectSetSelectedKeychain(f.setKeychain)
s.ConnectSelectedKeychain(f.getKeychain)
} }

View File

@ -61,6 +61,7 @@
<file alias="BubbleMenu.qml" >./qml/BridgeUI/BubbleMenu.qml</file> <file alias="BubbleMenu.qml" >./qml/BridgeUI/BubbleMenu.qml</file>
<file alias="Credits.qml" >./qml/BridgeUI/Credits.qml</file> <file alias="Credits.qml" >./qml/BridgeUI/Credits.qml</file>
<file alias="DialogFirstStart.qml" >./qml/BridgeUI/DialogFirstStart.qml</file> <file alias="DialogFirstStart.qml" >./qml/BridgeUI/DialogFirstStart.qml</file>
<file alias="DialogKeychainChange.qml" >./qml/BridgeUI/DialogKeychainChange.qml</file>
<file alias="DialogPortChange.qml" >./qml/BridgeUI/DialogPortChange.qml</file> <file alias="DialogPortChange.qml" >./qml/BridgeUI/DialogPortChange.qml</file>
<file alias="DialogYesNo.qml" >./qml/BridgeUI/DialogYesNo.qml</file> <file alias="DialogYesNo.qml" >./qml/BridgeUI/DialogYesNo.qml</file>
<file alias="DialogTLSCertInfo.qml" >./qml/BridgeUI/DialogTLSCertInfo.qml</file> <file alias="DialogTLSCertInfo.qml" >./qml/BridgeUI/DialogTLSCertInfo.qml</file>

View File

@ -1 +0,0 @@
IDI_ICON1 ICON DISCARDABLE "logo.ico"

View 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

View File

@ -75,13 +75,13 @@ type User interface {
type Bridger interface { type Bridger interface {
UserManager UserManager
GetCurrentClient() string
SetCurrentOS(os string)
ReportBug(osType, osVersion, description, accountName, address, emailClient string) error ReportBug(osType, osVersion, description, accountName, address, emailClient string) error
AllowProxy() AllowProxy()
DisallowProxy() DisallowProxy()
GetUpdateChannel() updater.UpdateChannel GetUpdateChannel() updater.UpdateChannel
SetUpdateChannel(updater.UpdateChannel) error SetUpdateChannel(updater.UpdateChannel) (needRestart bool, err error)
GetKeychainApp() string
SetKeychainApp(keychain string)
} }
type bridgeWrap struct { type bridgeWrap struct {
@ -114,10 +114,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
} }

View File

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

View File

@ -29,7 +29,6 @@ type cacheProvider interface {
} }
type bridger interface { type bridger interface {
SetCurrentClient(clientName, clientVersion string)
GetUser(query string) (bridgeUser, error) GetUser(query string) (bridgeUser, error)
} }
@ -62,7 +61,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 {
@ -78,5 +77,5 @@ func (u *bridgeUserWrap) GetStore() storeUserProvider {
if store == nil { if store == nil {
return nil return nil
} }
return newStoreUserWrap(store) return newStoreUserWrap(store) //nolint[typecheck] missing methods are inherited
} }

View File

@ -23,13 +23,13 @@ import (
) )
type currentClientSetter interface { type currentClientSetter interface {
SetCurrentClient(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
setter currentClientSetter clientSetter currentClientSetter
} }
func (ext *extension) Capabilities(conn imapserver.Conn) []string { func (ext *extension) Capabilities(conn imapserver.Conn) []string {
@ -44,8 +44,8 @@ func (ext *extension) Command(name string) imapserver.HandlerFactory {
return func() imapserver.Handler { return func() imapserver.Handler {
if hdlrID, ok := newIDHandler().(*imapid.Handler); ok { if hdlrID, ok := newIDHandler().(*imapid.Handler); ok {
return &handler{ return &handler{
hdlrID: hdlrID, hdlrID: hdlrID,
setter: ext.setter, clientSetter: ext.clientSetter,
} }
} }
return nil return nil
@ -57,8 +57,8 @@ func (ext *extension) NewConn(conn imapserver.Conn) imapserver.Conn {
} }
type handler struct { type handler struct {
hdlrID *imapid.Handler hdlrID *imapid.Handler
setter currentClientSetter clientSetter currentClientSetter
} }
func (hdlr *handler) Parse(fields []interface{}) error { func (hdlr *handler) Parse(fields []interface{}) error {
@ -69,21 +69,18 @@ func (hdlr *handler) Handle(conn imapserver.Conn) error {
err := hdlr.hdlrID.Handle(conn) err := hdlr.hdlrID.Handle(conn)
if err == nil { if err == nil {
id := hdlr.hdlrID.Command.ID id := hdlr.hdlrID.Command.ID
hdlr.setter.SetCurrentClient( hdlr.clientSetter.SetClient(id[imapid.FieldName], id[imapid.FieldVersion])
id[imapid.FieldName],
id[imapid.FieldVersion],
)
} }
return err return err
} }
// NewExtension returns extension which is adding RFC2871 ID capability, with // NewExtension returns extension which is adding RFC2871 ID capability, with
// direct interface to set information about email client to backend. // direct interface to set information about email client to backend.
func NewExtension(serverID imapid.ID, setter currentClientSetter) imapserver.Extension { func NewExtension(serverID imapid.ID, clientSetter currentClientSetter) imapserver.Extension {
if conExtID, ok := imapid.NewExtension(serverID).(imapserver.ConnExtension); ok { if conExtID, ok := imapid.NewExtension(serverID).(imapserver.ConnExtension); ok {
return &extension{ return &extension{
extID: conExtID, extID: conExtID,
setter: setter, clientSetter: clientSetter,
} }
} }
return nil return nil

View File

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

View File

@ -19,6 +19,7 @@ package imap
import ( import (
"strings" "strings"
"time"
"github.com/ProtonMail/proton-bridge/pkg/message" "github.com/ProtonMail/proton-bridge/pkg/message"
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
@ -36,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,
@ -53,9 +56,30 @@ 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,
} }
} }
// logCommand is helper to log commands requested by IMAP client with their
// params, result, and duration, but without private data.
// It's logged as INFO so it's logged for every user by default. This should
// help devs to find out reasons why clients, mostly Apple Mail, does re-sync.
// FETCH, APPEND, STORE, COPY, MOVE, and EXPUNGE should be using this helper.
func (im *imapMailbox) logCommand(callback func() error, cmd string, params ...interface{}) error {
start := time.Now()
err := callback()
// Not using im.log to not include addressID which is not needed in this case.
log.WithFields(logrus.Fields{
"userID": im.storeUser.UserID(),
"labelID": im.storeMailbox.LabelID(),
"duration": time.Since(start),
"err": err,
"params": params,
}).Info(cmd)
return err
}
// Name returns this mailbox name. // Name returns this mailbox name.
func (im *imapMailbox) Name() string { func (im *imapMailbox) Name() string {
// Called from go-imap in goroutines - we need to handle panics for each function. // Called from go-imap in goroutines - we need to handle panics for each function.
@ -177,17 +201,16 @@ func (im *imapMailbox) Check() error {
// Expunge permanently removes all messages that have the \Deleted flag set // Expunge permanently removes all messages that have the \Deleted flag set
// from the currently selected mailbox. // from the currently selected mailbox.
func (im *imapMailbox) Expunge() error { func (im *imapMailbox) Expunge() error {
// Wait for any APPENDS to finish in order to avoid data loss when // See comment of appendExpungeLock.
// Outlook sends commands too quickly STORE \Deleted, APPEND, EXPUNGE, if im.storeMailbox.LabelID() == pmapi.TrashLabel || im.storeMailbox.LabelID() == pmapi.SpamLabel {
// APPEND FINISHED: im.user.appendExpungeLock.Lock()
// defer im.user.appendExpungeLock.Unlock()
// Based on Outlook APPEND request we will not create new message but }
// move the original to desired mailbox. If the message is currently
// in Trash or Spam and EXPUNGE happens before APPEND processing is
// finished the message is deleted from Proton instead of moved to
// the desired mailbox.
im.user.waitForAppend()
return im.logCommand(im.expunge, "EXPUNGE")
}
func (im *imapMailbox) expunge() error {
im.user.backend.updates.block(im.user.currentAddressLowercase, im.name, operationDeleteMessage) im.user.backend.updates.block(im.user.currentAddressLowercase, im.name, operationDeleteMessage)
defer im.user.backend.updates.unblock(im.user.currentAddressLowercase, im.name, operationDeleteMessage) defer im.user.backend.updates.unblock(im.user.currentAddressLowercase, im.name, operationDeleteMessage)
@ -197,6 +220,18 @@ func (im *imapMailbox) Expunge() error {
// UIDExpunge permanently removes messages that have the \Deleted flag set // UIDExpunge permanently removes messages that have the \Deleted flag set
// and UID passed from SeqSet from the currently selected mailbox. // and UID passed from SeqSet from the currently selected mailbox.
func (im *imapMailbox) UIDExpunge(seqSet *imap.SeqSet) error { func (im *imapMailbox) UIDExpunge(seqSet *imap.SeqSet) error {
return im.logCommand(func() error {
return im.uidExpunge(seqSet)
}, "UID EXPUNGE", seqSet)
}
func (im *imapMailbox) uidExpunge(seqSet *imap.SeqSet) error {
// See comment of appendExpungeLock.
if im.storeMailbox.LabelID() == pmapi.TrashLabel || im.storeMailbox.LabelID() == pmapi.SpamLabel {
im.user.appendExpungeLock.Lock()
defer im.user.appendExpungeLock.Unlock()
}
im.user.backend.updates.block(im.user.currentAddressLowercase, im.name, operationDeleteMessage) im.user.backend.updates.block(im.user.currentAddressLowercase, im.name, operationDeleteMessage)
defer im.user.backend.updates.unblock(im.user.currentAddressLowercase, im.name, operationDeleteMessage) defer im.user.backend.updates.unblock(im.user.currentAddressLowercase, im.name, operationDeleteMessage)

View File

@ -19,9 +19,9 @@ package imap
import ( import (
"bytes" "bytes"
"context"
"fmt" "fmt"
"io" "io"
"mime/multipart"
"net/mail" "net/mail"
"net/textproto" "net/textproto"
"sort" "sort"
@ -32,12 +32,10 @@ import (
"github.com/ProtonMail/proton-bridge/internal/imap/cache" "github.com/ProtonMail/proton-bridge/internal/imap/cache"
"github.com/ProtonMail/proton-bridge/internal/imap/uidplus" "github.com/ProtonMail/proton-bridge/internal/imap/uidplus"
"github.com/ProtonMail/proton-bridge/pkg/message" "github.com/ProtonMail/proton-bridge/pkg/message"
"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/hashicorp/go-multierror" "github.com/hashicorp/go-multierror"
"github.com/pkg/errors" "github.com/pkg/errors"
openpgperrors "golang.org/x/crypto/openpgp/errors"
) )
var ( var (
@ -66,13 +64,16 @@ func (dnc *doNotCacheError) errorOrNil() error {
// //
// If the Backend implements Updater, it must notify the client immediately // If the Backend implements Updater, it must notify the client immediately
// via a mailbox update. // via a mailbox update.
func (im *imapMailbox) CreateMessage(flags []string, date time.Time, body imap.Literal) error { // nolint[funlen] 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. // Called from go-imap in goroutines - we need to handle panics for each function.
defer im.panicHandler.HandlePanic() defer im.panicHandler.HandlePanic()
im.user.appendStarted()
defer im.user.appendFinished()
m, _, _, readers, err := message.Parse(body) m, _, _, readers, err := message.Parse(body)
if err != nil { if err != nil {
return err return err
@ -154,6 +155,9 @@ func (im *imapMailbox) CreateMessage(flags []string, date time.Time, body imap.L
} }
} }
im.user.appendExpungeLock.Lock()
defer im.user.appendExpungeLock.Unlock()
// Avoid appending a message which is already on the server. Apply the // Avoid appending a message which is already on the server. Apply the
// new label instead. This always happens with Outlook (it uses APPEND // new label instead. This always happens with Outlook (it uses APPEND
// instead of COPY). // instead of COPY).
@ -175,7 +179,7 @@ func (im *imapMailbox) CreateMessage(flags []string, date time.Time, body imap.L
return err return err
} }
targetSeq := im.storeMailbox.GetUIDList([]string{m.ID}) targetSeq := im.storeMailbox.GetUIDList(IDs)
return uidplus.AppendResponse(im.storeMailbox.UIDValidity(), targetSeq) return uidplus.AppendResponse(im.storeMailbox.UIDValidity(), targetSeq)
} }
} }
@ -220,8 +224,9 @@ func (im *imapMailbox) importMessage(m *pmapi.Message, readers []io.Reader, kr *
return im.storeMailbox.ImportMessage(m, body, labels) return im.storeMailbox.ImportMessage(m, body, labels)
} }
func (im *imapMailbox) getMessage(storeMessage storeMessageProvider, items []imap.FetchItem) (msg *imap.Message, err error) { func (im *imapMailbox) getMessage(storeMessage storeMessageProvider, items []imap.FetchItem, msgBuildCountHistogram *msgBuildCountHistogram) (msg *imap.Message, err error) { //nolint[funlen]
im.log.WithField("msgID", storeMessage.ID()).Trace("Getting message") msglog := im.log.WithField("msgID", storeMessage.ID())
msglog.Trace("Getting message")
seqNum, err := storeMessage.SequenceNumber() seqNum, err := storeMessage.SequenceNumber()
if err != nil { if err != nil {
@ -234,7 +239,9 @@ func (im *imapMailbox) getMessage(storeMessage storeMessageProvider, items []ima
for _, item := range items { for _, item := range items {
switch item { switch item {
case imap.FetchEnvelope: case imap.FetchEnvelope:
msg.Envelope = message.GetEnvelope(m) // No need to check IsFullHeaderCached here. API header
// contain enough information to build the envelope.
msg.Envelope = message.GetEnvelope(m, storeMessage.GetHeader())
case imap.FetchBody, imap.FetchBodyStructure: case imap.FetchBody, imap.FetchBodyStructure:
var structure *message.BodyStructure var structure *message.BodyStructure
structure, err = im.getBodyStructure(storeMessage) structure, err = im.getBodyStructure(storeMessage)
@ -261,8 +268,13 @@ func (im *imapMailbox) getMessage(storeMessage storeMessageProvider, items []ima
// Size attribute on the server counts encrypted data. The value is cleared // 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. // on our part and we need to compute "real" size of decrypted data.
if m.Size <= 0 { if m.Size <= 0 {
im.log.WithField("msgID", storeMessage.ID()).Trace("Size unknown - downloading body") msglog.Debug("Size unknown - downloading body")
if _, _, err = im.getBodyAndStructure(storeMessage); err != nil { // 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 return
} }
} }
@ -272,8 +284,10 @@ func (im *imapMailbox) getMessage(storeMessage storeMessageProvider, items []ima
if err != nil { if err != nil {
return nil, err 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: default:
if err = im.getLiteralForSection(item, msg, storeMessage); err != nil { if err = im.getLiteralForSection(item, msg, storeMessage, msgBuildCountHistogram); err != nil {
return return
} }
} }
@ -282,14 +296,15 @@ func (im *imapMailbox) getMessage(storeMessage storeMessageProvider, items []ima
return msg, err return msg, err
} }
func (im *imapMailbox) getLiteralForSection(itemSection imap.FetchItem, msg *imap.Message, storeMessage storeMessageProvider) error { func (im *imapMailbox) getLiteralForSection(itemSection imap.FetchItem, msg *imap.Message, storeMessage storeMessageProvider, msgBuildCountHistogram *msgBuildCountHistogram) error {
section, err := imap.ParseBodySectionName(itemSection) section, err := imap.ParseBodySectionName(itemSection)
if err != nil { // Ignore error if err != nil {
return 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 var literal imap.Literal
if literal, err = im.getMessageBodySection(storeMessage, section); err != nil { if literal, err = im.getMessageBodySection(storeMessage, section, msgBuildCountHistogram); err != nil {
return err return err
} }
@ -307,14 +322,20 @@ func (im *imapMailbox) getBodyStructure(storeMessage storeMessageProvider) (bs *
im.log.WithError(err).Debug("Fail to retrieve bodystructure from database") im.log.WithError(err).Debug("Fail to retrieve bodystructure from database")
} }
if bs == nil { if bs == nil {
if bs, _, err = im.getBodyAndStructure(storeMessage); err != 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
} }
} }
return return
} }
func (im *imapMailbox) getBodyAndStructure(storeMessage storeMessageProvider) ( //nolint[funlen] Jakub will fix in refactor
func (im *imapMailbox) getBodyAndStructure(storeMessage storeMessageProvider, msgBuildCountHistogram *msgBuildCountHistogram) (
structure *message.BodyStructure, structure *message.BodyStructure,
bodyReader *bytes.Reader, err error, bodyReader *bytes.Reader, err error,
) { ) {
@ -341,10 +362,26 @@ func (im *imapMailbox) getBodyAndStructure(storeMessage storeMessageProvider) (
} }
} }
if err == nil && structure != nil && len(body) > 0 { if err == nil && structure != nil && len(body) > 0 {
if err := storeMessage.SetContentTypeAndHeader(m.MIMEType, m.Header); err != nil { header, errHead := structure.GetMailHeaderBytes(bytes.NewReader(body))
im.log.WithError(err). if errHead == nil {
if errHead := storeMessage.SetHeader(header); errHead != nil {
im.log.WithError(errHead).
WithField("msgID", m.ID).
Warn("Cannot update header after building")
}
} else {
im.log.WithError(errHead).
WithField("msgID", m.ID). WithField("msgID", m.ID).
Warn("Cannot update header while building") Warn("Cannot get header bytes after building")
}
if msgBuildCountHistogram != nil {
times, err := storeMessage.IncreaseBuildCount()
if err != nil {
im.log.WithError(err).
WithField("msgID", m.ID).
Warn("Cannot increase build count")
}
msgBuildCountHistogram.add(times)
} }
// Drafts can change and we don't want to cache them. // Drafts can change and we don't want to cache them.
if !isMessageInDraftFolder(m) { if !isMessageInDraftFolder(m) {
@ -373,40 +410,32 @@ func isMessageInDraftFolder(m *pmapi.Message) bool {
// This will download message (or read from cache) and pick up the section, // This will download message (or read from cache) and pick up the section,
// extract data (header,body, both) and trim the output if needed. // 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] func (im *imapMailbox) getMessageBodySection(
var ( storeMessage storeMessageProvider,
structure *message.BodyStructure section *imap.BodySectionName,
bodyReader *bytes.Reader msgBuildCountHistogram *msgBuildCountHistogram,
header textproto.MIMEHeader ) (imap.Literal, error) {
response []byte var header textproto.MIMEHeader
) var response []byte
im.log.WithField("msgID", storeMessage.ID()).Trace("Getting message body") im.log.WithField("msgID", storeMessage.ID()).Trace("Getting message body")
m := storeMessage.Message() isMainHeaderRequested := len(section.Path) == 0 && section.Specifier == imap.HeaderSpecifier
if isMainHeaderRequested && storeMessage.IsFullHeaderCached() {
if len(section.Path) == 0 && section.Specifier == imap.HeaderSpecifier { // In order to speed up (avoid download and decryptions) we
// We can extract message header without decrypting. // cache the header. If a mail header was requested and DB
header = message.GetHeader(m) // contains full header (it means it was already built once)
// We need to ensure we use the correct content-type, // the DB header can be used without downloading and decrypting.
// otherwise AppleMail expects `text/plain` in HTML mails. // Otherwise header is incomplete and clients would have issues
if header.Get("Content-Type") == "" { // e.g. AppleMail expects `text/plain` in HTML mails.
if err = im.fetchMessage(m); err != nil { header = storeMessage.GetHeader()
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 { } else {
// The rest of cases need download and decrypt. // For all other cases it is necessary to download and decrypt the message
structure, bodyReader, err = im.getBodyAndStructure(storeMessage) // and drop the header which was obtained from cache. The header will
// will be stored in DB once successfully built. Check `getBodyAndStructure`.
structure, bodyReader, err := im.getBodyAndStructure(storeMessage, msgBuildCountHistogram)
if err != nil { if err != nil {
return return nil, err
} }
switch { switch {
@ -417,368 +446,86 @@ func (im *imapMailbox) getMessageBodySection(storeMessage storeMessageProvider,
// The TEXT specifier refers to the content of the message (or section), omitting the [RFC-2822] header. // 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. // Non-empty section with no specifier (imap.EntireSpecifier) refers to section content without header.
response, err = structure.GetSectionContent(bodyReader, section.Path) response, err = structure.GetSectionContent(bodyReader, section.Path)
case section.Specifier == imap.MIMESpecifier: case section.Specifier == imap.MIMESpecifier: // The MIME part specifier refers to the [MIME-IMB] header for this part.
// The MIME part specifier refers to the [MIME-IMB] header for this part.
fallthrough fallthrough
case section.Specifier == imap.HeaderSpecifier: case section.Specifier == imap.HeaderSpecifier:
header, err = structure.GetSectionHeader(section.Path) header, err = structure.GetSectionHeader(section.Path)
default: default:
err = errors.New("Unknown specifier " + string(section.Specifier)) err = errors.New("Unknown specifier " + string(section.Specifier))
} }
if err != nil {
return nil, err
}
} }
if err != nil {
return
}
// Filter header. Options are: all fields, only selected fields, all fields except selected.
if header != nil { if header != nil {
// remove fields response = filteredHeaderAsBytes(header, section)
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. // Trim any output if requested.
literal = bytes.NewBuffer(section.ExtractPartial(response)) return bytes.NewBuffer(section.ExtractPartial(response)), nil
return literal, nil
} }
func (im *imapMailbox) fetchMessage(m *pmapi.Message) (err error) { // filteredHeaderAsBytes filters the header fields by section fields and it
im.log.Trace("Fetching message") // returns the filtered fields as bytes.
// Options are: all fields, only selected fields, all fields except selected.
complete, err := im.storeMailbox.FetchMessage(m.ID) func filteredHeaderAsBytes(header textproto.MIMEHeader, section *imap.BodySectionName) []byte {
if err != nil { // remove fields
im.log.WithError(err).Error("Could not get message from store") if len(section.Fields) != 0 && section.NotFields {
return for _, field := range section.Fields {
} header.Del(field)
*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) fields := make([]string, 0, len(header))
if err != nil { if len(section.Fields) == 0 || section.NotFields { // add all and sort
return errors.Wrap(err, "failed to get keyring for address ID") for f := range header {
} fields = append(fields, f)
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) sort.Strings(fields)
err = nil } else { // add only requested (in requested order)
} for _, f := range section.Fields {
fields = append(fields, textproto.CanonicalMIMEHeaderKey(f))
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) headerBuf := &bytes.Buffer{}
if p, err = related.CreatePart(h); err != nil { for _, canonical := range fields {
return if values, ok := header[canonical]; !ok {
continue
} else {
for _, val := range values {
fmt.Fprintf(headerBuf, "%s: %s\r\n", canonical, val)
}
} }
_, _ = buf.WriteTo(p)
} }
return headerBuf.Bytes()
_ = 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. // buildMessage from PM to IMAP.
func (im *imapMailbox) buildMessage(m *pmapi.Message) (structure *message.BodyStructure, msgBody []byte, err error) { func (im *imapMailbox) buildMessage(m *pmapi.Message) (*message.BodyStructure, []byte, error) {
im.log.Trace("Building message") body, err := im.builder.NewJobWithOptions(
context.Background(),
var errNoCache doNotCacheError im.user.client(),
m.ID,
// If fetch or decryption fails we need to change the MIMEType (in customMessage). message.JobOptions{
err = im.fetchMessage(m) 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 { 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 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() structure, err := message.NewBodyStructure(bytes.NewReader(body))
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 { if err != nil {
return return nil, nil, err
} }
tmpBuf := &bytes.Buffer{} return structure, body, nil
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
} }

View File

@ -38,6 +38,12 @@ import (
// If the Backend implements Updater, it must notify the client immediately // If the Backend implements Updater, it must notify the client immediately
// via a message update. // via a message update.
func (im *imapMailbox) UpdateMessagesFlags(uid bool, seqSet *imap.SeqSet, operation imap.FlagsOp, flags []string) error { func (im *imapMailbox) UpdateMessagesFlags(uid bool, seqSet *imap.SeqSet, operation imap.FlagsOp, flags []string) error {
return im.logCommand(func() error {
return im.updateMessagesFlags(uid, seqSet, operation, flags)
}, "STORE", uid, seqSet, operation, flags)
}
func (im *imapMailbox) updateMessagesFlags(uid bool, seqSet *imap.SeqSet, operation imap.FlagsOp, flags []string) error {
log.WithFields(logrus.Fields{ log.WithFields(logrus.Fields{
"flags": flags, "flags": flags,
"operation": operation, "operation": operation,
@ -135,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
@ -146,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
@ -157,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
@ -176,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:
@ -198,6 +204,12 @@ func (im *imapMailbox) addOrRemoveFlags(operation imap.FlagsOp, messageIDs, flag
// destination mailbox. The flags and internal date of the message(s) SHOULD // destination mailbox. The flags and internal date of the message(s) SHOULD
// be preserved, and the Recent flag SHOULD be set, in the copy. // be preserved, and the Recent flag SHOULD be set, in the copy.
func (im *imapMailbox) CopyMessages(uid bool, seqSet *imap.SeqSet, targetLabel string) error { func (im *imapMailbox) CopyMessages(uid bool, seqSet *imap.SeqSet, targetLabel string) error {
return im.logCommand(func() error {
return im.copyMessages(uid, seqSet, targetLabel)
}, "COPY", uid, seqSet, targetLabel)
}
func (im *imapMailbox) copyMessages(uid bool, seqSet *imap.SeqSet, targetLabel string) error {
// Called from go-imap in goroutines - we need to handle panics for each function. // Called from go-imap in goroutines - we need to handle panics for each function.
defer im.panicHandler.HandlePanic() defer im.panicHandler.HandlePanic()
@ -209,6 +221,12 @@ func (im *imapMailbox) CopyMessages(uid bool, seqSet *imap.SeqSet, targetLabel s
// This should not be used until MOVE extension has option to send UIDPLUS // This should not be used until MOVE extension has option to send UIDPLUS
// responses. // responses.
func (im *imapMailbox) MoveMessages(uid bool, seqSet *imap.SeqSet, targetLabel string) error { func (im *imapMailbox) MoveMessages(uid bool, seqSet *imap.SeqSet, targetLabel string) error {
return im.logCommand(func() error {
return im.moveMessages(uid, seqSet, targetLabel)
}, "MOVE", uid, seqSet, targetLabel)
}
func (im *imapMailbox) moveMessages(uid bool, seqSet *imap.SeqSet, targetLabel string) error {
// Called from go-imap in goroutines - we need to handle panics for each function. // Called from go-imap in goroutines - we need to handle panics for each function.
defer im.panicHandler.HandlePanic() defer im.panicHandler.HandlePanic()
@ -340,23 +358,29 @@ 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.GetHeader()
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 {
@ -364,6 +388,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":
@ -463,7 +489,14 @@ func (im *imapMailbox) SearchMessages(isUID bool, criteria *imap.SearchCriteria)
// 3501 section 6.4.5 for a list of items that can be requested. // 3501 section 6.4.5 for a list of items that can be requested.
// //
// 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) (err error) { //nolint[funlen] 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.listMessages(isUID, seqSet, items, msgResponse, msgBuildCountHistogram)
}, "FETCH", isUID, seqSet, items, msgBuildCountHistogram)
}
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 {
@ -511,7 +544,7 @@ func (im *imapMailbox) ListMessages(isUID bool, seqSet *imap.SeqSet, items []ima
} }
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 {
@ -520,7 +553,7 @@ 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)
@ -545,12 +578,12 @@ func (im *imapMailbox) ListMessages(isUID bool, seqSet *imap.SeqSet, items []ima
} }
collectCallback := func(idx int, value interface{}) error { collectCallback := func(idx int, value interface{}) error {
msg := value.(*imap.Message) msg := value.(*imap.Message) //nolint[forcetypeassert] we want to panic here
msgResponse <- msg msgResponse <- msg
return nil return nil
} }
err = parallel.RunParallel(fetchMessagesWorkers, input, processCallback, collectCallback) err = parallel.RunParallel(fetchWorkers, input, processCallback, collectCallback)
if err != nil { if err != nil {
return err return err
} }

View File

@ -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]++
}

View File

@ -28,6 +28,7 @@ import (
imapid "github.com/ProtonMail/go-imap-id" imapid "github.com/ProtonMail/go-imap-id"
"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/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"
@ -39,6 +40,7 @@ import (
imapmove "github.com/emersion/go-imap-move" imapmove "github.com/emersion/go-imap-move"
imapquota "github.com/emersion/go-imap-quota" imapquota "github.com/emersion/go-imap-quota"
imapunselect "github.com/emersion/go-imap-unselect" imapunselect "github.com/emersion/go-imap-unselect"
"github.com/emersion/go-imap/backend"
imapserver "github.com/emersion/go-imap/server" imapserver "github.com/emersion/go-imap/server"
"github.com/emersion/go-sasl" "github.com/emersion/go-sasl"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@ -47,6 +49,7 @@ import (
type imapServer struct { type imapServer struct {
panicHandler panicHandler panicHandler panicHandler
server *imapserver.Server server *imapserver.Server
userAgent *useragent.UserAgent
eventListener listener.Listener eventListener listener.Listener
debugClient bool debugClient bool
debugServer bool debugServer bool
@ -55,7 +58,7 @@ type imapServer struct {
} }
// NewIMAPServer constructs a new IMAP server configured with the given options. // NewIMAPServer constructs a new IMAP server configured with the given options.
func NewIMAPServer(panicHandler panicHandler, debugClient, debugServer bool, port int, tls *tls.Config, imapBackend *imapBackend, eventListener listener.Listener) *imapServer { //nolint[golint] func NewIMAPServer(panicHandler panicHandler, debugClient, debugServer bool, port int, tls *tls.Config, imapBackend backend.Backend, userAgent *useragent.UserAgent, eventListener listener.Listener) *imapServer { // nolint[golint]
s := imapserver.New(imapBackend) s := imapserver.New(imapBackend)
s.Addr = fmt.Sprintf("%v:%v", bridge.Host, port) s.Addr = fmt.Sprintf("%v:%v", bridge.Host, port)
s.TLSConfig = tls s.TLSConfig = tls
@ -93,7 +96,7 @@ func NewIMAPServer(panicHandler panicHandler, debugClient, debugServer bool, por
s.Enable( s.Enable(
imapidle.NewExtension(), imapidle.NewExtension(),
imapmove.NewExtension(), imapmove.NewExtension(),
id.NewExtension(serverID, imapBackend.bridge), id.NewExtension(serverID, userAgent),
imapquota.NewExtension(), imapquota.NewExtension(),
imapappendlimit.NewExtension(), imapappendlimit.NewExtension(),
imapunselect.NewExtension(), imapunselect.NewExtension(),
@ -103,6 +106,7 @@ func NewIMAPServer(panicHandler panicHandler, debugClient, debugServer bool, por
server := &imapServer{ server := &imapServer{
panicHandler: panicHandler, panicHandler: panicHandler,
server: s, server: s,
userAgent: userAgent,
eventListener: eventListener, eventListener: eventListener,
debugClient: debugClient, debugClient: debugClient,
debugServer: debugServer, debugServer: debugServer,
@ -144,9 +148,10 @@ func (s *imapServer) listenAndServe(retries int) {
return return
} }
err = s.server.Serve(&debugListener{ err = s.server.Serve(&connListener{
Listener: l, Listener: l,
server: s, server: s,
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,
@ -233,18 +238,19 @@ func (s *imapServer) monitorDisconnectedUsers() {
} }
} }
// debugListener sets debug loggers on server containing fields with local // connListener sets debug loggers on server containing fields with local
// and remote addresses right after new connection is accepted. // and remote addresses right after new connection is accepted.
type debugListener struct { type connListener struct {
net.Listener net.Listener
server *imapServer server *imapServer
userAgent *useragent.UserAgent
} }
func (dl *debugListener) Accept() (net.Conn, error) { func (l *connListener) Accept() (net.Conn, error) {
conn, err := dl.Listener.Accept() conn, err := l.Listener.Accept()
if err == nil && (dl.server.debugServer || dl.server.debugClient) { if err == nil && (l.server.debugServer || l.server.debugClient) {
debugLog := log debugLog := log
if addr := conn.LocalAddr(); addr != nil { if addr := conn.LocalAddr(); addr != nil {
debugLog = debugLog.WithField("loc", addr.String()) debugLog = debugLog.WithField("loc", addr.String())
@ -254,14 +260,18 @@ func (dl *debugListener) Accept() (net.Conn, error) {
} }
var localDebug, remoteDebug io.Writer var localDebug, remoteDebug io.Writer
if dl.server.debugServer { if l.server.debugServer {
localDebug = debugLog.WithField("pkg", "imap/server").WriterLevel(logrus.DebugLevel) localDebug = debugLog.WithField("pkg", "imap/server").WriterLevel(logrus.DebugLevel)
} }
if dl.server.debugClient { if l.server.debugClient {
remoteDebug = debugLog.WithField("pkg", "imap/client").WriterLevel(logrus.DebugLevel) remoteDebug = debugLog.WithField("pkg", "imap/client").WriterLevel(logrus.DebugLevel)
} }
dl.server.server.Debug = imap.NewDebugWriter(localDebug, remoteDebug) l.server.server.Debug = imap.NewDebugWriter(localDebug, remoteDebug)
}
if !l.userAgent.HasClient() {
l.userAgent.SetClient("UnknownClient", "0.0.1")
} }
return conn, err return conn, err

View File

@ -23,6 +23,7 @@ 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/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/ports" "github.com/ProtonMail/proton-bridge/pkg/ports"
@ -48,6 +49,7 @@ func TestIMAPServerTurnOffAndOnAgain(t *testing.T) {
panicHandler: panicHandler, panicHandler: panicHandler,
server: server, server: server,
eventListener: eventListener, eventListener: eventListener,
userAgent: useragent.New(),
} }
s.isRunning.Store(false) s.isRunning.Store(false)

View File

@ -20,6 +20,7 @@ package imap
import ( import (
"io" "io"
"net/mail" "net/mail"
"net/textproto"
"github.com/ProtonMail/gopenpgp/v2/crypto" "github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/ProtonMail/proton-bridge/internal/imap/uidplus" "github.com/ProtonMail/proton-bridge/internal/imap/uidplus"
@ -100,9 +101,12 @@ type storeMessageProvider interface {
IsMarkedDeleted() bool IsMarkedDeleted() bool
SetSize(int64) error SetSize(int64) error
SetContentTypeAndHeader(string, mail.Header) error SetHeader([]byte) error
GetHeader() 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 +126,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 +140,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 +150,7 @@ func (s *storeAddressWrap) GetMailbox(name string) (storeMailboxProvider, error)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return newStoreMailboxWrap(mailbox), nil return newStoreMailboxWrap(mailbox), nil //nolint[typecheck] missing methods are inherited
} }
type storeMailboxWrap struct { type storeMailboxWrap struct {

View File

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

View File

@ -41,7 +41,22 @@ type imapUser struct {
currentAddressLowercase string currentAddressLowercase string
appendInProcess sync.WaitGroup // Some clients, for example Outlook, do MOVE by STORE \Deleted, APPEND,
// EXPUNGE where APPEN and EXPUNGE can go in parallel. Usual IMAP servers
// do not deduplicate messages and this it's not an issue, but for APPEND
// for PM means just assigning label. That would cause to assign label and
// then delete the message, or in other words cause data loss.
// go-imap does not call CreateMessage till it gets the whole message from
// IMAP client, therefore with big message, simple wait for APPEND before
// performing EXPUNGE is not enough. There has to be two-way lock. Only
// that way even if EXPUNGE is called few ms before APPEND and message
// is deleted, APPEND will not just assing label but creates the message
// again.
// The issue is only when moving message from folder which is causing
// real removal, so Trash and Spam. Those only need to use the lock to
// not cause huge slow down as EXPUNGE is implicitly called also after
// UNSELECT, CLOSE, or LOGOUT.
appendExpungeLock sync.Mutex
} }
// newIMAPUser returns struct implementing go-imap/user interface. // newIMAPUser returns struct implementing go-imap/user interface.
@ -120,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)
} }
@ -152,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.
@ -218,8 +233,9 @@ func (iu *imapUser) GetQuota(name string) (*imapquota.Status, error) {
resources := make(map[string][2]uint32) resources := make(map[string][2]uint32)
var list [2]uint32 var list [2]uint32
list[0] = uint32(usedSpace / 1000) // Quota is "in units of 1024 octets" (or KB) and PM returns bytes.
list[1] = uint32(maxSpace / 1000) list[0] = uint32(usedSpace / 1024)
list[1] = uint32(maxSpace / 1024)
resources[imapquota.ResourceStorage] = list resources[imapquota.ResourceStorage] = list
status := &imapquota.Status{ status := &imapquota.Status{
Name: "", Name: "",
@ -250,15 +266,3 @@ func (iu *imapUser) CreateMessageLimit() *uint32 {
upload := uint32(maxUpload) upload := uint32(maxUpload)
return &upload return &upload
} }
func (iu *imapUser) appendStarted() {
iu.appendInProcess.Add(1)
}
func (iu *imapUser) appendFinished() {
iu.appendInProcess.Done()
}
func (iu *imapUser) waitForAppend() {
iu.appendInProcess.Wait()
}

View File

@ -1,22 +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/>.
// Code generated by ./credits.sh at Mon Feb 1 10:34:22 CET 2021. DO NOT EDIT.
package importexport
const Credits = "github.com/0xAX/notificator;github.com/Masterminds/semver/v3;github.com/ProtonMail/bcrypt;github.com/ProtonMail/crypto;github.com/ProtonMail/docker-credential-helpers;github.com/ProtonMail/go-autostart;github.com/ProtonMail/go-imap;github.com/ProtonMail/go-imap-id;github.com/ProtonMail/go-rfc5322;github.com/ProtonMail/go-vcard;github.com/ProtonMail/gopenpgp/v2;github.com/PuerkitoBio/goquery;github.com/abiosoft/ishell;github.com/abiosoft/readline;github.com/allan-simon/go-singleinstance;github.com/chzyer/logex;github.com/chzyer/test;github.com/cucumber/godog;github.com/docker/docker-credential-helpers;github.com/emersion/go-imap;github.com/emersion/go-imap-appendlimit;github.com/emersion/go-imap-idle;github.com/emersion/go-imap-move;github.com/emersion/go-imap-quota;github.com/emersion/go-imap-unselect;github.com/emersion/go-mbox;github.com/emersion/go-message;github.com/emersion/go-sasl;github.com/emersion/go-smtp;github.com/emersion/go-textwrapper;github.com/emersion/go-vcard;github.com/fatih/color;github.com/flynn-archive/go-shlex;github.com/getsentry/sentry-go;github.com/go-resty/resty/v2;github.com/golang/mock;github.com/google/go-cmp;github.com/google/uuid;github.com/gopherjs/gopherjs;github.com/hashicorp/go-multierror;github.com/jameskeane/bcrypt;github.com/jaytaylor/html2text;github.com/keybase/go-keychain;github.com/logrusorgru/aurora;github.com/mattn/go-runewidth;github.com/miekg/dns;github.com/nsf/jsondiff;github.com/olekukonko/tablewriter;github.com/pkg/errors;github.com/sirupsen/logrus;github.com/skratchdot/open-golang;github.com/ssor/bom;github.com/stretchr/testify;github.com/therecipe/qt;github.com/urfave/cli/v2;github.com/vmihailenco/msgpack/v5;go.etcd.io/bbolt;golang.org/x/crypto;golang.org/x/net;golang.org/x/text;;Font Awesome 4.7.0;;Qt 5.13 by Qt group;"

View File

@ -88,7 +88,7 @@ func (ie *ImportExport) ReportBug(osType, osVersion, description, accountName, a
return nil 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() c := ie.clientManager.GetAnonymousClient()
defer c.Logout() defer c.Logout()
@ -118,9 +118,9 @@ func (ie *ImportExport) ReportFile(osType, osVersion, accountName, address strin
} }
// 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 +132,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 +149,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 +163,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 +176,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
} }

View File

@ -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.

View File

@ -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)

View File

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

View File

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

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