Compare commits

...

102 Commits

Author SHA1 Message Date
5d82c218ca GODT-1165: Handle UID FETCH with sequence range of empty mailbox 2021-05-19 16:17:19 +02:00
6ff4c8a738 Other: Bridge James 1.8.0 2021-05-07 15:56:11 +02:00
dd66b7f8d0 GODT-1159 SMTP server not restarting after restored internet
- [x] write tests to check that IMAP and SMTP servers are closed when there
  is no internet
- [x] always create new go-smtp instance during listenAndServe(int)
2021-05-07 10:34:14 +00:00
0b95ed4dea GODT-1146: Refactor header filtering 2021-05-03 15:53:46 +02:00
ce64aeb05f Other: avoid API jail 2021-05-03 07:05:15 +02:00
27cfda680d GODT-1152: Correctly resolve wildcard sequence/UID set 2021-04-30 10:35:34 +00:00
323303a98b GODT-1089 Explicitly open system preferences window on BigSur. 2021-04-30 09:10:14 +00:00
8109831c07 GODT-35: Finish all details and make tests pass 2021-04-30 05:41:39 +02:00
2284e9ede1 GODT-35: New pmapi client and manager using resty 2021-04-30 05:34:36 +02:00
1d538e8540 GODT-876 Set default from if empty for importing draft 2021-04-29 14:07:20 +00:00
8ccaac8090 GODT-1056 Check encrypted size of the message before upload 2021-04-29 12:40:29 +00:00
22bf8f62ce GODT-1143 Turn off SMTP server while no connection 2021-04-28 11:23:41 +02:00
fed031ebaa Other: early release notes 1.7.1 2021-04-28 10:43:42 +02:00
7a15ebbd54 Other: update go.mod with qt docs 2021-04-27 17:11:23 +02:00
94b5799ba7 Other refactor: clean old builder 2021-04-27 08:12:50 +00:00
286f51a4e7 Other: Bridge Iron 1.7.1 2021-04-23 11:29:03 +02:00
ee961ae4a8 GODT-1141 Use attachment name from content type if not specified in content disposition 2021-04-23 07:18:41 +00:00
4038752a9a Other: preserve message header in PGP/MIME passthrough message 2021-04-22 16:30:29 +02:00
ebf724412b Other: fix custom message on decryption error for externally encrypted message 2021-04-22 12:28:54 +00:00
14d42b5e76 GODT-1081 Return newline after headers with every fetch 2021-04-21 13:02:23 +00:00
2b8d92e82d Other: fix release notes 2021-04-21 12:11:52 +02:00
11b1e3acf5 Other: Release notes Iron early 1.7.0 2021-04-21 10:47:46 +02:00
c5eb660315 Other: fix live test: API sanitize timestamp 2021-04-16 08:32:51 +02:00
5ad23715ec Other: Release Bridge Iron v1.7.0 2021-04-15 13:27:05 +02:00
8ab05a000c GODT-1136 DB Cache header from builder and test 2021-04-15 09:51:08 +00:00
454d248819 GODT-213: Preserve contenttype for undecryptable message body 2021-04-15 09:51:08 +00:00
6c8e5f7cd3 GODT-213: Use application/octet-stream for encrypted parts 2021-04-15 09:51:08 +00:00
f5aba717b2 GODT-213: Force no transfer encoding for embedded message/rfc822 parts 2021-04-15 09:51:08 +00:00
1359c39bc0 GODT-213: Remove dead code GetRelatedHeader/GetRelatedBoundary 2021-04-15 09:51:08 +00:00
4850681f1d GODT-213: correctly expect text/plain in custom message text parts 2021-04-15 09:51:08 +00:00
aa55c69307 Other: fix linter 2021-04-15 09:51:08 +00:00
1f19d4df75 GODT-213: Force text/plain for custom message text part 2021-04-15 09:51:08 +00:00
c0f6af9eb5 GODT-213: Complex external encrypted tests (multipart/alternative, message/rfc822 attachment) 2021-04-15 09:51:08 +00:00
ef6a3d4999 GODT-213: Add comments for newly added code 2021-04-15 09:51:08 +00:00
50550d42b4 GODT-213: Message Builder 2021-04-15 09:51:08 +00:00
8db89a1a6c GODT-1113: Fix tray icon size on macOS Big Sur.
Add patched libqcocoa based on Qt 5.13.0
2021-04-15 09:08:19 +00:00
ba1dfb1bf4 GODT-947 Force colors in logs 2021-04-15 07:20:53 +00:00
d243880753 Other: stop rejecting old TLS versions 2021-04-14 09:28:31 +02:00
cccaaa3d82 Other: turn off bad login in live test 2021-04-12 06:16:34 +02:00
2d95f21567 Other: add straightforward linters 2021-04-08 16:09:40 +02:00
7d0af7624c Other: Bump linter 2021-04-07 10:54:09 +02:00
2f35c453a1 Other: Release notes stable 2021-04-01 08:05:04 +02:00
05dd137bc8 Other: Release notes 2021-03-31 06:52:00 +02:00
767628946f Other: Bridge HZM 1.6.9 2021-03-29 12:08:46 +02:00
d4efa7131f GODT-1121 Initial value of silent updates toggle button 2021-03-29 06:15:33 +02:00
144cf6e40c Other: Bridge HZM 1.6.8 & Import-Export Farg 1.3.3 2021-03-26 11:17:01 +01:00
a205d8c046 GODT-1120 hotfix: use Info level in internal/app logs 2021-03-25 11:33:32 +01:00
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
377 changed files with 14169 additions and 11599 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,161 @@
Changelog [format](http://keepachangelog.com/en/1.0.0/) Changelog [format](http://keepachangelog.com/en/1.0.0/)
## [Bridge 1.8.0] James
### Added
* GODT-1056 Check encrypted size of the message before upload.
* GODT-1143 Turn off SMTP server while no connection.
* GODT-1089 Explicitly open system preferences window on BigSur.
### Fixed
* GODT-1159 SMTP server not restarting after restored internet.
* GODT-1146 Refactor handling of fetching BODY[HEADER] (and similar) regarding trailing newline.
* GODT-1152 Correctly resolve wildcard sequence/UID set.
* Other: Avoid API jail.
## [Bridge 1.7.1] Iron
### Fixed
* GODT-1081 Properly return newlines when returning headers.
* GODT-1150 Externally encrypted messages with missing private key would not be built with custom message.
* GODT-1141 Attachment is named as attachment.bin in some cases.
## [Bridge 1.7.0] Iron
### Added
* GODT-213 New message builder:
* Preserve Content-Type for undecryptable message body.
* Use application/octet-stream for encrypted parts.
* Force no transfer encoding for embedded message/rfc822 parts.
* Remove dead code GetRelatedHeader/GetRelatedBoundary.
* Correctly expect text/plain in custom message text parts.
* Force text/plain for custom message text part.
* Complex external encrypted tests (multipart/alternative, message/rfc822 attachment).
### Fixed
* GODT-1136 DB Cache header from builder and test.
* GODT-1113 Fix tray icon size on macOS Big Sur.
* GODT-947 Force colors in logs.
## [Bridge 1.6.9] HZM
### Fixed
* GODT-1121 'Keep the application up to date' switches off after restarting Bridge.
## [Bridge 1.6.8] HZM
### Fixed
* GODT-1120 Use Info level in internal/app logs.
## [IE 1.3.3] Farg
### Fixed
* GODT-1120 Use Info level in internal/app logs.
## [Bridge 1.6.7] HZM
### 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 +165,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.8.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
@ -242,21 +253,25 @@ bench:
coverage: test coverage: test
go tool cover -html=/tmp/coverage.out -o=coverage.html go tool cover -html=/tmp/coverage.out -o=coverage.html
mocks: integration-test-bridge:
mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/users Locator,PanicHandler,ClientManager,CredentialsStorer,StoreMaker > internal/users/mocks/mocks.go ${MAKE} -C test test-bridge
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/pkg/listener Listener > internal/store/mocks/utils_mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/pkg/pmapi Client > pkg/pmapi/mocks/mocks.go
lint: lint-golang lint-license lint-changelog mocks:
mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/users Locator,PanicHandler,CredentialsStorer,StoreMaker > internal/users/mocks/mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/pkg/listener Listener > internal/users/mocks/listener_mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/transfer PanicHandler,IMAPClientProvider > internal/transfer/mocks/mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/store PanicHandler,BridgeUser,ChangeNotifier > internal/store/mocks/mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/pkg/listener Listener > internal/store/mocks/utils_mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/pkg/pmapi Client,Manager > pkg/pmapi/mocks/mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/pkg/message Fetcher > pkg/message/mocks/mocks.go
lint: gofiles lint-golang lint-license lint-changelog
lint-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 +307,7 @@ LOG?=debug
LOG_IMAP?=client # client/server/all, or empty to turn it off LOG_IMAP?=client # client/server/all, or empty to turn it off
LOG_SMTP?=--log-smtp # empty to turn it off LOG_SMTP?=--log-smtp # empty to turn it off
RUN_FLAGS?=-m -l=${LOG} --log-imap=${LOG_IMAP} ${LOG_SMTP} RUN_FLAGS?=-m -l=${LOG} --log-imap=${LOG_IMAP} ${LOG_SMTP}
RUN_FLAGS_IE?=-m -l=${LOG}
run: run-nogui-cli run: run-nogui-cli
@ -301,12 +317,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 +330,11 @@ run-ie-qml-preview:
$(MAKE) -C internal/frontend/qt-ie -f Makefile.local qmlpreview $(MAKE) -C internal/frontend/qt-ie -f Makefile.local qmlpreview
run-ie: run-ie:
TARGET_CMD=Import-Export $(MAKE) run TARGET_CMD=Import-Export RUN_FLAGS="${RUN_FLAGS_IE}" $(MAKE) run
run-ie-qt: run-ie-qt:
TARGET_CMD=Import-Export $(MAKE) run-qt TARGET_CMD=Import-Export RUN_FLAGS="${RUN_FLAGS_IE}" $(MAKE) run-qt
run-ie-nogui: run-ie-nogui:
TARGET_CMD=Import-Export $(MAKE) run-nogui TARGET_CMD=Import-Export RUN_FLAGS="${RUN_FLAGS_IE}" $(MAKE) run-nogui
clean-frontend-qt: clean-frontend-qt:
$(MAKE) -C internal/frontend/qt -f Makefile.local clean $(MAKE) -C internal/frontend/qt -f Makefile.local clean
@ -335,7 +351,7 @@ clean: clean-vendor
rm -rf cmd/Desktop-Bridge/deploy rm -rf cmd/Desktop-Bridge/deploy
rm -rf cmd/Import-Export/deploy rm -rf cmd/Import-Export/deploy
rm -f build last.log mem.pprof main.go rm -f build last.log mem.pprof main.go
rm -rf logo.ico icon.rc icon_windows.syso internal/frontend/qt/icon_windows.syso rm -f resource.syso
rm -f release-notes/bridge.html rm -f release-notes/bridge.html
rm -f release-notes/import-export.html rm -f release-notes/import-export.html
@ -343,3 +359,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()

12
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
@ -40,7 +40,7 @@ require (
github.com/fatih/color v1.9.0 github.com/fatih/color v1.9.0
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
github.com/getsentry/sentry-go v0.8.0 github.com/getsentry/sentry-go v0.8.0
github.com/go-resty/resty/v2 v2.3.0 github.com/go-resty/resty/v2 v2.6.0
github.com/golang/mock v1.4.4 github.com/golang/mock v1.4.4
github.com/google/go-cmp v0.5.1 github.com/google/go-cmp v0.5.1
github.com/google/uuid v1.1.1 github.com/google/uuid v1.1.1
@ -50,7 +50,7 @@ require (
github.com/keybase/go-keychain v0.0.0-20200502122510-cda31fe0c86d github.com/keybase/go-keychain v0.0.0-20200502122510-cda31fe0c86d
github.com/logrusorgru/aurora v2.0.3+incompatible github.com/logrusorgru/aurora v2.0.3+incompatible
github.com/mattn/go-runewidth v0.0.9 // indirect github.com/mattn/go-runewidth v0.0.9 // indirect
github.com/miekg/dns v1.1.30 github.com/miekg/dns v1.1.41
github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce
github.com/olekukonko/tablewriter v0.0.4 // indirect github.com/olekukonko/tablewriter v0.0.4 // indirect
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
@ -59,10 +59,12 @@ require (
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
github.com/stretchr/testify v1.6.1 github.com/stretchr/testify v1.6.1
github.com/therecipe/qt v0.0.0-20200701200531-7f61353ee73e github.com/therecipe/qt v0.0.0-20200701200531-7f61353ee73e
github.com/therecipe/qt/internal/binding/files/docs/5.12.0 v0.0.0-20200904063919-c0c124a5770d // indirect
github.com/therecipe/qt/internal/binding/files/docs/5.13.0 v0.0.0-20200904063919-c0c124a5770d // indirect
github.com/urfave/cli/v2 v2.2.0 github.com/urfave/cli/v2 v2.2.0
github.com/vmihailenco/msgpack/v5 v5.1.3 github.com/vmihailenco/msgpack/v5 v5.1.3
go.etcd.io/bbolt v1.3.5 go.etcd.io/bbolt v1.3.5
golang.org/x/net v0.0.0-20200707034311-ab3426394381 golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4
golang.org/x/text v0.3.5-0.20201125200606-c27b9fd57aec golang.org/x/text v0.3.5-0.20201125200606-c27b9fd57aec
) )

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

View File

@ -23,7 +23,7 @@
// - persistent settings // - persistent settings
// - event listener // - event listener
// - credentials store // - credentials store
// - pmapi ClientManager // - pmapi Manager
// In addition, the base initialises logging and reacts to command line arguments // In addition, the base initialises logging and reacts to command line arguments
// which control the log verbosity and enable cpu/memory profiling. // which control the log verbosity and enable cpu/memory profiling.
package base package base
@ -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.Manager
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),
@ -161,25 +181,24 @@ func New( // nolint[funlen]
kc = keychain.NewMissingKeychain() kc = keychain.NewMissingKeychain()
} }
cfg := pmapi.NewConfig(configName, constants.Version)
cfg.GetUserAgent = userAgent.String
cfg.UpgradeApplicationHandler = func() { listener.Emit(events.UpgradeApplicationEvent, "") }
cfg.TLSIssueHandler = func() { listener.Emit(events.TLSCertIssue, "") }
cm := pmapi.New(cfg)
cm.AddConnectionObserver(pmapi.NewConnectionObserver(
func() { listener.Emit(events.InternetOffEvent, "") },
func() { listener.Emit(events.InternetOnEvent, "") },
))
jar, err := cookies.NewCookieJar(settingsObj) jar, err := cookies.NewCookieJar(settingsObj)
if err != nil { if err != nil {
return nil, err return nil, err
} }
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, "")
}
cm := pmapi.NewClientManager(apiConfig)
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 +239,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 +273,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 +323,22 @@ 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))
b.CM.SetLogging(logrus.WithField("pkg", "pmapi"), logrus.GetLevel() == logrus.TraceLevel)
logrus. logrus.
WithField("appName", b.Name). WithField("appName", b.Name).
@ -328,7 +350,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")

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,13 @@ 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"), b.CrashHandler,
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 +112,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 +125,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 +140,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 +190,10 @@ func generateTLSCerts(b *base.Base) error {
} }
func checkAndHandleUpdate(u types.Updater, f frontend.Frontend, autoUpdate bool) { func checkAndHandleUpdate(u types.Updater, f frontend.Frontend, autoUpdate bool) {
log := logrus.WithField("pkg", "app/bridge")
version, err := u.Check() version, err := u.Check()
if err != nil { if err != nil {
logrus.WithError(err).Error("An error occurred while checking for updates") log.WithError(err).Error("An error occurred while checking for updates")
return return
} }
@ -194,11 +203,11 @@ func checkAndHandleUpdate(u types.Updater, f frontend.Frontend, autoUpdate bool)
f.SetVersion(version) f.SetVersion(version)
if !u.IsUpdateApplicable(version) { if !u.IsUpdateApplicable(version) {
logrus.Debug("No need to update") log.Info("No need to update")
return return
} }
logrus.WithField("version", version.Version).Info("An update is available") log.WithField("version", version.Version).Info("An update is available")
if !autoUpdate { if !autoUpdate {
f.NotifyManualUpdate(version, u.CanInstall(version)) f.NotifyManualUpdate(version, u.CanInstall(version))
@ -206,16 +215,16 @@ func checkAndHandleUpdate(u types.Updater, f frontend.Frontend, autoUpdate bool)
} }
if !u.CanInstall(version) { if !u.CanInstall(version) {
logrus.Info("A manual update is required") log.Info("A manual update is required")
f.NotifySilentUpdateError(updater.ErrManualUpdateRequired) f.NotifySilentUpdateError(updater.ErrManualUpdateRequired)
return return
} }
if err := u.InstallUpdate(version); err != nil { if err := u.InstallUpdate(version); err != nil {
if errors.Cause(err) == updater.ErrDownloadVerify { if errors.Cause(err) == updater.ErrDownloadVerify {
logrus.WithError(err).Warning("Skipping update installation due to temporary error") log.WithError(err).Warning("Skipping update installation due to temporary error")
} else { } else {
logrus.WithError(err).Error("The update couldn't be installed") log.WithError(err).Error("The update couldn't be installed")
f.NotifySilentUpdateError(err) f.NotifySilentUpdateError(err)
} }

View File

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

@ -19,6 +19,7 @@
package bridge package bridge
import ( import (
"context"
"fmt" "fmt"
"strconv" "strconv"
"time" "time"
@ -26,6 +27,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"
@ -43,22 +45,19 @@ type Bridge struct {
locations Locator locations Locator
settings SettingsProvider settings SettingsProvider
clientManager users.ClientManager clientManager pmapi.Manager
updater Updater updater Updater
versioner Versioner versioner Versioner
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 pmapi.Manager,
credStorer users.CredentialsStorer, credStorer users.CredentialsStorer,
updater Updater, updater Updater,
versioner Versioner, versioner Versioner,
@ -69,7 +68,7 @@ func New(
clientManager.AllowProxy() clientManager.AllowProxy()
} }
storeFactory := newStoreFactory(cache, panicHandler, clientManager, eventListener) storeFactory := newStoreFactory(cache, sentryReporter, panicHandler, eventListener)
u := users.New(locations, panicHandler, eventListener, clientManager, credStorer, storeFactory, true) u := users.New(locations, panicHandler, eventListener, clientManager, credStorer, storeFactory, true)
b := &Bridge{ b := &Bridge{
Users: u, Users: u,
@ -118,64 +117,17 @@ 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() return b.clientManager.ReportBug(context.Background(), pmapi.ReportBugReq{
defer c.Logout()
title := "[Bridge] Bug"
report := pmapi.ReportReq{
OS: osType, OS: osType,
OSVersion: osVersion, OSVersion: osVersion,
Browser: emailClient, Browser: emailClient,
Title: title, Title: "[Bridge] Bug",
Description: description, Description: description,
Username: accountName, Username: accountName,
Email: address, Email: address,
} })
if err := c.Report(report); err != nil {
log.Error("Reporting bug failed: ", err)
return err
}
log.Info("Bug successfully reported")
return nil
} }
// GetUpdateChannel returns currently set update channel. // GetUpdateChannel returns currently set update channel.
@ -187,26 +139,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,39 @@ 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 eventListener listener.Listener
storeCache *store.Cache storeCache *store.Cache
} }
func newStoreFactory( func newStoreFactory(
cache Cacher, cache Cacher,
sentryReporter *sentry.Reporter,
panicHandler users.PanicHandler, panicHandler users.PanicHandler,
clientManager users.ClientManager,
eventListener listener.Listener, eventListener listener.Listener,
) *storeFactory { ) *storeFactory {
return &storeFactory{ return &storeFactory{
cache: cache, cache: cache,
panicHandler: panicHandler, sentryReporter: sentryReporter,
clientManager: clientManager, panicHandler: panicHandler,
eventListener: eventListener, eventListener: eventListener,
storeCache: store.NewCache(cache.GetIMAPCachePath()), 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.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

@ -0,0 +1,61 @@
// 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 (
"os/exec"
"runtime"
"strings"
"github.com/Masterminds/semver/v3"
)
// IsCatalinaOrNewer checks whether the host is MacOS Catalina 10.15.x or higher.
func IsCatalinaOrNewer() bool {
return isThisDarwinNewerOrEqual(getMinCatalina())
}
// IsBigSurOrNewer checks whether the host is MacOS BigSur 10.16.x or higher.
func IsBigSurOrNewer() bool {
return isThisDarwinNewerOrEqual(getMinBigSur())
}
func getMinCatalina() *semver.Version { return semver.MustParse("10.15.0") }
func getMinBigSur() *semver.Version { return semver.MustParse("10.16.0") }
func isThisDarwinNewerOrEqual(minVersion *semver.Version) bool {
if runtime.GOOS != "darwin" {
return false
}
rawVersion, err := exec.Command("sw_vers", "-productVersion").Output()
if err != nil {
return false
}
return isVersionEqualOrNewer(minVersion, strings.TrimSpace(string(rawVersion)))
}
// isVersionEqualOrNewer is separated to be able to run test on other than darwin.
func isVersionEqualOrNewer(minVersion *semver.Version, rawVersion string) bool {
semVersion, err := semver.NewVersion(rawVersion)
if err != nil {
return false
}
return semVersion.GreaterThan(minVersion) || semVersion.Equal(minVersion)
}

View File

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

View File

@ -18,36 +18,42 @@
package useragent package useragent
import ( import (
"os/exec" "fmt"
"regexp"
"runtime" "runtime"
"strings"
"github.com/Masterminds/semver/v3"
) )
// IsCatalinaOrNewer checks that host is MacOS Catalina 10.15.x or higher. type UserAgent struct {
func IsCatalinaOrNewer() bool { client, platform string
if runtime.GOOS != "darwin" {
return false
}
return isVersionCatalinaOrNewer(getMacVersion())
} }
func getMacVersion() string { func New() *UserAgent {
out, err := exec.Command("sw_vers", "-productVersion").Output() return &UserAgent{
if err != nil { client: "",
return "" platform: runtime.GOOS,
} }
return strings.TrimSpace(string(out))
} }
func isVersionCatalinaOrNewer(version string) bool { func (ua *UserAgent) SetClient(name, version string) {
v, err := semver.NewVersion(version) ua.client = fmt.Sprintf("%v/%v", name, regexp.MustCompile(`(.*) \((.*)\)`).ReplaceAllString(version, "$1-$2"))
if err != nil { }
return false
func (ua *UserAgent) HasClient() bool {
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"
} }
catalina := semver.MustParse("10.15.0") return fmt.Sprintf("%v (%v)", client, ua.platform)
return v.GreaterThan(catalina) || v.Equal(catalina)
} }

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

@ -15,9 +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 pmapi // +build build_qa
// ConnectionReporter provides a way to report when internet connection is lost. package constants
type ConnectionReporter interface {
NotifyConnectionLost() error 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

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

@ -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,19 +109,53 @@ func New( //nolint[funlen]
}) })
fe.AddCmd(changeCmd) fe.AddCmd(changeCmd)
// Check commands. // DoH commands.
checkCmd := &ishell.Cmd{Name: "check", Help: "check internet connection or new version."} dohCmd := &ishell.Cmd{Name: "proxy",
checkCmd.AddCmd(&ishell.Cmd{Name: "updates", Help: "allow or disallow bridge to securely connect to proton via a third party when it is being blocked",
Help: "check for Bridge updates. (aliases: u, v, version)", }
Aliases: []string{"u", "version", "v"}, dohCmd.AddCmd(&ishell.Cmd{Name: "allow",
Func: fe.checkUpdates, Help: "allow bridge to securely connect to proton via a third party when it is being blocked",
Func: fe.allowProxy,
}) })
checkCmd.AddCmd(&ishell.Cmd{Name: "internet", dohCmd.AddCmd(&ishell.Cmd{Name: "disallow",
Help: "check internet connection. (aliases: i, conn, connection)", Help: "disallow bridge to securely connect to proton via a third party when it is being blocked",
Aliases: []string{"i", "con", "connection"}, Func: fe.disallowProxy,
Func: fe.checkInternetConnection,
}) })
fe.AddCmd(checkCmd) 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)
// Print info commands. // Print info commands.
fe.AddCmd(&ishell.Cmd{Name: "log-dir", fe.AddCmd(&ishell.Cmd{Name: "log-dir",
@ -189,14 +219,14 @@ func New( //nolint[funlen]
} }
func (f *frontendCLI) watchEvents() { func (f *frontendCLI) watchEvents() {
errorCh := f.getEventChannel(events.ErrorEvent) errorCh := f.eventListener.ProvideChannel(events.ErrorEvent)
credentialsErrorCh := f.getEventChannel(events.CredentialsErrorEvent) credentialsErrorCh := f.eventListener.ProvideChannel(events.CredentialsErrorEvent)
internetOffCh := f.getEventChannel(events.InternetOffEvent) internetOffCh := f.eventListener.ProvideChannel(events.InternetOffEvent)
internetOnCh := f.getEventChannel(events.InternetOnEvent) internetOnCh := f.eventListener.ProvideChannel(events.InternetOnEvent)
addressChangedCh := f.getEventChannel(events.AddressChangedEvent) addressChangedCh := f.eventListener.ProvideChannel(events.AddressChangedEvent)
addressChangedLogoutCh := f.getEventChannel(events.AddressChangedLogoutEvent) addressChangedLogoutCh := f.eventListener.ProvideChannel(events.AddressChangedLogoutEvent)
logoutCh := f.getEventChannel(events.LogoutEvent) logoutCh := f.eventListener.ProvideChannel(events.LogoutEvent)
certIssue := f.getEventChannel(events.TLSCertIssue) certIssue := f.eventListener.ProvideChannel(events.TLSCertIssue)
for { for {
select { select {
case errorDetails := <-errorCh: case errorDetails := <-errorCh:
@ -223,13 +253,6 @@ func (f *frontendCLI) watchEvents() {
} }
} }
func (f *frontendCLI) getEventChannel(event string) <-chan string {
ch := make(chan string)
f.eventListener.Add(event, ch)
f.eventListener.RetryEmit(event)
return ch
}
// Loop starts the frontend loop with an interactive shell. // Loop starts the frontend loop with an interactive shell.
func (f *frontendCLI) Loop() error { func (f *frontendCLI) Loop() error {
f.Print(` f.Print(`

View File

@ -39,14 +39,6 @@ func (f *frontendCLI) restart(c *ishell.Context) {
} }
} }
func (f *frontendCLI) checkInternetConnection(c *ishell.Context) {
if f.bridge.CheckConnection() == nil {
f.Println("Internet connection is available.")
} else {
f.Println("Can not contact the server, please check your internet connection.")
}
}
func (f *frontendCLI) printLogDir(c *ishell.Context) { func (f *frontendCLI) printLogDir(c *ishell.Context) {
if path, err := f.locations.ProvideLogsPath(); err != nil { if path, err := f.locations.ProvideLogsPath(); err != nil {
f.Println("Failed to determine location of log files") f.Println("Failed to determine location of log files")
@ -132,24 +124,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

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

View File

@ -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
@ -408,7 +409,6 @@ Dialog {
onShow: { onShow: {
if (winMain.updateState==gui.enums.statusNoInternet) { if (winMain.updateState==gui.enums.statusNoInternet) {
go.checkInternet()
if (winMain.updateState==gui.enums.statusNoInternet) { if (winMain.updateState==gui.enums.statusNoInternet) {
go.notifyError(gui.enums.errNoInternet) go.notifyError(gui.enums.errNoInternet)
root.hide() root.hide()
@ -428,7 +428,7 @@ Dialog {
onTriggered : { onTriggered : {
switch (currentIndex) { switch (currentIndex) {
case 0: case 0:
go.loadStructureForExport(root.address) go.loadStructureForExport(root.account, root.address)
sourceFoldersInput.hasItems = (transferRules.rowCount() > 0) sourceFoldersInput.hasItems = (transferRules.rowCount() > 0)
break break
case 2: case 2:

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 != ""
@ -856,14 +857,12 @@ Dialog {
inputPort . checkIsANumber() inputPort . checkIsANumber()
//emailProvider . currentIndex!=0 //emailProvider . currentIndex!=0
)) isOK = false )) isOK = false
go.checkInternet()
if (winMain.updateState == gui.enums.statusNoInternet) { // todo: use main error dialog for this if (winMain.updateState == gui.enums.statusNoInternet) { // todo: use main error dialog for this
errorPopup.show(qsTr("Please check your internet connection.")) errorPopup.show(qsTr("Please check your internet connection."))
return false return false
} }
break break
case 2: // loading structure case 2: // loading structure
go.checkInternet()
if (winMain.updateState == gui.enums.statusNoInternet) { if (winMain.updateState == gui.enums.statusNoInternet) {
errorPopup.show(qsTr("Please check your internet connection.")) errorPopup.show(qsTr("Please check your internet connection."))
return false return false
@ -948,7 +947,6 @@ Dialog {
onShow : { onShow : {
root.clear() root.clear()
if (winMain.updateState==gui.enums.statusNoInternet) { if (winMain.updateState==gui.enums.statusNoInternet) {
go.checkInternet()
if (winMain.updateState==gui.enums.statusNoInternet) { if (winMain.updateState==gui.enums.statusNoInternet) {
winMain.popupMessage.show(go.canNotReachAPI) winMain.popupMessage.show(go.canNotReachAPI)
root.hide() root.hide()
@ -1032,6 +1030,7 @@ Dialog {
root.isFromIMAP, root.isFromIMAP,
root.inputPath, root.inputPath,
inputEmail.text, inputPassword.text, inputServer.text, inputPort.text, inputEmail.text, inputPassword.text, inputServer.text, inputPort.text,
root.account,
root.address root.address
) )
break break

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

@ -25,33 +25,12 @@ import ProtonUI 1.0
Rectangle { Rectangle {
id: root id: root
property var iTry: 0 property var iTry: 0
property var secLeft: 0
property var second: 1000 // convert millisecond to second property var second: 1000 // convert millisecond to second
property var checkInterval: [ 5, 10, 30, 60, 120, 300, 600 ] // seconds
property bool isVisible: true property bool isVisible: true
property var fontSize : 1.2 * Style.main.fontSize property var fontSize : 1.2 * Style.main.fontSize
color : "black" color : "black"
state: "upToDate" state: "upToDate"
Timer {
id: retryInternet
interval: second
triggeredOnStart: false
repeat: true
onTriggered : {
secLeft--
if (secLeft <= 0) {
retryInternet.stop()
go.checkInternet()
if (iTry < checkInterval.length-1) {
iTry++
}
secLeft=checkInterval[iTry]
retryInternet.start()
}
}
}
Row { Row {
id: messageRow id: messageRow
anchors.centerIn: root anchors.centerIn: root
@ -66,14 +45,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,49 +87,21 @@ Rectangle {
onStateChanged : { onStateChanged : {
switch (root.state) { switch (root.state) {
case "internetCheck": case "internetCheck":
break; break;
case "noInternet" : case "noInternet" :
gui.warningFlags |= Style.warnInfoBar break;
retryInternet.start()
secLeft=checkInterval[iTry]
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 break;
iTry = 0
secLeft=checkInterval[iTry]
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") {
retryInternet.stop()
}
}
function timeToRetry() {
if (secLeft==1){
return qsTr("a second", "time to wait till internet connection is retried")
} else if (secLeft<60){
return secLeft + " " + qsTr("seconds", "time to wait till internet connection is retried")
} else {
var leading = ""+secLeft%60
if (leading.length < 2) {
leading = "0" + leading
}
return Math.floor(secLeft/60) + ":" + leading
} }
} }
@ -199,23 +151,15 @@ Rectangle {
PropertyChanges { PropertyChanges {
target: message target: message
color: Style.main.line color: Style.main.line
text: qsTr("Cannot contact server. Retrying in ", "displayed when the app is disconnected from the internet or server has problems")+timeToRetry()+"." text: qsTr("Cannot contact server. Please wait...", "displayed when the app is disconnected from the internet or server has problems")
} }
PropertyChanges { PropertyChanges {
target: linkText target: linkText
visible: false visible: false
} }
PropertyChanges {
target: actionText
visible: true
text: qsTr("Retry now", "click to try to connect to the internet when the app is disconnected from the internet")
onClicked: {
go.checkInternet()
}
}
PropertyChanges { PropertyChanges {
target: separatorText target: separatorText
visible: true visible: false
text: "|" text: "|"
} }
PropertyChanges { PropertyChanges {
@ -247,7 +191,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 +214,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()
@ -1331,10 +1331,6 @@ Window {
return (fname!="fail") return (fname!="fail")
} }
function checkInternet() {
// nothing to do
}
function loadImportReports(fname) { function loadImportReports(fname) {
console.log("load import reports for ", fname) console.log("load import reports for ", fname)
} }
@ -1355,10 +1351,10 @@ Window {
return !fname.includes("fail") return !fname.includes("fail")
} }
onToggleAutoUpdate: { //onToggleAutoUpdate: {
workAndClose() // workAndClose()
isAutoUpdate = (isAutoUpdate!=false) ? false : true // isAutoUpdate = (isAutoUpdate!=false) ? false : true
console.log (" Test: onToggleAutoUpdate "+isAutoUpdate) // console.log (" Test: onToggleAutoUpdate "+isAutoUpdate)
} //}
} }
} }

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

View File

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

View File

@ -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,24 +135,24 @@ func (f *FrontendQt) SetVersion(version updater.VersionInfo) {
} }
func (f *FrontendQt) NotifySilentUpdateInstalled() { func (f *FrontendQt) NotifySilentUpdateInstalled() {
f.Qml.NotifySilentUpdateRestartNeeded() //f.Qml.NotifySilentUpdateRestartNeeded()
} }
func (f *FrontendQt) NotifySilentUpdateError(err error) { func (f *FrontendQt) NotifySilentUpdateError(err error) {
f.Qml.NotifySilentUpdateError() //f.Qml.NotifySilentUpdateError()
} }
func (f *FrontendQt) watchEvents() { func (f *FrontendQt) watchEvents() {
credentialsErrorCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.CredentialsErrorEvent) credentialsErrorCh := f.eventListener.ProvideChannel(events.CredentialsErrorEvent)
internetOffCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.InternetOffEvent) internetOffCh := f.eventListener.ProvideChannel(events.InternetOffEvent)
internetOnCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.InternetOnEvent) internetOnCh := f.eventListener.ProvideChannel(events.InternetOnEvent)
secondInstanceCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.SecondInstanceEvent) secondInstanceCh := f.eventListener.ProvideChannel(events.SecondInstanceEvent)
restartBridgeCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.RestartBridgeEvent) restartBridgeCh := f.eventListener.ProvideChannel(events.RestartBridgeEvent)
addressChangedCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.AddressChangedEvent) addressChangedCh := f.eventListener.ProvideChannel(events.AddressChangedEvent)
addressChangedLogoutCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.AddressChangedLogoutEvent) addressChangedLogoutCh := f.eventListener.ProvideChannel(events.AddressChangedLogoutEvent)
logoutCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.LogoutEvent) logoutCh := f.eventListener.ProvideChannel(events.LogoutEvent)
updateApplicationCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.UpgradeApplicationEvent) updateApplicationCh := f.eventListener.ProvideChannel(events.UpgradeApplicationEvent)
newUserCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.UserRefreshEvent) newUserCh := f.eventListener.ProvideChannel(events.UserRefreshEvent)
for { for {
select { select {
case <-credentialsErrorCh: case <-credentialsErrorCh:
@ -245,11 +245,11 @@ func (f *FrontendQt) QtExecute(Procedure func(*FrontendQt) error) error {
f.Qml.SetCredits(importexport.Credits) f.Qml.SetCredits(importexport.Credits)
f.Qml.SetFullversion(f.buildVersion) f.Qml.SetFullversion(f.buildVersion)
if f.settings.GetBool(settings.AutoUpdateKey) { //if f.settings.GetBool(settings.AutoUpdateKey) {
f.Qml.SetIsAutoUpdate(true) // f.Qml.SetIsAutoUpdate(true)
} else { //} else {
f.Qml.SetIsAutoUpdate(false) // f.Qml.SetIsAutoUpdate(false)
} //}
go func() { go func() {
defer f.panicHandler.HandlePanic() defer f.panicHandler.HandlePanic()
@ -339,22 +339,17 @@ func (f *FrontendQt) sendBug(description, emailClient, address string) bool {
return true return true
} }
func (f *FrontendQt) toggleAutoUpdate() { //func (f *FrontendQt) toggleAutoUpdate() {
defer f.Qml.ProcessFinished() // defer f.Qml.ProcessFinished()
//
if f.settings.GetBool(settings.AutoUpdateKey) { // if f.settings.GetBool(settings.AutoUpdateKey) {
f.settings.SetBool(settings.AutoUpdateKey, false) // f.settings.SetBool(settings.AutoUpdateKey, false)
f.Qml.SetIsAutoUpdate(false) // f.Qml.SetIsAutoUpdate(false)
} else { // } else {
f.settings.SetBool(settings.AutoUpdateKey, true) // f.settings.SetBool(settings.AutoUpdateKey, true)
f.Qml.SetIsAutoUpdate(true) // f.Qml.SetIsAutoUpdate(true)
} // }
} //}
// checkInternet is almost idetical to bridge
func (f *FrontendQt) checkInternet() {
f.Qml.SetConnectionStatus(f.ie.CheckConnection() == nil)
}
func (f *FrontendQt) showError(code int, err error) { func (f *FrontendQt) showError(code int, err error) {
f.Qml.SetErrorDescription(err.Error()) f.Qml.SetErrorDescription(err.Error())

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,7 @@ type GoQMLInterface struct {
_ string `property:"credentialsNotRemoved"` _ string `property:"credentialsNotRemoved"`
_ string `property:"versionCheckFailed"` _ string `property:"versionCheckFailed"`
// //
_ func(isAvailable bool) `signal:"setConnectionStatus"` _ func(isAvailable bool) `signal:"setConnectionStatus"`
_ func(updateState string) `signal:"setUpdateState"`
_ func() `slot:"checkInternet"`
_ func() `slot:"setToRestart"` _ func() `slot:"setToRestart"`
@ -93,7 +92,7 @@ type GoQMLInterface struct {
_ func() `signal:"showWindow"` _ func() `signal:"showWindow"`
_ func() `slot:"toggleAutoUpdate"` //_ func() `slot:"toggleAutoUpdate"`
_ func() `slot:"quit"` _ func() `slot:"quit"`
_ func() `slot:"loadAccounts"` _ func() `slot:"loadAccounts"`
_ func() `slot:"openLogs"` _ func() `slot:"openLogs"`
@ -108,14 +107,14 @@ type GoQMLInterface struct {
_ func(description, client, address string) bool `slot:"sendBug"` _ func(description, client, address string) bool `slot:"sendBug"`
_ func(address string) bool `slot:"sendImportReport"` _ func(address string) bool `slot:"sendImportReport"`
_ func(address string) `slot:"loadStructureForExport"` _ func(username, address string) `slot:"loadStructureForExport"`
_ func() string `slot:"leastUsedColor"` _ func() string `slot:"leastUsedColor"`
_ func(username string, name string, color string, isLabel bool, sourceID string) bool `slot:"createLabelOrFolder"` _ func(username string, name string, color string, isLabel bool, sourceID string) bool `slot:"createLabelOrFolder"`
_ func(fpath, address, fileType string, attachEncryptedBody bool) `slot:"startExport"` _ func(fpath, address, fileType string, attachEncryptedBody bool) `slot:"startExport"`
_ func(email string, importEncrypted bool) `slot:"startImport"` _ func(email string, importEncrypted bool) `slot:"startImport"`
_ func() `slot:"resetSource"` _ func() `slot:"resetSource"`
_ func(isFromIMAP bool, sourcePath, sourceEmail, sourcePassword, sourceServe, sourcePort, targetAddress string) `slot:"setupAndLoadForImport"` _ func(isFromIMAP bool, sourcePath, sourceEmail, sourcePassword, sourceServe, sourcePort, targetUsername, targetAddress string) `slot:"setupAndLoadForImport"`
_ string `property:"progressInit"` _ string `property:"progressInit"`
@ -162,7 +161,7 @@ func (s *GoQMLInterface) init() {}
func (s *GoQMLInterface) SetFrontend(f *FrontendQt) { func (s *GoQMLInterface) SetFrontend(f *FrontendQt) {
s.ConnectQuit(f.App.Quit) s.ConnectQuit(f.App.Quit)
s.ConnectToggleAutoUpdate(f.toggleAutoUpdate) //s.ConnectToggleAutoUpdate(f.toggleAutoUpdate)
s.ConnectLoadAccounts(f.Accounts.LoadAccounts) s.ConnectLoadAccounts(f.Accounts.LoadAccounts)
s.ConnectOpenLogs(f.openLogs) s.ConnectOpenLogs(f.openLogs)
s.ConnectOpenDownloadLink(f.openDownloadLink) s.ConnectOpenDownloadLink(f.openDownloadLink)
@ -189,8 +188,6 @@ func (s *GoQMLInterface) SetFrontend(f *FrontendQt) {
return f.programVersion return f.programVersion
}) })
s.ConnectCheckInternet(f.checkInternet)
s.ConnectSetToRestart(f.restarter.SetToRestart) s.ConnectSetToRestart(f.restarter.SetToRestart)
s.ConnectLoadStructureForExport(f.LoadStructureForExport) s.ConnectLoadStructureForExport(f.LoadStructureForExport)
@ -207,4 +204,6 @@ func (s *GoQMLInterface) SetFrontend(f *FrontendQt) {
s.ConnectCheckPathStatus(CheckPathStatus) s.ConnectCheckPathStatus(CheckPathStatus)
s.ConnectEmitEvent(f.emitEvent) s.ConnectEmitEvent(f.emitEvent)
s.ConnectStartManualUpdate(f.startManualUpdate)
} }

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

View File

@ -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.
@ -189,20 +191,20 @@ func (s *FrontendQt) NotifySilentUpdateError(err error) {
func (s *FrontendQt) watchEvents() { func (s *FrontendQt) watchEvents() {
s.WaitUntilFrontendIsReady() s.WaitUntilFrontendIsReady()
errorCh := s.getEventChannel(events.ErrorEvent) errorCh := s.eventListener.ProvideChannel(events.ErrorEvent)
credentialsErrorCh := s.getEventChannel(events.CredentialsErrorEvent) credentialsErrorCh := s.eventListener.ProvideChannel(events.CredentialsErrorEvent)
outgoingNoEncCh := s.getEventChannel(events.OutgoingNoEncEvent) outgoingNoEncCh := s.eventListener.ProvideChannel(events.OutgoingNoEncEvent)
noActiveKeyForRecipientCh := s.getEventChannel(events.NoActiveKeyForRecipientEvent) noActiveKeyForRecipientCh := s.eventListener.ProvideChannel(events.NoActiveKeyForRecipientEvent)
internetOffCh := s.getEventChannel(events.InternetOffEvent) internetOffCh := s.eventListener.ProvideChannel(events.InternetOffEvent)
internetOnCh := s.getEventChannel(events.InternetOnEvent) internetOnCh := s.eventListener.ProvideChannel(events.InternetOnEvent)
secondInstanceCh := s.getEventChannel(events.SecondInstanceEvent) secondInstanceCh := s.eventListener.ProvideChannel(events.SecondInstanceEvent)
restartBridgeCh := s.getEventChannel(events.RestartBridgeEvent) restartBridgeCh := s.eventListener.ProvideChannel(events.RestartBridgeEvent)
addressChangedCh := s.getEventChannel(events.AddressChangedEvent) addressChangedCh := s.eventListener.ProvideChannel(events.AddressChangedEvent)
addressChangedLogoutCh := s.getEventChannel(events.AddressChangedLogoutEvent) addressChangedLogoutCh := s.eventListener.ProvideChannel(events.AddressChangedLogoutEvent)
logoutCh := s.getEventChannel(events.LogoutEvent) logoutCh := s.eventListener.ProvideChannel(events.LogoutEvent)
updateApplicationCh := s.getEventChannel(events.UpgradeApplicationEvent) updateApplicationCh := s.eventListener.ProvideChannel(events.UpgradeApplicationEvent)
newUserCh := s.getEventChannel(events.UserRefreshEvent) newUserCh := s.eventListener.ProvideChannel(events.UserRefreshEvent)
certIssue := s.getEventChannel(events.TLSCertIssue) certIssue := s.eventListener.ProvideChannel(events.TLSCertIssue)
for { for {
select { select {
case errorDetails := <-errorCh: case errorDetails := <-errorCh:
@ -252,13 +254,6 @@ func (s *FrontendQt) watchEvents() {
} }
} }
func (s *FrontendQt) getEventChannel(event string) <-chan string {
ch := make(chan string)
s.eventListener.Add(event, ch)
s.eventListener.RetryEmit(event)
return ch
}
// Loop function for tests. // Loop function for tests.
// //
// It runs QtExecute in new thread with function returning itself after setup. // It runs QtExecute in new thread with function returning itself after setup.
@ -338,39 +333,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 +499,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 +549,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 +589,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() {
@ -640,10 +646,6 @@ func (s *FrontendQt) isSMTPSTARTTLS() bool {
return !s.settings.GetBool(settings.SMTPSSLKey) return !s.settings.GetBool(settings.SMTPSSLKey)
} }
func (s *FrontendQt) checkInternet() {
s.Qml.SetConnectionStatus(s.bridge.CheckConnection() == nil)
}
func (s *FrontendQt) switchAddressModeUser(iAccount int) { func (s *FrontendQt) switchAddressModeUser(iAccount int) {
defer s.Qml.ProcessFinished() defer s.Qml.ProcessFinished()
userID := s.Accounts.get(iAccount).UserID() userID := s.Accounts.get(iAccount).UserID()
@ -711,3 +713,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,7 @@ 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:"setToRestart"` _ func() `slot:"setToRestart"`
@ -202,11 +204,12 @@ func (s *GoQMLInterface) SetFrontend(f *FrontendQt) {
return f.programVer return f.programVer
}) })
s.ConnectCheckInternet(f.checkInternet)
s.ConnectSetToRestart(f.restarter.SetToRestart) s.ConnectSetToRestart(f.restarter.SetToRestart)
s.ConnectToggleIsReportingOutgoingNoEnc(f.toggleIsReportingOutgoingNoEnc) s.ConnectToggleIsReportingOutgoingNoEnc(f.toggleIsReportingOutgoingNoEnc)
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

@ -55,7 +55,6 @@ type UserManager interface {
GetUser(query string) (User, error) GetUser(query string) (User, error)
DeleteUser(userID string, clearCache bool) error DeleteUser(userID string, clearCache bool) error
ClearData() error ClearData() error
CheckConnection() error
} }
// User is an interface of user needed by frontend. // User is an interface of user needed by frontend.
@ -75,13 +74,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 +113,10 @@ func (b *bridgeWrap) GetUser(query string) (User, error) {
type ImportExporter interface { type ImportExporter interface {
UserManager UserManager
GetLocalImporter(string, string) (*transfer.Transfer, error) GetLocalImporter(string, string, string) (*transfer.Transfer, error)
GetRemoteImporter(string, string, string, string, string) (*transfer.Transfer, error) GetRemoteImporter(string, string, string, string, string, string) (*transfer.Transfer, error)
GetEMLExporter(string, string) (*transfer.Transfer, error) GetEMLExporter(string, string, string) (*transfer.Transfer, error)
GetMBOXExporter(string, string) (*transfer.Transfer, error) GetMBOXExporter(string, string, string) (*transfer.Transfer, error)
ReportBug(osType, osVersion, description, accountName, address, emailClient string) error ReportBug(osType, osVersion, description, accountName, address, emailClient string) error
ReportFile(osType, osVersion, accountName, address string, logdata []byte) error ReportFile(osType, osVersion, accountName, address string, logdata []byte) error
} }

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

View File

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

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

View File

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

View File

@ -0,0 +1,67 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package imap
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestFilterHeader(t *testing.T) {
const header = "To: somebody\r\nFrom: somebody else\r\nSubject: this is\r\n\ta multiline field\r\n\r\n"
assert.Equal(t, "To: somebody\r\n\r\n", string(filterHeaderLines([]byte(header), func(field string) bool {
return strings.EqualFold(field, "To")
})))
assert.Equal(t, "From: somebody else\r\n\r\n", string(filterHeaderLines([]byte(header), func(field string) bool {
return strings.EqualFold(field, "From")
})))
assert.Equal(t, "To: somebody\r\nFrom: somebody else\r\n\r\n", string(filterHeaderLines([]byte(header), func(field string) bool {
return strings.EqualFold(field, "To") || strings.EqualFold(field, "From")
})))
assert.Equal(t, "Subject: this is\r\n\ta multiline field\r\n\r\n", string(filterHeaderLines([]byte(header), func(field string) bool {
return strings.EqualFold(field, "Subject")
})))
}
// TestFilterHeaderNoNewline tests that we don't include a trailing newline when filtering
// if the original header also lacks one (which it can legally do if there is no body).
func TestFilterHeaderNoNewline(t *testing.T) {
const header = "To: somebody\r\nFrom: somebody else\r\nSubject: this is\r\n\ta multiline field\r\n"
assert.Equal(t, "To: somebody\r\n", string(filterHeaderLines([]byte(header), func(field string) bool {
return strings.EqualFold(field, "To")
})))
assert.Equal(t, "From: somebody else\r\n", string(filterHeaderLines([]byte(header), func(field string) bool {
return strings.EqualFold(field, "From")
})))
assert.Equal(t, "To: somebody\r\nFrom: somebody else\r\n", string(filterHeaderLines([]byte(header), func(field string) bool {
return strings.EqualFold(field, "To") || strings.EqualFold(field, "From")
})))
assert.Equal(t, "Subject: this is\r\n\ta multiline field\r\n", string(filterHeaderLines([]byte(header), func(field string) bool {
return strings.EqualFold(field, "Subject")
})))
}

View File

@ -0,0 +1,104 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package imap
import (
"bufio"
"bytes"
"io"
"strings"
"github.com/emersion/go-imap"
"github.com/pkg/errors"
)
func filterHeader(header []byte, section *imap.BodySectionName) []byte {
// Empty section.Fields means BODY[HEADER] was requested so we should return the full header.
if len(section.Fields) == 0 {
return header
}
fieldMap := make(map[string]struct{})
for _, field := range section.Fields {
fieldMap[strings.ToLower(field)] = struct{}{}
}
return filterHeaderLines(header, func(field string) bool {
_, ok := fieldMap[strings.ToLower(field)]
if section.NotFields {
ok = !ok
}
return ok
})
}
func filterHeaderLines(header []byte, wantField func(string) bool) []byte {
var res []byte
for _, line := range headerLines(header) {
if len(bytes.TrimSpace(line)) == 0 {
res = append(res, line...)
} else {
split := bytes.SplitN(line, []byte(": "), 2)
if len(split) != 2 {
continue
}
if wantField(string(bytes.ToLower(split[0]))) {
res = append(res, line...)
}
}
}
return res
}
// NOTE: This sucks because we trim and split stuff here already, only to do it again when we use this function!
func headerLines(header []byte) [][]byte {
var lines [][]byte
r := bufio.NewReader(bytes.NewReader(header))
for {
b, err := r.ReadBytes('\n')
if err != nil {
if err != io.EOF {
panic(errors.Wrap(err, "failed to read header line"))
}
break
}
switch {
case len(bytes.TrimSpace(b)) == 0:
lines = append(lines, b)
case len(bytes.SplitN(b, []byte(": "), 2)) != 2:
lines[len(lines)-1] = append(lines[len(lines)-1], b...)
default:
lines = append(lines, b)
}
}
return lines
}

View File

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

View File

@ -18,7 +18,6 @@
package imap package imap
import ( import (
"errors"
"fmt" "fmt"
"net/mail" "net/mail"
"strings" "strings"
@ -30,6 +29,7 @@ import (
"github.com/ProtonMail/proton-bridge/pkg/parallel" "github.com/ProtonMail/proton-bridge/pkg/parallel"
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/emersion/go-imap" "github.com/emersion/go-imap"
"github.com/pkg/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@ -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,28 @@ func (im *imapMailbox) SearchMessages(isUID bool, criteria *imap.SearchCriteria)
continue continue
} }
} }
// In order to speed up search it is not needed to check if IsFullHeaderCached.
header := storeMessage.GetMIMEHeader()
if !criteria.SentBefore.IsZero() || !criteria.SentSince.IsZero() { if !criteria.SentBefore.IsZero() || !criteria.SentSince.IsZero() {
if t, err := m.Header.Date(); err == nil && !t.IsZero() { t, err := mail.Header(header).Date()
if !criteria.SentBefore.IsZero() { if err != nil || t.IsZero() {
if truncated := criteria.SentBefore.Truncate(24 * time.Hour); t.Unix() > truncated.Unix() { t = time.Unix(m.Time, 0)
continue }
} if !criteria.SentBefore.IsZero() {
if truncated := criteria.SentBefore.Truncate(24 * time.Hour); t.Unix() > truncated.Unix() {
continue
} }
if !criteria.SentSince.IsZero() { }
if truncated := criteria.SentSince.Truncate(24 * time.Hour); t.Unix() < truncated.Unix() { if !criteria.SentSince.IsZero() {
continue if truncated := criteria.SentSince.Truncate(24 * time.Hour); t.Unix() < truncated.Unix() {
} continue
} }
} }
} }
// Filter by headers. // Filter by headers.
header := message.GetHeader(m)
headerMatch := true headerMatch := true
for criteriaKey, criteriaValues := range criteria.Header { for criteriaKey, criteriaValues := range criteria.Header {
for _, criteriaValue := range criteriaValues { for _, criteriaValue := range criteriaValues {
@ -364,6 +387,8 @@ func (im *imapMailbox) SearchMessages(isUID bool, criteria *imap.SearchCriteria)
continue continue
} }
switch criteriaKey { switch criteriaKey {
case "Subject":
headerMatch = strings.Contains(strings.ToLower(m.Subject), strings.ToLower(criteriaValue))
case "From": case "From":
headerMatch = addressMatch([]*mail.Address{m.Sender}, criteriaValue) headerMatch = addressMatch([]*mail.Address{m.Sender}, criteriaValue)
case "To": case "To":
@ -396,7 +421,7 @@ func (im *imapMailbox) SearchMessages(isUID bool, criteria *imap.SearchCriteria)
if isStringInList(m.LabelIDs, pmapi.StarredLabel) { if isStringInList(m.LabelIDs, pmapi.StarredLabel) {
messageFlagsMap[imap.FlaggedFlag] = true messageFlagsMap[imap.FlaggedFlag] = true
} }
if m.Unread == 0 { if !m.Unread {
messageFlagsMap[imap.SeenFlag] = true messageFlagsMap[imap.SeenFlag] = true
} }
if m.Has(pmapi.FlagReplied) || m.Has(pmapi.FlagRepliedAll) { if m.Has(pmapi.FlagReplied) || m.Has(pmapi.FlagRepliedAll) {
@ -463,7 +488,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 {
@ -493,25 +525,13 @@ func (im *imapMailbox) ListMessages(isUID bool, seqSet *imap.SeqSet, items []ima
return err return err
} }
// From RFC: UID range of 559:* always includes the UID of the last message
// in the mailbox, even if 559 is higher than any assigned UID value.
// See: https://tools.ietf.org/html/rfc3501#page-61
if isUID && seqSet.Dynamic() && len(apiIDs) == 0 {
l.Debug("Requesting empty UID dynamic fetch, adding latest message")
apiID, err := im.storeMailbox.GetLatestAPIID()
if err != nil {
return nil
}
apiIDs = []string{apiID}
}
input := make([]interface{}, len(apiIDs)) input := make([]interface{}, len(apiIDs))
for i, apiID := range apiIDs { for i, apiID := range apiIDs {
input[i] = apiID input[i] = apiID
} }
processCallback := func(value interface{}) (interface{}, error) { processCallback := func(value interface{}) (interface{}, error) {
apiID := value.(string) apiID := value.(string) //nolint[forcetypeassert] we want to panic here
storeMessage, err := im.storeMailbox.GetMessage(apiID) storeMessage, err := im.storeMailbox.GetMessage(apiID)
if err != nil { if err != nil {
@ -520,14 +540,14 @@ func (im *imapMailbox) ListMessages(isUID bool, seqSet *imap.SeqSet, items []ima
return nil, err return nil, err
} }
msg, err := im.getMessage(storeMessage, items) msg, err := im.getMessage(storeMessage, items, msgBuildCountHistogram)
if err != nil { if err != nil {
err = fmt.Errorf("list message build: %v", err) err = fmt.Errorf("list message build: %v", err)
l.WithField("metaID", storeMessage.ID()).Error(err) l.WithField("metaID", storeMessage.ID()).Error(err)
return nil, err return nil, err
} }
if storeMessage.Message().Unread == 1 { if storeMessage.Message().Unread {
for section := range msg.Body { for section := range msg.Body {
// Peek means get messages without marking them as read. // Peek means get messages without marking them as read.
// If client does not only ask for peek, we have to mark them as read. // If client does not only ask for peek, we have to mark them as read.
@ -545,12 +565,12 @@ func (im *imapMailbox) ListMessages(isUID bool, seqSet *imap.SeqSet, items []ima
} }
collectCallback := func(idx int, value interface{}) error { collectCallback := func(idx int, value interface{}) error {
msg := value.(*imap.Message) msg := value.(*imap.Message) //nolint[forcetypeassert] we want to panic here
msgResponse <- msg msgResponse <- msg
return nil return nil
} }
err = parallel.RunParallel(fetchMessagesWorkers, input, processCallback, collectCallback) err = parallel.RunParallel(fetchWorkers, input, processCallback, collectCallback)
if err != nil { if err != nil {
return err return err
} }

View File

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

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

View File

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

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