Compare commits

...

49 Commits

Author SHA1 Message Date
5ad23715ec Other: Release Bridge Iron v1.7.0 2021-04-15 13:27:05 +02:00
8ab05a000c GODT-1136 DB Cache header from builder and test 2021-04-15 09:51:08 +00:00
454d248819 GODT-213: Preserve contenttype for undecryptable message body 2021-04-15 09:51:08 +00:00
6c8e5f7cd3 GODT-213: Use application/octet-stream for encrypted parts 2021-04-15 09:51:08 +00:00
f5aba717b2 GODT-213: Force no transfer encoding for embedded message/rfc822 parts 2021-04-15 09:51:08 +00:00
1359c39bc0 GODT-213: Remove dead code GetRelatedHeader/GetRelatedBoundary 2021-04-15 09:51:08 +00:00
4850681f1d GODT-213: correctly expect text/plain in custom message text parts 2021-04-15 09:51:08 +00:00
aa55c69307 Other: fix linter 2021-04-15 09:51:08 +00:00
1f19d4df75 GODT-213: Force text/plain for custom message text part 2021-04-15 09:51:08 +00:00
c0f6af9eb5 GODT-213: Complex external encrypted tests (multipart/alternative, message/rfc822 attachment) 2021-04-15 09:51:08 +00:00
ef6a3d4999 GODT-213: Add comments for newly added code 2021-04-15 09:51:08 +00:00
50550d42b4 GODT-213: Message Builder 2021-04-15 09:51:08 +00:00
8db89a1a6c GODT-1113: Fix tray icon size on macOS Big Sur.
Add patched libqcocoa based on Qt 5.13.0
2021-04-15 09:08:19 +00:00
ba1dfb1bf4 GODT-947 Force colors in logs 2021-04-15 07:20:53 +00:00
d243880753 Other: stop rejecting old TLS versions 2021-04-14 09:28:31 +02:00
cccaaa3d82 Other: turn off bad login in live test 2021-04-12 06:16:34 +02:00
2d95f21567 Other: add straightforward linters 2021-04-08 16:09:40 +02:00
7d0af7624c Other: Bump linter 2021-04-07 10:54:09 +02:00
2f35c453a1 Other: Release notes stable 2021-04-01 08:05:04 +02:00
05dd137bc8 Other: Release notes 2021-03-31 06:52:00 +02:00
767628946f Other: Bridge HZM 1.6.9 2021-03-29 12:08:46 +02:00
d4efa7131f GODT-1121 Initial value of silent updates toggle button 2021-03-29 06:15:33 +02:00
144cf6e40c Other: Bridge HZM 1.6.8 & Import-Export Farg 1.3.3 2021-03-26 11:17:01 +01:00
a205d8c046 GODT-1120 hotfix: use Info level in internal/app logs 2021-03-25 11:33:32 +01:00
cccadaee42 Other: Bridge HZM 1.6.7 & Import-Export Farg 1.3.2 2021-03-24 15:11:46 +01:00
bbb365f8a5 Merge branch 'release/farg' into devel 2021-03-24 14:55:55 +01:00
1f18d9d917 GODT-1117 Do not change updates location for Bridge now 2021-03-24 10:45:55 +01:00
59e0d63485 GODT-1105 Fix: Dylib hijack vulnerability found by https://objective-see.com/products/dhs.html 2021-03-24 08:37:30 +00:00
72fe5a636e GODT-1063: Add metainfo to launcher
Refactor metainfo file a bit
2021-03-24 07:04:28 +00:00
45a83133ba Other: increase SMTP line limit to 2^16 2021-03-17 11:45:54 +01:00
215eb4d6eb GODT-1085 Ignore live test of importing to sent and custom label 2021-03-17 08:10:50 +01:00
479b951c50 GODT-1076 Fix UIDPLUS response for importing existing message 2021-03-16 11:55:36 +00:00
a94c8a943f GODT-1077 IMAP sync counting 2021-03-16 12:35:36 +01:00
ea306f405e Other: print address mode in info level 2021-03-12 09:02:54 +01:00
1b405506b8 Merge remote-tracking branch 'remotes/origin/release-notes' into farg 2021-03-11 00:01:21 +01:00
38c6132f81 Other: Import-Export Farg 1.3.1 2021-03-11 00:00:40 +01:00
b7351dfaf8 Other 2021-03-10 21:52:52 +00:00
7e8f6943f2 Other 2021-03-10 21:35:31 +00:00
a0132e8440 GODT-1047 No silent updates for Import-Export app 2021-03-10 18:56:55 +00:00
27541784aa Merge master into devel 2021-03-10 14:52:45 +01:00
9e567f08b2 Other: release notes for 1.6.6 stable 2021-03-04 11:56:11 +00:00
bf274f984e Other: include latest go.mod/go.sum changes 2021-03-04 11:25:33 +00:00
3b60bbe13b Other 2021-03-04 09:50:29 +00:00
a73a1b623a GODT-803 Fix import to wrong target address 2021-03-02 16:02:23 +01:00
c0a8877018 Other: include latest go.mod/go.sum changes 2021-03-01 17:48:22 +01:00
904166c01c GODT-247 Cache and update files moved from user's cache to config 2021-03-01 14:06:58 +00:00
4761bc935a GODT-948 Embedded messages 2021-03-01 09:22:08 +00:00
71301d891f Other 2021-02-28 20:55:23 +01:00
d47be3c4c0 GODT-1043 Fix showing long login error in GUI dialog 2021-02-26 12:21:12 +00:00
153 changed files with 5125 additions and 1474 deletions

View File

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

View File

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

View File

@ -10,8 +10,8 @@ TARGET_OS?=${GOOS}
.PHONY: build build-ie build-nogui build-ie-nogui build-launcher build-launcher-ie versioner hasher
# 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
View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -93,7 +93,7 @@ func (c *appleMail) Configure(imapPort, smtpPort int, imapSSL, smtpSSL bool, use
})()
// Make sure the file is only readable for the current user.
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
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,45 @@
#define STRINGIZE_(x) #x
#define STRINGIZE(x) STRINGIZE_(x)
IDI_ICON1 ICON DISCARDABLE STRINGIZE(ICO_FILE)
#if defined BUILD_BRIDGE
#define FILE_COMMENTS "The Bridge is an application that runs on your computer in the background and seamlessly encrypts and decrypts your mail as it enters and leaves your computer."
#define FILE_DESCRIPTION "ProtonMail Bridge"
#define INTERNAL_NAME STRINGIZE(EXE_NAME)
#define PRODUCT_NAME "ProtonMail Bridge for Windows"
#elif defined BUILD_IE
#define FILE_COMMENTS "The Import-Export app helps you to migrate your emails from local files or remote IMAP servers to ProtonMail or simply export emails to local folder."
#define FILE_DESCRIPTION "ProtonMail Import-Export app"
#define INTERNAL_NAME STRINGIZE(EXE_NAME)
#define PRODUCT_NAME "ProtonMail Import-Export app for Windows"
#else
#error No target specified
#endif
#define LEGAL_COPYRIGHT "(C) " STRINGIZE(YEAR) " Proton Technologies AG"
1 VERSIONINFO
FILEVERSION FILE_VERSION_COMMA,0
PRODUCTVERSION FILE_VERSION_COMMA,0
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904b0"
BEGIN
VALUE "Comments", FILE_COMMENTS
VALUE "CompanyName", "Proton Technologies AG"
VALUE "FileDescription", FILE_DESCRIPTION
VALUE "FileVersion", STRINGIZE(FILE_VERSION)
VALUE "InternalName", INTERNAL_NAME
VALUE "LegalCopyright", LEGAL_COPYRIGHT
VALUE "OriginalFilename", STRINGIZE(ORIGINAL_FILE_NAME)
VALUE "ProductName", PRODUCT_NAME
VALUE "ProductVersion", STRINGIZE(PRODUCT_VERSION)
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x0409, 0x04B0
END
END

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,65 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package imap
import (
"fmt"
"sync"
)
// msgBuildCountHistogram is used to analyse and log the number of repetitive
// downloads of requested messages per one fetch. The number of builds per each
// messageID is stored in persistent database. The msgBuildCountHistogram will
// take this number for each message in ongoing fetch and create histogram of
// repeats.
//
// Example: During `fetch 1:300` there were
// - 100 messages were downloaded first time
// - 100 messages were downloaded second time
// - 99 messages were downloaded 10th times
// - 1 messages were downloaded 100th times.
type msgBuildCountHistogram struct {
// Key represents how many times message was build.
// Value stores how many messages are build X times based on the key.
counts map[uint32]uint32
lock sync.Locker
}
func newMsgBuildCountHistogram() *msgBuildCountHistogram {
return &msgBuildCountHistogram{
counts: map[uint32]uint32{},
lock: &sync.Mutex{},
}
}
func (c *msgBuildCountHistogram) String() string {
res := ""
for nRebuild, counts := range c.counts {
if res != "" {
res += ", "
}
res += fmt.Sprintf("[%d]:%d", nRebuild, counts)
}
return res
}
func (c *msgBuildCountHistogram) add(nRebuild uint32) {
c.lock.Lock()
defer c.lock.Unlock()
c.counts[nRebuild]++
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -78,7 +78,7 @@ type VersionInfo struct {
// "...": {
// ...
// }
// }
// }.
type VersionMap map[string]VersionInfo
// getVersionFileURL returns the URL of the version file.

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View 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
View 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
}

View 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
View 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
View 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, ", ")
}

View 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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

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

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

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

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

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

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

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

View 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