mirror of
https://github.com/ProtonMail/proton-bridge.git
synced 2025-12-10 12:46:46 +00:00
Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5ad23715ec | |||
| 8ab05a000c | |||
| 454d248819 | |||
| 6c8e5f7cd3 | |||
| f5aba717b2 | |||
| 1359c39bc0 | |||
| 4850681f1d | |||
| aa55c69307 | |||
| 1f19d4df75 | |||
| c0f6af9eb5 | |||
| ef6a3d4999 | |||
| 50550d42b4 | |||
| 8db89a1a6c | |||
| ba1dfb1bf4 | |||
| d243880753 | |||
| cccaaa3d82 | |||
| 2d95f21567 | |||
| 7d0af7624c | |||
| 2f35c453a1 | |||
| 05dd137bc8 | |||
| 767628946f | |||
| d4efa7131f | |||
| 144cf6e40c | |||
| a205d8c046 | |||
| cccadaee42 | |||
| bbb365f8a5 | |||
| 1f18d9d917 | |||
| 59e0d63485 | |||
| 72fe5a636e | |||
| 45a83133ba | |||
| 215eb4d6eb | |||
| 479b951c50 | |||
| a94c8a943f | |||
| ea306f405e | |||
| 1b405506b8 | |||
| 38c6132f81 | |||
| b7351dfaf8 | |||
| 7e8f6943f2 | |||
| a0132e8440 | |||
| 27541784aa | |||
| 9e567f08b2 | |||
| bf274f984e | |||
| 3b60bbe13b | |||
| a73a1b623a | |||
| c0a8877018 | |||
| 904166c01c | |||
| 4761bc935a | |||
| 71301d891f | |||
| d47be3c4c0 |
@ -1,3 +1,4 @@
|
||||
---
|
||||
run:
|
||||
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]
|
||||
|
||||
|
||||
83
Changelog.md
83
Changelog.md
@ -2,6 +2,89 @@
|
||||
|
||||
Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
|
||||
## [Bridge 1.7.0] Iron
|
||||
|
||||
### Added
|
||||
* GODT-213 New message builder:
|
||||
* Preserve Content-Type for undecryptable message body.
|
||||
* Use application/octet-stream for encrypted parts.
|
||||
* Force no transfer encoding for embedded message/rfc822 parts.
|
||||
* Remove dead code GetRelatedHeader/GetRelatedBoundary.
|
||||
* Correctly expect text/plain in custom message text parts.
|
||||
* Force text/plain for custom message text part.
|
||||
* Complex external encrypted tests (multipart/alternative, message/rfc822 attachment).
|
||||
|
||||
### Fixed
|
||||
* GODT-1136 DB Cache header from builder and test.
|
||||
* GODT-1113 Fix tray icon size on macOS Big Sur.
|
||||
* GODT-947 Force colors in logs.
|
||||
|
||||
|
||||
## [Bridge 1.6.9] HZM
|
||||
|
||||
### Fixed
|
||||
* GODT-1121 'Keep the application up to date' switches off after restarting Bridge.
|
||||
|
||||
|
||||
## [Bridge 1.6.8] HZM
|
||||
|
||||
### Fixed
|
||||
* GODT-1120 Use Info level in internal/app logs.
|
||||
|
||||
|
||||
## [IE 1.3.3] Farg
|
||||
|
||||
### Fixed
|
||||
* GODT-1120 Use Info level in internal/app logs.
|
||||
|
||||
|
||||
## [Bridge 1.6.7] HZM
|
||||
|
||||
### Added
|
||||
* GODT-1111 Add correct metadata to Windows executables.
|
||||
* GODT-1112 Add application to Windows Firewall exclusion list on install.
|
||||
* GODT-1077 Track how many times message is built to help understand re-syncs.
|
||||
|
||||
### Changed
|
||||
* GODT-247 Revise all storage locations (cache, config, local etc).
|
||||
|
||||
### Fixed
|
||||
* GODT-948 Parser does not handle embedding of Content-Type: message/rfc822.
|
||||
* GODT-1079 Correct 9001 error handling on login.
|
||||
|
||||
### Security
|
||||
* GODT-1105 Dylib Hijacking security fix.
|
||||
|
||||
|
||||
## [IE 1.3.2] Farg
|
||||
|
||||
### Added
|
||||
* GODT-1111 Add correct metadata to Windows executables.
|
||||
* GODT-1112 Add application to Windows Firewall exclusion list on install.
|
||||
|
||||
### Changed
|
||||
* GODT-247 Revise all storage locations (cache, config, local etc).
|
||||
|
||||
### Fixed
|
||||
* GODT-1079 Correct 9001 error handling on login.
|
||||
|
||||
### Security
|
||||
* GODT-1105 Dylib Hijacking security fix.
|
||||
|
||||
|
||||
## [IE 1.3.1] Farg
|
||||
|
||||
### Changed
|
||||
* GODT-1047 No silent updates for Import-Export app.
|
||||
* GODT-247 Cache and update files moved from user's cache to config.
|
||||
|
||||
### Fixed
|
||||
* Other: include latest go.mod/go.sum changes.
|
||||
* GODT-803 Fix import to wrong target address.
|
||||
* GODT-948 Embedded messages.
|
||||
* GODT-1043 Fix showing long login error in GUI dialog.
|
||||
|
||||
|
||||
## [Bridge 1.6.6] HZM
|
||||
|
||||
### Added
|
||||
|
||||
46
Makefile
46
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.6+git
|
||||
IE_APP_VERSION?=1.3.0+git
|
||||
BRIDGE_APP_VERSION?=1.7.0+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,6 +28,7 @@ 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)
|
||||
@ -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
|
||||
@ -89,8 +91,14 @@ build-nogui: gofiles
|
||||
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,7 +142,7 @@ 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_GUI} ${QT_BUILD_TARGET}
|
||||
@ -142,13 +150,12 @@ ${EXE_TARGET}: check-has-go gofiles ${ICO_FILES} ${VENDOR_TARGET}
|
||||
if [ "${EXE_QT_TARGET}" != "${EXE_TARGET}" ]; then mv ${EXE_QT_TARGET} ${EXE_TARGET}; fi
|
||||
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)
|
||||
@ -181,7 +189,7 @@ update-qt-docs:
|
||||
|
||||
## Dev dependencies
|
||||
.PHONY: install-devel-tools install-linter install-go-mod-outdated install-git-hooks
|
||||
LINTVER:="v1.29.0"
|
||||
LINTVER:="v1.39.0"
|
||||
LINTSRC:="https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh"
|
||||
|
||||
install-dev-dependencies: install-devel-tools install-linter install-go-mod-outdated
|
||||
@ -250,6 +258,7 @@ mocks:
|
||||
mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/transfer PanicHandler,ClientManager,IMAPClientProvider > internal/transfer/mocks/mocks.go
|
||||
mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/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/message Fetcher > pkg/message/mocks/mocks.go
|
||||
mockgen --package mocks github.com/ProtonMail/proton-bridge/pkg/pmapi Client > pkg/pmapi/mocks/mocks.go
|
||||
|
||||
lint: gofiles lint-golang lint-license lint-changelog
|
||||
@ -294,6 +303,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
|
||||
|
||||
@ -316,11 +326,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
|
||||
@ -337,7 +347,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
|
||||
|
||||
@ -345,3 +355,5 @@ clean: clean-vendor
|
||||
generate:
|
||||
go generate ./...
|
||||
$(MAKE) add-license
|
||||
|
||||
.FORCE:
|
||||
|
||||
1
go.mod
1
go.mod
@ -54,7 +54,6 @@ require (
|
||||
github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce
|
||||
github.com/olekukonko/tablewriter v0.0.4 // indirect
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/pkg/math v0.0.0-20141027224758-f2ed9e40e245
|
||||
github.com/sirupsen/logrus v1.7.0
|
||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
|
||||
|
||||
2
go.sum
2
go.sum
@ -223,8 +223,6 @@ github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTw
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/math v0.0.0-20141027224758-f2ed9e40e245 h1:gk/AF9SGRj+RafNCoDcS3RRscb8S4BVbvqODOgWA7/8=
|
||||
github.com/pkg/math v0.0.0-20141027224758-f2ed9e40e245/go.mod h1:2dhPPj2Li3DXrSY2U2ADdZy2B7sjQsT57lqENx1+FSE=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
|
||||
|
||||
@ -69,7 +69,7 @@ const (
|
||||
flagMemProfileShort = "m"
|
||||
flagLogLevel = "log-level"
|
||||
flagLogLevelShort = "l"
|
||||
// FlagCLI indicate to start with command line interface
|
||||
// FlagCLI indicate to start with command line interface.
|
||||
FlagCLI = "cli"
|
||||
flagCLIShort = "c"
|
||||
flagRestart = "restart"
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -189,9 +189,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
|
||||
}
|
||||
|
||||
@ -201,11 +202,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))
|
||||
@ -213,16 +214,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"
|
||||
)
|
||||
@ -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))
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -93,7 +93,7 @@ func (c *appleMail) Configure(imapPort, smtpPort int, imapSSL, smtpSSL bool, use
|
||||
})()
|
||||
|
||||
// Make sure the file is only readable for the current user.
|
||||
f, err := os.OpenFile(filepath.Join(dir, "protonmail.mobileconfig"), os.O_RDWR|os.O_CREATE, 0600)
|
||||
f, err := os.OpenFile(filepath.Clean(filepath.Join(dir, "protonmail.mobileconfig")), os.O_RDWR|os.O_CREATE, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -25,7 +25,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() {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
||||
@ -28,7 +28,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() {
|
||||
|
||||
@ -161,7 +161,7 @@ func (f *frontendCLI) disallowProxy(c *ishell.Context) {
|
||||
}
|
||||
|
||||
func (f *frontendCLI) isPortFree(port string) bool {
|
||||
port = strings.Replace(port, ":", "", -1)
|
||||
port = strings.ReplaceAll(port, ":", "")
|
||||
if port == "" || port == currentPort {
|
||||
return true
|
||||
}
|
||||
|
||||
@ -215,7 +215,7 @@ Item {
|
||||
}
|
||||
go.updateState = "updateRestart"
|
||||
winMain.dialogUpdate.finished(false)
|
||||
|
||||
|
||||
// after manual update - just retart immidiatly
|
||||
go.setToRestart()
|
||||
Qt.quit()
|
||||
@ -236,13 +236,13 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
onNotifySilentUpdateRestartNeeded: {
|
||||
go.updateState = "updateRestart"
|
||||
}
|
||||
|
||||
onNotifySilentUpdateError: {
|
||||
go.updateState = "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
|
||||
|
||||
@ -428,7 +429,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 != ""
|
||||
@ -1032,6 +1033,7 @@ Dialog {
|
||||
root.isFromIMAP,
|
||||
root.inputPath,
|
||||
inputEmail.text, inputPassword.text, inputServer.text, inputPort.text,
|
||||
root.account,
|
||||
root.address
|
||||
)
|
||||
break
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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"
|
||||
@ -858,15 +858,15 @@ Window {
|
||||
|
||||
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()
|
||||
@ -1355,10 +1355,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)
|
||||
//}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -135,11 +135,11 @@ 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() {
|
||||
@ -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,17 +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)
|
||||
}
|
||||
}
|
||||
//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() {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"`
|
||||
@ -62,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"`
|
||||
@ -77,8 +77,8 @@ type GoQMLInterface struct {
|
||||
_ string `property:"credentialsNotRemoved"`
|
||||
_ string `property:"versionCheckFailed"`
|
||||
//
|
||||
_ func(isAvailable bool) `signal:"setConnectionStatus"`
|
||||
_ func() `slot:"checkInternet"`
|
||||
_ func(isAvailable bool) `signal:"setConnectionStatus"`
|
||||
_ func() `slot:"checkInternet"`
|
||||
|
||||
_ func() `slot:"setToRestart"`
|
||||
|
||||
@ -93,7 +93,7 @@ type GoQMLInterface struct {
|
||||
|
||||
_ func() `signal:"showWindow"`
|
||||
|
||||
_ func() `slot:"toggleAutoUpdate"`
|
||||
//_ func() `slot:"toggleAutoUpdate"`
|
||||
_ func() `slot:"quit"`
|
||||
_ func() `slot:"loadAccounts"`
|
||||
_ func() `slot:"openLogs"`
|
||||
@ -108,14 +108,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 +162,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)
|
||||
@ -207,4 +207,6 @@ func (s *GoQMLInterface) SetFrontend(f *FrontendQt) {
|
||||
s.ConnectCheckPathStatus(CheckPathStatus)
|
||||
|
||||
s.ConnectEmitEvent(f.emitEvent)
|
||||
|
||||
s.ConnectStartManualUpdate(f.startManualUpdate)
|
||||
}
|
||||
|
||||
@ -370,24 +370,15 @@ func (s *FrontendQt) qtExecute(Procedure func(*FrontendQt) error) error {
|
||||
}
|
||||
s.Qml.SetIsAutoStart(s.autostart.IsEnabled())
|
||||
|
||||
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)
|
||||
}
|
||||
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)
|
||||
|
||||
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.
|
||||
|
||||
@ -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
|
||||
@ -114,10 +114,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
|
||||
}
|
||||
|
||||
@ -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{},
|
||||
}
|
||||
|
||||
@ -61,7 +61,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 {
|
||||
@ -77,5 +77,5 @@ func (u *bridgeUserWrap) GetStore() storeUserProvider {
|
||||
if store == nil {
|
||||
return nil
|
||||
}
|
||||
return newStoreUserWrap(store)
|
||||
return newStoreUserWrap(store) //nolint[typecheck] missing methods are inherited
|
||||
}
|
||||
|
||||
@ -26,7 +26,7 @@ type currentClientSetter interface {
|
||||
SetClient(name, version string)
|
||||
}
|
||||
|
||||
// Extension for IMAP server
|
||||
// Extension for IMAP server.
|
||||
type extension struct {
|
||||
extID imapserver.ConnExtension
|
||||
clientSetter currentClientSetter
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -37,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,
|
||||
@ -54,6 +56,8 @@ func newIMAPMailbox(panicHandler panicHandler, user *imapUser, storeMailbox stor
|
||||
storeUser: user.storeUser,
|
||||
storeAddress: user.storeAddress,
|
||||
storeMailbox: storeMailbox,
|
||||
|
||||
builder: builder,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -19,9 +19,9 @@ package imap
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/mail"
|
||||
"net/textproto"
|
||||
"sort"
|
||||
@ -32,12 +32,10 @@ import (
|
||||
"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 (
|
||||
@ -181,7 +179,7 @@ func (im *imapMailbox) createMessage(flags []string, date time.Time, body imap.L
|
||||
return err
|
||||
}
|
||||
|
||||
targetSeq := im.storeMailbox.GetUIDList([]string{m.ID})
|
||||
targetSeq := im.storeMailbox.GetUIDList(IDs)
|
||||
return uidplus.AppendResponse(im.storeMailbox.UIDValidity(), targetSeq)
|
||||
}
|
||||
}
|
||||
@ -226,8 +224,9 @@ func (im *imapMailbox) importMessage(m *pmapi.Message, readers []io.Reader, kr *
|
||||
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")
|
||||
func (im *imapMailbox) getMessage(storeMessage storeMessageProvider, items []imap.FetchItem, msgBuildCountHistogram *msgBuildCountHistogram) (msg *imap.Message, err error) { //nolint[funlen]
|
||||
msglog := im.log.WithField("msgID", storeMessage.ID())
|
||||
msglog.Trace("Getting message")
|
||||
|
||||
seqNum, err := storeMessage.SequenceNumber()
|
||||
if err != nil {
|
||||
@ -240,7 +239,9 @@ func (im *imapMailbox) getMessage(storeMessage storeMessageProvider, items []ima
|
||||
for _, item := range items {
|
||||
switch item {
|
||||
case imap.FetchEnvelope:
|
||||
msg.Envelope = message.GetEnvelope(m)
|
||||
// No need to check IsFullHeaderCached here. API header
|
||||
// contain enough information to build the envelope.
|
||||
msg.Envelope = message.GetEnvelope(m, storeMessage.GetHeader())
|
||||
case imap.FetchBody, imap.FetchBodyStructure:
|
||||
var structure *message.BodyStructure
|
||||
structure, err = im.getBodyStructure(storeMessage)
|
||||
@ -267,8 +268,13 @@ func (im *imapMailbox) getMessage(storeMessage storeMessageProvider, items []ima
|
||||
// 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 {
|
||||
msglog.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
|
||||
}
|
||||
}
|
||||
@ -278,8 +284,10 @@ func (im *imapMailbox) getMessage(storeMessage storeMessageProvider, items []ima
|
||||
if 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); err != nil {
|
||||
if err = im.getLiteralForSection(item, msg, storeMessage, msgBuildCountHistogram); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
@ -288,14 +296,15 @@ func (im *imapMailbox) getMessage(storeMessage storeMessageProvider, items []ima
|
||||
return msg, err
|
||||
}
|
||||
|
||||
func (im *imapMailbox) getLiteralForSection(itemSection imap.FetchItem, msg *imap.Message, storeMessage storeMessageProvider) error {
|
||||
func (im *imapMailbox) getLiteralForSection(itemSection imap.FetchItem, msg *imap.Message, storeMessage storeMessageProvider, msgBuildCountHistogram *msgBuildCountHistogram) error {
|
||||
section, err := imap.ParseBodySectionName(itemSection)
|
||||
if err != nil { // Ignore error
|
||||
return nil
|
||||
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); err != nil {
|
||||
if literal, err = im.getMessageBodySection(storeMessage, section, msgBuildCountHistogram); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -313,14 +322,20 @@ func (im *imapMailbox) getBodyStructure(storeMessage storeMessageProvider) (bs *
|
||||
im.log.WithError(err).Debug("Fail to retrieve bodystructure from database")
|
||||
}
|
||||
if bs == nil {
|
||||
if bs, _, err = im.getBodyAndStructure(storeMessage); err != nil {
|
||||
// We are sure the body structure is not a problem right now.
|
||||
// Clients might do first fetch body structure so we couldn't
|
||||
// be sure if seeing 1st or 2nd sync is all right or not.
|
||||
// Therefore, it's better to exclude first body structure fetch
|
||||
// from the counting and see build count as real message build.
|
||||
if bs, _, err = im.getBodyAndStructure(storeMessage, nil); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (im *imapMailbox) getBodyAndStructure(storeMessage storeMessageProvider) (
|
||||
//nolint[funlen] Jakub will fix in refactor
|
||||
func (im *imapMailbox) getBodyAndStructure(storeMessage storeMessageProvider, msgBuildCountHistogram *msgBuildCountHistogram) (
|
||||
structure *message.BodyStructure,
|
||||
bodyReader *bytes.Reader, err error,
|
||||
) {
|
||||
@ -347,10 +362,26 @@ func (im *imapMailbox) getBodyAndStructure(storeMessage storeMessageProvider) (
|
||||
}
|
||||
}
|
||||
if err == nil && structure != nil && len(body) > 0 {
|
||||
if err := storeMessage.SetContentTypeAndHeader(m.MIMEType, m.Header); err != nil {
|
||||
im.log.WithError(err).
|
||||
header, errHead := structure.GetMailHeaderBytes(bytes.NewReader(body))
|
||||
if errHead == nil {
|
||||
if errHead := storeMessage.SetHeader(header); errHead != nil {
|
||||
im.log.WithError(errHead).
|
||||
WithField("msgID", m.ID).
|
||||
Warn("Cannot update header after building")
|
||||
}
|
||||
} else {
|
||||
im.log.WithError(errHead).
|
||||
WithField("msgID", m.ID).
|
||||
Warn("Cannot update header while building")
|
||||
Warn("Cannot get header bytes after building")
|
||||
}
|
||||
if msgBuildCountHistogram != nil {
|
||||
times, err := storeMessage.IncreaseBuildCount()
|
||||
if err != nil {
|
||||
im.log.WithError(err).
|
||||
WithField("msgID", m.ID).
|
||||
Warn("Cannot increase build count")
|
||||
}
|
||||
msgBuildCountHistogram.add(times)
|
||||
}
|
||||
// Drafts can change and we don't want to cache them.
|
||||
if !isMessageInDraftFolder(m) {
|
||||
@ -379,40 +410,32 @@ func isMessageInDraftFolder(m *pmapi.Message) bool {
|
||||
|
||||
// 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
|
||||
)
|
||||
func (im *imapMailbox) getMessageBodySection(
|
||||
storeMessage storeMessageProvider,
|
||||
section *imap.BodySectionName,
|
||||
msgBuildCountHistogram *msgBuildCountHistogram,
|
||||
) (imap.Literal, error) {
|
||||
var header textproto.MIMEHeader
|
||||
var 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)
|
||||
}
|
||||
isMainHeaderRequested := len(section.Path) == 0 && section.Specifier == imap.HeaderSpecifier
|
||||
if isMainHeaderRequested && storeMessage.IsFullHeaderCached() {
|
||||
// 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.
|
||||
header = storeMessage.GetHeader()
|
||||
} else {
|
||||
// The rest of cases need download and decrypt.
|
||||
structure, bodyReader, err = im.getBodyAndStructure(storeMessage)
|
||||
// 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`.
|
||||
structure, bodyReader, err := im.getBodyAndStructure(storeMessage, msgBuildCountHistogram)
|
||||
if err != nil {
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch {
|
||||
@ -423,368 +446,86 @@ func (im *imapMailbox) getMessageBodySection(storeMessage storeMessageProvider,
|
||||
// The TEXT specifier refers to the content of the message (or section), omitting the [RFC-2822] header.
|
||||
// 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.
|
||||
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 nil, err
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
response = filteredHeaderAsBytes(header, section)
|
||||
}
|
||||
|
||||
// Trim any output if requested.
|
||||
literal = bytes.NewBuffer(section.ExtractPartial(response))
|
||||
return literal, nil
|
||||
return bytes.NewBuffer(section.ExtractPartial(response)), 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
|
||||
// filteredHeaderAsBytes filters the header fields by section fields and it
|
||||
// returns the filtered fields as bytes.
|
||||
// Options are: all fields, only selected fields, all fields except selected.
|
||||
func filteredHeaderAsBytes(header textproto.MIMEHeader, section *imap.BodySectionName) []byte {
|
||||
// remove fields
|
||||
if len(section.Fields) != 0 && section.NotFields {
|
||||
for _, field := range section.Fields {
|
||||
header.Del(field)
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
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)
|
||||
}
|
||||
_, _ = 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
|
||||
sort.Strings(fields)
|
||||
} else { // add only requested (in requested order)
|
||||
for _, f := range section.Fields {
|
||||
fields = append(fields, textproto.CanonicalMIMEHeaderKey(f))
|
||||
}
|
||||
}
|
||||
|
||||
h := message.GetAttachmentHeader(inline)
|
||||
if p, err = related.CreatePart(h); err != nil {
|
||||
return
|
||||
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)
|
||||
}
|
||||
}
|
||||
_, _ = 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
|
||||
return headerBuf.Bytes()
|
||||
}
|
||||
|
||||
// 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)
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
structure, err := message.NewBodyStructure(bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
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
|
||||
return structure, body, nil
|
||||
}
|
||||
|
||||
@ -141,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
|
||||
@ -152,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
|
||||
@ -163,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
|
||||
@ -182,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:
|
||||
@ -358,23 +358,29 @@ 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.GetHeader()
|
||||
|
||||
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 {
|
||||
@ -382,6 +388,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":
|
||||
@ -482,12 +490,13 @@ func (im *imapMailbox) SearchMessages(isUID bool, criteria *imap.SearchCriteria)
|
||||
//
|
||||
// Messages must be sent to msgResponse. When the function returns, msgResponse must be closed.
|
||||
func (im *imapMailbox) ListMessages(isUID bool, seqSet *imap.SeqSet, items []imap.FetchItem, msgResponse chan<- *imap.Message) error {
|
||||
msgBuildCountHistogram := newMsgBuildCountHistogram()
|
||||
return im.logCommand(func() error {
|
||||
return im.listMessages(isUID, seqSet, items, msgResponse)
|
||||
}, "FETCH", isUID, seqSet, items)
|
||||
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) (err error) { //nolint[funlen]
|
||||
func (im *imapMailbox) listMessages(isUID bool, seqSet *imap.SeqSet, items []imap.FetchItem, msgResponse chan<- *imap.Message, msgBuildCountHistogram *msgBuildCountHistogram) (err error) { //nolint[funlen]
|
||||
defer func() {
|
||||
close(msgResponse)
|
||||
if err != nil {
|
||||
@ -535,7 +544,7 @@ func (im *imapMailbox) listMessages(isUID bool, seqSet *imap.SeqSet, items []ima
|
||||
}
|
||||
|
||||
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 {
|
||||
@ -544,7 +553,7 @@ 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)
|
||||
@ -569,12 +578,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
|
||||
}
|
||||
|
||||
65
internal/imap/msg_build_counts.go
Normal file
65
internal/imap/msg_build_counts.go
Normal file
@ -0,0 +1,65 @@
|
||||
// Copyright (c) 2021 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.
|
||||
//
|
||||
// ProtonMail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// ProtonMail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package imap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// msgBuildCountHistogram is used to analyse and log the number of repetitive
|
||||
// downloads of requested messages per one fetch. The number of builds per each
|
||||
// messageID is stored in persistent database. The msgBuildCountHistogram will
|
||||
// take this number for each message in ongoing fetch and create histogram of
|
||||
// repeats.
|
||||
//
|
||||
// Example: During `fetch 1:300` there were
|
||||
// - 100 messages were downloaded first time
|
||||
// - 100 messages were downloaded second time
|
||||
// - 99 messages were downloaded 10th times
|
||||
// - 1 messages were downloaded 100th times.
|
||||
type msgBuildCountHistogram struct {
|
||||
// Key represents how many times message was build.
|
||||
// Value stores how many messages are build X times based on the key.
|
||||
counts map[uint32]uint32
|
||||
lock sync.Locker
|
||||
}
|
||||
|
||||
func newMsgBuildCountHistogram() *msgBuildCountHistogram {
|
||||
return &msgBuildCountHistogram{
|
||||
counts: map[uint32]uint32{},
|
||||
lock: &sync.Mutex{},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *msgBuildCountHistogram) String() string {
|
||||
res := ""
|
||||
for nRebuild, counts := range c.counts {
|
||||
if res != "" {
|
||||
res += ", "
|
||||
}
|
||||
res += fmt.Sprintf("[%d]:%d", nRebuild, counts)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func (c *msgBuildCountHistogram) add(nRebuild uint32) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
c.counts[nRebuild]++
|
||||
}
|
||||
@ -20,6 +20,7 @@ package imap
|
||||
import (
|
||||
"io"
|
||||
"net/mail"
|
||||
"net/textproto"
|
||||
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/ProtonMail/proton-bridge/internal/imap/uidplus"
|
||||
@ -100,9 +101,12 @@ type storeMessageProvider interface {
|
||||
IsMarkedDeleted() bool
|
||||
|
||||
SetSize(int64) error
|
||||
SetContentTypeAndHeader(string, mail.Header) error
|
||||
SetHeader([]byte) error
|
||||
GetHeader() textproto.MIMEHeader
|
||||
IsFullHeaderCached() bool
|
||||
SetBodyStructure(*pkgMsg.BodyStructure) error
|
||||
GetBodyStructure() (*pkgMsg.BodyStructure, error)
|
||||
IncreaseBuildCount() (uint32, error)
|
||||
}
|
||||
|
||||
type storeUserWrap struct {
|
||||
@ -122,7 +126,7 @@ func (s *storeUserWrap) GetAddress(addressID string) (storeAddressProvider, erro
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newStoreAddressWrap(address), nil
|
||||
return newStoreAddressWrap(address), nil //nolint[typecheck] missing methods are inherited
|
||||
}
|
||||
|
||||
type storeAddressWrap struct {
|
||||
@ -136,7 +140,7 @@ func newStoreAddressWrap(address *store.Address) *storeAddressWrap {
|
||||
func (s *storeAddressWrap) ListMailboxes() []storeMailboxProvider {
|
||||
mailboxes := []storeMailboxProvider{}
|
||||
for _, mailbox := range s.Address.ListMailboxes() {
|
||||
mailboxes = append(mailboxes, newStoreMailboxWrap(mailbox))
|
||||
mailboxes = append(mailboxes, newStoreMailboxWrap(mailbox)) //nolint[typecheck] missing methods are inherited
|
||||
}
|
||||
return mailboxes
|
||||
}
|
||||
@ -146,7 +150,7 @@ func (s *storeAddressWrap) GetMailbox(name string) (storeMailboxProvider, error)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newStoreMailboxWrap(mailbox), nil
|
||||
return newStoreMailboxWrap(mailbox), nil //nolint[typecheck] missing methods are inherited
|
||||
}
|
||||
|
||||
type storeMailboxWrap struct {
|
||||
|
||||
@ -33,7 +33,7 @@ import (
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Capability extension identifier
|
||||
// Capability extension identifier.
|
||||
const Capability = "UIDPLUS"
|
||||
|
||||
const (
|
||||
@ -228,7 +228,9 @@ func getStatusResponseCopy(uidValidity uint32, sourceSeq, targetSeq *OrderedSeq)
|
||||
|
||||
// CopyResponse prepares OK response with extended UID information about copied message.
|
||||
func CopyResponse(uidValidity uint32, sourceSeq, targetSeq *OrderedSeq) error {
|
||||
return server.ErrStatusResp(getStatusResponseCopy(uidValidity, sourceSeq, targetSeq))
|
||||
return &imap.ErrStatusResp{
|
||||
Resp: getStatusResponseCopy(uidValidity, sourceSeq, targetSeq),
|
||||
}
|
||||
}
|
||||
|
||||
func getStatusResponseAppend(uidValidity uint32, targetSeq *OrderedSeq) *imap.StatusResp {
|
||||
@ -250,5 +252,7 @@ func getStatusResponseAppend(uidValidity uint32, targetSeq *OrderedSeq) *imap.St
|
||||
|
||||
// AppendResponse prepares OK response with extended UID information about appended message.
|
||||
func AppendResponse(uidValidity uint32, targetSeq *OrderedSeq) error {
|
||||
return server.ErrStatusResp(getStatusResponseAppend(uidValidity, targetSeq))
|
||||
return &imap.ErrStatusResp{
|
||||
Resp: getStatusResponseAppend(uidValidity, targetSeq),
|
||||
}
|
||||
}
|
||||
|
||||
@ -135,7 +135,7 @@ func (iu *imapUser) ListMailboxes(showOnlySubcribed bool) ([]goIMAPBackend.Mailb
|
||||
if showOnlySubcribed && !iu.isSubscribed(storeMailbox.LabelID()) {
|
||||
continue
|
||||
}
|
||||
mailbox := newIMAPMailbox(iu.panicHandler, iu, storeMailbox)
|
||||
mailbox := newIMAPMailbox(iu.panicHandler, iu, storeMailbox, iu.backend.builder)
|
||||
mailboxes = append(mailboxes, mailbox)
|
||||
}
|
||||
|
||||
@ -167,7 +167,7 @@ func (iu *imapUser) GetMailbox(name string) (mb goIMAPBackend.Mailbox, err error
|
||||
return
|
||||
}
|
||||
|
||||
return newIMAPMailbox(iu.panicHandler, iu, storeMailbox), nil
|
||||
return newIMAPMailbox(iu.panicHandler, iu, storeMailbox, iu.backend.builder), nil
|
||||
}
|
||||
|
||||
// CreateMailbox creates a new mailbox.
|
||||
|
||||
@ -88,7 +88,7 @@ func (ie *ImportExport) ReportBug(osType, osVersion, description, accountName, a
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReportFile submits import report file
|
||||
// ReportFile submits import report file.
|
||||
func (ie *ImportExport) ReportFile(osType, osVersion, accountName, address string, logdata []byte) error {
|
||||
c := ie.clientManager.GetAnonymousClient()
|
||||
defer c.Logout()
|
||||
@ -118,9 +118,9 @@ func (ie *ImportExport) ReportFile(osType, osVersion, accountName, address strin
|
||||
}
|
||||
|
||||
// GetLocalImporter returns transferrer from local EML or MBOX structure to ProtonMail account.
|
||||
func (ie *ImportExport) GetLocalImporter(address, path string) (*transfer.Transfer, error) {
|
||||
func (ie *ImportExport) GetLocalImporter(username, address, path string) (*transfer.Transfer, error) {
|
||||
source := transfer.NewLocalProvider(path)
|
||||
target, err := ie.getPMAPIProvider(address)
|
||||
target, err := ie.getPMAPIProvider(username, address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -132,12 +132,12 @@ func (ie *ImportExport) GetLocalImporter(address, path string) (*transfer.Transf
|
||||
}
|
||||
|
||||
// GetRemoteImporter returns transferrer from remote IMAP to ProtonMail account.
|
||||
func (ie *ImportExport) GetRemoteImporter(address, username, password, host, port string) (*transfer.Transfer, error) {
|
||||
source, err := transfer.NewIMAPProvider(username, password, host, port)
|
||||
func (ie *ImportExport) GetRemoteImporter(username, address, remoteUsername, remotePassword, host, port string) (*transfer.Transfer, error) {
|
||||
source, err := transfer.NewIMAPProvider(remoteUsername, remotePassword, host, port)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
target, err := ie.getPMAPIProvider(address)
|
||||
target, err := ie.getPMAPIProvider(username, address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -149,8 +149,8 @@ func (ie *ImportExport) GetRemoteImporter(address, username, password, host, por
|
||||
}
|
||||
|
||||
// GetEMLExporter returns transferrer from ProtonMail account to local EML structure.
|
||||
func (ie *ImportExport) GetEMLExporter(address, path string) (*transfer.Transfer, error) {
|
||||
source, err := ie.getPMAPIProvider(address)
|
||||
func (ie *ImportExport) GetEMLExporter(username, address, path string) (*transfer.Transfer, error) {
|
||||
source, err := ie.getPMAPIProvider(username, address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -163,8 +163,8 @@ func (ie *ImportExport) GetEMLExporter(address, path string) (*transfer.Transfer
|
||||
}
|
||||
|
||||
// GetMBOXExporter returns transferrer from ProtonMail account to local MBOX structure.
|
||||
func (ie *ImportExport) GetMBOXExporter(address, path string) (*transfer.Transfer, error) {
|
||||
source, err := ie.getPMAPIProvider(address)
|
||||
func (ie *ImportExport) GetMBOXExporter(username, address, path string) (*transfer.Transfer, error) {
|
||||
source, err := ie.getPMAPIProvider(username, address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -176,8 +176,8 @@ func (ie *ImportExport) GetMBOXExporter(address, path string) (*transfer.Transfe
|
||||
return transfer.New(ie.panicHandler, newExportMetricsManager(ie), logsPath, ie.cache.GetTransferDir(), source, target)
|
||||
}
|
||||
|
||||
func (ie *ImportExport) getPMAPIProvider(address string) (*transfer.PMAPIProvider, error) {
|
||||
user, err := ie.Users.GetUser(address)
|
||||
func (ie *ImportExport) getPMAPIProvider(username, address string) (*transfer.PMAPIProvider, error) {
|
||||
user, err := ie.Users.GetUser(username)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@ -32,9 +32,9 @@ import (
|
||||
// On linux:
|
||||
// - settings: ~/.config/protonmail/<app>
|
||||
// - logs: ~/.cache/protonmail/<app>/logs
|
||||
// - cache: ~/.cache/protonmail/<app>/cache
|
||||
// - updates: ~/.cache/protonmail/<app>/updates
|
||||
// - lockfile: ~/.cache/protonmail/<app>/<app>.lock
|
||||
// - cache: ~/.config/protonmail/<app>/cache
|
||||
// - updates: ~/.config/protonmail/<app>/updates
|
||||
// - lockfile: ~/.cache/protonmail/<app>/<app>.lock .
|
||||
type Locations struct {
|
||||
userConfig, userCache string
|
||||
configName string
|
||||
@ -129,7 +129,7 @@ func (l *Locations) ProvideLogsPath() (string, error) {
|
||||
return l.getLogsPath(), nil
|
||||
}
|
||||
|
||||
// ProvideCachePath returns a location for user cache dirs (e.g. ~/.cache/<company>/<app>/cache).
|
||||
// ProvideCachePath returns a location for user cache dirs (e.g. ~/.config/<company>/<app>/cache).
|
||||
// It creates it if it doesn't already exist.
|
||||
func (l *Locations) ProvideCachePath() (string, error) {
|
||||
if err := os.MkdirAll(l.getCachePath(), 0700); err != nil {
|
||||
@ -139,6 +139,11 @@ func (l *Locations) ProvideCachePath() (string, error) {
|
||||
return l.getCachePath(), nil
|
||||
}
|
||||
|
||||
// GetOldCachePath returns a former location for user cache dirs used for migration scripts only.
|
||||
func (l *Locations) GetOldCachePath() string {
|
||||
return filepath.Join(l.userCache, "cache")
|
||||
}
|
||||
|
||||
// ProvideUpdatesPath returns a location for update files (e.g. ~/.cache/<company>/<app>/updates).
|
||||
// It creates it if it doesn't already exist.
|
||||
func (l *Locations) ProvideUpdatesPath() (string, error) {
|
||||
@ -149,6 +154,16 @@ func (l *Locations) ProvideUpdatesPath() (string, error) {
|
||||
return l.getUpdatesPath(), nil
|
||||
}
|
||||
|
||||
// GetUpdatesPath returns a new location for update files used for migration scripts only.
|
||||
func (l *Locations) GetUpdatesPath() string {
|
||||
return l.getUpdatesPath()
|
||||
}
|
||||
|
||||
// GetOldUpdatesPath returns a former location for update files used for migration scripts only.
|
||||
func (l *Locations) GetOldUpdatesPath() string {
|
||||
return filepath.Join(l.userCache, "updates")
|
||||
}
|
||||
|
||||
func (l *Locations) getSettingsPath() string {
|
||||
return l.userConfig
|
||||
}
|
||||
@ -158,11 +173,33 @@ func (l *Locations) getLogsPath() string {
|
||||
}
|
||||
|
||||
func (l *Locations) getCachePath() string {
|
||||
return filepath.Join(l.userCache, "cache")
|
||||
// Bridge cache is not a typical cache which can be deleted with only
|
||||
// downside that the app has to download everything again.
|
||||
// Cache for bridge is database with IMAP UIDs and UIDVALIDITY, and also
|
||||
// other IMAP setup. Deleting such data leads to either re-sync of client,
|
||||
// or mix of headers and bodies. Both is caused because of need of re-sync
|
||||
// between Bridge and API which will happen in different order than before.
|
||||
// In the first case, UIDVALIDITY is also changed and causes the better
|
||||
// outcome to "just" re-sync everything; in the later, UIDVALIDITY stays
|
||||
// the same, causing the client to not re-sync but UIDs in the client does
|
||||
// not match UIDs in Bridge.
|
||||
// Because users might use tools to regularly clear caches, Bridge cache
|
||||
// cannot be located in a standard cache folder.
|
||||
return filepath.Join(l.userConfig, "cache")
|
||||
}
|
||||
|
||||
func (l *Locations) getUpdatesPath() string {
|
||||
return filepath.Join(l.userCache, "updates")
|
||||
// In order to properly update Bridge 1.6.X and higher we need to
|
||||
// change the launcher first. Since this is not part of automatic
|
||||
// updates the migration must wait until manual update. Until that
|
||||
// we need to keep old path.
|
||||
if l.configName == "bridge" {
|
||||
return l.GetOldUpdatesPath()
|
||||
}
|
||||
|
||||
// Users might use tools to regularly clear caches, which would mean always
|
||||
// removing updates, therefore Bridge updates have to be somewhere else.
|
||||
return filepath.Join(l.userConfig, "updates")
|
||||
}
|
||||
|
||||
// Clear removes everything except the lock and update files.
|
||||
|
||||
@ -45,7 +45,8 @@ func TestClearRemovesEverythingExceptLockAndUpdateFiles(t *testing.T) {
|
||||
assert.NoError(t, l.Clear())
|
||||
|
||||
assert.FileExists(t, l.GetLockFile())
|
||||
assert.NoDirExists(t, l.getSettingsPath())
|
||||
assert.DirExists(t, l.getSettingsPath())
|
||||
assert.NoFileExists(t, filepath.Join(l.getSettingsPath(), "prefs.json"))
|
||||
assert.NoDirExists(t, l.getLogsPath())
|
||||
assert.NoDirExists(t, l.getCachePath())
|
||||
assert.DirExists(t, l.getUpdatesPath())
|
||||
@ -58,6 +59,7 @@ func TestClearUpdateFiles(t *testing.T) {
|
||||
|
||||
assert.FileExists(t, l.GetLockFile())
|
||||
assert.DirExists(t, l.getSettingsPath())
|
||||
assert.FileExists(t, filepath.Join(l.getSettingsPath(), "prefs.json"))
|
||||
assert.DirExists(t, l.getLogsPath())
|
||||
assert.DirExists(t, l.getCachePath())
|
||||
assert.NoDirExists(t, l.getUpdatesPath())
|
||||
@ -75,6 +77,7 @@ func TestCleanLeavesStandardLocationsUntouched(t *testing.T) {
|
||||
|
||||
assert.FileExists(t, l.GetLockFile())
|
||||
assert.DirExists(t, l.getSettingsPath())
|
||||
assert.FileExists(t, filepath.Join(l.getSettingsPath(), "prefs.json"))
|
||||
assert.DirExists(t, l.getLogsPath())
|
||||
assert.FileExists(t, filepath.Join(l.getLogsPath(), "log1.txt"))
|
||||
assert.FileExists(t, filepath.Join(l.getLogsPath(), "log2.txt"))
|
||||
@ -138,6 +141,9 @@ func newTestLocations(t *testing.T) *Locations {
|
||||
require.NoError(t, err)
|
||||
require.DirExists(t, settings)
|
||||
|
||||
createFilesInDir(t, settings, "prefs.json")
|
||||
require.FileExists(t, filepath.Join(settings, "prefs.json"))
|
||||
|
||||
logs, err := l.ProvideLogsPath()
|
||||
require.NoError(t, err)
|
||||
require.DirExists(t, logs)
|
||||
|
||||
@ -34,7 +34,7 @@ func DumpStackTrace(logsPath string) crash.RecoveryAction {
|
||||
return func(r interface{}) error {
|
||||
file := filepath.Join(logsPath, getStackTraceName(constants.Version, constants.Revision))
|
||||
|
||||
f, err := os.OpenFile(file, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0600)
|
||||
f, err := os.OpenFile(filepath.Clean(file), os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -42,6 +42,7 @@ const (
|
||||
|
||||
func Init(logsPath string) error {
|
||||
logrus.SetFormatter(&logrus.TextFormatter{
|
||||
ForceColors: true,
|
||||
FullTimestamp: true,
|
||||
TimestampFormat: time.StampMilli,
|
||||
})
|
||||
@ -69,6 +70,10 @@ func Init(logsPath string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetLevel will change the level of logging and in case of Debug or Trace
|
||||
// level it will also prevent from writing to file. Setting level to Info or
|
||||
// higher will not set writing to file again if it was previously cancelled by
|
||||
// Debug or Trace.
|
||||
func SetLevel(level string) {
|
||||
if lvl, err := logrus.ParseLevel(level); err == nil {
|
||||
logrus.SetLevel(lvl)
|
||||
|
||||
@ -51,7 +51,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 {
|
||||
|
||||
@ -173,7 +173,7 @@ func (b *sendPreferencesBuilder) withPublicKey(v *crypto.KeyRing) {
|
||||
// | 16 (PGP/MIME),
|
||||
// mimeType: 'text/html' | 'text/plain' | 'multipart/mixed',
|
||||
// publicKey: OpenPGPKey | undefined/null
|
||||
// }
|
||||
// }.
|
||||
func (b *sendPreferencesBuilder) build() (p SendPreferences) {
|
||||
p.Encrypt = b.shouldEncrypt()
|
||||
p.Sign = b.shouldSign()
|
||||
@ -492,6 +492,8 @@ func (b *sendPreferencesBuilder) setEncryptionPreferences(mailSettings pmapi.Mai
|
||||
b.withSchemeDefault(pgpInline)
|
||||
case pmapi.PGPMIMEPackage:
|
||||
b.withSchemeDefault(pgpMIME)
|
||||
case pmapi.ClearMIMEPackage, pmapi.ClearPackage, pmapi.EncryptedOutsidePackage, pmapi.InternalPackage:
|
||||
// nothing to set
|
||||
}
|
||||
|
||||
// Its value is constrained by the sign flag and the PGP scheme:
|
||||
|
||||
@ -42,6 +42,7 @@ func NewSMTPServer(debug bool, port int, useSSL bool, tls *tls.Config, smtpBacke
|
||||
s.TLSConfig = tls
|
||||
s.Domain = bridge.Host
|
||||
s.AllowInsecureAuth = true
|
||||
s.MaxLineLength = 2 << 16
|
||||
|
||||
if debug {
|
||||
fmt.Println("THE LOG WILL CONTAIN **DECRYPTED** MESSAGE DATA")
|
||||
|
||||
@ -156,6 +156,7 @@ func (loop *eventLoop) loop() {
|
||||
return
|
||||
case <-t.C:
|
||||
// Randomise periodic calls within range pollInterval ± pollSpread to reduces potential load spikes on API.
|
||||
//nolint[gosec] It is OK to use weaker random number generator here
|
||||
time.Sleep(time.Duration(rand.Intn(2*int(pollIntervalSpread.Milliseconds()))) * time.Millisecond)
|
||||
case eventProcessedCh = <-loop.pollCh:
|
||||
// We don't want to wait here. Polling should happen instantly.
|
||||
@ -268,8 +269,6 @@ func (loop *eventLoop) processNextEvent() (more bool, err error) { // nolint[fun
|
||||
return false, errors.New("received empty event")
|
||||
}
|
||||
|
||||
l = l.WithField("newEventID", event.EventID)
|
||||
|
||||
if err = loop.processEvent(event); err != nil {
|
||||
return false, errors.Wrap(err, "failed to process event")
|
||||
}
|
||||
@ -383,6 +382,8 @@ func (loop *eventLoop) processAddresses(log *logrus.Entry, addressEvents []*pmap
|
||||
log.WithField("email", email).Debug("Address was deleted")
|
||||
loop.user.CloseConnection(email)
|
||||
loop.events.Emit(bridgeEvents.AddressChangedLogoutEvent, email)
|
||||
case pmapi.EventUpdateFlags:
|
||||
log.Error("EventUpdateFlags for address event is uknown operation")
|
||||
}
|
||||
}
|
||||
|
||||
@ -411,6 +412,8 @@ func (loop *eventLoop) processLabels(eventLog *logrus.Entry, labels []*pmapi.Eve
|
||||
if err := loop.store.deleteMailboxEvent(eventLabel.ID); err != nil {
|
||||
return errors.Wrap(err, "failed to delete label")
|
||||
}
|
||||
case pmapi.EventUpdateFlags:
|
||||
log.Error("EventUpdateFlags for label event is uknown operation")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -254,7 +254,7 @@ func (storeMailbox *Mailbox) txGetAPIIDsBucket(tx *bolt.Tx) *bolt.Bucket {
|
||||
return storeMailbox.txGetBucket(tx).Bucket(apiIDsBucket)
|
||||
}
|
||||
|
||||
// txGetDeletedIDsBucket returns the bucket with messagesID marked as deleted
|
||||
// txGetDeletedIDsBucket returns the bucket with messagesID marked as deleted.
|
||||
func (storeMailbox *Mailbox) txGetDeletedIDsBucket(tx *bolt.Tx) *bolt.Bucket {
|
||||
return storeMailbox.txGetBucket(tx).Bucket(deletedIDsBucket)
|
||||
}
|
||||
|
||||
@ -25,7 +25,7 @@ import (
|
||||
)
|
||||
|
||||
// ErrAllMailOpNotAllowed is error user when user tries to do unsupported
|
||||
// operation on All Mail folder
|
||||
// operation on All Mail folder.
|
||||
var ErrAllMailOpNotAllowed = errors.New("operation not allowed for 'All Mail' folder")
|
||||
|
||||
// GetMessage returns the `pmapi.Message` struct wrapped in `StoreMessage`
|
||||
@ -67,10 +67,14 @@ func (storeMailbox *Mailbox) ImportMessage(msg *pmapi.Message, body []byte, labe
|
||||
}
|
||||
|
||||
res, err := storeMailbox.client().Import([]*pmapi.ImportMsgReq{importReqs})
|
||||
if err == nil && len(res) > 0 {
|
||||
msg.ID = res[0].MessageID
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
if len(res) == 0 {
|
||||
return errors.New("no import response")
|
||||
}
|
||||
msg.ID = res[0].MessageID
|
||||
return res[0].Error
|
||||
}
|
||||
|
||||
// LabelMessages adds the label by calling an API.
|
||||
@ -173,7 +177,7 @@ func (storeMailbox *Mailbox) MarkMessagesUnstarred(apiIDs []string) error {
|
||||
}
|
||||
|
||||
// MarkMessagesDeleted adds local flag \Deleted. This is not propagated to API
|
||||
// until RemoveDeleted is called
|
||||
// until RemoveDeleted is called.
|
||||
func (storeMailbox *Mailbox) MarkMessagesDeleted(apiIDs []string) error {
|
||||
log.WithFields(logrus.Fields{
|
||||
"messages": apiIDs,
|
||||
|
||||
@ -18,7 +18,10 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"net/mail"
|
||||
"net/textproto"
|
||||
|
||||
pkgMsg "github.com/ProtonMail/proton-bridge/pkg/message"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
@ -64,7 +67,7 @@ func (message *Message) Message() *pmapi.Message {
|
||||
}
|
||||
|
||||
// IsMarkedDeleted returns true if message is marked as deleted for specific
|
||||
// mailbox
|
||||
// mailbox.
|
||||
func (message *Message) IsMarkedDeleted() bool {
|
||||
isMarkedAsDeleted := false
|
||||
err := message.storeMailbox.db().View(func(tx *bolt.Tx) error {
|
||||
@ -103,6 +106,8 @@ func (message *Message) SetSize(size int64) error {
|
||||
// header of decrypted message. This should not trigger any IMAP update.
|
||||
// NOTE: Content type depends on details of decrypted message which we want to
|
||||
// cache.
|
||||
//
|
||||
// Deprecated: Use SetHeader instead.
|
||||
func (message *Message) SetContentTypeAndHeader(mimeType string, header mail.Header) error {
|
||||
message.msg.MIMEType = mimeType
|
||||
message.msg.Header = header
|
||||
@ -121,6 +126,45 @@ func (message *Message) SetContentTypeAndHeader(mimeType string, header mail.Hea
|
||||
return message.store.db.Update(txUpdate)
|
||||
}
|
||||
|
||||
// SetHeader checks header can be parsed and if yes it stores header bytes in
|
||||
// database.
|
||||
func (message *Message) SetHeader(header []byte) error {
|
||||
_, err := textproto.NewReader(bufio.NewReader(bytes.NewReader(header))).ReadMIMEHeader()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return message.store.db.Update(func(tx *bolt.Tx) error {
|
||||
return tx.Bucket(headersBucket).Put([]byte(message.ID()), header)
|
||||
})
|
||||
}
|
||||
|
||||
// IsFullHeaderCached will check that valid full header is stored in DB.
|
||||
func (message *Message) IsFullHeaderCached() bool {
|
||||
header, err := message.getRawHeader()
|
||||
return err == nil && header != nil
|
||||
}
|
||||
|
||||
func (message *Message) getRawHeader() (raw []byte, err error) {
|
||||
err = message.store.db.View(func(tx *bolt.Tx) error {
|
||||
raw = tx.Bucket(headersBucket).Get([]byte(message.ID()))
|
||||
return nil
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// GetHeader will return cached header from DB.
|
||||
func (message *Message) GetHeader() textproto.MIMEHeader {
|
||||
raw, err := message.getRawHeader()
|
||||
if err != nil && raw == nil {
|
||||
return textproto.MIMEHeader(message.msg.Header)
|
||||
}
|
||||
header, err := textproto.NewReader(bufio.NewReader(bytes.NewReader(raw))).ReadMIMEHeader()
|
||||
if err != nil {
|
||||
return textproto.MIMEHeader(message.msg.Header)
|
||||
}
|
||||
return header
|
||||
}
|
||||
|
||||
// SetBodyStructure stores serialized body structure in database.
|
||||
func (message *Message) SetBodyStructure(bs *pkgMsg.BodyStructure) error {
|
||||
txUpdate := func(tx *bolt.Tx) error {
|
||||
@ -148,3 +192,17 @@ func (message *Message) GetBodyStructure() (bs *pkgMsg.BodyStructure, err error)
|
||||
}
|
||||
return bs, nil
|
||||
}
|
||||
|
||||
func (message *Message) IncreaseBuildCount() (times uint32, err error) {
|
||||
txUpdate := func(tx *bolt.Tx) error {
|
||||
times, err = message.store.txIncreaseMsgBuildCount(
|
||||
tx.Bucket(msgBuildCountBucket),
|
||||
message.ID(),
|
||||
)
|
||||
return err
|
||||
}
|
||||
if err = message.store.db.Update(txUpdate); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return times, nil
|
||||
}
|
||||
|
||||
@ -34,15 +34,15 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
// PathDelimiter for IMAP
|
||||
// PathDelimiter for IMAP.
|
||||
PathDelimiter = "/"
|
||||
// UserLabelsMailboxName for IMAP
|
||||
// UserLabelsMailboxName for IMAP.
|
||||
UserLabelsMailboxName = "Labels"
|
||||
// UserLabelsPrefix contains name with delimiter for IMAP
|
||||
// UserLabelsPrefix contains name with delimiter for IMAP.
|
||||
UserLabelsPrefix = UserLabelsMailboxName + PathDelimiter
|
||||
// UserFoldersMailboxName for IMAP
|
||||
// UserFoldersMailboxName for IMAP.
|
||||
UserFoldersMailboxName = "Folders"
|
||||
// UserFoldersPrefix contains name with delimiter for IMAP
|
||||
// UserFoldersPrefix contains name with delimiter for IMAP.
|
||||
UserFoldersPrefix = UserFoldersMailboxName + PathDelimiter
|
||||
)
|
||||
|
||||
@ -51,9 +51,13 @@ var (
|
||||
|
||||
// Database structure:
|
||||
// * metadata
|
||||
// * {messageID} -> message data (subject, from, to, time, headers, body size, ...)
|
||||
// * {messageID} -> message data (subject, from, to, time, body size, ...)
|
||||
// * headers
|
||||
// * {messageID} -> header bytes
|
||||
// * bodystructure
|
||||
// * {messageID} -> message body structure
|
||||
// * msgbuildcount
|
||||
// * {messageID} -> uint32 number of message builds to track re-sync issues
|
||||
// * counts
|
||||
// * {mailboxID} -> mailboxCounts: totalOnAPI, unreadOnAPI, labelName, labelColor, labelIsExclusive
|
||||
// * address_info
|
||||
@ -75,7 +79,9 @@ var (
|
||||
// * deleted_ids (can be missing or have no keys)
|
||||
// * {messageID} -> true
|
||||
metadataBucket = []byte("metadata") //nolint[gochecknoglobals]
|
||||
headersBucket = []byte("headers") //nolint[gochecknoglobals]
|
||||
bodystructureBucket = []byte("bodystructure") //nolint[gochecknoglobals]
|
||||
msgBuildCountBucket = []byte("msgbuildcount") //nolint[gochecknoglobals]
|
||||
countsBucket = []byte("counts") //nolint[gochecknoglobals]
|
||||
addressInfoBucket = []byte("address_info") //nolint[gochecknoglobals]
|
||||
addressModeBucket = []byte("address_mode") //nolint[gochecknoglobals]
|
||||
@ -196,36 +202,24 @@ func openBoltDatabase(filePath string) (db *bolt.DB, err error) {
|
||||
}
|
||||
|
||||
tx := func(tx *bolt.Tx) (err error) {
|
||||
if _, err = tx.CreateBucketIfNotExists(metadataBucket); err != nil {
|
||||
return
|
||||
buckets := [][]byte{
|
||||
metadataBucket,
|
||||
headersBucket,
|
||||
bodystructureBucket,
|
||||
msgBuildCountBucket,
|
||||
countsBucket,
|
||||
addressInfoBucket,
|
||||
addressModeBucket,
|
||||
syncStateBucket,
|
||||
mailboxesBucket,
|
||||
mboxVersionBucket,
|
||||
}
|
||||
|
||||
if _, err = tx.CreateBucketIfNotExists(bodystructureBucket); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = tx.CreateBucketIfNotExists(countsBucket); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = tx.CreateBucketIfNotExists(addressInfoBucket); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = tx.CreateBucketIfNotExists(addressModeBucket); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = tx.CreateBucketIfNotExists(syncStateBucket); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = tx.CreateBucketIfNotExists(mailboxesBucket); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = tx.CreateBucketIfNotExists(mboxVersionBucket); err != nil {
|
||||
return
|
||||
for _, bucket := range buckets {
|
||||
if _, err = tx.CreateBucketIfNotExists(bucket); err != nil {
|
||||
err = errors.Wrap(err, string(bucket))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
@ -263,7 +257,7 @@ func (store *Store) init(firstInit bool) (err error) {
|
||||
}
|
||||
}
|
||||
|
||||
store.log.WithField("mode", store.addressMode).Debug("Initialising store")
|
||||
store.log.WithField("mode", store.addressMode).Info("Initialising store")
|
||||
|
||||
labels, err := store.initCounts()
|
||||
if err != nil {
|
||||
|
||||
@ -90,10 +90,7 @@ func (store *Store) TestDumpDB(tb assert.TestingT) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := txMails(tx); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return txMails(tx)
|
||||
}
|
||||
|
||||
assert.NoError(tb, store.db.View(txDump))
|
||||
|
||||
@ -191,6 +191,19 @@ func (store *Store) txGetBodyStructure(bsBucket *bolt.Bucket, msgID string) (*pk
|
||||
return pkgMsg.DeserializeBodyStructure(raw)
|
||||
}
|
||||
|
||||
func (store *Store) txIncreaseMsgBuildCount(b *bolt.Bucket, msgID string) (uint32, error) {
|
||||
key := []byte(msgID)
|
||||
count := uint32(0)
|
||||
|
||||
raw := b.Get(key)
|
||||
if raw != nil {
|
||||
count = btoi(raw)
|
||||
}
|
||||
|
||||
count++
|
||||
return count, b.Put(key, itob(count))
|
||||
}
|
||||
|
||||
// createOrUpdateMessageEvent is helper to create only one message with
|
||||
// createOrUpdateMessagesEvent.
|
||||
func (store *Store) createOrUpdateMessageEvent(msg *pmapi.Message) error {
|
||||
@ -276,7 +289,7 @@ func clearNonMetadata(onlyMeta *pmapi.Message) {
|
||||
// If there is stored message in metaBucket the size, header and MIMEType are
|
||||
// not changed if already set. To change these:
|
||||
// * size must be updated by Message.SetSize
|
||||
// * contentType and header must be updated by Message.SetContentTypeAndHeader
|
||||
// * contentType and header must be updated by Message.SetContentTypeAndHeader.
|
||||
func txUpdateMetadaFromDB(metaBucket *bolt.Bucket, onlyMeta *pmapi.Message, log *logrus.Entry) {
|
||||
// Size attribute on the server is counting encrypted data. We need to compute
|
||||
// "real" size of decrypted data. Negative values will be processed during fetch.
|
||||
|
||||
@ -35,7 +35,7 @@ var systemFolderMapping = map[string]string{ //nolint[gochecknoglobals]
|
||||
// Add more translations.
|
||||
}
|
||||
|
||||
// LeastUsedColor is intended to return color for creating a new inbox or label
|
||||
// LeastUsedColor is intended to return color for creating a new inbox or label.
|
||||
func LeastUsedColor(mailboxes []Mailbox) string {
|
||||
usedColors := []string{}
|
||||
for _, m := range mailboxes {
|
||||
|
||||
@ -27,7 +27,7 @@ import (
|
||||
|
||||
type IMAPClientProvider interface {
|
||||
Capability() (map[string]bool, error)
|
||||
Support(cap string) (bool, error)
|
||||
Support(capability string) (bool, error)
|
||||
State() imap.ConnState
|
||||
SupportAuth(mech string) (bool, error)
|
||||
Authenticate(auth sasl.Client) error
|
||||
|
||||
@ -62,10 +62,10 @@ func imapClientDial(addr string) (IMAPClientProvider, error) {
|
||||
client.ErrorLog = &imapErrorLogger{logrus.WithField("pkg", "imap-client")}
|
||||
// Logrus `WriterLevel` fails for big messages because of bufio.MaxScanTokenSize limit.
|
||||
// Also, this spams a lot, uncomment once needed during development.
|
||||
//client.SetDebug(imap.NewDebugWriter(
|
||||
// client.SetDebug(imap.NewDebugWriter(
|
||||
// logrus.WithField("pkg", "imap/client").WriterLevel(logrus.TraceLevel),
|
||||
// logrus.WithField("pkg", "imap/server").WriterLevel(logrus.TraceLevel),
|
||||
//))
|
||||
// ))
|
||||
}
|
||||
return client, err
|
||||
}
|
||||
@ -84,7 +84,7 @@ func imapClientDialHelper(addr string) (*imapClient.Client, error) {
|
||||
var tlsConf *tls.Config
|
||||
if strings.Contains(strings.ToLower(host), "yahoo") {
|
||||
log.Warning("Yahoo server detected: limiting maximal TLS version to 1.2.")
|
||||
tlsConf = &tls.Config{MaxVersion: tls.VersionTLS12}
|
||||
tlsConf = &tls.Config{MaxVersion: tls.VersionTLS12} //nolint[gosec] G402
|
||||
}
|
||||
return imapClient.DialTLS(addr, tlsConf)
|
||||
}
|
||||
|
||||
@ -63,7 +63,7 @@ func (p *MBOXProvider) writeMessage(msg Message) error {
|
||||
}
|
||||
|
||||
mboxPath := filepath.Join(p.root, mboxName)
|
||||
mboxFile, err := os.OpenFile(mboxPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
|
||||
mboxFile, err := os.OpenFile(filepath.Clean(mboxPath), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
|
||||
if err != nil {
|
||||
multiErr = multierror.Append(multiErr, err)
|
||||
continue
|
||||
|
||||
@ -21,16 +21,24 @@ import (
|
||||
"sort"
|
||||
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/message"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
fetchWorkers = 20 // In how many workers to fetch message (group list on IMAP).
|
||||
attachWorkers = 5 // In how many workers to fetch attachments (for one message).
|
||||
buildWorkers = 20 // In how many workers to build messages.
|
||||
)
|
||||
|
||||
// PMAPIProvider implements import and export to/from ProtonMail server.
|
||||
type PMAPIProvider struct {
|
||||
clientManager ClientManager
|
||||
userID string
|
||||
addressID string
|
||||
keyRing *crypto.KeyRing
|
||||
builder *message.Builder
|
||||
|
||||
nextImportRequests map[string]*pmapi.ImportMsgReq // Key is msg transfer ID.
|
||||
nextImportRequestsSize int
|
||||
@ -44,6 +52,7 @@ func NewPMAPIProvider(clientManager ClientManager, userID, addressID string) (*P
|
||||
clientManager: clientManager,
|
||||
userID: userID,
|
||||
addressID: addressID,
|
||||
builder: message.NewBuilder(fetchWorkers, attachWorkers, buildWorkers),
|
||||
|
||||
nextImportRequests: map[string]*pmapi.ImportMsgReq{},
|
||||
nextImportRequestsSize: 0,
|
||||
|
||||
@ -18,12 +18,13 @@
|
||||
package transfer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
pkgMsg "github.com/ProtonMail/proton-bridge/pkg/message"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/message"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
@ -144,6 +145,7 @@ func (p *PMAPIProvider) transferTo(rule *Rule, progress *Progress, ch chan<- Mes
|
||||
|
||||
func (p *PMAPIProvider) exportMessage(rule *Rule, progress *Progress, pmapiMsgID, msgID string, skipEncryptedMessages bool) (Message, error) {
|
||||
var msg *pmapi.Message
|
||||
|
||||
progress.callWrap(func() error {
|
||||
var err error
|
||||
msg, err = p.getMessage(pmapiMsgID)
|
||||
@ -153,19 +155,18 @@ func (p *PMAPIProvider) exportMessage(rule *Rule, progress *Progress, pmapiMsgID
|
||||
p.timeIt.start("build", msgID)
|
||||
defer p.timeIt.stop("build", msgID)
|
||||
|
||||
msgBuilder := pkgMsg.NewBuilder(p.client(), msg)
|
||||
msgBuilder.EncryptedToHTML = false
|
||||
_, body, err := msgBuilder.BuildMessage()
|
||||
body, err := p.builder.NewJobWithOptions(
|
||||
context.Background(),
|
||||
p.client(),
|
||||
msg.ID,
|
||||
message.JobOptions{IgnoreDecryptionErrors: !skipEncryptedMessages},
|
||||
).GetResult()
|
||||
if err != nil {
|
||||
return Message{
|
||||
Body: body, // Keep body to show details about the message to user.
|
||||
}, errors.Wrap(err, "failed to build message")
|
||||
}
|
||||
if errors.Is(err, message.ErrDecryptionFailed) && skipEncryptedMessages {
|
||||
err = errors.New("skipping encrypted message")
|
||||
}
|
||||
|
||||
if !msgBuilder.SuccessfullyDecrypted() && skipEncryptedMessages {
|
||||
return Message{
|
||||
Body: body, // Keep body to show details about the message to user.
|
||||
}, errors.New("skipping encrypted message")
|
||||
return Message{Body: []byte(msg.Body)}, err
|
||||
}
|
||||
|
||||
unread := false
|
||||
|
||||
@ -329,10 +329,10 @@ func (p *PMAPIProvider) importMessage(msgSourceID string, progress *Progress, re
|
||||
}
|
||||
if results[0].Error != nil {
|
||||
importedErr = errors.Wrap(results[0].Error, "failed to import message")
|
||||
return nil // Call passed but API refused this message, skip this one.
|
||||
return nil //nolint[nilerr] Call passed but API refused this message, skip this one.
|
||||
}
|
||||
importedID = results[0].MessageID
|
||||
return nil
|
||||
})
|
||||
return
|
||||
return importedID, importedErr
|
||||
}
|
||||
|
||||
@ -85,7 +85,7 @@ func testTransferFrom(t *testing.T, rules transferRules, provider TargetProvider
|
||||
progress.finish()
|
||||
}()
|
||||
|
||||
maxWait := time.Duration(len(messages)) * 2 * time.Second
|
||||
maxWait := time.Duration(len(messages)*2) * time.Second
|
||||
a.Eventually(t, func() bool {
|
||||
return progress.updateCh == nil
|
||||
}, maxWait, 10*time.Millisecond, "Waiting for imported messages timed out")
|
||||
|
||||
@ -125,7 +125,7 @@ func mkdirAllClear(path string) error {
|
||||
|
||||
// checksum assumes the file is a regular file and that it exists.
|
||||
func checksum(path string) (hash string) {
|
||||
file, err := os.Open(path) //nolint[gosec]
|
||||
file, err := os.Open(filepath.Clean(path))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@ -224,7 +224,7 @@ func copyRecursively(srcDir, dstDir string) error { // nolint[funlen]
|
||||
}
|
||||
|
||||
// Create/overwrite regular file.
|
||||
srcReader, err := os.Open(srcPath) //nolint[gosec]
|
||||
srcReader, err := os.Open(filepath.Clean(srcPath))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -244,7 +244,7 @@ func copyToTmpFileRename(srcReader io.Reader, dstPath string, dstMode os.FileMod
|
||||
|
||||
func copyToFileTruncate(srcReader io.Reader, dstPath string, dstMode os.FileMode) error {
|
||||
logrus.Debug("Copy and truncate ", dstPath)
|
||||
dstWriter, err := os.OpenFile(dstPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, dstMode)
|
||||
dstWriter, err := os.OpenFile(filepath.Clean(dstPath), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, dstMode) //nolint[gosec] Cannot guess the safe part of path
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -78,7 +78,7 @@ type VersionInfo struct {
|
||||
// "...": {
|
||||
// ...
|
||||
// }
|
||||
// }
|
||||
// }.
|
||||
type VersionMap map[string]VersionInfo
|
||||
|
||||
// getVersionFileURL returns the URL of the version file.
|
||||
|
||||
@ -67,7 +67,7 @@ func (s *testCredentials) MarshalGob() string {
|
||||
if err := enc.Encode(s); err != nil {
|
||||
return ""
|
||||
}
|
||||
fmt.Printf("MarshalGob: %#v\n", buf.String())
|
||||
log.Infof("MarshalGob: %#v\n", buf.String())
|
||||
return base64.StdEncoding.EncodeToString(buf.Bytes())
|
||||
}
|
||||
|
||||
@ -88,13 +88,13 @@ func (s *testCredentials) UnmarshalGob(secret string) error {
|
||||
s.Clear()
|
||||
b, err := base64.StdEncoding.DecodeString(secret)
|
||||
if err != nil {
|
||||
fmt.Println("decode base64", b)
|
||||
log.Infoln("decode base64", b)
|
||||
return err
|
||||
}
|
||||
buf := bytes.NewBuffer(b)
|
||||
dec := gob.NewDecoder(buf)
|
||||
if err = dec.Decode(s); err != nil {
|
||||
fmt.Println("decode gob", b, buf.Bytes())
|
||||
log.Info("decode gob", b, buf.Bytes())
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
@ -102,7 +102,7 @@ func (s *testCredentials) UnmarshalGob(secret string) error {
|
||||
|
||||
func (s *testCredentials) ToJSON() string {
|
||||
if b, err := json.Marshal(s); err == nil {
|
||||
fmt.Printf("MarshalJSON: %#v\n", string(b))
|
||||
log.Infof("MarshalJSON: %#v\n", string(b))
|
||||
return base64.StdEncoding.EncodeToString(b)
|
||||
}
|
||||
return ""
|
||||
@ -134,7 +134,7 @@ func (s *testCredentials) MarshalFmt() string {
|
||||
s.IsHidden,
|
||||
s.IsCombinedAddressMode,
|
||||
)
|
||||
fmt.Printf("MarshalFmt: %#v\n", buf.String())
|
||||
log.Infof("MarshalFmt: %#v\n", buf.String())
|
||||
return base64.StdEncoding.EncodeToString(buf.Bytes())
|
||||
}
|
||||
|
||||
@ -144,7 +144,7 @@ func (s *testCredentials) UnmarshalFmt(secret string) error {
|
||||
return err
|
||||
}
|
||||
buf := bytes.NewBuffer(b)
|
||||
fmt.Println("decode fmt", b, buf.Bytes())
|
||||
log.Infoln("decode fmt", b, buf.Bytes())
|
||||
_, err = fmt.Fscanf(
|
||||
buf, secretFormat,
|
||||
&s.UserID,
|
||||
@ -190,7 +190,7 @@ func (s *testCredentials) MarshalStrings() string { // this is the most space ef
|
||||
|
||||
str := strings.Join(items, sep)
|
||||
|
||||
fmt.Printf("MarshalJoin: %#v\n", str)
|
||||
log.Infof("MarshalJoin: %#v\n", str)
|
||||
return base64.StdEncoding.EncodeToString([]byte(str))
|
||||
}
|
||||
|
||||
@ -237,37 +237,37 @@ func (s *testCredentials) IsSame(rhs *testCredentials) bool {
|
||||
|
||||
func TestMarshalFormats(t *testing.T) {
|
||||
input := testCredentials{UserID: "007", Emails: "ja@pm.me;jakub@cu.th", Timestamp: 152469263742, IsHidden: true}
|
||||
fmt.Printf("input %#v\n", input)
|
||||
log.Infof("input %#v\n", input)
|
||||
|
||||
secretStrings := input.MarshalStrings()
|
||||
fmt.Printf("secretStrings %#v %d\n", secretStrings, len(secretStrings))
|
||||
log.Infof("secretStrings %#v %d\n", secretStrings, len(secretStrings))
|
||||
secretGob := input.MarshalGob()
|
||||
fmt.Printf("secretGob %#v %d\n", secretGob, len(secretGob))
|
||||
log.Infof("secretGob %#v %d\n", secretGob, len(secretGob))
|
||||
secretJSON := input.ToJSON()
|
||||
fmt.Printf("secretJSON %#v %d\n", secretJSON, len(secretJSON))
|
||||
log.Infof("secretJSON %#v %d\n", secretJSON, len(secretJSON))
|
||||
secretFmt := input.MarshalFmt()
|
||||
fmt.Printf("secretFmt %#v %d\n", secretFmt, len(secretFmt))
|
||||
log.Infof("secretFmt %#v %d\n", secretFmt, len(secretFmt))
|
||||
|
||||
output := testCredentials{APIToken: "refresh"}
|
||||
require.NoError(t, output.UnmarshalStrings(secretStrings))
|
||||
fmt.Printf("strings out %#v \n", output)
|
||||
log.Infof("strings out %#v \n", output)
|
||||
require.True(t, input.IsSame(&output), "strings out not same")
|
||||
|
||||
output = testCredentials{APIToken: "refresh"}
|
||||
require.NoError(t, output.UnmarshalGob(secretGob))
|
||||
fmt.Printf("gob out %#v\n \n", output)
|
||||
log.Infof("gob out %#v\n \n", output)
|
||||
assert.Equal(t, input, output)
|
||||
|
||||
output = testCredentials{APIToken: "refresh"}
|
||||
require.NoError(t, output.FromJSON(secretJSON))
|
||||
fmt.Printf("json out %#v \n", output)
|
||||
log.Infof("json out %#v \n", output)
|
||||
require.True(t, input.IsSame(&output), "json out not same")
|
||||
|
||||
/*
|
||||
// Simple Fscanf not working!
|
||||
output = testCredentials{APIToken: "refresh"}
|
||||
require.NoError(t, output.UnmarshalFmt(secretFmt))
|
||||
fmt.Printf("fmt out %#v \n", output)
|
||||
log.Infof("fmt out %#v \n", output)
|
||||
require.True(t, input.IsSame(&output), "fmt out not same")
|
||||
*/
|
||||
}
|
||||
@ -285,13 +285,13 @@ func TestMarshal(t *testing.T) {
|
||||
IsHidden: true,
|
||||
IsCombinedAddressMode: false,
|
||||
}
|
||||
fmt.Printf("input %#v\n", input)
|
||||
log.Infof("input %#v\n", input)
|
||||
|
||||
secret := input.Marshal()
|
||||
fmt.Printf("secret %#v %d\n", secret, len(secret))
|
||||
log.Infof("secret %#v %d\n", secret, len(secret))
|
||||
|
||||
output := Credentials{APIToken: "refresh"}
|
||||
require.NoError(t, output.Unmarshal(secret))
|
||||
fmt.Printf("output %#v\n", output)
|
||||
log.Infof("output %#v\n", output)
|
||||
assert.Equal(t, input, output)
|
||||
}
|
||||
|
||||
@ -23,6 +23,7 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/events"
|
||||
imapcache "github.com/ProtonMail/proton-bridge/internal/imap/cache"
|
||||
"github.com/ProtonMail/proton-bridge/internal/metrics"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/listener"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
@ -404,6 +405,10 @@ func (u *Users) ClearData() error {
|
||||
result = multierror.Append(result, err)
|
||||
}
|
||||
|
||||
// Need to clear imap cache otherwise fetch response will be remembered
|
||||
// from previous test
|
||||
imapcache.Clear()
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
@ -27,6 +27,7 @@ import (
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/sum"
|
||||
tests "github.com/ProtonMail/proton-bridge/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@ -68,7 +69,7 @@ func TestVerifyWithBadFile(t *testing.T) {
|
||||
filepath.Join("sub", "f5.tgz"),
|
||||
)
|
||||
|
||||
badKeyRing := makeKeyRing(t)
|
||||
badKeyRing := tests.MakeKeyRing(t)
|
||||
signFile(t, filepath.Join(tempDir, "f3.bad"), badKeyRing)
|
||||
|
||||
assert.Error(t, version.VerifyFiles(kr))
|
||||
@ -91,14 +92,14 @@ func TestVerifyWithBadSubFile(t *testing.T) {
|
||||
filepath.Join("sub", "f5.bad"),
|
||||
)
|
||||
|
||||
badKeyRing := makeKeyRing(t)
|
||||
badKeyRing := tests.MakeKeyRing(t)
|
||||
signFile(t, filepath.Join(tempDir, "sub", "f5.bad"), badKeyRing)
|
||||
|
||||
assert.Error(t, version.VerifyFiles(kr))
|
||||
}
|
||||
|
||||
func createSignedFiles(t *testing.T, root string, paths ...string) *crypto.KeyRing {
|
||||
kr := makeKeyRing(t)
|
||||
kr := tests.MakeKeyRing(t)
|
||||
|
||||
for _, path := range paths {
|
||||
makeFile(t, filepath.Join(root, path))
|
||||
@ -118,16 +119,6 @@ func createSignedFiles(t *testing.T, root string, paths ...string) *crypto.KeyRi
|
||||
return kr
|
||||
}
|
||||
|
||||
func makeKeyRing(t *testing.T) *crypto.KeyRing {
|
||||
key, err := crypto.GenerateKey("name", "email", "rsa", 2048)
|
||||
require.NoError(t, err)
|
||||
|
||||
kr, err := crypto.NewKeyRing(key)
|
||||
require.NoError(t, err)
|
||||
|
||||
return kr
|
||||
}
|
||||
|
||||
func makeFile(t *testing.T, path string) {
|
||||
require.NoError(t, os.MkdirAll(filepath.Dir(path), 0700))
|
||||
|
||||
|
||||
@ -19,7 +19,7 @@ package algo
|
||||
|
||||
import "reflect"
|
||||
|
||||
// SetIntersection complexity: O(n^2), could be better but this is simple enough
|
||||
// SetIntersection complexity: O(n^2), could be better but this is simple enough.
|
||||
func SetIntersection(a, b interface{}, eq func(a, b interface{}) bool) []interface{} {
|
||||
set := make([]interface{}, 0)
|
||||
av := reflect.ValueOf(a)
|
||||
|
||||
@ -86,11 +86,8 @@ func (h *macOSHelper) Delete(secretURL string) error {
|
||||
}
|
||||
|
||||
query := newQuery(hostURL, userID)
|
||||
if err := keychain.DeleteItem(query); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return keychain.DeleteItem(query)
|
||||
}
|
||||
|
||||
func (h *macOSHelper) Get(secretURL string) (string, string, error) {
|
||||
|
||||
@ -1,75 +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 message
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/quotedprintable"
|
||||
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
"github.com/emersion/go-textwrapper"
|
||||
openpgperrors "golang.org/x/crypto/openpgp/errors"
|
||||
)
|
||||
|
||||
func WriteBody(w io.Writer, kr *crypto.KeyRing, m *pmapi.Message) error {
|
||||
// Decrypt body.
|
||||
if err := m.Decrypt(kr); err != nil && err != openpgperrors.ErrSignatureExpired {
|
||||
return err
|
||||
}
|
||||
if m.MIMEType != pmapi.ContentTypeMultipartMixed {
|
||||
// Encode it.
|
||||
qp := quotedprintable.NewWriter(w)
|
||||
if _, err := io.WriteString(qp, m.Body); err != nil {
|
||||
return err
|
||||
}
|
||||
return qp.Close()
|
||||
}
|
||||
_, err := io.WriteString(w, m.Body)
|
||||
return err
|
||||
}
|
||||
|
||||
func WriteAttachmentBody(w io.Writer, kr *crypto.KeyRing, m *pmapi.Message, att *pmapi.Attachment, r io.Reader) (err error) {
|
||||
// Decrypt it
|
||||
var dr io.Reader
|
||||
dr, err = att.Decrypt(r, kr)
|
||||
if err == openpgperrors.ErrKeyIncorrect {
|
||||
// Do not fail if attachment is encrypted with a different key.
|
||||
dr = r
|
||||
err = nil
|
||||
att.Name += ".gpg"
|
||||
att.MIMEType = "application/pgp-encrypted" //nolint
|
||||
} else if err != nil && err != openpgperrors.ErrSignatureExpired {
|
||||
err = fmt.Errorf("cannot decrypt attachment: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Encode it.
|
||||
ww := textwrapper.NewRFC822(w)
|
||||
bw := base64.NewEncoder(base64.StdEncoding, ww)
|
||||
|
||||
var n int64
|
||||
if n, err = io.Copy(bw, dr); err != nil {
|
||||
err = fmt.Errorf("cannot write attachment: %v (wrote %v bytes)", err, n)
|
||||
}
|
||||
|
||||
_ = bw.Close()
|
||||
return
|
||||
}
|
||||
@ -18,331 +18,141 @@
|
||||
package message
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"context"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"mime/multipart"
|
||||
"mime/quotedprintable"
|
||||
"net/textproto"
|
||||
"sync"
|
||||
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
"github.com/emersion/go-textwrapper"
|
||||
openpgperrors "golang.org/x/crypto/openpgp/errors"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrDecryptionFailed = errors.New("message could not be decrypted")
|
||||
ErrNoSuchKeyRing = errors.New("the keyring to decrypt this message could not be found")
|
||||
)
|
||||
|
||||
// Builder for converting PM message to RFC822. Builder will directly write
|
||||
// changes to message when fetching or building message.
|
||||
type Builder struct {
|
||||
cl pmapi.Client
|
||||
msg *pmapi.Message
|
||||
|
||||
EncryptedToHTML bool
|
||||
successfullyDecrypted bool
|
||||
reqs chan fetchReq
|
||||
done chan struct{}
|
||||
jobs map[string]*BuildJob
|
||||
locker sync.Mutex
|
||||
}
|
||||
|
||||
// NewBuilder initiated with client and message meta info.
|
||||
func NewBuilder(client pmapi.Client, message *pmapi.Message) *Builder {
|
||||
return &Builder{cl: client, msg: message, EncryptedToHTML: true, successfullyDecrypted: false}
|
||||
type Fetcher interface {
|
||||
GetMessage(string) (*pmapi.Message, error)
|
||||
GetAttachment(string) (io.ReadCloser, error)
|
||||
KeyRingForAddressID(string) (*crypto.KeyRing, error)
|
||||
}
|
||||
|
||||
// fetchMessage will update original PM message if successful
|
||||
func (bld *Builder) fetchMessage() (err error) {
|
||||
if bld.msg.Body != "" {
|
||||
return nil
|
||||
}
|
||||
// NewBuilder creates a new builder which manages the given number of fetch/attach/build workers.
|
||||
// - fetchWorkers: the number of workers which fetch messages from API
|
||||
// - attachWorkers: the number of workers which fetch attachments from API.
|
||||
// - buildWorkers: the number of workers which decrypt/build RFC822 message literals.
|
||||
//
|
||||
// NOTE: Each fetch worker spawns a unique set of attachment workers!
|
||||
// There can therefore be up to fetchWorkers*attachWorkers simultaneous API connections.
|
||||
//
|
||||
// The returned builder is ready to handle jobs -- see (*Builder).NewJob for more information.
|
||||
//
|
||||
// Call (*Builder).Done to shut down the builder and stop all workers.
|
||||
func NewBuilder(fetchWorkers, attachWorkers, buildWorkers int) *Builder {
|
||||
b := newBuilder()
|
||||
|
||||
complete, err := bld.cl.GetMessage(bld.msg.ID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
fetchReqCh, fetchResCh := startFetchWorkers(fetchWorkers, attachWorkers)
|
||||
buildReqCh, buildResCh := startBuildWorkers(buildWorkers)
|
||||
|
||||
*bld.msg = *complete
|
||||
go func() {
|
||||
defer close(fetchReqCh)
|
||||
|
||||
return
|
||||
}
|
||||
for {
|
||||
select {
|
||||
case req := <-b.reqs:
|
||||
fetchReqCh <- req
|
||||
|
||||
func (bld *Builder) writeMessageBody(w io.Writer) error {
|
||||
if err := bld.fetchMessage(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err := bld.WriteBody(w)
|
||||
if err != nil {
|
||||
_, _ = io.WriteString(w, "\r\n")
|
||||
if bld.EncryptedToHTML {
|
||||
_ = CustomMessage(bld.msg, err, true)
|
||||
}
|
||||
_, err = io.WriteString(w, bld.msg.Body)
|
||||
_, _ = io.WriteString(w, "\r\n")
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (bld *Builder) writeAttachmentBody(w io.Writer, att *pmapi.Attachment) error {
|
||||
// Retrieve encrypted attachment
|
||||
r, err := bld.cl.GetAttachment(att.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer r.Close() //nolint[errcheck]
|
||||
|
||||
if err := bld.WriteAttachmentBody(w, att, r); err != nil {
|
||||
// Returning an error here makes e-mail clients like Thunderbird behave
|
||||
// badly, trying to retrieve the message again and again
|
||||
log.Warnln("Cannot write attachment body:", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bld *Builder) writeRelatedPart(p io.Writer, inlines []*pmapi.Attachment) error {
|
||||
related := multipart.NewWriter(p)
|
||||
|
||||
_ = related.SetBoundary(GetRelatedBoundary(bld.msg))
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
if err := bld.writeMessageBody(buf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Write the body part
|
||||
h := GetBodyHeader(bld.msg)
|
||||
|
||||
var err error
|
||||
if p, err = related.CreatePart(h); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, _ = buf.WriteTo(p)
|
||||
|
||||
for _, inline := range inlines {
|
||||
buf = &bytes.Buffer{}
|
||||
if err = bld.writeAttachmentBody(buf, inline); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
h := GetAttachmentHeader(inline)
|
||||
if p, err = related.CreatePart(h); err != nil {
|
||||
return err
|
||||
}
|
||||
_, _ = buf.WriteTo(p)
|
||||
}
|
||||
|
||||
_ = related.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
// BuildMessage converts PM message to body structure (not RFC3501) and bytes
|
||||
// of RC822 message. If successful the original PM message will contain decrypted body.
|
||||
func (bld *Builder) BuildMessage() (structure *BodyStructure, message []byte, err error) { //nolint[funlen]
|
||||
if err = bld.fetchMessage(); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
bodyBuf := &bytes.Buffer{}
|
||||
|
||||
mainHeader := GetHeader(bld.msg)
|
||||
mainHeader.Set("Content-Type", "multipart/mixed; boundary="+GetBoundary(bld.msg))
|
||||
if err = WriteHeader(bodyBuf, mainHeader); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
_, _ = io.WriteString(bodyBuf, "\r\n")
|
||||
|
||||
// NOTE: Do we really need extra encapsulation? i.e. Bridge-IMAP message is always multipart/mixed
|
||||
|
||||
if bld.msg.MIMEType == pmapi.ContentTypeMultipartMixed {
|
||||
_, _ = io.WriteString(bodyBuf, "\r\n--"+GetBoundary(bld.msg)+"\r\n")
|
||||
if err = bld.writeMessageBody(bodyBuf); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
_, _ = io.WriteString(bodyBuf, "\r\n--"+GetBoundary(bld.msg)+"--\r\n")
|
||||
} else {
|
||||
mw := multipart.NewWriter(bodyBuf)
|
||||
_ = mw.SetBoundary(GetBoundary(bld.msg))
|
||||
|
||||
var partWriter io.Writer
|
||||
atts, inlines := SeparateInlineAttachments(bld.msg)
|
||||
|
||||
if len(inlines) > 0 {
|
||||
relatedHeader := GetRelatedHeader(bld.msg)
|
||||
if partWriter, err = mw.CreatePart(relatedHeader); err != nil {
|
||||
return nil, nil, err
|
||||
case <-b.done:
|
||||
return
|
||||
}
|
||||
_ = bld.writeRelatedPart(partWriter, inlines)
|
||||
} else {
|
||||
buf := &bytes.Buffer{}
|
||||
if err = bld.writeMessageBody(buf); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Write the body part
|
||||
bodyHeader := GetBodyHeader(bld.msg)
|
||||
if partWriter, err = mw.CreatePart(bodyHeader); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
_, _ = buf.WriteTo(partWriter)
|
||||
}
|
||||
}()
|
||||
|
||||
// Write the attachments parts
|
||||
for _, att := range atts {
|
||||
buf := &bytes.Buffer{}
|
||||
if err = bld.writeAttachmentBody(buf, att); err != nil {
|
||||
return nil, nil, err
|
||||
go func() {
|
||||
defer close(buildReqCh)
|
||||
|
||||
for res := range fetchResCh {
|
||||
if res.err != nil {
|
||||
b.jobFailure(res.messageID, res.err)
|
||||
} else {
|
||||
buildReqCh <- res
|
||||
}
|
||||
|
||||
attachmentHeader := GetAttachmentHeader(att)
|
||||
if partWriter, err = mw.CreatePart(attachmentHeader); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
_, _ = buf.WriteTo(partWriter)
|
||||
}
|
||||
}()
|
||||
|
||||
_ = mw.Close()
|
||||
}
|
||||
go func() {
|
||||
for res := range buildResCh {
|
||||
if res.err != nil {
|
||||
b.jobFailure(res.messageID, res.err)
|
||||
} else {
|
||||
b.jobSuccess(res.messageID, res.literal)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// wee need to copy buffer before building body structure
|
||||
message = bodyBuf.Bytes()
|
||||
structure, err = NewBodyStructure(bodyBuf)
|
||||
return structure, message, err
|
||||
return b
|
||||
}
|
||||
|
||||
// SuccessfullyDecrypted is true when message was fetched and decrypted successfully
|
||||
func (bld *Builder) SuccessfullyDecrypted() bool { return bld.successfullyDecrypted }
|
||||
|
||||
// WriteBody decrypts PM message and writes main body section. The external PGP
|
||||
// message is written as is (including attachments)
|
||||
func (bld *Builder) WriteBody(w io.Writer) error {
|
||||
kr, err := bld.cl.KeyRingForAddressID(bld.msg.AddressID)
|
||||
if err != nil {
|
||||
return err
|
||||
func newBuilder() *Builder {
|
||||
return &Builder{
|
||||
reqs: make(chan fetchReq),
|
||||
done: make(chan struct{}),
|
||||
jobs: make(map[string]*BuildJob),
|
||||
}
|
||||
// decrypt body
|
||||
if err := bld.msg.Decrypt(kr); err != nil && err != openpgperrors.ErrSignatureExpired {
|
||||
return err
|
||||
}
|
||||
bld.successfullyDecrypted = true
|
||||
if bld.msg.MIMEType != pmapi.ContentTypeMultipartMixed {
|
||||
// transfer encoding
|
||||
qp := quotedprintable.NewWriter(w)
|
||||
if _, err := io.WriteString(qp, bld.msg.Body); err != nil {
|
||||
return err
|
||||
}
|
||||
return qp.Close()
|
||||
}
|
||||
_, err = io.WriteString(w, bld.msg.Body)
|
||||
return err
|
||||
}
|
||||
|
||||
// WriteAttachmentBody decrypts and writes the attachments
|
||||
func (bld *Builder) WriteAttachmentBody(w io.Writer, att *pmapi.Attachment, attReader io.Reader) (err error) {
|
||||
kr, err := bld.cl.KeyRingForAddressID(bld.msg.AddressID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Decrypt it
|
||||
var dr io.Reader
|
||||
dr, err = att.Decrypt(attReader, kr)
|
||||
if err == openpgperrors.ErrKeyIncorrect {
|
||||
// Do not fail if attachment is encrypted with a different key
|
||||
dr = attReader
|
||||
err = nil
|
||||
att.Name += ".gpg"
|
||||
att.MIMEType = "application/pgp-encrypted"
|
||||
} else if err != nil && err != openpgperrors.ErrSignatureExpired {
|
||||
err = fmt.Errorf("cannot decrypt attachment: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// transfer encoding
|
||||
ww := textwrapper.NewRFC822(w)
|
||||
bw := base64.NewEncoder(base64.StdEncoding, ww)
|
||||
|
||||
var n int64
|
||||
if n, err = io.Copy(bw, dr); err != nil {
|
||||
err = fmt.Errorf("cannot write attachment: %v (wrote %v bytes)", err, n)
|
||||
}
|
||||
|
||||
_ = bw.Close()
|
||||
return err
|
||||
// NewJob tells the builder to begin building the message with the given ID.
|
||||
// The result (or any error which occurred during building) can be retrieved from the returned job when available.
|
||||
func (b *Builder) NewJob(ctx context.Context, api Fetcher, messageID string) *BuildJob {
|
||||
return b.NewJobWithOptions(ctx, api, messageID, JobOptions{})
|
||||
}
|
||||
|
||||
func BuildEncrypted(m *pmapi.Message, readers []io.Reader, kr *crypto.KeyRing) ([]byte, error) { //nolint[funlen]
|
||||
b := &bytes.Buffer{}
|
||||
// NewJobWithOptions creates a new job with custom options. See NewJob for more information.
|
||||
func (b *Builder) NewJobWithOptions(ctx context.Context, api Fetcher, messageID string, opts JobOptions) *BuildJob {
|
||||
b.locker.Lock()
|
||||
defer b.locker.Unlock()
|
||||
|
||||
// Overwrite content for main header for import.
|
||||
// Even if message has just simple body we should upload as multipart/mixed.
|
||||
// Each part has encrypted body and header reflects the original header.
|
||||
mainHeader := GetHeader(m)
|
||||
mainHeader.Set("Content-Type", "multipart/mixed; boundary="+GetBoundary(m))
|
||||
mainHeader.Del("Content-Disposition")
|
||||
mainHeader.Del("Content-Transfer-Encoding")
|
||||
if err := WriteHeader(b, mainHeader); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mw := multipart.NewWriter(b)
|
||||
if err := mw.SetBoundary(GetBoundary(m)); err != nil {
|
||||
return nil, err
|
||||
if job, ok := b.jobs[messageID]; ok {
|
||||
return job
|
||||
}
|
||||
|
||||
// Write the body part.
|
||||
bodyHeader := make(textproto.MIMEHeader)
|
||||
bodyHeader.Set("Content-Type", m.MIMEType+"; charset=utf-8")
|
||||
bodyHeader.Set("Content-Disposition", "inline")
|
||||
bodyHeader.Set("Content-Transfer-Encoding", "7bit")
|
||||
b.jobs[messageID] = newBuildJob(messageID)
|
||||
|
||||
p, err := mw.CreatePart(bodyHeader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// First, encrypt the message body.
|
||||
if err := m.Encrypt(kr, kr); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := io.WriteString(p, m.Body); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
go func() { b.reqs <- fetchReq{ctx: ctx, api: api, messageID: messageID, opts: opts} }()
|
||||
|
||||
// Write the attachments parts.
|
||||
for i := 0; i < len(m.Attachments); i++ {
|
||||
att := m.Attachments[i]
|
||||
r := readers[i]
|
||||
h := GetAttachmentHeader(att)
|
||||
p, err := mw.CreatePart(h)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Create line wrapper writer.
|
||||
ww := textwrapper.NewRFC822(p)
|
||||
|
||||
// Create base64 writer.
|
||||
bw := base64.NewEncoder(base64.StdEncoding, ww)
|
||||
|
||||
data, err := ioutil.ReadAll(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create encrypted writer.
|
||||
pgpMessage, err := kr.Encrypt(crypto.NewPlainMessage(data), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := bw.Write(pgpMessage.GetBinary()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := bw.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if err := mw.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return b.Bytes(), nil
|
||||
return b.jobs[messageID]
|
||||
}
|
||||
|
||||
// Done shuts down the builder and stops all workers.
|
||||
func (b *Builder) Done() {
|
||||
b.locker.Lock()
|
||||
defer b.locker.Unlock()
|
||||
|
||||
close(b.done)
|
||||
}
|
||||
|
||||
func (b *Builder) jobSuccess(messageID string, literal []byte) {
|
||||
b.locker.Lock()
|
||||
defer b.locker.Unlock()
|
||||
|
||||
b.jobs[messageID].postSuccess(literal)
|
||||
|
||||
delete(b.jobs, messageID)
|
||||
}
|
||||
|
||||
func (b *Builder) jobFailure(messageID string, err error) {
|
||||
b.locker.Lock()
|
||||
defer b.locker.Unlock()
|
||||
|
||||
b.jobs[messageID].postFailure(err)
|
||||
|
||||
delete(b.jobs, messageID)
|
||||
}
|
||||
|
||||
@ -18,39 +18,26 @@
|
||||
package message
|
||||
|
||||
import (
|
||||
"net/mail"
|
||||
"strings"
|
||||
|
||||
"github.com/emersion/go-imap"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
)
|
||||
|
||||
func getAddresses(addrs []*mail.Address) (imapAddrs []*imap.Address) {
|
||||
for _, a := range addrs {
|
||||
if a == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.SplitN(a.Address, "@", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
imapAddrs = append(imapAddrs, &imap.Address{
|
||||
PersonalName: a.Name,
|
||||
MailboxName: parts[0],
|
||||
HostName: parts[1],
|
||||
})
|
||||
}
|
||||
|
||||
return
|
||||
type boundary struct {
|
||||
val string
|
||||
}
|
||||
|
||||
func formatAddressList(addrs []*mail.Address) (s string) {
|
||||
for i, addr := range addrs {
|
||||
if i > 0 {
|
||||
s += ", "
|
||||
}
|
||||
s += addr.String()
|
||||
}
|
||||
return
|
||||
func newBoundary(seed string) *boundary {
|
||||
return &boundary{val: seed}
|
||||
}
|
||||
|
||||
func (bw *boundary) gen() string {
|
||||
hash := sha256.New()
|
||||
|
||||
if _, err := hash.Write([]byte(bw.val)); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
bw.val = hex.EncodeToString(hash.Sum(nil))
|
||||
|
||||
return bw.val
|
||||
}
|
||||
89
pkg/message/build_build.go
Normal file
89
pkg/message/build_build.go
Normal file
@ -0,0 +1,89 @@
|
||||
// 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 message
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type buildRes struct {
|
||||
messageID string
|
||||
literal []byte
|
||||
err error
|
||||
}
|
||||
|
||||
func newBuildResSuccess(messageID string, literal []byte) buildRes {
|
||||
return buildRes{
|
||||
messageID: messageID,
|
||||
literal: literal,
|
||||
}
|
||||
}
|
||||
|
||||
func newBuildResFailure(messageID string, err error) buildRes {
|
||||
return buildRes{
|
||||
messageID: messageID,
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
// startBuildWorkers starts the given number of build workers.
|
||||
// These workers decrypt and build messages into RFC822 literals.
|
||||
// Two channels are returned:
|
||||
// - buildReqCh: used to send work items to the worker pool
|
||||
// - buildResCh: used to receive work results from the worker pool
|
||||
func startBuildWorkers(buildWorkers int) (chan fetchRes, chan buildRes) {
|
||||
buildReqCh := make(chan fetchRes)
|
||||
buildResCh := make(chan buildRes)
|
||||
|
||||
go func() {
|
||||
defer close(buildResCh)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
wg.Add(buildWorkers)
|
||||
|
||||
for workerID := 0; workerID < buildWorkers; workerID++ {
|
||||
go buildWorker(buildReqCh, buildResCh, &wg)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}()
|
||||
|
||||
return buildReqCh, buildResCh
|
||||
}
|
||||
|
||||
func buildWorker(buildReqCh <-chan fetchRes, buildResCh chan<- buildRes, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
|
||||
for req := range buildReqCh {
|
||||
l := log.
|
||||
WithField("addrID", req.msg.AddressID).
|
||||
WithField("msgID", req.msg.ID)
|
||||
if kr, err := req.api.KeyRingForAddressID(req.msg.AddressID); err != nil {
|
||||
l.WithError(err).Warn("Cannot find keyring for address")
|
||||
buildResCh <- newBuildResFailure(req.msg.ID, errors.Wrap(ErrNoSuchKeyRing, err.Error()))
|
||||
} else if literal, err := buildRFC822(kr, req.msg, req.atts, req.opts); err != nil {
|
||||
l.WithError(err).Warn("Build failed")
|
||||
buildResCh <- newBuildResFailure(req.msg.ID, err)
|
||||
} else {
|
||||
buildResCh <- newBuildResSuccess(req.msg.ID, literal)
|
||||
}
|
||||
}
|
||||
}
|
||||
114
pkg/message/build_encrypted.go
Normal file
114
pkg/message/build_encrypted.go
Normal file
@ -0,0 +1,114 @@
|
||||
// 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 message
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/textproto"
|
||||
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
"github.com/emersion/go-textwrapper"
|
||||
)
|
||||
|
||||
func BuildEncrypted(m *pmapi.Message, readers []io.Reader, kr *crypto.KeyRing) ([]byte, error) { //nolint[funlen]
|
||||
b := &bytes.Buffer{}
|
||||
|
||||
// Overwrite content for main header for import.
|
||||
// Even if message has just simple body we should upload as multipart/mixed.
|
||||
// Each part has encrypted body and header reflects the original header.
|
||||
mainHeader := GetHeader(m)
|
||||
mainHeader.Set("Content-Type", "multipart/mixed; boundary="+GetBoundary(m))
|
||||
mainHeader.Del("Content-Disposition")
|
||||
mainHeader.Del("Content-Transfer-Encoding")
|
||||
if err := WriteHeader(b, mainHeader); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mw := multipart.NewWriter(b)
|
||||
if err := mw.SetBoundary(GetBoundary(m)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Write the body part.
|
||||
bodyHeader := make(textproto.MIMEHeader)
|
||||
bodyHeader.Set("Content-Type", m.MIMEType+"; charset=utf-8")
|
||||
bodyHeader.Set("Content-Disposition", pmapi.DispositionInline)
|
||||
bodyHeader.Set("Content-Transfer-Encoding", "7bit")
|
||||
|
||||
p, err := mw.CreatePart(bodyHeader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// First, encrypt the message body.
|
||||
if err := m.Encrypt(kr, kr); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := io.WriteString(p, m.Body); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Write the attachments parts.
|
||||
for i := 0; i < len(m.Attachments); i++ {
|
||||
att := m.Attachments[i]
|
||||
r := readers[i]
|
||||
h := GetAttachmentHeader(att, false)
|
||||
p, err := mw.CreatePart(h)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data, err := ioutil.ReadAll(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create encrypted writer.
|
||||
pgpMessage, err := kr.Encrypt(crypto.NewPlainMessage(data), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ww := textwrapper.NewRFC822(p)
|
||||
bw := base64.NewEncoder(base64.StdEncoding, ww)
|
||||
if _, err := bw.Write(pgpMessage.GetBinary()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := bw.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if err := mw.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return b.Bytes(), nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
141
pkg/message/build_fetch.go
Normal file
141
pkg/message/build_fetch.go
Normal file
@ -0,0 +1,141 @@
|
||||
// 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 message
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"sync"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/pkg/parallel"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
)
|
||||
|
||||
type fetchReq struct {
|
||||
ctx context.Context
|
||||
api Fetcher
|
||||
messageID string
|
||||
opts JobOptions
|
||||
}
|
||||
|
||||
type fetchRes struct {
|
||||
fetchReq
|
||||
|
||||
msg *pmapi.Message
|
||||
atts [][]byte
|
||||
err error
|
||||
}
|
||||
|
||||
func newFetchResSuccess(req fetchReq, msg *pmapi.Message, atts [][]byte) fetchRes {
|
||||
return fetchRes{
|
||||
fetchReq: req,
|
||||
msg: msg,
|
||||
atts: atts,
|
||||
}
|
||||
}
|
||||
|
||||
func newFetchResFailure(req fetchReq, err error) fetchRes {
|
||||
return fetchRes{
|
||||
fetchReq: req,
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
// startFetchWorkers starts the given number of fetch workers.
|
||||
// These workers download message and attachment data from API.
|
||||
// Each fetch worker will use up to the given number of attachment workers to download attachments.
|
||||
// Two channels are returned:
|
||||
// - fetchReqCh: used to send work items to the worker pool
|
||||
// - fetchResCh: used to receive work results from the worker pool
|
||||
func startFetchWorkers(fetchWorkers, attachWorkers int) (chan fetchReq, chan fetchRes) {
|
||||
fetchReqCh := make(chan fetchReq)
|
||||
fetchResCh := make(chan fetchRes)
|
||||
|
||||
go func() {
|
||||
defer close(fetchResCh)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
wg.Add(fetchWorkers)
|
||||
|
||||
for workerID := 0; workerID < fetchWorkers; workerID++ {
|
||||
go fetchWorker(fetchReqCh, fetchResCh, attachWorkers, &wg)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}()
|
||||
|
||||
return fetchReqCh, fetchResCh
|
||||
}
|
||||
|
||||
func fetchWorker(fetchReqCh <-chan fetchReq, fetchResCh chan<- fetchRes, attachWorkers int, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
|
||||
for req := range fetchReqCh {
|
||||
msg, atts, err := fetchMessage(req, attachWorkers)
|
||||
if err != nil {
|
||||
fetchResCh <- newFetchResFailure(req, err)
|
||||
} else {
|
||||
fetchResCh <- newFetchResSuccess(req, msg, atts)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fetchMessage(req fetchReq, attachWorkers int) (*pmapi.Message, [][]byte, error) {
|
||||
msg, err := req.api.GetMessage(req.messageID)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
attList := make([]interface{}, len(msg.Attachments))
|
||||
|
||||
for i, att := range msg.Attachments {
|
||||
attList[i] = att.ID
|
||||
}
|
||||
|
||||
process := func(value interface{}) (interface{}, error) {
|
||||
rc, err := req.api.GetAttachment(value.(string))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
b, err := ioutil.ReadAll(rc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := rc.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
attData := make([][]byte, len(msg.Attachments))
|
||||
|
||||
collect := func(idx int, value interface{}) error {
|
||||
attData[idx] = value.([]byte) //nolint[forcetypeassert] we wan't to panic here
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := parallel.RunParallel(attachWorkers, attList, process, collect); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return msg, attData, nil
|
||||
}
|
||||
332
pkg/message/build_framework_test.go
Normal file
332
pkg/message/build_framework_test.go
Normal file
@ -0,0 +1,332 @@
|
||||
// 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 message
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/message/mocks"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/message/parser"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func newTestFetcher(
|
||||
m *gomock.Controller,
|
||||
kr *crypto.KeyRing,
|
||||
msg *pmapi.Message,
|
||||
attData ...[]byte,
|
||||
) Fetcher {
|
||||
f := mocks.NewMockFetcher(m)
|
||||
|
||||
f.EXPECT().GetMessage(msg.ID).Return(msg, nil)
|
||||
|
||||
for i, att := range msg.Attachments {
|
||||
f.EXPECT().GetAttachment(att.ID).Return(newTestReadCloser(attData[i]), nil)
|
||||
}
|
||||
|
||||
f.EXPECT().KeyRingForAddressID(msg.AddressID).Return(kr, nil)
|
||||
|
||||
return f
|
||||
}
|
||||
|
||||
func newTestMessage(
|
||||
t *testing.T,
|
||||
kr *crypto.KeyRing,
|
||||
messageID, addressID, mimeType, body string, // nolint[unparam]
|
||||
date time.Time,
|
||||
) *pmapi.Message {
|
||||
enc, err := kr.Encrypt(crypto.NewPlainMessageFromString(body), kr)
|
||||
require.NoError(t, err)
|
||||
|
||||
arm, err := enc.GetArmored()
|
||||
require.NoError(t, err)
|
||||
|
||||
return &pmapi.Message{
|
||||
ID: messageID,
|
||||
AddressID: addressID,
|
||||
MIMEType: mimeType,
|
||||
Header: map[string][]string{
|
||||
"Content-Type": {mimeType},
|
||||
"Date": {date.In(time.UTC).Format(time.RFC1123Z)},
|
||||
},
|
||||
Body: arm,
|
||||
Time: date.Unix(),
|
||||
}
|
||||
}
|
||||
|
||||
func addTestAttachment(
|
||||
t *testing.T,
|
||||
kr *crypto.KeyRing,
|
||||
msg *pmapi.Message,
|
||||
attachmentID, name, mimeType, disposition, data string,
|
||||
) []byte {
|
||||
enc, err := kr.EncryptAttachment(crypto.NewPlainMessageFromString(data), attachmentID+".bin")
|
||||
require.NoError(t, err)
|
||||
|
||||
msg.Attachments = append(msg.Attachments, &pmapi.Attachment{
|
||||
ID: attachmentID,
|
||||
Name: name,
|
||||
MIMEType: mimeType,
|
||||
Header: map[string][]string{
|
||||
"Content-Type": {mimeType},
|
||||
"Content-Disposition": {disposition},
|
||||
"Content-Transfer-Encoding": {"base64"},
|
||||
},
|
||||
Disposition: disposition,
|
||||
KeyPackets: base64.StdEncoding.EncodeToString(enc.GetBinaryKeyPacket()),
|
||||
})
|
||||
|
||||
return enc.GetBinaryDataPacket()
|
||||
}
|
||||
|
||||
type testReadCloser struct {
|
||||
io.Reader
|
||||
}
|
||||
|
||||
func newTestReadCloser(b []byte) *testReadCloser {
|
||||
return &testReadCloser{Reader: bytes.NewReader(b)}
|
||||
}
|
||||
|
||||
func (testReadCloser) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type testSection struct {
|
||||
t *testing.T
|
||||
part *parser.Part
|
||||
raw []byte
|
||||
}
|
||||
|
||||
// NOTE: Each section is parsed individually --> cleaner test code but slower... improve this one day?
|
||||
func section(t *testing.T, b []byte, section ...int) *testSection {
|
||||
p, err := parser.New(bytes.NewReader(b))
|
||||
assert.NoError(t, err)
|
||||
|
||||
part, err := p.Section(section)
|
||||
require.NoError(t, err)
|
||||
|
||||
bs, err := NewBodyStructure(bytes.NewReader(b))
|
||||
require.NoError(t, err)
|
||||
|
||||
raw, err := bs.GetSection(bytes.NewReader(b), append([]int{}, section...))
|
||||
require.NoError(t, err)
|
||||
|
||||
return &testSection{
|
||||
t: t,
|
||||
part: part,
|
||||
raw: raw,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *testSection) expectBody(wantBody matcher) *testSection {
|
||||
wantBody.match(s.t, string(s.part.Body))
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *testSection) expectSection(wantSection matcher) *testSection { // nolint[unparam]
|
||||
wantSection.match(s.t, string(s.raw))
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *testSection) expectContentType(wantContentType matcher) *testSection {
|
||||
mimeType, _, err := s.part.Header.ContentType()
|
||||
require.NoError(s.t, err)
|
||||
|
||||
wantContentType.match(s.t, mimeType)
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *testSection) expectContentTypeParam(key string, wantParam matcher) *testSection { // nolint[unparam]
|
||||
_, params, err := s.part.Header.ContentType()
|
||||
require.NoError(s.t, err)
|
||||
|
||||
wantParam.match(s.t, params[key])
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *testSection) expectContentDisposition(wantDisposition matcher) *testSection {
|
||||
disposition, _, err := s.part.Header.ContentDisposition()
|
||||
require.NoError(s.t, err)
|
||||
|
||||
wantDisposition.match(s.t, disposition)
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *testSection) expectContentDispositionParam(key string, wantParam matcher) *testSection { // nolint[unparam]
|
||||
_, params, err := s.part.Header.ContentDisposition()
|
||||
require.NoError(s.t, err)
|
||||
|
||||
wantParam.match(s.t, params[key])
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *testSection) expectTransferEncoding(wantTransferEncoding matcher) *testSection {
|
||||
wantTransferEncoding.match(s.t, s.part.Header.Get("Content-Transfer-Encoding"))
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *testSection) expectDate(wantDate matcher) *testSection {
|
||||
wantDate.match(s.t, s.part.Header.Get("Date"))
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *testSection) expectHeader(key string, wantValue matcher) *testSection {
|
||||
wantValue.match(s.t, s.part.Header.Get(key))
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *testSection) expectDecodedHeader(key string, wantValue matcher) *testSection { // nolint[unparam]
|
||||
dec, err := s.part.Header.Text(key)
|
||||
require.NoError(s.t, err)
|
||||
|
||||
wantValue.match(s.t, dec)
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *testSection) pubKey() *crypto.KeyRing {
|
||||
key, err := crypto.NewKeyFromArmored(string(s.part.Body))
|
||||
require.NoError(s.t, err)
|
||||
|
||||
kr, err := crypto.NewKeyRing(key)
|
||||
require.NoError(s.t, err)
|
||||
|
||||
return kr
|
||||
}
|
||||
|
||||
func (s *testSection) signature() *crypto.PGPSignature {
|
||||
sig, err := crypto.NewPGPSignatureFromArmored(string(s.part.Body))
|
||||
require.NoError(s.t, err)
|
||||
|
||||
return sig
|
||||
}
|
||||
|
||||
type matcher interface {
|
||||
match(*testing.T, string)
|
||||
}
|
||||
|
||||
type isMatcher struct {
|
||||
want string
|
||||
}
|
||||
|
||||
func (matcher isMatcher) match(t *testing.T, have string) {
|
||||
assert.Equal(t, matcher.want, have)
|
||||
}
|
||||
|
||||
func is(want string) isMatcher {
|
||||
return isMatcher{want: want}
|
||||
}
|
||||
|
||||
func isMissing() isMatcher {
|
||||
return isMatcher{}
|
||||
}
|
||||
|
||||
type isNotMatcher struct {
|
||||
notWant string
|
||||
}
|
||||
|
||||
func (matcher isNotMatcher) match(t *testing.T, have string) {
|
||||
assert.NotEqual(t, matcher.notWant, have)
|
||||
}
|
||||
|
||||
func isNot(notWant string) isNotMatcher {
|
||||
return isNotMatcher{notWant: notWant}
|
||||
}
|
||||
|
||||
type containsMatcher struct {
|
||||
contains string
|
||||
}
|
||||
|
||||
func (matcher containsMatcher) match(t *testing.T, have string) {
|
||||
assert.Contains(t, have, matcher.contains)
|
||||
}
|
||||
|
||||
func contains(contains string) containsMatcher {
|
||||
return containsMatcher{contains: contains}
|
||||
}
|
||||
|
||||
type decryptsToMatcher struct {
|
||||
kr *crypto.KeyRing
|
||||
want string
|
||||
}
|
||||
|
||||
func (matcher decryptsToMatcher) match(t *testing.T, have string) {
|
||||
haveMsg, err := crypto.NewPGPMessageFromArmored(have)
|
||||
require.NoError(t, err)
|
||||
|
||||
dec, err := matcher.kr.Decrypt(haveMsg, nil, crypto.GetUnixTime())
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, matcher.want, dec.GetString())
|
||||
}
|
||||
|
||||
func decryptsTo(kr *crypto.KeyRing, want string) decryptsToMatcher {
|
||||
return decryptsToMatcher{kr: kr, want: want}
|
||||
}
|
||||
|
||||
type verifiesAgainstMatcher struct {
|
||||
kr *crypto.KeyRing
|
||||
sig *crypto.PGPSignature
|
||||
}
|
||||
|
||||
func (matcher verifiesAgainstMatcher) match(t *testing.T, have string) {
|
||||
assert.NoError(t, matcher.kr.VerifyDetached(
|
||||
crypto.NewPlainMessage(bytes.TrimSuffix([]byte(have), []byte("\r\n"))),
|
||||
matcher.sig,
|
||||
crypto.GetUnixTime()),
|
||||
)
|
||||
}
|
||||
|
||||
func verifiesAgainst(kr *crypto.KeyRing, sig *crypto.PGPSignature) verifiesAgainstMatcher {
|
||||
return verifiesAgainstMatcher{kr: kr, sig: sig}
|
||||
}
|
||||
|
||||
type maxLineLengthMatcher struct {
|
||||
wantMax int
|
||||
}
|
||||
|
||||
func (matcher maxLineLengthMatcher) match(t *testing.T, have string) {
|
||||
scanner := bufio.NewScanner(strings.NewReader(have))
|
||||
|
||||
for scanner.Scan() {
|
||||
assert.Less(t, len(scanner.Text()), matcher.wantMax)
|
||||
}
|
||||
}
|
||||
|
||||
func hasMaxLineLength(wantMax int) maxLineLengthMatcher {
|
||||
return maxLineLengthMatcher{wantMax: wantMax}
|
||||
}
|
||||
59
pkg/message/build_job.go
Normal file
59
pkg/message/build_job.go
Normal file
@ -0,0 +1,59 @@
|
||||
// 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 message
|
||||
|
||||
type JobOptions struct {
|
||||
IgnoreDecryptionErrors bool // Whether to ignore decryption errors and create a "custom message" instead.
|
||||
SanitizeDate bool // Whether to replace all dates before 1970 with RFC822's birthdate.
|
||||
AddInternalID bool // Whether to include MessageID as X-Pm-Internal-Id.
|
||||
AddExternalID bool // Whether to include ExternalID as X-Pm-External-Id.
|
||||
AddMessageDate bool // Whether to include message time as X-Pm-Date.
|
||||
AddMessageIDReference bool // Whether to include the MessageID in References.
|
||||
}
|
||||
|
||||
type BuildJob struct {
|
||||
messageID string
|
||||
literal []byte
|
||||
err error
|
||||
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
func newBuildJob(messageID string) *BuildJob {
|
||||
return &BuildJob{
|
||||
messageID: messageID,
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// GetResult returns the build result or any error which occurred during building.
|
||||
// If the result is not ready yet, it blocks.
|
||||
func (job *BuildJob) GetResult() ([]byte, error) {
|
||||
<-job.done
|
||||
return job.literal, job.err
|
||||
}
|
||||
|
||||
func (job *BuildJob) postSuccess(literal []byte) {
|
||||
job.literal = literal
|
||||
close(job.done)
|
||||
}
|
||||
|
||||
func (job *BuildJob) postFailure(err error) {
|
||||
job.err = err
|
||||
close(job.done)
|
||||
}
|
||||
434
pkg/message/build_rfc822.go
Normal file
434
pkg/message/build_rfc822.go
Normal file
@ -0,0 +1,434 @@
|
||||
// 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 message
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"io/ioutil"
|
||||
"mime"
|
||||
"net/mail"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/ProtonMail/go-rfc5322"
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
"github.com/emersion/go-message"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func buildRFC822(kr *crypto.KeyRing, msg *pmapi.Message, attData [][]byte, opts JobOptions) ([]byte, error) {
|
||||
switch {
|
||||
case len(msg.Attachments) > 0:
|
||||
return buildMultipartRFC822(kr, msg, attData, opts)
|
||||
|
||||
case msg.MIMEType == "multipart/mixed":
|
||||
return buildEncryptedRFC822(kr, msg, opts)
|
||||
|
||||
default:
|
||||
return buildSimpleRFC822(kr, msg, opts)
|
||||
}
|
||||
}
|
||||
|
||||
func buildSimpleRFC822(kr *crypto.KeyRing, msg *pmapi.Message, opts JobOptions) ([]byte, error) {
|
||||
dec, err := msg.Decrypt(kr)
|
||||
if err != nil {
|
||||
if !opts.IgnoreDecryptionErrors {
|
||||
return nil, errors.Wrap(ErrDecryptionFailed, err.Error())
|
||||
}
|
||||
|
||||
return buildMultipartRFC822(kr, msg, nil, opts)
|
||||
}
|
||||
|
||||
hdr := getTextPartHeader(getMessageHeader(msg, opts), dec, msg.MIMEType)
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
w, err := message.CreateWriter(buf, hdr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err := w.Write(dec); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := w.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func buildMultipartRFC822(
|
||||
kr *crypto.KeyRing,
|
||||
msg *pmapi.Message,
|
||||
attData [][]byte,
|
||||
opts JobOptions,
|
||||
) ([]byte, error) {
|
||||
boundary := newBoundary(msg.ID)
|
||||
|
||||
hdr := getMessageHeader(msg, opts)
|
||||
|
||||
hdr.SetContentType("multipart/mixed", map[string]string{"boundary": boundary.gen()})
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
w, err := message.CreateWriter(buf, hdr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var (
|
||||
inlineAtts []*pmapi.Attachment
|
||||
inlineData [][]byte
|
||||
attachAtts []*pmapi.Attachment
|
||||
attachData [][]byte
|
||||
)
|
||||
|
||||
for i, att := range msg.Attachments {
|
||||
if att.Disposition == pmapi.DispositionInline {
|
||||
inlineAtts = append(inlineAtts, att)
|
||||
inlineData = append(inlineData, attData[i])
|
||||
} else {
|
||||
attachAtts = append(attachAtts, att)
|
||||
attachData = append(attachData, attData[i])
|
||||
}
|
||||
}
|
||||
|
||||
if len(inlineAtts) > 0 {
|
||||
if err := writeRelatedParts(w, kr, boundary, msg, inlineAtts, inlineData, opts); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if err := writeTextPart(w, kr, msg, opts); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i, att := range attachAtts {
|
||||
if err := writeAttachmentPart(w, kr, att, attachData[i], opts); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if err := w.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func writeTextPart(
|
||||
w *message.Writer,
|
||||
kr *crypto.KeyRing,
|
||||
msg *pmapi.Message,
|
||||
opts JobOptions,
|
||||
) error {
|
||||
dec, err := msg.Decrypt(kr)
|
||||
if err != nil {
|
||||
if !opts.IgnoreDecryptionErrors {
|
||||
return errors.Wrap(ErrDecryptionFailed, err.Error())
|
||||
}
|
||||
|
||||
/*
|
||||
if len(msg.Attachments) > 0 {
|
||||
return writeCustomTextPartAsAttachment(w, msg, err)
|
||||
}
|
||||
*/
|
||||
|
||||
return writeCustomTextPart(w, msg, err)
|
||||
}
|
||||
|
||||
part, err := w.CreatePart(getTextPartHeader(message.Header{}, dec, msg.MIMEType))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := part.Write(dec); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return part.Close()
|
||||
}
|
||||
|
||||
func writeAttachmentPart(
|
||||
w *message.Writer,
|
||||
kr *crypto.KeyRing,
|
||||
att *pmapi.Attachment,
|
||||
attData []byte,
|
||||
opts JobOptions,
|
||||
) error {
|
||||
kps, err := base64.StdEncoding.DecodeString(att.KeyPackets)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
msg := crypto.NewPGPSplitMessage(kps, attData).GetPGPMessage()
|
||||
|
||||
dec, err := kr.Decrypt(msg, nil, crypto.GetUnixTime())
|
||||
if err != nil {
|
||||
if !opts.IgnoreDecryptionErrors {
|
||||
return errors.Wrap(ErrDecryptionFailed, err.Error())
|
||||
}
|
||||
|
||||
log.
|
||||
WithField("attID", att.ID).
|
||||
WithField("msgID", att.MessageID).
|
||||
WithError(err).
|
||||
Warn("Attachment decryption failed")
|
||||
|
||||
return writeCustomAttachmentPart(w, att, msg, err)
|
||||
}
|
||||
|
||||
part, err := w.CreatePart(getAttachmentPartHeader(att))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := part.Write(dec.GetBinary()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return part.Close()
|
||||
}
|
||||
|
||||
func writeRelatedParts(
|
||||
w *message.Writer,
|
||||
kr *crypto.KeyRing,
|
||||
boundary *boundary,
|
||||
msg *pmapi.Message,
|
||||
atts []*pmapi.Attachment,
|
||||
attData [][]byte,
|
||||
opts JobOptions,
|
||||
) error {
|
||||
hdr := message.Header{}
|
||||
|
||||
hdr.SetContentType("multipart/related", map[string]string{"boundary": boundary.gen()})
|
||||
|
||||
rel, err := w.CreatePart(hdr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := writeTextPart(rel, kr, msg, opts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i, att := range atts {
|
||||
if err := writeAttachmentPart(rel, kr, att, attData[i], opts); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return rel.Close()
|
||||
}
|
||||
|
||||
func buildEncryptedRFC822(kr *crypto.KeyRing, msg *pmapi.Message, opts JobOptions) ([]byte, error) {
|
||||
hdr := getMessageHeader(msg, opts)
|
||||
|
||||
hdr.SetContentType("multipart/mixed", map[string]string{"boundary": newBoundary(msg.ID).gen()})
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
w, err := message.CreateWriter(buf, hdr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dec, err := msg.Decrypt(kr)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ErrDecryptionFailed, err.Error())
|
||||
}
|
||||
|
||||
ent, err := message.Read(bytes.NewReader(dec))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
part, err := w.CreatePart(ent.Header)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(ent.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err := part.Write(body); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := part.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := w.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func getMessageHeader(msg *pmapi.Message, opts JobOptions) message.Header { // nolint[funlen]
|
||||
hdr := toMessageHeader(msg.Header)
|
||||
|
||||
// SetText will RFC2047-encode.
|
||||
if msg.Subject != "" {
|
||||
hdr.SetText("Subject", msg.Subject)
|
||||
}
|
||||
|
||||
// mail.Address.String() will RFC2047-encode if necessary.
|
||||
if msg.Sender != nil {
|
||||
hdr.Set("From", msg.Sender.String())
|
||||
}
|
||||
|
||||
if len(msg.ReplyTos) > 0 {
|
||||
hdr.Set("Reply-To", toAddressList(msg.ReplyTos))
|
||||
}
|
||||
|
||||
if len(msg.ToList) > 0 {
|
||||
hdr.Set("To", toAddressList(msg.ToList))
|
||||
}
|
||||
|
||||
if len(msg.CCList) > 0 {
|
||||
hdr.Set("Cc", toAddressList(msg.CCList))
|
||||
}
|
||||
|
||||
if len(msg.BCCList) > 0 {
|
||||
hdr.Set("Bcc", toAddressList(msg.BCCList))
|
||||
}
|
||||
|
||||
setMessageIDIfNeeded(msg, &hdr)
|
||||
|
||||
// Sanitize the date; it needs to have a valid unix timestamp.
|
||||
if opts.SanitizeDate {
|
||||
if date, err := rfc5322.ParseDateTime(hdr.Get("Date")); err != nil || date.Before(time.Unix(0, 0)) {
|
||||
msgDate := sanitizeMessageDate(msg.Time)
|
||||
hdr.Set("Date", msgDate.In(time.UTC).Format(time.RFC1123Z))
|
||||
// We clobbered the date so we save it under X-Original-Date.
|
||||
hdr.Set("X-Original-Date", date.In(time.UTC).Format(time.RFC1123Z))
|
||||
}
|
||||
}
|
||||
|
||||
// Set our internal ID if requested.
|
||||
// This is important for us to detect whether APPENDed things are actually "move like outlook".
|
||||
if opts.AddInternalID {
|
||||
hdr.Set("X-Pm-Internal-Id", msg.ID)
|
||||
}
|
||||
|
||||
// Set our external ID if requested.
|
||||
// This was useful during debugging of applemail recovered messages; doesn't help with any behaviour.
|
||||
if opts.AddExternalID {
|
||||
hdr.Set("X-Pm-External-Id", "<"+msg.ExternalID+">")
|
||||
}
|
||||
|
||||
// Set our server date if requested.
|
||||
// Can be useful to see how long it took for a message to arrive.
|
||||
if opts.AddMessageDate {
|
||||
hdr.Set("X-Pm-Date", time.Unix(msg.Time, 0).In(time.UTC).Format(time.RFC1123Z))
|
||||
}
|
||||
|
||||
// Include the message ID in the references (supposedly this somehow improves outlook support...).
|
||||
if opts.AddMessageIDReference {
|
||||
if references := hdr.Get("References"); !strings.Contains(references, msg.ID) {
|
||||
hdr.Set("References", references+" <"+msg.ID+"@"+pmapi.InternalIDDomain+">")
|
||||
}
|
||||
}
|
||||
|
||||
return hdr
|
||||
}
|
||||
|
||||
// sanitizeMessageDate will return time from msgTime timestamp. If timestamp is
|
||||
// not after epoch the RFC822 publish day will be used. No message should
|
||||
// realistically be older than RFC822 itself.
|
||||
func sanitizeMessageDate(msgTime int64) time.Time {
|
||||
if msgTime := time.Unix(msgTime, 0); msgTime.After(time.Unix(0, 0)) {
|
||||
return msgTime
|
||||
}
|
||||
return time.Date(1982, 8, 13, 0, 0, 0, 0, time.UTC)
|
||||
}
|
||||
|
||||
// setMessageIDIfNeeded sets Message-Id from ExternalID or ID if it's not
|
||||
// already set.
|
||||
func setMessageIDIfNeeded(msg *pmapi.Message, hdr *message.Header) {
|
||||
if hdr.Get("Message-Id") == "" {
|
||||
if msg.ExternalID != "" {
|
||||
hdr.Set("Message-Id", "<"+msg.ExternalID+">")
|
||||
} else {
|
||||
hdr.Set("Message-Id", "<"+msg.ID+"@"+pmapi.InternalIDDomain+">")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getTextPartHeader(hdr message.Header, body []byte, mimeType string) message.Header {
|
||||
params := make(map[string]string)
|
||||
|
||||
if utf8.Valid(body) {
|
||||
params["charset"] = "utf-8"
|
||||
}
|
||||
|
||||
hdr.SetContentType(mimeType, params)
|
||||
|
||||
// Use quoted-printable for all text/... parts
|
||||
hdr.Set("Content-Transfer-Encoding", "quoted-printable")
|
||||
|
||||
return hdr
|
||||
}
|
||||
|
||||
func getAttachmentPartHeader(att *pmapi.Attachment) message.Header {
|
||||
hdr := toMessageHeader(mail.Header(att.Header))
|
||||
|
||||
// All attachments have a content type.
|
||||
hdr.SetContentType(att.MIMEType, map[string]string{"name": mime.QEncoding.Encode("utf-8", att.Name)})
|
||||
|
||||
// All attachments have a content disposition.
|
||||
hdr.SetContentDisposition(att.Disposition, map[string]string{"filename": mime.QEncoding.Encode("utf-8", att.Name)})
|
||||
|
||||
// Use base64 for all attachments except embedded RFC822 messages.
|
||||
if att.MIMEType != "message/rfc822" {
|
||||
hdr.Set("Content-Transfer-Encoding", "base64")
|
||||
} else {
|
||||
hdr.Del("Content-Transfer-Encoding")
|
||||
}
|
||||
|
||||
return hdr
|
||||
}
|
||||
|
||||
func toMessageHeader(hdr mail.Header) message.Header {
|
||||
var res message.Header
|
||||
|
||||
for key, val := range hdr {
|
||||
for _, val := range val {
|
||||
res.Add(key, val)
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func toAddressList(addrs []*mail.Address) string {
|
||||
res := make([]string, len(addrs))
|
||||
|
||||
for i, addr := range addrs {
|
||||
res[i] = addr.String()
|
||||
}
|
||||
|
||||
return strings.Join(res, ", ")
|
||||
}
|
||||
97
pkg/message/build_rfc822_custom.go
Normal file
97
pkg/message/build_rfc822_custom.go
Normal file
@ -0,0 +1,97 @@
|
||||
// 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 message
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"mime"
|
||||
|
||||
"github.com/ProtonMail/gopenpgp/v2/constants"
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
"github.com/emersion/go-message"
|
||||
)
|
||||
|
||||
// writeCustomTextPart writes an armored-PGP text part for a message body that couldn't be decrypted.
|
||||
func writeCustomTextPart(
|
||||
w *message.Writer,
|
||||
msg *pmapi.Message,
|
||||
decError error,
|
||||
) error {
|
||||
enc, err := crypto.NewPGPMessageFromArmored(msg.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
arm, err := enc.GetArmoredWithCustomHeaders(
|
||||
fmt.Sprintf("This message could not be decrypted: %v", decError),
|
||||
constants.ArmorHeaderVersion,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var hdr message.Header
|
||||
|
||||
hdr.SetContentType(msg.MIMEType, nil)
|
||||
|
||||
part, err := w.CreatePart(hdr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := part.Write([]byte(arm)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeCustomAttachmentPart writes an armored-PGP data part for an attachment that couldn't be decrypted.
|
||||
func writeCustomAttachmentPart(
|
||||
w *message.Writer,
|
||||
att *pmapi.Attachment,
|
||||
msg *crypto.PGPMessage,
|
||||
decError error,
|
||||
) error {
|
||||
arm, err := msg.GetArmoredWithCustomHeaders(
|
||||
fmt.Sprintf("This attachment could not be decrypted: %v", decError),
|
||||
constants.ArmorHeaderVersion,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
filename := mime.QEncoding.Encode("utf-8", att.Name+".pgp")
|
||||
|
||||
var hdr message.Header
|
||||
|
||||
hdr.SetContentType("application/octet-stream", map[string]string{"name": filename})
|
||||
hdr.SetContentDisposition(att.Disposition, map[string]string{"filename": filename})
|
||||
|
||||
part, err := w.CreatePart(hdr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := part.Write([]byte(arm)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return part.Close()
|
||||
}
|
||||
1239
pkg/message/build_test.go
Normal file
1239
pkg/message/build_test.go
Normal file
File diff suppressed because it is too large
Load Diff
@ -19,30 +19,49 @@ package message
|
||||
|
||||
import (
|
||||
"net/mail"
|
||||
"time"
|
||||
"net/textproto"
|
||||
"strings"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
"github.com/emersion/go-imap"
|
||||
)
|
||||
|
||||
func GetEnvelope(m *pmapi.Message) *imap.Envelope {
|
||||
messageID := m.ExternalID
|
||||
if messageID == "" {
|
||||
messageID = m.Header.Get("Message-Id")
|
||||
} else {
|
||||
messageID = "<" + messageID + ">"
|
||||
}
|
||||
// GetEnvelope will prepare envelope from pmapi message and cached header.
|
||||
func GetEnvelope(msg *pmapi.Message, header textproto.MIMEHeader) *imap.Envelope {
|
||||
hdr := toMessageHeader(mail.Header(header))
|
||||
setMessageIDIfNeeded(msg, &hdr)
|
||||
|
||||
return &imap.Envelope{
|
||||
Date: time.Unix(m.Time, 0),
|
||||
Subject: m.Subject,
|
||||
From: getAddresses([]*mail.Address{m.Sender}),
|
||||
Sender: getAddresses([]*mail.Address{m.Sender}),
|
||||
ReplyTo: getAddresses(m.ReplyTos),
|
||||
To: getAddresses(m.ToList),
|
||||
Cc: getAddresses(m.CCList),
|
||||
Bcc: getAddresses(m.BCCList),
|
||||
InReplyTo: m.Header.Get("In-Reply-To"),
|
||||
MessageId: messageID,
|
||||
Date: sanitizeMessageDate(msg.Time),
|
||||
Subject: msg.Subject,
|
||||
From: getAddresses([]*mail.Address{msg.Sender}),
|
||||
Sender: getAddresses([]*mail.Address{msg.Sender}),
|
||||
ReplyTo: getAddresses(msg.ReplyTos),
|
||||
To: getAddresses(msg.ToList),
|
||||
Cc: getAddresses(msg.CCList),
|
||||
Bcc: getAddresses(msg.BCCList),
|
||||
InReplyTo: hdr.Get("In-Reply-To"),
|
||||
MessageId: hdr.Get("Message-Id"),
|
||||
}
|
||||
}
|
||||
|
||||
func getAddresses(addrs []*mail.Address) (imapAddrs []*imap.Address) {
|
||||
for _, a := range addrs {
|
||||
if a == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.SplitN(a.Address, "@", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
imapAddrs = append(imapAddrs, &imap.Address{
|
||||
PersonalName: a.Name,
|
||||
MailboxName: parts[0],
|
||||
HostName: parts[1],
|
||||
})
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@ -42,16 +42,16 @@ func GetHeader(msg *pmapi.Message) textproto.MIMEHeader { //nolint[funlen]
|
||||
h.Set("From", pmmime.EncodeHeader(msg.Sender.String()))
|
||||
}
|
||||
if len(msg.ReplyTos) > 0 {
|
||||
h.Set("Reply-To", pmmime.EncodeHeader(formatAddressList(msg.ReplyTos)))
|
||||
h.Set("Reply-To", pmmime.EncodeHeader(toAddressList(msg.ReplyTos)))
|
||||
}
|
||||
if len(msg.ToList) > 0 {
|
||||
h.Set("To", pmmime.EncodeHeader(formatAddressList(msg.ToList)))
|
||||
h.Set("To", pmmime.EncodeHeader(toAddressList(msg.ToList)))
|
||||
}
|
||||
if len(msg.CCList) > 0 {
|
||||
h.Set("Cc", pmmime.EncodeHeader(formatAddressList(msg.CCList)))
|
||||
h.Set("Cc", pmmime.EncodeHeader(toAddressList(msg.CCList)))
|
||||
}
|
||||
if len(msg.BCCList) > 0 {
|
||||
h.Set("Bcc", pmmime.EncodeHeader(formatAddressList(msg.BCCList)))
|
||||
h.Set("Bcc", pmmime.EncodeHeader(toAddressList(msg.BCCList)))
|
||||
}
|
||||
|
||||
// Add or rewrite date related fields.
|
||||
@ -91,7 +91,7 @@ func GetHeader(msg *pmapi.Message) textproto.MIMEHeader { //nolint[funlen]
|
||||
|
||||
func SetBodyContentFields(h *textproto.MIMEHeader, m *pmapi.Message) {
|
||||
h.Set("Content-Type", m.MIMEType+"; charset=utf-8")
|
||||
h.Set("Content-Disposition", "inline")
|
||||
h.Set("Content-Disposition", pmapi.DispositionInline)
|
||||
h.Set("Content-Transfer-Encoding", "quoted-printable")
|
||||
}
|
||||
|
||||
@ -101,27 +101,28 @@ func GetBodyHeader(m *pmapi.Message) textproto.MIMEHeader {
|
||||
return h
|
||||
}
|
||||
|
||||
func GetRelatedHeader(m *pmapi.Message) textproto.MIMEHeader {
|
||||
h := make(textproto.MIMEHeader)
|
||||
h.Set("Content-Type", "multipart/related; boundary="+GetRelatedBoundary(m))
|
||||
return h
|
||||
}
|
||||
|
||||
func GetAttachmentHeader(att *pmapi.Attachment) textproto.MIMEHeader {
|
||||
func GetAttachmentHeader(att *pmapi.Attachment, buildForIMAP bool) textproto.MIMEHeader {
|
||||
mediaType := att.MIMEType
|
||||
if mediaType == "application/pgp-encrypted" {
|
||||
mediaType = "application/octet-stream"
|
||||
}
|
||||
|
||||
transferEncoding := "base64"
|
||||
if mediaType == rfc822Message && buildForIMAP {
|
||||
transferEncoding = "8bit"
|
||||
}
|
||||
|
||||
encodedName := pmmime.EncodeHeader(att.Name)
|
||||
disposition := "attachment" //nolint[goconst]
|
||||
if strings.Contains(att.Header.Get("Content-Disposition"), "inline") {
|
||||
disposition = "inline"
|
||||
if strings.Contains(att.Header.Get("Content-Disposition"), pmapi.DispositionInline) {
|
||||
disposition = pmapi.DispositionInline
|
||||
}
|
||||
|
||||
h := make(textproto.MIMEHeader)
|
||||
h.Set("Content-Type", mime.FormatMediaType(mediaType, map[string]string{"name": encodedName}))
|
||||
h.Set("Content-Transfer-Encoding", "base64")
|
||||
if transferEncoding != "" {
|
||||
h.Set("Content-Transfer-Encoding", transferEncoding)
|
||||
}
|
||||
h.Set("Content-Disposition", mime.FormatMediaType(disposition, map[string]string{"filename": encodedName}))
|
||||
|
||||
// Forward some original header lines.
|
||||
|
||||
@ -18,31 +18,27 @@
|
||||
package message
|
||||
|
||||
import (
|
||||
"crypto/sha512"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
rfc822Message = "message/rfc822"
|
||||
)
|
||||
|
||||
var log = logrus.WithField("pkg", "pkg/message") //nolint[gochecknoglobals]
|
||||
|
||||
func GetBoundary(m *pmapi.Message) string {
|
||||
// The boundary needs to be deterministic because messages are not supposed to
|
||||
// change.
|
||||
return fmt.Sprintf("%x", sha512.Sum512_256([]byte(m.ID)))
|
||||
}
|
||||
|
||||
func GetRelatedBoundary(m *pmapi.Message) string {
|
||||
// The boundary needs to be deterministic because messages are not supposed to
|
||||
// change.
|
||||
return fmt.Sprintf("%x", sha512.Sum512_256([]byte(m.ID+m.ID)))
|
||||
return newBoundary(m.ID).gen()
|
||||
}
|
||||
|
||||
func SeparateInlineAttachments(m *pmapi.Message) (atts, inlines []*pmapi.Attachment) {
|
||||
for _, att := range m.Attachments {
|
||||
if strings.Contains(att.Header.Get("Content-Disposition"), "inline") {
|
||||
if strings.Contains(att.Header.Get("Content-Disposition"), pmapi.DispositionInline) {
|
||||
inlines = append(inlines, att)
|
||||
} else {
|
||||
atts = append(atts, att)
|
||||
|
||||
82
pkg/message/mocks/mocks.go
Normal file
82
pkg/message/mocks/mocks.go
Normal file
@ -0,0 +1,82 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/ProtonMail/proton-bridge/pkg/message (interfaces: Fetcher)
|
||||
|
||||
// Package mocks is a generated GoMock package.
|
||||
package mocks
|
||||
|
||||
import (
|
||||
io "io"
|
||||
reflect "reflect"
|
||||
|
||||
crypto "github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
pmapi "github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
)
|
||||
|
||||
// MockFetcher is a mock of Fetcher interface
|
||||
type MockFetcher struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockFetcherMockRecorder
|
||||
}
|
||||
|
||||
// MockFetcherMockRecorder is the mock recorder for MockFetcher
|
||||
type MockFetcherMockRecorder struct {
|
||||
mock *MockFetcher
|
||||
}
|
||||
|
||||
// NewMockFetcher creates a new mock instance
|
||||
func NewMockFetcher(ctrl *gomock.Controller) *MockFetcher {
|
||||
mock := &MockFetcher{ctrl: ctrl}
|
||||
mock.recorder = &MockFetcherMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
func (m *MockFetcher) EXPECT() *MockFetcherMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// GetAttachment mocks base method
|
||||
func (m *MockFetcher) GetAttachment(arg0 string) (io.ReadCloser, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetAttachment", arg0)
|
||||
ret0, _ := ret[0].(io.ReadCloser)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetAttachment indicates an expected call of GetAttachment
|
||||
func (mr *MockFetcherMockRecorder) GetAttachment(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAttachment", reflect.TypeOf((*MockFetcher)(nil).GetAttachment), arg0)
|
||||
}
|
||||
|
||||
// GetMessage mocks base method
|
||||
func (m *MockFetcher) GetMessage(arg0 string) (*pmapi.Message, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetMessage", arg0)
|
||||
ret0, _ := ret[0].(*pmapi.Message)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetMessage indicates an expected call of GetMessage
|
||||
func (mr *MockFetcherMockRecorder) GetMessage(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMessage", reflect.TypeOf((*MockFetcher)(nil).GetMessage), arg0)
|
||||
}
|
||||
|
||||
// KeyRingForAddressID mocks base method
|
||||
func (m *MockFetcher) KeyRingForAddressID(arg0 string) (*crypto.KeyRing, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "KeyRingForAddressID", arg0)
|
||||
ret0, _ := ret[0].(*crypto.KeyRing)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// KeyRingForAddressID indicates an expected call of KeyRingForAddressID
|
||||
func (mr *MockFetcherMockRecorder) KeyRingForAddressID(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KeyRingForAddressID", reflect.TypeOf((*MockFetcher)(nil).KeyRingForAddressID), arg0)
|
||||
}
|
||||
@ -536,7 +536,7 @@ func parseAttachment(h message.Header) (*pmapi.Attachment, error) {
|
||||
if h.Has("Content-Disposition") {
|
||||
if disp, _, err := h.ContentDisposition(); err != nil {
|
||||
return nil, err
|
||||
} else if disp == "inline" {
|
||||
} else if disp == pmapi.DispositionInline {
|
||||
att.ContentID = strings.Trim(h.Get("Content-Id"), " <>")
|
||||
}
|
||||
} else if h.Has("Content-Id") {
|
||||
|
||||
@ -38,7 +38,7 @@ type SectionInfo struct {
|
||||
reader io.Reader
|
||||
}
|
||||
|
||||
// Read and count
|
||||
// Read and count.
|
||||
func (si *SectionInfo) Read(p []byte) (n int, err error) {
|
||||
n, err = si.reader.Read(p)
|
||||
si.Size += n
|
||||
@ -237,11 +237,11 @@ func (bs *BodyStructure) parseAllChildSections(r io.Reader, currentPath []int, s
|
||||
}
|
||||
|
||||
// Clear all buffers.
|
||||
bodyReader = nil
|
||||
bodyReader = nil //nolint[wastedassign] just to be sure we clear garbage collector
|
||||
bodyInfo.reader = nil
|
||||
tp.R = nil
|
||||
tp = nil
|
||||
bufInfo = nil // nolint
|
||||
tp = nil //nolint[wastedassign] just to be sure we clear garbage collector
|
||||
bufInfo = nil //nolint[ineffassign] just to be sure we clear garbage collector
|
||||
info.reader = nil
|
||||
|
||||
// Store boundaries.
|
||||
@ -305,6 +305,11 @@ func stringPathFromInts(ints []int) (ret string) {
|
||||
return
|
||||
}
|
||||
|
||||
func (bs *BodyStructure) hasInfo(sectionPath []int) bool {
|
||||
_, err := bs.getInfo(sectionPath)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (bs *BodyStructure) getInfo(sectionPath []int) (sectionInfo *SectionInfo, err error) {
|
||||
path := stringPathFromInts(sectionPath)
|
||||
sectionInfo, ok := (*bs)[path]
|
||||
@ -332,30 +337,40 @@ func (bs *BodyStructure) GetSectionContent(wholeMail io.ReadSeeker, sectionPath
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if _, err = wholeMail.Seek(int64(info.Start+info.Size-info.BSize), io.SeekStart); err != nil {
|
||||
return
|
||||
}
|
||||
section = make([]byte, info.BSize)
|
||||
_, err = wholeMail.Read(section)
|
||||
return
|
||||
return goToOffsetAndReadNBytes(wholeMail, info.Start+info.Size-info.BSize, info.BSize)
|
||||
}
|
||||
|
||||
/* This is slow:
|
||||
sectionBuf, err := bs.GetSection(wholeMail, sectionPath)
|
||||
// GetMailHeader returns the main header of mail.
|
||||
func (bs *BodyStructure) GetMailHeader() (header textproto.MIMEHeader, err error) {
|
||||
return bs.GetSectionHeader([]int{})
|
||||
}
|
||||
|
||||
// GetMailHeaderBytes returns the bytes with main mail header.
|
||||
// Warning: It can contain extra lines or multipart comment.
|
||||
func (bs *BodyStructure) GetMailHeaderBytes(wholeMail io.ReadSeeker) (header []byte, err error) {
|
||||
info, err := bs.getInfo([]int{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
tp := textproto.NewReader(bufio.NewReader(buf))
|
||||
if _, err = tp.ReadMIMEHeader(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sectionBuf = &bytes.Buffer{}
|
||||
_, err = io.Copy(sectionBuf, tp.R)
|
||||
return
|
||||
*/
|
||||
headerLength := info.Size - info.BSize
|
||||
return goToOffsetAndReadNBytes(wholeMail, 0, headerLength)
|
||||
}
|
||||
|
||||
func goToOffsetAndReadNBytes(wholeMail io.ReadSeeker, offset, length int) ([]byte, error) {
|
||||
if length < 1 {
|
||||
return nil, errors.New("requested non positive length")
|
||||
}
|
||||
if offset > 0 {
|
||||
if _, err := wholeMail.Seek(int64(offset), io.SeekStart); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
out := make([]byte, length)
|
||||
_, err := wholeMail.Read(out)
|
||||
return out, err
|
||||
}
|
||||
|
||||
// GetSectionHeader returns the mime header of specified section.
|
||||
func (bs *BodyStructure) GetSectionHeader(sectionPath []int) (header textproto.MIMEHeader, err error) {
|
||||
info, err := bs.getInfo(sectionPath)
|
||||
if err != nil {
|
||||
@ -404,7 +419,7 @@ func (bs *BodyStructure) IMAPBodyStructure(currentPart []int) (imapBS *imap.Body
|
||||
|
||||
nextPart := append(currentPart, 1)
|
||||
for {
|
||||
if _, err := bs.getInfo(nextPart); err != nil {
|
||||
if !bs.hasInfo(nextPart) {
|
||||
break
|
||||
}
|
||||
var subStruct *imap.BodyStructure
|
||||
|
||||
@ -108,6 +108,24 @@ func TestGetSection(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetMainHeaderBytes(t *testing.T) {
|
||||
wantHeader := []byte(`Subject: Sample mail
|
||||
From: John Doe <jdoe@machine.example>
|
||||
To: Mary Smith <mary@example.net>
|
||||
Date: Fri, 21 Nov 1997 09:55:06 -0600
|
||||
Content-Type: multipart/mixed; boundary="0000MAIN"
|
||||
|
||||
`)
|
||||
|
||||
structReader := strings.NewReader(sampleMail)
|
||||
bs, err := NewBodyStructure(structReader)
|
||||
require.NoError(t, err)
|
||||
|
||||
haveHeader, err := bs.GetMailHeaderBytes(strings.NewReader(sampleMail))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, wantHeader, haveHeader)
|
||||
}
|
||||
|
||||
/* Structure example:
|
||||
HEADER ([RFC-2822] header of the message)
|
||||
TEXT ([RFC-2822] text body of the message) MULTIPART/MIXED
|
||||
|
||||
33
pkg/message/testdata/pgp-mime-body-html.eml
vendored
Normal file
33
pkg/message/testdata/pgp-mime-body-html.eml
vendored
Normal file
@ -0,0 +1,33 @@
|
||||
Content-Type: multipart/mixed; boundary="u5NoTcx3NkhqapFjjYFKJZdxCaEWvrsGw";
|
||||
protected-headers="v1"
|
||||
Subject: html no pubkey no sign
|
||||
From: "pm.bridge.qa" <pm.bridge.qa@gmail.com>
|
||||
To: schizofrenic@pm.me
|
||||
Message-ID: <c38ad850-0916-e290-ee1c-326c3ff9fb5f@gmail.com>
|
||||
|
||||
--u5NoTcx3NkhqapFjjYFKJZdxCaEWvrsGw
|
||||
Content-Type: text/html; charset=utf-8
|
||||
Content-Language: en-US
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
<html>
|
||||
<head>
|
||||
|
||||
<meta http-equiv=3D"content-type" content=3D"text/html; charset=3DUTF=
|
||||
-8">
|
||||
</head>
|
||||
<body>
|
||||
<ul>
|
||||
<li><i>What do you call a poor Santa Claus?</i> <b>St.
|
||||
Nickel-less.</b></li>
|
||||
<li><i>Where do boats go when they're sick?</i> <b>To the boat
|
||||
doc.</b><br>
|
||||
</li>
|
||||
</ul>
|
||||
<p><br>
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
--u5NoTcx3NkhqapFjjYFKJZdxCaEWvrsGw--
|
||||
17
pkg/message/testdata/pgp-mime-body-plaintext.eml
vendored
Normal file
17
pkg/message/testdata/pgp-mime-body-plaintext.eml
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
Content-Type: multipart/mixed; boundary="unlHEst6hn6dMAzATXJvy5dCLgUfF9Vvs";
|
||||
protected-headers="v1"
|
||||
Subject: plain no pubkey no sign
|
||||
From: "pm.bridge.qa" <pm.bridge.qa@gmail.com>
|
||||
To: schizofrenic@pm.me
|
||||
Message-ID: <564b9c7c-91eb-6508-107a-35108f383a44@gmail.com>
|
||||
|
||||
--unlHEst6hn6dMAzATXJvy5dCLgUfF9Vvs
|
||||
Content-Type: text/plain; charset=utf-8; format=flowed
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
Content-Language: en-US
|
||||
|
||||
Where do fruits go on vacation? Pear-is!
|
||||
|
||||
|
||||
|
||||
--unlHEst6hn6dMAzATXJvy5dCLgUfF9Vvs--
|
||||
212
pkg/message/testdata/pgp-mime-body-signed-embedded-message-rfc822-with-pubkey.eml
vendored
Normal file
212
pkg/message/testdata/pgp-mime-body-signed-embedded-message-rfc822-with-pubkey.eml
vendored
Normal file
@ -0,0 +1,212 @@
|
||||
Content-Type: multipart/signed; micalg=pgp-sha256;
|
||||
protocol="application/pgp-signature";
|
||||
boundary="Rrmlds5vN3IeeCVjbnepHmuVgyROSBjsS"
|
||||
|
||||
This is an OpenPGP/MIME signed message (RFC 4880 and 3156)
|
||||
--Rrmlds5vN3IeeCVjbnepHmuVgyROSBjsS
|
||||
Content-Type: multipart/mixed; boundary="avFkF0LAPYPXcFHcnsgGmACbGIPeVDdYc";
|
||||
protected-headers="v1"
|
||||
Subject: Fwd: HTML with attachment external PGP
|
||||
From: "pm.bridge.qa" <pm.bridge.qa@gmail.com>
|
||||
To: schizofrenic@pm.me
|
||||
Message-ID: <7c04869b-c470-116f-b8e5-8b4fd5e1195d@gmail.com>
|
||||
References: <LCxIUvb0rqBufdwR4JNxMg4ZDMI8x7pRG0vEHGHYwA@cp7-web-042.plabs.ch>
|
||||
In-Reply-To: <LCxIUvb0rqBufdwR4JNxMg4ZDMI8x7pRG0vEHGHYwA@cp7-web-042.plabs.ch>
|
||||
|
||||
--avFkF0LAPYPXcFHcnsgGmACbGIPeVDdYc
|
||||
Content-Type: multipart/mixed;
|
||||
boundary="------------2F19EE9A8A1A6F779F5D14AF"
|
||||
Content-Language: en-US
|
||||
|
||||
This is a multi-part message in MIME format.
|
||||
--------------2F19EE9A8A1A6F779F5D14AF
|
||||
Content-Type: text/plain; charset=utf-8; format=flowed
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
|
||||
|
||||
--------------2F19EE9A8A1A6F779F5D14AF
|
||||
Content-Type: application/pgp-keys;
|
||||
name="OpenPGP_0x161C0875822359F7.asc"
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
Content-Disposition: attachment;
|
||||
filename="OpenPGP_0x161C0875822359F7.asc"
|
||||
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
xsBNBFxlUPwBCACx954Ey4SD88f8DSKFw9BaZNXrNwYxNYSgqaqOGHQ0WllF3mstEhTfuxxCZ=
|
||||
pDh
|
||||
I5IhWCXUNxanzsFkn88mRDwFRVl2sf2aAG4/P/p1381oh2kd0UElMRQaQGzoCadQMaQOL9WYT=
|
||||
f4S
|
||||
PWSCzjrPyKgjq5FbqjbF/ndu376na9L+tnsEXyL6RrI6aZhjWG73xlqxS65dzTIYzsyM/P97x=
|
||||
Snd
|
||||
NvlvWtGvLlpFkzxfAEGpVzfOYVYFKoc8rGmUDwrDWYfk5JczRDDogJnY+BNMZf9pjSqk6rTyB=
|
||||
OfN
|
||||
H5fpU8r7A5Q7l+HVakvMUQ9DzDWJtg2ru1Y8hexnJOF68avO4+a1ABEBAAHNKEJyaWRnZSBLe=
|
||||
XUt
|
||||
RWh5aiA8cG0uYnJpZGdlLnFhQGdtYWlsLmNvbT7CwJQEEwEIAD4CGwMFCwkIBwIGFQoJCAsCB=
|
||||
BYC
|
||||
AwECHgECF4AWIQRc5gl5cC8oW/Mo+bEWHAh1giNZ9wUCYC32ygUJB4sMzgAKCRAWHAh1giNZ9=
|
||||
/K8
|
||||
B/4qs84Ii/zKH+q+C8vwO4jUJkOM73qD0pgB7zBs651zWbpgopyol1YUKNpFaHlx/Qch7RDI7=
|
||||
Vcz
|
||||
1+60/KZJSJR19/N2EDVbCUdh8ueioUp9X/218YWV2TRJNxTnljd4FAn7smZnXuP1TsLjQ6sKO=
|
||||
V0U
|
||||
u6JoiG6LZFXqDgxYpA++58Rkl6xaY6R71VkmVQlbEKtubX9AjHydq97Y+Jvn11XzWZaKhv4L7=
|
||||
6Pa
|
||||
4tMKXvvrKh1oywMmh6mZJo+5ZA/ABTkr45cwlTPYqGTS9+uvOHt+PH/oYwwJB4ls2cIAUldSj=
|
||||
TVQ
|
||||
IsseYz3LlbcCfKJiiCFxeHOQXA5J6zNLKOT58TsczsBNBFxlUPwBCADh2HsX23yVnJt9fxFz3=
|
||||
D07
|
||||
kCBNvu4HQfps6h1rgNxGhE32VmpESHebvIB5xjL6xKbIqqRa3x/7KDVBNJvca0gUsqEt5kzYF=
|
||||
88F
|
||||
yf2NBcejpIbcP7BS/g+C6KOowYj+Et26T6GdwFXExUcl80JvoX8yHQOfvJpdiBRbjyB8UqfCa=
|
||||
knm
|
||||
3c7dNuXmhflz/w3aBj32q9ZyGqA1NpHCpLyVAlvSNQ/pat/rGUCPZ9duw4KhUUqEmatQPVFPk=
|
||||
utT
|
||||
ouEZQbMK+i+chOH3AsKCuNDfvCDwirnsSqIJmAgl1lC4de+bsWYCMqN9ei99hOCRUyhZ3g3sr=
|
||||
8RB
|
||||
owVAdcvjZxeIDKALABEBAAHCwHwEGAEIACYCGwwWIQRc5gl5cC8oW/Mo+bEWHAh1giNZ9wUCY=
|
||||
C32
|
||||
lAUJB4sMmAAKCRAWHAh1giNZ9+Y2B/9rTKZaKviae+ummXNumXcrKvbkAAvfuLpKUn53FlQLm=
|
||||
L6H
|
||||
jB++lJnPWvVSzdZxdv8FiPP3d632XHKUrkQRQM/9byRDXDommi7Qttx7YCkhd4JLVYqJqpnAQ=
|
||||
xI5
|
||||
RMkXiZNWyr1lz8JOM1XvDk1M7sJwPMWews8VOIE03E1nt7AsQGnvHtadgEnQaufrYNX3hFA8S=
|
||||
osO
|
||||
HSnedcys6yrzCSIGCqCD9VHbnMtS4DOv0XJGh2hwc8omzH0KZA517dyKBorJRwadcVauGXDKx=
|
||||
Etv
|
||||
Im4rl94PR/3An1Mj6HeeVVpLqDQ5Jb9J90BahWeQ53FzRa4EQzYCw0nLnxcsT1ZEEP5u
|
||||
=3Dv/1p
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
--------------2F19EE9A8A1A6F779F5D14AF
|
||||
Content-Type: message/rfc822;
|
||||
name="HTML with attachment external PGP.eml"
|
||||
Content-Transfer-Encoding: 7bit
|
||||
Content-Disposition: attachment;
|
||||
filename="HTML with attachment external PGP.eml"
|
||||
|
||||
Delivered-To: pm.bridge.qa@gmail.com
|
||||
Received: by 2002:a17:906:a051:0:0:0:0 with SMTP id bg17csp66709ejb;
|
||||
Wed, 24 Mar 2021 22:03:32 -0700 (PDT)
|
||||
X-Google-Smtp-Source: ABdhPJxllBuHnnJzKWy77R291tZbVFVk0iahkLm1TQsluEYTvyAXdOWB/zp1y10e60UlGGZYH3YF
|
||||
X-Received: by 2002:a05:6000:118c:: with SMTP id g12mr6758087wrx.353.1616648612550;
|
||||
Wed, 24 Mar 2021 22:03:32 -0700 (PDT)
|
||||
ARC-Seal: i=1; a=rsa-sha256; t=1616648612; cv=none;
|
||||
d=google.com; s=arc-20160816;
|
||||
b=Jf4vmKEoeJQ3rIDMbI2twiDkfn50ejNnqIbs2nkaFruITcw6XhvhbcfV9HLC80Yt8E
|
||||
tfN7TV9qoBneSWzfSJ+Sqw31hBKKtKpMhuqZT9GPzBN5gdMJKj5ISAQ8Lgm9zvR3Zbjn
|
||||
N0nOzCu/oT1amMMm+48hpKj8VL2tydjvNG+g/a5lk1Aw7JdqIKV6t1XhsyyYaa1O+NFC
|
||||
rQThdalcQj2NjoZWba1mjZSzI7B7hJdZg5d+jado2TPMQXe2kz2wGmr3+/JcKvPJjrSA
|
||||
S+jzhpjcd7ZnctkzTfpsdlBJAGKoDBnSvQc3eMJ/AgRHFc+5ks5nRDt/1DowSjQ7i7rp
|
||||
4a+g==
|
||||
ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=arc-20160816;
|
||||
h=mime-version:message-id:subject:reply-to:from:to:dkim-signature
|
||||
:date;
|
||||
bh=vmJ0JT+IfeO4idMYP7zPvldBkdONjKTXWTp7ly/B9qk=;
|
||||
b=f8VY+ajsE/XNYrqD666FM0WCtNEQtUyU/Zh3pFCI9sFrMnAui4Qp9Gs1fe/8HLxt2v
|
||||
/C4l4eHELvPBv4vX0KtUvOlRZYPZbLZCNdtTcFtiuZEKUHWx370p7yyMWcmSMdlUbq4J
|
||||
NrKMPGfaYiZe5Rt3MyD5RKm4RJpqvep34VCHMYtoFQP/0Po4/1JMDw0Fy6SXUJ54rBRw
|
||||
bmzqNNBkonda3YghhK3WNrxTxzZ8I7KW9YdpENNS9ewJLeVtFQKdiLZwz5EpMZxOxG0I
|
||||
LW0jRtDlmZnqRe7bvTAo51IuLf9okHRI8PRiK0UHl+4Vr5Igq4mub7Ee8pC/Nz3Yj29G
|
||||
KODw==
|
||||
ARC-Authentication-Results: i=1; mx.google.com;
|
||||
dkim=pass header.i=@protonmail.com header.s=protonmail header.b=EX07e46H;
|
||||
spf=pass (google.com: domain of bridge-test-user@protonmail.com designates 185.70.40.22 as permitted sender) smtp.mailfrom=bridge-test-user@protonmail.com;
|
||||
dmarc=pass (p=QUARANTINE sp=QUARANTINE dis=NONE) header.from=protonmail.com
|
||||
Return-Path: <bridge-test-user@protonmail.com>
|
||||
Received: from mail2.protonmail.ch (mail2.protonmail.ch. [185.70.40.22])
|
||||
by mx.google.com with ESMTPS id g6si2999785wrr.110.2021.03.24.22.03.32
|
||||
for <pm.bridge.qa@gmail.com>
|
||||
(version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256);
|
||||
Wed, 24 Mar 2021 22:03:32 -0700 (PDT)
|
||||
Received-SPF: pass (google.com: domain of bridge-test-user@protonmail.com designates 185.70.40.22 as permitted sender) client-ip=185.70.40.22;
|
||||
Authentication-Results: mx.google.com;
|
||||
dkim=pass header.i=@protonmail.com header.s=protonmail header.b=EX07e46H;
|
||||
spf=pass (google.com: domain of bridge-test-user@protonmail.com designates 185.70.40.22 as permitted sender) smtp.mailfrom=bridge-test-user@protonmail.com;
|
||||
dmarc=pass (p=QUARANTINE sp=QUARANTINE dis=NONE) header.from=protonmail.com
|
||||
Date: Thu, 25 Mar 2021 05:03:27 +0000
|
||||
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=protonmail.com;
|
||||
s=protonmail; t=1616648611;
|
||||
bh=vmJ0JT+IfeO4idMYP7zPvldBkdONjKTXWTp7ly/B9qk=;
|
||||
h=Date:To:From:Reply-To:Subject:From;
|
||||
b=EX07e46H5/HmotAWZ69I4qa5jCVRao/p3KEM3eQn/AQ8s+cLMaR5b2ozdHrPCsTw5
|
||||
i5b1DLUHZHBf+6Ven47WJfKNwLUfkAGD2P0aI/dAk/h/h0Bg4Ni85pv+uPpRHLNQKv
|
||||
T3VnDP9MSwl6IUJu5zoM2EC70MLoiHS07lxhM2pw=
|
||||
To: External Bridge <pm.bridge.qa@gmail.com>
|
||||
From: Bridge Test <bridge-test-user@protonmail.com>
|
||||
Reply-To: Bridge Test <bridge-test-user@protonmail.com>
|
||||
Subject: HTML with attachment external PGP
|
||||
Message-ID: <LCxIUvb0rqBufdwR4JNxMg4ZDMI8x7pRG0vEHGHYwA@cp7-web-042.plabs.ch>
|
||||
MIME-Version: 1.0
|
||||
Content-Type: multipart/mixed;
|
||||
boundary="b1_LCxIUvb0rqBufdwR4JNxMg4ZDMI8x7pRG0vEHGHYwA"
|
||||
X-Spam-Status: No, score=-1.2 required=10.0 tests=ALL_TRUSTED,DKIM_SIGNED,
|
||||
DKIM_VALID,DKIM_VALID_AU,DKIM_VALID_EF,FREEMAIL_FROM,HTML_MESSAGE
|
||||
shortcircuit=no autolearn=disabled version=3.4.4
|
||||
X-Spam-Checker-Version: SpamAssassin 3.4.4 (2020-01-24) on
|
||||
mailout.protonmail.ch
|
||||
|
||||
This is a multi-part message in MIME format.
|
||||
|
||||
--b1_LCxIUvb0rqBufdwR4JNxMg4ZDMI8x7pRG0vEHGHYwA
|
||||
Content-Type: multipart/alternative;
|
||||
boundary="b2_LCxIUvb0rqBufdwR4JNxMg4ZDMI8x7pRG0vEHGHYwA"
|
||||
|
||||
--b2_LCxIUvb0rqBufdwR4JNxMg4ZDMI8x7pRG0vEHGHYwA
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
VGhpcyBpcyBib2R5IG9mIEhUTUwgbWFpbCB3aXRoIGF0dGFjaG1lbnQ=
|
||||
|
||||
--b2_LCxIUvb0rqBufdwR4JNxMg4ZDMI8x7pRG0vEHGHYwA
|
||||
Content-Type: text/html; charset=utf-8
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
PGh0bWw+PGhlYWQ+PC9oZWFkPjxib2R5PlRoaXMgaXMgYm9keSBvZiA8Yj5IVE1MIG1haWw8L2I+
|
||||
IHdpdGggYXR0YWNobWVudA0KPC9ib2R5PjwvaHRtbD4=
|
||||
|
||||
|
||||
--b2_LCxIUvb0rqBufdwR4JNxMg4ZDMI8x7pRG0vEHGHYwA--
|
||||
|
||||
--b1_LCxIUvb0rqBufdwR4JNxMg4ZDMI8x7pRG0vEHGHYwA
|
||||
Content-Type: image/png; name=outline-light-instagram-48.png
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-Disposition: attachment; filename=outline-light-instagram-48.png
|
||||
|
||||
iVBORw0KGgoAAAANSUhEUgAAADAAAAAwBAMAAAClLOS0AAAALVBMVEUAAAD/////////////////
|
||||
//////////////////////////////////////+hSKubAAAADnRSTlMAgO8QQM+/IJ9gj1AwcIQd
|
||||
OXUAAAGdSURBVDjLXJC9SgNBFIVPXDURTYhgIQghINgowyLYCAYtRFAIgtYhpAjYhC0srCRW6YIg
|
||||
WNpoHVSsg/gEii+Qnfxq4DyDc3cyMfrBwl2+O+fOHTi8p7LS5RUf/9gpMKL7iT9sK47Q95ggpkzv
|
||||
1cvRcsGYNMYsmP+zKN27NR2vcDyTNVdfkOuuniNPMWafvIbljt+YoMEvW8y7lt+ARwhvrgPjhA0I
|
||||
BTng7S1GLPlypBvtIBPidY4YBDJFdtnkscQ5JGaGqxC9i7jSDwcwnB8qHWBaQjw1ABI8wYgtVoG6
|
||||
9pFkH8iZIiJeulFt4JLvJq8I5N2GMWYbHWDWzM3JZTMdeSWla0kW86FcuI0mfStiNKQ/AhEeh8h0
|
||||
YUTffFwrMTT5oSwdojIQ0UKcocgAKRH1HiqhFQmmJa5qRaYHNbRiSsOgslY0NdixItUTUWlZkedP
|
||||
HXVyAgAIA1F0wP5btQZPIyTwvAqa/Fl4oacuP+e4XHAjSYpkQkxSiMX+T7FPoZJToSStzED70HCy
|
||||
KE3NGCg4jJrC6Ti7AFwZLhnW0gMbzFZc0RmmeAAAAABJRU5ErkJggg==
|
||||
|
||||
--b1_LCxIUvb0rqBufdwR4JNxMg4ZDMI8x7pRG0vEHGHYwA--
|
||||
|
||||
|
||||
--------------2F19EE9A8A1A6F779F5D14AF--
|
||||
|
||||
--avFkF0LAPYPXcFHcnsgGmACbGIPeVDdYc--
|
||||
|
||||
--Rrmlds5vN3IeeCVjbnepHmuVgyROSBjsS
|
||||
Content-Type: application/pgp-signature; name="OpenPGP_signature.asc"
|
||||
Content-Description: OpenPGP digital signature
|
||||
Content-Disposition: attachment; filename="OpenPGP_signature"
|
||||
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
wsB5BAABCAAjFiEEXOYJeXAvKFvzKPmxFhwIdYIjWfcFAmBciIAFAwAAAAAACgkQFhwIdYIjWfcN
|
||||
ZQf+NzAoEJRTSW5JFNgSGkwLsH89wAbw3wEt4PYuZaa+35xBuU8Sojm1oLOyuPkIasQf98Iu5P1o
|
||||
8cokViEa6wm+ZZpcFMi6T2/3+UNlSm81Epm7GrFyjAFTWrdTPLb4k4x47sz77RoTp/UEwm/7fVI5
|
||||
gMYhQyIYaocXHmDk61UshWE9q/Po6qjHBnnWS8YBnhUS9lK8uimpfRO9UQ9bIUjIYDGDPAtBoYnb
|
||||
X9V4SjBvbbdNrgoVaDxPw6HYCb3RhzRXunr5Icdnjfbc2H40/FayVi/p7GzFh+8zv/TzRxMkHo72
|
||||
DBsONaC7r8bxQ9BwJvpmWufqL7ZXHfVXQ6z+M43e1Q==
|
||||
=Stx+
|
||||
-----END PGP SIGNATURE-----
|
||||
|
||||
--Rrmlds5vN3IeeCVjbnepHmuVgyROSBjsS--
|
||||
116
pkg/message/testdata/pgp-mime-body-signed-html-with-pubkey.eml
vendored
Normal file
116
pkg/message/testdata/pgp-mime-body-signed-html-with-pubkey.eml
vendored
Normal file
@ -0,0 +1,116 @@
|
||||
Content-Type: multipart/signed; micalg=pgp-sha256;
|
||||
protocol="application/pgp-signature";
|
||||
boundary="pavrbLYh8Q4RWBboYnVxY3mNBBzan1Zz4"
|
||||
|
||||
This is an OpenPGP/MIME signed message (RFC 4880 and 3156)
|
||||
--pavrbLYh8Q4RWBboYnVxY3mNBBzan1Zz4
|
||||
Content-Type: multipart/mixed; boundary="avFoFILZo8SdHM1Pc1OUviN4UKQh16HyR";
|
||||
protected-headers="v1"
|
||||
Subject: simple html body
|
||||
From: "pm.bridge.qa" <pm.bridge.qa@gmail.com>
|
||||
To: schizofrenic@pm.me
|
||||
Message-ID: <d9c99685-4e1c-8f95-8b68-c6b0fcfd62ef@gmail.com>
|
||||
|
||||
--avFoFILZo8SdHM1Pc1OUviN4UKQh16HyR
|
||||
Content-Type: multipart/mixed;
|
||||
boundary="------------9EAE2E1A715ACB9849E5C4E3"
|
||||
Content-Language: en-US
|
||||
|
||||
This is a multi-part message in MIME format.
|
||||
--------------9EAE2E1A715ACB9849E5C4E3
|
||||
Content-Type: text/html; charset=utf-8
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
<html>
|
||||
<head>
|
||||
|
||||
<meta http-equiv=3D"content-type" content=3D"text/html; charset=3DUTF=
|
||||
-8">
|
||||
</head>
|
||||
<body>
|
||||
And this is HTML<br>
|
||||
<ul>
|
||||
<li><b>Do I enjoy making courthouse puns?</b> Guilty.=E2=80=94 <i>@=
|
||||
baddadjokes</i></li>
|
||||
<li><b>Can February March?</b> No, but April May. =E2=80=94<i>@Bear=
|
||||
dedMOGuy</i></li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
--------------9EAE2E1A715ACB9849E5C4E3
|
||||
Content-Type: application/pgp-keys;
|
||||
name="OpenPGP_0x161C0875822359F7.asc"
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
Content-Disposition: attachment;
|
||||
filename="OpenPGP_0x161C0875822359F7.asc"
|
||||
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
xsBNBFxlUPwBCACx954Ey4SD88f8DSKFw9BaZNXrNwYxNYSgqaqOGHQ0WllF3mstEhTfuxxCZ=
|
||||
pDh
|
||||
I5IhWCXUNxanzsFkn88mRDwFRVl2sf2aAG4/P/p1381oh2kd0UElMRQaQGzoCadQMaQOL9WYT=
|
||||
f4S
|
||||
PWSCzjrPyKgjq5FbqjbF/ndu376na9L+tnsEXyL6RrI6aZhjWG73xlqxS65dzTIYzsyM/P97x=
|
||||
Snd
|
||||
NvlvWtGvLlpFkzxfAEGpVzfOYVYFKoc8rGmUDwrDWYfk5JczRDDogJnY+BNMZf9pjSqk6rTyB=
|
||||
OfN
|
||||
H5fpU8r7A5Q7l+HVakvMUQ9DzDWJtg2ru1Y8hexnJOF68avO4+a1ABEBAAHNKEJyaWRnZSBLe=
|
||||
XUt
|
||||
RWh5aiA8cG0uYnJpZGdlLnFhQGdtYWlsLmNvbT7CwJQEEwEIAD4CGwMFCwkIBwIGFQoJCAsCB=
|
||||
BYC
|
||||
AwECHgECF4AWIQRc5gl5cC8oW/Mo+bEWHAh1giNZ9wUCYC32ygUJB4sMzgAKCRAWHAh1giNZ9=
|
||||
/K8
|
||||
B/4qs84Ii/zKH+q+C8vwO4jUJkOM73qD0pgB7zBs651zWbpgopyol1YUKNpFaHlx/Qch7RDI7=
|
||||
Vcz
|
||||
1+60/KZJSJR19/N2EDVbCUdh8ueioUp9X/218YWV2TRJNxTnljd4FAn7smZnXuP1TsLjQ6sKO=
|
||||
V0U
|
||||
u6JoiG6LZFXqDgxYpA++58Rkl6xaY6R71VkmVQlbEKtubX9AjHydq97Y+Jvn11XzWZaKhv4L7=
|
||||
6Pa
|
||||
4tMKXvvrKh1oywMmh6mZJo+5ZA/ABTkr45cwlTPYqGTS9+uvOHt+PH/oYwwJB4ls2cIAUldSj=
|
||||
TVQ
|
||||
IsseYz3LlbcCfKJiiCFxeHOQXA5J6zNLKOT58TsczsBNBFxlUPwBCADh2HsX23yVnJt9fxFz3=
|
||||
D07
|
||||
kCBNvu4HQfps6h1rgNxGhE32VmpESHebvIB5xjL6xKbIqqRa3x/7KDVBNJvca0gUsqEt5kzYF=
|
||||
88F
|
||||
yf2NBcejpIbcP7BS/g+C6KOowYj+Et26T6GdwFXExUcl80JvoX8yHQOfvJpdiBRbjyB8UqfCa=
|
||||
knm
|
||||
3c7dNuXmhflz/w3aBj32q9ZyGqA1NpHCpLyVAlvSNQ/pat/rGUCPZ9duw4KhUUqEmatQPVFPk=
|
||||
utT
|
||||
ouEZQbMK+i+chOH3AsKCuNDfvCDwirnsSqIJmAgl1lC4de+bsWYCMqN9ei99hOCRUyhZ3g3sr=
|
||||
8RB
|
||||
owVAdcvjZxeIDKALABEBAAHCwHwEGAEIACYCGwwWIQRc5gl5cC8oW/Mo+bEWHAh1giNZ9wUCY=
|
||||
C32
|
||||
lAUJB4sMmAAKCRAWHAh1giNZ9+Y2B/9rTKZaKviae+ummXNumXcrKvbkAAvfuLpKUn53FlQLm=
|
||||
L6H
|
||||
jB++lJnPWvVSzdZxdv8FiPP3d632XHKUrkQRQM/9byRDXDommi7Qttx7YCkhd4JLVYqJqpnAQ=
|
||||
xI5
|
||||
RMkXiZNWyr1lz8JOM1XvDk1M7sJwPMWews8VOIE03E1nt7AsQGnvHtadgEnQaufrYNX3hFA8S=
|
||||
osO
|
||||
HSnedcys6yrzCSIGCqCD9VHbnMtS4DOv0XJGh2hwc8omzH0KZA517dyKBorJRwadcVauGXDKx=
|
||||
Etv
|
||||
Im4rl94PR/3An1Mj6HeeVVpLqDQ5Jb9J90BahWeQ53FzRa4EQzYCw0nLnxcsT1ZEEP5u
|
||||
=3Dv/1p
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
--------------9EAE2E1A715ACB9849E5C4E3--
|
||||
|
||||
--avFoFILZo8SdHM1Pc1OUviN4UKQh16HyR--
|
||||
|
||||
--pavrbLYh8Q4RWBboYnVxY3mNBBzan1Zz4
|
||||
Content-Type: application/pgp-signature; name="OpenPGP_signature.asc"
|
||||
Content-Description: OpenPGP digital signature
|
||||
Content-Disposition: attachment; filename="OpenPGP_signature"
|
||||
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
wsB5BAABCAAjFiEEXOYJeXAvKFvzKPmxFhwIdYIjWfcFAmBa9hAFAwAAAAAACgkQFhwIdYIjWffL
|
||||
1AgApF18AVOPEm9y5R+d0NQmxqhSwAtvaqCwqQpG3mArIYK3Y0zrDkPQZZl/3emW8LWht7ZyYCAb
|
||||
NZo7HoYxjLy3yxAOPUl/Pc0nJpEqk/wAZT58yOnzv8DU5Q9o+444FfTMJpcrcH/M5cXYyqRtVhas
|
||||
k5wu5u2DEgSO3Kj/5l7lThb+CUgRC6wSiOuUkqGEWLiAguCdd88XDkLMbwrDnOu3PbhcA8o1msns
|
||||
PfkBdq3mFjp4M8M4ha+D2MxmV6tBv1E7snWf/spBVb9fHIa7zI4ZS6shpzGHCnJarO0Jco0Qh3IZ
|
||||
ZVfwhtJeFsmdqSm6DLvCmQWAYk2fDOZDMVKqe9IbUA==
|
||||
=pkS0
|
||||
-----END PGP SIGNATURE-----
|
||||
|
||||
--pavrbLYh8Q4RWBboYnVxY3mNBBzan1Zz4--
|
||||
58
pkg/message/testdata/pgp-mime-body-signed-html.eml
vendored
Normal file
58
pkg/message/testdata/pgp-mime-body-signed-html.eml
vendored
Normal file
@ -0,0 +1,58 @@
|
||||
Content-Type: multipart/signed; micalg=pgp-sha256;
|
||||
protocol="application/pgp-signature";
|
||||
boundary="YxKjBoCQD3a9PdAWo8ztsilFlSghrT8M5"
|
||||
|
||||
This is an OpenPGP/MIME signed message (RFC 4880 and 3156)
|
||||
--YxKjBoCQD3a9PdAWo8ztsilFlSghrT8M5
|
||||
Content-Type: multipart/mixed; boundary="6GLjuOzexqUw1CoA6CFjmA6r51g9FOPK7";
|
||||
protected-headers="v1"
|
||||
Subject: html body no pubkey
|
||||
From: "pm.bridge.qa" <pm.bridge.qa@gmail.com>
|
||||
To: schizofrenic@pm.me
|
||||
Message-ID: <5e22f83a-c4f0-d61a-55c8-8230854dc052@gmail.com>
|
||||
|
||||
--6GLjuOzexqUw1CoA6CFjmA6r51g9FOPK7
|
||||
Content-Type: text/html; charset=utf-8
|
||||
Content-Language: en-US
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
<html>
|
||||
<head>
|
||||
|
||||
<meta http-equiv=3D"content-type" content=3D"text/html; charset=3DUTF=
|
||||
-8">
|
||||
</head>
|
||||
<body>
|
||||
Behold another <font color=3D"#ee24cc">HTML</font><br>
|
||||
<ul>
|
||||
<li><b>I only know 25 letters of the alphabet.</b> <b>I don't
|
||||
know y.</b></li>
|
||||
<li><b>What did one wall say to the other?</b><i> I'll meet you at
|
||||
the corner.</i></li>
|
||||
<li><b>What did the zero say to the eight?</b> <i>Damn, that belt
|
||||
looks good on you.</i><br>
|
||||
</li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
--6GLjuOzexqUw1CoA6CFjmA6r51g9FOPK7--
|
||||
|
||||
--YxKjBoCQD3a9PdAWo8ztsilFlSghrT8M5
|
||||
Content-Type: application/pgp-signature; name="OpenPGP_signature.asc"
|
||||
Content-Description: OpenPGP digital signature
|
||||
Content-Disposition: attachment; filename="OpenPGP_signature"
|
||||
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
wsB5BAABCAAjFiEEXOYJeXAvKFvzKPmxFhwIdYIjWfcFAmBa+RsFAwAAAAAACgkQFhwIdYIjWfcK
|
||||
aQf/a9w4OwdyFerAW5Y45SdjAOA7WKUbm0gnrifbM2zk03bMEsdgfJQawC1p0hVyUCeqFYNJ9JQ4
|
||||
JF5/+7iWEe6oRFp3nW3LbBNr8wu3iN/dp5AWjTqnzx9VXLcvEryV/FJXwMUngO6z0eNVlxjdDFH/
|
||||
ucomItcmXFmfDx68ghLkumyWwX4SDfd/W70Wqi1f35wLBjfVIeFik4AS0bmpGFfMt1MKHrgirn2S
|
||||
+9sKPBiTQ+EFGK9V1wFrrDFleLDDE6oTMl75OUmY1Rr0y9q9jmws3cciEFYT3hTV9LNSwV9hMhZZ
|
||||
IEKAzLTy6nYnVltYkFC1ggwAVouq4o6Bcw/5bUt2fA==
|
||||
=lk/3
|
||||
-----END PGP SIGNATURE-----
|
||||
|
||||
--YxKjBoCQD3a9PdAWo8ztsilFlSghrT8M5--
|
||||
161
pkg/message/testdata/pgp-mime-body-signed-multipart-alternative-with-pubkey.eml
vendored
Normal file
161
pkg/message/testdata/pgp-mime-body-signed-multipart-alternative-with-pubkey.eml
vendored
Normal file
@ -0,0 +1,161 @@
|
||||
Content-Type: multipart/signed; micalg=pgp-sha256;
|
||||
protocol="application/pgp-signature";
|
||||
boundary="MHEDFShwcX18dyE3X7RXujo5fjpgdjHNM"
|
||||
|
||||
This is an OpenPGP/MIME signed message (RFC 4880 and 3156)
|
||||
--MHEDFShwcX18dyE3X7RXujo5fjpgdjHNM
|
||||
Content-Type: multipart/mixed; boundary="FBBl2LNv76z8UkvHhSkT9vLwVwxqV8378";
|
||||
protected-headers="v1"
|
||||
Subject: Alternative
|
||||
From: "pm.bridge.qa" <pm.bridge.qa@gmail.com>
|
||||
To: schizofrenic@pm.me
|
||||
Message-ID: <753d0314-0286-2c88-2abb-f8080ac7a4cb@gmail.com>
|
||||
|
||||
--FBBl2LNv76z8UkvHhSkT9vLwVwxqV8378
|
||||
Content-Type: multipart/mixed;
|
||||
boundary="------------F97C8ED4878E94675762AE43"
|
||||
Content-Language: en-US
|
||||
|
||||
This is a multi-part message in MIME format.
|
||||
--------------F97C8ED4878E94675762AE43
|
||||
Content-Type: multipart/alternative;
|
||||
boundary="------------041318B15DD3FA540FED32C6"
|
||||
|
||||
|
||||
--------------041318B15DD3FA540FED32C6
|
||||
Content-Type: text/plain; charset=utf-8; format=flowed
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
This Rich formated text
|
||||
|
||||
* /What kind of shoes do ninjas wear? /*Sneakers!*
|
||||
* /How does a penguin build its house?/**_/*Igloos it together.*/_
|
||||
|
||||
|
||||
|
||||
|
||||
--------------041318B15DD3FA540FED32C6
|
||||
Content-Type: text/html; charset=utf-8
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
<html>
|
||||
<head>
|
||||
|
||||
<meta http-equiv=3D"content-type" content=3D"text/html; charset=3DUTF=
|
||||
-8">
|
||||
</head>
|
||||
<body>
|
||||
<p>This <font color=3D"#ee24cc">Rich</font> formated text</p>
|
||||
<ul>
|
||||
<li><i>What kind of shoes do ninjas wear? </i><b>Sneakers!</b></li>=
|
||||
|
||||
<li><i>How does a penguin build its house?</i><b> </b><u><i><b>Iglo=
|
||||
os
|
||||
it together.</b></i></u></li>
|
||||
</ul>
|
||||
<p><br>
|
||||
</p>
|
||||
<p><br>
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
--------------041318B15DD3FA540FED32C6--
|
||||
|
||||
--------------F97C8ED4878E94675762AE43
|
||||
Content-Type: application/pdf;
|
||||
name="minimal.pdf"
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-Disposition: attachment;
|
||||
filename="minimal.pdf"
|
||||
|
||||
JVBERi0xLjEKJcKlwrHDqwoKMSAwIG9iagogIDw8IC9UeXBlIC9DYXRhbG9nCiAgICAgL1Bh
|
||||
Z2VzIDIgMCBSCiAgPj4KZW5kb2JqCgoyIDAgb2JqCiAgPDwgL1R5cGUgL1BhZ2VzCiAgICAg
|
||||
L0tpZHMgWzMgMCBSXQogICAgIC9Db3VudCAxCiAgICAgL01lZGlhQm94IFswIDAgMzAwIDE0
|
||||
NF0KICA+PgplbmRvYmoKCjMgMCBvYmoKICA8PCAgL1R5cGUgL1BhZ2UKICAgICAgL1BhcmVu
|
||||
dCAyIDAgUgogICAgICAvUmVzb3VyY2VzCiAgICAgICA8PCAvRm9udAogICAgICAgICAgIDw8
|
||||
IC9GMQogICAgICAgICAgICAgICA8PCAvVHlwZSAvRm9udAogICAgICAgICAgICAgICAgICAv
|
||||
U3VidHlwZSAvVHlwZTEKICAgICAgICAgICAgICAgICAgL0Jhc2VGb250IC9UaW1lcy1Sb21h
|
||||
bgogICAgICAgICAgICAgICA+PgogICAgICAgICAgID4+CiAgICAgICA+PgogICAgICAvQ29u
|
||||
dGVudHMgNCAwIFIKICA+PgplbmRvYmoKCjQgMCBvYmoKICA8PCAvTGVuZ3RoIDU1ID4+CnN0
|
||||
cmVhbQogIEJUCiAgICAvRjEgMTggVGYKICAgIDAgMCBUZAogICAgKEhlbGxvIFdvcmxkKSBU
|
||||
agogIEVUCmVuZHN0cmVhbQplbmRvYmoKCnhyZWYKMCA1CjAwMDAwMDAwMDAgNjU1MzUgZiAK
|
||||
MDAwMDAwMDAxOCAwMDAwMCBuIAowMDAwMDAwMDc3IDAwMDAwIG4gCjAwMDAwMDAxNzggMDAw
|
||||
MDAgbiAKMDAwMDAwMDQ1NyAwMDAwMCBuIAp0cmFpbGVyCiAgPDwgIC9Sb290IDEgMCBSCiAg
|
||||
ICAgIC9TaXplIDUKICA+PgpzdGFydHhyZWYKNTY1CiUlRU9GCg==
|
||||
--------------F97C8ED4878E94675762AE43
|
||||
Content-Type: application/pgp-keys;
|
||||
name="OpenPGP_0x161C0875822359F7.asc"
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
Content-Disposition: attachment;
|
||||
filename="OpenPGP_0x161C0875822359F7.asc"
|
||||
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
xsBNBFxlUPwBCACx954Ey4SD88f8DSKFw9BaZNXrNwYxNYSgqaqOGHQ0WllF3mstEhTfuxxCZ=
|
||||
pDh
|
||||
I5IhWCXUNxanzsFkn88mRDwFRVl2sf2aAG4/P/p1381oh2kd0UElMRQaQGzoCadQMaQOL9WYT=
|
||||
f4S
|
||||
PWSCzjrPyKgjq5FbqjbF/ndu376na9L+tnsEXyL6RrI6aZhjWG73xlqxS65dzTIYzsyM/P97x=
|
||||
Snd
|
||||
NvlvWtGvLlpFkzxfAEGpVzfOYVYFKoc8rGmUDwrDWYfk5JczRDDogJnY+BNMZf9pjSqk6rTyB=
|
||||
OfN
|
||||
H5fpU8r7A5Q7l+HVakvMUQ9DzDWJtg2ru1Y8hexnJOF68avO4+a1ABEBAAHNKEJyaWRnZSBLe=
|
||||
XUt
|
||||
RWh5aiA8cG0uYnJpZGdlLnFhQGdtYWlsLmNvbT7CwJQEEwEIAD4CGwMFCwkIBwIGFQoJCAsCB=
|
||||
BYC
|
||||
AwECHgECF4AWIQRc5gl5cC8oW/Mo+bEWHAh1giNZ9wUCYC32ygUJB4sMzgAKCRAWHAh1giNZ9=
|
||||
/K8
|
||||
B/4qs84Ii/zKH+q+C8vwO4jUJkOM73qD0pgB7zBs651zWbpgopyol1YUKNpFaHlx/Qch7RDI7=
|
||||
Vcz
|
||||
1+60/KZJSJR19/N2EDVbCUdh8ueioUp9X/218YWV2TRJNxTnljd4FAn7smZnXuP1TsLjQ6sKO=
|
||||
V0U
|
||||
u6JoiG6LZFXqDgxYpA++58Rkl6xaY6R71VkmVQlbEKtubX9AjHydq97Y+Jvn11XzWZaKhv4L7=
|
||||
6Pa
|
||||
4tMKXvvrKh1oywMmh6mZJo+5ZA/ABTkr45cwlTPYqGTS9+uvOHt+PH/oYwwJB4ls2cIAUldSj=
|
||||
TVQ
|
||||
IsseYz3LlbcCfKJiiCFxeHOQXA5J6zNLKOT58TsczsBNBFxlUPwBCADh2HsX23yVnJt9fxFz3=
|
||||
D07
|
||||
kCBNvu4HQfps6h1rgNxGhE32VmpESHebvIB5xjL6xKbIqqRa3x/7KDVBNJvca0gUsqEt5kzYF=
|
||||
88F
|
||||
yf2NBcejpIbcP7BS/g+C6KOowYj+Et26T6GdwFXExUcl80JvoX8yHQOfvJpdiBRbjyB8UqfCa=
|
||||
knm
|
||||
3c7dNuXmhflz/w3aBj32q9ZyGqA1NpHCpLyVAlvSNQ/pat/rGUCPZ9duw4KhUUqEmatQPVFPk=
|
||||
utT
|
||||
ouEZQbMK+i+chOH3AsKCuNDfvCDwirnsSqIJmAgl1lC4de+bsWYCMqN9ei99hOCRUyhZ3g3sr=
|
||||
8RB
|
||||
owVAdcvjZxeIDKALABEBAAHCwHwEGAEIACYCGwwWIQRc5gl5cC8oW/Mo+bEWHAh1giNZ9wUCY=
|
||||
C32
|
||||
lAUJB4sMmAAKCRAWHAh1giNZ9+Y2B/9rTKZaKviae+ummXNumXcrKvbkAAvfuLpKUn53FlQLm=
|
||||
L6H
|
||||
jB++lJnPWvVSzdZxdv8FiPP3d632XHKUrkQRQM/9byRDXDommi7Qttx7YCkhd4JLVYqJqpnAQ=
|
||||
xI5
|
||||
RMkXiZNWyr1lz8JOM1XvDk1M7sJwPMWews8VOIE03E1nt7AsQGnvHtadgEnQaufrYNX3hFA8S=
|
||||
osO
|
||||
HSnedcys6yrzCSIGCqCD9VHbnMtS4DOv0XJGh2hwc8omzH0KZA517dyKBorJRwadcVauGXDKx=
|
||||
Etv
|
||||
Im4rl94PR/3An1Mj6HeeVVpLqDQ5Jb9J90BahWeQ53FzRa4EQzYCw0nLnxcsT1ZEEP5u
|
||||
=3Dv/1p
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
--------------F97C8ED4878E94675762AE43--
|
||||
|
||||
--FBBl2LNv76z8UkvHhSkT9vLwVwxqV8378--
|
||||
|
||||
--MHEDFShwcX18dyE3X7RXujo5fjpgdjHNM
|
||||
Content-Type: application/pgp-signature; name="OpenPGP_signature.asc"
|
||||
Content-Description: OpenPGP digital signature
|
||||
Content-Disposition: attachment; filename="OpenPGP_signature"
|
||||
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
wsB5BAABCAAjFiEEXOYJeXAvKFvzKPmxFhwIdYIjWfcFAmBciUoFAwAAAAAACgkQFhwIdYIjWfez
|
||||
rgf+NZCibnCUTovpWRVRiiPQtBPGeHUPEwz2xq2zz4AaqrHC2v4mYUIPe6am7INk8fkBLsa8Dj/A
|
||||
UN/28Qh7tNb7JsXtHDT4PIoXszukQ8VIRbe09mSkkP6jR4WzNR166d6n3rSxzHpviOyQldjjpOMr
|
||||
Zl7LxmgGr4ojsgCf6pvurWwCCOGJqbSusrD6JVv6DsmPmmQeBmnlTK/0oG9pnlNkugpNB1WS2K5d
|
||||
RY6+kWkSrxbq95HrgILpHip8Y/+ITWvQocm14PBIAAdW8Hr7iFQLETFJ/KDA+VP19Bt8n4Kitdi8
|
||||
DPqMsV0oOhATqBjnD63AePJ0VWg8R1z6GEK5A+WOpg==
|
||||
=Bc6p
|
||||
-----END PGP SIGNATURE-----
|
||||
|
||||
--MHEDFShwcX18dyE3X7RXujo5fjpgdjHNM--
|
||||
103
pkg/message/testdata/pgp-mime-body-signed-plaintext-with-pubkey.eml
vendored
Normal file
103
pkg/message/testdata/pgp-mime-body-signed-plaintext-with-pubkey.eml
vendored
Normal file
@ -0,0 +1,103 @@
|
||||
Content-Type: multipart/signed; micalg=pgp-sha256;
|
||||
protocol="application/pgp-signature";
|
||||
boundary="x4FrOFG2PnNJvlbzxe80NPwxzh2yUHABp"
|
||||
|
||||
This is an OpenPGP/MIME signed message (RFC 4880 and 3156)
|
||||
--x4FrOFG2PnNJvlbzxe80NPwxzh2yUHABp
|
||||
Content-Type: multipart/mixed; boundary="bBln6dwDJTLkin5LPHkHBQudqRLwIzTUH";
|
||||
protected-headers="v1"
|
||||
Subject: simple plaintext body
|
||||
From: "pm.bridge.qa" <pm.bridge.qa@gmail.com>
|
||||
To: schizofrenic@pm.me
|
||||
Message-ID: <adb5ac5d-b8f6-c9a3-5cc0-0fb2e9677512@gmail.com>
|
||||
|
||||
--bBln6dwDJTLkin5LPHkHBQudqRLwIzTUH
|
||||
Content-Type: multipart/mixed;
|
||||
boundary="------------1B34C666A4C2FB03E0324F1A"
|
||||
Content-Language: en-US
|
||||
|
||||
This is a multi-part message in MIME format.
|
||||
--------------1B34C666A4C2FB03E0324F1A
|
||||
Content-Type: text/plain; charset=utf-8; format=flowed
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
Why don't crabs give to charity? Because they're shellfish.
|
||||
|
||||
|
||||
|
||||
--------------1B34C666A4C2FB03E0324F1A
|
||||
Content-Type: application/pgp-keys;
|
||||
name="OpenPGP_0x161C0875822359F7.asc"
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
Content-Disposition: attachment;
|
||||
filename="OpenPGP_0x161C0875822359F7.asc"
|
||||
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
xsBNBFxlUPwBCACx954Ey4SD88f8DSKFw9BaZNXrNwYxNYSgqaqOGHQ0WllF3mstEhTfuxxCZ=
|
||||
pDh
|
||||
I5IhWCXUNxanzsFkn88mRDwFRVl2sf2aAG4/P/p1381oh2kd0UElMRQaQGzoCadQMaQOL9WYT=
|
||||
f4S
|
||||
PWSCzjrPyKgjq5FbqjbF/ndu376na9L+tnsEXyL6RrI6aZhjWG73xlqxS65dzTIYzsyM/P97x=
|
||||
Snd
|
||||
NvlvWtGvLlpFkzxfAEGpVzfOYVYFKoc8rGmUDwrDWYfk5JczRDDogJnY+BNMZf9pjSqk6rTyB=
|
||||
OfN
|
||||
H5fpU8r7A5Q7l+HVakvMUQ9DzDWJtg2ru1Y8hexnJOF68avO4+a1ABEBAAHNKEJyaWRnZSBLe=
|
||||
XUt
|
||||
RWh5aiA8cG0uYnJpZGdlLnFhQGdtYWlsLmNvbT7CwJQEEwEIAD4CGwMFCwkIBwIGFQoJCAsCB=
|
||||
BYC
|
||||
AwECHgECF4AWIQRc5gl5cC8oW/Mo+bEWHAh1giNZ9wUCYC32ygUJB4sMzgAKCRAWHAh1giNZ9=
|
||||
/K8
|
||||
B/4qs84Ii/zKH+q+C8vwO4jUJkOM73qD0pgB7zBs651zWbpgopyol1YUKNpFaHlx/Qch7RDI7=
|
||||
Vcz
|
||||
1+60/KZJSJR19/N2EDVbCUdh8ueioUp9X/218YWV2TRJNxTnljd4FAn7smZnXuP1TsLjQ6sKO=
|
||||
V0U
|
||||
u6JoiG6LZFXqDgxYpA++58Rkl6xaY6R71VkmVQlbEKtubX9AjHydq97Y+Jvn11XzWZaKhv4L7=
|
||||
6Pa
|
||||
4tMKXvvrKh1oywMmh6mZJo+5ZA/ABTkr45cwlTPYqGTS9+uvOHt+PH/oYwwJB4ls2cIAUldSj=
|
||||
TVQ
|
||||
IsseYz3LlbcCfKJiiCFxeHOQXA5J6zNLKOT58TsczsBNBFxlUPwBCADh2HsX23yVnJt9fxFz3=
|
||||
D07
|
||||
kCBNvu4HQfps6h1rgNxGhE32VmpESHebvIB5xjL6xKbIqqRa3x/7KDVBNJvca0gUsqEt5kzYF=
|
||||
88F
|
||||
yf2NBcejpIbcP7BS/g+C6KOowYj+Et26T6GdwFXExUcl80JvoX8yHQOfvJpdiBRbjyB8UqfCa=
|
||||
knm
|
||||
3c7dNuXmhflz/w3aBj32q9ZyGqA1NpHCpLyVAlvSNQ/pat/rGUCPZ9duw4KhUUqEmatQPVFPk=
|
||||
utT
|
||||
ouEZQbMK+i+chOH3AsKCuNDfvCDwirnsSqIJmAgl1lC4de+bsWYCMqN9ei99hOCRUyhZ3g3sr=
|
||||
8RB
|
||||
owVAdcvjZxeIDKALABEBAAHCwHwEGAEIACYCGwwWIQRc5gl5cC8oW/Mo+bEWHAh1giNZ9wUCY=
|
||||
C32
|
||||
lAUJB4sMmAAKCRAWHAh1giNZ9+Y2B/9rTKZaKviae+ummXNumXcrKvbkAAvfuLpKUn53FlQLm=
|
||||
L6H
|
||||
jB++lJnPWvVSzdZxdv8FiPP3d632XHKUrkQRQM/9byRDXDommi7Qttx7YCkhd4JLVYqJqpnAQ=
|
||||
xI5
|
||||
RMkXiZNWyr1lz8JOM1XvDk1M7sJwPMWews8VOIE03E1nt7AsQGnvHtadgEnQaufrYNX3hFA8S=
|
||||
osO
|
||||
HSnedcys6yrzCSIGCqCD9VHbnMtS4DOv0XJGh2hwc8omzH0KZA517dyKBorJRwadcVauGXDKx=
|
||||
Etv
|
||||
Im4rl94PR/3An1Mj6HeeVVpLqDQ5Jb9J90BahWeQ53FzRa4EQzYCw0nLnxcsT1ZEEP5u
|
||||
=3Dv/1p
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
--------------1B34C666A4C2FB03E0324F1A--
|
||||
|
||||
--bBln6dwDJTLkin5LPHkHBQudqRLwIzTUH--
|
||||
|
||||
--x4FrOFG2PnNJvlbzxe80NPwxzh2yUHABp
|
||||
Content-Type: application/pgp-signature; name="OpenPGP_signature.asc"
|
||||
Content-Description: OpenPGP digital signature
|
||||
Content-Disposition: attachment; filename="OpenPGP_signature"
|
||||
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
wsB5BAABCAAjFiEEXOYJeXAvKFvzKPmxFhwIdYIjWfcFAmBa9YIFAwAAAAAACgkQFhwIdYIjWfem
|
||||
vQgAjUMAaxL7D6fRtFBqLjdQGr7PkDBigeQD9ax17CJFld7Zfo2dAYUzYJRi0HP0Kn1YCSBppF0w
|
||||
5/P8458H2sqfPC32ptbDCZ/seL0Rpt/gRx6yufbz7wQC0iUZxqxBq2Ox9PGZYSCrTO837lAVYxUo
|
||||
aMnDL/K9ohAGIyTZVv31z+r3LLWQsFpfpB5hJFqsjQXA9IGKSQIkWbaeE+0wveJSwqxdTwYvsHs2
|
||||
xjBw+s8tRHO/whP4pvzL185fGsHAb8x9a9oyoDVcszhw5xBpiWW37mI58qkQ6g+4wTarreuXGTp3
|
||||
RKgPupoYOMJja90yh3TWovcmuZz6QOgne5Rbn3s+Vg==
|
||||
=hUb8
|
||||
-----END PGP SIGNATURE-----
|
||||
|
||||
--x4FrOFG2PnNJvlbzxe80NPwxzh2yUHABp--
|
||||
43
pkg/message/testdata/pgp-mime-body-signed-plaintext.eml
vendored
Normal file
43
pkg/message/testdata/pgp-mime-body-signed-plaintext.eml
vendored
Normal file
@ -0,0 +1,43 @@
|
||||
Content-Type: multipart/signed; micalg=pgp-sha256;
|
||||
protocol="application/pgp-signature";
|
||||
boundary="M2HYr2fNKsmidMKeWqsSlKJaGCe2l1guZ"
|
||||
|
||||
This is an OpenPGP/MIME signed message (RFC 4880 and 3156)
|
||||
--M2HYr2fNKsmidMKeWqsSlKJaGCe2l1guZ
|
||||
Content-Type: multipart/mixed; boundary="ijQgYCMAVOgOyTMqn30h68dd5lQKbMzCn";
|
||||
protected-headers="v1"
|
||||
Subject: plain body no pubkey
|
||||
From: "pm.bridge.qa" <pm.bridge.qa@gmail.com>
|
||||
To: schizofrenic@pm.me
|
||||
Message-ID: <7414d726-2f14-54bf-3abe-75805aa6cc7f@gmail.com>
|
||||
|
||||
--ijQgYCMAVOgOyTMqn30h68dd5lQKbMzCn
|
||||
Content-Type: text/plain; charset=utf-8; format=flowed
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
Content-Language: en-US
|
||||
|
||||
Why do seagulls fly over the ocean?
|
||||
|
||||
Because if they flew over the bay, we'd call them bagels.
|
||||
|
||||
|
||||
|
||||
--ijQgYCMAVOgOyTMqn30h68dd5lQKbMzCn--
|
||||
|
||||
--M2HYr2fNKsmidMKeWqsSlKJaGCe2l1guZ
|
||||
Content-Type: application/pgp-signature; name="OpenPGP_signature.asc"
|
||||
Content-Description: OpenPGP digital signature
|
||||
Content-Disposition: attachment; filename="OpenPGP_signature"
|
||||
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
wsB5BAABCAAjFiEEXOYJeXAvKFvzKPmxFhwIdYIjWfcFAmBa+F4FAwAAAAAACgkQFhwIdYIjWfew
|
||||
6wf/Ts05KX3py8C2L3FPKkdNf+Ci1hd5aE7ARM8Zp5l0cFuuf6M3+Lud94VKYonoayNu5XfSGoyA
|
||||
OO1HtpW+8hf5A+KSnyh8jp2dA/aLnU1RPZsfEN2cmgamMd6NyTL5cpYuAfxcSmWT79xeCcxPcjor
|
||||
GtrVAojN1tkP2bynYzNI09uygWXzfzgB5f25povN2pAj7DFMAqRKf9bt3nZxO1wIh/aKHoEyjU3w
|
||||
tO2AEKnn7dUnPS37wKomZr/LI1ZbNSLBJ+Gaan4w5c92gfEixttEuHXq2GwkJzJq6SInrxmyZQdl
|
||||
dGR/kiAy9wFwQlErhyjI5lTtd12y3XNTyhaO5cS0bQ==
|
||||
=Th/B
|
||||
-----END PGP SIGNATURE-----
|
||||
|
||||
--M2HYr2fNKsmidMKeWqsSlKJaGCe2l1guZ--
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user