forked from Silverfish/proton-bridge
Compare commits
166 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 91dcb2f773 | |||
| c676c732ab | |||
| 444f2d8a12 | |||
| f10da3c7f0 | |||
| b8dd9f82bd | |||
| 1157e60972 | |||
| e9e4d8c725 | |||
| 186fa24106 | |||
| 63780b7b8d | |||
| e3e4769d78 | |||
| b2e9c4e4e9 | |||
| db4cb36538 | |||
| 984864553e | |||
| 2707a5627c | |||
| 8e0d6d41a6 | |||
| 2b76a45e17 | |||
| 7ab54da8c4 | |||
| 2cce29e858 | |||
| ef1223391b | |||
| fb98a797ba | |||
| 6be31bdeb2 | |||
| fce5990d19 | |||
| 1d4ee0c33e | |||
| a3e102e456 | |||
| 21dcac9fac | |||
| f0ee82fdd2 | |||
| 0c6a098af9 | |||
| 5bf359d34f | |||
| 6d784f2444 | |||
| 835bf1e77f | |||
| df5fbda72f | |||
| c482f768d9 | |||
| 21cf7459c9 | |||
| cf1ba6588a | |||
| 858f2c7f29 | |||
| f63238faed | |||
| f6ff85f69d | |||
| ec5b5939b9 | |||
| dec00ff9cc | |||
| 9fddd77f0d | |||
| aae65c9d38 | |||
| f3b197fa56 | |||
| 0a9ce5f526 | |||
| a2029002c4 | |||
| 7c41c8e23a | |||
| 36fdb88d96 | |||
| c69239ca16 | |||
| e10aa89313 | |||
| d0a97a3f4a | |||
| e01dc77a61 | |||
| 509ba52ba2 | |||
| c37a0338c5 | |||
| 9f23d5a6f4 | |||
| 885fb95454 | |||
| 629d6c5e4d | |||
| 3f50bf66f4 | |||
| 4072205709 | |||
| 233c55ab19 | |||
| 5d82c218ca | |||
| cb30dd91e3 | |||
| 41d82e10f9 | |||
| 8496c9e181 | |||
| 3dadad5131 | |||
| 00146e7474 | |||
| 12ac47e949 | |||
| 6ff4c8a738 | |||
| dd66b7f8d0 | |||
| 0b95ed4dea | |||
| ce64aeb05f | |||
| 27cfda680d | |||
| 323303a98b | |||
| 8109831c07 | |||
| 2284e9ede1 | |||
| 1d538e8540 | |||
| 8ccaac8090 | |||
| 22bf8f62ce | |||
| fed031ebaa | |||
| 7a15ebbd54 | |||
| 94b5799ba7 | |||
| 286f51a4e7 | |||
| ee961ae4a8 | |||
| 4038752a9a | |||
| ebf724412b | |||
| 14d42b5e76 | |||
| 2b8d92e82d | |||
| 11b1e3acf5 | |||
| c5eb660315 | |||
| 5ad23715ec | |||
| 8ab05a000c | |||
| 454d248819 | |||
| 6c8e5f7cd3 | |||
| f5aba717b2 | |||
| 1359c39bc0 | |||
| 4850681f1d | |||
| aa55c69307 | |||
| 1f19d4df75 | |||
| c0f6af9eb5 | |||
| ef6a3d4999 | |||
| 50550d42b4 | |||
| 8db89a1a6c | |||
| ba1dfb1bf4 | |||
| d243880753 | |||
| cccaaa3d82 | |||
| 2d95f21567 | |||
| 7d0af7624c | |||
| 2f35c453a1 | |||
| 05dd137bc8 | |||
| 767628946f | |||
| d4efa7131f | |||
| 144cf6e40c | |||
| a205d8c046 | |||
| cccadaee42 | |||
| bbb365f8a5 | |||
| 1f18d9d917 | |||
| 59e0d63485 | |||
| 72fe5a636e | |||
| 45a83133ba | |||
| 215eb4d6eb | |||
| 479b951c50 | |||
| a94c8a943f | |||
| ea306f405e | |||
| 1b405506b8 | |||
| 38c6132f81 | |||
| b7351dfaf8 | |||
| 7e8f6943f2 | |||
| a0132e8440 | |||
| 27541784aa | |||
| 9e567f08b2 | |||
| bf274f984e | |||
| 3b60bbe13b | |||
| a73a1b623a | |||
| c0a8877018 | |||
| 904166c01c | |||
| 4761bc935a | |||
| 71301d891f | |||
| d47be3c4c0 | |||
| 199a4d1e3a | |||
| 18668aafc9 | |||
| fd73ec6861 | |||
| feeb7179f5 | |||
| 0e5a45671f | |||
| 2beb0d298e | |||
| 22a6fcd87f | |||
| f499252444 | |||
| b27e3fdb28 | |||
| 415e56d928 | |||
| 845074f421 | |||
| 28f46deef9 | |||
| 2a078b76e6 | |||
| 3428557b15 | |||
| 1f25aeab31 | |||
| 4e531d4524 | |||
| 7fc7083c76 | |||
| 0fe69d9de1 | |||
| 8b436186a4 | |||
| 4d000c2376 | |||
| 56bce8e06f | |||
| 6fd614595d | |||
| 7bb7e1a518 | |||
| fb89fb7b31 | |||
| e6ae344f1f | |||
| bad8cad97d | |||
| 77cd2955f1 | |||
| 567b65df8d | |||
| 06b3ed9b85 | |||
| 565c0b6ddf |
3
.gitignore
vendored
3
.gitignore
vendored
@ -26,6 +26,9 @@ internal/frontend/qml/ProtonUI/images
|
||||
internal/frontend/qml/ImportExportUI/images
|
||||
frontend/qml/*.qmlc
|
||||
|
||||
# Credits files (generated).
|
||||
internal/**/credits.go
|
||||
|
||||
# Build files
|
||||
/launcher-*
|
||||
/bridge_*_*.tgz
|
||||
|
||||
@ -187,77 +187,6 @@ build-ie-darwin-qa:
|
||||
paths:
|
||||
- ie_*.tgz
|
||||
|
||||
.build-windows-base:
|
||||
extends: .build-base
|
||||
services:
|
||||
- docker:dind
|
||||
variables:
|
||||
DOCKER_HOST: tcp://docker:2375
|
||||
|
||||
build-windows:
|
||||
extends: .build-windows-base
|
||||
script:
|
||||
# We need to install docker because qtdeploy builds for windows inside a docker container.
|
||||
# Docker will connect to the dockerd daemon provided by the runner service docker:dind at tcp://docker:2375.
|
||||
- curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh
|
||||
- apt-get update && apt-get -y install binutils-mingw-w64 tar gzip
|
||||
- ln -s /usr/bin/x86_64-w64-mingw32-windres /usr/bin/windres
|
||||
- go mod download
|
||||
- TARGET_OS=windows make build
|
||||
artifacts:
|
||||
name: "bridge-windows-$CI_COMMIT_SHORT_SHA"
|
||||
paths:
|
||||
- bridge_*.tgz
|
||||
|
||||
build-windows-qa:
|
||||
extends: .build-windows-base
|
||||
only:
|
||||
- web
|
||||
script:
|
||||
# We need to install docker because qtdeploy builds for windows inside a docker container.
|
||||
# Docker will connect to the dockerd daemon provided by the runner service docker:dind at tcp://docker:2375.
|
||||
- curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh
|
||||
- apt-get update && apt-get -y install binutils-mingw-w64 tar gzip
|
||||
- ln -s /usr/bin/x86_64-w64-mingw32-windres /usr/bin/windres
|
||||
- go mod download
|
||||
- TARGET_OS=windows BUILD_TAGS="build_qa" make build
|
||||
artifacts:
|
||||
name: "bridge-windows-qa-$CI_COMMIT_SHORT_SHA"
|
||||
paths:
|
||||
- bridge_*.tgz
|
||||
|
||||
build-ie-windows:
|
||||
extends: .build-windows-base
|
||||
script:
|
||||
# We need to install docker because qtdeploy builds for windows inside a docker container.
|
||||
# Docker will connect to the dockerd daemon provided by the runner service docker:dind at tcp://docker:2375.
|
||||
- curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh
|
||||
- apt-get update && apt-get -y install binutils-mingw-w64 tar gzip
|
||||
- ln -s /usr/bin/x86_64-w64-mingw32-windres /usr/bin/windres
|
||||
- go mod download
|
||||
- TARGET_OS=windows make build-ie
|
||||
artifacts:
|
||||
name: "ie-windows-$CI_COMMIT_SHORT_SHA"
|
||||
paths:
|
||||
- ie_*.tgz
|
||||
|
||||
build-ie-windows-qa:
|
||||
extends: .build-windows-base
|
||||
only:
|
||||
- web
|
||||
script:
|
||||
# We need to install docker because qtdeploy builds for windows inside a docker container.
|
||||
# Docker will connect to the dockerd daemon provided by the runner service docker:dind at tcp://docker:2375.
|
||||
- curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh
|
||||
- apt-get update && apt-get -y install binutils-mingw-w64 tar gzip
|
||||
- ln -s /usr/bin/x86_64-w64-mingw32-windres /usr/bin/windres
|
||||
- go mod download
|
||||
- TARGET_OS=windows BUILD_TAGS="build_qa" make build-ie
|
||||
artifacts:
|
||||
name: "ie-windows-qa-$CI_COMMIT_SHORT_SHA"
|
||||
paths:
|
||||
- ie_*.tgz
|
||||
|
||||
# Stage: MIRROR
|
||||
|
||||
mirror-repo:
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
---
|
||||
run:
|
||||
timeout: 10m
|
||||
build-tags:
|
||||
@ -8,9 +9,11 @@ run:
|
||||
issues:
|
||||
exclude-use-default: false
|
||||
exclude:
|
||||
- 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.
|
||||
- at least one file in a package should have a package comment # For now we are missing a lot of comments.
|
||||
- Using the variable on range scope `tt` in function literal
|
||||
# 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:
|
||||
- path: _test\.go
|
||||
@ -30,7 +33,7 @@ linters-settings:
|
||||
linters:
|
||||
# setting disable-all will make only explicitly enabled linters run
|
||||
disable-all: true
|
||||
|
||||
|
||||
enable:
|
||||
- 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]
|
||||
@ -49,7 +52,6 @@ linters:
|
||||
- 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]
|
||||
- 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]
|
||||
- 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]
|
||||
@ -58,15 +60,52 @@ linters:
|
||||
- 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]
|
||||
- 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]
|
||||
- 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]
|
||||
- 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]
|
||||
- unconvert # Remove unnecessary type conversions [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]
|
||||
#- wsl # Whitespace Linter - Forces you to use empty lines! [fast: true, auto-fix: false]
|
||||
#- lll # Reports long 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]
|
||||
- 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]
|
||||
|
||||
|
||||
242
Changelog.md
242
Changelog.md
@ -2,6 +2,240 @@
|
||||
|
||||
Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
|
||||
## [Bridge 1.8.9] James
|
||||
|
||||
### Fixed
|
||||
* GODT-1263: Fix crash on invalid or empty header.
|
||||
* GODT-1235: Fix 401 response error handling.
|
||||
* GODT-1261: Fix building messages with long key.
|
||||
* Other: use windows-compatible filename when dumping message in QA builds.
|
||||
|
||||
|
||||
## [Bridge 1.8.8] James
|
||||
|
||||
### Changed
|
||||
* GODT-1234 Set attachment name 'message.eml' for `message/rfc822` attachments.
|
||||
|
||||
|
||||
## [Bridge 1.8.7] James
|
||||
|
||||
### Changed
|
||||
* GODT-1201: Update gopenpgp to 2.1.10.
|
||||
|
||||
### Fixed
|
||||
* GODT-1193: Do not doubly encode parts.
|
||||
|
||||
|
||||
## [Bridge 1.8.6] James
|
||||
|
||||
### Removed
|
||||
* GODT-1187: Remove IMAP/SMTP blocking when no internet.
|
||||
|
||||
### Changed
|
||||
* GODT-1166: Reduce the number of auth for live test.
|
||||
|
||||
### Fixed
|
||||
* GODT-1193: Do not use message.Read permit non-UTF-8 charsets.
|
||||
|
||||
|
||||
## [Bridge 1.8.5] James
|
||||
|
||||
### Fixed
|
||||
* GODT-1189: Draft created on Outlook is synced on web.
|
||||
* GODT-1190: Fix some random crashes of Bridge on Windows.
|
||||
* GODT-1191: Fix data loss of some drafts messages when restarting outlook on Windows.
|
||||
|
||||
## [Bridge 1.8.4] James
|
||||
|
||||
### Added
|
||||
* GODT-1155: Update gopenpgp v2.1.9 and use go-srp.
|
||||
* GODT-1044: Lite parser for appended messages.
|
||||
* GODT-1183: Add test for getting contact emails by email.
|
||||
* GODT-1184: Preserve signatures in externally signed messages.
|
||||
|
||||
### Changed
|
||||
* GODT-949: Ignore some InvalidMediaParameter errors in lite parser.
|
||||
|
||||
### Fixed
|
||||
* GODT-1161: Guarantee order of responses when creating new message.
|
||||
* GODT-1162: Fix wrong section 1 error when email has no MIME parts.
|
||||
|
||||
|
||||
## [Bridge 1.8.3] James
|
||||
|
||||
### Fixed
|
||||
* GODT-1182: Use correct contact route.
|
||||
|
||||
|
||||
## [Bridge 1.8.2] James
|
||||
|
||||
### Fixed
|
||||
* GODT-1175: Bug reporting.
|
||||
|
||||
|
||||
## [Bridge 1.8.1] James
|
||||
|
||||
### Fixed
|
||||
* GODT-1165: Handle UID FETCH with sequence range of empty mailbox.
|
||||
|
||||
|
||||
## [Bridge 1.8.0] James
|
||||
|
||||
### Added
|
||||
* GODT-1056 Check encrypted size of the message before upload.
|
||||
* GODT-1143 Turn off SMTP server while no connection.
|
||||
* GODT-1089 Explicitly open system preferences window on BigSur.
|
||||
* GODT-35: Connection manager with resty.
|
||||
|
||||
### 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.
|
||||
* GODT-876 Set default from if empty for importing draft.
|
||||
* 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
|
||||
|
||||
### Added
|
||||
@ -10,15 +244,15 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
### Changed
|
||||
* 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-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-1033 Retry starting IMAP server after connection was down
|
||||
* 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-1033 Retry starting IMAP server after connection was down.
|
||||
|
||||
### Fixed
|
||||
* 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-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
|
||||
|
||||
86
Makefile
86
Makefile
@ -10,8 +10,8 @@ TARGET_OS?=${GOOS}
|
||||
.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.
|
||||
BRIDGE_APP_VERSION?=1.6.3+git
|
||||
IE_APP_VERSION?=1.3.0+git
|
||||
BRIDGE_APP_VERSION?=1.8.9+git
|
||||
IE_APP_VERSION?=1.3.3+git
|
||||
APP_VERSION:=${BRIDGE_APP_VERSION}
|
||||
SRC_ICO:=logo.ico
|
||||
SRC_ICNS:=Bridge.icns
|
||||
@ -19,6 +19,7 @@ SRC_SVG:=logo.svg
|
||||
TGT_ICNS:=Bridge.icns
|
||||
EXE_NAME:=proton-bridge
|
||||
CONFIGNAME:=bridge
|
||||
WINDRES_DEFINE:=BUILD_BRIDGE
|
||||
ifeq "${TARGET_CMD}" "Import-Export"
|
||||
APP_VERSION:=${IE_APP_VERSION}
|
||||
SRC_ICO:=ie.ico
|
||||
@ -27,13 +28,14 @@ ifeq "${TARGET_CMD}" "Import-Export"
|
||||
TGT_ICNS:=ImportExport.icns
|
||||
EXE_NAME:=proton-ie
|
||||
CONFIGNAME:=importExport
|
||||
WINDRES_DEFINE:=BUILD_IE
|
||||
endif
|
||||
REVISION:=$(shell git rev-parse --short=10 HEAD)
|
||||
BUILD_TIME:=$(shell date +%FT%T%z)
|
||||
|
||||
BUILD_FLAGS:=-tags='${BUILD_TAGS}'
|
||||
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})
|
||||
ifneq "${BUILD_LDFLAGS}" ""
|
||||
GO_LDFLAGS+=${BUILD_LDFLAGS}
|
||||
@ -45,7 +47,7 @@ ifeq "${TARGET_OS}" "windows"
|
||||
endif
|
||||
|
||||
BUILD_FLAGS+=-ldflags '${GO_LDFLAGS}'
|
||||
BUILD_FLAGS_NOGUI+=-ldflags '${GO_LDFLAGS}'
|
||||
BUILD_FLAGS_GUI+=-ldflags '${GO_LDFLAGS}'
|
||||
BUILD_FLAGS_LAUNCHER+=-ldflags '${GO_LDFLAGS_LAUNCHER}'
|
||||
|
||||
DEPLOY_DIR:=cmd/${TARGET_CMD}/deploy
|
||||
@ -56,7 +58,7 @@ EXE_QT:=${DIRNAME}
|
||||
ifeq "${TARGET_OS}" "windows"
|
||||
EXE:=${EXE}.exe
|
||||
EXE_QT:=${EXE_QT}.exe
|
||||
ICO_FILES:=${SRC_ICO} icon.rc icon_windows.syso
|
||||
RESOURCE_FILE:=resource.syso
|
||||
endif
|
||||
ifeq "${TARGET_OS}" "darwin"
|
||||
DARWINAPP_CONTENTS:=${DEPLOY_DIR}/darwin/${EXE}.app/Contents
|
||||
@ -83,14 +85,20 @@ build: ${TGZ_TARGET}
|
||||
build-ie:
|
||||
TARGET_CMD=Import-Export $(MAKE) build
|
||||
|
||||
build-nogui:
|
||||
go build ${BUILD_FLAGS_NOGUI} -o ${EXE_NAME} cmd/${TARGET_CMD}/main.go
|
||||
build-nogui: gofiles
|
||||
go build ${BUILD_FLAGS} -o ${EXE_NAME} cmd/${TARGET_CMD}/main.go
|
||||
|
||||
build-ie-nogui:
|
||||
TARGET_CMD=Import-Export $(MAKE) build-nogui
|
||||
|
||||
build-launcher:
|
||||
go build ${BUILD_FLAGS_LAUNCHER} -o launcher-${APP} cmd/launcher/main.go
|
||||
ifeq "${GOOS}" "windows"
|
||||
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:
|
||||
TARGET_CMD=Import-Export $(MAKE) build-launcher
|
||||
@ -134,21 +142,20 @@ ifneq "${GOOS}" "${TARGET_OS}"
|
||||
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}
|
||||
cp cmd/${TARGET_CMD}/main.go .
|
||||
qtdeploy ${BUILD_FLAGS} ${QT_BUILD_TARGET}
|
||||
qtdeploy ${BUILD_FLAGS_GUI} ${QT_BUILD_TARGET}
|
||||
mv deploy cmd/${TARGET_CMD}
|
||||
if [ "${EXE_QT_TARGET}" != "${EXE_TARGET}" ]; then mv ${EXE_QT_TARGET} ${EXE_TARGET}; fi
|
||||
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
|
||||
.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
|
||||
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.
|
||||
# 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)
|
||||
|
||||
## Dev dependencies
|
||||
.PHONY: install-devel-tools install-linter install-go-mod-outdated
|
||||
LINTVER:="v1.29.0"
|
||||
.PHONY: install-devel-tools install-linter install-go-mod-outdated install-git-hooks
|
||||
LINTVER:="v1.39.0"
|
||||
LINTSRC:="https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh"
|
||||
|
||||
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:
|
||||
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
|
||||
.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
|
||||
go tool cover -html=/tmp/coverage.out -o=coverage.html
|
||||
|
||||
mocks:
|
||||
mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/users Locator,PanicHandler,ClientManager,CredentialsStorer,StoreMaker > internal/users/mocks/mocks.go
|
||||
mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/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
|
||||
integration-test-bridge:
|
||||
${MAKE} -C test test-bridge
|
||||
|
||||
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:
|
||||
./utils/missing_license.sh check
|
||||
|
||||
lint-changelog:
|
||||
./utils/changelog_linter.sh Changelog.md
|
||||
./utils/changelog_linter.sh unreleased.md
|
||||
|
||||
lint-golang:
|
||||
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_SMTP?=--log-smtp # empty to turn it off
|
||||
RUN_FLAGS?=-m -l=${LOG} --log-imap=${LOG_IMAP} ${LOG_SMTP}
|
||||
RUN_FLAGS_IE?=-m -l=${LOG}
|
||||
|
||||
run: run-nogui-cli
|
||||
|
||||
@ -301,12 +317,12 @@ run-qt-cli: ${EXE_TARGET}
|
||||
PROTONMAIL_ENV=dev ./$< ${RUN_FLAGS} -c
|
||||
|
||||
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
|
||||
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:
|
||||
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:
|
||||
$(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
|
||||
|
||||
run-ie:
|
||||
TARGET_CMD=Import-Export $(MAKE) run
|
||||
TARGET_CMD=Import-Export RUN_FLAGS="${RUN_FLAGS_IE}" $(MAKE) run
|
||||
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:
|
||||
TARGET_CMD=Import-Export $(MAKE) run-nogui
|
||||
TARGET_CMD=Import-Export RUN_FLAGS="${RUN_FLAGS_IE}" $(MAKE) run-nogui
|
||||
|
||||
clean-frontend-qt:
|
||||
$(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/Import-Export/deploy
|
||||
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/import-export.html
|
||||
|
||||
@ -343,3 +359,5 @@ clean: clean-vendor
|
||||
generate:
|
||||
go generate ./...
|
||||
$(MAKE) add-license
|
||||
|
||||
.FORCE:
|
||||
|
||||
@ -25,13 +25,14 @@ import (
|
||||
"runtime"
|
||||
|
||||
"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/crash"
|
||||
"github.com/ProtonMail/proton-bridge/internal/locations"
|
||||
"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/versioner"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/sentry"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
@ -44,7 +45,7 @@ var (
|
||||
)
|
||||
|
||||
func main() { // nolint[funlen]
|
||||
reporter := sentry.NewReporter(appName, constants.Version)
|
||||
reporter := sentry.NewReporter(appName, constants.Version, useragent.New())
|
||||
|
||||
crashHandler := crash.NewHandler(reporter.ReportException)
|
||||
defer crashHandler.HandlePanic()
|
||||
|
||||
30
go.mod
30
go.mod
@ -1,24 +1,25 @@
|
||||
module github.com/ProtonMail/proton-bridge
|
||||
|
||||
go 1.13
|
||||
go 1.15
|
||||
|
||||
// These dependencies are `replace`d below, so the version numbers should be ignored.
|
||||
// They are in a separate require block to highlight this.
|
||||
require (
|
||||
github.com/docker/docker-credential-helpers v0.6.3
|
||||
github.com/emersion/go-imap v1.0.6-0.20200708083111-011063d6c9df
|
||||
github.com/jameskeane/bcrypt v0.0.0-20170924085257-7509ea014998
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
|
||||
github.com/emersion/go-imap v1.0.6
|
||||
github.com/jameskeane/bcrypt v0.0.0-20170924085257-7509ea014998 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/0xAX/notificator v0.0.0-20191016112426-3962a5ea8da1
|
||||
github.com/Masterminds/semver/v3 v3.1.0
|
||||
github.com/ProtonMail/go-autostart v0.0.0-20181114175602-c5272053443a
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7
|
||||
github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde
|
||||
github.com/ProtonMail/go-rfc5322 v0.5.0
|
||||
github.com/ProtonMail/go-rfc5322 v0.8.0
|
||||
github.com/ProtonMail/go-srp v0.0.0-20210514134713-bd9454f3fa01
|
||||
github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.1.3
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.1.10
|
||||
github.com/PuerkitoBio/goquery v1.5.1
|
||||
github.com/abiosoft/ishell v2.0.0+incompatible
|
||||
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db // indirect
|
||||
@ -27,9 +28,8 @@ require (
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect
|
||||
github.com/cucumber/godog v0.8.1
|
||||
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-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-mbox v1.0.2
|
||||
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/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
|
||||
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/google/go-cmp v0.5.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/logrusorgru/aurora v2.0.3+incompatible
|
||||
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/olekukonko/tablewriter v0.0.4 // indirect
|
||||
github.com/pkg/errors v0.9.1
|
||||
@ -59,16 +59,20 @@ require (
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
|
||||
github.com/stretchr/testify v1.6.1
|
||||
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/vmihailenco/msgpack/v5 v5.1.3
|
||||
go.etcd.io/bbolt v1.3.5
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4
|
||||
golang.org/x/text v0.3.5-0.20201125200606-c27b9fd57aec
|
||||
|
||||
)
|
||||
|
||||
replace (
|
||||
github.com/docker/docker-credential-helpers => github.com/ProtonMail/docker-credential-helpers v1.1.0
|
||||
github.com/emersion/go-imap => github.com/ProtonMail/go-imap v0.0.0-20201228133358-4db68cea0cac
|
||||
github.com/jameskeane/bcrypt => github.com/ProtonMail/bcrypt v0.0.0-20170924085257-7509ea014998
|
||||
golang.org/x/crypto => github.com/ProtonMail/crypto v0.0.0-20201112115411-41db4ea0dd1c
|
||||
github.com/emersion/go-message => github.com/ProtonMail/go-message v0.0.0-20210611055058-fabeff2ec753
|
||||
github.com/jameskeane/bcrypt => github.com/ProtonMail/bcrypt v0.0.0-20210511135022-227b4adcab57
|
||||
)
|
||||
|
||||
81
go.sum
81
go.sum
@ -8,28 +8,30 @@ github.com/CloudyKit/jet/v3 v3.0.0/go.mod h1:HKQPgSJmdK8hdoAbKUUWajkHyHo4RaU5rMd
|
||||
github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY=
|
||||
github.com/Masterminds/semver/v3 v3.1.0 h1:Y2lUDsFKVRSYGojLJ1yLxSXdMmMYTYls0rCvoqmMUQk=
|
||||
github.com/Masterminds/semver/v3 v3.1.0/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
|
||||
github.com/ProtonMail/bcrypt v0.0.0-20170924085257-7509ea014998 h1:YT2uVwQiRQZxCaaahwfcgTq2j3j66w00n/27gb/zubs=
|
||||
github.com/ProtonMail/bcrypt v0.0.0-20170924085257-7509ea014998/go.mod h1:HecWFHognK8GfRDGnFQbW/LiV7A3MX3gZVs45vk5h8I=
|
||||
github.com/ProtonMail/crypto v0.0.0-20201112115411-41db4ea0dd1c h1:iaVbEOnskSGgcH7XQWHG6VPirHDRoYe+Idd0/dl4m8A=
|
||||
github.com/ProtonMail/crypto v0.0.0-20201112115411-41db4ea0dd1c/go.mod h1:Pxr7w4gA2ikI4sWyYwEffm+oew1WAJHzG1SiDpQMkrI=
|
||||
github.com/ProtonMail/bcrypt v0.0.0-20210511135022-227b4adcab57 h1:pHA4K54ifoogVLunGGHi3xyF5Nz4x+Uh3dJuy3NwGQQ=
|
||||
github.com/ProtonMail/bcrypt v0.0.0-20210511135022-227b4adcab57/go.mod h1:HecWFHognK8GfRDGnFQbW/LiV7A3MX3gZVs45vk5h8I=
|
||||
github.com/ProtonMail/docker-credential-helpers v1.1.0 h1:+kvUIpwWcbtP3WFv5sSvkFn/XLzSqPOB5AAthuk9xPk=
|
||||
github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g=
|
||||
github.com/ProtonMail/go-autostart v0.0.0-20181114175602-c5272053443a h1:fXK2KsfnkBV9Nh+9SKzHchYjuE9s0vI20JG1mbtEAcc=
|
||||
github.com/ProtonMail/go-autostart v0.0.0-20181114175602-c5272053443a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20201208171014-cdb7591792e2 h1:pQkjJELHayW59jp7r4G5Dlmnicr5McejDfwsjcwI1SU=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20201208171014-cdb7591792e2/go.mod h1:HTM9X7e9oLwn7RiqLG0UVwVRJenLs3wN+tQ0NPAfwMQ=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7 h1:YoJbenK9C67SkzkDfmQuVln04ygHj3vjZfd9FL+GmQQ=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo=
|
||||
github.com/ProtonMail/go-imap v0.0.0-20201228133358-4db68cea0cac h1:2xU3QncAiS/W3UlWZTkbNKW5WkLzk6Egl1T0xX+sbjs=
|
||||
github.com/ProtonMail/go-imap v0.0.0-20201228133358-4db68cea0cac/go.mod h1:yKASt+C3ZiDAiCSssxg9caIckWF/JG7ZQTO7GAmvicU=
|
||||
github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde h1:5koQozTDELymYOyFbQ/VSubexAEXzDR8qGM5mO8GRdw=
|
||||
github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde/go.mod h1:795VPXcRUIQ9JyMNHP4el582VokQfippgjkQP3Gk0r0=
|
||||
github.com/ProtonMail/go-message v0.0.0-20210611055058-fabeff2ec753 h1:I8IsYA297x0QLU80G5I6aLYUu3JYNSpo8j5fkXtFDW0=
|
||||
github.com/ProtonMail/go-message v0.0.0-20210611055058-fabeff2ec753/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4=
|
||||
github.com/ProtonMail/go-mime v0.0.0-20190923161245-9b5a4261663a h1:W6RrgN/sTxg1msqzFFb+G80MFmpjMw61IU+slm+wln4=
|
||||
github.com/ProtonMail/go-mime v0.0.0-20190923161245-9b5a4261663a/go.mod h1:NYt+V3/4rEeDuaev/zw1zCq8uqVEuPHzDPo3OZrlGJ4=
|
||||
github.com/ProtonMail/go-rfc5322 v0.5.0 h1:LbKWjgfvumYZCr8BgGyTUk3ETGkFLAjQdkuSUpZ5CcE=
|
||||
github.com/ProtonMail/go-rfc5322 v0.5.0/go.mod h1:mzZWlMWnQJuYLL7JpzuPF5+FimV2lZ9f0jeq24kJjpU=
|
||||
github.com/ProtonMail/go-rfc5322 v0.8.0 h1:7emrf75n3CDIduQflx7aT1nJa5h/kGsiFKUYX/+IAkU=
|
||||
github.com/ProtonMail/go-rfc5322 v0.8.0/go.mod h1:BwpTbkJxkMGkc+pC84AXZnwuWOisEULBpfPIyIKS/Us=
|
||||
github.com/ProtonMail/go-srp v0.0.0-20210514134713-bd9454f3fa01 h1:sRxNvPGnJFh6yWlSr9BpGsSrshFkZLClSm5oIi++a0I=
|
||||
github.com/ProtonMail/go-srp v0.0.0-20210514134713-bd9454f3fa01/go.mod h1:jOXzdvWTILIJzl83yzi/EZcnnhpI+A/5EyflaeVfi/0=
|
||||
github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5 h1:Uga1DHFN4GUxuDQr0F71tpi8I9HqPIlZodZAI1lR6VQ=
|
||||
github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5/go.mod h1:oeP9CMN+ajWp5jKp1kue5daJNwMMxLF+ujPaUIoJWlA=
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.1.3 h1:4+nFDJ9WtcUQTip/je2Ll3P21XhAUl4asWsafLrw97c=
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.1.3/go.mod h1:WeYndoqEcRR4/QbgRL24z6OwYX5T1RWerRk8NfZ6rJM=
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.1.10 h1:WPwpzVQFvzzFIzHCsJ1RzuVkN4JsMIydl/0vc05yU0E=
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.1.10/go.mod h1:CHIXesUdnPxIxtJTg2P/cxoA0cvUwIBpZIS8SsY82QA=
|
||||
github.com/PuerkitoBio/goquery v1.5.1 h1:PSPBGne8NIUWw+/7vFBV+kG2J/5MOjbzc7154OaKCSE=
|
||||
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
|
||||
github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0=
|
||||
@ -73,25 +75,19 @@ github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25Kn
|
||||
github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM=
|
||||
github.com/emersion/go-imap-appendlimit v0.0.0-20190308131241-25671c986a6a h1:bMdSPm6sssuOFpIaveu3XGAijMS3Tq2S3EqFZmZxidc=
|
||||
github.com/emersion/go-imap-appendlimit v0.0.0-20190308131241-25671c986a6a/go.mod h1:ikgISoP7pRAolqsVP64yMteJa2FIpS6ju88eBT6K1yQ=
|
||||
github.com/emersion/go-imap-idle v0.0.0-20200601154248-f05f54664cc4 h1:/JIALzmCduf5o8TWJSiOBzTb9+R0SChwElUrJLlp2po=
|
||||
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/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-20200423100218-dcfd1b7d2b41/go.mod h1:iApyhIQBiU4XFyr+3kdJyyGqle82TbQyuP2o+OZHrV0=
|
||||
github.com/emersion/go-imap-quota v0.0.0-20210203125329-619074823f3c h1:khcEdu1yFiZjBgi7gGnQiLhpSgghJ0YTnKD0l4EUqqc=
|
||||
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/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/go.mod h1:Yp9IVuuOYLEuMv4yjgDHvhb5mHOcYH6x92Oas3QqEZI=
|
||||
github.com/emersion/go-message v0.11.1/go.mod h1:C4jnca5HOTo4bGN9YdqNQM9sITuT3Y0K6bSUw9RklvY=
|
||||
github.com/emersion/go-message v0.12.1-0.20201221184100-40c3f864532b h1:xYuhW6egTaCP+zjbUcfoy/Dr3ASdVPR9W7fmkHvZHPE=
|
||||
github.com/emersion/go-message v0.12.1-0.20201221184100-40c3f864532b/go.mod h1:N1JWdZQ2WRUalmdHAX308CWBq747VJ8oUorFI3VCBwU=
|
||||
github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k=
|
||||
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ=
|
||||
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
||||
github.com/emersion/go-smtp v0.14.0 h1:RYW203p+EcPjL8Z/ZpT9lZ6iOc8MG1MQzEx1UKEkXlA=
|
||||
github.com/emersion/go-smtp v0.14.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
|
||||
github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
|
||||
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwoTQyw0fIM5xv1HF+Y+3ZijDR839WMulgxCcUY=
|
||||
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
|
||||
github.com/emersion/go-vcard v0.0.0-20190105225839-8856043f13c5 h1:n9qx98xiS5V4x2WIpPC2rr9mUM5ri9r/YhCEKbhCHro=
|
||||
@ -113,8 +109,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/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||
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.3.0/go.mod h1:UpN9CgLZNsv4e9XG50UU8xdI0F43UQ4HmxLBDwaroHU=
|
||||
github.com/go-resty/resty/v2 v2.6.0 h1:joIR5PNLM2EFqqESUjCMGXrWmXNHEU9CEiK813oKYS4=
|
||||
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/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
|
||||
@ -178,9 +174,6 @@ github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL
|
||||
github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8=
|
||||
github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/martinlindhe/base36 v1.0.0/go.mod h1:+AtEs8xrBpCeYgSLoY/aJ6Wf37jtBuR0s35750M27+8=
|
||||
github.com/martinlindhe/base36 v1.1.0 h1:cIwvvwYse/0+1CkUPYH5ZvVIYG3JrILmQEIbLuar02Y=
|
||||
github.com/martinlindhe/base36 v1.1.0/go.mod h1:+AtEs8xrBpCeYgSLoY/aJ6Wf37jtBuR0s35750M27+8=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
@ -195,8 +188,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/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/miekg/dns v1.1.30 h1:Qww6FseFn8PRfw07jueqIXqodm0JKiiKuK0DeXSqfyo=
|
||||
github.com/miekg/dns v1.1.30/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
|
||||
github.com/miekg/dns v1.1.41 h1:WMszZWJG0XmzbK9FEmzH2TVcqYzFesusSIB41b8KHxY=
|
||||
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/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
@ -262,6 +255,10 @@ 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/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/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.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
@ -287,10 +284,21 @@ github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDf
|
||||
github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc=
|
||||
go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0=
|
||||
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190418165655-df01cb2cc480/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w=
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190910184405-b558ed863381/go.mod h1:p895TfNkDgPEmEQrNiOtIl3j98d/tGU95djDj7NfyjQ=
|
||||
golang.org/x/mobile v0.0.0-20200801112145-973feb4309de/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
@ -305,34 +313,37 @@ 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-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-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-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-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
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-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/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-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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/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-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-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-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
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-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.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/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@ -342,8 +353,8 @@ golang.org/x/tools v0.0.0-20190327201419-c70d86f8b7cf/go.mod h1:LCzVGOaR6xXOjkQ3
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/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-20190909214602-067311248421/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/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
||||
@ -23,7 +23,7 @@
|
||||
// - persistent settings
|
||||
// - event listener
|
||||
// - credentials store
|
||||
// - pmapi ClientManager
|
||||
// - pmapi Manager
|
||||
// In addition, the base initialises logging and reacts to command line arguments
|
||||
// which control the log verbosity and enable cpu/memory profiling.
|
||||
package base
|
||||
@ -43,38 +43,55 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/internal/config/cache"
|
||||
"github.com/ProtonMail/proton-bridge/internal/config/settings"
|
||||
"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/cookies"
|
||||
"github.com/ProtonMail/proton-bridge/internal/crash"
|
||||
"github.com/ProtonMail/proton-bridge/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/internal/locations"
|
||||
"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/users/credentials"
|
||||
"github.com/ProtonMail/proton-bridge/internal/versioner"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/keychain"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/listener"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/sentry"
|
||||
"github.com/allan-simon/go-singleinstance"
|
||||
"github.com/sirupsen/logrus"
|
||||
"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 {
|
||||
CrashHandler *crash.Handler
|
||||
Locations *locations.Locations
|
||||
Settings *settings.Settings
|
||||
Lock *os.File
|
||||
Cache *cache.Cache
|
||||
Listener listener.Listener
|
||||
Creds *credentials.Store
|
||||
CM *pmapi.ClientManager
|
||||
CookieJar *cookies.Jar
|
||||
Updater *updater.Updater
|
||||
Versioner *versioner.Versioner
|
||||
TLS *tls.TLS
|
||||
Autostart *autostart.App
|
||||
SentryReporter *sentry.Reporter
|
||||
CrashHandler *crash.Handler
|
||||
Locations *locations.Locations
|
||||
Settings *settings.Settings
|
||||
Lock *os.File
|
||||
Cache *cache.Cache
|
||||
Listener listener.Listener
|
||||
Creds *credentials.Store
|
||||
CM pmapi.Manager
|
||||
CookieJar *cookies.Jar
|
||||
UserAgent *useragent.UserAgent
|
||||
Updater *updater.Updater
|
||||
Versioner *versioner.Versioner
|
||||
TLS *tls.TLS
|
||||
Autostart *autostart.App
|
||||
|
||||
Name string // the app's name
|
||||
usage string // the app's usage description
|
||||
@ -92,7 +109,10 @@ func New( // nolint[funlen]
|
||||
keychainName,
|
||||
cacheVersion string,
|
||||
) (*Base, error) {
|
||||
sentryReporter := sentry.NewReporter(appName, constants.Version)
|
||||
userAgent := useragent.New()
|
||||
|
||||
sentryReporter := sentry.NewReporter(appName, constants.Version, userAgent)
|
||||
|
||||
crashHandler := crash.NewHandler(
|
||||
sentryReporter.ReportException,
|
||||
crash.ShowErrorNotification(appName),
|
||||
@ -161,25 +181,24 @@ func New( // nolint[funlen]
|
||||
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)
|
||||
if err != nil {
|
||||
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)
|
||||
sentryReporter.SetUserAgentProvider(cm)
|
||||
|
||||
key, err := crypto.NewKeyFromArmored(updater.DefaultPublicKey)
|
||||
if err != nil {
|
||||
@ -220,19 +239,21 @@ func New( // nolint[funlen]
|
||||
}
|
||||
|
||||
return &Base{
|
||||
CrashHandler: crashHandler,
|
||||
Locations: locations,
|
||||
Settings: settingsObj,
|
||||
Lock: lock,
|
||||
Cache: cache,
|
||||
Listener: listener,
|
||||
Creds: credentials.NewStore(kc),
|
||||
CM: cm,
|
||||
CookieJar: jar,
|
||||
Updater: updater,
|
||||
Versioner: versioner,
|
||||
TLS: tls.New(settingsPath),
|
||||
Autostart: autostart,
|
||||
SentryReporter: sentryReporter,
|
||||
CrashHandler: crashHandler,
|
||||
Locations: locations,
|
||||
Settings: settingsObj,
|
||||
Lock: lock,
|
||||
Cache: cache,
|
||||
Listener: listener,
|
||||
Creds: credentials.NewStore(kc),
|
||||
CM: cm,
|
||||
CookieJar: jar,
|
||||
UserAgent: userAgent,
|
||||
Updater: updater,
|
||||
Versioner: versioner,
|
||||
TLS: tls.New(settingsPath),
|
||||
Autostart: autostart,
|
||||
|
||||
Name: appName,
|
||||
usage: appUsage,
|
||||
@ -252,32 +273,32 @@ func (b *Base) NewApp(action func(*Base, *cli.Context) error) *cli.App {
|
||||
app.Action = b.run(action)
|
||||
app.Flags = []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "cpu-prof",
|
||||
Aliases: []string{"p"},
|
||||
Name: flagCPUProfile,
|
||||
Aliases: []string{flagCPUProfileShort},
|
||||
Usage: "Generate CPU profile",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "mem-prof",
|
||||
Aliases: []string{"m"},
|
||||
Name: flagMemProfile,
|
||||
Aliases: []string{flagMemProfileShort},
|
||||
Usage: "Generate memory profile",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "log-level",
|
||||
Aliases: []string{"l"},
|
||||
Name: flagLogLevel,
|
||||
Aliases: []string{flagLogLevelShort},
|
||||
Usage: "Set the log level (one of panic, fatal, error, warn, info, debug)",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "cli",
|
||||
Aliases: []string{"c"},
|
||||
Name: FlagCLI,
|
||||
Aliases: []string{flagCLIShort},
|
||||
Usage: "Use command line interface",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "restart",
|
||||
Name: flagRestart,
|
||||
Usage: "The number of times the application has already restarted",
|
||||
Hidden: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "launcher",
|
||||
Name: flagLauncher,
|
||||
Usage: "The launcher to use to restart the application",
|
||||
Hidden: true,
|
||||
},
|
||||
@ -302,21 +323,22 @@ func (b *Base) run(appMainLoop func(*Base, *cli.Context) error) cli.ActionFunc {
|
||||
defer func() { _ = b.Lock.Close() }()
|
||||
|
||||
// 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.command = launcher
|
||||
}
|
||||
|
||||
if doCPUProfile := c.Bool("cpu-prof"); doCPUProfile {
|
||||
if c.Bool(flagCPUProfile) {
|
||||
startCPUProfile()
|
||||
defer pprof.StopCPUProfile()
|
||||
}
|
||||
|
||||
if doMemoryProfile := c.Bool("mem-prof"); doMemoryProfile {
|
||||
if c.Bool(flagMemProfile) {
|
||||
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.
|
||||
WithField("appName", b.Name).
|
||||
@ -328,7 +350,7 @@ func (b *Base) run(appMainLoop func(*Base, *cli.Context) error) cli.ActionFunc {
|
||||
Info("Run app")
|
||||
|
||||
b.CrashHandler.AddRecoveryAction(func(interface{}) error {
|
||||
if c.Int("restart") > maxAllowedRestarts {
|
||||
if c.Int(flagRestart) > maxAllowedRestarts {
|
||||
logrus.
|
||||
WithField("restart", c.Int("restart")).
|
||||
Warn("Not restarting, already restarted too many times")
|
||||
|
||||
@ -29,10 +29,12 @@ import (
|
||||
// migrateFiles migrates files from their old (pre-refactor) locations to their new locations.
|
||||
// We can remove this eventually.
|
||||
//
|
||||
// | entity | old location | new location |
|
||||
// |--------|-------------------------------------------|----------------------------------------|
|
||||
// | prefs | ~/.cache/protonmail/<app>/c11/prefs.json | ~/.config/protonmail/<app>/prefs.json |
|
||||
// | c11 | ~/.cache/protonmail/<app>/c11 | ~/.cache/protonmail/<app>/cache/c11 |
|
||||
// | entity | old location | new location |
|
||||
// |-----------|-------------------------------------------|----------------------------------------|
|
||||
// | prefs | ~/.cache/protonmail/<app>/c11/prefs.json | ~/.config/protonmail/<app>/prefs.json |
|
||||
// | c11 1.5.x | ~/.cache/protonmail/<app>/c11 | ~/.cache/protonmail/<app>/cache/c11 |
|
||||
// | c11 1.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 {
|
||||
locationsProvider, err := locations.NewDefaultProvider(filepath.Join(constants.VendorName, configName))
|
||||
if err != nil {
|
||||
@ -41,43 +43,89 @@ func migrateFiles(configName string) error {
|
||||
|
||||
locations := locations.New(locationsProvider, configName)
|
||||
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()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := moveIfExists(
|
||||
return moveIfExists(
|
||||
filepath.Join(userCacheDir, "c11", "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 {
|
||||
return err
|
||||
}
|
||||
|
||||
// Migration for versions before 1.6.x.
|
||||
if err := moveIfExists(
|
||||
filepath.Join(userCacheDir, "c11"),
|
||||
filepath.Join(newCacheDir, "c11"),
|
||||
filepath.Join(olderCacheDir, "c11"),
|
||||
filepath.Join(latestCacheDir, "c11"),
|
||||
); err != nil {
|
||||
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 {
|
||||
l := logrus.WithField("source", source).WithField("destination", destination)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
l.Info("Migrating files")
|
||||
return os.Rename(source, destination)
|
||||
}
|
||||
|
||||
@ -38,21 +38,28 @@ import (
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
flagLogIMAP = "log-imap"
|
||||
flagLogSMTP = "log-smtp"
|
||||
flagNoWindow = "no-window"
|
||||
flagNonInteractive = "noninteractive"
|
||||
)
|
||||
|
||||
func New(base *base.Base) *cli.App {
|
||||
app := base.NewApp(run)
|
||||
|
||||
app.Flags = append(app.Flags, []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "log-imap",
|
||||
Name: flagLogIMAP,
|
||||
Usage: "Enable logging of IMAP communications (all|client|server) (may contain decrypted data!)"},
|
||||
&cli.BoolFlag{
|
||||
Name: "log-smtp",
|
||||
Name: flagLogSMTP,
|
||||
Usage: "Enable logging of SMTP communications (may contain decrypted data!)"},
|
||||
&cli.BoolFlag{
|
||||
Name: "no-window",
|
||||
Name: flagNoWindow,
|
||||
Usage: "Don't show window after start"},
|
||||
&cli.BoolFlag{
|
||||
Name: "noninteractive",
|
||||
Name: flagNonInteractive,
|
||||
Usage: "Start Bridge entirely noninteractively"},
|
||||
}...)
|
||||
|
||||
@ -64,8 +71,7 @@ func run(b *base.Base, c *cli.Context) error { // nolint[funlen]
|
||||
if err != nil {
|
||||
logrus.WithError(err).Fatal("Failed to load TLS config")
|
||||
}
|
||||
|
||||
bridge := bridge.New(b.Locations, b.Cache, b.Settings, b.CrashHandler, b.Listener, b.CM, b.Creds, b.Updater, b.Versioner)
|
||||
bridge := bridge.New(b.Locations, b.Cache, b.Settings, b.SentryReporter, b.CrashHandler, b.Listener, b.CM, b.Creds, b.Updater, b.Versioner)
|
||||
imapBackend := imap.NewIMAPBackend(b.CrashHandler, b.Listener, b.Cache, 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)
|
||||
imap.NewIMAPServer(
|
||||
b.CrashHandler,
|
||||
c.String("log-imap") == "client" || c.String("log-imap") == "all",
|
||||
c.String("log-imap") == "server" || c.String("log-imap") == "all",
|
||||
imapPort, tlsConfig, imapBackend, b.Listener).ListenAndServe()
|
||||
c.String(flagLogIMAP) == "client" || c.String(flagLogIMAP) == "all",
|
||||
c.String(flagLogIMAP) == "server" || c.String(flagLogIMAP) == "all",
|
||||
imapPort, tlsConfig, imapBackend, b.UserAgent, b.Listener).ListenAndServe()
|
||||
}()
|
||||
|
||||
go func() {
|
||||
@ -89,12 +95,13 @@ func run(b *base.Base, c *cli.Context) error { // nolint[funlen]
|
||||
smtpPort := b.Settings.GetInt(settings.SMTPPortKey)
|
||||
useSSL := b.Settings.GetBool(settings.SMTPSSLKey)
|
||||
smtp.NewSMTPServer(
|
||||
c.Bool("log-smtp"),
|
||||
b.CrashHandler,
|
||||
c.Bool(flagLogSMTP),
|
||||
smtpPort, useSSL, tlsConfig, smtpBackend, b.Listener).ListenAndServe()
|
||||
}()
|
||||
|
||||
// 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.
|
||||
b.AddTeardownAction(b.Versioner.RemoveOldVersions)
|
||||
@ -105,9 +112,9 @@ func run(b *base.Base, c *cli.Context) error { // nolint[funlen]
|
||||
var frontendMode string
|
||||
|
||||
switch {
|
||||
case c.Bool("cli"):
|
||||
case c.Bool(base.FlagCLI):
|
||||
frontendMode = "cli"
|
||||
case c.Bool("noninteractive"):
|
||||
case c.Bool(flagNonInteractive):
|
||||
return <-(make(chan error)) // Block forever.
|
||||
default:
|
||||
frontendMode = "qt"
|
||||
@ -118,12 +125,13 @@ func run(b *base.Base, c *cli.Context) error { // nolint[funlen]
|
||||
constants.BuildVersion,
|
||||
b.Name,
|
||||
frontendMode,
|
||||
!c.Bool("no-window"),
|
||||
!c.Bool(flagNoWindow),
|
||||
b.CrashHandler,
|
||||
b.Locations,
|
||||
b.Settings,
|
||||
b.Listener,
|
||||
b.Updater,
|
||||
b.UserAgent,
|
||||
bridge,
|
||||
smtpBackend,
|
||||
b.Autostart,
|
||||
@ -132,7 +140,7 @@ func run(b *base.Base, c *cli.Context) error { // nolint[funlen]
|
||||
|
||||
// Watch for updates routine
|
||||
go func() {
|
||||
ticker := time.NewTicker(time.Hour)
|
||||
ticker := time.NewTicker(constants.UpdateCheckInterval)
|
||||
|
||||
for {
|
||||
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) {
|
||||
log := logrus.WithField("pkg", "app/bridge")
|
||||
version, err := u.Check()
|
||||
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
|
||||
}
|
||||
|
||||
@ -194,11 +203,11 @@ func checkAndHandleUpdate(u types.Updater, f frontend.Frontend, autoUpdate bool)
|
||||
f.SetVersion(version)
|
||||
|
||||
if !u.IsUpdateApplicable(version) {
|
||||
logrus.Debug("No need to update")
|
||||
log.Info("No need to update")
|
||||
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))
|
||||
@ -206,16 +215,16 @@ func checkAndHandleUpdate(u types.Updater, f frontend.Frontend, autoUpdate bool)
|
||||
}
|
||||
|
||||
if !u.CanInstall(version) {
|
||||
logrus.Info("A manual update is required")
|
||||
log.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")
|
||||
log.WithError(err).Warning("Skipping update installation due to temporary error")
|
||||
} else {
|
||||
logrus.WithError(err).Error("The update couldn't be installed")
|
||||
log.WithError(err).Error("The update couldn't be installed")
|
||||
f.NotifySilentUpdateError(err)
|
||||
}
|
||||
|
||||
|
||||
@ -28,8 +28,6 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/internal/frontend"
|
||||
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
|
||||
"github.com/ProtonMail/proton-bridge/internal/importexport"
|
||||
"github.com/ProtonMail/proton-bridge/internal/updater"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@ -49,7 +47,7 @@ func run(b *base.Base, c *cli.Context) error {
|
||||
var frontendMode string
|
||||
|
||||
switch {
|
||||
case c.Bool("cli"):
|
||||
case c.Bool(base.FlagCLI):
|
||||
frontendMode = "cli"
|
||||
default:
|
||||
frontendMode = "qt"
|
||||
@ -88,10 +86,11 @@ func run(b *base.Base, c *cli.Context) error {
|
||||
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()
|
||||
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
|
||||
}
|
||||
|
||||
@ -101,33 +100,11 @@ func checkAndHandleUpdate(u types.Updater, f frontend.Frontend, autoUpdate bool)
|
||||
f.SetVersion(version)
|
||||
|
||||
if !u.IsUpdateApplicable(version) {
|
||||
logrus.Debug("No need to update")
|
||||
log.Info("No need to update")
|
||||
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))
|
||||
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()
|
||||
f.NotifyManualUpdate(version, u.CanInstall(version))
|
||||
}
|
||||
|
||||
@ -19,6 +19,7 @@
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
@ -26,6 +27,7 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/internal/config/settings"
|
||||
"github.com/ProtonMail/proton-bridge/internal/constants"
|
||||
"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/users"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
@ -43,22 +45,19 @@ type Bridge struct {
|
||||
|
||||
locations Locator
|
||||
settings SettingsProvider
|
||||
clientManager users.ClientManager
|
||||
clientManager pmapi.Manager
|
||||
updater Updater
|
||||
versioner Versioner
|
||||
|
||||
userAgentClientName string
|
||||
userAgentClientVersion string
|
||||
userAgentOS string
|
||||
}
|
||||
|
||||
func New(
|
||||
locations Locator,
|
||||
cache Cacher,
|
||||
s SettingsProvider,
|
||||
sentryReporter *sentry.Reporter,
|
||||
panicHandler users.PanicHandler,
|
||||
eventListener listener.Listener,
|
||||
clientManager users.ClientManager,
|
||||
clientManager pmapi.Manager,
|
||||
credStorer users.CredentialsStorer,
|
||||
updater Updater,
|
||||
versioner Versioner,
|
||||
@ -69,7 +68,7 @@ func New(
|
||||
clientManager.AllowProxy()
|
||||
}
|
||||
|
||||
storeFactory := newStoreFactory(cache, panicHandler, clientManager, eventListener)
|
||||
storeFactory := newStoreFactory(cache, sentryReporter, panicHandler, eventListener)
|
||||
u := users.New(locations, panicHandler, eventListener, clientManager, credStorer, storeFactory, true)
|
||||
b := &Bridge{
|
||||
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.
|
||||
func (b *Bridge) ReportBug(osType, osVersion, description, accountName, address, emailClient string) error {
|
||||
c := b.clientManager.GetAnonymousClient()
|
||||
defer c.Logout()
|
||||
|
||||
title := "[Bridge] Bug"
|
||||
report := pmapi.ReportReq{
|
||||
return b.clientManager.ReportBug(context.Background(), pmapi.ReportBugReq{
|
||||
OS: osType,
|
||||
OSVersion: osVersion,
|
||||
Browser: emailClient,
|
||||
Title: title,
|
||||
Title: "[Bridge] Bug",
|
||||
Description: description,
|
||||
Username: accountName,
|
||||
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.
|
||||
@ -187,26 +139,41 @@ func (b *Bridge) GetUpdateChannel() updater.UpdateChannel {
|
||||
// Downgrading to previous version (by switching from early to stable, for example)
|
||||
// requires clearing all data including update files due to possibility of
|
||||
// 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))
|
||||
|
||||
version, err := b.updater.Check()
|
||||
if err != nil {
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
|
||||
if b.updater.IsDowngrade(version) {
|
||||
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")
|
||||
}
|
||||
// We have to deal right away only with downgrade - that action needs to
|
||||
// clear data and updates, and install bridge right away. But regular
|
||||
// upgrade can be leaved out for periodic check.
|
||||
if !b.updater.IsDowngrade(version) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
|
||||
@ -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;"
|
||||
@ -21,39 +21,39 @@ import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/sentry"
|
||||
"github.com/ProtonMail/proton-bridge/internal/store"
|
||||
"github.com/ProtonMail/proton-bridge/internal/users"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/pkg/listener"
|
||||
)
|
||||
|
||||
type storeFactory struct {
|
||||
cache Cacher
|
||||
panicHandler users.PanicHandler
|
||||
clientManager users.ClientManager
|
||||
eventListener listener.Listener
|
||||
storeCache *store.Cache
|
||||
cache Cacher
|
||||
sentryReporter *sentry.Reporter
|
||||
panicHandler users.PanicHandler
|
||||
eventListener listener.Listener
|
||||
storeCache *store.Cache
|
||||
}
|
||||
|
||||
func newStoreFactory(
|
||||
cache Cacher,
|
||||
sentryReporter *sentry.Reporter,
|
||||
panicHandler users.PanicHandler,
|
||||
clientManager users.ClientManager,
|
||||
eventListener listener.Listener,
|
||||
) *storeFactory {
|
||||
return &storeFactory{
|
||||
cache: cache,
|
||||
panicHandler: panicHandler,
|
||||
clientManager: clientManager,
|
||||
eventListener: eventListener,
|
||||
storeCache: store.NewCache(cache.GetIMAPCachePath()),
|
||||
cache: cache,
|
||||
sentryReporter: sentryReporter,
|
||||
panicHandler: panicHandler,
|
||||
eventListener: eventListener,
|
||||
storeCache: store.NewCache(cache.GetIMAPCachePath()),
|
||||
}
|
||||
}
|
||||
|
||||
// New creates new store for given user.
|
||||
func (f *storeFactory) New(user store.BridgeUser) (*store.Store, error) {
|
||||
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.
|
||||
|
||||
@ -21,6 +21,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strconv"
|
||||
"sync"
|
||||
@ -73,13 +74,12 @@ func (p *keyValueStore) save() error {
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
|
||||
f, err := os.Create(p.path)
|
||||
b, err := json.MarshalIndent(p.cache, "", "\t")
|
||||
if err != nil {
|
||||
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) {
|
||||
|
||||
@ -72,20 +72,20 @@ func TestKeyValueStoreSetDefault(t *testing.T) {
|
||||
func TestKeyValueStoreSet(t *testing.T) {
|
||||
pref := newTestEmptyKeyValueStore(t)
|
||||
pref.Set("str", "value")
|
||||
checkSavedKeyValueStore(t, "{\"str\":\"value\"}")
|
||||
checkSavedKeyValueStore(t, "{\n\t\"str\": \"value\"\n}")
|
||||
}
|
||||
|
||||
func TestKeyValueStoreSetInt(t *testing.T) {
|
||||
pref := newTestEmptyKeyValueStore(t)
|
||||
pref.SetInt("int", 42)
|
||||
checkSavedKeyValueStore(t, "{\"int\":\"42\"}")
|
||||
checkSavedKeyValueStore(t, "{\n\t\"int\": \"42\"\n}")
|
||||
}
|
||||
|
||||
func TestKeyValueStoreSetBool(t *testing.T) {
|
||||
pref := newTestEmptyKeyValueStore(t)
|
||||
pref.SetBool("trueBool", true)
|
||||
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 {
|
||||
@ -101,5 +101,5 @@ func newTestKeyValueStore(t *testing.T) *keyValueStore {
|
||||
func checkSavedKeyValueStore(t *testing.T, expected string) {
|
||||
data, err := ioutil.ReadFile(testPrefFilePath)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expected+"\n", string(data))
|
||||
require.Equal(t, expected, string(data))
|
||||
}
|
||||
|
||||
@ -78,7 +78,7 @@ func (s *Settings) setDefaultValues() {
|
||||
s.setDefault(ReportOutgoingNoEncKey, "false")
|
||||
s.setDefault(LastVersionKey, "")
|
||||
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(APIPortKey, DefaultAPIPort)
|
||||
|
||||
@ -122,11 +122,7 @@ func (t *TLS) GenerateCerts(template *x509.Certificate) error {
|
||||
}
|
||||
defer keyOut.Close() // nolint[errcheck]
|
||||
|
||||
if err := pem.Encode(keyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return pem.Encode(keyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})
|
||||
}
|
||||
|
||||
// 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.AddCert(c.Leaf)
|
||||
|
||||
// nolint[gosec]: We need to support older TLS versions for AppleMail and Outlook.
|
||||
return &tls.Config{
|
||||
Certificates: []tls.Certificate{c},
|
||||
ServerName: "127.0.0.1",
|
||||
|
||||
61
internal/config/useragent/platform.go
Normal file
61
internal/config/useragent/platform.go
Normal 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)
|
||||
}
|
||||
@ -38,7 +38,27 @@ func TestIsVersionCatalinaOrNewer(t *testing.T) {
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@ -18,36 +18,42 @@
|
||||
package useragent
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
)
|
||||
|
||||
// IsCatalinaOrNewer checks that host is MacOS Catalina 10.15.x or higher.
|
||||
func IsCatalinaOrNewer() bool {
|
||||
if runtime.GOOS != "darwin" {
|
||||
return false
|
||||
}
|
||||
return isVersionCatalinaOrNewer(getMacVersion())
|
||||
type UserAgent struct {
|
||||
client, platform string
|
||||
}
|
||||
|
||||
func getMacVersion() string {
|
||||
out, err := exec.Command("sw_vers", "-productVersion").Output()
|
||||
if err != nil {
|
||||
return ""
|
||||
func New() *UserAgent {
|
||||
return &UserAgent{
|
||||
client: "",
|
||||
platform: runtime.GOOS,
|
||||
}
|
||||
|
||||
return strings.TrimSpace(string(out))
|
||||
}
|
||||
|
||||
func isVersionCatalinaOrNewer(version string) bool {
|
||||
v, err := semver.NewVersion(version)
|
||||
if err != nil {
|
||||
return false
|
||||
func (ua *UserAgent) SetClient(name, version string) {
|
||||
ua.client = fmt.Sprintf("%v/%v", name, regexp.MustCompile(`(.*) \((.*)\)`).ReplaceAllString(version, "$1-$2"))
|
||||
}
|
||||
|
||||
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 v.GreaterThan(catalina) || v.Equal(catalina)
|
||||
return fmt.Sprintf("%v (%v)", client, ua.platform)
|
||||
}
|
||||
86
internal/config/useragent/useragent_test.go
Normal file
86
internal/config/useragent/useragent_test.go
Normal 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())
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -15,18 +15,14 @@
|
||||
// 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
|
||||
// +build !build_qa
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/textproto"
|
||||
package constants
|
||||
|
||||
import "time"
|
||||
|
||||
// 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
|
||||
}
|
||||
28
internal/constants/update_qa.go
Normal file
28
internal/constants/update_qa.go
Normal file
@ -0,0 +1,28 @@
|
||||
// Copyright (c) 2021 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.
|
||||
//
|
||||
// ProtonMail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// ProtonMail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// +build build_qa
|
||||
|
||||
package constants
|
||||
|
||||
import "time"
|
||||
|
||||
// nolint[gochecknoglobals]
|
||||
var (
|
||||
// UpdateCheckInterval defines how often we check for new version
|
||||
UpdateCheckInterval = time.Duration(5 * time.Minute)
|
||||
)
|
||||
@ -19,7 +19,7 @@
|
||||
package crash
|
||||
|
||||
import (
|
||||
"github.com/ProtonMail/proton-bridge/pkg/sentry"
|
||||
"github.com/ProtonMail/proton-bridge/internal/sentry"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
|
||||
@ -29,10 +29,15 @@ import (
|
||||
"time"
|
||||
|
||||
"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/pkg/mobileconfig"
|
||||
)
|
||||
|
||||
const (
|
||||
bigSurPreferncesPane = "/System/Library/PreferencePanes/Profiles.prefPane"
|
||||
)
|
||||
|
||||
func init() { //nolint[gochecknoinit]
|
||||
available = append(available, &appleMail{})
|
||||
}
|
||||
@ -43,7 +48,22 @@ func (c *appleMail) Name() string {
|
||||
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 displayName string
|
||||
|
||||
@ -62,7 +82,7 @@ func (c *appleMail) Configure(imapPort, smtpPort int, imapSSL, smtpSSL bool, use
|
||||
|
||||
timestamp := strconv.FormatInt(time.Now().Unix(), 10)
|
||||
|
||||
mc := &mobileconfig.Config{
|
||||
return &mobileconfig.Config{
|
||||
EmailAddress: addresses,
|
||||
DisplayName: displayName,
|
||||
Identifier: "protonmail " + displayName + timestamp,
|
||||
@ -80,10 +100,12 @@ func (c *appleMail) Configure(imapPort, smtpPort int, imapSSL, smtpSSL bool, use
|
||||
Username: displayName,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func saveConfigTemporarily(mc *mobileconfig.Config) (fname string, err error) {
|
||||
dir, err := ioutil.TempDir("", "protonmail-autoconfig")
|
||||
if err != nil {
|
||||
return err
|
||||
return
|
||||
}
|
||||
|
||||
// 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.
|
||||
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 {
|
||||
return err
|
||||
return
|
||||
}
|
||||
|
||||
if err := mc.WriteOut(f); err != nil {
|
||||
if err = mc.WriteOut(f); err != nil {
|
||||
_ = f.Close()
|
||||
return err
|
||||
return
|
||||
}
|
||||
_ = f.Close()
|
||||
|
||||
return exec.Command("open", f.Name()).Run() // nolint[gosec]
|
||||
return
|
||||
}
|
||||
|
||||
@ -18,6 +18,7 @@
|
||||
package cliie
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/abiosoft/ishell"
|
||||
@ -25,7 +26,7 @@ import (
|
||||
|
||||
func (f *frontendCLI) listAccounts(c *ishell.Context) {
|
||||
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() {
|
||||
connected := "disconnected"
|
||||
if user.IsConnected() {
|
||||
@ -67,7 +68,7 @@ func (f *frontendCLI) loginAccount(c *ishell.Context) { // nolint[funlen]
|
||||
}
|
||||
|
||||
f.Println("Authenticating ... ")
|
||||
client, auth, err := f.ie.Login(loginName, password)
|
||||
client, auth, err := f.ie.Login(loginName, []byte(password))
|
||||
if err != nil {
|
||||
f.processAPIError(err)
|
||||
return
|
||||
@ -79,7 +80,7 @@ func (f *frontendCLI) loginAccount(c *ishell.Context) { // nolint[funlen]
|
||||
return
|
||||
}
|
||||
|
||||
err = client.Auth2FA(twoFactor, auth)
|
||||
err = client.Auth2FA(context.Background(), twoFactor)
|
||||
if err != nil {
|
||||
f.processAPIError(err)
|
||||
return
|
||||
@ -95,7 +96,7 @@ func (f *frontendCLI) loginAccount(c *ishell.Context) { // nolint[funlen]
|
||||
}
|
||||
|
||||
f.Println("Adding account ...")
|
||||
user, err := f.ie.FinishLogin(client, auth, mailboxPassword)
|
||||
user, err := f.ie.FinishLogin(client, auth, []byte(mailboxPassword))
|
||||
if err != nil {
|
||||
log.WithField("username", loginName).WithError(err).Error("Login was unsuccessful")
|
||||
f.Println("Adding account was unsuccessful:", err)
|
||||
|
||||
@ -84,11 +84,6 @@ func New( //nolint[funlen]
|
||||
Aliases: []string{"u", "version", "v"},
|
||||
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)
|
||||
|
||||
// Print info commands.
|
||||
@ -177,13 +172,13 @@ func New( //nolint[funlen]
|
||||
}
|
||||
|
||||
func (f *frontendCLI) watchEvents() {
|
||||
errorCh := f.getEventChannel(events.ErrorEvent)
|
||||
credentialsErrorCh := f.getEventChannel(events.CredentialsErrorEvent)
|
||||
internetOffCh := f.getEventChannel(events.InternetOffEvent)
|
||||
internetOnCh := f.getEventChannel(events.InternetOnEvent)
|
||||
addressChangedLogoutCh := f.getEventChannel(events.AddressChangedLogoutEvent)
|
||||
logoutCh := f.getEventChannel(events.LogoutEvent)
|
||||
certIssue := f.getEventChannel(events.TLSCertIssue)
|
||||
errorCh := f.eventListener.ProvideChannel(events.ErrorEvent)
|
||||
credentialsErrorCh := f.eventListener.ProvideChannel(events.CredentialsErrorEvent)
|
||||
internetOffCh := f.eventListener.ProvideChannel(events.InternetOffEvent)
|
||||
internetOnCh := f.eventListener.ProvideChannel(events.InternetOnEvent)
|
||||
addressChangedLogoutCh := f.eventListener.ProvideChannel(events.AddressChangedLogoutEvent)
|
||||
logoutCh := f.eventListener.ProvideChannel(events.LogoutEvent)
|
||||
certIssue := f.eventListener.ProvideChannel(events.TLSCertIssue)
|
||||
for {
|
||||
select {
|
||||
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.
|
||||
func (f *frontendCLI) Loop() error {
|
||||
f.Print(`
|
||||
|
||||
@ -38,7 +38,7 @@ func (f *frontendCLI) importLocalMessages(c *ishell.Context) {
|
||||
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)
|
||||
}
|
||||
|
||||
@ -68,7 +68,7 @@ func (f *frontendCLI) importRemoteMessages(c *ishell.Context) {
|
||||
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)
|
||||
}
|
||||
|
||||
@ -81,7 +81,7 @@ func (f *frontendCLI) exportMessagesToEML(c *ishell.Context) {
|
||||
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)
|
||||
}
|
||||
|
||||
@ -94,7 +94,7 @@ func (f *frontendCLI) exportMessagesToMBOX(c *ishell.Context) {
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@ -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) {
|
||||
if path, err := f.locations.ProvideLogsPath(); err != nil {
|
||||
f.Println("Failed to determine location of log files")
|
||||
|
||||
@ -20,7 +20,7 @@ package cliie
|
||||
import (
|
||||
"strings"
|
||||
|
||||
pmapi "github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
"github.com/fatih/color"
|
||||
)
|
||||
|
||||
@ -71,7 +71,7 @@ func (f *frontendCLI) printAndLogError(args ...interface{}) {
|
||||
func (f *frontendCLI) processAPIError(err error) {
|
||||
log.Warn("API error: ", err)
|
||||
switch err {
|
||||
case pmapi.ErrAPINotReachable:
|
||||
case pmapi.ErrNoConnection:
|
||||
f.notifyInternetOff()
|
||||
case pmapi.ErrUpgradeApplication:
|
||||
f.notifyNeedUpgrade()
|
||||
|
||||
@ -18,6 +18,7 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/bridge"
|
||||
@ -28,7 +29,7 @@ import (
|
||||
|
||||
func (f *frontendCLI) listAccounts(c *ishell.Context) {
|
||||
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() {
|
||||
connected := "disconnected"
|
||||
if user.IsConnected() {
|
||||
@ -114,7 +115,7 @@ func (f *frontendCLI) loginAccount(c *ishell.Context) { // nolint[funlen]
|
||||
}
|
||||
|
||||
f.Println("Authenticating ... ")
|
||||
client, auth, err := f.bridge.Login(loginName, password)
|
||||
client, auth, err := f.bridge.Login(loginName, []byte(password))
|
||||
if err != nil {
|
||||
f.processAPIError(err)
|
||||
return
|
||||
@ -126,7 +127,7 @@ func (f *frontendCLI) loginAccount(c *ishell.Context) { // nolint[funlen]
|
||||
return
|
||||
}
|
||||
|
||||
err = client.Auth2FA(twoFactor, auth)
|
||||
err = client.Auth2FA(context.Background(), twoFactor)
|
||||
if err != nil {
|
||||
f.processAPIError(err)
|
||||
return
|
||||
@ -142,7 +143,7 @@ func (f *frontendCLI) loginAccount(c *ishell.Context) { // nolint[funlen]
|
||||
}
|
||||
|
||||
f.Println("Adding account ...")
|
||||
user, err := f.bridge.FinishLogin(client, auth, mailboxPassword)
|
||||
user, err := f.bridge.FinishLogin(client, auth, []byte(mailboxPassword))
|
||||
if err != nil {
|
||||
log.WithField("username", loginName).WithError(err).Error("Login was unsuccessful")
|
||||
f.Println("Adding account was unsuccessful:", err)
|
||||
|
||||
@ -102,10 +102,6 @@ func New( //nolint[funlen]
|
||||
Aliases: []string{"p"},
|
||||
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",
|
||||
Help: "change port numbers of IMAP and SMTP servers.(alias: ssl, starttls)",
|
||||
Aliases: []string{"ssl", "starttls"},
|
||||
@ -113,19 +109,53 @@ func New( //nolint[funlen]
|
||||
})
|
||||
fe.AddCmd(changeCmd)
|
||||
|
||||
// Check commands.
|
||||
checkCmd := &ishell.Cmd{Name: "check", Help: "check internet connection or new version."}
|
||||
checkCmd.AddCmd(&ishell.Cmd{Name: "updates",
|
||||
Help: "check for Bridge updates. (aliases: u, v, version)",
|
||||
Aliases: []string{"u", "version", "v"},
|
||||
Func: fe.checkUpdates,
|
||||
// DoH commands.
|
||||
dohCmd := &ishell.Cmd{Name: "proxy",
|
||||
Help: "allow or disallow bridge to securely connect to proton via a third party when it is being blocked",
|
||||
}
|
||||
dohCmd.AddCmd(&ishell.Cmd{Name: "allow",
|
||||
Help: "allow bridge to securely connect to proton via a third party when it is being blocked",
|
||||
Func: fe.allowProxy,
|
||||
})
|
||||
checkCmd.AddCmd(&ishell.Cmd{Name: "internet",
|
||||
Help: "check internet connection. (aliases: i, conn, connection)",
|
||||
Aliases: []string{"i", "con", "connection"},
|
||||
Func: fe.checkInternetConnection,
|
||||
dohCmd.AddCmd(&ishell.Cmd{Name: "disallow",
|
||||
Help: "disallow bridge to securely connect to proton via a third party when it is being blocked",
|
||||
Func: fe.disallowProxy,
|
||||
})
|
||||
fe.AddCmd(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.
|
||||
fe.AddCmd(&ishell.Cmd{Name: "log-dir",
|
||||
@ -189,14 +219,14 @@ func New( //nolint[funlen]
|
||||
}
|
||||
|
||||
func (f *frontendCLI) watchEvents() {
|
||||
errorCh := f.getEventChannel(events.ErrorEvent)
|
||||
credentialsErrorCh := f.getEventChannel(events.CredentialsErrorEvent)
|
||||
internetOffCh := f.getEventChannel(events.InternetOffEvent)
|
||||
internetOnCh := f.getEventChannel(events.InternetOnEvent)
|
||||
addressChangedCh := f.getEventChannel(events.AddressChangedEvent)
|
||||
addressChangedLogoutCh := f.getEventChannel(events.AddressChangedLogoutEvent)
|
||||
logoutCh := f.getEventChannel(events.LogoutEvent)
|
||||
certIssue := f.getEventChannel(events.TLSCertIssue)
|
||||
errorCh := f.eventListener.ProvideChannel(events.ErrorEvent)
|
||||
credentialsErrorCh := f.eventListener.ProvideChannel(events.CredentialsErrorEvent)
|
||||
internetOffCh := f.eventListener.ProvideChannel(events.InternetOffEvent)
|
||||
internetOnCh := f.eventListener.ProvideChannel(events.InternetOnEvent)
|
||||
addressChangedCh := f.eventListener.ProvideChannel(events.AddressChangedEvent)
|
||||
addressChangedLogoutCh := f.eventListener.ProvideChannel(events.AddressChangedLogoutEvent)
|
||||
logoutCh := f.eventListener.ProvideChannel(events.LogoutEvent)
|
||||
certIssue := f.eventListener.ProvideChannel(events.TLSCertIssue)
|
||||
for {
|
||||
select {
|
||||
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.
|
||||
func (f *frontendCLI) Loop() error {
|
||||
f.Print(`
|
||||
|
||||
@ -39,14 +39,6 @@ func (f *frontendCLI) restart(c *ishell.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
func (f *frontendCLI) checkInternetConnection(c *ishell.Context) {
|
||||
if f.bridge.CheckConnection() == nil {
|
||||
f.Println("Internet connection is available.")
|
||||
} else {
|
||||
f.Println("Can not contact the server, please check your internet connection.")
|
||||
}
|
||||
}
|
||||
|
||||
func (f *frontendCLI) printLogDir(c *ishell.Context) {
|
||||
if path, err := f.locations.ProvideLogsPath(); err != nil {
|
||||
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) {
|
||||
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()
|
||||
}
|
||||
} 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") {
|
||||
f.settings.SetBool(settings.AllowProxyKey, true)
|
||||
f.bridge.AllowProxy()
|
||||
}
|
||||
f.Println("Bridge is already set to use alternative routing to connect to Proton if it is being blocked.")
|
||||
return
|
||||
}
|
||||
|
||||
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") {
|
||||
f.settings.SetBool(settings.AllowProxyKey, true)
|
||||
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 {
|
||||
port = strings.Replace(port, ":", "", -1)
|
||||
port = strings.ReplaceAll(port, ":", "")
|
||||
if port == "" || port == currentPort {
|
||||
return true
|
||||
}
|
||||
|
||||
@ -21,11 +21,23 @@ import (
|
||||
"strings"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
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) {
|
||||
@ -33,3 +45,68 @@ func (f *frontendCLI) printCredits(c *ishell.Context) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -71,7 +71,7 @@ func (f *frontendCLI) printAndLogError(args ...interface{}) {
|
||||
func (f *frontendCLI) processAPIError(err error) {
|
||||
log.Warn("API error: ", err)
|
||||
switch err {
|
||||
case pmapi.ErrAPINotReachable:
|
||||
case pmapi.ErrNoConnection:
|
||||
f.notifyInternetOff()
|
||||
case pmapi.ErrUpgradeApplication:
|
||||
f.notifyNeedUpgrade()
|
||||
|
||||
@ -22,6 +22,7 @@ import (
|
||||
"github.com/ProtonMail/go-autostart"
|
||||
"github.com/ProtonMail/proton-bridge/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/internal/config/settings"
|
||||
"github.com/ProtonMail/proton-bridge/internal/config/useragent"
|
||||
"github.com/ProtonMail/proton-bridge/internal/frontend/cli"
|
||||
cliie "github.com/ProtonMail/proton-bridge/internal/frontend/cli-ie"
|
||||
"github.com/ProtonMail/proton-bridge/internal/frontend/qt"
|
||||
@ -60,6 +61,7 @@ func New(
|
||||
settings *settings.Settings,
|
||||
eventListener listener.Listener,
|
||||
updater types.Updater,
|
||||
userAgent *useragent.UserAgent,
|
||||
bridge *bridge.Bridge,
|
||||
noEncConfirmator types.NoEncConfirmator,
|
||||
autostart *autostart.App,
|
||||
@ -77,6 +79,7 @@ func New(
|
||||
settings,
|
||||
eventListener,
|
||||
updater,
|
||||
userAgent,
|
||||
bridgeWrap,
|
||||
noEncConfirmator,
|
||||
autostart,
|
||||
@ -95,6 +98,7 @@ func newBridgeFrontend(
|
||||
settings *settings.Settings,
|
||||
eventListener listener.Listener,
|
||||
updater types.Updater,
|
||||
userAgent *useragent.UserAgent,
|
||||
bridge types.Bridger,
|
||||
noEncConfirmator types.NoEncConfirmator,
|
||||
autostart *autostart.App,
|
||||
@ -122,6 +126,7 @@ func newBridgeFrontend(
|
||||
settings,
|
||||
eventListener,
|
||||
updater,
|
||||
userAgent,
|
||||
bridge,
|
||||
noEncConfirmator,
|
||||
autostart,
|
||||
|
||||
194
internal/frontend/qml/BridgeUI/DialogKeychainChange.qml
Normal file
194
internal/frontend/qml/BridgeUI/DialogKeychainChange.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -229,7 +229,7 @@ Dialog {
|
||||
currentIndex : 0
|
||||
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")
|
||||
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")
|
||||
}
|
||||
},
|
||||
@ -310,7 +310,7 @@ Dialog {
|
||||
target: root
|
||||
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.")
|
||||
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")
|
||||
answer : qsTr("Disabling early access...")
|
||||
}
|
||||
|
||||
@ -94,7 +94,7 @@ Item {
|
||||
font.pointSize : Style.main.fontSize * Style.pt
|
||||
text:
|
||||
"ProtonMail Bridge "+go.getBackendVersion()+"\n"+
|
||||
"© 2020 Proton Technologies AG"
|
||||
"© 2021 Proton Technologies AG"
|
||||
}
|
||||
}
|
||||
Row {
|
||||
|
||||
@ -303,6 +303,10 @@ Window {
|
||||
id: dialogChangePort
|
||||
}
|
||||
|
||||
DialogKeychainChange {
|
||||
id: dialogChangeKeychain
|
||||
}
|
||||
|
||||
DialogConnectionTroubleshoot {
|
||||
id: dialogConnectionTroubleshoot
|
||||
}
|
||||
|
||||
@ -239,6 +239,25 @@ Item {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ module BridgeUI
|
||||
AccountDelegate 1.0 AccountDelegate.qml
|
||||
Credits 1.0 Credits.qml
|
||||
DialogFirstStart 1.0 DialogFirstStart.qml
|
||||
DialogKeychainChange 1.0 DialogKeychainChange.qml
|
||||
DialogPortChange 1.0 DialogPortChange.qml
|
||||
DialogYesNo 1.0 DialogYesNo.qml
|
||||
DialogTLSCertInfo 1.0 DialogTLSCertInfo.qml
|
||||
|
||||
@ -107,17 +107,53 @@ Item {
|
||||
gui.openMainWindow(false)
|
||||
if (go.isConnectionOK) {
|
||||
if( winMain.updateState=="noInternet") {
|
||||
go.setUpdateState("upToDate")
|
||||
go.updateState = "upToDate"
|
||||
}
|
||||
} 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
|
||||
if (winMain.updateState != "forceUpdate") {
|
||||
winMain.updateState = updateState
|
||||
winMain.updateState = go.updateState
|
||||
}
|
||||
}
|
||||
|
||||
@ -129,15 +165,14 @@ Item {
|
||||
}
|
||||
|
||||
onNotifyManualUpdate: {
|
||||
go.setUpdateState("oldVersion")
|
||||
go.updateState = "oldVersion"
|
||||
}
|
||||
|
||||
onNotifyManualUpdateRestartNeeded: {
|
||||
if (!winMain.dialogUpdate.visible) {
|
||||
gui.openMainWindow(true)
|
||||
winMain.dialogUpdate.show()
|
||||
}
|
||||
go.setUpdateState("updateRestart")
|
||||
go.updateState = "updateRestart"
|
||||
winMain.dialogUpdate.finished(false)
|
||||
|
||||
// after manual update - just retart immidiatly
|
||||
@ -147,28 +182,25 @@ Item {
|
||||
|
||||
onNotifyManualUpdateError: {
|
||||
if (!winMain.dialogUpdate.visible) {
|
||||
gui.openMainWindow(true)
|
||||
winMain.dialogUpdate.show()
|
||||
}
|
||||
go.setUpdateState("updateError")
|
||||
go.updateState = "updateError"
|
||||
winMain.dialogUpdate.finished(true)
|
||||
}
|
||||
|
||||
onNotifyForceUpdate : {
|
||||
go.setUpdateState("forceUpdate")
|
||||
go.updateState = "forceUpdate"
|
||||
if (!winMain.dialogUpdate.visible) {
|
||||
gui.openMainWindow(true)
|
||||
winMain.dialogUpdate.show()
|
||||
}
|
||||
}
|
||||
|
||||
onNotifySilentUpdateRestartNeeded: {
|
||||
go.setUpdateState("updateRestart")
|
||||
go.updateState = "updateRestart"
|
||||
}
|
||||
|
||||
onNotifySilentUpdateError: {
|
||||
go.setUpdateState("updateError")
|
||||
gui.openMainWindow(true)
|
||||
go.updateState = "updateError"
|
||||
}
|
||||
|
||||
onNotifyLogout : {
|
||||
@ -287,9 +319,17 @@ Item {
|
||||
if (showAndRise) {
|
||||
gui.winMain.showAndRise()
|
||||
}
|
||||
|
||||
// restore update notification bar: trigger updateStateChanged
|
||||
var tmp = go.updateState
|
||||
go.updateState = ""
|
||||
go.updateState = tmp
|
||||
}
|
||||
|
||||
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.destroy(5000)
|
||||
gui.winMain = null
|
||||
|
||||
@ -30,7 +30,6 @@ Item {
|
||||
id: gui
|
||||
property alias winMain: winMain
|
||||
property bool isFirstWindow: true
|
||||
property int warningFlags: 0
|
||||
|
||||
property var locale : Qt.locale("en_US")
|
||||
property date netBday : new Date("1989-03-13T00:00:00")
|
||||
@ -96,17 +95,17 @@ Item {
|
||||
go.isConnectionOK = isAvailable
|
||||
if (go.isConnectionOK) {
|
||||
if( winMain.updateState==gui.enums.statusNoInternet) {
|
||||
go.setUpdateState(gui.enums.statusUpToDate)
|
||||
go.updateState = gui.enums.statusUpToDate
|
||||
}
|
||||
} else {
|
||||
go.setUpdateState(gui.enums.statusNoInternet)
|
||||
go.updateState = gui.enums.statusNoInternet
|
||||
}
|
||||
}
|
||||
|
||||
onSetUpdateState : {
|
||||
onUpdateStateChanged : {
|
||||
// once app is outdated prevent from state change
|
||||
if (winMain.updateState != "forceUpdate") {
|
||||
winMain.updateState = updateState
|
||||
winMain.updateState = go.updateState
|
||||
}
|
||||
}
|
||||
|
||||
@ -207,16 +206,16 @@ Item {
|
||||
}
|
||||
|
||||
onNotifyManualUpdate: {
|
||||
go.setUpdateState("oldVersion")
|
||||
go.updateState = "oldVersion"
|
||||
}
|
||||
|
||||
onNotifyManualUpdateRestartNeeded: {
|
||||
if (!winMain.dialogUpdate.visible) {
|
||||
winMain.dialogUpdate.show()
|
||||
}
|
||||
go.setUpdateState("updateRestart")
|
||||
go.updateState = "updateRestart"
|
||||
winMain.dialogUpdate.finished(false)
|
||||
|
||||
|
||||
// after manual update - just retart immidiatly
|
||||
go.setToRestart()
|
||||
Qt.quit()
|
||||
@ -226,24 +225,24 @@ Item {
|
||||
if (!winMain.dialogUpdate.visible) {
|
||||
winMain.dialogUpdate.show()
|
||||
}
|
||||
go.setUpdateState("updateError")
|
||||
go.updateState = "updateError"
|
||||
winMain.dialogUpdate.finished(true)
|
||||
}
|
||||
|
||||
onNotifyForceUpdate : {
|
||||
go.setUpdateState("forceUpdate")
|
||||
go.updateState = "forceUpdate"
|
||||
if (!winMain.dialogUpdate.visible) {
|
||||
winMain.dialogUpdate.show()
|
||||
}
|
||||
}
|
||||
|
||||
onNotifySilentUpdateRestartNeeded: {
|
||||
go.setUpdateState("updateRestart")
|
||||
}
|
||||
|
||||
onNotifySilentUpdateError: {
|
||||
go.setUpdateState("updateError")
|
||||
}
|
||||
//onNotifySilentUpdateRestartNeeded: {
|
||||
// go.updateState = "updateRestart"
|
||||
//}
|
||||
//
|
||||
//onNotifySilentUpdateError: {
|
||||
// go.updateState = "updateError"
|
||||
//}
|
||||
|
||||
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) )
|
||||
|
||||
@ -165,6 +165,7 @@ Column {
|
||||
textColor : Style.main.textBlue
|
||||
onClicked: {
|
||||
dialogExport.currentIndex = 0
|
||||
dialogExport.account = account
|
||||
dialogExport.address = account
|
||||
dialogExport.show()
|
||||
}
|
||||
@ -321,6 +322,7 @@ Column {
|
||||
textBold: true
|
||||
textColor: Style.main.textBlue
|
||||
onClicked: {
|
||||
dialogExport.account = account
|
||||
dialogExport.address = listalias[index]
|
||||
dialogExport.show()
|
||||
}
|
||||
@ -339,6 +341,7 @@ Column {
|
||||
textBold: true
|
||||
textColor: enabled ? Style.main.textBlue : Style.main.textDisabled
|
||||
onClicked: {
|
||||
dialogImport.account = account
|
||||
dialogImport.address = listalias[index]
|
||||
dialogImport.show()
|
||||
}
|
||||
|
||||
@ -35,6 +35,7 @@ Dialog {
|
||||
|
||||
title : set_title()
|
||||
|
||||
property string account
|
||||
property string address
|
||||
property alias finish: finish
|
||||
|
||||
@ -408,7 +409,6 @@ Dialog {
|
||||
|
||||
onShow: {
|
||||
if (winMain.updateState==gui.enums.statusNoInternet) {
|
||||
go.checkInternet()
|
||||
if (winMain.updateState==gui.enums.statusNoInternet) {
|
||||
go.notifyError(gui.enums.errNoInternet)
|
||||
root.hide()
|
||||
@ -428,7 +428,7 @@ Dialog {
|
||||
onTriggered : {
|
||||
switch (currentIndex) {
|
||||
case 0:
|
||||
go.loadStructureForExport(root.address)
|
||||
go.loadStructureForExport(root.account, root.address)
|
||||
sourceFoldersInput.hasItems = (transferRules.rowCount() > 0)
|
||||
break
|
||||
case 2:
|
||||
|
||||
@ -34,6 +34,7 @@ Dialog {
|
||||
|
||||
isDialogBusy: currentIndex==3 || currentIndex==4
|
||||
|
||||
property string account
|
||||
property string address
|
||||
property string inputPath : ""
|
||||
property bool isFromFile : inputEmail.text == "" && root.inputPath != ""
|
||||
@ -856,14 +857,12 @@ Dialog {
|
||||
inputPort . checkIsANumber()
|
||||
//emailProvider . currentIndex!=0
|
||||
)) isOK = false
|
||||
go.checkInternet()
|
||||
if (winMain.updateState == gui.enums.statusNoInternet) { // todo: use main error dialog for this
|
||||
errorPopup.show(qsTr("Please check your internet connection."))
|
||||
return false
|
||||
}
|
||||
break
|
||||
case 2: // loading structure
|
||||
go.checkInternet()
|
||||
if (winMain.updateState == gui.enums.statusNoInternet) {
|
||||
errorPopup.show(qsTr("Please check your internet connection."))
|
||||
return false
|
||||
@ -948,7 +947,6 @@ Dialog {
|
||||
onShow : {
|
||||
root.clear()
|
||||
if (winMain.updateState==gui.enums.statusNoInternet) {
|
||||
go.checkInternet()
|
||||
if (winMain.updateState==gui.enums.statusNoInternet) {
|
||||
winMain.popupMessage.show(go.canNotReachAPI)
|
||||
root.hide()
|
||||
@ -1032,6 +1030,7 @@ Dialog {
|
||||
root.isFromIMAP,
|
||||
root.inputPath,
|
||||
inputEmail.text, inputPassword.text, inputServer.text, inputPort.text,
|
||||
root.account,
|
||||
root.address
|
||||
)
|
||||
break
|
||||
|
||||
@ -81,7 +81,7 @@ Item {
|
||||
color: Style.main.textDisabled
|
||||
horizontalAlignment: Qt.AlignHCenter
|
||||
font.pointSize : Style.main.fontSize * Style.pt
|
||||
text: "ProtonMail Import-Export app Version "+go.getBackendVersion()+"\n© 2020 Proton Technologies AG"
|
||||
text: "ProtonMail Import-Export app Version "+go.getBackendVersion()+"\n© 2021 Proton Technologies AG"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -96,6 +96,8 @@ Item {
|
||||
onClicked: bugreportWin.show()
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
ButtonIconText {
|
||||
id: autoUpdates
|
||||
text: qsTr("Keep the application up to date", "label for toggle that activates and disables the automatic updates")
|
||||
@ -115,8 +117,6 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
ButtonIconText {
|
||||
id: cacheClear
|
||||
text: qsTr("Clear Cache")
|
||||
|
||||
@ -18,6 +18,7 @@
|
||||
// Dialog with adding new user
|
||||
|
||||
import QtQuick 2.8
|
||||
import QtQuick.Controls 2.1
|
||||
import QtQuick.Layouts 1.3
|
||||
import ProtonUI 1.0
|
||||
|
||||
@ -83,6 +84,9 @@ StackLayout {
|
||||
text : ""
|
||||
color: Style.main.textBlue
|
||||
visible: false
|
||||
width: root.width
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
// prevent any action below
|
||||
@ -111,6 +115,11 @@ StackLayout {
|
||||
Accessible.description: title
|
||||
Accessible.focusable: true
|
||||
|
||||
onVisibleChanged: {
|
||||
if (background.visible != visible) {
|
||||
background.visible = visible
|
||||
}
|
||||
}
|
||||
|
||||
visible : false
|
||||
anchors {
|
||||
|
||||
@ -70,7 +70,8 @@ Dialog {
|
||||
id: topSep
|
||||
color : "transparent"
|
||||
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 {
|
||||
|
||||
@ -107,7 +107,7 @@ Dialog {
|
||||
text: qsTr("Automatically update in the future", "Checkbox label for using autoupdates later on")
|
||||
checked: go.isAutoUpdate
|
||||
onToggled: go.toggleAutoUpdate()
|
||||
visible: !root.forceUpdate
|
||||
visible: !root.forceUpdate && (go.isAutoUpdate != undefined)
|
||||
}
|
||||
|
||||
Row {
|
||||
|
||||
@ -25,33 +25,12 @@ import ProtonUI 1.0
|
||||
Rectangle {
|
||||
id: root
|
||||
property var iTry: 0
|
||||
property var secLeft: 0
|
||||
property var second: 1000 // convert millisecond to second
|
||||
property var checkInterval: [ 5, 10, 30, 60, 120, 300, 600 ] // seconds
|
||||
property bool isVisible: true
|
||||
property var fontSize : 1.2 * Style.main.fontSize
|
||||
color : "black"
|
||||
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 {
|
||||
id: messageRow
|
||||
anchors.centerIn: root
|
||||
@ -66,14 +45,15 @@ Rectangle {
|
||||
ClickIconText {
|
||||
id: linkText
|
||||
anchors.verticalCenter : message.verticalCenter
|
||||
iconText : ""
|
||||
iconText : " "
|
||||
fontSize : root.fontSize
|
||||
textUnderline: true
|
||||
}
|
||||
|
||||
ClickIconText {
|
||||
id: actionText
|
||||
anchors.verticalCenter : message.verticalCenter
|
||||
iconText : ""
|
||||
iconText : " "
|
||||
fontSize : root.fontSize
|
||||
textUnderline: true
|
||||
}
|
||||
@ -107,49 +87,21 @@ Rectangle {
|
||||
onStateChanged : {
|
||||
switch (root.state) {
|
||||
case "internetCheck":
|
||||
break;
|
||||
break;
|
||||
case "noInternet" :
|
||||
gui.warningFlags |= Style.warnInfoBar
|
||||
retryInternet.start()
|
||||
secLeft=checkInterval[iTry]
|
||||
break;
|
||||
break;
|
||||
case "oldVersion":
|
||||
gui.warningFlags |= Style.warnInfoBar
|
||||
break;
|
||||
break;
|
||||
case "forceUpdate":
|
||||
gui.warningFlags |= Style.errorInfoBar
|
||||
break;
|
||||
break;
|
||||
case "upToDate":
|
||||
gui.warningFlags &= ~Style.warnInfoBar
|
||||
iTry = 0
|
||||
secLeft=checkInterval[iTry]
|
||||
break;
|
||||
break;
|
||||
case "updateRestart":
|
||||
gui.warningFlags |= Style.warnInfoBar
|
||||
break;
|
||||
break;
|
||||
case "updateError":
|
||||
gui.warningFlags |= Style.errorInfoBar
|
||||
break;
|
||||
break;
|
||||
default :
|
||||
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
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@ -199,23 +151,15 @@ Rectangle {
|
||||
PropertyChanges {
|
||||
target: message
|
||||
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 {
|
||||
target: linkText
|
||||
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 {
|
||||
target: separatorText
|
||||
visible: true
|
||||
visible: false
|
||||
text: "|"
|
||||
}
|
||||
PropertyChanges {
|
||||
@ -247,7 +191,7 @@ Rectangle {
|
||||
PropertyChanges {
|
||||
target: linkText
|
||||
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()
|
||||
}
|
||||
PropertyChanges {
|
||||
@ -270,7 +214,7 @@ Rectangle {
|
||||
target: closeSign
|
||||
visible: true
|
||||
onClicked: {
|
||||
root.state = "upToDate"
|
||||
go.updateState = "upToDate"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@ -24,7 +24,7 @@ import QtQuick.Window 2.2
|
||||
|
||||
Window {
|
||||
id: testroot
|
||||
width : 150
|
||||
width : 250
|
||||
height : 600
|
||||
flags : Qt.Window | Qt.Dialog | Qt.FramelessWindowHint
|
||||
visible : true
|
||||
@ -60,7 +60,7 @@ Window {
|
||||
Text {
|
||||
id: systrText
|
||||
anchors {
|
||||
right : test_systray.right
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
verticalCenter: test_systray.verticalCenter
|
||||
}
|
||||
text: "unset"
|
||||
@ -281,6 +281,9 @@ Window {
|
||||
|
||||
property bool hasNoKeychain : true
|
||||
|
||||
property var availableKeychain: ["pass-app", "gnome-keyring"]
|
||||
property var selectedKeychain: "gnome-keyring"
|
||||
|
||||
property string wrongCredentials
|
||||
property string wrongMailboxPassword
|
||||
property string canNotReachAPI
|
||||
@ -296,6 +299,7 @@ Window {
|
||||
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 updateState
|
||||
property string updateVersion : "QA.1.0"
|
||||
property bool updateCanInstall: true
|
||||
property string updateLandingPage : "https://protonmail.com/bridge/download/"
|
||||
@ -337,7 +341,6 @@ Window {
|
||||
|
||||
signal notifyPortIssue(bool busyPortIMAP, bool busyPortSMTP)
|
||||
signal notifyVersionIsTheLatest()
|
||||
signal setUpdateState(string updateState)
|
||||
signal notifyKeychainRebuild()
|
||||
signal notifyHasNoKeychain()
|
||||
|
||||
|
||||
@ -23,13 +23,13 @@ import QtQuick.Window 2.2
|
||||
|
||||
Window {
|
||||
id : testroot
|
||||
width : 100
|
||||
width : 150
|
||||
height : 600
|
||||
flags : Qt.Window | Qt.Dialog | Qt.FramelessWindowHint
|
||||
visible : true
|
||||
title : "GUI test Window"
|
||||
color : "transparent"
|
||||
x : testgui.winMain.x - 120
|
||||
x : testgui.winMain.x - 170
|
||||
y : testgui.winMain.y
|
||||
|
||||
property bool newVersion : true
|
||||
@ -110,8 +110,8 @@ Window {
|
||||
ListElement { title: "NotifyManualUpdateRestart" }
|
||||
ListElement { title: "NotifyManualUpdateError" }
|
||||
ListElement { title: "ForceUpdate" }
|
||||
ListElement { title: "NotifySilentUpdateRestartNeeded" }
|
||||
ListElement { title: "NotifySilentUpdateError" }
|
||||
//ListElement { title: "NotifySilentUpdateRestartNeeded" }
|
||||
//ListElement { title: "NotifySilentUpdateError" }
|
||||
ListElement { title : "ImportStructure" }
|
||||
ListElement { title : "DraftImpFailed" }
|
||||
ListElement { title : "NoInterImp" }
|
||||
@ -183,12 +183,12 @@ Window {
|
||||
case "ForceUpdate" :
|
||||
go.notifyForceUpdate()
|
||||
break;
|
||||
case "NotifySilentUpdateRestartNeeded" :
|
||||
go.notifySilentUpdateRestartNeeded()
|
||||
break;
|
||||
case "NotifySilentUpdateError" :
|
||||
go.notifySilentUpdateError()
|
||||
break;
|
||||
//case "NotifySilentUpdateRestartNeeded" :
|
||||
//go.notifySilentUpdateRestartNeeded()
|
||||
//break;
|
||||
//case "NotifySilentUpdateError" :
|
||||
//go.notifySilentUpdateError()
|
||||
//break;
|
||||
case "ImportStructure" :
|
||||
testgui.winMain.dialogImport.address = "cuto@pm.com"
|
||||
testgui.winMain.dialogImport.show()
|
||||
@ -836,7 +836,7 @@ Window {
|
||||
id: go
|
||||
|
||||
property int isAutoStart : 1
|
||||
property bool isAutoUpdate : false
|
||||
//property bool isAutoUpdate : false
|
||||
property bool isFirstStart : false
|
||||
property string currentAddress : "none"
|
||||
//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 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 bool updateCanInstall: true
|
||||
property bool updateCanInstall: false
|
||||
property string updateLandingPage : "https://protonmail.com/import-export/download/"
|
||||
property string updateReleaseNotesLink : "https://protonmail.com/download/ie/release_notes.html"
|
||||
signal notifyManualUpdate()
|
||||
signal notifyManualUpdateRestartNeeded()
|
||||
signal notifyManualUpdateError()
|
||||
signal notifyForceUpdate()
|
||||
signal notifySilentUpdateRestartNeeded()
|
||||
signal notifySilentUpdateError()
|
||||
//signal notifySilentUpdateRestartNeeded()
|
||||
//signal notifySilentUpdateError()
|
||||
function checkForUpdates() {
|
||||
console.log("checkForUpdates")
|
||||
go.notifyVersionIsTheLatest()
|
||||
@ -900,7 +901,6 @@ Window {
|
||||
signal showQuit()
|
||||
|
||||
signal notifyVersionIsTheLatest()
|
||||
signal setUpdateState(string updateState)
|
||||
|
||||
signal showMainWin()
|
||||
signal hideMainWin()
|
||||
@ -1331,10 +1331,6 @@ Window {
|
||||
return (fname!="fail")
|
||||
}
|
||||
|
||||
function checkInternet() {
|
||||
// nothing to do
|
||||
}
|
||||
|
||||
function loadImportReports(fname) {
|
||||
console.log("load import reports for ", fname)
|
||||
}
|
||||
@ -1355,10 +1351,10 @@ Window {
|
||||
return !fname.includes("fail")
|
||||
}
|
||||
|
||||
onToggleAutoUpdate: {
|
||||
workAndClose()
|
||||
isAutoUpdate = (isAutoUpdate!=false) ? false : true
|
||||
console.log (" Test: onToggleAutoUpdate "+isAutoUpdate)
|
||||
}
|
||||
//onToggleAutoUpdate: {
|
||||
// workAndClose()
|
||||
// isAutoUpdate = (isAutoUpdate!=false) ? false : true
|
||||
// console.log (" Test: onToggleAutoUpdate "+isAutoUpdate)
|
||||
//}
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// +build !nogui
|
||||
// +build build_qt
|
||||
|
||||
package qtcommon
|
||||
|
||||
|
||||
@ -15,11 +15,12 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// +build !nogui
|
||||
// +build build_qt
|
||||
|
||||
package qtcommon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
@ -164,7 +165,7 @@ func (a *Accounts) showLoginError(err error, scope string) bool {
|
||||
return false
|
||||
}
|
||||
log.Warnf("%s: %v", scope, err)
|
||||
if err == pmapi.ErrAPINotReachable {
|
||||
if err == pmapi.ErrNoConnection {
|
||||
a.qml.SetConnectionStatus(false)
|
||||
SendNotification(a.qml, TabAccount, a.qml.CanNotReachAPI())
|
||||
a.qml.ProcessFinished()
|
||||
@ -185,7 +186,7 @@ func (a *Accounts) showLoginError(err error, scope string) bool {
|
||||
// 2: when has no 2FA but have MBOX
|
||||
func (a *Accounts) Login(login, password string) int {
|
||||
var err error
|
||||
a.authClient, a.auth, err = a.um.Login(login, password)
|
||||
a.authClient, a.auth, err = a.um.Login(login, []byte(password))
|
||||
if a.showLoginError(err, "login") {
|
||||
return -1
|
||||
}
|
||||
@ -207,7 +208,7 @@ func (a *Accounts) Auth2FA(twoFacAuth string) int {
|
||||
if a.auth == nil || a.authClient == nil {
|
||||
err = fmt.Errorf("missing authentication in auth2FA %p %p", a.auth, a.authClient)
|
||||
} else {
|
||||
err = a.authClient.Auth2FA(twoFacAuth, a.auth)
|
||||
err = a.authClient.Auth2FA(context.Background(), twoFacAuth)
|
||||
}
|
||||
|
||||
if a.showLoginError(err, "auth2FA") {
|
||||
@ -229,7 +230,7 @@ func (a *Accounts) AddAccount(mailboxPassword string) int {
|
||||
return -1
|
||||
}
|
||||
|
||||
user, err := a.um.FinishLogin(a.authClient, a.auth, mailboxPassword)
|
||||
user, err := a.um.FinishLogin(a.authClient, a.auth, []byte(mailboxPassword))
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Login was unsuccessful")
|
||||
a.qml.SetAddAccountWarning("Failure: "+err.Error(), -2)
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// +build !nogui
|
||||
// +build build_qt
|
||||
|
||||
#include "common.h"
|
||||
#include "_cgo_export.h"
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// +build !nogui
|
||||
// +build build_qt
|
||||
|
||||
package qtcommon
|
||||
|
||||
@ -113,10 +113,3 @@ type Listener interface {
|
||||
Add(string, chan<- string)
|
||||
RetryEmit(string)
|
||||
}
|
||||
|
||||
func MakeAndRegisterEvent(eventListener Listener, event string) <-chan string {
|
||||
ch := make(chan string)
|
||||
eventListener.Add(event, ch)
|
||||
eventListener.RetryEmit(event)
|
||||
return ch
|
||||
}
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// +build !nogui
|
||||
// +build build_qt
|
||||
|
||||
package qtcommon
|
||||
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// +build !nogui
|
||||
// +build build_qt
|
||||
|
||||
package qtie
|
||||
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// +build !nogui
|
||||
// +build build_qt
|
||||
|
||||
package qtie
|
||||
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// +build !nogui
|
||||
// +build build_qt
|
||||
|
||||
package qtie
|
||||
|
||||
@ -29,7 +29,7 @@ const (
|
||||
TypeMBOX = "MBOX"
|
||||
)
|
||||
|
||||
func (f *FrontendQt) LoadStructureForExport(addressOrID string) {
|
||||
func (f *FrontendQt) LoadStructureForExport(username, addressOrID string) {
|
||||
errCode := errUnknownError
|
||||
var err error
|
||||
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.
|
||||
errCode = errPMLoadFailed
|
||||
return
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// +build !nogui
|
||||
// +build build_qt
|
||||
|
||||
package qtie
|
||||
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// +build !nogui
|
||||
// +build build_qt
|
||||
|
||||
package qtie
|
||||
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// +build !nogui
|
||||
// +build build_qt
|
||||
|
||||
package qtie
|
||||
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// +build !nogui
|
||||
// +build build_qt
|
||||
|
||||
package qtie
|
||||
|
||||
@ -135,24 +135,24 @@ func (f *FrontendQt) SetVersion(version updater.VersionInfo) {
|
||||
}
|
||||
|
||||
func (f *FrontendQt) NotifySilentUpdateInstalled() {
|
||||
f.Qml.NotifySilentUpdateRestartNeeded()
|
||||
//f.Qml.NotifySilentUpdateRestartNeeded()
|
||||
}
|
||||
|
||||
func (f *FrontendQt) NotifySilentUpdateError(err error) {
|
||||
f.Qml.NotifySilentUpdateError()
|
||||
//f.Qml.NotifySilentUpdateError()
|
||||
}
|
||||
|
||||
func (f *FrontendQt) watchEvents() {
|
||||
credentialsErrorCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.CredentialsErrorEvent)
|
||||
internetOffCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.InternetOffEvent)
|
||||
internetOnCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.InternetOnEvent)
|
||||
secondInstanceCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.SecondInstanceEvent)
|
||||
restartBridgeCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.RestartBridgeEvent)
|
||||
addressChangedCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.AddressChangedEvent)
|
||||
addressChangedLogoutCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.AddressChangedLogoutEvent)
|
||||
logoutCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.LogoutEvent)
|
||||
updateApplicationCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.UpgradeApplicationEvent)
|
||||
newUserCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.UserRefreshEvent)
|
||||
credentialsErrorCh := f.eventListener.ProvideChannel(events.CredentialsErrorEvent)
|
||||
internetOffCh := f.eventListener.ProvideChannel(events.InternetOffEvent)
|
||||
internetOnCh := f.eventListener.ProvideChannel(events.InternetOnEvent)
|
||||
secondInstanceCh := f.eventListener.ProvideChannel(events.SecondInstanceEvent)
|
||||
restartBridgeCh := f.eventListener.ProvideChannel(events.RestartBridgeEvent)
|
||||
addressChangedCh := f.eventListener.ProvideChannel(events.AddressChangedEvent)
|
||||
addressChangedLogoutCh := f.eventListener.ProvideChannel(events.AddressChangedLogoutEvent)
|
||||
logoutCh := f.eventListener.ProvideChannel(events.LogoutEvent)
|
||||
updateApplicationCh := f.eventListener.ProvideChannel(events.UpgradeApplicationEvent)
|
||||
newUserCh := f.eventListener.ProvideChannel(events.UserRefreshEvent)
|
||||
for {
|
||||
select {
|
||||
case <-credentialsErrorCh:
|
||||
@ -245,11 +245,11 @@ func (f *FrontendQt) QtExecute(Procedure func(*FrontendQt) error) error {
|
||||
f.Qml.SetCredits(importexport.Credits)
|
||||
f.Qml.SetFullversion(f.buildVersion)
|
||||
|
||||
if f.settings.GetBool(settings.AutoUpdateKey) {
|
||||
f.Qml.SetIsAutoUpdate(true)
|
||||
} else {
|
||||
f.Qml.SetIsAutoUpdate(false)
|
||||
}
|
||||
//if f.settings.GetBool(settings.AutoUpdateKey) {
|
||||
// f.Qml.SetIsAutoUpdate(true)
|
||||
//} else {
|
||||
// f.Qml.SetIsAutoUpdate(false)
|
||||
//}
|
||||
|
||||
go func() {
|
||||
defer f.panicHandler.HandlePanic()
|
||||
@ -339,22 +339,17 @@ func (f *FrontendQt) sendBug(description, emailClient, address string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (f *FrontendQt) toggleAutoUpdate() {
|
||||
defer f.Qml.ProcessFinished()
|
||||
|
||||
if f.settings.GetBool(settings.AutoUpdateKey) {
|
||||
f.settings.SetBool(settings.AutoUpdateKey, false)
|
||||
f.Qml.SetIsAutoUpdate(false)
|
||||
} else {
|
||||
f.settings.SetBool(settings.AutoUpdateKey, 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) toggleAutoUpdate() {
|
||||
// defer f.Qml.ProcessFinished()
|
||||
//
|
||||
// if f.settings.GetBool(settings.AutoUpdateKey) {
|
||||
// f.settings.SetBool(settings.AutoUpdateKey, false)
|
||||
// f.Qml.SetIsAutoUpdate(false)
|
||||
// } else {
|
||||
// f.settings.SetBool(settings.AutoUpdateKey, true)
|
||||
// f.Qml.SetIsAutoUpdate(true)
|
||||
// }
|
||||
//}
|
||||
|
||||
func (f *FrontendQt) showError(code int, err error) {
|
||||
f.Qml.SetErrorDescription(err.Error())
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// +build nogui
|
||||
// +build !build_qt
|
||||
|
||||
package qtie
|
||||
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// +build !nogui
|
||||
// +build build_qt
|
||||
|
||||
package qtie
|
||||
|
||||
@ -26,7 +26,7 @@ import (
|
||||
)
|
||||
|
||||
// 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
|
||||
var err error
|
||||
defer func() {
|
||||
@ -39,7 +39,7 @@ func (f *FrontendQt) setupAndLoadForImport(isFromIMAP bool, sourcePath, sourceEm
|
||||
}()
|
||||
|
||||
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 {
|
||||
switch {
|
||||
case errors.Is(err, &transfer.ErrIMAPConnection{}):
|
||||
@ -54,7 +54,7 @@ func (f *FrontendQt) setupAndLoadForImport(isFromIMAP bool, sourcePath, sourceEm
|
||||
return
|
||||
}
|
||||
} else {
|
||||
f.transfer, err = f.ie.GetLocalImporter(targetAddress, sourcePath)
|
||||
f.transfer, err = f.ie.GetLocalImporter(targetUsername, targetAddress, sourcePath)
|
||||
if err != nil {
|
||||
// The only error can be problem to load PM user and address.
|
||||
errCode = errPMLoadFailed
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// +build !nogui
|
||||
// +build build_qt
|
||||
|
||||
package qtie
|
||||
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// +build !nogui
|
||||
// +build build_qt
|
||||
|
||||
package qtie
|
||||
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// +build !nogui
|
||||
// +build build_qt
|
||||
|
||||
package qtie
|
||||
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// +build !nogui
|
||||
// +build build_qt
|
||||
|
||||
package qtie
|
||||
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// +build !nogui
|
||||
// +build build_qt
|
||||
|
||||
package qtie
|
||||
|
||||
@ -33,7 +33,7 @@ type GoQMLInterface struct {
|
||||
|
||||
_ func() `constructor:"init"`
|
||||
|
||||
_ bool `property:"isAutoUpdate"`
|
||||
//_ bool `property:"isAutoUpdate"`
|
||||
_ string `property:"currentAddress"`
|
||||
_ string `property:"goos"`
|
||||
_ string `property:"credits"`
|
||||
@ -53,6 +53,7 @@ type GoQMLInterface struct {
|
||||
_ string `property:"fullversion"`
|
||||
_ string `property:"downloadLink"`
|
||||
|
||||
_ string `property:"updateState"`
|
||||
_ string `property:"updateVersion"`
|
||||
_ bool `property:"updateCanInstall"`
|
||||
_ string `property:"updateLandingPage"`
|
||||
@ -61,8 +62,8 @@ type GoQMLInterface struct {
|
||||
_ func() `signal:"notifyManualUpdateRestartNeeded"`
|
||||
_ func() `signal:"notifyManualUpdateError"`
|
||||
_ func() `signal:"notifyForceUpdate"`
|
||||
_ func() `signal:"notifySilentUpdateRestartNeeded"`
|
||||
_ func() `signal:"notifySilentUpdateError"`
|
||||
//_ func() `signal:"notifySilentUpdateRestartNeeded"`
|
||||
//_ func() `signal:"notifySilentUpdateError"`
|
||||
_ func() `slot:"checkForUpdates"`
|
||||
_ func() `slot:"checkAndOpenReleaseNotes"`
|
||||
_ func() `signal:"openReleaseNotesExternally"`
|
||||
@ -76,9 +77,7 @@ type GoQMLInterface struct {
|
||||
_ string `property:"credentialsNotRemoved"`
|
||||
_ string `property:"versionCheckFailed"`
|
||||
//
|
||||
_ func(isAvailable bool) `signal:"setConnectionStatus"`
|
||||
_ func(updateState string) `signal:"setUpdateState"`
|
||||
_ func() `slot:"checkInternet"`
|
||||
_ func(isAvailable bool) `signal:"setConnectionStatus"`
|
||||
|
||||
_ func() `slot:"setToRestart"`
|
||||
|
||||
@ -93,7 +92,7 @@ type GoQMLInterface struct {
|
||||
|
||||
_ func() `signal:"showWindow"`
|
||||
|
||||
_ func() `slot:"toggleAutoUpdate"`
|
||||
//_ func() `slot:"toggleAutoUpdate"`
|
||||
_ func() `slot:"quit"`
|
||||
_ func() `slot:"loadAccounts"`
|
||||
_ func() `slot:"openLogs"`
|
||||
@ -108,14 +107,14 @@ type GoQMLInterface struct {
|
||||
|
||||
_ func(description, client, address string) bool `slot:"sendBug"`
|
||||
_ func(address string) bool `slot:"sendImportReport"`
|
||||
_ func(address string) `slot:"loadStructureForExport"`
|
||||
_ func(username, address string) `slot:"loadStructureForExport"`
|
||||
_ func() string `slot:"leastUsedColor"`
|
||||
_ func(username string, name string, color string, isLabel bool, sourceID string) bool `slot:"createLabelOrFolder"`
|
||||
_ func(fpath, address, fileType string, attachEncryptedBody bool) `slot:"startExport"`
|
||||
_ func(email string, importEncrypted bool) `slot:"startImport"`
|
||||
_ 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"`
|
||||
|
||||
@ -162,7 +161,7 @@ func (s *GoQMLInterface) init() {}
|
||||
func (s *GoQMLInterface) SetFrontend(f *FrontendQt) {
|
||||
s.ConnectQuit(f.App.Quit)
|
||||
|
||||
s.ConnectToggleAutoUpdate(f.toggleAutoUpdate)
|
||||
//s.ConnectToggleAutoUpdate(f.toggleAutoUpdate)
|
||||
s.ConnectLoadAccounts(f.Accounts.LoadAccounts)
|
||||
s.ConnectOpenLogs(f.openLogs)
|
||||
s.ConnectOpenDownloadLink(f.openDownloadLink)
|
||||
@ -189,8 +188,6 @@ func (s *GoQMLInterface) SetFrontend(f *FrontendQt) {
|
||||
return f.programVersion
|
||||
})
|
||||
|
||||
s.ConnectCheckInternet(f.checkInternet)
|
||||
|
||||
s.ConnectSetToRestart(f.restarter.SetToRestart)
|
||||
|
||||
s.ConnectLoadStructureForExport(f.LoadStructureForExport)
|
||||
@ -207,4 +204,6 @@ func (s *GoQMLInterface) SetFrontend(f *FrontendQt) {
|
||||
s.ConnectCheckPathStatus(CheckPathStatus)
|
||||
|
||||
s.ConnectEmitEvent(f.emitEvent)
|
||||
|
||||
s.ConnectStartManualUpdate(f.startManualUpdate)
|
||||
}
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// +build !nogui
|
||||
// +build build_qt
|
||||
|
||||
package qt
|
||||
|
||||
|
||||
@ -15,11 +15,12 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// +build !nogui
|
||||
// +build build_qt
|
||||
|
||||
package qt
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
@ -84,7 +85,7 @@ func (s *FrontendQt) clearCache() {
|
||||
|
||||
channel := s.bridge.GetUpdateChannel()
|
||||
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()
|
||||
return
|
||||
}
|
||||
@ -130,7 +131,7 @@ func (s *FrontendQt) showLoginError(err error, scope string) bool {
|
||||
return false
|
||||
}
|
||||
log.Warnf("%s: %v", scope, err)
|
||||
if err == pmapi.ErrAPINotReachable {
|
||||
if err == pmapi.ErrNoConnection {
|
||||
s.Qml.SetConnectionStatus(false)
|
||||
s.SendNotification(TabAccount, s.Qml.CanNotReachAPI())
|
||||
s.Qml.ProcessFinished()
|
||||
@ -151,7 +152,7 @@ func (s *FrontendQt) showLoginError(err error, scope string) bool {
|
||||
// 2: when has no 2FA but have MBOX
|
||||
func (s *FrontendQt) login(login, password string) int {
|
||||
var err error
|
||||
s.authClient, s.auth, err = s.bridge.Login(login, password)
|
||||
s.authClient, s.auth, err = s.bridge.Login(login, []byte(password))
|
||||
if s.showLoginError(err, "login") {
|
||||
return -1
|
||||
}
|
||||
@ -173,7 +174,7 @@ func (s *FrontendQt) auth2FA(twoFacAuth string) int {
|
||||
if s.auth == nil || s.authClient == nil {
|
||||
err = fmt.Errorf("missing authentication in auth2FA %p %p", s.auth, s.authClient)
|
||||
} else {
|
||||
err = s.authClient.Auth2FA(twoFacAuth, s.auth)
|
||||
err = s.authClient.Auth2FA(context.Background(), twoFacAuth)
|
||||
}
|
||||
|
||||
if s.showLoginError(err, "auth2FA") {
|
||||
@ -194,7 +195,7 @@ func (s *FrontendQt) addAccount(mailboxPassword string) int {
|
||||
return -1
|
||||
}
|
||||
|
||||
user, err := s.bridge.FinishLogin(s.authClient, s.auth, mailboxPassword)
|
||||
user, err := s.bridge.FinishLogin(s.authClient, s.auth, []byte(mailboxPassword))
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Login was unsuccessful")
|
||||
s.Qml.SetAddAccountWarning("Failure: "+err.Error(), -2)
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// +build !nogui
|
||||
// +build build_qt
|
||||
|
||||
// Package qt is the Qt User interface for Desktop bridge.
|
||||
//
|
||||
@ -39,16 +39,17 @@ import (
|
||||
"github.com/ProtonMail/go-autostart"
|
||||
"github.com/ProtonMail/proton-bridge/internal/bridge"
|
||||
"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/frontend/autoconfig"
|
||||
qtcommon "github.com/ProtonMail/proton-bridge/internal/frontend/qt-common"
|
||||
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
|
||||
"github.com/ProtonMail/proton-bridge/internal/locations"
|
||||
"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/pmapi"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/ports"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/useragent"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/skratchdot/open-golang/open"
|
||||
"github.com/therecipe/qt/core"
|
||||
@ -74,6 +75,7 @@ type FrontendQt struct {
|
||||
settings *settings.Settings
|
||||
eventListener listener.Listener
|
||||
updater types.Updater
|
||||
userAgent *useragent.UserAgent
|
||||
bridge types.Bridger
|
||||
noEncConfirmator types.NoEncConfirmator
|
||||
|
||||
@ -113,12 +115,15 @@ func New(
|
||||
settings *settings.Settings,
|
||||
eventListener listener.Listener,
|
||||
updater types.Updater,
|
||||
userAgent *useragent.UserAgent,
|
||||
bridge types.Bridger,
|
||||
noEncConfirmator types.NoEncConfirmator,
|
||||
autostart *autostart.App,
|
||||
restarter types.Restarter,
|
||||
) *FrontendQt {
|
||||
tmp := &FrontendQt{
|
||||
userAgent.SetPlatform(core.QSysInfo_PrettyProductName())
|
||||
|
||||
f := &FrontendQt{
|
||||
version: version,
|
||||
buildVersion: buildVersion,
|
||||
programName: programName,
|
||||
@ -128,6 +133,7 @@ func New(
|
||||
settings: settings,
|
||||
eventListener: eventListener,
|
||||
updater: updater,
|
||||
userAgent: userAgent,
|
||||
bridge: bridge,
|
||||
noEncConfirmator: noEncConfirmator,
|
||||
programVer: "v" + version,
|
||||
@ -137,13 +143,9 @@ func New(
|
||||
|
||||
// Initializing.Done is only called sync.Once. Please keep the increment
|
||||
// set to 1
|
||||
tmp.initializing.Add(1)
|
||||
f.initializing.Add(1)
|
||||
|
||||
// Nicer string for OS.
|
||||
currentOS := core.QSysInfo_PrettyProductName()
|
||||
bridge.SetCurrentOS(currentOS)
|
||||
|
||||
return tmp
|
||||
return f
|
||||
}
|
||||
|
||||
// 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() {
|
||||
s.WaitUntilFrontendIsReady()
|
||||
|
||||
errorCh := s.getEventChannel(events.ErrorEvent)
|
||||
credentialsErrorCh := s.getEventChannel(events.CredentialsErrorEvent)
|
||||
outgoingNoEncCh := s.getEventChannel(events.OutgoingNoEncEvent)
|
||||
noActiveKeyForRecipientCh := s.getEventChannel(events.NoActiveKeyForRecipientEvent)
|
||||
internetOffCh := s.getEventChannel(events.InternetOffEvent)
|
||||
internetOnCh := s.getEventChannel(events.InternetOnEvent)
|
||||
secondInstanceCh := s.getEventChannel(events.SecondInstanceEvent)
|
||||
restartBridgeCh := s.getEventChannel(events.RestartBridgeEvent)
|
||||
addressChangedCh := s.getEventChannel(events.AddressChangedEvent)
|
||||
addressChangedLogoutCh := s.getEventChannel(events.AddressChangedLogoutEvent)
|
||||
logoutCh := s.getEventChannel(events.LogoutEvent)
|
||||
updateApplicationCh := s.getEventChannel(events.UpgradeApplicationEvent)
|
||||
newUserCh := s.getEventChannel(events.UserRefreshEvent)
|
||||
certIssue := s.getEventChannel(events.TLSCertIssue)
|
||||
errorCh := s.eventListener.ProvideChannel(events.ErrorEvent)
|
||||
credentialsErrorCh := s.eventListener.ProvideChannel(events.CredentialsErrorEvent)
|
||||
outgoingNoEncCh := s.eventListener.ProvideChannel(events.OutgoingNoEncEvent)
|
||||
noActiveKeyForRecipientCh := s.eventListener.ProvideChannel(events.NoActiveKeyForRecipientEvent)
|
||||
internetOffCh := s.eventListener.ProvideChannel(events.InternetOffEvent)
|
||||
internetOnCh := s.eventListener.ProvideChannel(events.InternetOnEvent)
|
||||
secondInstanceCh := s.eventListener.ProvideChannel(events.SecondInstanceEvent)
|
||||
restartBridgeCh := s.eventListener.ProvideChannel(events.RestartBridgeEvent)
|
||||
addressChangedCh := s.eventListener.ProvideChannel(events.AddressChangedEvent)
|
||||
addressChangedLogoutCh := s.eventListener.ProvideChannel(events.AddressChangedLogoutEvent)
|
||||
logoutCh := s.eventListener.ProvideChannel(events.LogoutEvent)
|
||||
updateApplicationCh := s.eventListener.ProvideChannel(events.UpgradeApplicationEvent)
|
||||
newUserCh := s.eventListener.ProvideChannel(events.UserRefreshEvent)
|
||||
certIssue := s.eventListener.ProvideChannel(events.TLSCertIssue)
|
||||
for {
|
||||
select {
|
||||
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.
|
||||
//
|
||||
// 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.SetFullversion(s.buildVersion)
|
||||
|
||||
// Autostart.
|
||||
if s.Qml.IsFirstStart() {
|
||||
if s.autostart.IsEnabled() {
|
||||
// Autostart: rewrite the current definition of autostart
|
||||
// - when it is the first time
|
||||
// - 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 {
|
||||
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.toggleAutoStart()
|
||||
}
|
||||
if s.autostart.IsEnabled() {
|
||||
s.Qml.SetIsAutoStart(true)
|
||||
} else {
|
||||
s.Qml.SetIsAutoStart(false)
|
||||
if err := s.autostart.Enable(); err != nil {
|
||||
log.
|
||||
WithField("first", s.Qml.IsFirstStart()).
|
||||
WithField("wasEnabled", isAutoStartEnabled).
|
||||
WithError(err).
|
||||
Error("Enable on start failed.")
|
||||
s.autostartError(err)
|
||||
}
|
||||
}
|
||||
s.Qml.SetIsAutoStart(s.autostart.IsEnabled())
|
||||
|
||||
if s.settings.GetBool(settings.AutoUpdateKey) {
|
||||
s.Qml.SetIsAutoUpdate(true)
|
||||
} else {
|
||||
s.Qml.SetIsAutoUpdate(false)
|
||||
}
|
||||
s.Qml.SetIsAutoUpdate(s.settings.GetBool(settings.AutoUpdateKey))
|
||||
s.Qml.SetIsProxyAllowed(s.settings.GetBool(settings.AllowProxyKey))
|
||||
s.Qml.SetIsEarlyAccess(updater.UpdateChannel(s.settings.Get(settings.UpdateChannelKey)) == updater.EarlyChannel)
|
||||
|
||||
if s.settings.GetBool(settings.AllowProxyKey) {
|
||||
s.Qml.SetIsProxyAllowed(true)
|
||||
} else {
|
||||
s.Qml.SetIsProxyAllowed(false)
|
||||
}
|
||||
|
||||
if updater.UpdateChannel(s.settings.Get(settings.UpdateChannelKey)) == updater.EarlyChannel {
|
||||
s.Qml.SetIsEarlyAccess(true)
|
||||
} else {
|
||||
s.Qml.SetIsEarlyAccess(false)
|
||||
availableKeychain := []string{}
|
||||
for chain := range keychain.Helpers {
|
||||
availableKeychain = append(availableKeychain, chain)
|
||||
}
|
||||
s.Qml.SetAvailableKeychain(availableKeychain)
|
||||
s.Qml.SetSelectedKeychain(s.settings.Get(settings.PreferredKeychainKey))
|
||||
|
||||
// Set reporting of outgoing email without encryption.
|
||||
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 {
|
||||
return s.bridge.GetCurrentClient()
|
||||
return s.userAgent.String()
|
||||
}
|
||||
|
||||
func (s *FrontendQt) configureAppleMail(iAccount, iAddress int) {
|
||||
@ -547,20 +549,22 @@ func (s *FrontendQt) configureAppleMail(iAccount, iAddress int) {
|
||||
func (s *FrontendQt) toggleAutoStart() {
|
||||
defer s.Qml.ProcessFinished()
|
||||
var err error
|
||||
if s.autostart.IsEnabled() {
|
||||
wasEnabled := s.autostart.IsEnabled()
|
||||
if wasEnabled {
|
||||
err = s.autostart.Disable()
|
||||
} else {
|
||||
err = s.autostart.Enable()
|
||||
}
|
||||
isEnabled := s.autostart.IsEnabled()
|
||||
if err != nil {
|
||||
log.Error("Enable autostart: ", err)
|
||||
log.
|
||||
WithField("wasEnabled", wasEnabled).
|
||||
WithField("isEnabled", isEnabled).
|
||||
WithError(err).
|
||||
Error("Autostart change failed.")
|
||||
s.autostartError(err)
|
||||
}
|
||||
if s.autostart.IsEnabled() {
|
||||
s.Qml.SetIsAutoStart(true)
|
||||
} else {
|
||||
s.Qml.SetIsAutoStart(false)
|
||||
}
|
||||
s.Qml.SetIsAutoStart(isEnabled)
|
||||
}
|
||||
|
||||
func (s *FrontendQt) toggleAutoUpdate() {
|
||||
@ -585,14 +589,16 @@ func (s *FrontendQt) toggleEarlyAccess() {
|
||||
channel = updater.EarlyChannel
|
||||
}
|
||||
|
||||
err := s.bridge.SetUpdateChannel(channel)
|
||||
needRestart, err := s.bridge.SetUpdateChannel(channel)
|
||||
s.Qml.SetIsEarlyAccess(channel == updater.EarlyChannel)
|
||||
if err != nil {
|
||||
s.Qml.NotifyManualUpdateError()
|
||||
return
|
||||
}
|
||||
s.restarter.SetToRestart()
|
||||
s.App.Quit()
|
||||
if needRestart {
|
||||
s.restarter.SetToRestart()
|
||||
s.App.Quit()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *FrontendQt) toggleAllowProxy() {
|
||||
@ -640,10 +646,6 @@ func (s *FrontendQt) isSMTPSTARTTLS() bool {
|
||||
return !s.settings.GetBool(settings.SMTPSSLKey)
|
||||
}
|
||||
|
||||
func (s *FrontendQt) checkInternet() {
|
||||
s.Qml.SetConnectionStatus(s.bridge.CheckConnection() == nil)
|
||||
}
|
||||
|
||||
func (s *FrontendQt) switchAddressModeUser(iAccount int) {
|
||||
defer s.Qml.ProcessFinished()
|
||||
userID := s.Accounts.get(iAccount).UserID()
|
||||
@ -711,3 +713,16 @@ func (s *FrontendQt) setGUIIsReady() {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// +build nogui
|
||||
// +build !build_qt
|
||||
|
||||
package qt
|
||||
|
||||
@ -25,6 +25,7 @@ import (
|
||||
|
||||
"github.com/ProtonMail/go-autostart"
|
||||
"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/locations"
|
||||
"github.com/ProtonMail/proton-bridge/internal/updater"
|
||||
@ -71,6 +72,7 @@ func New(
|
||||
settings *settings.Settings,
|
||||
eventListener listener.Listener,
|
||||
updater types.Updater,
|
||||
userAgent *useragent.UserAgent,
|
||||
bridge types.Bridger,
|
||||
noEncConfirmator types.NoEncConfirmator,
|
||||
autostart *autostart.App,
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// +build !nogui
|
||||
// +build build_qt
|
||||
|
||||
package qt
|
||||
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// +build !nogui
|
||||
// +build build_qt
|
||||
|
||||
package qt
|
||||
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// +build !nogui
|
||||
// +build build_qt
|
||||
|
||||
package qt
|
||||
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// +build !nogui
|
||||
// +build build_qt
|
||||
|
||||
package qt
|
||||
|
||||
@ -50,6 +50,7 @@ type GoQMLInterface struct {
|
||||
_ string `property:"fullversion"`
|
||||
_ string `property:"downloadLink"`
|
||||
|
||||
_ string `property:"updateState"`
|
||||
_ string `property:"updateVersion"`
|
||||
_ bool `property:"updateCanInstall"`
|
||||
_ string `property:"updateLandingPage"`
|
||||
@ -66,6 +67,9 @@ type GoQMLInterface struct {
|
||||
_ func() `slot:"startManualUpdate"`
|
||||
_ func() `slot:"guiIsReady"`
|
||||
|
||||
_ []string `property:"availableKeychain"`
|
||||
_ string `property:"selectedKeychain"`
|
||||
|
||||
// Translations.
|
||||
_ string `property:"wrongCredentials"`
|
||||
_ string `property:"wrongMailboxPassword"`
|
||||
@ -79,9 +83,7 @@ type GoQMLInterface struct {
|
||||
_ float32 `property:"progress"`
|
||||
_ string `property:"progressDescription"`
|
||||
|
||||
_ func(isAvailable bool) `signal:"setConnectionStatus"`
|
||||
_ func(updateState string) `signal:"setUpdateState"`
|
||||
_ func() `slot:"checkInternet"`
|
||||
_ func(isAvailable bool) `signal:"setConnectionStatus"`
|
||||
|
||||
_ func() `slot:"setToRestart"`
|
||||
|
||||
@ -202,11 +204,12 @@ func (s *GoQMLInterface) SetFrontend(f *FrontendQt) {
|
||||
return f.programVer
|
||||
})
|
||||
|
||||
s.ConnectCheckInternet(f.checkInternet)
|
||||
|
||||
s.ConnectSetToRestart(f.restarter.SetToRestart)
|
||||
|
||||
s.ConnectToggleIsReportingOutgoingNoEnc(f.toggleIsReportingOutgoingNoEnc)
|
||||
s.ConnectShouldSendAnswer(f.shouldSendAnswer)
|
||||
s.ConnectSaveOutgoingNoEncPopupCoord(f.saveOutgoingNoEncPopupCoord)
|
||||
|
||||
s.ConnectSetSelectedKeychain(f.setKeychain)
|
||||
s.ConnectSelectedKeychain(f.getKeychain)
|
||||
}
|
||||
|
||||
@ -61,6 +61,7 @@
|
||||
<file alias="BubbleMenu.qml" >./qml/BridgeUI/BubbleMenu.qml</file>
|
||||
<file alias="Credits.qml" >./qml/BridgeUI/Credits.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="DialogYesNo.qml" >./qml/BridgeUI/DialogYesNo.qml</file>
|
||||
<file alias="DialogTLSCertInfo.qml" >./qml/BridgeUI/DialogTLSCertInfo.qml</file>
|
||||
|
||||
@ -1 +0,0 @@
|
||||
IDI_ICON1 ICON DISCARDABLE "logo.ico"
|
||||
45
internal/frontend/share/info.rc
Normal file
45
internal/frontend/share/info.rc
Normal file
@ -0,0 +1,45 @@
|
||||
#define STRINGIZE_(x) #x
|
||||
#define STRINGIZE(x) STRINGIZE_(x)
|
||||
|
||||
IDI_ICON1 ICON DISCARDABLE STRINGIZE(ICO_FILE)
|
||||
|
||||
#if defined BUILD_BRIDGE
|
||||
#define FILE_COMMENTS "The Bridge is an application that runs on your computer in the background and seamlessly encrypts and decrypts your mail as it enters and leaves your computer."
|
||||
#define FILE_DESCRIPTION "ProtonMail Bridge"
|
||||
#define INTERNAL_NAME STRINGIZE(EXE_NAME)
|
||||
#define PRODUCT_NAME "ProtonMail Bridge for Windows"
|
||||
#elif defined BUILD_IE
|
||||
#define FILE_COMMENTS "The Import-Export app helps you to migrate your emails from local files or remote IMAP servers to ProtonMail or simply export emails to local folder."
|
||||
#define FILE_DESCRIPTION "ProtonMail Import-Export app"
|
||||
#define INTERNAL_NAME STRINGIZE(EXE_NAME)
|
||||
#define PRODUCT_NAME "ProtonMail Import-Export app for Windows"
|
||||
#else
|
||||
#error No target specified
|
||||
#endif
|
||||
|
||||
#define LEGAL_COPYRIGHT "(C) " STRINGIZE(YEAR) " Proton Technologies AG"
|
||||
|
||||
1 VERSIONINFO
|
||||
FILEVERSION FILE_VERSION_COMMA,0
|
||||
PRODUCTVERSION FILE_VERSION_COMMA,0
|
||||
BEGIN
|
||||
BLOCK "StringFileInfo"
|
||||
BEGIN
|
||||
BLOCK "040904b0"
|
||||
BEGIN
|
||||
VALUE "Comments", FILE_COMMENTS
|
||||
VALUE "CompanyName", "Proton Technologies AG"
|
||||
VALUE "FileDescription", FILE_DESCRIPTION
|
||||
VALUE "FileVersion", STRINGIZE(FILE_VERSION)
|
||||
VALUE "InternalName", INTERNAL_NAME
|
||||
VALUE "LegalCopyright", LEGAL_COPYRIGHT
|
||||
VALUE "OriginalFilename", STRINGIZE(ORIGINAL_FILE_NAME)
|
||||
VALUE "ProductName", PRODUCT_NAME
|
||||
VALUE "ProductVersion", STRINGIZE(PRODUCT_VERSION)
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
BEGIN
|
||||
VALUE "Translation", 0x0409, 0x04B0
|
||||
END
|
||||
END
|
||||
@ -49,13 +49,12 @@ type Updater interface {
|
||||
|
||||
// UserManager is an interface of users needed by frontend.
|
||||
type UserManager interface {
|
||||
Login(username, password string) (pmapi.Client, *pmapi.Auth, error)
|
||||
FinishLogin(client pmapi.Client, auth *pmapi.Auth, mailboxPassword string) (User, error)
|
||||
Login(username string, password []byte) (pmapi.Client, *pmapi.Auth, error)
|
||||
FinishLogin(client pmapi.Client, auth *pmapi.Auth, mailboxPassword []byte) (User, error)
|
||||
GetUsers() []User
|
||||
GetUser(query string) (User, error)
|
||||
DeleteUser(userID string, clearCache bool) error
|
||||
ClearData() error
|
||||
CheckConnection() error
|
||||
}
|
||||
|
||||
// User is an interface of user needed by frontend.
|
||||
@ -75,13 +74,13 @@ type User interface {
|
||||
type Bridger interface {
|
||||
UserManager
|
||||
|
||||
GetCurrentClient() string
|
||||
SetCurrentOS(os string)
|
||||
ReportBug(osType, osVersion, description, accountName, address, emailClient string) error
|
||||
AllowProxy()
|
||||
DisallowProxy()
|
||||
GetUpdateChannel() updater.UpdateChannel
|
||||
SetUpdateChannel(updater.UpdateChannel) error
|
||||
SetUpdateChannel(updater.UpdateChannel) (needRestart bool, err error)
|
||||
GetKeychainApp() string
|
||||
SetKeychainApp(keychain string)
|
||||
}
|
||||
|
||||
type bridgeWrap struct {
|
||||
@ -95,7 +94,7 @@ func NewBridgeWrap(bridge *bridge.Bridge) *bridgeWrap { //nolint[golint]
|
||||
return &bridgeWrap{Bridge: bridge}
|
||||
}
|
||||
|
||||
func (b *bridgeWrap) FinishLogin(client pmapi.Client, auth *pmapi.Auth, mailboxPassword string) (User, error) {
|
||||
func (b *bridgeWrap) FinishLogin(client pmapi.Client, auth *pmapi.Auth, mailboxPassword []byte) (User, error) {
|
||||
return b.Bridge.FinishLogin(client, auth, mailboxPassword)
|
||||
}
|
||||
|
||||
@ -114,10 +113,10 @@ func (b *bridgeWrap) GetUser(query string) (User, error) {
|
||||
type ImportExporter interface {
|
||||
UserManager
|
||||
|
||||
GetLocalImporter(string, string) (*transfer.Transfer, error)
|
||||
GetRemoteImporter(string, string, string, string, string) (*transfer.Transfer, error)
|
||||
GetEMLExporter(string, string) (*transfer.Transfer, error)
|
||||
GetMBOXExporter(string, string) (*transfer.Transfer, error)
|
||||
GetLocalImporter(string, string, string) (*transfer.Transfer, error)
|
||||
GetRemoteImporter(string, string, string, string, string, string) (*transfer.Transfer, error)
|
||||
GetEMLExporter(string, string, string) (*transfer.Transfer, error)
|
||||
GetMBOXExporter(string, string, string) (*transfer.Transfer, error)
|
||||
ReportBug(osType, osVersion, description, accountName, address, emailClient string) error
|
||||
ReportFile(osType, osVersion, accountName, address string, logdata []byte) error
|
||||
}
|
||||
@ -135,7 +134,7 @@ func NewImportExportWrap(ie *importexport.ImportExport) *importExportWrap { //no
|
||||
return &importExportWrap{ImportExport: ie}
|
||||
}
|
||||
|
||||
func (b *importExportWrap) FinishLogin(client pmapi.Client, auth *pmapi.Auth, mailboxPassword string) (User, error) {
|
||||
func (b *importExportWrap) FinishLogin(client pmapi.Client, auth *pmapi.Auth, mailboxPassword []byte) (User, error) {
|
||||
return b.ImportExport.FinishLogin(client, auth, mailboxPassword)
|
||||
}
|
||||
|
||||
|
||||
@ -16,6 +16,19 @@
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// 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
|
||||
|
||||
import (
|
||||
@ -26,10 +39,19 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/listener"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/message"
|
||||
"github.com/emersion/go-imap"
|
||||
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 {
|
||||
HandlePanic()
|
||||
}
|
||||
@ -43,6 +65,8 @@ type imapBackend struct {
|
||||
users map[string]*imapUser
|
||||
usersLocker sync.Locker
|
||||
|
||||
builder *message.Builder
|
||||
|
||||
imapCache map[string]map[string]string
|
||||
imapCachePath string
|
||||
imapCacheLock *sync.RWMutex
|
||||
@ -78,6 +102,8 @@ func newIMAPBackend(
|
||||
users: map[string]*imapUser{},
|
||||
usersLocker: &sync.Mutex{},
|
||||
|
||||
builder: message.NewBuilder(fetchWorkers, attachWorkers, buildWorkers),
|
||||
|
||||
imapCachePath: cache.GetIMAPCachePath(),
|
||||
imapCacheLock: &sync.RWMutex{},
|
||||
}
|
||||
|
||||
@ -29,7 +29,6 @@ type cacheProvider interface {
|
||||
}
|
||||
|
||||
type bridger interface {
|
||||
SetCurrentClient(clientName, clientVersion string)
|
||||
GetUser(query string) (bridgeUser, error)
|
||||
}
|
||||
|
||||
@ -39,11 +38,10 @@ type bridgeUser interface {
|
||||
IsCombinedAddressMode() bool
|
||||
GetAddressID(address string) (string, error)
|
||||
GetPrimaryAddress() string
|
||||
UpdateUser() error
|
||||
Logout() error
|
||||
CloseConnection(address string)
|
||||
GetStore() storeUserProvider
|
||||
GetTemporaryPMAPIClient() pmapi.Client
|
||||
GetClient() pmapi.Client
|
||||
}
|
||||
|
||||
type bridgeWrap struct {
|
||||
@ -62,7 +60,7 @@ func (b *bridgeWrap) GetUser(query string) (bridgeUser, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newBridgeUserWrap(user), nil
|
||||
return newBridgeUserWrap(user), nil //nolint[typecheck] missing methods are inherited
|
||||
}
|
||||
|
||||
type bridgeUserWrap struct {
|
||||
@ -78,5 +76,5 @@ func (u *bridgeUserWrap) GetStore() storeUserProvider {
|
||||
if store == nil {
|
||||
return nil
|
||||
}
|
||||
return newStoreUserWrap(store)
|
||||
return newStoreUserWrap(store) //nolint[typecheck] missing methods are inherited
|
||||
}
|
||||
|
||||
@ -23,13 +23,13 @@ import (
|
||||
)
|
||||
|
||||
type currentClientSetter interface {
|
||||
SetCurrentClient(name, version string)
|
||||
SetClient(name, version string)
|
||||
}
|
||||
|
||||
// Extension for IMAP server
|
||||
// Extension for IMAP server.
|
||||
type extension struct {
|
||||
extID imapserver.ConnExtension
|
||||
setter currentClientSetter
|
||||
extID imapserver.ConnExtension
|
||||
clientSetter currentClientSetter
|
||||
}
|
||||
|
||||
func (ext *extension) Capabilities(conn imapserver.Conn) []string {
|
||||
@ -44,8 +44,8 @@ func (ext *extension) Command(name string) imapserver.HandlerFactory {
|
||||
return func() imapserver.Handler {
|
||||
if hdlrID, ok := newIDHandler().(*imapid.Handler); ok {
|
||||
return &handler{
|
||||
hdlrID: hdlrID,
|
||||
setter: ext.setter,
|
||||
hdlrID: hdlrID,
|
||||
clientSetter: ext.clientSetter,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@ -57,8 +57,8 @@ func (ext *extension) NewConn(conn imapserver.Conn) imapserver.Conn {
|
||||
}
|
||||
|
||||
type handler struct {
|
||||
hdlrID *imapid.Handler
|
||||
setter currentClientSetter
|
||||
hdlrID *imapid.Handler
|
||||
clientSetter currentClientSetter
|
||||
}
|
||||
|
||||
func (hdlr *handler) Parse(fields []interface{}) error {
|
||||
@ -69,21 +69,18 @@ func (hdlr *handler) Handle(conn imapserver.Conn) error {
|
||||
err := hdlr.hdlrID.Handle(conn)
|
||||
if err == nil {
|
||||
id := hdlr.hdlrID.Command.ID
|
||||
hdlr.setter.SetCurrentClient(
|
||||
id[imapid.FieldName],
|
||||
id[imapid.FieldVersion],
|
||||
)
|
||||
hdlr.clientSetter.SetClient(id[imapid.FieldName], id[imapid.FieldVersion])
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// NewExtension returns extension which is adding RFC2871 ID capability, with
|
||||
// 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 {
|
||||
return &extension{
|
||||
extID: conExtID,
|
||||
setter: setter,
|
||||
extID: conExtID,
|
||||
clientSetter: clientSetter,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
85
internal/imap/idle/extension.go
Normal file
85
internal/imap/idle/extension.go
Normal file
@ -0,0 +1,85 @@
|
||||
// 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 idle
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/emersion/go-imap"
|
||||
"github.com/emersion/go-imap/server"
|
||||
)
|
||||
|
||||
const (
|
||||
idleCommand = "IDLE" // Capability and Command identificator
|
||||
doneLine = "DONE"
|
||||
)
|
||||
|
||||
// Handler for IDLE extension.
|
||||
type Handler struct{}
|
||||
|
||||
// Command for IDLE handler.
|
||||
func (h *Handler) Command() *imap.Command {
|
||||
return &imap.Command{Name: idleCommand}
|
||||
}
|
||||
|
||||
// Parse for IDLE handler.
|
||||
func (h *Handler) Parse(fields []interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Handle the IDLE request.
|
||||
func (h *Handler) Handle(conn server.Conn) error {
|
||||
cont := &imap.ContinuationReq{Info: "idling"}
|
||||
if err := conn.WriteResp(cont); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Wait for DONE
|
||||
scanner := bufio.NewScanner(conn)
|
||||
scanner.Scan()
|
||||
if err := scanner.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if strings.ToUpper(scanner.Text()) != doneLine {
|
||||
return errors.New("expected DONE")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type extension struct{}
|
||||
|
||||
func (ext *extension) Capabilities(c server.Conn) []string {
|
||||
return []string{idleCommand}
|
||||
}
|
||||
|
||||
func (ext *extension) Command(name string) server.HandlerFactory {
|
||||
if name != idleCommand {
|
||||
return nil
|
||||
}
|
||||
|
||||
return func() server.Handler {
|
||||
return &Handler{}
|
||||
}
|
||||
}
|
||||
|
||||
func NewExtension() server.Extension {
|
||||
return &extension{}
|
||||
}
|
||||
@ -19,11 +19,4 @@ package imap
|
||||
|
||||
import "github.com/sirupsen/logrus"
|
||||
|
||||
const (
|
||||
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]
|
||||
)
|
||||
var log = logrus.WithField("pkg", "imap") //nolint[gochecknoglobals]
|
||||
|
||||
@ -19,6 +19,7 @@ package imap
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/pkg/message"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
@ -36,10 +37,12 @@ type imapMailbox struct {
|
||||
storeUser storeUserProvider
|
||||
storeAddress storeAddressProvider
|
||||
storeMailbox storeMailboxProvider
|
||||
|
||||
builder *message.Builder
|
||||
}
|
||||
|
||||
// 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{
|
||||
panicHandler: panicHandler,
|
||||
user: user,
|
||||
@ -53,9 +56,30 @@ func newIMAPMailbox(panicHandler panicHandler, user *imapUser, storeMailbox stor
|
||||
storeUser: user.storeUser,
|
||||
storeAddress: user.storeAddress,
|
||||
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.
|
||||
func (im *imapMailbox) Name() string {
|
||||
// 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
|
||||
// from the currently selected mailbox.
|
||||
func (im *imapMailbox) Expunge() error {
|
||||
// Wait for any APPENDS to finish in order to avoid data loss when
|
||||
// Outlook sends commands too quickly STORE \Deleted, APPEND, EXPUNGE,
|
||||
// APPEND FINISHED:
|
||||
//
|
||||
// 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()
|
||||
// See comment of appendExpungeLock.
|
||||
if im.storeMailbox.LabelID() == pmapi.TrashLabel || im.storeMailbox.LabelID() == pmapi.SpamLabel {
|
||||
im.user.appendExpungeLock.Lock()
|
||||
defer im.user.appendExpungeLock.Unlock()
|
||||
}
|
||||
|
||||
return im.logCommand(im.expunge, "EXPUNGE")
|
||||
}
|
||||
|
||||
func (im *imapMailbox) expunge() error {
|
||||
im.user.backend.updates.block(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
|
||||
// and UID passed from SeqSet from the currently selected mailbox.
|
||||
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)
|
||||
defer im.user.backend.updates.unblock(im.user.currentAddressLowercase, im.name, operationDeleteMessage)
|
||||
|
||||
|
||||
231
internal/imap/mailbox_append.go
Normal file
231
internal/imap/mailbox_append.go
Normal file
@ -0,0 +1,231 @@
|
||||
// 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/ioutil"
|
||||
"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/emersion/go-message/textproto"
|
||||
"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(imapFlags []string, date time.Time, r imap.Literal) error { //nolint[funlen]
|
||||
// Called from go-imap in goroutines - we need to handle panics for each function.
|
||||
defer im.panicHandler.HandlePanic()
|
||||
|
||||
// NOTE: Is this lock meant to be here?
|
||||
im.user.appendExpungeLock.Lock()
|
||||
defer im.user.appendExpungeLock.Unlock()
|
||||
|
||||
body, err := ioutil.ReadAll(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
addr := im.storeAddress.APIAddress()
|
||||
if addr == nil {
|
||||
return errors.New("no available address for encryption")
|
||||
}
|
||||
|
||||
kr, err := im.user.client().KeyRingForAddressID(addr.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if im.storeMailbox.LabelID() == pmapi.DraftLabel {
|
||||
return im.createDraftMessage(kr, addr.Email, body)
|
||||
}
|
||||
|
||||
if im.storeMailbox.LabelID() == pmapi.SentLabel {
|
||||
m, _, _, _, err := message.Parse(bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if m.Sender == nil {
|
||||
m.Sender = &mail.Address{Address: addr.Email}
|
||||
}
|
||||
|
||||
if user, err := im.user.backend.bridge.GetUser(pmapi.SanitizeEmail(m.Sender.Address)); err == nil && user.ID() == im.storeUser.UserID() {
|
||||
logEntry := im.log.WithField("sender", m.Sender).WithField("extID", m.Header.Get("Message-Id")).WithField("date", date)
|
||||
|
||||
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})
|
||||
}
|
||||
|
||||
logEntry.Info("No matching UID, continuing APPEND to Sent")
|
||||
}
|
||||
}
|
||||
|
||||
hdr, err := textproto.ReadHeader(bufio.NewReader(bytes.NewReader(body)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Avoid appending a message which is already on the server. Apply the new label instead.
|
||||
// This always happens with Outlook because it uses APPEND instead of COPY.
|
||||
internalID := hdr.Get("X-Pm-Internal-Id")
|
||||
|
||||
// In case there is a mail client which corrupts headers, try "References" too.
|
||||
if internalID == "" {
|
||||
if references := strings.Fields(hdr.Get("References")); len(references) > 0 {
|
||||
if match := pmapi.RxInternalReferenceFormat.FindStringSubmatch(references[len(references)-1]); len(match) == 2 {
|
||||
internalID = match[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if internalID != "" {
|
||||
if msg, err := im.storeMailbox.GetMessage(internalID); err == nil {
|
||||
if im.user.user.IsCombinedAddressMode() || im.storeAddress.AddressID() == msg.Message().AddressID {
|
||||
return im.labelExistingMessage(msg.ID(), msg.IsMarkedDeleted())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return im.importMessage(kr, hdr, body, imapFlags, date)
|
||||
}
|
||||
|
||||
func (im *imapMailbox) createDraftMessage(kr *crypto.KeyRing, email string, body []byte) error {
|
||||
im.log.Info("Creating draft message")
|
||||
|
||||
m, _, _, readers, err := message.Parse(bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if m.Sender == nil {
|
||||
m.Sender = &mail.Address{}
|
||||
}
|
||||
|
||||
m.Sender.Address = pmapi.ConstructAddress(m.Sender.Address, email)
|
||||
|
||||
draft, _, err := im.user.storeUser.CreateDraft(kr, m, readers, "", "", "")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to create draft")
|
||||
}
|
||||
|
||||
return uidplus.AppendResponse(im.storeMailbox.UIDValidity(), im.storeMailbox.GetUIDList([]string{draft.ID}))
|
||||
}
|
||||
|
||||
func (im *imapMailbox) labelExistingMessage(messageID string, isDeleted bool) error {
|
||||
im.log.Info("Labelling existing message")
|
||||
|
||||
// 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.
|
||||
if isDeleted {
|
||||
if err := im.storeMailbox.MarkMessagesUndeleted([]string{messageID}); err != nil {
|
||||
log.WithError(err).Error("Failed to undelete re-imported message")
|
||||
}
|
||||
}
|
||||
|
||||
if err := im.storeMailbox.LabelMessages([]string{messageID}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return uidplus.AppendResponse(im.storeMailbox.UIDValidity(), im.storeMailbox.GetUIDList([]string{messageID}))
|
||||
}
|
||||
|
||||
func (im *imapMailbox) importMessage(kr *crypto.KeyRing, hdr textproto.Header, body []byte, imapFlags []string, date time.Time) error {
|
||||
im.log.Info("Importing external message")
|
||||
|
||||
var (
|
||||
seen bool
|
||||
flags int64
|
||||
labelIDs []string
|
||||
time int64
|
||||
)
|
||||
|
||||
if hdr.Get("received") == "" {
|
||||
flags = pmapi.FlagSent
|
||||
} else {
|
||||
flags = pmapi.FlagReceived
|
||||
}
|
||||
|
||||
for _, flag := range imapFlags {
|
||||
switch flag {
|
||||
case imap.DraftFlag:
|
||||
flags &= ^pmapi.FlagSent
|
||||
flags &= ^pmapi.FlagReceived
|
||||
|
||||
case imap.SeenFlag:
|
||||
seen = true
|
||||
|
||||
case imap.FlaggedFlag:
|
||||
labelIDs = append(labelIDs, pmapi.StarredLabel)
|
||||
|
||||
case imap.AnsweredFlag:
|
||||
flags |= pmapi.FlagReplied
|
||||
}
|
||||
}
|
||||
|
||||
if !date.IsZero() {
|
||||
time = date.Unix()
|
||||
}
|
||||
|
||||
enc, err := message.EncryptRFC822(kr, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
messageID, err := im.storeMailbox.ImportMessage(enc, seen, labelIDs, flags, time)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
msg, err := im.storeMailbox.GetMessage(messageID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if msg.IsMarkedDeleted() {
|
||||
if err := im.storeMailbox.MarkMessagesUndeleted([]string{messageID}); err != nil {
|
||||
log.WithError(err).Error("Failed to undelete re-imported message")
|
||||
}
|
||||
}
|
||||
|
||||
return uidplus.AppendResponse(im.storeMailbox.UIDValidity(), im.storeMailbox.GetUIDList([]string{messageID}))
|
||||
}
|
||||
322
internal/imap/mailbox_fetch.go
Normal file
322
internal/imap/mailbox_fetch.go
Normal file
@ -0,0 +1,322 @@
|
||||
// Copyright (c) 2021 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.
|
||||
//
|
||||
// ProtonMail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// ProtonMail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package imap
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/imap/cache"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/message"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
"github.com/emersion/go-imap"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func (im *imapMailbox) getMessage(
|
||||
storeMessage storeMessageProvider,
|
||||
items []imap.FetchItem,
|
||||
msgBuildCountHistogram *msgBuildCountHistogram,
|
||||
) (msg *imap.Message, err error) {
|
||||
msglog := im.log.WithField("msgID", storeMessage.ID())
|
||||
msglog.Trace("Getting message")
|
||||
|
||||
seqNum, err := storeMessage.SequenceNumber()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
m := storeMessage.Message()
|
||||
|
||||
msg = imap.NewMessage(seqNum, items)
|
||||
for _, item := range items {
|
||||
switch item {
|
||||
case imap.FetchEnvelope:
|
||||
// No need to check IsFullHeaderCached here. API header
|
||||
// contain enough information to build the envelope.
|
||||
msg.Envelope = message.GetEnvelope(m, storeMessage.GetMIMEHeader())
|
||||
case imap.FetchBody, imap.FetchBodyStructure:
|
||||
structure, err := im.getBodyStructure(storeMessage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if msg.BodyStructure, err = structure.IMAPBodyStructure([]int{}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case imap.FetchFlags:
|
||||
msg.Flags = message.GetFlags(m)
|
||||
if storeMessage.IsMarkedDeleted() {
|
||||
msg.Flags = append(msg.Flags, imap.DeletedFlag)
|
||||
}
|
||||
case imap.FetchInternalDate:
|
||||
// Apple Mail crashes fetching messages with date older than 1970.
|
||||
// There is no point having message older than RFC itself, it's not possible.
|
||||
msg.InternalDate = message.SanitizeMessageDate(m.Time)
|
||||
case imap.FetchRFC822Size:
|
||||
if msg.Size, err = im.getSize(storeMessage); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case imap.FetchUid:
|
||||
if msg.Uid, err = storeMessage.UID(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case imap.FetchAll, imap.FetchFast, imap.FetchFull, imap.FetchRFC822, imap.FetchRFC822Header, imap.FetchRFC822Text:
|
||||
fallthrough // this is list of defined items by go-imap, but items can be also sections generated from requests
|
||||
default:
|
||||
if err = im.getLiteralForSection(item, msg, storeMessage, msgBuildCountHistogram); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return msg, err
|
||||
}
|
||||
|
||||
// getSize returns cached size or it will build the message, save the size in
|
||||
// DB and then returns the size after build.
|
||||
//
|
||||
// We are storing size in DB as part of pmapi messages metada. The size
|
||||
// attribute on the server represents size of encrypted body. The value is
|
||||
// cleared in Bridge and the final decrypted size (including header, attachment
|
||||
// and MIME structure) is computed after building the message.
|
||||
func (im *imapMailbox) getSize(storeMessage storeMessageProvider) (uint32, error) {
|
||||
m := storeMessage.Message()
|
||||
if m.Size <= 0 {
|
||||
im.log.WithField("msgID", m.ID).Debug("Size unknown - downloading body")
|
||||
// We are sure the size is not a problem right now. Clients
|
||||
// might not first check sizes of all messages so we couldn't
|
||||
// be sure if seeing 1st or 2nd sync is all right or not.
|
||||
// Therefore, it's better to exclude getting size from the
|
||||
// counting and see build count as real message build.
|
||||
if _, _, err := im.getBodyAndStructure(storeMessage, nil); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
return uint32(m.Size), nil
|
||||
}
|
||||
|
||||
func (im *imapMailbox) getLiteralForSection(
|
||||
itemSection imap.FetchItem,
|
||||
msg *imap.Message,
|
||||
storeMessage storeMessageProvider,
|
||||
msgBuildCountHistogram *msgBuildCountHistogram,
|
||||
) error {
|
||||
section, err := imap.ParseBodySectionName(itemSection)
|
||||
if err != nil {
|
||||
log.WithError(err).Warn("Failed to parse body section name; part will be skipped")
|
||||
return nil //nolint[nilerr] ignore error
|
||||
}
|
||||
|
||||
var literal imap.Literal
|
||||
if literal, err = im.getMessageBodySection(storeMessage, section, msgBuildCountHistogram); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
msg.Body[section] = literal
|
||||
return nil
|
||||
}
|
||||
|
||||
// getBodyStructure returns the cached body structure or it will build the message,
|
||||
// save the structure in DB and then returns the structure after build.
|
||||
//
|
||||
// Apple Mail requests body structure for all messages irregularly. We cache
|
||||
// bodystructure in local database in order to not re-download all messages
|
||||
// from server.
|
||||
func (im *imapMailbox) getBodyStructure(storeMessage storeMessageProvider) (bs *message.BodyStructure, err error) {
|
||||
bs, err = storeMessage.GetBodyStructure()
|
||||
if err != nil {
|
||||
im.log.WithError(err).Debug("Fail to retrieve bodystructure from database")
|
||||
}
|
||||
if bs == nil {
|
||||
// We are sure the body structure is not a problem right now.
|
||||
// Clients might do first fetch body structure so we couldn't
|
||||
// be sure if seeing 1st or 2nd sync is all right or not.
|
||||
// Therefore, it's better to exclude first body structure fetch
|
||||
// from the counting and see build count as real message build.
|
||||
if bs, _, err = im.getBodyAndStructure(storeMessage, nil); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (im *imapMailbox) getBodyAndStructure(
|
||||
storeMessage storeMessageProvider, msgBuildCountHistogram *msgBuildCountHistogram,
|
||||
) (
|
||||
structure *message.BodyStructure, bodyReader *bytes.Reader, err error,
|
||||
) {
|
||||
m := storeMessage.Message()
|
||||
id := im.storeUser.UserID() + m.ID
|
||||
cache.BuildLock(id)
|
||||
defer cache.BuildUnlock(id)
|
||||
bodyReader, structure = cache.LoadMail(id)
|
||||
|
||||
// return the message which was found in cache
|
||||
if bodyReader.Len() != 0 && structure != nil {
|
||||
return structure, bodyReader, nil
|
||||
}
|
||||
|
||||
structure, body, err := im.buildMessage(m)
|
||||
bodyReader = bytes.NewReader(body)
|
||||
size := int64(len(body))
|
||||
l := im.log.WithField("newSize", size).WithField("msgID", m.ID)
|
||||
|
||||
if err != nil || structure == nil || size == 0 {
|
||||
l.WithField("hasStructure", structure != nil).Warn("Failed to build message")
|
||||
return structure, bodyReader, err
|
||||
}
|
||||
|
||||
// Save the size, body structure and header even for messages which
|
||||
// were unable to decrypt. Hence they doesn't have to be computed every
|
||||
// time.
|
||||
m.Size = size
|
||||
cacheMessageInStore(storeMessage, structure, body, l)
|
||||
|
||||
if msgBuildCountHistogram != nil {
|
||||
times, errCount := storeMessage.IncreaseBuildCount()
|
||||
if errCount != nil {
|
||||
l.WithError(errCount).Warn("Cannot increase build count")
|
||||
}
|
||||
msgBuildCountHistogram.add(times)
|
||||
}
|
||||
|
||||
// Drafts can change therefore we don't want to cache them.
|
||||
if !isMessageInDraftFolder(m) {
|
||||
cache.SaveMail(id, body, structure)
|
||||
}
|
||||
|
||||
return structure, bodyReader, err
|
||||
}
|
||||
|
||||
func cacheMessageInStore(storeMessage storeMessageProvider, structure *message.BodyStructure, body []byte, l *logrus.Entry) {
|
||||
m := storeMessage.Message()
|
||||
if errSize := storeMessage.SetSize(m.Size); errSize != nil {
|
||||
l.WithError(errSize).Warn("Cannot update size while building")
|
||||
}
|
||||
if structure != nil && !isMessageInDraftFolder(m) {
|
||||
if errStruct := storeMessage.SetBodyStructure(structure); errStruct != nil {
|
||||
l.WithError(errStruct).Warn("Cannot update bodystructure while building")
|
||||
}
|
||||
}
|
||||
header, errHead := structure.GetMailHeaderBytes(bytes.NewReader(body))
|
||||
if errHead == nil && len(header) != 0 {
|
||||
if errStore := storeMessage.SetHeader(header); errStore != nil {
|
||||
l.WithError(errStore).Warn("Cannot update header in store")
|
||||
}
|
||||
} else {
|
||||
l.WithError(errHead).Warn("Cannot get header bytes from structure")
|
||||
}
|
||||
}
|
||||
|
||||
func isMessageInDraftFolder(m *pmapi.Message) bool {
|
||||
for _, labelID := range m.LabelIDs {
|
||||
if labelID == pmapi.DraftLabel {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// This will download message (or read from cache) and pick up the section,
|
||||
// extract data (header,body, both) and trim the output if needed.
|
||||
//
|
||||
// In order to speed up (avoid download and decryptions) we
|
||||
// cache the header. If a mail header was requested and DB
|
||||
// contains full header (it means it was already built once)
|
||||
// the DB header can be used without downloading and decrypting.
|
||||
// Otherwise header is incomplete and clients would have issues
|
||||
// e.g. AppleMail expects `text/plain` in HTML mails.
|
||||
//
|
||||
// For all other cases it is necessary to download and decrypt the message
|
||||
// and drop the header which was obtained from cache. The header will
|
||||
// will be stored in DB once successfully built. Check `getBodyAndStructure`.
|
||||
func (im *imapMailbox) getMessageBodySection(
|
||||
storeMessage storeMessageProvider,
|
||||
section *imap.BodySectionName,
|
||||
msgBuildCountHistogram *msgBuildCountHistogram,
|
||||
) (imap.Literal, error) {
|
||||
var header []byte
|
||||
var response []byte
|
||||
|
||||
im.log.WithField("msgID", storeMessage.ID()).Trace("Getting message body")
|
||||
|
||||
isMainHeaderRequested := len(section.Path) == 0 && section.Specifier == imap.HeaderSpecifier
|
||||
if isMainHeaderRequested && storeMessage.IsFullHeaderCached() {
|
||||
header = storeMessage.GetHeader()
|
||||
} else {
|
||||
structure, bodyReader, err := im.getBodyAndStructure(storeMessage, msgBuildCountHistogram)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch {
|
||||
case section.Specifier == imap.EntireSpecifier && len(section.Path) == 0:
|
||||
// An empty section specification refers to the entire message, including the header.
|
||||
response, err = structure.GetSection(bodyReader, section.Path)
|
||||
case section.Specifier == imap.TextSpecifier || (section.Specifier == imap.EntireSpecifier && len(section.Path) != 0):
|
||||
// The TEXT specifier refers to the content of the message (or section), omitting the [RFC-2822] header.
|
||||
// Non-empty section with no specifier (imap.EntireSpecifier) refers to section content without header.
|
||||
response, err = structure.GetSectionContent(bodyReader, section.Path)
|
||||
case section.Specifier == imap.MIMESpecifier: // The MIME part specifier refers to the [MIME-IMB] header for this part.
|
||||
fallthrough
|
||||
case section.Specifier == imap.HeaderSpecifier:
|
||||
header, err = structure.GetSectionHeaderBytes(bodyReader, section.Path)
|
||||
default:
|
||||
err = errors.New("Unknown specifier " + string(section.Specifier))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if header != nil {
|
||||
response = filterHeader(header, section)
|
||||
}
|
||||
|
||||
// Trim any output if requested.
|
||||
return bytes.NewBuffer(section.ExtractPartial(response)), nil
|
||||
}
|
||||
|
||||
// buildMessage from PM to IMAP.
|
||||
func (im *imapMailbox) buildMessage(m *pmapi.Message) (*message.BodyStructure, []byte, error) {
|
||||
body, err := im.builder.NewJobWithOptions(
|
||||
context.Background(),
|
||||
im.user.client(),
|
||||
m.ID,
|
||||
message.JobOptions{
|
||||
IgnoreDecryptionErrors: true, // Whether to ignore decryption errors and create a "custom message" instead.
|
||||
SanitizeDate: true, // Whether to replace all dates before 1970 with RFC822's birthdate.
|
||||
AddInternalID: true, // Whether to include MessageID as X-Pm-Internal-Id.
|
||||
AddExternalID: true, // Whether to include ExternalID as X-Pm-External-Id.
|
||||
AddMessageDate: true, // Whether to include message time as X-Pm-Date.
|
||||
AddMessageIDReference: true, // Whether to include the MessageID in References.
|
||||
},
|
||||
).GetResult()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
structure, err := message.NewBodyStructure(bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return structure, body, nil
|
||||
}
|
||||
67
internal/imap/mailbox_fetch_test.go
Normal file
67
internal/imap/mailbox_fetch_test.go
Normal file
@ -0,0 +1,67 @@
|
||||
// Copyright (c) 2021 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.
|
||||
//
|
||||
// ProtonMail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// ProtonMail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package imap
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestFilterHeader(t *testing.T) {
|
||||
const header = "To: somebody\r\nFrom: somebody else\r\nSubject: this is\r\n\ta multiline field\r\n\r\n"
|
||||
|
||||
assert.Equal(t, "To: somebody\r\n\r\n", string(filterHeaderLines([]byte(header), func(field string) bool {
|
||||
return strings.EqualFold(field, "To")
|
||||
})))
|
||||
|
||||
assert.Equal(t, "From: somebody else\r\n\r\n", string(filterHeaderLines([]byte(header), func(field string) bool {
|
||||
return strings.EqualFold(field, "From")
|
||||
})))
|
||||
|
||||
assert.Equal(t, "To: somebody\r\nFrom: somebody else\r\n\r\n", string(filterHeaderLines([]byte(header), func(field string) bool {
|
||||
return strings.EqualFold(field, "To") || strings.EqualFold(field, "From")
|
||||
})))
|
||||
|
||||
assert.Equal(t, "Subject: this is\r\n\ta multiline field\r\n\r\n", string(filterHeaderLines([]byte(header), func(field string) bool {
|
||||
return strings.EqualFold(field, "Subject")
|
||||
})))
|
||||
}
|
||||
|
||||
// TestFilterHeaderNoNewline tests that we don't include a trailing newline when filtering
|
||||
// if the original header also lacks one (which it can legally do if there is no body).
|
||||
func TestFilterHeaderNoNewline(t *testing.T) {
|
||||
const header = "To: somebody\r\nFrom: somebody else\r\nSubject: this is\r\n\ta multiline field\r\n"
|
||||
|
||||
assert.Equal(t, "To: somebody\r\n", string(filterHeaderLines([]byte(header), func(field string) bool {
|
||||
return strings.EqualFold(field, "To")
|
||||
})))
|
||||
|
||||
assert.Equal(t, "From: somebody else\r\n", string(filterHeaderLines([]byte(header), func(field string) bool {
|
||||
return strings.EqualFold(field, "From")
|
||||
})))
|
||||
|
||||
assert.Equal(t, "To: somebody\r\nFrom: somebody else\r\n", string(filterHeaderLines([]byte(header), func(field string) bool {
|
||||
return strings.EqualFold(field, "To") || strings.EqualFold(field, "From")
|
||||
})))
|
||||
|
||||
assert.Equal(t, "Subject: this is\r\n\ta multiline field\r\n", string(filterHeaderLines([]byte(header), func(field string) bool {
|
||||
return strings.EqualFold(field, "Subject")
|
||||
})))
|
||||
}
|
||||
71
internal/imap/mailbox_header.go
Normal file
71
internal/imap/mailbox_header.go
Normal file
@ -0,0 +1,71 @@
|
||||
// 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"
|
||||
"strings"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/pkg/message"
|
||||
"github.com/emersion/go-imap"
|
||||
)
|
||||
|
||||
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 message.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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -18,7 +18,6 @@
|
||||
package imap
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/mail"
|
||||
"strings"
|
||||
@ -30,6 +29,7 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/pkg/parallel"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
"github.com/emersion/go-imap"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
@ -38,6 +38,12 @@ import (
|
||||
// If the Backend implements Updater, it must notify the client immediately
|
||||
// via a message update.
|
||||
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{
|
||||
"flags": flags,
|
||||
"operation": operation,
|
||||
@ -135,7 +141,7 @@ func (im *imapMailbox) addOrRemoveFlags(operation imap.FlagsOp, messageIDs, flag
|
||||
for _, f := range flags {
|
||||
switch f {
|
||||
case imap.SeenFlag:
|
||||
switch operation {
|
||||
switch operation { //nolint[exhaustive] imap.SetFlags is processed by im.setFlags
|
||||
case imap.AddFlags:
|
||||
if err := im.storeMailbox.MarkMessagesRead(messageIDs); err != nil {
|
||||
return err
|
||||
@ -146,7 +152,7 @@ func (im *imapMailbox) addOrRemoveFlags(operation imap.FlagsOp, messageIDs, flag
|
||||
}
|
||||
}
|
||||
case imap.FlaggedFlag:
|
||||
switch operation {
|
||||
switch operation { //nolint[exhaustive] imap.SetFlag is processed by im.setFlags
|
||||
case imap.AddFlags:
|
||||
if err := im.storeMailbox.MarkMessagesStarred(messageIDs); err != nil {
|
||||
return err
|
||||
@ -157,7 +163,7 @@ func (im *imapMailbox) addOrRemoveFlags(operation imap.FlagsOp, messageIDs, flag
|
||||
}
|
||||
}
|
||||
case imap.DeletedFlag:
|
||||
switch operation {
|
||||
switch operation { //nolint[exhaustive] imap.SetFlag is processed by im.setFlags
|
||||
case imap.AddFlags:
|
||||
if err := im.storeMailbox.MarkMessagesDeleted(messageIDs); err != nil {
|
||||
return err
|
||||
@ -176,7 +182,7 @@ func (im *imapMailbox) addOrRemoveFlags(operation imap.FlagsOp, messageIDs, flag
|
||||
}
|
||||
|
||||
// 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
|
||||
// will automatically take care of label removal.
|
||||
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
|
||||
// be preserved, and the Recent flag SHOULD be set, in the copy.
|
||||
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.
|
||||
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
|
||||
// responses.
|
||||
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.
|
||||
defer im.panicHandler.HandlePanic()
|
||||
|
||||
@ -340,23 +358,28 @@ func (im *imapMailbox) SearchMessages(isUID bool, criteria *imap.SearchCriteria)
|
||||
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 t, err := m.Header.Date(); err == nil && !t.IsZero() {
|
||||
if !criteria.SentBefore.IsZero() {
|
||||
if truncated := criteria.SentBefore.Truncate(24 * time.Hour); t.Unix() > truncated.Unix() {
|
||||
continue
|
||||
}
|
||||
t, err := mail.Header(header).Date()
|
||||
if err != nil || t.IsZero() {
|
||||
t = time.Unix(m.Time, 0)
|
||||
}
|
||||
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() {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if !criteria.SentSince.IsZero() {
|
||||
if truncated := criteria.SentSince.Truncate(24 * time.Hour); t.Unix() < truncated.Unix() {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by headers.
|
||||
header := message.GetHeader(m)
|
||||
headerMatch := true
|
||||
for criteriaKey, criteriaValues := range criteria.Header {
|
||||
for _, criteriaValue := range criteriaValues {
|
||||
@ -364,6 +387,8 @@ func (im *imapMailbox) SearchMessages(isUID bool, criteria *imap.SearchCriteria)
|
||||
continue
|
||||
}
|
||||
switch criteriaKey {
|
||||
case "Subject":
|
||||
headerMatch = strings.Contains(strings.ToLower(m.Subject), strings.ToLower(criteriaValue))
|
||||
case "From":
|
||||
headerMatch = addressMatch([]*mail.Address{m.Sender}, criteriaValue)
|
||||
case "To":
|
||||
@ -396,7 +421,7 @@ func (im *imapMailbox) SearchMessages(isUID bool, criteria *imap.SearchCriteria)
|
||||
if isStringInList(m.LabelIDs, pmapi.StarredLabel) {
|
||||
messageFlagsMap[imap.FlaggedFlag] = true
|
||||
}
|
||||
if m.Unread == 0 {
|
||||
if !m.Unread {
|
||||
messageFlagsMap[imap.SeenFlag] = true
|
||||
}
|
||||
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.
|
||||
//
|
||||
// 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() {
|
||||
close(msgResponse)
|
||||
if err != nil {
|
||||
@ -493,25 +525,13 @@ func (im *imapMailbox) ListMessages(isUID bool, seqSet *imap.SeqSet, items []ima
|
||||
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))
|
||||
for i, apiID := range apiIDs {
|
||||
input[i] = apiID
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
@ -520,14 +540,14 @@ func (im *imapMailbox) ListMessages(isUID bool, seqSet *imap.SeqSet, items []ima
|
||||
return nil, err
|
||||
}
|
||||
|
||||
msg, err := im.getMessage(storeMessage, items)
|
||||
msg, err := im.getMessage(storeMessage, items, msgBuildCountHistogram)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("list message build: %v", err)
|
||||
l.WithField("metaID", storeMessage.ID()).Error(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if storeMessage.Message().Unread == 1 {
|
||||
if storeMessage.Message().Unread {
|
||||
for section := range msg.Body {
|
||||
// Peek means get messages without marking 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 {
|
||||
msg := value.(*imap.Message)
|
||||
msg := value.(*imap.Message) //nolint[forcetypeassert] we want to panic here
|
||||
msgResponse <- msg
|
||||
return nil
|
||||
}
|
||||
|
||||
err = parallel.RunParallel(fetchMessagesWorkers, input, processCallback, collectCallback)
|
||||
err = parallel.RunParallel(fetchWorkers, input, processCallback, collectCallback)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user