Compare commits

...

61 Commits

Author SHA1 Message Date
91dcb2f773 Other: Bridge James 1.8.9 2021-09-01 12:20:59 +02:00
c676c732ab Merge branch 'release-notes' into devel 2021-09-01 12:13:20 +02:00
444f2d8a12 Other 2021-09-01 07:45:48 +00:00
f10da3c7f0 GODT-1263: Fix crash on invalid or empty header 2021-08-27 13:39:34 +00:00
b8dd9f82bd GODT-1235: Fix 401 response error handling 2021-08-27 12:29:42 +00:00
1157e60972 GODT-1261: Fix building messages with long key 2021-08-16 15:47:14 +02:00
e9e4d8c725 Other: use windows-compatible filename when dumping message in QA builds 2021-08-04 13:05:57 +02:00
186fa24106 Other: Bridge James v 1.8.8 2021-07-21 06:45:47 +02:00
63780b7b8d GODT-1234 Set attachment name 'message.eml' for message/rfc822 attachments. 2021-07-19 14:40:55 +02:00
e3e4769d78 Other: remove dead code 2021-07-19 07:45:56 +02:00
b2e9c4e4e9 Other: Revert "GODT-1224: don't strip trailing newlines from message bodies"
This reverts commit 54161e263fa2f95795fc8623e9dfd2134afb0ae5.
2021-07-19 07:45:29 +02:00
db4cb36538 GODT-1224: don't strip trailing newlines from message bodies 2021-07-19 07:45:12 +02:00
984864553e Other: better keychain logging 2021-07-19 07:44:58 +02:00
2707a5627c Other: prefer empty string check vs nil check 2021-06-25 15:34:48 +02:00
8e0d6d41a6 Other 2021-06-23 08:40:15 +00:00
2b76a45e17 Other 2021-06-21 20:59:37 +00:00
7ab54da8c4 Other: Bridge James 1.8.7 2021-06-17 13:54:43 +02:00
2cce29e858 GODT-1201: bump gopenpgp to 2.1.10 and update crypto time 2021-06-17 05:32:17 +00:00
ef1223391b GODT-1193: Don't doubly encode parts 2021-06-17 07:04:43 +02:00
fb98a797ba Merge branch 'release/james' into devel 2021-06-15 15:56:31 +02:00
6be31bdeb2 Other go.mod 2021-06-15 15:55:02 +02:00
fce5990d19 Other: release notes 1.8.5 2021-06-15 08:57:59 +02:00
1d4ee0c33e Other: Bridge 1.8.6 2021-06-14 09:07:50 +02:00
a3e102e456 GODT-1166: Do not expose current auth token in interface 2021-06-11 17:13:26 +02:00
21dcac9fac GODT-1187: Remove IMAP/SMTP blocking when no internet. 2021-06-11 09:16:47 +00:00
f0ee82fdd2 GODT-1166: Preliminary disable IMAP/SMTP blocking feature. 2021-06-11 09:16:47 +00:00
0c6a098af9 GODT-1166: Reduce the number of auth for live test
- Changed: Do not reauth controller clients.
- Changed: Verbosisty is set only once before run
- Changed: AddUser takes TestAccount as argument
- Added: Setup/clean up before/after test run
- Added: Access to the current refresh token from pmapi.Client interface.
- Added: Context function to add test a user to bridge without login, just call users.FinishLogin.
- Added: PMAPIController.GetAuthClient returns authenticated client for username.
- Added: Persistent clients does not loggout after every scenario.
- Changed: Disabled no-internet tests.
2021-06-11 09:16:47 +00:00
5bf359d34f GODT-1193: don't use message.Read; permit non-UTF-8 charsets 2021-06-11 06:43:21 +00:00
6d784f2444 Other: release notes 1.8.5 2021-06-11 08:33:54 +02:00
835bf1e77f Other: make changelog linter happy 2021-06-09 09:51:31 +02:00
df5fbda72f Other: Bridge James 1.8.5 2021-06-08 23:54:31 +02:00
c482f768d9 GODT-1189 GODT-1190 GODT-1191 Fix missing sender while creating draft. 2021-06-08 09:09:32 +02:00
21cf7459c9 Other: Bridge 1.8.4 Changelog 2021-06-02 11:14:19 +02:00
cf1ba6588a GODT-949: Fix section parsing issue 2021-06-02 05:56:17 +02:00
858f2c7f29 Other: add (failing) bodystructure test 2021-06-01 11:01:14 +00:00
f63238faed Other: stuff mostly passes but bodystructure parse is broken? 2021-06-01 11:01:14 +00:00
f6ff85f69d GODT-1184: Preserve signatures in externally signed messages 2021-06-01 11:01:14 +00:00
ec5b5939b9 GODT-949: Add comment about ignoring InvalidMediaParameter 2021-06-01 09:04:05 +00:00
dec00ff9cc GODT-949: Ignore some InvalidMediaParameter errors in lite parser 2021-06-01 10:54:01 +02:00
9fddd77f0d GODT-1183: Add test for getting contact emails by email 2021-05-31 12:16:26 +00:00
aae65c9d38 Other: fix license year in QML 2021-05-31 13:48:01 +02:00
f3b197fa56 Merge branch 'release/james' into devel 2021-05-28 10:18:46 +02:00
0a9ce5f526 GODT-1155: zero out mailbox password during logout 2021-05-27 16:48:45 +02:00
a2029002c4 GODT-1155 Update gopenpgp and use go-srp 2021-05-27 16:43:44 +02:00
7c41c8e23a Other: Bridge James 1.8.3 2021-05-27 15:13:04 +02:00
36fdb88d96 GODT-1182: use correct contacts route 2021-05-27 14:15:00 +02:00
c69239ca16 Other: bump go-rfc5322 dependency to v0.8.0 2021-05-27 11:03:49 +02:00
e10aa89313 Other: Bridge James 1.8.3 2021-05-26 17:21:33 +02:00
d0a97a3f4a GODT-1044: fix header lines parsing 2021-05-26 14:48:46 +00:00
e01dc77a61 GODT-1044: lite parser 2021-05-26 14:48:46 +00:00
509ba52ba2 GODT-1162: Fix wrong section 1 error when email has no MIME parts 2021-05-26 13:10:05 +00:00
c37a0338c5 Other: Release notes 1.8.2 2021-05-26 13:33:10 +02:00
9f23d5a6f4 Merge branch 'release/james' into devel 2021-05-26 09:42:43 +02:00
3f50bf66f4 Merge branch 'release/james' into devel 2021-05-20 08:45:23 +02:00
233c55ab19 Other: release notes Bridge James v1.8.1 2021-05-20 08:21:56 +02:00
cb30dd91e3 Other: fix no internet integration test 2021-05-19 08:45:59 +00:00
41d82e10f9 Other: Bridge James v1.8.0 release notes 2021-05-19 10:00:18 +02:00
8496c9e181 Other: bump SMTP test timeout time from 1 to 2 seconds, fingers crossed 2021-05-18 16:55:03 +02:00
3dadad5131 GODT-1161: Guarantee order of responses when creating new message 2021-05-17 17:54:28 +02:00
00146e7474 Other: Bridge James 1.8.0 release notes 2021-05-12 09:04:37 +02:00
12ac47e949 Other: fix typos regarding listener 2021-05-10 15:51:47 +02:00
122 changed files with 3855 additions and 2049 deletions

View File

@ -187,77 +187,6 @@ build-ie-darwin-qa:
paths:
- ie_*.tgz
.build-windows-base:
extends: .build-base
services:
- docker:dind
variables:
DOCKER_HOST: tcp://docker:2375
build-windows:
extends: .build-windows-base
script:
# We need to install docker because qtdeploy builds for windows inside a docker container.
# Docker will connect to the dockerd daemon provided by the runner service docker:dind at tcp://docker:2375.
- curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh
- apt-get update && apt-get -y install binutils-mingw-w64 tar gzip
- ln -s /usr/bin/x86_64-w64-mingw32-windres /usr/bin/windres
- go mod download
- TARGET_OS=windows make build
artifacts:
name: "bridge-windows-$CI_COMMIT_SHORT_SHA"
paths:
- bridge_*.tgz
build-windows-qa:
extends: .build-windows-base
only:
- web
script:
# We need to install docker because qtdeploy builds for windows inside a docker container.
# Docker will connect to the dockerd daemon provided by the runner service docker:dind at tcp://docker:2375.
- curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh
- apt-get update && apt-get -y install binutils-mingw-w64 tar gzip
- ln -s /usr/bin/x86_64-w64-mingw32-windres /usr/bin/windres
- go mod download
- TARGET_OS=windows BUILD_TAGS="build_qa" make build
artifacts:
name: "bridge-windows-qa-$CI_COMMIT_SHORT_SHA"
paths:
- bridge_*.tgz
build-ie-windows:
extends: .build-windows-base
script:
# We need to install docker because qtdeploy builds for windows inside a docker container.
# Docker will connect to the dockerd daemon provided by the runner service docker:dind at tcp://docker:2375.
- curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh
- apt-get update && apt-get -y install binutils-mingw-w64 tar gzip
- ln -s /usr/bin/x86_64-w64-mingw32-windres /usr/bin/windres
- go mod download
- TARGET_OS=windows make build-ie
artifacts:
name: "ie-windows-$CI_COMMIT_SHORT_SHA"
paths:
- ie_*.tgz
build-ie-windows-qa:
extends: .build-windows-base
only:
- web
script:
# We need to install docker because qtdeploy builds for windows inside a docker container.
# Docker will connect to the dockerd daemon provided by the runner service docker:dind at tcp://docker:2375.
- curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh
- apt-get update && apt-get -y install binutils-mingw-w64 tar gzip
- ln -s /usr/bin/x86_64-w64-mingw32-windres /usr/bin/windres
- go mod download
- TARGET_OS=windows BUILD_TAGS="build_qa" make build-ie
artifacts:
name: "ie-windows-qa-$CI_COMMIT_SHORT_SHA"
paths:
- ie_*.tgz
# Stage: MIRROR
mirror-repo:

View File

@ -2,6 +2,71 @@
Changelog [format](http://keepachangelog.com/en/1.0.0/)
## [Bridge 1.8.9] James
### Fixed
* GODT-1263: Fix crash on invalid or empty header.
* GODT-1235: Fix 401 response error handling.
* GODT-1261: Fix building messages with long key.
* Other: use windows-compatible filename when dumping message in QA builds.
## [Bridge 1.8.8] James
### Changed
* GODT-1234 Set attachment name 'message.eml' for `message/rfc822` attachments.
## [Bridge 1.8.7] James
### Changed
* GODT-1201: Update gopenpgp to 2.1.10.
### Fixed
* GODT-1193: Do not doubly encode parts.
## [Bridge 1.8.6] James
### Removed
* GODT-1187: Remove IMAP/SMTP blocking when no internet.
### Changed
* GODT-1166: Reduce the number of auth for live test.
### Fixed
* GODT-1193: Do not use message.Read permit non-UTF-8 charsets.
## [Bridge 1.8.5] James
### Fixed
* GODT-1189: Draft created on Outlook is synced on web.
* GODT-1190: Fix some random crashes of Bridge on Windows.
* GODT-1191: Fix data loss of some drafts messages when restarting outlook on Windows.
## [Bridge 1.8.4] James
### Added
* GODT-1155: Update gopenpgp v2.1.9 and use go-srp.
* GODT-1044: Lite parser for appended messages.
* GODT-1183: Add test for getting contact emails by email.
* GODT-1184: Preserve signatures in externally signed messages.
### Changed
* GODT-949: Ignore some InvalidMediaParameter errors in lite parser.
### Fixed
* GODT-1161: Guarantee order of responses when creating new message.
* GODT-1162: Fix wrong section 1 error when email has no MIME parts.
## [Bridge 1.8.3] James
### Fixed
* GODT-1182: Use correct contact route.
## [Bridge 1.8.2] James
### Fixed
@ -20,11 +85,13 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
* GODT-1056 Check encrypted size of the message before upload.
* GODT-1143 Turn off SMTP server while no connection.
* GODT-1089 Explicitly open system preferences window on BigSur.
* GODT-35: Connection manager with resty.
### Fixed
* GODT-1159 SMTP server not restarting after restored internet.
* GODT-1146 Refactor handling of fetching BODY[HEADER] (and similar) regarding trailing newline.
* GODT-1152 Correctly resolve wildcard sequence/UID set.
* GODT-876 Set default from if empty for importing draft.
* Other: Avoid API jail.

View File

@ -10,7 +10,7 @@ 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.8.2+git
BRIDGE_APP_VERSION?=1.8.9+git
IE_APP_VERSION?=1.3.3+git
APP_VERSION:=${BRIDGE_APP_VERSION}
SRC_ICO:=logo.ico

18
go.mod
View File

@ -1,24 +1,25 @@
module github.com/ProtonMail/proton-bridge
go 1.13
go 1.15
// These dependencies are `replace`d below, so the version numbers should be ignored.
// They are in a separate require block to highlight this.
require (
github.com/docker/docker-credential-helpers v0.6.3
github.com/emersion/go-imap v1.0.6
github.com/jameskeane/bcrypt v0.0.0-20170924085257-7509ea014998
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
github.com/jameskeane/bcrypt v0.0.0-20170924085257-7509ea014998 // indirect
)
require (
github.com/0xAX/notificator v0.0.0-20191016112426-3962a5ea8da1
github.com/Masterminds/semver/v3 v3.1.0
github.com/ProtonMail/go-autostart v0.0.0-20181114175602-c5272053443a
github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7
github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde
github.com/ProtonMail/go-rfc5322 v0.5.0
github.com/ProtonMail/go-rfc5322 v0.8.0
github.com/ProtonMail/go-srp v0.0.0-20210514134713-bd9454f3fa01
github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5
github.com/ProtonMail/gopenpgp/v2 v2.1.3
github.com/ProtonMail/gopenpgp/v2 v2.1.10
github.com/PuerkitoBio/goquery v1.5.1
github.com/abiosoft/ishell v2.0.0+incompatible
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db // indirect
@ -27,7 +28,6 @@ require (
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect
github.com/cucumber/godog v0.8.1
github.com/emersion/go-imap-appendlimit v0.0.0-20190308131241-25671c986a6a
github.com/emersion/go-imap-idle v0.0.0-20200601154248-f05f54664cc4
github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342
github.com/emersion/go-imap-quota v0.0.0-20210203125329-619074823f3c
github.com/emersion/go-imap-unselect v0.0.0-20171113212723-b985794e5f26
@ -64,13 +64,15 @@ require (
github.com/urfave/cli/v2 v2.2.0
github.com/vmihailenco/msgpack/v5 v5.1.3
go.etcd.io/bbolt v1.3.5
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4
golang.org/x/text v0.3.5-0.20201125200606-c27b9fd57aec
)
replace (
github.com/docker/docker-credential-helpers => github.com/ProtonMail/docker-credential-helpers v1.1.0
github.com/emersion/go-imap => github.com/ProtonMail/go-imap v0.0.0-20201228133358-4db68cea0cac
github.com/jameskeane/bcrypt => github.com/ProtonMail/bcrypt v0.0.0-20170924085257-7509ea014998
golang.org/x/crypto => github.com/ProtonMail/crypto v0.0.0-20201112115411-41db4ea0dd1c
github.com/emersion/go-message => github.com/ProtonMail/go-message v0.0.0-20210611055058-fabeff2ec753
github.com/jameskeane/bcrypt => github.com/ProtonMail/bcrypt v0.0.0-20210511135022-227b4adcab57
)

50
go.sum
View File

@ -8,28 +8,30 @@ github.com/CloudyKit/jet/v3 v3.0.0/go.mod h1:HKQPgSJmdK8hdoAbKUUWajkHyHo4RaU5rMd
github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY=
github.com/Masterminds/semver/v3 v3.1.0 h1:Y2lUDsFKVRSYGojLJ1yLxSXdMmMYTYls0rCvoqmMUQk=
github.com/Masterminds/semver/v3 v3.1.0/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/ProtonMail/bcrypt v0.0.0-20170924085257-7509ea014998 h1:YT2uVwQiRQZxCaaahwfcgTq2j3j66w00n/27gb/zubs=
github.com/ProtonMail/bcrypt v0.0.0-20170924085257-7509ea014998/go.mod h1:HecWFHognK8GfRDGnFQbW/LiV7A3MX3gZVs45vk5h8I=
github.com/ProtonMail/crypto v0.0.0-20201112115411-41db4ea0dd1c h1:iaVbEOnskSGgcH7XQWHG6VPirHDRoYe+Idd0/dl4m8A=
github.com/ProtonMail/crypto v0.0.0-20201112115411-41db4ea0dd1c/go.mod h1:Pxr7w4gA2ikI4sWyYwEffm+oew1WAJHzG1SiDpQMkrI=
github.com/ProtonMail/bcrypt v0.0.0-20210511135022-227b4adcab57 h1:pHA4K54ifoogVLunGGHi3xyF5Nz4x+Uh3dJuy3NwGQQ=
github.com/ProtonMail/bcrypt v0.0.0-20210511135022-227b4adcab57/go.mod h1:HecWFHognK8GfRDGnFQbW/LiV7A3MX3gZVs45vk5h8I=
github.com/ProtonMail/docker-credential-helpers v1.1.0 h1:+kvUIpwWcbtP3WFv5sSvkFn/XLzSqPOB5AAthuk9xPk=
github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g=
github.com/ProtonMail/go-autostart v0.0.0-20181114175602-c5272053443a h1:fXK2KsfnkBV9Nh+9SKzHchYjuE9s0vI20JG1mbtEAcc=
github.com/ProtonMail/go-autostart v0.0.0-20181114175602-c5272053443a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
github.com/ProtonMail/go-crypto v0.0.0-20201208171014-cdb7591792e2 h1:pQkjJELHayW59jp7r4G5Dlmnicr5McejDfwsjcwI1SU=
github.com/ProtonMail/go-crypto v0.0.0-20201208171014-cdb7591792e2/go.mod h1:HTM9X7e9oLwn7RiqLG0UVwVRJenLs3wN+tQ0NPAfwMQ=
github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7 h1:YoJbenK9C67SkzkDfmQuVln04ygHj3vjZfd9FL+GmQQ=
github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo=
github.com/ProtonMail/go-imap v0.0.0-20201228133358-4db68cea0cac h1:2xU3QncAiS/W3UlWZTkbNKW5WkLzk6Egl1T0xX+sbjs=
github.com/ProtonMail/go-imap v0.0.0-20201228133358-4db68cea0cac/go.mod h1:yKASt+C3ZiDAiCSssxg9caIckWF/JG7ZQTO7GAmvicU=
github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde h1:5koQozTDELymYOyFbQ/VSubexAEXzDR8qGM5mO8GRdw=
github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde/go.mod h1:795VPXcRUIQ9JyMNHP4el582VokQfippgjkQP3Gk0r0=
github.com/ProtonMail/go-message v0.0.0-20210611055058-fabeff2ec753 h1:I8IsYA297x0QLU80G5I6aLYUu3JYNSpo8j5fkXtFDW0=
github.com/ProtonMail/go-message v0.0.0-20210611055058-fabeff2ec753/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4=
github.com/ProtonMail/go-mime v0.0.0-20190923161245-9b5a4261663a h1:W6RrgN/sTxg1msqzFFb+G80MFmpjMw61IU+slm+wln4=
github.com/ProtonMail/go-mime v0.0.0-20190923161245-9b5a4261663a/go.mod h1:NYt+V3/4rEeDuaev/zw1zCq8uqVEuPHzDPo3OZrlGJ4=
github.com/ProtonMail/go-rfc5322 v0.5.0 h1:LbKWjgfvumYZCr8BgGyTUk3ETGkFLAjQdkuSUpZ5CcE=
github.com/ProtonMail/go-rfc5322 v0.5.0/go.mod h1:mzZWlMWnQJuYLL7JpzuPF5+FimV2lZ9f0jeq24kJjpU=
github.com/ProtonMail/go-rfc5322 v0.8.0 h1:7emrf75n3CDIduQflx7aT1nJa5h/kGsiFKUYX/+IAkU=
github.com/ProtonMail/go-rfc5322 v0.8.0/go.mod h1:BwpTbkJxkMGkc+pC84AXZnwuWOisEULBpfPIyIKS/Us=
github.com/ProtonMail/go-srp v0.0.0-20210514134713-bd9454f3fa01 h1:sRxNvPGnJFh6yWlSr9BpGsSrshFkZLClSm5oIi++a0I=
github.com/ProtonMail/go-srp v0.0.0-20210514134713-bd9454f3fa01/go.mod h1:jOXzdvWTILIJzl83yzi/EZcnnhpI+A/5EyflaeVfi/0=
github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5 h1:Uga1DHFN4GUxuDQr0F71tpi8I9HqPIlZodZAI1lR6VQ=
github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5/go.mod h1:oeP9CMN+ajWp5jKp1kue5daJNwMMxLF+ujPaUIoJWlA=
github.com/ProtonMail/gopenpgp/v2 v2.1.3 h1:4+nFDJ9WtcUQTip/je2Ll3P21XhAUl4asWsafLrw97c=
github.com/ProtonMail/gopenpgp/v2 v2.1.3/go.mod h1:WeYndoqEcRR4/QbgRL24z6OwYX5T1RWerRk8NfZ6rJM=
github.com/ProtonMail/gopenpgp/v2 v2.1.10 h1:WPwpzVQFvzzFIzHCsJ1RzuVkN4JsMIydl/0vc05yU0E=
github.com/ProtonMail/gopenpgp/v2 v2.1.10/go.mod h1:CHIXesUdnPxIxtJTg2P/cxoA0cvUwIBpZIS8SsY82QA=
github.com/PuerkitoBio/goquery v1.5.1 h1:PSPBGne8NIUWw+/7vFBV+kG2J/5MOjbzc7154OaKCSE=
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0=
@ -73,8 +75,6 @@ github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25Kn
github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM=
github.com/emersion/go-imap-appendlimit v0.0.0-20190308131241-25671c986a6a h1:bMdSPm6sssuOFpIaveu3XGAijMS3Tq2S3EqFZmZxidc=
github.com/emersion/go-imap-appendlimit v0.0.0-20190308131241-25671c986a6a/go.mod h1:ikgISoP7pRAolqsVP64yMteJa2FIpS6ju88eBT6K1yQ=
github.com/emersion/go-imap-idle v0.0.0-20200601154248-f05f54664cc4 h1:/JIALzmCduf5o8TWJSiOBzTb9+R0SChwElUrJLlp2po=
github.com/emersion/go-imap-idle v0.0.0-20200601154248-f05f54664cc4/go.mod h1:o14zPKCmEH5WC1vU5SdPoZGgNvQx7zzKSnxPQlobo78=
github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342 h1:5p1t3e1PomYgLWwEwhwEU5kVBwcyAcVrOpexv8AeZx0=
github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342/go.mod h1:QuMaZcKFDVI0yCrnAbPLfbwllz1wtOrZH8/vZ5yzp4w=
github.com/emersion/go-imap-quota v0.0.0-20210203125329-619074823f3c h1:khcEdu1yFiZjBgi7gGnQiLhpSgghJ0YTnKD0l4EUqqc=
@ -83,15 +83,11 @@ github.com/emersion/go-imap-unselect v0.0.0-20171113212723-b985794e5f26 h1:FiSb8
github.com/emersion/go-imap-unselect v0.0.0-20171113212723-b985794e5f26/go.mod h1:+gnnZx3Mg3MnCzZrv0eZdp5puxXQUgGT/6N6L7ShKfM=
github.com/emersion/go-mbox v1.0.2 h1:tE/rT+lEugK9y0myEymCCHnwlZN04hlXPrbKkxRBA5I=
github.com/emersion/go-mbox v1.0.2/go.mod h1:Yp9IVuuOYLEuMv4yjgDHvhb5mHOcYH6x92Oas3QqEZI=
github.com/emersion/go-message v0.11.1/go.mod h1:C4jnca5HOTo4bGN9YdqNQM9sITuT3Y0K6bSUw9RklvY=
github.com/emersion/go-message v0.12.1-0.20201221184100-40c3f864532b h1:xYuhW6egTaCP+zjbUcfoy/Dr3ASdVPR9W7fmkHvZHPE=
github.com/emersion/go-message v0.12.1-0.20201221184100-40c3f864532b/go.mod h1:N1JWdZQ2WRUalmdHAX308CWBq747VJ8oUorFI3VCBwU=
github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-smtp v0.14.0 h1:RYW203p+EcPjL8Z/ZpT9lZ6iOc8MG1MQzEx1UKEkXlA=
github.com/emersion/go-smtp v0.14.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwoTQyw0fIM5xv1HF+Y+3ZijDR839WMulgxCcUY=
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
github.com/emersion/go-vcard v0.0.0-20190105225839-8856043f13c5 h1:n9qx98xiS5V4x2WIpPC2rr9mUM5ri9r/YhCEKbhCHro=
@ -178,9 +174,6 @@ github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL
github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8=
github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/martinlindhe/base36 v1.0.0/go.mod h1:+AtEs8xrBpCeYgSLoY/aJ6Wf37jtBuR0s35750M27+8=
github.com/martinlindhe/base36 v1.1.0 h1:cIwvvwYse/0+1CkUPYH5ZvVIYG3JrILmQEIbLuar02Y=
github.com/martinlindhe/base36 v1.1.0/go.mod h1:+AtEs8xrBpCeYgSLoY/aJ6Wf37jtBuR0s35750M27+8=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
@ -262,7 +255,6 @@ github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/therecipe/qt v0.0.0-20200701200531-7f61353ee73e h1:G0DQ/TRQyrEZjtLlLwevFjaRiG8eeCMlq9WXQ2OO2bk=
github.com/therecipe/qt v0.0.0-20200701200531-7f61353ee73e/go.mod h1:SUUR2j3aE1z6/g76SdD6NwACEpvCxb3fvG82eKbD6us=
github.com/therecipe/qt v0.0.0-20200904063919-c0c124a5770d h1:T+d8FnaLSvM/1BdlDXhW4d5dr2F07bAbB+LpgzMxx+o=
github.com/therecipe/qt/internal/binding/files/docs/5.12.0 v0.0.0-20200904063919-c0c124a5770d h1:hAZyEG2swPRWjF0kqqdGERXUazYnRJdAk4a58f14z7Y=
github.com/therecipe/qt/internal/binding/files/docs/5.12.0 v0.0.0-20200904063919-c0c124a5770d/go.mod h1:7m8PDYDEtEVqfjoUQc2UrFqhG0CDmoVJjRlQxexndFc=
github.com/therecipe/qt/internal/binding/files/docs/5.13.0 v0.0.0-20200904063919-c0c124a5770d h1:AJRoBel/g9cDS+yE8BcN3E+TDD/xNAguG21aoR8DAIE=
@ -292,10 +284,21 @@ github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDf
github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc=
go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0=
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190418165655-df01cb2cc480/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w=
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190910184405-b558ed863381/go.mod h1:p895TfNkDgPEmEQrNiOtIl3j98d/tGU95djDj7NfyjQ=
golang.org/x/mobile v0.0.0-20200801112145-973feb4309de/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
@ -312,13 +315,11 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -327,6 +328,7 @@ golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -334,13 +336,10 @@ golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04 h1:cEhElsAv9LUt9ZUUocxzWe05oFLVd+AA2nstydTeI8g=
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44 h1:Bli41pIlzTzf3KEY06n+xnzK/BESIg2ze4Pgfh/aI8c=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
@ -354,6 +353,7 @@ golang.org/x/tools v0.0.0-20190327201419-c70d86f8b7cf/go.mod h1:LCzVGOaR6xXOjkQ3
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190909214602-067311248421/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69 h1:yBHHx+XZqXJBm6Exke3N7V9gnlsyXxoCPEb1yVenjfk=
golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=

View File

@ -68,7 +68,7 @@ func (f *frontendCLI) loginAccount(c *ishell.Context) { // nolint[funlen]
}
f.Println("Authenticating ... ")
client, auth, err := f.ie.Login(loginName, password)
client, auth, err := f.ie.Login(loginName, []byte(password))
if err != nil {
f.processAPIError(err)
return
@ -96,7 +96,7 @@ func (f *frontendCLI) loginAccount(c *ishell.Context) { // nolint[funlen]
}
f.Println("Adding account ...")
user, err := f.ie.FinishLogin(client, auth, mailboxPassword)
user, err := f.ie.FinishLogin(client, auth, []byte(mailboxPassword))
if err != nil {
log.WithField("username", loginName).WithError(err).Error("Login was unsuccessful")
f.Println("Adding account was unsuccessful:", err)

View File

@ -115,7 +115,7 @@ func (f *frontendCLI) loginAccount(c *ishell.Context) { // nolint[funlen]
}
f.Println("Authenticating ... ")
client, auth, err := f.bridge.Login(loginName, password)
client, auth, err := f.bridge.Login(loginName, []byte(password))
if err != nil {
f.processAPIError(err)
return
@ -143,7 +143,7 @@ func (f *frontendCLI) loginAccount(c *ishell.Context) { // nolint[funlen]
}
f.Println("Adding account ...")
user, err := f.bridge.FinishLogin(client, auth, mailboxPassword)
user, err := f.bridge.FinishLogin(client, auth, []byte(mailboxPassword))
if err != nil {
log.WithField("username", loginName).WithError(err).Error("Login was unsuccessful")
f.Println("Adding account was unsuccessful:", err)

View File

@ -94,7 +94,7 @@ Item {
font.pointSize : Style.main.fontSize * Style.pt
text:
"ProtonMail Bridge "+go.getBackendVersion()+"\n"+
"© 2020 Proton Technologies AG"
"© 2021 Proton Technologies AG"
}
}
Row {

View File

@ -81,7 +81,7 @@ Item {
color: Style.main.textDisabled
horizontalAlignment: Qt.AlignHCenter
font.pointSize : Style.main.fontSize * Style.pt
text: "ProtonMail Import-Export app Version "+go.getBackendVersion()+"\n© 2020 Proton Technologies AG"
text: "ProtonMail Import-Export app Version "+go.getBackendVersion()+"\n© 2021 Proton Technologies AG"
}
}

View File

@ -186,7 +186,7 @@ func (a *Accounts) showLoginError(err error, scope string) bool {
// 2: when has no 2FA but have MBOX
func (a *Accounts) Login(login, password string) int {
var err error
a.authClient, a.auth, err = a.um.Login(login, password)
a.authClient, a.auth, err = a.um.Login(login, []byte(password))
if a.showLoginError(err, "login") {
return -1
}
@ -230,7 +230,7 @@ func (a *Accounts) AddAccount(mailboxPassword string) int {
return -1
}
user, err := a.um.FinishLogin(a.authClient, a.auth, mailboxPassword)
user, err := a.um.FinishLogin(a.authClient, a.auth, []byte(mailboxPassword))
if err != nil {
log.WithError(err).Error("Login was unsuccessful")
a.qml.SetAddAccountWarning("Failure: "+err.Error(), -2)

View File

@ -152,7 +152,7 @@ func (s *FrontendQt) showLoginError(err error, scope string) bool {
// 2: when has no 2FA but have MBOX
func (s *FrontendQt) login(login, password string) int {
var err error
s.authClient, s.auth, err = s.bridge.Login(login, password)
s.authClient, s.auth, err = s.bridge.Login(login, []byte(password))
if s.showLoginError(err, "login") {
return -1
}
@ -195,7 +195,7 @@ func (s *FrontendQt) addAccount(mailboxPassword string) int {
return -1
}
user, err := s.bridge.FinishLogin(s.authClient, s.auth, mailboxPassword)
user, err := s.bridge.FinishLogin(s.authClient, s.auth, []byte(mailboxPassword))
if err != nil {
log.WithError(err).Error("Login was unsuccessful")
s.Qml.SetAddAccountWarning("Failure: "+err.Error(), -2)

View File

@ -49,8 +49,8 @@ type Updater interface {
// UserManager is an interface of users needed by frontend.
type UserManager interface {
Login(username, password string) (pmapi.Client, *pmapi.Auth, error)
FinishLogin(client pmapi.Client, auth *pmapi.Auth, mailboxPassword string) (User, error)
Login(username string, password []byte) (pmapi.Client, *pmapi.Auth, error)
FinishLogin(client pmapi.Client, auth *pmapi.Auth, mailboxPassword []byte) (User, error)
GetUsers() []User
GetUser(query string) (User, error)
DeleteUser(userID string, clearCache bool) error
@ -94,7 +94,7 @@ func NewBridgeWrap(bridge *bridge.Bridge) *bridgeWrap { //nolint[golint]
return &bridgeWrap{Bridge: bridge}
}
func (b *bridgeWrap) FinishLogin(client pmapi.Client, auth *pmapi.Auth, mailboxPassword string) (User, error) {
func (b *bridgeWrap) FinishLogin(client pmapi.Client, auth *pmapi.Auth, mailboxPassword []byte) (User, error) {
return b.Bridge.FinishLogin(client, auth, mailboxPassword)
}
@ -134,7 +134,7 @@ func NewImportExportWrap(ie *importexport.ImportExport) *importExportWrap { //no
return &importExportWrap{ImportExport: ie}
}
func (b *importExportWrap) FinishLogin(client pmapi.Client, auth *pmapi.Auth, mailboxPassword string) (User, error) {
func (b *importExportWrap) FinishLogin(client pmapi.Client, auth *pmapi.Auth, mailboxPassword []byte) (User, error) {
return b.ImportExport.FinishLogin(client, auth, mailboxPassword)
}

View File

@ -0,0 +1,85 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package idle
import (
"bufio"
"errors"
"strings"
"github.com/emersion/go-imap"
"github.com/emersion/go-imap/server"
)
const (
idleCommand = "IDLE" // Capability and Command identificator
doneLine = "DONE"
)
// Handler for IDLE extension.
type Handler struct{}
// Command for IDLE handler.
func (h *Handler) Command() *imap.Command {
return &imap.Command{Name: idleCommand}
}
// Parse for IDLE handler.
func (h *Handler) Parse(fields []interface{}) error {
return nil
}
// Handle the IDLE request.
func (h *Handler) Handle(conn server.Conn) error {
cont := &imap.ContinuationReq{Info: "idling"}
if err := conn.WriteResp(cont); err != nil {
return err
}
// Wait for DONE
scanner := bufio.NewScanner(conn)
scanner.Scan()
if err := scanner.Err(); err != nil {
return err
}
if strings.ToUpper(scanner.Text()) != doneLine {
return errors.New("expected DONE")
}
return nil
}
type extension struct{}
func (ext *extension) Capabilities(c server.Conn) []string {
return []string{idleCommand}
}
func (ext *extension) Command(name string) server.HandlerFactory {
if name != idleCommand {
return nil
}
return func() server.Handler {
return &Handler{}
}
}
func NewExtension() server.Extension {
return &extension{}
}

View File

@ -18,7 +18,9 @@
package imap
import (
"io"
"bufio"
"bytes"
"io/ioutil"
"net/mail"
"strings"
"time"
@ -28,6 +30,7 @@ import (
"github.com/ProtonMail/proton-bridge/pkg/message"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/emersion/go-imap"
"github.com/emersion/go-message/textproto"
"github.com/pkg/errors"
)
@ -43,11 +46,15 @@ func (im *imapMailbox) CreateMessage(flags []string, date time.Time, body imap.L
}, "APPEND", flags, date)
}
func (im *imapMailbox) createMessage(flags []string, date time.Time, body imap.Literal) error { //nolint[funlen]
func (im *imapMailbox) createMessage(imapFlags []string, date time.Time, r imap.Literal) error { //nolint[funlen]
// Called from go-imap in goroutines - we need to handle panics for each function.
defer im.panicHandler.HandlePanic()
m, _, _, readers, err := message.Parse(body)
// NOTE: Is this lock meant to be here?
im.user.appendExpungeLock.Lock()
defer im.user.appendExpungeLock.Unlock()
body, err := ioutil.ReadAll(r)
if err != nil {
return err
}
@ -56,112 +63,91 @@ func (im *imapMailbox) createMessage(flags []string, date time.Time, body imap.L
if addr == nil {
return errors.New("no available address for encryption")
}
m.AddressID = addr.ID
kr, err := im.user.client().KeyRingForAddressID(addr.ID)
if err != nil {
return err
}
// Handle imported messages which have no "Sender" address.
// This sometimes occurs with outlook which reports errors as imported emails or for drafts.
if im.storeMailbox.LabelID() == pmapi.DraftLabel {
return im.createDraftMessage(kr, addr.Email, body)
}
if im.storeMailbox.LabelID() == pmapi.SentLabel {
m, _, _, _, err := message.Parse(bytes.NewReader(body))
if err != nil {
return err
}
if m.Sender == nil {
im.log.Warning("Append: Missing email sender. Will use main address")
m.Sender = &mail.Address{
Name: "",
Address: addr.Email,
m.Sender = &mail.Address{Address: addr.Email}
}
if user, err := im.user.backend.bridge.GetUser(pmapi.SanitizeEmail(m.Sender.Address)); err == nil && user.ID() == im.storeUser.UserID() {
logEntry := im.log.WithField("sender", m.Sender).WithField("extID", m.Header.Get("Message-Id")).WithField("date", date)
if foundUID := im.storeMailbox.GetUIDByHeader(&m.Header); foundUID != uint32(0) {
logEntry.Info("Ignoring APPEND of duplicate to Sent folder")
return uidplus.AppendResponse(im.storeMailbox.UIDValidity(), &uidplus.OrderedSeq{foundUID})
}
logEntry.Info("No matching UID, continuing APPEND to Sent")
}
}
// "Drafts" needs to call special API routes.
// Clients always append the whole message again and remove the old one.
if im.storeMailbox.LabelID() == pmapi.DraftLabel {
// Sender address needs to be sanitised (drafts need to match cases exactly).
m.Sender.Address = pmapi.ConstructAddress(m.Sender.Address, addr.Email)
hdr, err := textproto.ReadHeader(bufio.NewReader(bytes.NewReader(body)))
if err != nil {
return err
}
// Avoid appending a message which is already on the server. Apply the new label instead.
// This always happens with Outlook because it uses APPEND instead of COPY.
internalID := hdr.Get("X-Pm-Internal-Id")
// In case there is a mail client which corrupts headers, try "References" too.
if internalID == "" {
if references := strings.Fields(hdr.Get("References")); len(references) > 0 {
if match := pmapi.RxInternalReferenceFormat.FindStringSubmatch(references[len(references)-1]); len(match) == 2 {
internalID = match[1]
}
}
}
if internalID != "" {
if msg, err := im.storeMailbox.GetMessage(internalID); err == nil {
if im.user.user.IsCombinedAddressMode() || im.storeAddress.AddressID() == msg.Message().AddressID {
return im.labelExistingMessage(msg.ID(), msg.IsMarkedDeleted())
}
}
}
return im.importMessage(kr, hdr, body, imapFlags, date)
}
func (im *imapMailbox) createDraftMessage(kr *crypto.KeyRing, email string, body []byte) error {
im.log.Info("Creating draft message")
m, _, _, readers, err := message.Parse(bytes.NewReader(body))
if err != nil {
return err
}
if m.Sender == nil {
m.Sender = &mail.Address{}
}
m.Sender.Address = pmapi.ConstructAddress(m.Sender.Address, email)
draft, _, err := im.user.storeUser.CreateDraft(kr, m, readers, "", "", "")
if err != nil {
return errors.Wrap(err, "failed to create draft")
}
targetSeq := im.storeMailbox.GetUIDList([]string{draft.ID})
return uidplus.AppendResponse(im.storeMailbox.UIDValidity(), targetSeq)
return uidplus.AppendResponse(im.storeMailbox.UIDValidity(), im.storeMailbox.GetUIDList([]string{draft.ID}))
}
// We need to make sure this is an import, and not a sent message from this account
// (sent messages from the account will be added by the event loop).
if im.storeMailbox.LabelID() == pmapi.SentLabel {
sanitizedSender := pmapi.SanitizeEmail(m.Sender.Address)
// Check whether this message was sent by a bridge user.
user, err := im.user.backend.bridge.GetUser(sanitizedSender)
if err == nil && user.ID() == im.storeUser.UserID() {
logEntry := im.log.WithField("addr", sanitizedSender).WithField("extID", m.Header.Get("Message-Id"))
// If we find the message in the store already, we can skip importing it.
if foundUID := im.storeMailbox.GetUIDByHeader(&m.Header); foundUID != uint32(0) {
logEntry.Info("Ignoring APPEND of duplicate to Sent folder")
return uidplus.AppendResponse(im.storeMailbox.UIDValidity(), &uidplus.OrderedSeq{foundUID})
}
// We didn't find the message in the store, so we are currently sending it.
logEntry.WithField("time", date).Info("No matching UID, continuing APPEND to Sent")
}
}
message.ParseFlags(m, flags)
if !date.IsZero() {
m.Time = date.Unix()
}
internalID := m.Header.Get("X-Pm-Internal-Id")
references := m.Header.Get("References")
referenceList := strings.Fields(references)
// In case there is a mail client which corrupts headers, try
// "References" too.
if internalID == "" && len(referenceList) > 0 {
lastReference := referenceList[len(referenceList)-1]
match := pmapi.RxInternalReferenceFormat.FindStringSubmatch(lastReference)
if len(match) == 2 {
internalID = match[1]
}
}
im.user.appendExpungeLock.Lock()
defer im.user.appendExpungeLock.Unlock()
// Avoid appending a message which is already on the server. Apply the
// new label instead. This always happens with Outlook (it uses APPEND
// instead of COPY).
if internalID != "" {
// Check to see if this belongs to a different address in split mode or another ProtonMail account.
msg, err := im.storeMailbox.GetMessage(internalID)
if err == nil && (im.user.user.IsCombinedAddressMode() || (im.storeAddress.AddressID() == msg.Message().AddressID)) {
IDs := []string{internalID}
// See the comment bellow.
if msg.IsMarkedDeleted() {
if err := im.storeMailbox.MarkMessagesUndeleted(IDs); err != nil {
log.WithError(err).Error("Failed to undelete re-imported internal message")
}
}
err = im.storeMailbox.LabelMessages(IDs)
if err != nil {
return err
}
targetSeq := im.storeMailbox.GetUIDList(IDs)
return uidplus.AppendResponse(im.storeMailbox.UIDValidity(), targetSeq)
}
}
im.log.Info("Importing external message")
if err := im.importMessage(m, readers, kr); err != nil {
im.log.Error("Import failed: ", err)
return err
}
func (im *imapMailbox) labelExistingMessage(messageID string, isDeleted bool) error {
im.log.Info("Labelling existing message")
// IMAP clients can move message to local folder (setting \Deleted flag)
// and then move it back (IMAP client does not remember the message,
@ -170,29 +156,76 @@ func (im *imapMailbox) createMessage(flags []string, date time.Time, body imap.L
// not delete the message (EXPUNGE would delete the original message and
// the new duplicate one would stay). API detects duplicates; therefore
// we need to remove \Deleted flag if IMAP client re-imports.
msg, err := im.storeMailbox.GetMessage(m.ID)
if err == nil && msg.IsMarkedDeleted() {
if err := im.storeMailbox.MarkMessagesUndeleted([]string{m.ID}); err != nil {
if isDeleted {
if err := im.storeMailbox.MarkMessagesUndeleted([]string{messageID}); err != nil {
log.WithError(err).Error("Failed to undelete re-imported message")
}
}
targetSeq := im.storeMailbox.GetUIDList([]string{m.ID})
return uidplus.AppendResponse(im.storeMailbox.UIDValidity(), targetSeq)
if err := im.storeMailbox.LabelMessages([]string{messageID}); err != nil {
return err
}
func (im *imapMailbox) importMessage(m *pmapi.Message, readers []io.Reader, kr *crypto.KeyRing) (err error) {
body, err := message.BuildEncrypted(m, readers, kr)
return uidplus.AppendResponse(im.storeMailbox.UIDValidity(), im.storeMailbox.GetUIDList([]string{messageID}))
}
func (im *imapMailbox) importMessage(kr *crypto.KeyRing, hdr textproto.Header, body []byte, imapFlags []string, date time.Time) error {
im.log.Info("Importing external message")
var (
seen bool
flags int64
labelIDs []string
time int64
)
if hdr.Get("received") == "" {
flags = pmapi.FlagSent
} else {
flags = pmapi.FlagReceived
}
for _, flag := range imapFlags {
switch flag {
case imap.DraftFlag:
flags &= ^pmapi.FlagSent
flags &= ^pmapi.FlagReceived
case imap.SeenFlag:
seen = true
case imap.FlaggedFlag:
labelIDs = append(labelIDs, pmapi.StarredLabel)
case imap.AnsweredFlag:
flags |= pmapi.FlagReplied
}
}
if !date.IsZero() {
time = date.Unix()
}
enc, err := message.EncryptRFC822(kr, bytes.NewReader(body))
if err != nil {
return err
}
labels := []string{}
for _, l := range m.LabelIDs {
if l == pmapi.StarredLabel {
labels = append(labels, pmapi.StarredLabel)
messageID, err := im.storeMailbox.ImportMessage(enc, seen, labelIDs, flags, time)
if err != nil {
return err
}
msg, err := im.storeMailbox.GetMessage(messageID)
if err != nil {
return err
}
if msg.IsMarkedDeleted() {
if err := im.storeMailbox.MarkMessagesUndeleted([]string{messageID}); err != nil {
log.WithError(err).Error("Failed to undelete re-imported message")
}
}
return im.storeMailbox.ImportMessage(m, body, labels)
return uidplus.AppendResponse(im.storeMailbox.UIDValidity(), im.storeMailbox.GetUIDList([]string{messageID}))
}

View File

@ -18,13 +18,11 @@
package imap
import (
"bufio"
"bytes"
"io"
"strings"
"github.com/ProtonMail/proton-bridge/pkg/message"
"github.com/emersion/go-imap"
"github.com/pkg/errors"
)
func filterHeader(header []byte, section *imap.BodySectionName) []byte {
@ -53,7 +51,7 @@ func filterHeader(header []byte, section *imap.BodySectionName) []byte {
func filterHeaderLines(header []byte, wantField func(string) bool) []byte {
var res []byte
for _, line := range headerLines(header) {
for _, line := range message.HeaderLines(header) {
if len(bytes.TrimSpace(line)) == 0 {
res = append(res, line...)
} else {
@ -71,34 +69,3 @@ func filterHeaderLines(header []byte, wantField func(string) bool) []byte {
return res
}
// NOTE: This sucks because we trim and split stuff here already, only to do it again when we use this function!
func headerLines(header []byte) [][]byte {
var lines [][]byte
r := bufio.NewReader(bytes.NewReader(header))
for {
b, err := r.ReadBytes('\n')
if err != nil {
if err != io.EOF {
panic(errors.Wrap(err, "failed to read header line"))
}
break
}
switch {
case len(bytes.TrimSpace(b)) == 0:
lines = append(lines, b)
case len(bytes.SplitN(b, []byte(": "), 2)) != 2:
lines[len(lines)-1] = append(lines[len(lines)-1], b...)
default:
lines = append(lines, b)
}
}
return lines
}

View File

@ -23,63 +23,76 @@ import (
"io"
"net"
"strings"
"sync/atomic"
"time"
imapid "github.com/ProtonMail/go-imap-id"
"github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/config/useragent"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/imap/id"
"github.com/ProtonMail/proton-bridge/internal/imap/idle"
"github.com/ProtonMail/proton-bridge/internal/imap/uidplus"
"github.com/ProtonMail/proton-bridge/internal/serverutil"
"github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/emersion/go-imap"
imapappendlimit "github.com/emersion/go-imap-appendlimit"
imapidle "github.com/emersion/go-imap-idle"
imapmove "github.com/emersion/go-imap-move"
imapquota "github.com/emersion/go-imap-quota"
imapunselect "github.com/emersion/go-imap-unselect"
"github.com/emersion/go-imap/backend"
imapserver "github.com/emersion/go-imap/server"
"github.com/emersion/go-sasl"
"github.com/sirupsen/logrus"
)
type imapServer struct {
// Server takes care of IMAP listening serving. It implements serverutil.Server.
type Server struct {
panicHandler panicHandler
server *imapserver.Server
userAgent *useragent.UserAgent
eventListener listener.Listener
debugClient bool
debugServer bool
port int
isRunning atomic.Value
server *imapserver.Server
controller serverutil.Controller
}
// NewIMAPServer constructs a new IMAP server configured with the given options.
func NewIMAPServer(panicHandler panicHandler, debugClient, debugServer bool, port int, tls *tls.Config, imapBackend backend.Backend, userAgent *useragent.UserAgent, eventListener listener.Listener) *imapServer { // nolint[golint]
s := imapserver.New(imapBackend)
s.Addr = fmt.Sprintf("%v:%v", bridge.Host, port)
s.TLSConfig = tls
s.AllowInsecureAuth = true
s.ErrorLog = newServerErrorLogger("server-imap")
s.AutoLogout = 30 * time.Minute
if debugServer {
fmt.Println("THE LOG WILL CONTAIN **DECRYPTED** MESSAGE DATA")
log.Warning("================================================")
log.Warning("THIS LOG WILL CONTAIN **DECRYPTED** MESSAGE DATA")
log.Warning("================================================")
func NewIMAPServer(
panicHandler panicHandler,
debugClient, debugServer bool,
port int,
tls *tls.Config,
imapBackend backend.Backend,
userAgent *useragent.UserAgent,
eventListener listener.Listener,
) *Server {
server := &Server{
panicHandler: panicHandler,
userAgent: userAgent,
debugClient: debugClient,
debugServer: debugServer,
port: port,
}
server.server = newGoIMAPServer(tls, imapBackend, server.Address(), userAgent)
server.controller = serverutil.NewController(server, eventListener)
return server
}
func newGoIMAPServer(tls *tls.Config, backend backend.Backend, address string, userAgent *useragent.UserAgent) *imapserver.Server {
server := imapserver.New(backend)
server.TLSConfig = tls
server.AllowInsecureAuth = true
server.ErrorLog = serverutil.NewServerErrorLogger(serverutil.IMAP)
server.AutoLogout = 30 * time.Minute
server.Addr = address
serverID := imapid.ID{
imapid.FieldName: "ProtonMail Bridge",
imapid.FieldVendor: "Proton Technologies AG",
imapid.FieldSupportURL: "https://protonmail.com/support",
}
s.EnableAuth(sasl.Login, func(conn imapserver.Conn) sasl.Server {
server.EnableAuth(sasl.Login, func(conn imapserver.Conn) sasl.Server {
return sasl.NewLoginServer(func(address, password string) error {
user, err := conn.Server().Backend.Login(nil, address, password)
if err != nil {
@ -93,8 +106,8 @@ func NewIMAPServer(panicHandler panicHandler, debugClient, debugServer bool, por
})
})
s.Enable(
imapidle.NewExtension(),
server.Enable(
idle.NewExtension(),
imapmove.NewExtension(),
id.NewExtension(serverID, userAgent),
imapquota.NewExtension(),
@ -103,87 +116,35 @@ func NewIMAPServer(panicHandler panicHandler, debugClient, debugServer bool, por
uidplus.NewExtension(),
)
server := &imapServer{
panicHandler: panicHandler,
server: s,
userAgent: userAgent,
eventListener: eventListener,
debugClient: debugClient,
debugServer: debugServer,
port: port,
}
server.isRunning.Store(false)
return server
}
func (s *imapServer) HandlePanic() { s.panicHandler.HandlePanic() }
func (s *imapServer) IsRunning() bool { return s.isRunning.Load().(bool) }
func (s *imapServer) Port() int { return s.port }
// ListenAndServe will run server and all monitors.
func (s *Server) ListenAndServe() { s.controller.ListenAndServe() }
// ListenAndServe starts the server and keeps it on based on internet
// availability.
func (s *imapServer) ListenAndServe() {
serverutil.ListenAndServe(s, s.eventListener)
}
// Close turns off server and monitors.
func (s *Server) Close() { s.controller.Close() }
// ListenRetryAndServe will start listener. If port is occupied it will try
// again after coolDown time. Once listener is OK it will serve.
func (s *imapServer) ListenRetryAndServe(retries int, retryAfter time.Duration) {
if s.IsRunning() {
return
}
s.isRunning.Store(true)
// Implements serverutil.Server interface.
l := log.WithField("address", s.server.Addr)
l.Info("IMAP server is starting")
listener, err := net.Listen("tcp", s.server.Addr)
if err != nil {
s.isRunning.Store(false)
if retries > 0 {
l.WithError(err).WithField("retries", retries).Warn("IMAP listener failed")
time.Sleep(retryAfter)
s.ListenRetryAndServe(retries-1, retryAfter)
return
}
func (Server) Protocol() serverutil.Protocol { return serverutil.IMAP }
func (s *Server) UseSSL() bool { return false }
func (s *Server) Address() string { return fmt.Sprintf("%s:%d", bridge.Host, s.port) }
func (s *Server) TLSConfig() *tls.Config { return s.server.TLSConfig }
func (s *Server) HandlePanic() { s.panicHandler.HandlePanic() }
l.WithError(err).Error("IMAP listener failed")
s.eventListener.Emit(events.ErrorEvent, "IMAP failed: "+err.Error())
return
}
func (s *Server) DebugServer() bool { return s.debugServer }
func (s *Server) DebugClient() bool { return s.debugClient }
err = s.server.Serve(&connListener{
Listener: listener,
server: s,
userAgent: s.userAgent,
})
// Serve returns error every time, even after closing the server.
// User shouldn't be notified about error if server shouldn't be running,
// but it should in case it was not closed by `s.Close()`.
if err != nil && s.IsRunning() {
s.isRunning.Store(false)
l.WithError(err).Error("IMAP server failed")
s.eventListener.Emit(events.ErrorEvent, "IMAP failed: "+err.Error())
return
}
defer s.server.Close() //nolint[errcheck]
func (s *Server) SetLoggers(localDebug, remoteDebug io.Writer) {
s.server.Debug = imap.NewDebugWriter(localDebug, remoteDebug)
l.Info("IMAP server stopped")
}
// Stops the server.
func (s *imapServer) Close() {
if !s.IsRunning() {
return
}
s.isRunning.Store(false)
log.Info("Closing IMAP server")
if err := s.server.Close(); err != nil {
log.WithError(err).Error("Failed to close the connection")
if !s.userAgent.HasClient() {
s.userAgent.SetClient("UnknownClient", "0.0.1")
}
}
func (s *imapServer) DisconnectUser(address string) {
func (s *Server) DisconnectUser(address string) {
log.Info("Disconnecting all open IMAP connections for ", address)
s.server.ForEachConn(func(conn imapserver.Conn) {
connUser := conn.Context().User
@ -195,60 +156,5 @@ func (s *imapServer) DisconnectUser(address string) {
})
}
// connListener sets debug loggers on server containing fields with local
// and remote addresses right after new connection is accepted.
type connListener struct {
net.Listener
server *imapServer
userAgent *useragent.UserAgent
}
func (l *connListener) Accept() (net.Conn, error) {
conn, err := l.Listener.Accept()
if err == nil && (l.server.debugServer || l.server.debugClient) {
debugLog := log
if addr := conn.LocalAddr(); addr != nil {
debugLog = debugLog.WithField("loc", addr.String())
}
if addr := conn.RemoteAddr(); addr != nil {
debugLog = debugLog.WithField("rem", addr.String())
}
var localDebug, remoteDebug io.Writer
if l.server.debugServer {
localDebug = debugLog.WithField("pkg", "imap/server").WriterLevel(logrus.DebugLevel)
}
if l.server.debugClient {
remoteDebug = debugLog.WithField("pkg", "imap/client").WriterLevel(logrus.DebugLevel)
}
l.server.server.Debug = imap.NewDebugWriter(localDebug, remoteDebug)
}
if !l.userAgent.HasClient() {
l.userAgent.SetClient("UnknownClient", "0.0.1")
}
return conn, err
}
// serverErrorLogger implements go-imap/logger interface.
type serverErrorLogger struct {
tag string
}
func newServerErrorLogger(tag string) *serverErrorLogger {
return &serverErrorLogger{tag}
}
func (s *serverErrorLogger) Printf(format string, args ...interface{}) {
err := fmt.Sprintf(format, args...)
log.WithField("pkg", s.tag).Error(err)
}
func (s *serverErrorLogger) Println(args ...interface{}) {
err := fmt.Sprintln(args...)
log.WithField("pkg", s.tag).Error(err)
}
func (s *Server) Serve(listener net.Listener) error { return s.server.Serve(listener) }
func (s *Server) StopServe() error { return s.server.Close() }

View File

@ -89,7 +89,7 @@ type storeMailboxProvider interface {
MarkMessagesUnstarred(apiID []string) error
MarkMessagesDeleted(apiID []string) error
MarkMessagesUndeleted(apiID []string) error
ImportMessage(msg *pmapi.Message, body []byte, labelIDs []string) error
ImportMessage(enc []byte, seen bool, labelIDs []string, flags, time int64) (string, error)
RemoveDeleted(apiIDs []string) error
}

View File

@ -188,10 +188,10 @@ func (iu *imapUpdates) MailboxStatus(address, mailboxName string, total, unread,
update.MailboxStatus.Messages = total
update.MailboxStatus.Unseen = unread
update.MailboxStatus.UnseenSeqNum = unreadSeqNum
iu.sendIMAPUpdate(update, false)
iu.sendIMAPUpdate(update, true)
}
func (iu *imapUpdates) sendIMAPUpdate(update goIMAPBackend.Update, block bool) {
func (iu *imapUpdates) sendIMAPUpdate(update goIMAPBackend.Update, isBlocking bool) {
if iu.ch == nil {
log.Trace("IMAP IDLE unavailable")
return
@ -207,7 +207,7 @@ func (iu *imapUpdates) sendIMAPUpdate(update goIMAPBackend.Update, block bool) {
}
}()
if !block {
if !isBlocking {
return
}

View File

@ -0,0 +1,117 @@
// 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 serverutil
import (
"crypto/tls"
"fmt"
"net"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/sirupsen/logrus"
)
// Controller will make sure that server is listening and serving and if needed
// users are disconnected.
type Controller interface {
ListenAndServe()
Close()
}
// NewController return simple server controller.
func NewController(s Server, l listener.Listener) Controller {
log := logrus.WithField("pkg", "serverutil").WithField("protocol", s.Protocol())
c := &controller{
server: s,
signals: l,
log: log,
closeDisconnectUsers: make(chan void),
}
if s.DebugServer() {
fmt.Println("THE LOG WILL CONTAIN **DECRYPTED** MESSAGE DATA")
log.Warning("================================================")
log.Warning("THIS LOG WILL CONTAIN **DECRYPTED** MESSAGE DATA")
log.Warning("================================================")
}
return c
}
type void struct{}
type controller struct {
server Server
signals listener.Listener
log *logrus.Entry
closeDisconnectUsers chan void
}
func (c *controller) Close() {
c.closeDisconnectUsers <- void{}
if err := c.server.StopServe(); err != nil {
c.log.WithError(err).Error("Issue when closing server")
}
}
// ListenAndServe starts the server and keeps it on based on internet
// availability. It also monitors and disconnect users if requested.
func (c *controller) ListenAndServe() {
go monitorDisconnectedUsers(c.server, c.signals, c.closeDisconnectUsers)
defer c.server.HandlePanic()
l := c.log.WithField("useSSL", c.server.UseSSL()).
WithField("address", c.server.Address())
var listener net.Listener
var err error
if c.server.UseSSL() {
listener, err = tls.Listen("tcp", c.server.Address(), c.server.TLSConfig())
} else {
listener, err = net.Listen("tcp", c.server.Address())
}
if err != nil {
l.WithError(err).Error("Cannot start listner.")
c.signals.Emit(events.ErrorEvent, string(c.server.Protocol())+" failed: "+err.Error())
return
}
// When starting the Bridge, we don't want to retry to notify user
// quickly about the issue. Very probably retry will not help anyway.
l.Info("Starting server")
err = c.server.Serve(&connListener{listener, c.server})
l.WithError(err).Debug("GoSMTP not serving")
}
func monitorDisconnectedUsers(s Server, l listener.Listener, done <-chan void) {
ch := make(chan string)
l.Add(events.CloseConnectionEvent, ch)
for {
select {
case <-done:
return
case address := <-ch:
s.DisconnectUser(address)
}
}
}

View File

@ -15,29 +15,25 @@
// 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 smtp
package serverutil
import (
"testing"
"github.com/ProtonMail/proton-bridge/internal/serverutil/mocks"
"github.com/stretchr/testify/require"
"github.com/sirupsen/logrus"
)
func TestSMTPServerTurnOffAndOnAgain(t *testing.T) {
r := require.New(t)
ts := mocks.NewTestServer(12342)
s := &Server{
panicHandler: ts.PanicHandler,
port: ts.WantPort,
eventListener: ts.EventListener,
// ServerErrorLogger implements go-imap/logger interface.
type ServerErrorLogger struct {
l *logrus.Entry
}
s.isRunning.Store(false)
r.True(ts.IsPortFree())
go s.ListenAndServe()
ts.RunServerTests(r)
func NewServerErrorLogger(protocol Protocol) *ServerErrorLogger {
return &ServerErrorLogger{l: logrus.WithField("protocol", protocol)}
}
func (s *ServerErrorLogger) Printf(format string, args ...interface{}) {
s.l.Errorf(format, args...)
}
func (s *ServerErrorLogger) Println(args ...interface{}) {
s.l.Errorln(args...)
}

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 serverutil
import (
"io"
"net"
"github.com/sirupsen/logrus"
)
// connListener sets debug loggers on server containing fields with local
// and remote addresses right after new connection is accepted.
type connListener struct {
net.Listener
server Server
}
func (l *connListener) Accept() (net.Conn, error) {
conn, err := l.Listener.Accept()
if err == nil && (l.server.DebugServer() || l.server.DebugClient()) {
debugLog := logrus.WithField("pkg", l.server.Protocol())
if addr := conn.LocalAddr(); addr != nil {
debugLog = debugLog.WithField("loc", addr.String())
}
if addr := conn.RemoteAddr(); addr != nil {
debugLog = debugLog.WithField("rem", addr.String())
}
var localDebug, remoteDebug io.Writer
if l.server.DebugServer() {
localDebug = debugLog.WithField("comm", "server").WriterLevel(logrus.DebugLevel)
}
if l.server.DebugClient() {
remoteDebug = debugLog.WithField("comm", "client").WriterLevel(logrus.DebugLevel)
}
l.server.SetLoggers(localDebug, remoteDebug)
}
return conn, err
}

View File

@ -1,150 +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 mocks
import (
"fmt"
"net/http"
"sync/atomic"
"time"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/ProtonMail/proton-bridge/pkg/ports"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/require"
)
type DummyPanicHandler struct{}
func (ph *DummyPanicHandler) HandlePanic() {}
type TestServer struct {
PanicHandler *DummyPanicHandler
WantPort int
EventListener listener.Listener
isRunning atomic.Value
srv *http.Server
}
func NewTestServer(port int) *TestServer {
s := &TestServer{
PanicHandler: &DummyPanicHandler{},
EventListener: listener.New(),
WantPort: ports.FindFreePortFrom(port),
}
s.isRunning.Store(false)
return s
}
func (s *TestServer) IsPortFree() bool {
return true
}
func (s *TestServer) IsPortOccupied() bool {
return true
}
func (s *TestServer) Emit(event string, try, iEvt int) int {
// Emit has separate go routine so it is needed to wait here to
// prevent event race condition.
time.Sleep(100 * time.Millisecond)
iEvt++
s.EventListener.Emit(event, fmt.Sprintf("%d:%d", try, iEvt))
return iEvt
}
func (s *TestServer) HandlePanic() {}
func (s *TestServer) DisconnectUser(string) {}
func (s *TestServer) Port() int { return s.WantPort }
func (s *TestServer) IsRunning() bool { return s.isRunning.Load().(bool) }
func (s *TestServer) ListenRetryAndServe(retries int, retryAfter time.Duration) {
if s.isRunning.Load().(bool) {
return
}
s.isRunning.Store(true)
// There can be delay when starting server
time.Sleep(200 * time.Millisecond)
s.srv = &http.Server{
Addr: fmt.Sprintf("127.0.0.1:%d", s.WantPort),
}
err := s.srv.ListenAndServe()
if err != nil {
s.isRunning.Store(false)
if retries > 0 {
time.Sleep(retryAfter)
s.ListenRetryAndServe(retries-1, retryAfter)
}
}
if s.IsRunning() {
logrus.Error("Not serving but isRunning is true")
s.isRunning.Store(false)
}
}
func (s *TestServer) Close() {
if !s.isRunning.Load().(bool) {
return
}
s.isRunning.Store(false)
// There can be delay when stopping server
time.Sleep(200 * time.Millisecond)
if err := s.srv.Close(); err != nil {
logrus.WithError(err).Error("Closing dummy server")
}
}
func (s *TestServer) RunServerTests(r *require.Assertions) {
// NOTE About choosing tick durations:
// In order to avoid ticks to synchronise and cause occasional race
// condition we choose the tick duration around 100ms but not exactly
// to have large common multiple.
r.Eventually(s.IsPortOccupied, 5*time.Second, 97*time.Millisecond)
// There was an issue where second time we were not able to restore server.
for try := 0; try < 3; try++ {
i := s.Emit(events.InternetOffEvent, try, 0)
r.Eventually(s.IsPortFree, 10*time.Second, 99*time.Millisecond, "signal off try %d : %d", try, i)
i = s.Emit(events.InternetOnEvent, try, i)
i = s.Emit(events.InternetOffEvent, try, i)
i = s.Emit(events.InternetOffEvent, try, i)
i = s.Emit(events.InternetOffEvent, try, i)
i = s.Emit(events.InternetOffEvent, try, i)
i = s.Emit(events.InternetOnEvent, try, i)
i = s.Emit(events.InternetOnEvent, try, i)
i = s.Emit(events.InternetOffEvent, try, i)
// Wait a bit longer if needed to process all events
r.Eventually(s.IsPortFree, 20*time.Second, 101*time.Millisecond, "again signal off number %d : %d", try, i)
i = s.Emit(events.InternetOnEvent, try, i)
r.Eventually(s.IsPortOccupied, 10*time.Second, 103*time.Millisecond, "signal on number %d : %d", try, i)
i = s.Emit(events.InternetOffEvent, try, i)
i = s.Emit(events.InternetOnEvent, try, i)
i = s.Emit(events.InternetOnEvent, try, i)
r.Eventually(s.IsPortOccupied, 10*time.Second, 107*time.Millisecond, "again signal on number %d : %d", try, i)
}
}

View File

@ -17,19 +17,10 @@
package serverutil
import (
"testing"
type Protocol string
"github.com/ProtonMail/proton-bridge/internal/serverutil/mocks"
"github.com/stretchr/testify/require"
const (
HTTP = Protocol("HTTP")
IMAP = Protocol("IMAP")
SMTP = Protocol("SMTP")
)
func TestServerTurnOffAndOnAgain(t *testing.T) {
r := require.New(t)
s := mocks.NewTestServer(12321)
r.True(s.IsPortFree())
go ListenAndServe(s, s.EventListener)
s.RunServerTests(r)
}

View File

@ -18,115 +18,24 @@
package serverutil
import (
"time"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/ProtonMail/proton-bridge/pkg/ports"
"github.com/sirupsen/logrus"
"crypto/tls"
"io"
"net"
)
// Server which can handle disconnected users and lost internet connection.
// Server can handle disconnected users.
type Server interface {
Protocol() Protocol
UseSSL() bool
Address() string
TLSConfig() *tls.Config
DebugServer() bool
DebugClient() bool
SetLoggers(localDebug, remoteDebug io.Writer)
HandlePanic()
DisconnectUser(string)
ListenRetryAndServe(int, time.Duration)
Close()
Port() int
IsRunning() bool
}
func monitorDisconnectedUsers(s Server, l listener.Listener) {
ch := make(chan string)
l.Add(events.CloseConnectionEvent, ch)
for address := range ch {
s.DisconnectUser(address)
}
}
func redirectInternetEventsToOneChannel(l listener.Listener) (isInternetOn chan bool) {
on := make(chan string)
l.Add(events.InternetOnEvent, on)
off := make(chan string)
l.Add(events.InternetOffEvent, off)
// Redirect two channels into one. When select was used the algorithm
// first read all on channels and then read all off channels.
isInternetOn = make(chan bool, 20)
go func() {
for {
logrus.WithField("try", <-on).Trace("Internet ON")
isInternetOn <- true
}
}()
go func() {
for {
logrus.WithField("try", <-off).Trace("Internet OFF")
isInternetOn <- false
}
}()
return
}
const (
recheckPortAfter = 50 * time.Millisecond
stopPortChecksAfter = 15 * time.Second
retryListnerAfter = 5 * time.Second
)
func monitorInternetConnection(s Server, l listener.Listener) {
isInternetOn := redirectInternetEventsToOneChannel(l)
for {
var expectedIsPortFree bool
if <-isInternetOn {
if s.IsRunning() {
continue
}
go func() {
defer s.HandlePanic()
// We had issues on Mac that from time to time something
// blocked our port for a bit after we closed IMAP server
// due to connection issues.
// Restart always helped, so we do retry to not bother user.
s.ListenRetryAndServe(10, retryListnerAfter)
}()
expectedIsPortFree = false
} else {
if !s.IsRunning() {
continue
}
s.Close()
expectedIsPortFree = true
}
start := time.Now()
for {
isPortFree := ports.IsPortFree(s.Port())
logrus.
WithField("port", s.Port()).
WithField("isFree", isPortFree).
WithField("wantToBeFree", expectedIsPortFree).
Trace("Check port")
if isPortFree == expectedIsPortFree {
break
}
// Safety stop if something went wrong.
if time.Since(start) > stopPortChecksAfter {
logrus.WithField("expectedIsPortFree", expectedIsPortFree).Warn("Server start/stop check timeouted")
break
}
time.Sleep(recheckPortAfter)
}
}
}
// ListenAndServe starts the server and keeps it on based on internet
// availability. It also monitors and disconnect users if requested.
func ListenAndServe(s Server, l listener.Listener) {
go monitorDisconnectedUsers(s, l)
go monitorInternetConnection(s, l)
// When starting the Bridge, we don't want to retry to notify user
// quickly about the issue. Very probably retry will not help anyway.
s.ListenRetryAndServe(0, 0)
Serve(net.Listener) error
StopServe() error
}

View File

@ -0,0 +1,153 @@
// 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 test
import (
"net/http"
"testing"
"time"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/serverutil"
"github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/stretchr/testify/require"
)
func setup(t *testing.T) (*require.Assertions, *testServer, listener.Listener, serverutil.Controller) {
r := require.New(t)
s := newTestServer()
l := listener.New()
c := serverutil.NewController(s, l)
return r, s, l, c
}
func TestControllerListernServeClose(t *testing.T) {
r, s, l, c := setup(t)
errorCh := l.ProvideChannel(events.ErrorEvent)
r.True(s.portIsFree())
go c.ListenAndServe()
r.Eventually(s.portIsOccupied, time.Second, 50*time.Millisecond)
r.NoError(s.ping())
r.Nil(s.localDebug)
r.Nil(s.remoteDebug)
c.Close()
r.Eventually(s.portIsFree, time.Second, 50*time.Millisecond)
select {
case msg := <-errorCh:
r.Fail("Expected no error but have %q", msg)
case <-time.Tick(100 * time.Millisecond):
break
}
}
func TestControllerFailOnBusyPort(t *testing.T) {
r, s, l, c := setup(t)
ocupator := http.Server{Addr: s.Address()}
defer ocupator.Close() //nolint[errcheck]
go ocupator.ListenAndServe() //nolint[errcheck]
r.Eventually(s.portIsOccupied, time.Second, 50*time.Millisecond)
errorCh := l.ProvideChannel(events.ErrorEvent)
go c.ListenAndServe()
r.Eventually(s.portIsOccupied, time.Second, 50*time.Millisecond)
select {
case <-errorCh:
break
case <-time.Tick(time.Second):
r.Fail("Expected error but have none.")
}
}
func TestControllerCallDisconnectUser(t *testing.T) {
r, s, l, c := setup(t)
go c.ListenAndServe()
r.Eventually(s.portIsOccupied, time.Second, 50*time.Millisecond)
r.NoError(s.ping())
l.Emit(events.CloseConnectionEvent, "")
r.Eventually(func() bool { return s.calledDisconnected == 1 }, time.Second, 50*time.Millisecond)
c.Close()
r.Eventually(s.portIsFree, time.Second, 50*time.Millisecond)
l.Emit(events.CloseConnectionEvent, "")
r.Equal(1, s.calledDisconnected)
}
func TestDebugClient(t *testing.T) {
r, s, _, c := setup(t)
s.debugServer = false
s.debugClient = true
go c.ListenAndServe()
r.Eventually(s.portIsOccupied, time.Second, 50*time.Millisecond)
r.NoError(s.ping())
r.Nil(s.localDebug)
r.NotNil(s.remoteDebug)
c.Close()
r.Eventually(s.portIsFree, time.Second, 50*time.Millisecond)
}
func TestDebugServer(t *testing.T) {
r, s, _, c := setup(t)
s.debugServer = true
s.debugClient = false
go c.ListenAndServe()
r.Eventually(s.portIsOccupied, time.Second, 50*time.Millisecond)
r.NoError(s.ping())
r.NotNil(s.localDebug)
r.Nil(s.remoteDebug)
c.Close()
r.Eventually(s.portIsFree, time.Second, 50*time.Millisecond)
}
func TestDebugBoth(t *testing.T) {
r, s, _, c := setup(t)
s.debugServer = true
s.debugClient = true
go c.ListenAndServe()
r.Eventually(s.portIsOccupied, time.Second, 50*time.Millisecond)
r.NoError(s.ping())
r.NotNil(s.localDebug)
r.NotNil(s.remoteDebug)
c.Close()
r.Eventually(s.portIsFree, time.Second, 50*time.Millisecond)
}

View File

@ -0,0 +1,88 @@
// 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 test
import (
"crypto/tls"
"fmt"
"io"
"net"
"net/http"
"github.com/ProtonMail/proton-bridge/internal/serverutil"
"github.com/ProtonMail/proton-bridge/pkg/ports"
)
func newTestServer() *testServer {
return &testServer{port: 11188}
}
type testServer struct {
http http.Server
useSSL,
debugServer,
debugClient bool
calledDisconnected int
port int
tls *tls.Config
localDebug, remoteDebug io.Writer
}
func (*testServer) Protocol() serverutil.Protocol { return serverutil.HTTP }
func (s *testServer) UseSSL() bool { return s.useSSL }
func (s *testServer) Address() string { return fmt.Sprintf("127.0.0.1:%d", s.port) }
func (s *testServer) TLSConfig() *tls.Config { return s.tls }
func (s *testServer) HandlePanic() {}
func (s *testServer) DebugServer() bool { return s.debugServer }
func (s *testServer) DebugClient() bool { return s.debugClient }
func (s *testServer) SetLoggers(localDebug, remoteDebug io.Writer) {
s.localDebug = localDebug
s.remoteDebug = remoteDebug
}
func (s *testServer) DisconnectUser(string) {
s.calledDisconnected++
}
func (s *testServer) Serve(l net.Listener) error {
return s.http.Serve(l)
}
func (s *testServer) StopServe() error { return s.http.Close() }
func (s *testServer) portIsFree() bool {
return ports.IsPortFree(s.port)
}
func (s *testServer) portIsOccupied() bool {
return !ports.IsPortFree(s.port)
}
func (s *testServer) ping() error {
client := &http.Client{}
resp, err := client.Get("http://" + s.Address() + "/ping")
if err != nil {
return err
}
return resp.Body.Close()
}

View File

@ -48,7 +48,7 @@ func dumpMessageData(b []byte, subject string) {
}
if err := ioutil.WriteFile(
filepath.Join(path, fmt.Sprintf("%v-%v.eml", subject, time.Now().Format(time.RFC3339Nano))),
filepath.Join(path, fmt.Sprintf("%v-%v.eml", subject, time.Now().Unix())),
b,
0600,
); err != nil {

View File

@ -20,72 +20,60 @@ package smtp
import (
"crypto/tls"
"fmt"
"io"
"net"
"sync/atomic"
"time"
"github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/serverutil"
"github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/emersion/go-sasl"
goSMTP "github.com/emersion/go-smtp"
"github.com/sirupsen/logrus"
)
// Server is Bridge SMTP server implementation.
type Server struct {
panicHandler panicHandler
backend goSMTP.Backend
server *goSMTP.Server
eventListener listener.Listener
debug bool
useSSL bool
port int
tls *tls.Config
isRunning atomic.Value
server *goSMTP.Server
controller serverutil.Controller
}
// NewSMTPServer returns an SMTP server configured with the given options.
func NewSMTPServer(panicHandler panicHandler, debug bool, port int, useSSL bool, tls *tls.Config, smtpBackend goSMTP.Backend, eventListener listener.Listener) *Server {
if debug {
fmt.Println("THE LOG WILL CONTAIN **DECRYPTED** MESSAGE DATA")
log.Warning("================================================")
log.Warning("THIS LOG WILL CONTAIN **DECRYPTED** MESSAGE DATA")
log.Warning("================================================")
}
func NewSMTPServer(
panicHandler panicHandler,
debug bool, port int, useSSL bool,
tls *tls.Config,
smtpBackend goSMTP.Backend,
eventListener listener.Listener,
) *Server {
server := &Server{
panicHandler: panicHandler,
backend: smtpBackend,
eventListener: eventListener,
debug: debug,
useSSL: useSSL,
port: port,
tls: tls,
}
server.isRunning.Store(false)
server.server = newGoSMTPServer(server)
server.controller = serverutil.NewController(server, eventListener)
return server
}
func (s *Server) HandlePanic() { s.panicHandler.HandlePanic() }
func (s *Server) IsRunning() bool { return s.isRunning.Load().(bool) }
func (s *Server) Port() int { return s.port }
func newGoSMTPServer(debug bool, smtpBackend goSMTP.Backend, port int, tls *tls.Config) *goSMTP.Server {
newSMTP := goSMTP.NewServer(smtpBackend)
newSMTP.Addr = fmt.Sprintf("%v:%v", bridge.Host, port)
newSMTP.TLSConfig = tls
func newGoSMTPServer(s *Server) *goSMTP.Server {
newSMTP := goSMTP.NewServer(s.backend)
newSMTP.Addr = s.Address()
newSMTP.TLSConfig = s.tls
newSMTP.Domain = bridge.Host
newSMTP.ErrorLog = serverutil.NewServerErrorLogger(serverutil.SMTP)
newSMTP.AllowInsecureAuth = true
newSMTP.MaxLineLength = 1 << 16
if debug {
newSMTP.Debug = logrus.
WithField("pkg", "smtp/server").
WriterLevel(logrus.DebugLevel)
}
newSMTP.EnableAuth(sasl.Login, func(conn *goSMTP.Conn) sasl.Server {
return sasl.NewLoginServer(func(address, password string) error {
user, err := conn.Server().Backend.Login(nil, address, password)
@ -100,80 +88,24 @@ func newGoSMTPServer(debug bool, smtpBackend goSMTP.Backend, port int, tls *tls.
return newSMTP
}
// ListenAndServe starts the server and keeps it on based on internet
// availability.
func (s *Server) ListenAndServe() {
serverutil.ListenAndServe(s, s.eventListener)
}
// ListenAndServe will run server and all monitors.
func (s *Server) ListenAndServe() { s.controller.ListenAndServe() }
func (s *Server) ListenRetryAndServe(retries int, retryAfter time.Duration) {
if s.IsRunning() {
return
}
s.isRunning.Store(true)
// Close turns off server and monitors.
func (s *Server) Close() { s.controller.Close() }
s.server = newGoSMTPServer(s.debug, s.backend, s.port, s.tls)
// Implements servertutil.Server interface.
l := log.WithField("useSSL", s.useSSL).WithField("address", s.server.Addr)
l.Info("SMTP server is starting")
func (Server) Protocol() serverutil.Protocol { return serverutil.SMTP }
func (s *Server) UseSSL() bool { return s.useSSL }
func (s *Server) Address() string { return fmt.Sprintf("%s:%d", bridge.Host, s.port) }
func (s *Server) TLSConfig() *tls.Config { return s.tls }
func (s *Server) HandlePanic() { s.panicHandler.HandlePanic() }
var listener net.Listener
var err error
if s.useSSL {
listener, err = tls.Listen("tcp", s.server.Addr, s.server.TLSConfig)
} else {
listener, err = net.Listen("tcp", s.server.Addr)
}
l.WithError(err).Debug("Listener for SMTP created")
if err != nil {
s.isRunning.Store(false)
if retries > 0 {
l.WithError(err).WithField("retries", retries).Warn("SMTP listener failed")
time.Sleep(retryAfter)
s.ListenRetryAndServe(retries-1, retryAfter)
return
}
func (s *Server) DebugServer() bool { return s.debug }
func (s *Server) DebugClient() bool { return s.debug }
l.WithError(err).Error("SMTP listener failed")
s.eventListener.Emit(events.ErrorEvent, "SMTP failed: "+err.Error())
return
}
err = s.server.Serve(listener)
l.WithError(err).Debug("GoSMTP not serving")
// Serve returns error every time, even after closing the server.
// User shouldn't be notified about error if server shouldn't be running,
// but it should in case it was not closed by `s.Close()`.
if err != nil && s.IsRunning() {
s.isRunning.Store(false)
l.WithError(err).Error("SMTP server failed")
s.eventListener.Emit(events.ErrorEvent, "SMTP failed: "+err.Error())
return
}
defer func() {
// Go SMTP server instance can be closed only once. Otherwise
// it returns an error. The error is not export therefore we
// will check the string value.
err := s.server.Close()
if err == nil || err.Error() != "smtp: server already closed" {
l.WithError(err).Warn("Server was not closed")
}
}()
l.Info("SMTP server closed")
}
// Close stops the server.
func (s *Server) Close() {
if !s.IsRunning() {
return
}
s.isRunning.Store(false)
if err := s.server.Close(); err != nil {
log.WithError(err).Error("Cannot close the server")
}
}
func (s *Server) SetLoggers(localDebug, remoteDebug io.Writer) { s.server.Debug = localDebug }
func (s *Server) DisconnectUser(address string) {
log.Info("Disconnecting all open SMTP connections for ", address)
@ -186,3 +118,6 @@ func (s *Server) DisconnectUser(address string) {
}
})
}
func (s *Server) Serve(l net.Listener) error { return s.server.Serve(l) }
func (s *Server) StopServe() error { return s.server.Close() }

View File

@ -48,9 +48,7 @@ func (storeMailbox *Mailbox) FetchMessage(apiID string) (*Message, error) {
return newStoreMessage(storeMailbox, msg), nil
}
// ImportMessage imports the message by calling an API.
// It has to be propagated to all mailboxes which is done by the event loop.
func (storeMailbox *Mailbox) ImportMessage(msg *pmapi.Message, body []byte, labelIDs []string) error {
func (storeMailbox *Mailbox) ImportMessage(enc []byte, seen bool, labelIDs []string, flags, time int64) (string, error) {
defer storeMailbox.pollNow()
if storeMailbox.labelID != pmapi.AllMailLabel {
@ -59,24 +57,25 @@ func (storeMailbox *Mailbox) ImportMessage(msg *pmapi.Message, body []byte, labe
importReqs := &pmapi.ImportMsgReq{
Metadata: &pmapi.ImportMetadata{
AddressID: msg.AddressID,
Unread: msg.Unread,
Flags: msg.Flags,
Time: msg.Time,
AddressID: storeMailbox.storeAddress.addressID,
Unread: pmapi.Boolean(!seen),
Flags: flags,
Time: time,
LabelIDs: labelIDs,
},
Message: body,
Message: append(enc, "\r\n"...),
}
res, err := storeMailbox.client().Import(exposeContextForIMAP(), pmapi.ImportMsgReqs{importReqs})
if err != nil {
return err
return "", err
}
if len(res) == 0 {
return errors.New("no import response")
return "", errors.New("no import response")
}
msg.ID = res[0].MessageID
return res[0].Error
return res[0].MessageID, res[0].Error
}
// LabelMessages adds the label by calling an API.
@ -355,6 +354,10 @@ func (storeMailbox *Mailbox) txCreateOrUpdateMessages(tx *bolt.Tx, msgs []*pmapi
// Buckets are not initialized right away because it's a heavy operation.
// The best option is to get the same bucket only once and only when needed.
var apiBucket, imapBucket, deletedBucket *bolt.Bucket
// Collect updates to send them later, after possibly sending the status/EXISTS update.
updates := make([]func(), 0, len(msgs))
for _, msg := range msgs {
if storeMailbox.txSkipAndRemoveFromMailbox(tx, msg) {
continue
@ -417,6 +420,8 @@ func (storeMailbox *Mailbox) txCreateOrUpdateMessages(tx *bolt.Tx, msgs []*pmapi
if err != nil {
return errors.Wrap(err, "cannot get sequence number from UID")
}
updates = append(updates, func() {
storeMailbox.store.notifyUpdateMessage(
storeMailbox.storeAddress.address,
storeMailbox.labelName,
@ -425,6 +430,8 @@ func (storeMailbox *Mailbox) txCreateOrUpdateMessages(tx *bolt.Tx, msgs []*pmapi
msg,
false, // new message is never marked as deleted
)
})
shouldSendMailboxUpdate = true
}
@ -434,6 +441,10 @@ func (storeMailbox *Mailbox) txCreateOrUpdateMessages(tx *bolt.Tx, msgs []*pmapi
}
}
for _, update := range updates {
update()
}
return nil
}

View File

@ -12,89 +12,89 @@ import (
gomock "github.com/golang/mock/gomock"
)
// MockPanicHandler is a mock of PanicHandler interface
// MockPanicHandler is a mock of PanicHandler interface.
type MockPanicHandler struct {
ctrl *gomock.Controller
recorder *MockPanicHandlerMockRecorder
}
// MockPanicHandlerMockRecorder is the mock recorder for MockPanicHandler
// MockPanicHandlerMockRecorder is the mock recorder for MockPanicHandler.
type MockPanicHandlerMockRecorder struct {
mock *MockPanicHandler
}
// NewMockPanicHandler creates a new mock instance
// NewMockPanicHandler creates a new mock instance.
func NewMockPanicHandler(ctrl *gomock.Controller) *MockPanicHandler {
mock := &MockPanicHandler{ctrl: ctrl}
mock.recorder = &MockPanicHandlerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockPanicHandler) EXPECT() *MockPanicHandlerMockRecorder {
return m.recorder
}
// HandlePanic mocks base method
// HandlePanic mocks base method.
func (m *MockPanicHandler) HandlePanic() {
m.ctrl.T.Helper()
m.ctrl.Call(m, "HandlePanic")
}
// HandlePanic indicates an expected call of HandlePanic
// HandlePanic indicates an expected call of HandlePanic.
func (mr *MockPanicHandlerMockRecorder) HandlePanic() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HandlePanic", reflect.TypeOf((*MockPanicHandler)(nil).HandlePanic))
}
// MockBridgeUser is a mock of BridgeUser interface
// MockBridgeUser is a mock of BridgeUser interface.
type MockBridgeUser struct {
ctrl *gomock.Controller
recorder *MockBridgeUserMockRecorder
}
// MockBridgeUserMockRecorder is the mock recorder for MockBridgeUser
// MockBridgeUserMockRecorder is the mock recorder for MockBridgeUser.
type MockBridgeUserMockRecorder struct {
mock *MockBridgeUser
}
// NewMockBridgeUser creates a new mock instance
// NewMockBridgeUser creates a new mock instance.
func NewMockBridgeUser(ctrl *gomock.Controller) *MockBridgeUser {
mock := &MockBridgeUser{ctrl: ctrl}
mock.recorder = &MockBridgeUserMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockBridgeUser) EXPECT() *MockBridgeUserMockRecorder {
return m.recorder
}
// CloseAllConnections mocks base method
// CloseAllConnections mocks base method.
func (m *MockBridgeUser) CloseAllConnections() {
m.ctrl.T.Helper()
m.ctrl.Call(m, "CloseAllConnections")
}
// CloseAllConnections indicates an expected call of CloseAllConnections
// CloseAllConnections indicates an expected call of CloseAllConnections.
func (mr *MockBridgeUserMockRecorder) CloseAllConnections() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CloseAllConnections", reflect.TypeOf((*MockBridgeUser)(nil).CloseAllConnections))
}
// CloseConnection mocks base method
// CloseConnection mocks base method.
func (m *MockBridgeUser) CloseConnection(arg0 string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "CloseConnection", arg0)
}
// CloseConnection indicates an expected call of CloseConnection
// CloseConnection indicates an expected call of CloseConnection.
func (mr *MockBridgeUserMockRecorder) CloseConnection(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CloseConnection", reflect.TypeOf((*MockBridgeUser)(nil).CloseConnection), arg0)
}
// GetAddressID mocks base method
// GetAddressID mocks base method.
func (m *MockBridgeUser) GetAddressID(arg0 string) (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetAddressID", arg0)
@ -103,13 +103,13 @@ func (m *MockBridgeUser) GetAddressID(arg0 string) (string, error) {
return ret0, ret1
}
// GetAddressID indicates an expected call of GetAddressID
// GetAddressID indicates an expected call of GetAddressID.
func (mr *MockBridgeUserMockRecorder) GetAddressID(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAddressID", reflect.TypeOf((*MockBridgeUser)(nil).GetAddressID), arg0)
}
// GetClient mocks base method
// GetClient mocks base method.
func (m *MockBridgeUser) GetClient() pmapi.Client {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetClient")
@ -117,13 +117,13 @@ func (m *MockBridgeUser) GetClient() pmapi.Client {
return ret0
}
// GetClient indicates an expected call of GetClient
// GetClient indicates an expected call of GetClient.
func (mr *MockBridgeUserMockRecorder) GetClient() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClient", reflect.TypeOf((*MockBridgeUser)(nil).GetClient))
}
// GetPrimaryAddress mocks base method
// GetPrimaryAddress mocks base method.
func (m *MockBridgeUser) GetPrimaryAddress() string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetPrimaryAddress")
@ -131,13 +131,13 @@ func (m *MockBridgeUser) GetPrimaryAddress() string {
return ret0
}
// GetPrimaryAddress indicates an expected call of GetPrimaryAddress
// GetPrimaryAddress indicates an expected call of GetPrimaryAddress.
func (mr *MockBridgeUserMockRecorder) GetPrimaryAddress() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPrimaryAddress", reflect.TypeOf((*MockBridgeUser)(nil).GetPrimaryAddress))
}
// GetStoreAddresses mocks base method
// GetStoreAddresses mocks base method.
func (m *MockBridgeUser) GetStoreAddresses() []string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetStoreAddresses")
@ -145,13 +145,13 @@ func (m *MockBridgeUser) GetStoreAddresses() []string {
return ret0
}
// GetStoreAddresses indicates an expected call of GetStoreAddresses
// GetStoreAddresses indicates an expected call of GetStoreAddresses.
func (mr *MockBridgeUserMockRecorder) GetStoreAddresses() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetStoreAddresses", reflect.TypeOf((*MockBridgeUser)(nil).GetStoreAddresses))
}
// ID mocks base method
// ID mocks base method.
func (m *MockBridgeUser) ID() string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ID")
@ -159,13 +159,13 @@ func (m *MockBridgeUser) ID() string {
return ret0
}
// ID indicates an expected call of ID
// ID indicates an expected call of ID.
func (mr *MockBridgeUserMockRecorder) ID() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ID", reflect.TypeOf((*MockBridgeUser)(nil).ID))
}
// IsCombinedAddressMode mocks base method
// IsCombinedAddressMode mocks base method.
func (m *MockBridgeUser) IsCombinedAddressMode() bool {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "IsCombinedAddressMode")
@ -173,13 +173,13 @@ func (m *MockBridgeUser) IsCombinedAddressMode() bool {
return ret0
}
// IsCombinedAddressMode indicates an expected call of IsCombinedAddressMode
// IsCombinedAddressMode indicates an expected call of IsCombinedAddressMode.
func (mr *MockBridgeUserMockRecorder) IsCombinedAddressMode() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsCombinedAddressMode", reflect.TypeOf((*MockBridgeUser)(nil).IsCombinedAddressMode))
}
// IsConnected mocks base method
// IsConnected mocks base method.
func (m *MockBridgeUser) IsConnected() bool {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "IsConnected")
@ -187,13 +187,13 @@ func (m *MockBridgeUser) IsConnected() bool {
return ret0
}
// IsConnected indicates an expected call of IsConnected
// IsConnected indicates an expected call of IsConnected.
func (mr *MockBridgeUserMockRecorder) IsConnected() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsConnected", reflect.TypeOf((*MockBridgeUser)(nil).IsConnected))
}
// Logout mocks base method
// Logout mocks base method.
func (m *MockBridgeUser) Logout() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Logout")
@ -201,13 +201,13 @@ func (m *MockBridgeUser) Logout() error {
return ret0
}
// Logout indicates an expected call of Logout
// Logout indicates an expected call of Logout.
func (mr *MockBridgeUserMockRecorder) Logout() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Logout", reflect.TypeOf((*MockBridgeUser)(nil).Logout))
}
// UpdateUser mocks base method
// UpdateUser mocks base method.
func (m *MockBridgeUser) UpdateUser(arg0 context.Context) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateUser", arg0)
@ -215,36 +215,36 @@ func (m *MockBridgeUser) UpdateUser(arg0 context.Context) error {
return ret0
}
// UpdateUser indicates an expected call of UpdateUser
// UpdateUser indicates an expected call of UpdateUser.
func (mr *MockBridgeUserMockRecorder) UpdateUser(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUser", reflect.TypeOf((*MockBridgeUser)(nil).UpdateUser), arg0)
}
// MockChangeNotifier is a mock of ChangeNotifier interface
// MockChangeNotifier is a mock of ChangeNotifier interface.
type MockChangeNotifier struct {
ctrl *gomock.Controller
recorder *MockChangeNotifierMockRecorder
}
// MockChangeNotifierMockRecorder is the mock recorder for MockChangeNotifier
// MockChangeNotifierMockRecorder is the mock recorder for MockChangeNotifier.
type MockChangeNotifierMockRecorder struct {
mock *MockChangeNotifier
}
// NewMockChangeNotifier creates a new mock instance
// NewMockChangeNotifier creates a new mock instance.
func NewMockChangeNotifier(ctrl *gomock.Controller) *MockChangeNotifier {
mock := &MockChangeNotifier{ctrl: ctrl}
mock.recorder = &MockChangeNotifierMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockChangeNotifier) EXPECT() *MockChangeNotifierMockRecorder {
return m.recorder
}
// CanDelete mocks base method
// CanDelete mocks base method.
func (m *MockChangeNotifier) CanDelete(arg0 string) (bool, func()) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CanDelete", arg0)
@ -253,67 +253,67 @@ func (m *MockChangeNotifier) CanDelete(arg0 string) (bool, func()) {
return ret0, ret1
}
// CanDelete indicates an expected call of CanDelete
// CanDelete indicates an expected call of CanDelete.
func (mr *MockChangeNotifierMockRecorder) CanDelete(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CanDelete", reflect.TypeOf((*MockChangeNotifier)(nil).CanDelete), arg0)
}
// DeleteMessage mocks base method
// DeleteMessage mocks base method.
func (m *MockChangeNotifier) DeleteMessage(arg0, arg1 string, arg2 uint32) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "DeleteMessage", arg0, arg1, arg2)
}
// DeleteMessage indicates an expected call of DeleteMessage
// DeleteMessage indicates an expected call of DeleteMessage.
func (mr *MockChangeNotifierMockRecorder) DeleteMessage(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteMessage", reflect.TypeOf((*MockChangeNotifier)(nil).DeleteMessage), arg0, arg1, arg2)
}
// MailboxCreated mocks base method
// MailboxCreated mocks base method.
func (m *MockChangeNotifier) MailboxCreated(arg0, arg1 string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "MailboxCreated", arg0, arg1)
}
// MailboxCreated indicates an expected call of MailboxCreated
// MailboxCreated indicates an expected call of MailboxCreated.
func (mr *MockChangeNotifierMockRecorder) MailboxCreated(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MailboxCreated", reflect.TypeOf((*MockChangeNotifier)(nil).MailboxCreated), arg0, arg1)
}
// MailboxStatus mocks base method
// MailboxStatus mocks base method.
func (m *MockChangeNotifier) MailboxStatus(arg0, arg1 string, arg2, arg3, arg4 uint32) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "MailboxStatus", arg0, arg1, arg2, arg3, arg4)
}
// MailboxStatus indicates an expected call of MailboxStatus
// MailboxStatus indicates an expected call of MailboxStatus.
func (mr *MockChangeNotifierMockRecorder) MailboxStatus(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MailboxStatus", reflect.TypeOf((*MockChangeNotifier)(nil).MailboxStatus), arg0, arg1, arg2, arg3, arg4)
}
// Notice mocks base method
// Notice mocks base method.
func (m *MockChangeNotifier) Notice(arg0, arg1 string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Notice", arg0, arg1)
}
// Notice indicates an expected call of Notice
// Notice indicates an expected call of Notice.
func (mr *MockChangeNotifierMockRecorder) Notice(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Notice", reflect.TypeOf((*MockChangeNotifier)(nil).Notice), arg0, arg1)
}
// UpdateMessage mocks base method
// UpdateMessage mocks base method.
func (m *MockChangeNotifier) UpdateMessage(arg0, arg1 string, arg2, arg3 uint32, arg4 *pmapi.Message, arg5 bool) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "UpdateMessage", arg0, arg1, arg2, arg3, arg4, arg5)
}
// UpdateMessage indicates an expected call of UpdateMessage
// UpdateMessage indicates an expected call of UpdateMessage.
func (mr *MockChangeNotifierMockRecorder) UpdateMessage(arg0, arg1, arg2, arg3, arg4, arg5 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateMessage", reflect.TypeOf((*MockChangeNotifier)(nil).UpdateMessage), arg0, arg1, arg2, arg3, arg4, arg5)

View File

@ -11,54 +11,54 @@ import (
gomock "github.com/golang/mock/gomock"
)
// MockListener is a mock of Listener interface
// MockListener is a mock of Listener interface.
type MockListener struct {
ctrl *gomock.Controller
recorder *MockListenerMockRecorder
}
// MockListenerMockRecorder is the mock recorder for MockListener
// MockListenerMockRecorder is the mock recorder for MockListener.
type MockListenerMockRecorder struct {
mock *MockListener
}
// NewMockListener creates a new mock instance
// NewMockListener creates a new mock instance.
func NewMockListener(ctrl *gomock.Controller) *MockListener {
mock := &MockListener{ctrl: ctrl}
mock.recorder = &MockListenerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockListener) EXPECT() *MockListenerMockRecorder {
return m.recorder
}
// Add mocks base method
// Add mocks base method.
func (m *MockListener) Add(arg0 string, arg1 chan<- string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Add", arg0, arg1)
}
// Add indicates an expected call of Add
// Add indicates an expected call of Add.
func (mr *MockListenerMockRecorder) Add(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Add", reflect.TypeOf((*MockListener)(nil).Add), arg0, arg1)
}
// Emit mocks base method
// Emit mocks base method.
func (m *MockListener) Emit(arg0, arg1 string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Emit", arg0, arg1)
}
// Emit indicates an expected call of Emit
// Emit indicates an expected call of Emit.
func (mr *MockListenerMockRecorder) Emit(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Emit", reflect.TypeOf((*MockListener)(nil).Emit), arg0, arg1)
}
// ProvideChannel mocks base method
// ProvideChannel mocks base method.
func (m *MockListener) ProvideChannel(arg0 string) <-chan string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ProvideChannel", arg0)
@ -66,55 +66,55 @@ func (m *MockListener) ProvideChannel(arg0 string) <-chan string {
return ret0
}
// ProvideChannel indicates an expected call of ProvideChannel
// ProvideChannel indicates an expected call of ProvideChannel.
func (mr *MockListenerMockRecorder) ProvideChannel(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProvideChannel", reflect.TypeOf((*MockListener)(nil).ProvideChannel), arg0)
}
// Remove mocks base method
// Remove mocks base method.
func (m *MockListener) Remove(arg0 string, arg1 chan<- string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Remove", arg0, arg1)
}
// Remove indicates an expected call of Remove
// Remove indicates an expected call of Remove.
func (mr *MockListenerMockRecorder) Remove(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Remove", reflect.TypeOf((*MockListener)(nil).Remove), arg0, arg1)
}
// RetryEmit mocks base method
// RetryEmit mocks base method.
func (m *MockListener) RetryEmit(arg0 string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "RetryEmit", arg0)
}
// RetryEmit indicates an expected call of RetryEmit
// RetryEmit indicates an expected call of RetryEmit.
func (mr *MockListenerMockRecorder) RetryEmit(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RetryEmit", reflect.TypeOf((*MockListener)(nil).RetryEmit), arg0)
}
// SetBuffer mocks base method
// SetBuffer mocks base method.
func (m *MockListener) SetBuffer(arg0 string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "SetBuffer", arg0)
}
// SetBuffer indicates an expected call of SetBuffer
// SetBuffer indicates an expected call of SetBuffer.
func (mr *MockListenerMockRecorder) SetBuffer(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetBuffer", reflect.TypeOf((*MockListener)(nil).SetBuffer), arg0)
}
// SetLimit mocks base method
// SetLimit mocks base method.
func (m *MockListener) SetLimit(arg0 string, arg1 time.Duration) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "SetLimit", arg0, arg1)
}
// SetLimit indicates an expected call of SetLimit
// SetLimit indicates an expected call of SetLimit.
func (mr *MockListenerMockRecorder) SetLimit(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetLimit", reflect.TypeOf((*MockListener)(nil).SetLimit), arg0, arg1)

View File

@ -12,65 +12,65 @@ import (
gomock "github.com/golang/mock/gomock"
)
// MockPanicHandler is a mock of PanicHandler interface
// MockPanicHandler is a mock of PanicHandler interface.
type MockPanicHandler struct {
ctrl *gomock.Controller
recorder *MockPanicHandlerMockRecorder
}
// MockPanicHandlerMockRecorder is the mock recorder for MockPanicHandler
// MockPanicHandlerMockRecorder is the mock recorder for MockPanicHandler.
type MockPanicHandlerMockRecorder struct {
mock *MockPanicHandler
}
// NewMockPanicHandler creates a new mock instance
// NewMockPanicHandler creates a new mock instance.
func NewMockPanicHandler(ctrl *gomock.Controller) *MockPanicHandler {
mock := &MockPanicHandler{ctrl: ctrl}
mock.recorder = &MockPanicHandlerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockPanicHandler) EXPECT() *MockPanicHandlerMockRecorder {
return m.recorder
}
// HandlePanic mocks base method
// HandlePanic mocks base method.
func (m *MockPanicHandler) HandlePanic() {
m.ctrl.T.Helper()
m.ctrl.Call(m, "HandlePanic")
}
// HandlePanic indicates an expected call of HandlePanic
// HandlePanic indicates an expected call of HandlePanic.
func (mr *MockPanicHandlerMockRecorder) HandlePanic() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HandlePanic", reflect.TypeOf((*MockPanicHandler)(nil).HandlePanic))
}
// MockIMAPClientProvider is a mock of IMAPClientProvider interface
// MockIMAPClientProvider is a mock of IMAPClientProvider interface.
type MockIMAPClientProvider struct {
ctrl *gomock.Controller
recorder *MockIMAPClientProviderMockRecorder
}
// MockIMAPClientProviderMockRecorder is the mock recorder for MockIMAPClientProvider
// MockIMAPClientProviderMockRecorder is the mock recorder for MockIMAPClientProvider.
type MockIMAPClientProviderMockRecorder struct {
mock *MockIMAPClientProvider
}
// NewMockIMAPClientProvider creates a new mock instance
// NewMockIMAPClientProvider creates a new mock instance.
func NewMockIMAPClientProvider(ctrl *gomock.Controller) *MockIMAPClientProvider {
mock := &MockIMAPClientProvider{ctrl: ctrl}
mock.recorder = &MockIMAPClientProviderMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockIMAPClientProvider) EXPECT() *MockIMAPClientProviderMockRecorder {
return m.recorder
}
// Authenticate mocks base method
// Authenticate mocks base method.
func (m *MockIMAPClientProvider) Authenticate(arg0 sasl.Client) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Authenticate", arg0)
@ -78,13 +78,13 @@ func (m *MockIMAPClientProvider) Authenticate(arg0 sasl.Client) error {
return ret0
}
// Authenticate indicates an expected call of Authenticate
// Authenticate indicates an expected call of Authenticate.
func (mr *MockIMAPClientProviderMockRecorder) Authenticate(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Authenticate", reflect.TypeOf((*MockIMAPClientProvider)(nil).Authenticate), arg0)
}
// Capability mocks base method
// Capability mocks base method.
func (m *MockIMAPClientProvider) Capability() (map[string]bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Capability")
@ -93,13 +93,13 @@ func (m *MockIMAPClientProvider) Capability() (map[string]bool, error) {
return ret0, ret1
}
// Capability indicates an expected call of Capability
// Capability indicates an expected call of Capability.
func (mr *MockIMAPClientProviderMockRecorder) Capability() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Capability", reflect.TypeOf((*MockIMAPClientProvider)(nil).Capability))
}
// Fetch mocks base method
// Fetch mocks base method.
func (m *MockIMAPClientProvider) Fetch(arg0 *imap.SeqSet, arg1 []imap.FetchItem, arg2 chan *imap.Message) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Fetch", arg0, arg1, arg2)
@ -107,13 +107,13 @@ func (m *MockIMAPClientProvider) Fetch(arg0 *imap.SeqSet, arg1 []imap.FetchItem,
return ret0
}
// Fetch indicates an expected call of Fetch
// Fetch indicates an expected call of Fetch.
func (mr *MockIMAPClientProviderMockRecorder) Fetch(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Fetch", reflect.TypeOf((*MockIMAPClientProvider)(nil).Fetch), arg0, arg1, arg2)
}
// List mocks base method
// List mocks base method.
func (m *MockIMAPClientProvider) List(arg0, arg1 string, arg2 chan *imap.MailboxInfo) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List", arg0, arg1, arg2)
@ -121,13 +121,13 @@ func (m *MockIMAPClientProvider) List(arg0, arg1 string, arg2 chan *imap.Mailbox
return ret0
}
// List indicates an expected call of List
// List indicates an expected call of List.
func (mr *MockIMAPClientProviderMockRecorder) List(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockIMAPClientProvider)(nil).List), arg0, arg1, arg2)
}
// Login mocks base method
// Login mocks base method.
func (m *MockIMAPClientProvider) Login(arg0, arg1 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Login", arg0, arg1)
@ -135,13 +135,13 @@ func (m *MockIMAPClientProvider) Login(arg0, arg1 string) error {
return ret0
}
// Login indicates an expected call of Login
// Login indicates an expected call of Login.
func (mr *MockIMAPClientProviderMockRecorder) Login(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Login", reflect.TypeOf((*MockIMAPClientProvider)(nil).Login), arg0, arg1)
}
// Select mocks base method
// Select mocks base method.
func (m *MockIMAPClientProvider) Select(arg0 string, arg1 bool) (*imap.MailboxStatus, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Select", arg0, arg1)
@ -150,13 +150,13 @@ func (m *MockIMAPClientProvider) Select(arg0 string, arg1 bool) (*imap.MailboxSt
return ret0, ret1
}
// Select indicates an expected call of Select
// Select indicates an expected call of Select.
func (mr *MockIMAPClientProviderMockRecorder) Select(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Select", reflect.TypeOf((*MockIMAPClientProvider)(nil).Select), arg0, arg1)
}
// State mocks base method
// State mocks base method.
func (m *MockIMAPClientProvider) State() imap.ConnState {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "State")
@ -164,13 +164,13 @@ func (m *MockIMAPClientProvider) State() imap.ConnState {
return ret0
}
// State indicates an expected call of State
// State indicates an expected call of State.
func (mr *MockIMAPClientProviderMockRecorder) State() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "State", reflect.TypeOf((*MockIMAPClientProvider)(nil).State))
}
// Support mocks base method
// Support mocks base method.
func (m *MockIMAPClientProvider) Support(arg0 string) (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Support", arg0)
@ -179,13 +179,13 @@ func (m *MockIMAPClientProvider) Support(arg0 string) (bool, error) {
return ret0, ret1
}
// Support indicates an expected call of Support
// Support indicates an expected call of Support.
func (mr *MockIMAPClientProviderMockRecorder) Support(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Support", reflect.TypeOf((*MockIMAPClientProvider)(nil).Support), arg0)
}
// SupportAuth mocks base method
// SupportAuth mocks base method.
func (m *MockIMAPClientProvider) SupportAuth(arg0 string) (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SupportAuth", arg0)
@ -194,13 +194,13 @@ func (m *MockIMAPClientProvider) SupportAuth(arg0 string) (bool, error) {
return ret0, ret1
}
// SupportAuth indicates an expected call of SupportAuth
// SupportAuth indicates an expected call of SupportAuth.
func (mr *MockIMAPClientProviderMockRecorder) SupportAuth(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SupportAuth", reflect.TypeOf((*MockIMAPClientProvider)(nil).SupportAuth), arg0)
}
// UidFetch mocks base method
// UidFetch mocks base method.
func (m *MockIMAPClientProvider) UidFetch(arg0 *imap.SeqSet, arg1 []imap.FetchItem, arg2 chan *imap.Message) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UidFetch", arg0, arg1, arg2)
@ -208,7 +208,7 @@ func (m *MockIMAPClientProvider) UidFetch(arg0 *imap.SeqSet, arg1 []imap.FetchIt
return ret0
}
// UidFetch indicates an expected call of UidFetch
// UidFetch indicates an expected call of UidFetch.
func (mr *MockIMAPClientProviderMockRecorder) UidFetch(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UidFetch", reflect.TypeOf((*MockIMAPClientProvider)(nil).UidFetch), arg0, arg1, arg2)

View File

@ -47,8 +47,8 @@ type Credentials struct {
UserID, // Do not marshal; used as a key.
Name,
Emails,
APIToken,
MailboxPassword,
APIToken string
MailboxPassword []byte
BridgePassword,
Version string
Timestamp int64
@ -61,7 +61,7 @@ func (s *Credentials) Marshal() string {
s.Name, // 0
s.Emails, // 1
s.APIToken, // 2
s.MailboxPassword, // 3
string(s.MailboxPassword), // 3
s.BridgePassword, // 4
s.Version, // 5
"", // 6
@ -97,7 +97,7 @@ func (s *Credentials) Unmarshal(secret string) error {
s.Name = items[0]
s.Emails = items[1]
s.APIToken = items[2]
s.MailboxPassword = items[3]
s.MailboxPassword = []byte(items[3])
switch len(items) {
case itemLengthBridge:
@ -143,11 +143,16 @@ func (s *Credentials) CheckPassword(password string) error {
func (s *Credentials) Logout() {
s.APIToken = ""
s.MailboxPassword = ""
for i := range s.MailboxPassword {
s.MailboxPassword[i] = 0
}
s.MailboxPassword = []byte{}
}
func (s *Credentials) IsConnected() bool {
return s.APIToken != "" && s.MailboxPassword != ""
return s.APIToken != "" && len(s.MailboxPassword) != 0
}
func (s *Credentials) SplitAPIToken() (string, string, error) {

View File

@ -32,7 +32,7 @@ var wantCredentials = Credentials{
Name: "name",
Emails: "email1;email2",
APIToken: "token",
MailboxPassword: "mailbox pass",
MailboxPassword: []byte("mailbox pass"),
BridgePassword: "bridge pass",
Version: "k11",
Timestamp: time.Now().Unix(),
@ -52,7 +52,7 @@ func TestUnmarshallImportExport(t *testing.T) {
wantCredentials.Name,
wantCredentials.Emails,
wantCredentials.APIToken,
wantCredentials.MailboxPassword,
string(wantCredentials.MailboxPassword),
"k11",
fmt.Sprint(wantCredentials.Timestamp),
}

View File

@ -18,6 +18,7 @@
package credentials
import (
"errors"
"fmt"
"sort"
"sync"
@ -39,7 +40,7 @@ func NewStore(keychain *keychain.Keychain) *Store {
return &Store{secrets: keychain}
}
func (s *Store) Add(userID, userName, uid, ref, mailboxPassword string, emails []string) (*Credentials, error) {
func (s *Store) Add(userID, userName, uid, ref string, mailboxPassword []byte, emails []string) (*Credentials, error) {
storeLocker.Lock()
defer storeLocker.Unlock()
@ -108,7 +109,7 @@ func (s *Store) UpdateEmails(userID string, emails []string) (*Credentials, erro
return credentials, s.saveCredentials(credentials)
}
func (s *Store) UpdatePassword(userID, password string) (*Credentials, error) {
func (s *Store) UpdatePassword(userID string, password []byte) (*Credentials, error) {
storeLocker.Lock()
defer storeLocker.Unlock()
@ -228,21 +229,28 @@ func (s *Store) Get(userID string) (creds *Credentials, err error) {
return s.get(userID)
}
func (s *Store) get(userID string) (creds *Credentials, err error) {
func (s *Store) get(userID string) (*Credentials, error) {
log := log.WithField("user", userID)
_, secret, err := s.secrets.Get(userID)
if err != nil {
log.WithError(err).Warn("Could not get credentials from native keychain")
return
return nil, err
}
if secret == "" {
return nil, errors.New("secret is empty")
}
credentials := &Credentials{UserID: userID}
if err = credentials.Unmarshal(secret); err != nil {
err = fmt.Errorf("backend/credentials: malformed secret: %v", err)
_ = s.secrets.Delete(userID)
log.WithError(err).Error("Could not unmarshal secret")
return
if err := credentials.Unmarshal(secret); err != nil {
log.WithError(fmt.Errorf("malformed secret: %w", err)).Error("Could not unmarshal secret")
if err := s.secrets.Delete(userID); err != nil {
log.WithError(err).Error("Failed to remove malformed secret")
}
return nil, err
}
return credentials, nil

View File

@ -277,7 +277,7 @@ func TestMarshal(t *testing.T) {
Name: "007",
Emails: "ja@pm.me;aj@cus.tom",
APIToken: "sdfdsfsdfsdfsdf",
MailboxPassword: "cdcdcdcd",
MailboxPassword: []byte("cdcdcdcd"),
BridgePassword: "wew123",
Version: "k11",
Timestamp: 152469263742,

View File

@ -11,54 +11,54 @@ import (
gomock "github.com/golang/mock/gomock"
)
// MockListener is a mock of Listener interface
// MockListener is a mock of Listener interface.
type MockListener struct {
ctrl *gomock.Controller
recorder *MockListenerMockRecorder
}
// MockListenerMockRecorder is the mock recorder for MockListener
// MockListenerMockRecorder is the mock recorder for MockListener.
type MockListenerMockRecorder struct {
mock *MockListener
}
// NewMockListener creates a new mock instance
// NewMockListener creates a new mock instance.
func NewMockListener(ctrl *gomock.Controller) *MockListener {
mock := &MockListener{ctrl: ctrl}
mock.recorder = &MockListenerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockListener) EXPECT() *MockListenerMockRecorder {
return m.recorder
}
// Add mocks base method
// Add mocks base method.
func (m *MockListener) Add(arg0 string, arg1 chan<- string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Add", arg0, arg1)
}
// Add indicates an expected call of Add
// Add indicates an expected call of Add.
func (mr *MockListenerMockRecorder) Add(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Add", reflect.TypeOf((*MockListener)(nil).Add), arg0, arg1)
}
// Emit mocks base method
// Emit mocks base method.
func (m *MockListener) Emit(arg0, arg1 string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Emit", arg0, arg1)
}
// Emit indicates an expected call of Emit
// Emit indicates an expected call of Emit.
func (mr *MockListenerMockRecorder) Emit(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Emit", reflect.TypeOf((*MockListener)(nil).Emit), arg0, arg1)
}
// ProvideChannel mocks base method
// ProvideChannel mocks base method.
func (m *MockListener) ProvideChannel(arg0 string) <-chan string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ProvideChannel", arg0)
@ -66,55 +66,55 @@ func (m *MockListener) ProvideChannel(arg0 string) <-chan string {
return ret0
}
// ProvideChannel indicates an expected call of ProvideChannel
// ProvideChannel indicates an expected call of ProvideChannel.
func (mr *MockListenerMockRecorder) ProvideChannel(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProvideChannel", reflect.TypeOf((*MockListener)(nil).ProvideChannel), arg0)
}
// Remove mocks base method
// Remove mocks base method.
func (m *MockListener) Remove(arg0 string, arg1 chan<- string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Remove", arg0, arg1)
}
// Remove indicates an expected call of Remove
// Remove indicates an expected call of Remove.
func (mr *MockListenerMockRecorder) Remove(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Remove", reflect.TypeOf((*MockListener)(nil).Remove), arg0, arg1)
}
// RetryEmit mocks base method
// RetryEmit mocks base method.
func (m *MockListener) RetryEmit(arg0 string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "RetryEmit", arg0)
}
// RetryEmit indicates an expected call of RetryEmit
// RetryEmit indicates an expected call of RetryEmit.
func (mr *MockListenerMockRecorder) RetryEmit(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RetryEmit", reflect.TypeOf((*MockListener)(nil).RetryEmit), arg0)
}
// SetBuffer mocks base method
// SetBuffer mocks base method.
func (m *MockListener) SetBuffer(arg0 string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "SetBuffer", arg0)
}
// SetBuffer indicates an expected call of SetBuffer
// SetBuffer indicates an expected call of SetBuffer.
func (mr *MockListenerMockRecorder) SetBuffer(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetBuffer", reflect.TypeOf((*MockListener)(nil).SetBuffer), arg0)
}
// SetLimit mocks base method
// SetLimit mocks base method.
func (m *MockListener) SetLimit(arg0 string, arg1 time.Duration) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "SetLimit", arg0, arg1)
}
// SetLimit indicates an expected call of SetLimit
// SetLimit indicates an expected call of SetLimit.
func (mr *MockListenerMockRecorder) SetLimit(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetLimit", reflect.TypeOf((*MockListener)(nil).SetLimit), arg0, arg1)

View File

@ -12,30 +12,30 @@ import (
gomock "github.com/golang/mock/gomock"
)
// MockLocator is a mock of Locator interface
// MockLocator is a mock of Locator interface.
type MockLocator struct {
ctrl *gomock.Controller
recorder *MockLocatorMockRecorder
}
// MockLocatorMockRecorder is the mock recorder for MockLocator
// MockLocatorMockRecorder is the mock recorder for MockLocator.
type MockLocatorMockRecorder struct {
mock *MockLocator
}
// NewMockLocator creates a new mock instance
// NewMockLocator creates a new mock instance.
func NewMockLocator(ctrl *gomock.Controller) *MockLocator {
mock := &MockLocator{ctrl: ctrl}
mock.recorder = &MockLocatorMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockLocator) EXPECT() *MockLocatorMockRecorder {
return m.recorder
}
// Clear mocks base method
// Clear mocks base method.
func (m *MockLocator) Clear() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Clear")
@ -43,72 +43,72 @@ func (m *MockLocator) Clear() error {
return ret0
}
// Clear indicates an expected call of Clear
// Clear indicates an expected call of Clear.
func (mr *MockLocatorMockRecorder) Clear() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Clear", reflect.TypeOf((*MockLocator)(nil).Clear))
}
// MockPanicHandler is a mock of PanicHandler interface
// MockPanicHandler is a mock of PanicHandler interface.
type MockPanicHandler struct {
ctrl *gomock.Controller
recorder *MockPanicHandlerMockRecorder
}
// MockPanicHandlerMockRecorder is the mock recorder for MockPanicHandler
// MockPanicHandlerMockRecorder is the mock recorder for MockPanicHandler.
type MockPanicHandlerMockRecorder struct {
mock *MockPanicHandler
}
// NewMockPanicHandler creates a new mock instance
// NewMockPanicHandler creates a new mock instance.
func NewMockPanicHandler(ctrl *gomock.Controller) *MockPanicHandler {
mock := &MockPanicHandler{ctrl: ctrl}
mock.recorder = &MockPanicHandlerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockPanicHandler) EXPECT() *MockPanicHandlerMockRecorder {
return m.recorder
}
// HandlePanic mocks base method
// HandlePanic mocks base method.
func (m *MockPanicHandler) HandlePanic() {
m.ctrl.T.Helper()
m.ctrl.Call(m, "HandlePanic")
}
// HandlePanic indicates an expected call of HandlePanic
// HandlePanic indicates an expected call of HandlePanic.
func (mr *MockPanicHandlerMockRecorder) HandlePanic() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HandlePanic", reflect.TypeOf((*MockPanicHandler)(nil).HandlePanic))
}
// MockCredentialsStorer is a mock of CredentialsStorer interface
// MockCredentialsStorer is a mock of CredentialsStorer interface.
type MockCredentialsStorer struct {
ctrl *gomock.Controller
recorder *MockCredentialsStorerMockRecorder
}
// MockCredentialsStorerMockRecorder is the mock recorder for MockCredentialsStorer
// MockCredentialsStorerMockRecorder is the mock recorder for MockCredentialsStorer.
type MockCredentialsStorerMockRecorder struct {
mock *MockCredentialsStorer
}
// NewMockCredentialsStorer creates a new mock instance
// NewMockCredentialsStorer creates a new mock instance.
func NewMockCredentialsStorer(ctrl *gomock.Controller) *MockCredentialsStorer {
mock := &MockCredentialsStorer{ctrl: ctrl}
mock.recorder = &MockCredentialsStorerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockCredentialsStorer) EXPECT() *MockCredentialsStorerMockRecorder {
return m.recorder
}
// Add mocks base method
func (m *MockCredentialsStorer) Add(arg0, arg1, arg2, arg3, arg4 string, arg5 []string) (*credentials.Credentials, error) {
// Add mocks base method.
func (m *MockCredentialsStorer) Add(arg0, arg1, arg2, arg3 string, arg4 []byte, arg5 []string) (*credentials.Credentials, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Add", arg0, arg1, arg2, arg3, arg4, arg5)
ret0, _ := ret[0].(*credentials.Credentials)
@ -116,13 +116,13 @@ func (m *MockCredentialsStorer) Add(arg0, arg1, arg2, arg3, arg4 string, arg5 []
return ret0, ret1
}
// Add indicates an expected call of Add
// Add indicates an expected call of Add.
func (mr *MockCredentialsStorerMockRecorder) Add(arg0, arg1, arg2, arg3, arg4, arg5 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Add", reflect.TypeOf((*MockCredentialsStorer)(nil).Add), arg0, arg1, arg2, arg3, arg4, arg5)
}
// Delete mocks base method
// Delete mocks base method.
func (m *MockCredentialsStorer) Delete(arg0 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", arg0)
@ -130,13 +130,13 @@ func (m *MockCredentialsStorer) Delete(arg0 string) error {
return ret0
}
// Delete indicates an expected call of Delete
// Delete indicates an expected call of Delete.
func (mr *MockCredentialsStorerMockRecorder) Delete(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockCredentialsStorer)(nil).Delete), arg0)
}
// Get mocks base method
// Get mocks base method.
func (m *MockCredentialsStorer) Get(arg0 string) (*credentials.Credentials, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Get", arg0)
@ -145,13 +145,13 @@ func (m *MockCredentialsStorer) Get(arg0 string) (*credentials.Credentials, erro
return ret0, ret1
}
// Get indicates an expected call of Get
// Get indicates an expected call of Get.
func (mr *MockCredentialsStorerMockRecorder) Get(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockCredentialsStorer)(nil).Get), arg0)
}
// List mocks base method
// List mocks base method.
func (m *MockCredentialsStorer) List() ([]string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List")
@ -160,13 +160,13 @@ func (m *MockCredentialsStorer) List() ([]string, error) {
return ret0, ret1
}
// List indicates an expected call of List
// List indicates an expected call of List.
func (mr *MockCredentialsStorerMockRecorder) List() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockCredentialsStorer)(nil).List))
}
// Logout mocks base method
// Logout mocks base method.
func (m *MockCredentialsStorer) Logout(arg0 string) (*credentials.Credentials, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Logout", arg0)
@ -175,13 +175,13 @@ func (m *MockCredentialsStorer) Logout(arg0 string) (*credentials.Credentials, e
return ret0, ret1
}
// Logout indicates an expected call of Logout
// Logout indicates an expected call of Logout.
func (mr *MockCredentialsStorerMockRecorder) Logout(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Logout", reflect.TypeOf((*MockCredentialsStorer)(nil).Logout), arg0)
}
// SwitchAddressMode mocks base method
// SwitchAddressMode mocks base method.
func (m *MockCredentialsStorer) SwitchAddressMode(arg0 string) (*credentials.Credentials, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SwitchAddressMode", arg0)
@ -190,13 +190,13 @@ func (m *MockCredentialsStorer) SwitchAddressMode(arg0 string) (*credentials.Cre
return ret0, ret1
}
// SwitchAddressMode indicates an expected call of SwitchAddressMode
// SwitchAddressMode indicates an expected call of SwitchAddressMode.
func (mr *MockCredentialsStorerMockRecorder) SwitchAddressMode(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SwitchAddressMode", reflect.TypeOf((*MockCredentialsStorer)(nil).SwitchAddressMode), arg0)
}
// UpdateEmails mocks base method
// UpdateEmails mocks base method.
func (m *MockCredentialsStorer) UpdateEmails(arg0 string, arg1 []string) (*credentials.Credentials, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateEmails", arg0, arg1)
@ -205,14 +205,14 @@ func (m *MockCredentialsStorer) UpdateEmails(arg0 string, arg1 []string) (*crede
return ret0, ret1
}
// UpdateEmails indicates an expected call of UpdateEmails
// UpdateEmails indicates an expected call of UpdateEmails.
func (mr *MockCredentialsStorerMockRecorder) UpdateEmails(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateEmails", reflect.TypeOf((*MockCredentialsStorer)(nil).UpdateEmails), arg0, arg1)
}
// UpdatePassword mocks base method
func (m *MockCredentialsStorer) UpdatePassword(arg0, arg1 string) (*credentials.Credentials, error) {
// UpdatePassword mocks base method.
func (m *MockCredentialsStorer) UpdatePassword(arg0 string, arg1 []byte) (*credentials.Credentials, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdatePassword", arg0, arg1)
ret0, _ := ret[0].(*credentials.Credentials)
@ -220,13 +220,13 @@ func (m *MockCredentialsStorer) UpdatePassword(arg0, arg1 string) (*credentials.
return ret0, ret1
}
// UpdatePassword indicates an expected call of UpdatePassword
// UpdatePassword indicates an expected call of UpdatePassword.
func (mr *MockCredentialsStorerMockRecorder) UpdatePassword(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatePassword", reflect.TypeOf((*MockCredentialsStorer)(nil).UpdatePassword), arg0, arg1)
}
// UpdateToken mocks base method
// UpdateToken mocks base method.
func (m *MockCredentialsStorer) UpdateToken(arg0, arg1, arg2 string) (*credentials.Credentials, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateToken", arg0, arg1, arg2)
@ -235,36 +235,36 @@ func (m *MockCredentialsStorer) UpdateToken(arg0, arg1, arg2 string) (*credentia
return ret0, ret1
}
// UpdateToken indicates an expected call of UpdateToken
// UpdateToken indicates an expected call of UpdateToken.
func (mr *MockCredentialsStorerMockRecorder) UpdateToken(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateToken", reflect.TypeOf((*MockCredentialsStorer)(nil).UpdateToken), arg0, arg1, arg2)
}
// MockStoreMaker is a mock of StoreMaker interface
// MockStoreMaker is a mock of StoreMaker interface.
type MockStoreMaker struct {
ctrl *gomock.Controller
recorder *MockStoreMakerMockRecorder
}
// MockStoreMakerMockRecorder is the mock recorder for MockStoreMaker
// MockStoreMakerMockRecorder is the mock recorder for MockStoreMaker.
type MockStoreMakerMockRecorder struct {
mock *MockStoreMaker
}
// NewMockStoreMaker creates a new mock instance
// NewMockStoreMaker creates a new mock instance.
func NewMockStoreMaker(ctrl *gomock.Controller) *MockStoreMaker {
mock := &MockStoreMaker{ctrl: ctrl}
mock.recorder = &MockStoreMakerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockStoreMaker) EXPECT() *MockStoreMakerMockRecorder {
return m.recorder
}
// New mocks base method
// New mocks base method.
func (m *MockStoreMaker) New(arg0 store.BridgeUser) (*store.Store, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "New", arg0)
@ -273,13 +273,13 @@ func (m *MockStoreMaker) New(arg0 store.BridgeUser) (*store.Store, error) {
return ret0, ret1
}
// New indicates an expected call of New
// New indicates an expected call of New.
func (mr *MockStoreMakerMockRecorder) New(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "New", reflect.TypeOf((*MockStoreMaker)(nil).New), arg0)
}
// Remove mocks base method
// Remove mocks base method.
func (m *MockStoreMaker) Remove(arg0 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Remove", arg0)
@ -287,7 +287,7 @@ func (m *MockStoreMaker) Remove(arg0 string) error {
return ret0
}
// Remove indicates an expected call of Remove
// Remove indicates an expected call of Remove.
func (mr *MockStoreMakerMockRecorder) Remove(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Remove", reflect.TypeOf((*MockStoreMaker)(nil).Remove), arg0)

View File

@ -32,11 +32,11 @@ type PanicHandler interface {
type CredentialsStorer interface {
List() (userIDs []string, err error)
Add(userID, userName, uid, ref, mailboxPassword string, emails []string) (*credentials.Credentials, error)
Add(userID, userName, uid, ref string, mailboxPassword []byte, emails []string) (*credentials.Credentials, error)
Get(userID string) (*credentials.Credentials, error)
SwitchAddressMode(userID string) (*credentials.Credentials, error)
UpdateEmails(userID string, emails []string) (*credentials.Credentials, error)
UpdatePassword(userID, password string) (*credentials.Credentials, error)
UpdatePassword(userID string, password []byte) (*credentials.Credentials, error)
UpdateToken(userID, uid, ref string) (*credentials.Credentials, error)
Logout(userID string) (*credentials.Credentials, error)
Delete(userID string) error

View File

@ -227,7 +227,7 @@ func (u *User) unlockIfNecessary() error {
// client. Unlock should only finish unlocking when connection is back up.
// That means it should try it fast enough and not retry if connection
// is still down.
err := u.client.Unlock(pmapi.ContextWithoutRetry(context.Background()), []byte(u.creds.MailboxPassword))
err := u.client.Unlock(pmapi.ContextWithoutRetry(context.Background()), u.creds.MailboxPassword)
if err == nil {
return nil
}
@ -279,26 +279,6 @@ func (u *User) GetStoreAddresses() []string {
return u.creds.EmailList()
}
// getStoreAddresses returns a user's used addresses (with the original address in first place).
func (u *User) getStoreAddresses() []string { // nolint[unused]
addrInfo, err := u.store.GetAddressInfo()
if err != nil {
u.log.WithError(err).Error("Failed getting address info from store")
return nil
}
addresses := []string{}
for _, addr := range addrInfo {
addresses = append(addresses, addr.Address)
}
if u.IsCombinedAddressMode() {
return addresses[:1]
}
return addresses
}
// GetAddresses returns list of all addresses.
func (u *User) GetAddresses() []string {
u.lock.RLock()
@ -364,7 +344,7 @@ func (u *User) UpdateUser(ctx context.Context) error {
return err
}
if err := u.client.ReloadKeys(ctx, []byte(u.creds.MailboxPassword)); err != nil {
if err := u.client.ReloadKeys(ctx, u.creds.MailboxPassword); err != nil {
return errors.Wrap(err, "failed to reload keys")
}

View File

@ -37,7 +37,7 @@ func TestUpdateUser(t *testing.T) {
gomock.InOrder(
m.pmapiClient.EXPECT().UpdateUser(gomock.Any()).Return(nil, nil),
m.pmapiClient.EXPECT().ReloadKeys(gomock.Any(), []byte(testCredentials.MailboxPassword)).Return(nil),
m.pmapiClient.EXPECT().ReloadKeys(gomock.Any(), testCredentials.MailboxPassword).Return(nil),
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress}),
m.credentialsStore.EXPECT().UpdateEmails("user", []string{testPMAPIAddress.Email}).Return(testCredentials, nil),

View File

@ -46,7 +46,7 @@ func TestNewUserUnlockFails(t *testing.T) {
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil),
m.pmapiClient.EXPECT().AddAuthRefreshHandler(gomock.Any()),
m.pmapiClient.EXPECT().IsUnlocked().Return(false),
m.pmapiClient.EXPECT().Unlock(gomock.Any(), []byte(testCredentials.MailboxPassword)).Return(errors.New("bad password")),
m.pmapiClient.EXPECT().Unlock(gomock.Any(), testCredentials.MailboxPassword).Return(errors.New("bad password")),
// Handle of unlock error.
m.pmapiClient.EXPECT().AuthDelete(gomock.Any()).Return(nil),

View File

@ -200,14 +200,14 @@ func (u *Users) closeAllConnections() {
// Login authenticates a user by username/password, returning an authorised client and an auth object.
// The authorisation scope may not yet be full if the user has 2FA enabled.
func (u *Users) Login(username, password string) (authClient pmapi.Client, auth *pmapi.Auth, err error) {
func (u *Users) Login(username string, password []byte) (authClient pmapi.Client, auth *pmapi.Auth, err error) {
u.crashBandicoot(username)
return u.clientManager.NewClientWithLogin(context.Background(), username, password)
}
// FinishLogin finishes the login procedure and adds the user into the credentials store.
func (u *Users) FinishLogin(client pmapi.Client, auth *pmapi.Auth, password string) (user *User, err error) { //nolint[funlen]
func (u *Users) FinishLogin(client pmapi.Client, auth *pmapi.Auth, password []byte) (user *User, err error) { //nolint[funlen]
apiUser, passphrase, err := getAPIUser(context.Background(), client, password)
if err != nil {
return nil, err
@ -228,7 +228,7 @@ func (u *Users) FinishLogin(client pmapi.Client, auth *pmapi.Auth, password stri
}
// Update the password in case the user changed it.
creds, err := u.credStorer.UpdatePassword(apiUser.ID, string(passphrase))
creds, err := u.credStorer.UpdatePassword(apiUser.ID, passphrase)
if err != nil {
return nil, errors.Wrap(err, "failed to update password of user in credentials store")
}
@ -264,7 +264,7 @@ func (u *Users) addNewUser(client pmapi.Client, apiUser *pmapi.User, auth *pmapi
emails = client.Addresses().AllEmails()
}
if _, err := u.credStorer.Add(apiUser.ID, apiUser.Name, auth.UID, auth.RefreshToken, string(passphrase), emails); err != nil {
if _, err := u.credStorer.Add(apiUser.ID, apiUser.Name, auth.UID, auth.RefreshToken, passphrase, emails); err != nil {
return errors.Wrap(err, "failed to add user credentials to credentials store")
}
@ -286,7 +286,7 @@ func (u *Users) addNewUser(client pmapi.Client, apiUser *pmapi.User, auth *pmapi
return nil
}
func getAPIUser(ctx context.Context, client pmapi.Client, password string) (*pmapi.User, []byte, error) {
func getAPIUser(ctx context.Context, client pmapi.Client, password []byte) (*pmapi.User, []byte, error) {
salt, err := client.AuthSalt(ctx)
if err != nil {
return nil, nil, errors.Wrap(err, "failed to get salt")

View File

@ -37,7 +37,7 @@ func TestUsersFinishLoginBadMailboxPassword(t *testing.T) {
// Set up mocks for FinishLogin.
m.pmapiClient.EXPECT().AuthSalt(gomock.Any()).Return("", nil)
m.pmapiClient.EXPECT().Unlock(gomock.Any(), []byte(testCredentials.MailboxPassword)).Return(errors.New("no keys could be unlocked"))
m.pmapiClient.EXPECT().Unlock(gomock.Any(), testCredentials.MailboxPassword).Return(errors.New("no keys could be unlocked"))
checkUsersFinishLogin(t, m, testAuthRefresh, testCredentials.MailboxPassword, "", ErrWrongMailboxPassword)
}
@ -69,7 +69,7 @@ func TestUsersFinishLoginExistingDisconnectedUser(t *testing.T) {
// Mock process of FinishLogin of already added user.
gomock.InOrder(
m.pmapiClient.EXPECT().AuthSalt(gomock.Any()).Return("", nil),
m.pmapiClient.EXPECT().Unlock(gomock.Any(), []byte(testCredentials.MailboxPassword)).Return(nil),
m.pmapiClient.EXPECT().Unlock(gomock.Any(), testCredentials.MailboxPassword).Return(nil),
m.pmapiClient.EXPECT().CurrentUser(gomock.Any()).Return(testPMAPIUserDisconnected, nil),
m.credentialsStore.EXPECT().UpdateToken(testCredentialsDisconnected.UserID, testAuthRefresh.UID, testAuthRefresh.RefreshToken).Return(testCredentials, nil),
m.credentialsStore.EXPECT().UpdatePassword(testCredentialsDisconnected.UserID, testCredentials.MailboxPassword).Return(testCredentials, nil),
@ -101,7 +101,7 @@ func TestUsersFinishLoginConnectedUser(t *testing.T) {
// Mock process of FinishLogin of already connected user.
gomock.InOrder(
m.pmapiClient.EXPECT().AuthSalt(gomock.Any()).Return("", nil),
m.pmapiClient.EXPECT().Unlock(gomock.Any(), []byte(testCredentials.MailboxPassword)).Return(nil),
m.pmapiClient.EXPECT().Unlock(gomock.Any(), testCredentials.MailboxPassword).Return(nil),
m.pmapiClient.EXPECT().CurrentUser(gomock.Any()).Return(testPMAPIUser, nil),
m.pmapiClient.EXPECT().AuthDelete(gomock.Any()).Return(nil),
)
@ -113,7 +113,7 @@ func TestUsersFinishLoginConnectedUser(t *testing.T) {
r.EqualError(t, err, "user is already connected")
}
func checkUsersFinishLogin(t *testing.T, m mocks, auth *pmapi.Auth, mailboxPassword string, expectedUserID string, expectedErr error) {
func checkUsersFinishLogin(t *testing.T, m mocks, auth *pmapi.Auth, mailboxPassword []byte, expectedUserID string, expectedErr error) {
users := testNewUsers(t, m)
defer cleanUpUsersData(users)

View File

@ -84,7 +84,7 @@ func TestNewUsersWithConnectedUserWithBadToken(t *testing.T) {
m.clientManager.EXPECT().NewClient("uid", "", "acc", time.Time{}).Return(m.pmapiClient)
m.pmapiClient.EXPECT().AddAuthRefreshHandler(gomock.Any())
m.pmapiClient.EXPECT().IsUnlocked().Return(false)
m.pmapiClient.EXPECT().Unlock(gomock.Any(), []byte(testCredentials.MailboxPassword)).Return(errors.New("not authorized"))
m.pmapiClient.EXPECT().Unlock(gomock.Any(), testCredentials.MailboxPassword).Return(errors.New("not authorized"))
m.pmapiClient.EXPECT().AuthDelete(gomock.Any())
m.credentialsStore.EXPECT().List().Return([]string{"user"}, nil)

View File

@ -63,7 +63,7 @@ var (
Name: "username",
Emails: "user@pm.me",
APIToken: "uid:acc",
MailboxPassword: "pass",
MailboxPassword: []byte("pass"),
BridgePassword: "0123456789abcdef",
Version: "v1",
Timestamp: 123456789,
@ -76,7 +76,7 @@ var (
Name: "usersname",
Emails: "users@pm.me;anotheruser@pm.me;alsouser@pm.me",
APIToken: "uid:acc",
MailboxPassword: "pass",
MailboxPassword: []byte("pass"),
BridgePassword: "0123456789abcdef",
Version: "v1",
Timestamp: 123456789,
@ -89,7 +89,7 @@ var (
Name: "username",
Emails: "user@pm.me",
APIToken: "",
MailboxPassword: "",
MailboxPassword: []byte{},
BridgePassword: "0123456789abcdef",
Version: "v1",
Timestamp: 123456789,
@ -102,7 +102,7 @@ var (
Name: "usersname",
Emails: "users@pm.me;anotheruser@pm.me;alsouser@pm.me",
APIToken: "",
MailboxPassword: "",
MailboxPassword: []byte{},
BridgePassword: "0123456789abcdef",
Version: "v1",
Timestamp: 123456789,
@ -249,7 +249,7 @@ func mockAddingConnectedUser(m mocks) {
gomock.InOrder(
// Mock of users.FinishLogin.
m.pmapiClient.EXPECT().AuthSalt(gomock.Any()).Return("", nil),
m.pmapiClient.EXPECT().Unlock(gomock.Any(), []byte(testCredentials.MailboxPassword)).Return(nil),
m.pmapiClient.EXPECT().Unlock(gomock.Any(), testCredentials.MailboxPassword).Return(nil),
m.pmapiClient.EXPECT().CurrentUser(gomock.Any()).Return(testPMAPIUser, nil),
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress}),
m.credentialsStore.EXPECT().Add("user", "username", testAuthRefresh.UID, testAuthRefresh.RefreshToken, testCredentials.MailboxPassword, []string{testPMAPIAddress.Email}).Return(testCredentials, nil),

View File

@ -90,7 +90,7 @@ func (l *listener) Add(eventName string, channel chan<- string) {
log := log.WithField("name", eventName).WithField("i", len(l.channels[eventName]))
l.channels[eventName] = append(l.channels[eventName], channel)
log.Debug("Added event listner")
log.Debug("Added event listener")
}
// Remove removes an event listener.

View File

@ -33,6 +33,7 @@ import (
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/text/encoding/htmlindex"
)
func newTestFetcher(
@ -66,6 +67,10 @@ func newTestMessage(
arm, err := enc.GetArmored()
require.NoError(t, err)
return newRawTestMessage(messageID, addressID, mimeType, arm, date)
}
func newRawTestMessage(messageID, addressID, mimeType, body string, date time.Time) *pmapi.Message {
return &pmapi.Message{
ID: messageID,
AddressID: addressID,
@ -74,7 +79,7 @@ func newTestMessage(
"Content-Type": {mimeType},
"Date": {date.In(time.UTC).Format(time.RFC1123Z)},
},
Body: arm,
Body: body,
Time: date.Unix(),
}
}
@ -133,7 +138,7 @@ func section(t *testing.T, b []byte, section ...int) *testSection {
bs, err := NewBodyStructure(bytes.NewReader(b))
require.NoError(t, err)
raw, err := bs.GetSection(bytes.NewReader(b), append([]int{}, section...))
raw, err := bs.GetSection(bytes.NewReader(b), section)
require.NoError(t, err)
return &testSection{
@ -298,6 +303,25 @@ func decryptsTo(kr *crypto.KeyRing, want string) decryptsToMatcher {
return decryptsToMatcher{kr: kr, want: want}
}
type decodesToMatcher struct {
charset string
want string
}
func (matcher decodesToMatcher) match(t *testing.T, have string) {
enc, err := htmlindex.Get(matcher.charset)
require.NoError(t, err)
dec, err := enc.NewDecoder().String(have)
require.NoError(t, err)
assert.Equal(t, matcher.want, dec)
}
func decodesTo(charset string, want string) decodesToMatcher {
return decodesToMatcher{charset: charset, want: want}
}
type verifiesAgainstMatcher struct {
kr *crypto.KeyRing
sig *crypto.PGPSignature

View File

@ -20,7 +20,6 @@ package message
import (
"bytes"
"encoding/base64"
"io/ioutil"
"mime"
"net/mail"
"strings"
@ -31,6 +30,7 @@ import (
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/emersion/go-message"
"github.com/emersion/go-message/textproto"
"github.com/pkg/errors"
)
@ -40,7 +40,7 @@ func buildRFC822(kr *crypto.KeyRing, msg *pmapi.Message, attData [][]byte, opts
return buildMultipartRFC822(kr, msg, attData, opts)
case msg.MIMEType == "multipart/mixed":
return buildExternallyEncryptedRFC822(kr, msg, opts)
return buildPGPRFC822(kr, msg, opts)
default:
return buildSimpleRFC822(kr, msg, opts)
@ -212,49 +212,31 @@ func writeRelatedParts(
})
}
func buildExternallyEncryptedRFC822(kr *crypto.KeyRing, msg *pmapi.Message, opts JobOptions) ([]byte, error) {
func buildPGPRFC822(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 buildPGPMIMERFC822(msg, opts)
return buildPGPMIMEFallbackRFC822(msg, opts)
}
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)
sigs, err := msg.ExtractSignatures(kr)
if err != nil {
return nil, err
log.WithError(err).WithField("id", msg.ID).Warn("Extract signature failed")
}
ent, err := message.Read(bytes.NewReader(dec))
if err != nil {
return nil, err
if len(sigs) > 0 {
return writeMultipartSignedRFC822(hdr, dec, sigs[0])
}
body, err := ioutil.ReadAll(ent.Body)
if err != nil {
return nil, err
return writeMultipartEncryptedRFC822(hdr, dec)
}
if err := writePart(w, ent.Header, body); err != nil {
return nil, err
}
if err := w.Close(); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func buildPGPMIMERFC822(msg *pmapi.Message, opts JobOptions) ([]byte, error) {
func buildPGPMIMEFallbackRFC822(msg *pmapi.Message, opts JobOptions) ([]byte, error) {
hdr := getMessageHeader(msg, opts)
hdr.SetContentType("multipart/encrypted", map[string]string{
@ -295,6 +277,99 @@ func buildPGPMIMERFC822(msg *pmapi.Message, opts JobOptions) ([]byte, error) {
return buf.Bytes(), nil
}
func writeMultipartSignedRFC822(header message.Header, body []byte, sig pmapi.Signature) ([]byte, error) { //nolint[funlen]
buf := new(bytes.Buffer)
boundary := newBoundary("").gen()
header.SetContentType("multipart/signed", map[string]string{
"micalg": sig.Hash,
"protocol": "application/pgp-signature",
"boundary": boundary,
})
if err := textproto.WriteHeader(buf, header.Header); err != nil {
return nil, err
}
mw := textproto.NewMultipartWriter(buf)
if err := mw.SetBoundary(boundary); err != nil {
return nil, err
}
bodyHeader, bodyData, err := readHeaderBody(body)
if err != nil {
return nil, err
}
bodyPart, err := mw.CreatePart(*bodyHeader)
if err != nil {
return nil, err
}
if _, err := bodyPart.Write(bodyData); err != nil {
return nil, err
}
var sigHeader message.Header
sigHeader.SetContentType("application/pgp-signature", map[string]string{"name": "OpenPGP_signature.asc"})
sigHeader.SetContentDisposition("attachment", map[string]string{"filename": "OpenPGP_signature"})
sigHeader.Set("Content-Description", "OpenPGP digital signature")
sigPart, err := mw.CreatePart(sigHeader.Header)
if err != nil {
return nil, err
}
sigData, err := crypto.NewPGPSignature(sig.Data).GetArmored()
if err != nil {
return nil, err
}
if _, err := sigPart.Write([]byte(sigData)); err != nil {
return nil, err
}
if err := mw.Close(); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func writeMultipartEncryptedRFC822(header message.Header, body []byte) ([]byte, error) {
buf := new(bytes.Buffer)
bodyHeader, bodyData, err := readHeaderBody(body)
if err != nil {
return nil, err
}
// If parsed header is empty then either it is malformed or it is missing.
// Anyway message could not be considered multipart/mixed anymore since there will be no boundary.
if bodyHeader.Len() == 0 {
header.Del("Content-Type")
}
entFields := bodyHeader.Fields()
for entFields.Next() {
header.Set(entFields.Key(), entFields.Value())
}
if err := textproto.WriteHeader(buf, header.Header); err != nil {
return nil, err
}
if _, err := buf.Write(bodyData); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func getMessageHeader(msg *pmapi.Message, opts JobOptions) message.Header { // nolint[funlen]
hdr := toMessageHeader(msg.Header)
@ -411,7 +486,7 @@ func getAttachmentPartHeader(att *pmapi.Attachment) message.Header {
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" {
if att.MIMEType != rfc822Message {
hdr.Set("Content-Transfer-Encoding", "base64")
} else {
hdr.Del("Content-Transfer-Encoding")
@ -425,7 +500,10 @@ func toMessageHeader(hdr mail.Header) message.Header {
for key, val := range hdr {
for _, val := range val {
res.Add(key, val)
// Using AddRaw instead of Add to save key-value pair as byte buffer within Header.
// This buffer is used latter on in message writer to construct message and avoid crash
// when key length is more than 76 characters long.
res.AddRaw([]byte(key + ": " + val + "\r\n"))
}
}

View File

@ -25,6 +25,7 @@ import (
"testing"
"time"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/ProtonMail/proton-bridge/pkg/message/mocks"
tests "github.com/ProtonMail/proton-bridge/test"
"github.com/golang/mock/gomock"
@ -51,6 +52,27 @@ func TestBuildPlainMessage(t *testing.T) {
expectTransferEncoding(is(`quoted-printable`))
}
func TestBuildPlainMessageWithLongKey(t *testing.T) {
m := gomock.NewController(t)
defer m.Finish()
b := NewBuilder(1, 1, 1)
defer b.Done()
kr := tests.MakeKeyRing(t)
msg := newTestMessage(t, kr, "messageID", "addressID", "text/plain", "body", time.Now())
msg.Header["ReallyVeryVeryVeryVeryVeryLongLongLongLongLongLongLongKeyThatWillHaveNotSoLongValue"] = []string{"value"}
res, err := b.NewJob(context.Background(), newTestFetcher(m, kr, msg), msg.ID).GetResult()
require.NoError(t, err)
section(t, res).
expectContentType(is(`text/plain`)).
expectBody(is(`body`)).
expectTransferEncoding(is(`quoted-printable`)).
expectHeader(`ReallyVeryVeryVeryVeryVeryLongLongLongLongLongLongLongKeyThatWillHaveNotSoLongValue`, is(`value`))
}
func TestBuildHTMLMessage(t *testing.T) {
m := gomock.NewController(t)
defer m.Finish()
@ -87,20 +109,162 @@ func TestBuildPlainEncryptedMessage(t *testing.T) {
section(t, res).
expectContentType(is(`multipart/mixed`)).
expectDate(is(`Wed, 01 Jan 2020 00:00:00 +0000`))
section(t, res, 1).
expectContentType(is(`multipart/mixed`)).
expectDate(is(`Wed, 01 Jan 2020 00:00:00 +0000`)).
expectContentTypeParam(`protected-headers`, is(`v1`)).
expectHeader(`Subject`, is(`plain no pubkey no sign`)).
expectHeader(`From`, is(`"pm.bridge.qa" <pm.bridge.qa@gmail.com>`)).
expectHeader(`To`, is(`schizofrenic@pm.me`))
section(t, res, 1, 1).
section(t, res, 1).
expectContentType(is(`text/plain`)).
expectBody(contains(`Where do fruits go on vacation? Pear-is!`))
}
func TestBuildPlainEncryptedMessageMissingHeader(t *testing.T) {
m := gomock.NewController(t)
defer m.Finish()
b := NewBuilder(1, 1, 1)
defer b.Done()
body := readerToString(getFileReader("plaintext-missing-header.eml"))
kr := tests.MakeKeyRing(t)
msg := newTestMessage(t, kr, "messageID", "addressID", "multipart/mixed", body, time.Now())
res, err := b.NewJob(context.Background(), newTestFetcher(m, kr, msg), msg.ID).GetResult()
require.NoError(t, err)
section(t, res).
expectContentType(is(`text/plain`)).
expectBody(is("How do we know that the ocean is friendly? It waves!\r\n"))
}
func TestBuildPlainEncryptedMessageInvalidHeader(t *testing.T) {
m := gomock.NewController(t)
defer m.Finish()
b := NewBuilder(1, 1, 1)
defer b.Done()
body := readerToString(getFileReader("plaintext-invalid-header.eml"))
kr := tests.MakeKeyRing(t)
msg := newTestMessage(t, kr, "messageID", "addressID", "multipart/mixed", body, time.Now())
res, err := b.NewJob(context.Background(), newTestFetcher(m, kr, msg), msg.ID).GetResult()
require.NoError(t, err)
section(t, res).
expectContentType(is(`text/plain`)).
expectBody(is("MalformedKey Value\r\n\r\nHow do we know that the ocean is friendly? It waves!\r\n"))
}
func TestBuildPlainSignedEncryptedMessageMissingHeader(t *testing.T) {
m := gomock.NewController(t)
defer m.Finish()
b := NewBuilder(1, 1, 1)
defer b.Done()
body := readerToString(getFileReader("plaintext-missing-header.eml"))
kr := tests.MakeKeyRing(t)
sig := tests.MakeKeyRing(t)
enc, err := kr.Encrypt(crypto.NewPlainMessageFromString(body), sig)
require.NoError(t, err)
arm, err := enc.GetArmored()
require.NoError(t, err)
msg := newRawTestMessage("messageID", "addressID", "multipart/mixed", arm, time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC))
res, err := b.NewJob(context.Background(), newTestFetcher(m, kr, msg), msg.ID).GetResult()
require.NoError(t, err)
section(t, res).
expectContentType(is(`multipart/signed`)).
expectContentTypeParam(`micalg`, is(`SHA-256`)). // NOTE: Maybe this is bad... should probably be pgp-sha256
expectContentTypeParam(`protocol`, is(`application/pgp-signature`)).
expectDate(is(`Wed, 01 Jan 2020 00:00:00 +0000`))
section(t, res, 1).
expectContentType(is(`text/plain`)).
expectBody(is("How do we know that the ocean is friendly? It waves!\r\n"))
section(t, res, 2).
expectContentType(is(`application/pgp-signature`)).
expectContentTypeParam(`name`, is(`OpenPGP_signature.asc`)).
expectContentDisposition(is(`attachment`)).
expectContentDispositionParam(`filename`, is(`OpenPGP_signature`))
}
func TestBuildPlainSignedEncryptedMessageInvalidHeader(t *testing.T) {
m := gomock.NewController(t)
defer m.Finish()
b := NewBuilder(1, 1, 1)
defer b.Done()
body := readerToString(getFileReader("plaintext-invalid-header.eml"))
kr := tests.MakeKeyRing(t)
sig := tests.MakeKeyRing(t)
enc, err := kr.Encrypt(crypto.NewPlainMessageFromString(body), sig)
require.NoError(t, err)
arm, err := enc.GetArmored()
require.NoError(t, err)
msg := newRawTestMessage("messageID", "addressID", "multipart/mixed", arm, time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC))
res, err := b.NewJob(context.Background(), newTestFetcher(m, kr, msg), msg.ID).GetResult()
require.NoError(t, err)
section(t, res).
expectContentType(is(`multipart/signed`)).
expectContentTypeParam(`micalg`, is(`SHA-256`)). // NOTE: Maybe this is bad... should probably be pgp-sha256
expectContentTypeParam(`protocol`, is(`application/pgp-signature`)).
expectDate(is(`Wed, 01 Jan 2020 00:00:00 +0000`))
section(t, res, 1).
expectContentType(is(`text/plain`)).
expectBody(is("MalformedKey Value\r\n\r\nHow do we know that the ocean is friendly? It waves!\r\n"))
section(t, res, 2).
expectContentType(is(`application/pgp-signature`)).
expectContentTypeParam(`name`, is(`OpenPGP_signature.asc`)).
expectContentDisposition(is(`attachment`)).
expectContentDispositionParam(`filename`, is(`OpenPGP_signature`))
}
func TestBuildPlainEncryptedLatin2Message(t *testing.T) {
m := gomock.NewController(t)
defer m.Finish()
b := NewBuilder(1, 1, 1)
defer b.Done()
body := readerToString(getFileReader("pgp-mime-body-plaintext-latin2.eml"))
kr := tests.MakeKeyRing(t)
msg := newTestMessage(t, kr, "messageID", "addressID", "multipart/mixed", body, time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC))
res, err := b.NewJob(context.Background(), newTestFetcher(m, kr, msg), msg.ID).GetResult()
require.NoError(t, err)
section(t, res).
expectContentType(is(`text/plain`)).
expectContentTypeParam("charset", is(`iso-8859-2`)).
expectDate(is(`Wed, 01 Jan 2020 00:00:00 +0000`)).
expectHeader(`Subject`, is(`plain no pubkey no sign`)).
expectHeader(`From`, is(`"pm.bridge.qa" <pm.bridge.qa@gmail.com>`)).
expectHeader(`To`, is(`schizofrenic@pm.me`)).
expectBody(decodesTo("iso-8859-2", "řšřšřš\r\n"))
}
func TestBuildHTMLEncryptedMessage(t *testing.T) {
m := gomock.NewController(t)
defer m.Finish()
@ -118,21 +282,101 @@ func TestBuildHTMLEncryptedMessage(t *testing.T) {
section(t, res).
expectContentType(is(`multipart/mixed`)).
expectDate(is(`Wed, 01 Jan 2020 00:00:00 +0000`))
section(t, res, 1).
expectContentType(is(`multipart/mixed`)).
expectDate(is(`Wed, 01 Jan 2020 00:00:00 +0000`)).
expectContentTypeParam(`protected-headers`, is(`v1`)).
expectHeader(`Subject`, is(`html no pubkey no sign`)).
expectHeader(`From`, is(`"pm.bridge.qa" <pm.bridge.qa@gmail.com>`)).
expectHeader(`To`, is(`schizofrenic@pm.me`))
section(t, res, 1, 1).
section(t, res, 1).
expectContentType(is(`text/html`)).
expectBody(contains(`What do you call a poor Santa Claus`)).
expectBody(contains(`Where do boats go when they're sick`))
}
func TestBuildPlainSignedMessage(t *testing.T) {
m := gomock.NewController(t)
defer m.Finish()
b := NewBuilder(1, 1, 1)
defer b.Done()
body := readerToString(getFileReader("text_plain.eml"))
kr := tests.MakeKeyRing(t)
sig := tests.MakeKeyRing(t)
enc, err := kr.Encrypt(crypto.NewPlainMessageFromString(body), sig)
require.NoError(t, err)
arm, err := enc.GetArmored()
require.NoError(t, err)
msg := newRawTestMessage("messageID", "addressID", "multipart/mixed", arm, time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC))
res, err := b.NewJob(context.Background(), newTestFetcher(m, kr, msg), msg.ID).GetResult()
require.NoError(t, err)
section(t, res).
expectContentType(is(`multipart/signed`)).
expectContentTypeParam(`micalg`, is(`SHA-256`)). // NOTE: Maybe this is bad... should probably be pgp-sha256
expectContentTypeParam(`protocol`, is(`application/pgp-signature`)).
expectDate(is(`Wed, 01 Jan 2020 00:00:00 +0000`))
section(t, res, 1).
expectContentType(is(`text/plain`)).
expectBody(is(`body`)).
expectSection(verifiesAgainst(sig, section(t, res, 2).signature()))
section(t, res, 2).
expectContentType(is(`application/pgp-signature`)).
expectContentTypeParam(`name`, is(`OpenPGP_signature.asc`)).
expectContentDisposition(is(`attachment`)).
expectContentDispositionParam(`filename`, is(`OpenPGP_signature`))
}
func TestBuildPlainSignedBase64Message(t *testing.T) {
m := gomock.NewController(t)
defer m.Finish()
b := NewBuilder(1, 1, 1)
defer b.Done()
body := readerToString(getFileReader("text_plain_base64.eml"))
kr := tests.MakeKeyRing(t)
sig := tests.MakeKeyRing(t)
enc, err := kr.Encrypt(crypto.NewPlainMessageFromString(body), sig)
require.NoError(t, err)
arm, err := enc.GetArmored()
require.NoError(t, err)
msg := newRawTestMessage("messageID", "addressID", "multipart/mixed", arm, time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC))
res, err := b.NewJob(context.Background(), newTestFetcher(m, kr, msg), msg.ID).GetResult()
require.NoError(t, err)
section(t, res).
expectContentType(is(`multipart/signed`)).
expectContentTypeParam(`micalg`, is(`SHA-256`)). // NOTE: Maybe this is bad... should probably be pgp-sha256
expectContentTypeParam(`protocol`, is(`application/pgp-signature`)).
expectDate(is(`Wed, 01 Jan 2020 00:00:00 +0000`))
section(t, res, 1).
expectContentType(is(`text/plain`)).
expectTransferEncoding(is(`base64`)).
expectBody(is(`body`)).
expectSection(verifiesAgainst(sig, section(t, res, 2).signature()))
section(t, res, 2).
expectContentType(is(`application/pgp-signature`)).
expectContentTypeParam(`name`, is(`OpenPGP_signature.asc`)).
expectContentDisposition(is(`attachment`)).
expectContentDispositionParam(`filename`, is(`OpenPGP_signature`))
}
func TestBuildSignedPlainEncryptedMessage(t *testing.T) {
m := gomock.NewController(t)
defer m.Finish()
@ -149,27 +393,24 @@ func TestBuildSignedPlainEncryptedMessage(t *testing.T) {
require.NoError(t, err)
section(t, res).
expectContentType(is(`multipart/mixed`)).
expectDate(is(`Wed, 01 Jan 2020 00:00:00 +0000`))
section(t, res, 1).
expectDate(is(`Wed, 01 Jan 2020 00:00:00 +0000`)).
expectContentType(is(`multipart/signed`)).
expectContentTypeParam(`micalg`, is(`pgp-sha256`)).
expectContentTypeParam(`protocol`, is(`application/pgp-signature`))
section(t, res, 1, 1).
section(t, res, 1).
expectContentType(is(`multipart/mixed`)).
expectContentTypeParam(`protected-headers`, is(`v1`)).
expectHeader(`Subject`, is(`plain body no pubkey`)).
expectHeader(`From`, is(`"pm.bridge.qa" <pm.bridge.qa@gmail.com>`)).
expectHeader(`To`, is(`schizofrenic@pm.me`))
section(t, res, 1, 1, 1).
section(t, res, 1, 1).
expectContentType(is(`text/plain`)).
expectBody(contains(`Why do seagulls fly over the ocean`)).
expectBody(contains(`Because if they flew over the bay, we'd call them bagels`))
section(t, res, 1, 2).
section(t, res, 2).
expectContentType(is(`application/pgp-signature`)).
expectContentTypeParam(`name`, is(`OpenPGP_signature.asc`)).
expectContentDisposition(is(`attachment`)).
@ -192,29 +433,26 @@ func TestBuildSignedHTMLEncryptedMessage(t *testing.T) {
require.NoError(t, err)
section(t, res).
expectContentType(is(`multipart/mixed`)).
expectDate(is(`Wed, 01 Jan 2020 00:00:00 +0000`))
section(t, res, 1).
expectDate(is(`Wed, 01 Jan 2020 00:00:00 +0000`)).
expectContentType(is(`multipart/signed`)).
expectContentTypeParam(`micalg`, is(`pgp-sha256`)).
expectContentTypeParam(`protocol`, is(`application/pgp-signature`))
section(t, res, 1, 1).
section(t, res, 1).
expectContentType(is(`multipart/mixed`)).
expectContentTypeParam(`protected-headers`, is(`v1`)).
expectHeader(`Subject`, is(`html body no pubkey`)).
expectHeader(`From`, is(`"pm.bridge.qa" <pm.bridge.qa@gmail.com>`)).
expectHeader(`To`, is(`schizofrenic@pm.me`))
section(t, res, 1, 1, 1).
section(t, res, 1, 1).
expectContentType(is(`text/html`)).
expectBody(contains(`Behold another <font color="#ee24cc">HTML</font>`)).
expectBody(contains(`I only know 25 letters of the alphabet`)).
expectBody(contains(`What did one wall say to the other`)).
expectBody(contains(`What did the zero say to the eight`))
section(t, res, 1, 2).
section(t, res, 2).
expectContentType(is(`application/pgp-signature`)).
expectContentTypeParam(`name`, is(`OpenPGP_signature.asc`)).
expectContentDisposition(is(`attachment`)).
@ -237,36 +475,33 @@ func TestBuildSignedPlainEncryptedMessageWithPubKey(t *testing.T) {
require.NoError(t, err)
section(t, res).
expectContentType(is(`multipart/mixed`)).
expectDate(is(`Wed, 01 Jan 2020 00:00:00 +0000`))
section(t, res, 1).
expectDate(is(`Wed, 01 Jan 2020 00:00:00 +0000`)).
expectContentType(is(`multipart/signed`)).
expectContentTypeParam(`micalg`, is(`pgp-sha256`)).
expectContentTypeParam(`protocol`, is(`application/pgp-signature`))
section(t, res, 1, 1).
section(t, res, 1).
expectContentType(is(`multipart/mixed`)).
expectContentTypeParam(`protected-headers`, is(`v1`)).
expectHeader(`Subject`, is(`simple plaintext body`)).
expectHeader(`From`, is(`"pm.bridge.qa" <pm.bridge.qa@gmail.com>`)).
expectHeader(`To`, is(`schizofrenic@pm.me`)).
expectSection(verifiesAgainst(section(t, res, 1, 1, 1, 2).pubKey(), section(t, res, 1, 2).signature()))
expectSection(verifiesAgainst(section(t, res, 1, 1, 2).pubKey(), section(t, res, 2).signature()))
section(t, res, 1, 1, 1).
section(t, res, 1, 1).
expectContentType(is(`multipart/mixed`))
section(t, res, 1, 1, 1, 1).
section(t, res, 1, 1, 1).
expectContentType(is(`text/plain`)).
expectBody(contains(`Why don't crabs give to charity? Because they're shellfish.`))
section(t, res, 1, 1, 1, 2).
section(t, res, 1, 1, 2).
expectContentType(is(`application/pgp-keys`)).
expectContentTypeParam(`name`, is(`OpenPGP_0x161C0875822359F7.asc`)).
expectContentDisposition(is(`attachment`)).
expectContentDispositionParam(`filename`, is(`OpenPGP_0x161C0875822359F7.asc`))
section(t, res, 1, 2).
section(t, res, 2).
expectContentType(is(`application/pgp-signature`)).
expectContentTypeParam(`name`, is(`OpenPGP_signature.asc`)).
expectContentDisposition(is(`attachment`)).
@ -289,37 +524,34 @@ func TestBuildSignedHTMLEncryptedMessageWithPubKey(t *testing.T) {
require.NoError(t, err)
section(t, res).
expectContentType(is(`multipart/mixed`)).
expectDate(is(`Wed, 01 Jan 2020 00:00:00 +0000`))
section(t, res, 1).
expectDate(is(`Wed, 01 Jan 2020 00:00:00 +0000`)).
expectContentType(is(`multipart/signed`)).
expectContentTypeParam(`micalg`, is(`pgp-sha256`)).
expectContentTypeParam(`protocol`, is(`application/pgp-signature`))
section(t, res, 1, 1).
section(t, res, 1).
expectContentType(is(`multipart/mixed`)).
expectContentTypeParam(`protected-headers`, is(`v1`)).
expectHeader(`Subject`, is(`simple html body`)).
expectHeader(`From`, is(`"pm.bridge.qa" <pm.bridge.qa@gmail.com>`)).
expectHeader(`To`, is(`schizofrenic@pm.me`)).
expectSection(verifiesAgainst(section(t, res, 1, 1, 1, 2).pubKey(), section(t, res, 1, 2).signature()))
expectSection(verifiesAgainst(section(t, res, 1, 1, 2).pubKey(), section(t, res, 2).signature()))
section(t, res, 1, 1, 1).
section(t, res, 1, 1).
expectContentType(is(`multipart/mixed`))
section(t, res, 1, 1, 1, 1).
section(t, res, 1, 1, 1).
expectContentType(is(`text/html`)).
expectBody(contains(`Do I enjoy making courthouse puns`)).
expectBody(contains(`Can February March`))
section(t, res, 1, 1, 1, 2).
section(t, res, 1, 1, 2).
expectContentType(is(`application/pgp-keys`)).
expectContentTypeParam(`name`, is(`OpenPGP_0x161C0875822359F7.asc`)).
expectContentDisposition(is(`attachment`)).
expectContentDispositionParam(`filename`, is(`OpenPGP_0x161C0875822359F7.asc`))
section(t, res, 1, 2).
section(t, res, 2).
expectContentType(is(`application/pgp-signature`)).
expectContentTypeParam(`name`, is(`OpenPGP_signature.asc`)).
expectContentDisposition(is(`attachment`)).
@ -342,53 +574,50 @@ func TestBuildSignedMultipartAlternativeEncryptedMessageWithPubKey(t *testing.T)
require.NoError(t, err)
section(t, res).
expectContentType(is(`multipart/mixed`)).
expectDate(is(`Wed, 01 Jan 2020 00:00:00 +0000`))
section(t, res, 1).
expectDate(is(`Wed, 01 Jan 2020 00:00:00 +0000`)).
expectContentType(is(`multipart/signed`)).
expectContentTypeParam(`micalg`, is(`pgp-sha256`)).
expectContentTypeParam(`protocol`, is(`application/pgp-signature`))
section(t, res, 1, 1).
section(t, res, 1).
expectContentType(is(`multipart/mixed`)).
expectContentTypeParam(`protected-headers`, is(`v1`)).
expectHeader(`Subject`, is(`Alternative`)).
expectHeader(`From`, is(`"pm.bridge.qa" <pm.bridge.qa@gmail.com>`)).
expectHeader(`To`, is(`schizofrenic@pm.me`)).
expectSection(verifiesAgainst(section(t, res, 1, 1, 1, 3).pubKey(), section(t, res, 1, 2).signature()))
expectSection(verifiesAgainst(section(t, res, 1, 1, 3).pubKey(), section(t, res, 2).signature()))
section(t, res, 1, 1, 1).
section(t, res, 1, 1).
expectContentType(is(`multipart/mixed`))
section(t, res, 1, 1, 1, 1).
section(t, res, 1, 1, 1).
expectContentType(is(`multipart/alternative`))
section(t, res, 1, 1, 1, 1, 1).
section(t, res, 1, 1, 1, 1).
expectContentType(is(`text/plain`)).
expectBody(contains(`This Rich formated text`)).
expectBody(contains(`What kind of shoes do ninjas wear`)).
expectBody(contains(`How does a penguin build its house`))
section(t, res, 1, 1, 1, 1, 2).
section(t, res, 1, 1, 1, 2).
expectContentType(is(`text/html`)).
expectBody(contains(`This <font color="#ee24cc">Rich</font> formated text`)).
expectBody(contains(`What kind of shoes do ninjas wear`)).
expectBody(contains(`How does a penguin build its house`))
section(t, res, 1, 1, 1, 2).
section(t, res, 1, 1, 2).
expectContentType(is(`application/pdf`)).
expectTransferEncoding(is(`base64`)).
expectContentTypeParam(`name`, is(`minimal.pdf`)).
expectContentDispositionParam(`filename`, is(`minimal.pdf`))
section(t, res, 1, 1, 1, 3).
section(t, res, 1, 1, 3).
expectContentType(is(`application/pgp-keys`)).
expectContentTypeParam(`name`, is(`OpenPGP_0x161C0875822359F7.asc`)).
expectContentDisposition(is(`attachment`)).
expectContentDispositionParam(`filename`, is(`OpenPGP_0x161C0875822359F7.asc`))
section(t, res, 1, 2).
section(t, res, 2).
expectContentType(is(`application/pgp-signature`)).
expectContentTypeParam(`name`, is(`OpenPGP_signature.asc`)).
expectContentDisposition(is(`attachment`)).
@ -411,41 +640,38 @@ func TestBuildSignedEmbeddedMessageRFC822EncryptedMessageWithPubKey(t *testing.T
require.NoError(t, err)
section(t, res).
expectContentType(is(`multipart/mixed`)).
expectDate(is(`Wed, 01 Jan 2020 00:00:00 +0000`))
section(t, res, 1).
expectDate(is(`Wed, 01 Jan 2020 00:00:00 +0000`)).
expectContentType(is(`multipart/signed`)).
expectContentTypeParam(`micalg`, is(`pgp-sha256`)).
expectContentTypeParam(`protocol`, is(`application/pgp-signature`))
section(t, res, 1, 1).
section(t, res, 1).
expectContentType(is(`multipart/mixed`)).
expectContentTypeParam(`protected-headers`, is(`v1`)).
expectHeader(`Subject`, is(`Fwd: HTML with attachment external PGP`)).
expectHeader(`From`, is(`"pm.bridge.qa" <pm.bridge.qa@gmail.com>`)).
expectHeader(`To`, is(`schizofrenic@pm.me`)).
expectSection(verifiesAgainst(section(t, res, 1, 1, 1, 2).pubKey(), section(t, res, 1, 2).signature()))
expectSection(verifiesAgainst(section(t, res, 1, 1, 2).pubKey(), section(t, res, 2).signature()))
section(t, res, 1, 1, 1).
section(t, res, 1, 1).
expectContentType(is(`multipart/mixed`))
section(t, res, 1, 1, 1, 1).
section(t, res, 1, 1, 1).
expectContentType(is(`text/plain`))
section(t, res, 1, 1, 1, 2).
section(t, res, 1, 1, 2).
expectContentType(is(`application/pgp-keys`)).
expectContentTypeParam(`name`, is(`OpenPGP_0x161C0875822359F7.asc`)).
expectContentDisposition(is(`attachment`)).
expectContentDispositionParam(`filename`, is(`OpenPGP_0x161C0875822359F7.asc`))
section(t, res, 1, 1, 1, 3).
section(t, res, 1, 1, 3).
expectContentType(is(`message/rfc822`)).
expectContentTypeParam(`name`, is(`HTML with attachment external PGP.eml`)).
expectContentDisposition(is(`attachment`)).
expectContentDispositionParam(`filename`, is(`HTML with attachment external PGP.eml`))
section(t, res, 1, 2).
section(t, res, 2).
expectContentType(is(`application/pgp-signature`)).
expectContentTypeParam(`name`, is(`OpenPGP_signature.asc`)).
expectContentDisposition(is(`attachment`)).

246
pkg/message/encrypt.go Normal file
View File

@ -0,0 +1,246 @@
// 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"
"mime/quotedprintable"
"strings"
"github.com/ProtonMail/gopenpgp/v2/crypto"
pmmime "github.com/ProtonMail/proton-bridge/pkg/mime"
"github.com/emersion/go-message/textproto"
"github.com/pkg/errors"
)
func EncryptRFC822(kr *crypto.KeyRing, r io.Reader) ([]byte, error) {
b, err := ioutil.ReadAll(r)
if err != nil {
return nil, err
}
header, body, err := readHeaderBody(b)
if err != nil {
return nil, err
}
buf := new(bytes.Buffer)
result, err := writeEncryptedPart(kr, header, bytes.NewReader(body))
if err != nil {
return nil, err
}
if err := textproto.WriteHeader(buf, *header); err != nil {
return nil, err
}
if _, err := result.WriteTo(buf); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func writeEncryptedPart(kr *crypto.KeyRing, header *textproto.Header, r io.Reader) (io.WriterTo, error) {
decoder := getTransferDecoder(r, header.Get("Content-Transfer-Encoding"))
encoded := new(bytes.Buffer)
contentType, contentParams, err := parseContentType(header.Get("Content-Type"))
// Ignoring invalid media parameter makes it work for invalid tutanota RFC2047-encoded attachment filenames since we often only really need the content type and not the optional media parameters.
if err != nil && !errors.Is(err, mime.ErrInvalidMediaParameter) {
return nil, err
}
switch {
case contentType == "", strings.HasPrefix(contentType, "text/"), strings.HasPrefix(contentType, "message/"):
header.Del("Content-Transfer-Encoding")
if charset, ok := contentParams["charset"]; ok {
if reader, err := pmmime.CharsetReader(charset, decoder); err == nil {
decoder = reader
// We can decode the charset to utf-8 so let's set that as the content type charset parameter.
contentParams["charset"] = "utf-8"
header.Set("Content-Type", mime.FormatMediaType(contentType, contentParams))
}
}
if err := encode(&writeCloser{encoded}, func(w io.Writer) error {
return writeEncryptedTextPart(w, decoder, kr)
}); err != nil {
return nil, err
}
case contentType == "multipart/encrypted":
if _, err := encoded.ReadFrom(decoder); err != nil {
return nil, err
}
case strings.HasPrefix(contentType, "multipart/"):
if err := encode(&writeCloser{encoded}, func(w io.Writer) error {
return writeEncryptedMultiPart(kr, w, header, decoder)
}); err != nil {
return nil, err
}
default:
header.Set("Content-Transfer-Encoding", "base64")
if err := encode(base64.NewEncoder(base64.StdEncoding, encoded), func(w io.Writer) error {
return writeEncryptedAttachmentPart(w, decoder, kr)
}); err != nil {
return nil, err
}
}
return encoded, nil
}
func writeEncryptedTextPart(w io.Writer, r io.Reader, kr *crypto.KeyRing) error {
dec, err := ioutil.ReadAll(r)
if err != nil {
return err
}
var arm string
if msg, err := crypto.NewPGPMessageFromArmored(string(dec)); err != nil {
enc, err := kr.Encrypt(crypto.NewPlainMessage(dec), kr)
if err != nil {
return err
}
if arm, err = enc.GetArmored(); err != nil {
return err
}
} else if arm, err = msg.GetArmored(); err != nil {
return err
}
if _, err := io.WriteString(w, arm); err != nil {
return err
}
return nil
}
func writeEncryptedAttachmentPart(w io.Writer, r io.Reader, kr *crypto.KeyRing) error {
dec, err := ioutil.ReadAll(r)
if err != nil {
return err
}
enc, err := kr.Encrypt(crypto.NewPlainMessage(dec), kr)
if err != nil {
return err
}
if _, err := w.Write(enc.GetBinary()); err != nil {
return err
}
return nil
}
func writeEncryptedMultiPart(kr *crypto.KeyRing, w io.Writer, header *textproto.Header, r io.Reader) error {
_, contentParams, err := parseContentType(header.Get("Content-Type"))
if err != nil {
return err
}
scanner, err := newPartScanner(r, contentParams["boundary"])
if err != nil {
return err
}
parts, err := scanner.scanAll()
if err != nil {
return err
}
writer := newPartWriter(w, contentParams["boundary"])
for _, part := range parts {
header, body, err := readHeaderBody(part.b)
if err != nil {
return err
}
result, err := writeEncryptedPart(kr, header, bytes.NewReader(body))
if err != nil {
return err
}
if err := writer.createPart(func(w io.Writer) error {
if err := textproto.WriteHeader(w, *header); err != nil {
return err
}
if _, err := result.WriteTo(w); err != nil {
return err
}
return nil
}); err != nil {
return err
}
}
return writer.done()
}
func getTransferDecoder(r io.Reader, encoding string) io.Reader {
switch strings.ToLower(encoding) {
case "base64":
return base64.NewDecoder(base64.StdEncoding, r)
case "quoted-printable":
return quotedprintable.NewReader(r)
default:
return r
}
}
func encode(wc io.WriteCloser, fn func(io.Writer) error) error {
if err := fn(wc); err != nil {
return err
}
return wc.Close()
}
type writeCloser struct {
io.Writer
}
func (writeCloser) Close() error { return nil }
func parseContentType(val string) (string, map[string]string, error) {
if val == "" {
val = "text/plain"
}
return pmmime.ParseMediaType(val)
}

101
pkg/message/encrypt_test.go Normal file
View File

@ -0,0 +1,101 @@
// 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"
"io/ioutil"
"testing"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/stretchr/testify/require"
)
func TestEncryptRFC822(t *testing.T) {
literal, err := ioutil.ReadFile("testdata/text_plain_latin1.eml")
require.NoError(t, err)
key, err := crypto.GenerateKey("name", "email", "rsa", 2048)
require.NoError(t, err)
kr, err := crypto.NewKeyRing(key)
require.NoError(t, err)
enc, err := EncryptRFC822(kr, bytes.NewReader(literal))
require.NoError(t, err)
section(t, enc).
expectContentType(is(`text/plain`)).
expectContentTypeParam(`charset`, is(`utf-8`)).
expectBody(decryptsTo(kr, `ééééééé`))
}
func TestEncryptRFC822Multipart(t *testing.T) {
literal, err := ioutil.ReadFile("testdata/multipart_alternative_nested.eml")
require.NoError(t, err)
key, err := crypto.GenerateKey("name", "email", "rsa", 2048)
require.NoError(t, err)
kr, err := crypto.NewKeyRing(key)
require.NoError(t, err)
enc, err := EncryptRFC822(kr, bytes.NewReader(literal))
require.NoError(t, err)
section(t, enc).
expectContentType(is(`multipart/alternative`))
section(t, enc, 1).
expectContentType(is(`multipart/alternative`))
section(t, enc, 1, 1).
expectContentType(is(`text/plain`)).
expectBody(decryptsTo(kr, "*multipart 1.1*\n\n"))
section(t, enc, 1, 2).
expectContentType(is(`text/html`)).
expectBody(decryptsTo(kr, `<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
</head>
<body>
<b>multipart 1.2</b>
</body>
</html>
`))
section(t, enc, 2).
expectContentType(is(`multipart/alternative`))
section(t, enc, 2, 1).
expectContentType(is(`text/plain`)).
expectBody(decryptsTo(kr, "*multipart 2.1*\n\n"))
section(t, enc, 2, 2).
expectContentType(is(`text/html`)).
expectBody(decryptsTo(kr, `<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
</head>
<body>
<b>multipart 2.2</b>
</body>
</html>
`))
}

View File

@ -59,30 +59,3 @@ func GetFlags(m *pmapi.Message) (flags []string) {
return
}
// ParseFlags sets attributes to pmapi messages based on imap flags.
func ParseFlags(m *pmapi.Message, flags []string) {
if m.Header.Get("received") == "" {
m.Flags = pmapi.FlagSent
} else {
m.Flags = pmapi.FlagReceived
}
m.Unread = true
for _, f := range flags {
switch f {
case imap.SeenFlag:
m.Unread = false
case imap.DraftFlag:
m.Flags &= ^pmapi.FlagSent
m.Flags &= ^pmapi.FlagReceived
m.LabelIDs = append(m.LabelIDs, pmapi.DraftLabel)
case imap.FlaggedFlag:
m.LabelIDs = append(m.LabelIDs, pmapi.StarredLabel)
case imap.AnsweredFlag:
m.Flags |= pmapi.FlagReplied
case AppleMailJunkFlag, ThunderbirdJunkFlag:
m.LabelIDs = append(m.LabelIDs, pmapi.SpamLabel)
}
}
}

150
pkg/message/header.go Normal file
View File

@ -0,0 +1,150 @@
// 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"
"io"
"io/ioutil"
"unicode"
"github.com/emersion/go-message/textproto"
"github.com/pkg/errors"
)
// HeaderLines returns each line in the given header.
func HeaderLines(header []byte) [][]byte {
var (
lines [][]byte
quote int
)
forEachLine(bufio.NewReader(bytes.NewReader(header)), func(line []byte) {
l := bytes.SplitN(line, []byte(`: `), 2)
isLineContinuation := quote%2 != 0 || // no quotes opened
!bytes.Equal(bytes.TrimLeftFunc(l[0], unicode.IsSpace), l[0]) // has whitespace indent at beginning
switch {
case len(bytes.TrimSpace(line)) == 0:
lines = append(lines, line)
case isLineContinuation:
if len(lines) > 0 {
lines[len(lines)-1] = append(lines[len(lines)-1], line...)
} else {
lines = append(lines, line)
}
default:
lines = append(lines, line)
}
quote += bytes.Count(line, []byte(`"`))
})
return lines
}
func forEachLine(br *bufio.Reader, fn func([]byte)) {
for {
b, err := br.ReadBytes('\n')
if err != nil {
if !errors.Is(err, io.EOF) {
panic(err)
}
if len(b) > 0 {
fn(b)
}
return
}
fn(b)
}
}
func readHeaderBody(b []byte) (*textproto.Header, []byte, error) {
rawHeader, body, err := splitHeaderBody(b)
if err != nil {
return nil, nil, err
}
lines := HeaderLines(rawHeader)
var header textproto.Header
// We assume that everything before first occurrence of empty line is header.
// If header is invalid for any reason or empty - put everything as body and let header be empty.
if !isHeaderValid(lines) {
return &header, b, nil
}
// We add lines in reverse so that calling textproto.WriteHeader later writes with the correct order.
for i := len(lines) - 1; i >= 0; i-- {
if len(bytes.TrimSpace(lines[i])) > 0 {
header.AddRaw(lines[i])
}
}
return &header, body, nil
}
func isHeaderValid(headerLines [][]byte) bool {
if len(headerLines) == 0 {
return false
}
for _, line := range headerLines {
if (bytes.IndexByte(line, ':') == -1) && (len(bytes.TrimSpace(line)) > 0) {
return false
}
}
return true
}
func splitHeaderBody(b []byte) ([]byte, []byte, error) {
br := bufio.NewReader(bytes.NewReader(b))
var header []byte
for {
b, err := br.ReadBytes('\n')
if err != nil {
if !errors.Is(err, io.EOF) {
panic(err)
}
break
}
header = append(header, b...)
if len(bytes.TrimSpace(b)) == 0 {
break
}
}
body, err := ioutil.ReadAll(br)
if err != nil && !errors.Is(err, io.EOF) {
return nil, nil, err
}
return header, body, nil
}

112
pkg/message/header_test.go Normal file
View File

@ -0,0 +1,112 @@
// 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 (
"testing"
"github.com/stretchr/testify/assert"
)
func TestHeaderLines(t *testing.T) {
want := [][]byte{
[]byte("To: somebody\r\n"),
[]byte("From: somebody else\r\n"),
[]byte("Subject: RE: this is\r\n\ta multiline field: with colon\r\n\tor: many: more: colons\r\n"),
[]byte("X-Special: \r\n\tNothing on the first line\r\n\tbut has something on the other lines\r\n"),
[]byte("\r\n"),
}
var header []byte
for _, line := range want {
header = append(header, line...)
}
assert.Equal(t, want, HeaderLines(header))
}
func TestHeaderLinesMultilineFilename(t *testing.T) {
const header = "Content-Type: application/msword; name=\"this is a very long\nfilename.doc\""
assert.Equal(t, [][]byte{
[]byte("Content-Type: application/msword; name=\"this is a very long\nfilename.doc\""),
}, HeaderLines([]byte(header)))
}
func TestHeaderLinesMultilineFilenameWithColon(t *testing.T) {
const header = "Content-Type: application/msword; name=\"this is a very long\nfilename: too long.doc\""
assert.Equal(t, [][]byte{
[]byte("Content-Type: application/msword; name=\"this is a very long\nfilename: too long.doc\""),
}, HeaderLines([]byte(header)))
}
func TestHeaderLinesMultilineFilenameWithColonAndNewline(t *testing.T) {
const header = "Content-Type: application/msword; name=\"this is a very long\nfilename: too long.doc\"\n"
assert.Equal(t, [][]byte{
[]byte("Content-Type: application/msword; name=\"this is a very long\nfilename: too long.doc\"\n"),
}, HeaderLines([]byte(header)))
}
func TestHeaderLinesMultipleMultilineFilenames(t *testing.T) {
const header = `Content-Type: application/msword; name="=E5=B8=B6=E6=9C=89=E5=A4=96=E5=9C=8B=E5=AD=97=E7=AC=A6=E7=9A=84=E9=99=84=E4=
=BB=B6.DOC"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="=E5=B8=B6=E6=9C=89=E5=A4=96=E5=9C=8B=E5=AD=97=E7=AC=A6=E7=9A=84=E9=99=84=E4=
=BB=B6.DOC"
Content-ID: <>
`
assert.Equal(t, [][]byte{
[]byte("Content-Type: application/msword; name=\"=E5=B8=B6=E6=9C=89=E5=A4=96=E5=9C=8B=E5=AD=97=E7=AC=A6=E7=9A=84=E9=99=84=E4=\n=BB=B6.DOC\"\n"),
[]byte("Content-Transfer-Encoding: base64\n"),
[]byte("Content-Disposition: attachment; filename=\"=E5=B8=B6=E6=9C=89=E5=A4=96=E5=9C=8B=E5=AD=97=E7=AC=A6=E7=9A=84=E9=99=84=E4=\n=BB=B6.DOC\"\n"),
[]byte("Content-ID: <>\n"),
}, HeaderLines([]byte(header)))
}
func TestReadHeaderBody(t *testing.T) {
const data = "key: value\r\n\r\nbody\n"
header, body, err := readHeaderBody([]byte(data))
assert.NoError(t, err)
assert.Equal(t, 1, header.Len())
assert.Equal(t, "value", header.Get("key"))
assert.Equal(t, []byte("body\n"), body)
}
func TestReadHeaderBodyWithoutHeader(t *testing.T) {
const data = "body\n"
header, body, err := readHeaderBody([]byte(data))
assert.NoError(t, err)
assert.Equal(t, 0, header.Len())
assert.Equal(t, []byte(data), body)
}
func TestReadHeaderBodyInvalidHeader(t *testing.T) {
const data = "value\r\n\r\nbody\n"
header, body, err := readHeaderBody([]byte(data))
assert.NoError(t, err)
assert.Equal(t, 0, header.Len())
assert.Equal(t, []byte(data), body)
}

View File

@ -14,30 +14,30 @@ import (
gomock "github.com/golang/mock/gomock"
)
// MockFetcher is a mock of Fetcher interface
// MockFetcher is a mock of Fetcher interface.
type MockFetcher struct {
ctrl *gomock.Controller
recorder *MockFetcherMockRecorder
}
// MockFetcherMockRecorder is the mock recorder for MockFetcher
// MockFetcherMockRecorder is the mock recorder for MockFetcher.
type MockFetcherMockRecorder struct {
mock *MockFetcher
}
// NewMockFetcher creates a new mock instance
// 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
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockFetcher) EXPECT() *MockFetcherMockRecorder {
return m.recorder
}
// GetAttachment mocks base method
// GetAttachment mocks base method.
func (m *MockFetcher) GetAttachment(arg0 context.Context, arg1 string) (io.ReadCloser, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetAttachment", arg0, arg1)
@ -46,13 +46,13 @@ func (m *MockFetcher) GetAttachment(arg0 context.Context, arg1 string) (io.ReadC
return ret0, ret1
}
// GetAttachment indicates an expected call of GetAttachment
// GetAttachment indicates an expected call of GetAttachment.
func (mr *MockFetcherMockRecorder) GetAttachment(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAttachment", reflect.TypeOf((*MockFetcher)(nil).GetAttachment), arg0, arg1)
}
// GetMessage mocks base method
// GetMessage mocks base method.
func (m *MockFetcher) GetMessage(arg0 context.Context, arg1 string) (*pmapi.Message, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetMessage", arg0, arg1)
@ -61,13 +61,13 @@ func (m *MockFetcher) GetMessage(arg0 context.Context, arg1 string) (*pmapi.Mess
return ret0, ret1
}
// GetMessage indicates an expected call of GetMessage
// GetMessage indicates an expected call of GetMessage.
func (mr *MockFetcherMockRecorder) GetMessage(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMessage", reflect.TypeOf((*MockFetcher)(nil).GetMessage), arg0, arg1)
}
// KeyRingForAddressID mocks base method
// KeyRingForAddressID mocks base method.
func (m *MockFetcher) KeyRingForAddressID(arg0 string) (*crypto.KeyRing, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "KeyRingForAddressID", arg0)
@ -76,7 +76,7 @@ func (m *MockFetcher) KeyRingForAddressID(arg0 string) (*crypto.KeyRing, error)
return ret0, ret1
}
// KeyRingForAddressID indicates an expected call of KeyRingForAddressID
// 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

@ -528,6 +528,9 @@ func parseAttachment(h message.Header) (*pmapi.Attachment, error) {
if att.Name == "" {
att.Name = mimeTypeParams["name"]
}
if att.Name == "" && mimeType == rfc822Message {
att.Name = "message.eml"
}
if att.Name == "" {
att.Name = "attachment.bin"
}

View File

@ -222,6 +222,22 @@ func TestParseTextPlainWithOctetAttachmentGoodFilename(t *testing.T) {
assert.Equal(t, "😁😂.txt", m.Attachments[0].Name)
}
func TestParseTextPlainWithRFC822Attachment(t *testing.T) {
f := getFileReader("text_plain_rfc822_attachment.eml")
m, _, plainBody, attReaders, err := Parse(f)
require.NoError(t, err)
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
assert.Equal(t, "body", m.Body)
assert.Equal(t, "body", plainBody)
assert.Len(t, attReaders, 1)
assert.Equal(t, "message.eml", m.Attachments[0].Name)
}
func TestParseTextPlainWithOctetAttachmentBadFilename(t *testing.T) {
f := getFileReader("text_plain_octet_attachment_bad_2231_filename.eml")

96
pkg/message/scanner.go Normal file
View File

@ -0,0 +1,96 @@
// 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"
"errors"
"io"
)
type partScanner struct {
r *bufio.Reader
boundary string
progress int
}
type part struct {
b []byte
offset int
}
func newPartScanner(r io.Reader, boundary string) (*partScanner, error) {
scanner := &partScanner{r: bufio.NewReader(r), boundary: boundary}
if _, _, err := scanner.readToBoundary(); err != nil {
return nil, err
}
return scanner, nil
}
func (s *partScanner) scanAll() ([]part, error) {
var parts []part
for {
offset := s.progress
b, more, err := s.readToBoundary()
if err != nil {
return nil, err
}
if !more {
return parts, nil
}
parts = append(parts, part{b: b, offset: offset})
}
}
func (s *partScanner) readToBoundary() ([]byte, bool, error) {
var res []byte
for {
line, err := s.r.ReadBytes('\n')
if err != nil {
if !errors.Is(err, io.EOF) {
return nil, false, err
}
if len(line) == 0 {
return nil, false, nil
}
}
s.progress += len(line)
switch {
case bytes.HasPrefix(bytes.TrimSpace(line), []byte("--"+s.boundary)):
return bytes.TrimSuffix(bytes.TrimSuffix(res, []byte("\n")), []byte("\r")), true, nil
case bytes.HasSuffix(bytes.TrimSpace(line), []byte(s.boundary+"--")):
return bytes.TrimSuffix(bytes.TrimSuffix(res, []byte("\n")), []byte("\r")), false, nil
default:
res = append(res, line...)
}
}
}

136
pkg/message/scanner_test.go Normal file
View File

@ -0,0 +1,136 @@
// 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 (
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestScanner(t *testing.T) {
const literal = `this part of the text should be ignored
--longrandomstring
body1
--longrandomstring
body2
--longrandomstring--
`
scanner, err := newPartScanner(strings.NewReader(literal), "longrandomstring")
require.NoError(t, err)
parts, err := scanner.scanAll()
require.NoError(t, err)
assert.Equal(t, "\nbody1\n", string(parts[0].b))
assert.Equal(t, "\nbody2\n", string(parts[1].b))
assert.Equal(t, "\nbody1\n", literal[parts[0].offset:parts[0].offset+len(parts[0].b)])
assert.Equal(t, "\nbody2\n", literal[parts[1].offset:parts[1].offset+len(parts[1].b)])
}
func TestScannerNested(t *testing.T) {
const literal = `This is the preamble. It is to be ignored, though it
is a handy place for mail composers to include an
explanatory note to non-MIME compliant readers.
--simple boundary
Content-type: multipart/mixed; boundary="nested boundary"
This is the preamble. It is to be ignored, though it
is a handy place for mail composers to include an
explanatory note to non-MIME compliant readers.
--nested boundary
Content-type: text/plain; charset=us-ascii
This part does not end with a linebreak.
--nested boundary
Content-type: text/plain; charset=us-ascii
This part does end with a linebreak.
--nested boundary--
--simple boundary
Content-type: text/plain; charset=us-ascii
This part does end with a linebreak.
--simple boundary--
This is the epilogue. It is also to be ignored.
`
scanner, err := newPartScanner(strings.NewReader(literal), "simple boundary")
require.NoError(t, err)
parts, err := scanner.scanAll()
require.NoError(t, err)
assert.Equal(t, `Content-type: multipart/mixed; boundary="nested boundary"
This is the preamble. It is to be ignored, though it
is a handy place for mail composers to include an
explanatory note to non-MIME compliant readers.
--nested boundary
Content-type: text/plain; charset=us-ascii
This part does not end with a linebreak.
--nested boundary
Content-type: text/plain; charset=us-ascii
This part does end with a linebreak.
--nested boundary--`, string(parts[0].b))
assert.Equal(t, `Content-type: text/plain; charset=us-ascii
This part does end with a linebreak.
`, string(parts[1].b))
}
func TestScannerNoFinalLinebreak(t *testing.T) {
const literal = `--nested boundary
Content-type: text/plain; charset=us-ascii
This part does not end with a linebreak.
--nested boundary
Content-type: text/plain; charset=us-ascii
This part does end with a linebreak.
--nested boundary--`
scanner, err := newPartScanner(strings.NewReader(literal), "nested boundary")
require.NoError(t, err)
parts, err := scanner.scanAll()
require.NoError(t, err)
assert.Equal(t, `Content-type: text/plain; charset=us-ascii
This part does not end with a linebreak.`, string(parts[0].b))
assert.Equal(t, `Content-type: text/plain; charset=us-ascii
This part does end with a linebreak.
`, string(parts[1].b))
}

View File

@ -103,8 +103,8 @@ func (bs *BodyStructure) parseAllChildSections(r io.Reader, currentPath []int, s
mediaType, params, _ := pmmime.ParseMediaType(info.Header.Get("Content-Type"))
// If multipart, call getAllParts, else read to count lines.
if (strings.HasPrefix(mediaType, "multipart/") || mediaType == "message/rfc822") && params["boundary"] != "" {
newPath := append(currentPath, 1)
if (strings.HasPrefix(mediaType, "multipart/") || mediaType == rfc822Message) && params["boundary"] != "" {
nextPath := getChildPath(currentPath)
var br *boundaryReader
br, err = newBoundaryReader(bodyReader, params["boundary"])
@ -121,9 +121,9 @@ func (bs *BodyStructure) parseAllChildSections(r io.Reader, currentPath []int, s
if err != nil {
break
}
err = bs.parseAllChildSections(part, newPath, start)
err = bs.parseAllChildSections(part, nextPath, start)
part.Reset()
newPath[len(newPath)-1]++
nextPath[len(nextPath)-1]++
}
br.reader = nil
@ -152,7 +152,7 @@ func (bs *BodyStructure) parseAllChildSections(r io.Reader, currentPath []int, s
(*bs)[path] = info
// Fix start of subsections.
newPath := append(currentPath, 1)
newPath := getChildPath(currentPath)
shift := info.Size - info.BSize
subInfo, err := bs.getInfo(newPath)
@ -197,6 +197,14 @@ func (bs *BodyStructure) parseAllChildSections(r io.Reader, currentPath []int, s
return nil
}
// getChildPath will return the first child path of parent path.
// NOTE: Return value can be used to iterate over parts so it is necessary to
// copy parrent values in order to not rewrite values in parent.
func getChildPath(parent []int) []int {
// append alloc inline is the fasted way to copy
return append(append(make([]int, 0, len(parent)+1), parent...), 1)
}
func stringPathFromInts(ints []int) (ret string) {
for i, n := range ints {
if i != 0 {
@ -212,6 +220,13 @@ func (bs *BodyStructure) hasInfo(sectionPath []int) bool {
return err == nil
}
func (bs *BodyStructure) getInfoCheckSection(sectionPath []int) (sectionInfo *SectionInfo, err error) {
if len(*bs) == 1 && len(sectionPath) == 1 && sectionPath[0] == 1 {
sectionPath = []int{}
}
return bs.getInfo(sectionPath)
}
func (bs *BodyStructure) getInfo(sectionPath []int) (sectionInfo *SectionInfo, err error) {
path := stringPathFromInts(sectionPath)
sectionInfo, ok := (*bs)[path]
@ -223,7 +238,7 @@ func (bs *BodyStructure) getInfo(sectionPath []int) (sectionInfo *SectionInfo, e
// GetSection returns bytes of section including MIME header.
func (bs *BodyStructure) GetSection(wholeMail io.ReadSeeker, sectionPath []int) (section []byte, err error) {
info, err := bs.getInfo(sectionPath)
info, err := bs.getInfoCheckSection(sectionPath)
if err != nil {
return
}
@ -232,7 +247,7 @@ func (bs *BodyStructure) GetSection(wholeMail io.ReadSeeker, sectionPath []int)
// GetSectionContent returns bytes of section content (excluding MIME header).
func (bs *BodyStructure) GetSectionContent(wholeMail io.ReadSeeker, sectionPath []int) (section []byte, err error) {
info, err := bs.getInfo(sectionPath)
info, err := bs.getInfoCheckSection(sectionPath)
if err != nil {
return
}
@ -251,8 +266,11 @@ func (bs *BodyStructure) GetMailHeaderBytes(wholeMail io.ReadSeeker) (header []b
}
func goToOffsetAndReadNBytes(wholeMail io.ReadSeeker, offset, length int) ([]byte, error) {
if length < 1 {
return nil, errors.New("requested non positive length")
if length == 0 {
return []byte{}, nil
}
if length < 0 {
return nil, errors.New("requested negative length")
}
if offset > 0 {
if _, err := wholeMail.Seek(int64(offset), io.SeekStart); err != nil {
@ -266,7 +284,7 @@ func goToOffsetAndReadNBytes(wholeMail io.ReadSeeker, offset, length int) ([]byt
// GetSectionHeader returns the mime header of specified section.
func (bs *BodyStructure) GetSectionHeader(sectionPath []int) (header textproto.MIMEHeader, err error) {
info, err := bs.getInfo(sectionPath)
info, err := bs.getInfoCheckSection(sectionPath)
if err != nil {
return
}
@ -275,7 +293,7 @@ func (bs *BodyStructure) GetSectionHeader(sectionPath []int) (header textproto.M
}
func (bs *BodyStructure) GetSectionHeaderBytes(wholeMail io.ReadSeeker, sectionPath []int) (header []byte, err error) {
info, err := bs.getInfo(sectionPath)
info, err := bs.getInfoCheckSection(sectionPath)
if err != nil {
return
}

View File

@ -20,6 +20,7 @@ package message
import (
"bytes"
"fmt"
"io/ioutil"
"net/textproto"
"path/filepath"
"runtime"
@ -70,7 +71,7 @@ func TestParseBodyStructure(t *testing.T) {
debug("%10s: %-50s %5s %5s %5s %5s", "section", "type", "start", "size", "bsize", "lines")
for _, path := range paths {
sec := (*bs)[path]
contentType := sec.Header.Get("Content-Type")
contentType := (*bs)[path].Header.Get("Content-Type")
debug("%10s: %-50s %5d %5d %5d %5d", path, contentType, sec.Start, sec.Size, sec.BSize, sec.Lines)
require.Equal(t, expectedStructure[path], contentType)
}
@ -78,10 +79,45 @@ func TestParseBodyStructure(t *testing.T) {
require.True(t, len(*bs) == len(expectedStructure), "Wrong number of sections expected %d but have %d", len(expectedStructure), len(*bs))
}
func TestParseBodyStructurePGP(t *testing.T) {
expectedStructure := map[string]string{
"": "multipart/signed; micalg=pgp-sha256; protocol=\"application/pgp-signature\"; boundary=\"MHEDFShwcX18dyE3X7RXujo5fjpgdjHNM\"",
"1": "multipart/mixed; boundary=\"FBBl2LNv76z8UkvHhSkT9vLwVwxqV8378\"; protected-headers=\"v1\"",
"1.1": "multipart/mixed; boundary=\"------------F97C8ED4878E94675762AE43\"",
"1.1.1": "multipart/alternative; boundary=\"------------041318B15DD3FA540FED32C6\"",
"1.1.1.1": "text/plain; charset=utf-8; format=flowed",
"1.1.1.2": "text/html; charset=utf-8",
"1.1.2": "application/pdf; name=\"minimal.pdf\"",
"1.1.3": "application/pgp-keys; name=\"OpenPGP_0x161C0875822359F7.asc\"",
"2": "application/pgp-signature; name=\"OpenPGP_signature.asc\"",
}
b, err := ioutil.ReadFile("testdata/enc-body-structure.eml")
require.NoError(t, err)
bs, err := NewBodyStructure(bytes.NewReader(b))
require.NoError(t, err)
haveStructure := map[string]string{}
for path := range *bs {
haveStructure[path] = (*bs)[path].Header.Get("Content-Type")
}
require.Equal(t, expectedStructure, haveStructure)
}
func TestGetSection(t *testing.T) {
structReader := strings.NewReader(sampleMail)
bs, err := NewBodyStructure(structReader)
require.NoError(t, err)
// Bad paths
wantPaths := [][]int{{0}, {-1}, {3, 2, 3}}
for _, wantPath := range wantPaths {
_, err = bs.getInfo(wantPath)
require.Error(t, err, "path %v", wantPath)
}
// Whole section.
for _, try := range testPaths {
mailReader := strings.NewReader(sampleMail)
@ -108,6 +144,60 @@ func TestGetSection(t *testing.T) {
}
}
func TestGetSecionNoMIMEParts(t *testing.T) {
wantBody := "This is just a simple mail with no multipart structure.\n"
wantHeader := `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: plain/text
`
wantMail := wantHeader + wantBody
r := require.New(t)
bs, err := NewBodyStructure(strings.NewReader(wantMail))
r.NoError(err)
// Bad parts
wantPaths := [][]int{{0}, {2}, {1, 2, 3}}
for _, wantPath := range wantPaths {
_, err = bs.getInfoCheckSection(wantPath)
r.Error(err, "path %v: %d %d\n__\n%s\n", wantPath)
}
debug := func(wantPath []int, info *SectionInfo, section []byte) string {
if info == nil {
info = &SectionInfo{}
}
return fmt.Sprintf("path %v %q: %d %d\n___\n%s\n‾‾‾\n",
wantPath, stringPathFromInts(wantPath), info.Start, info.Size,
string(section),
)
}
// Ok Parts
wantPaths = [][]int{{}, {1}}
for _, p := range wantPaths {
wantPath := append([]int{}, p...)
info, err := bs.getInfoCheckSection(wantPath)
r.NoError(err, debug(wantPath, info, []byte{}))
section, err := bs.GetSection(strings.NewReader(wantMail), wantPath)
r.NoError(err, debug(wantPath, info, section))
r.Equal(wantMail, string(section), debug(wantPath, info, section))
haveBody, err := bs.GetSectionContent(strings.NewReader(wantMail), wantPath)
r.NoError(err, debug(wantPath, info, haveBody))
r.Equal(wantBody, string(haveBody), debug(wantPath, info, haveBody))
haveHeader, err := bs.GetSectionHeaderBytes(strings.NewReader(wantMail), wantPath)
r.NoError(err, debug(wantPath, info, haveHeader))
r.Equal(wantHeader, string(haveHeader), debug(wantPath, info, haveHeader))
}
}
func TestGetMainHeaderBytes(t *testing.T) {
wantHeader := []byte(`Subject: Sample mail
From: John Doe <jdoe@machine.example>

View File

@ -0,0 +1,164 @@
Mime-Version: 1.0
Content-Type: multipart/signed; micalg=pgp-sha256;
protocol="application/pgp-signature";
boundary="MHEDFShwcX18dyE3X7RXujo5fjpgdjHNM"
Message-Id: <messageID@protonmail.internalid>
Date: Wed, 01 Jan 2020 00:00:00 +0000
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,8 @@
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>
Content-Type: text/plain; charset=iso-8859-2; format=flowed
Content-Language: en-US
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>

View File

@ -0,0 +1,3 @@
MalformedKey Value
How do we know that the ocean is friendly? It waves!

View File

@ -0,0 +1 @@
How do we know that the ocean is friendly? It waves!

View File

@ -0,0 +1,5 @@
From: Sender <sender@pm.me>
To: Receiver <receiver@pm.me>
Content-Transfer-Encoding: base64
Ym9keQ==

View File

@ -0,0 +1,16 @@
From: Sender <sender@pm.me>
To: Receiver <receiver@pm.me>
Content-Type: multipart/mixed; boundary=longrandomstring
--longrandomstring
body
--longrandomstring
Content-Type: message/rfc822
Content-Disposition: attachment
From: Sender <sender@pm.me>
To: Receiver <receiver@pm.me>
inner body
--longrandomstring--

48
pkg/message/writer.go Normal file
View File

@ -0,0 +1,48 @@
// 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"
"io"
)
type partWriter struct {
w io.Writer
boundary string
}
func newPartWriter(w io.Writer, boundary string) *partWriter {
return &partWriter{w: w, boundary: boundary}
}
func (w *partWriter) createPart(fn func(io.Writer) error) error {
if _, err := fmt.Fprintf(w.w, "\r\n--%v\r\n", w.boundary); err != nil {
return err
}
return fn(w.w)
}
func (w *partWriter) done() error {
if _, err := fmt.Fprintf(w.w, "\r\n--%v--\r\n", w.boundary); err != nil {
return err
}
return nil
}

View File

@ -33,16 +33,24 @@ import (
pmmime "github.com/ProtonMail/proton-bridge/pkg/mime"
a "github.com/stretchr/testify/assert"
r "github.com/stretchr/testify/require"
"github.com/stretchr/testify/require"
)
const testAttachmentCleartext = `cc,
dille.
`
// Attachment cleartext encrypted with testPrivateKeyRing.
const testKeyPacket = `wcBMA0fcZ7XLgmf2AQf/cHhfDRM9zlIuBi+h2W6DKjbbyIHMkgF6ER3JEvn/tSruUH8KTGt0N7Z+a80FFMCuXn1Y1I/nW7MVrNhGuJZAF4OymD8ugvuoAMIQX0eCYEpPXzRIWJBZg82AuowmFMsv8Dgvq4bTZq4cttI3CZcxKUNXuAearmNpmgplUKWj5USmRXK4iGB3VFGjidXkxbElrP4fD5A/rfEZ5aJgCsegqcXxX3MEjWXi9pFzgd/9phOvl1ZFm9U9hNoVAW3QsgmVeihnKaDZUyf2Qsigij21QKAUxw9U3y89eTUIqZAcmIgqeDujA3RWBgJwjtY/lOyhEmkf3AWKzehvf1xtJmCWDg==`
const testDataPacket = `0ksB6S4f4l8C1NB8yzmd/jNi0xqEZsyTDLdTP+N4Qxh3NZjla+yGRvC9rGmoUL7XVyowsG/GKTf2LXF/5E5FkX/3WMYwIv1n11ExyAE=`
var testAttachment = &Attachment{
ID: "y6uKIlc2HdoHPAwPSrvf7dXoZNMYvBgxshYUN67cY5DJjL2O8NYewuvGHcYvCfd8LpEoAI_GdymO0Jr0mHlsEw==",
Name: "croutonmail.txt",
Size: 77,
MIMEType: "text/plain",
KeyPackets: "wcBMA0fcZ7XLgmf2AQgAiRsOlnm1kSB4/lr7tYe6pBsRGn10GqwUhrwU5PMKOHdCgnO12jO3y3CzP0Yl/jGhAYja9wLDqH8X0sk3tY32u4Sb1Qe5IuzggAiCa4dwOJj5gEFMTHMzjIMPHR7A70XqUxMhmILye8V4KRm/j4c1sxbzA1rM3lYBumQuB5l/ck0Kgt4ZqxHVXHK5Q1l65FHhSXRj8qnunasHa30TYNzP8nmBA8BinnJxpiQ7FGc2umnUhgkFtjm5ixu9vyjr9ukwDTbwAXXfmY+o7tK7kqIXJcmTL6k2UeC6Mz1AagQtRCRtU+bv/3zGojq/trZo9lom3naIeQYa36Ketmcpj2Qwjg==",
KeyPackets: testKeyPacket,
Header: textproto.MIMEHeader{
"Content-Description": {"You'll never believe what's in this text file"},
"X-Mailer": {"Microsoft Outlook 15.0", "Microsoft Live Mail 42.0"},
@ -50,12 +58,13 @@ var testAttachment = &Attachment{
MessageID: "h3CD-DT7rLoAw1vmpcajvIPAl-wwDfXR2MHtWID3wuQURDBKTiGUAwd6E2WBbS44QQKeXImW-axm6X0hAfcVCA==",
}
// Part of GET /mail/messages/{id} response from server.
const testAttachmentJSON = `{
"ID": "y6uKIlc2HdoHPAwPSrvf7dXoZNMYvBgxshYUN67cY5DJjL2O8NYewuvGHcYvCfd8LpEoAI_GdymO0Jr0mHlsEw==",
"Name": "croutonmail.txt",
"Size": 77,
"MIMEType": "text/plain",
"KeyPackets": "wcBMA0fcZ7XLgmf2AQgAiRsOlnm1kSB4/lr7tYe6pBsRGn10GqwUhrwU5PMKOHdCgnO12jO3y3CzP0Yl/jGhAYja9wLDqH8X0sk3tY32u4Sb1Qe5IuzggAiCa4dwOJj5gEFMTHMzjIMPHR7A70XqUxMhmILye8V4KRm/j4c1sxbzA1rM3lYBumQuB5l/ck0Kgt4ZqxHVXHK5Q1l65FHhSXRj8qnunasHa30TYNzP8nmBA8BinnJxpiQ7FGc2umnUhgkFtjm5ixu9vyjr9ukwDTbwAXXfmY+o7tK7kqIXJcmTL6k2UeC6Mz1AagQtRCRtU+bv/3zGojq/trZo9lom3naIeQYa36Ketmcpj2Qwjg==",
"KeyPackets": "` + testKeyPacket + `",
"Headers": {
"content-description": "You'll never believe what's in this text file",
"x-mailer": [
@ -66,68 +75,66 @@ const testAttachmentJSON = `{
}
`
const testAttachmentCleartext = `cc,
dille.
`
const testAttachmentEncrypted = `wcBMA0fcZ7XLgmf2AQf/cHhfDRM9zlIuBi+h2W6DKjbbyIHMkgF6ER3JEvn/tSruUH8KTGt0N7Z+a80FFMCuXn1Y1I/nW7MVrNhGuJZAF4OymD8ugvuoAMIQX0eCYEpPXzRIWJBZg82AuowmFMsv8Dgvq4bTZq4cttI3CZcxKUNXuAearmNpmgplUKWj5USmRXK4iGB3VFGjidXkxbElrP4fD5A/rfEZ5aJgCsegqcXxX3MEjWXi9pFzgd/9phOvl1ZFm9U9hNoVAW3QsgmVeihnKaDZUyf2Qsigij21QKAUxw9U3y89eTUIqZAcmIgqeDujA3RWBgJwjtY/lOyhEmkf3AWKzehvf1xtJmCWDtJLAekuH+JfAtTQfMs5nf4zYtMahGbMkwy3Uz/jeEMYdzWY5WvshkbwvaxpqFC+11cqMLBvxik39i1xf+RORZF/91jGMCL9Z9dRMcgB`
const testCreateAttachmentBody = `{
// POST /mail/attachment/ response from server.
const testCreatedAttachmentBody = `{
"Code": 1000,
"Attachment": {"ID": "y6uKIlc2HdoHPAwPSrvf7dXoZNMYvBgxshYUN67cY5DJjL2O8NYewuvGHcYvCfd8LpEoAI_GdymO0Jr0mHlsEw=="}
}`
func TestAttachment_UnmarshalJSON(t *testing.T) {
r := require.New(t)
att := new(Attachment)
err := json.Unmarshal([]byte(testAttachmentJSON), att)
r.NoError(t, err)
r.NoError(err)
att.MessageID = testAttachment.MessageID // This isn't in the JSON object
att.MessageID = testAttachment.MessageID // This isn't in the server response
r.Equal(t, testAttachment, att)
r.Equal(testAttachment, att)
}
func TestClient_CreateAttachment(t *testing.T) {
r := require.New(t)
s, c := newTestClient(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
r.NoError(t, checkMethodAndPath(req, "POST", "/mail/v4/attachments"))
r.NoError(checkMethodAndPath(req, "POST", "/mail/v4/attachments"))
contentType, params, err := pmmime.ParseMediaType(req.Header.Get("Content-Type"))
r.NoError(t, err)
r.Equal(t, "multipart/form-data", contentType)
r.NoError(err)
r.Equal("multipart/form-data", contentType)
mr := multipart.NewReader(req.Body, params["boundary"])
form, err := mr.ReadForm(10 * 1024)
r.NoError(t, err)
defer r.NoError(t, form.RemoveAll())
r.NoError(err)
defer r.NoError(form.RemoveAll())
r.Equal(t, testAttachment.Name, form.Value["Filename"][0])
r.Equal(t, testAttachment.MessageID, form.Value["MessageID"][0])
r.Equal(t, testAttachment.MIMEType, form.Value["MIMEType"][0])
r.Equal(testAttachment.Name, form.Value["Filename"][0])
r.Equal(testAttachment.MessageID, form.Value["MessageID"][0])
r.Equal(testAttachment.MIMEType, form.Value["MIMEType"][0])
dataFile, err := form.File["DataPacket"][0].Open()
r.NoError(t, err)
defer r.NoError(t, dataFile.Close())
r.NoError(err)
defer r.NoError(dataFile.Close())
b, err := ioutil.ReadAll(dataFile)
r.NoError(t, err)
r.Equal(t, testAttachmentCleartext, string(b))
r.NoError(err)
r.Equal(testAttachmentCleartext, string(b))
w.Header().Set("Content-Type", "application/json")
fmt.Fprint(w, testCreateAttachmentBody)
fmt.Fprint(w, testCreatedAttachmentBody)
}))
defer s.Close()
reader := strings.NewReader(testAttachmentCleartext) // In reality, this thing is encrypted
created, err := c.CreateAttachment(context.Background(), testAttachment, reader, strings.NewReader(""))
r.NoError(t, err)
r.NoError(err)
r.Equal(t, testAttachment.ID, created.ID)
r.Equal(testAttachment.ID, created.ID)
}
func TestClient_GetAttachment(t *testing.T) {
r := require.New(t)
s, c := newTestClient(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
r.NoError(t, checkMethodAndPath(req, "GET", "/mail/v4/attachments/"+testAttachment.ID))
r.NoError(checkMethodAndPath(req, "GET", "/mail/v4/attachments/"+testAttachment.ID))
w.Header().Set("Content-Type", "application/json")
fmt.Fprint(w, testAttachmentCleartext)
@ -135,39 +142,61 @@ func TestClient_GetAttachment(t *testing.T) {
defer s.Close()
att, err := c.GetAttachment(context.Background(), testAttachment.ID)
r.NoError(t, err)
r.NoError(err)
defer att.Close() //nolint[errcheck]
// In reality, r contains encrypted data
b, err := ioutil.ReadAll(att)
r.NoError(t, err)
r.NoError(err)
r.Equal(t, testAttachmentCleartext, string(b))
r.Equal(testAttachmentCleartext, string(b))
}
func TestAttachment_Encrypt(t *testing.T) {
data := bytes.NewBufferString(testAttachmentCleartext)
r, err := testAttachment.Encrypt(testPublicKeyRing, data)
a.Nil(t, err)
b, err := ioutil.ReadAll(r)
a.Nil(t, err)
func TestAttachmentDecrypt(t *testing.T) {
r := require.New(t)
// Result is always different, so the best way is to test it by decrypting again.
// Another test for decrypting will help us to be sure it's working.
dataEnc := bytes.NewBuffer(b)
decryptAndCheck(t, dataEnc)
rawKeyPacket, err := base64.StdEncoding.DecodeString(testKeyPacket)
r.NoError(err)
rawDataPacket, err := base64.StdEncoding.DecodeString(testDataPacket)
r.NoError(err)
decryptAndCheck(r, bytes.NewBuffer(append(rawKeyPacket, rawDataPacket...)))
}
func TestAttachment_Decrypt(t *testing.T) {
dataBytes, _ := base64.StdEncoding.DecodeString(testAttachmentEncrypted)
dataReader := bytes.NewBuffer(dataBytes)
decryptAndCheck(t, dataReader)
func TestAttachmentEncrypt(t *testing.T) {
r := require.New(t)
encryptedReader, err := testAttachment.Encrypt(
testPublicKeyRing,
bytes.NewBufferString(testAttachmentCleartext),
)
r.NoError(err)
// The result is always different due to session key. The best way is to
// test result of encryption by decrypting again acn coparet to cleartext.
decryptAndCheck(r, encryptedReader)
}
func decryptAndCheck(t *testing.T, data io.Reader) {
r, err := testAttachment.Decrypt(data, testPrivateKeyRing)
a.Nil(t, err)
b, err := ioutil.ReadAll(r)
a.Nil(t, err)
a.Equal(t, testAttachmentCleartext, string(b))
func decryptAndCheck(r *require.Assertions, data io.Reader) {
// First separate KeyPacket from encrypted data. In our case keypacket
// has 271 bytes.
raw, err := ioutil.ReadAll(data)
r.NoError(err)
rawKeyPacket := raw[:271]
rawDataPacket := raw[271:]
// KeyPacket is retrieve by get GET /mail/messages/{id}
haveAttachment := &Attachment{
KeyPackets: base64.StdEncoding.EncodeToString(rawKeyPacket),
}
// DataPacket is received from GET /mail/attachments/{id}
decryptedReader, err := haveAttachment.Decrypt(bytes.NewBuffer(rawDataPacket), testPrivateKeyRing)
r.NoError(err)
b, err := ioutil.ReadAll(decryptedReader)
r.NoError(err)
r.Equal(testAttachmentCleartext, string(b))
}

View File

@ -217,3 +217,13 @@ func randomString(length int) string {
return base64.StdEncoding.EncodeToString(noise)[:length]
}
func (c *client) GetCurrentAuth() *Auth {
return &Auth{
UserID: c.user.ID,
AuthRefresh: AuthRefresh{
UID: c.uid,
RefreshToken: c.ref,
},
}
}

View File

@ -143,6 +143,51 @@ func Test401RevokedAuth(t *testing.T) {
r.EqualError(t, err, ErrUnauthorized.Error())
}
func Test401RevokedAuthTokenUpdate(t *testing.T) {
var oldAuth = &AuthRefresh{
UID: "UID",
AccessToken: "oldAcc",
RefreshToken: "oldRef",
ExpiresIn: 3600,
}
var newAuth = &AuthRefresh{
UID: "UID",
AccessToken: "newAcc",
RefreshToken: "newRef",
}
mux := http.NewServeMux()
mux.HandleFunc("/auth/refresh", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(newAuth); err != nil {
panic(err)
}
})
mux.HandleFunc("/addresses", func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Authorization") == ("Bearer " + oldAuth.AccessToken) {
w.WriteHeader(http.StatusUnauthorized)
return
}
if r.Header.Get("Authorization") == ("Bearer " + newAuth.AccessToken) {
w.WriteHeader(http.StatusOK)
return
}
})
ts := httptest.NewServer(mux)
c := New(Config{HostURL: ts.URL}).
NewClient(oldAuth.UID, oldAuth.AccessToken, oldAuth.RefreshToken, time.Now().Add(time.Hour))
// The request will fail with 401, triggering a refresh. After the refresh it should succeed.
_, err := c.GetAddresses(context.Background())
r.NoError(t, err)
}
func TestAuth2FA(t *testing.T) {
twoFACode := "code"

View File

@ -84,6 +84,8 @@ func (c *client) r(ctx context.Context) (*resty.Request, error) {
return r, nil
}
// do executes fn and may repeate it in case "401 Unauthorized" error is returned.
// Note: fn may be called more than once.
func (c *client) do(ctx context.Context, fn func(*resty.Request) (*resty.Response, error)) (*resty.Response, error) {
r, err := c.r(ctx)
if err != nil {
@ -102,6 +104,12 @@ func (c *client) do(ctx context.Context, fn func(*resty.Request) (*resty.Respons
return nil, err
}
// We need to reconstruct request since access token is changed with authRefresh.
r, err := c.r(ctx)
if err != nil {
return nil, err
}
return wrapNoConnection(fn(r))
}

View File

@ -64,29 +64,6 @@ var errVerificationFailed = errors.New("signature verification failed")
// ================= Public utility functions ======================
func (c *client) EncryptAndSignCards(cards []Card) ([]Card, error) {
var err error
for i := range cards {
card := &cards[i]
if isEncryptedCardType(card.Type) {
if isSignedCardType(card.Type) {
if card.Signature, err = c.sign(card.Data); err != nil {
return nil, err
}
}
if card.Data, err = c.encrypt(card.Data, nil); err != nil {
return nil, err
}
} else if isSignedCardType(card.Type) {
if card.Signature, err = c.sign(card.Data); err != nil {
return nil, err
}
}
}
return cards, nil
}
func (c *client) DecryptAndVerifyCards(cards []Card) ([]Card, error) {
for i := range cards {
card := &cards[i]
@ -136,7 +113,7 @@ func (c *client) GetContactEmailByEmail(ctx context.Context, email string, page
if pageSize != 0 {
r.SetQueryParam("PageSize", strconv.Itoa(pageSize))
}
return r.SetResult(&res).Get("/contacts/v4")
return r.SetResult(&res).Get("/contacts/v4/emails")
}); err != nil {
return nil, err
}

View File

@ -71,6 +71,29 @@ var testGetContactByIDResponseBody = `{
}
}`
var testGetContactEmailByEmailResponseBody = `{
"Code": 1000,
"ContactEmails": [
{
"ID": "aefew4323jFv0BhSMw==",
"Name": "ProtonMail Features",
"Email": "features@protonmail.black",
"Type": [
"work"
],
"Defaults": 1,
"Order": 1,
"ContactID": "a29olIjFv0rnXxBhSMw==",
"LabelIDs": [
"I6hgx3Ol-d3HYa3E394T_ACXDmTaBub14w=="
],
"CanonicalEmail": "features@protonmail.black",
"LastUsedTime": 1612546350
}
],
"Total": 2
}`
var testGetContactByID = Contact{
ID: "s_SN9y1q0jczjYCH4zhvfOdHv1QNovKhnJ9bpDcTE0u7WCr2Z-NV9uubHXvOuRozW-HRVam6bQupVYRMC3BCqg==",
Name: "Alice",
@ -105,6 +128,19 @@ var testGetContactByID = Contact{
LabelIDs: []string{},
}
var testGetContactEmailByEmail = []ContactEmail{
{
ID: "aefew4323jFv0BhSMw==",
Name: "ProtonMail Features",
Email: "features@protonmail.black",
Type: []string{"work"},
Defaults: 1,
Order: 1,
ContactID: "a29olIjFv0rnXxBhSMw==",
LabelIDs: []string{"I6hgx3Ol-d3HYa3E394T_ACXDmTaBub14w=="},
},
}
func TestContact_GetContactById(t *testing.T) {
s, c := newTestClient(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
r.NoError(t, checkMethodAndPath(req, "GET", "/contacts/v4/s_SN9y1q0jczjYCH4zhvfOdHv1QNovKhnJ9bpDcTE0u7WCr2Z-NV9uubHXvOuRozW-HRVam6bQupVYRMC3BCqg=="))
@ -122,6 +158,23 @@ func TestContact_GetContactById(t *testing.T) {
}
}
func TestContact_GetContactEmailByEmail(t *testing.T) {
s, c := newTestClient(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
r.NoError(t, checkMethodAndPath(req, "GET", "/contacts/v4/emails?Email=someone%40pm.me&Page=1&PageSize=10"))
w.Header().Set("Content-Type", "application/json")
fmt.Fprint(w, testGetContactEmailByEmailResponseBody)
}))
defer s.Close()
contact, err := c.GetContactEmailByEmail(context.Background(), "someone@pm.me", 1, 10)
r.NoError(t, err)
if !reflect.DeepEqual(contact, testGetContactEmailByEmail) {
t.Fatalf("Invalid got contact: expected %+v, got %+v", testGetContactByID, contact)
}
}
func TestContact_isSignedCardType(t *testing.T) {
if !isSignedCardType(SignedCard) || !isSignedCardType(EncryptedSignedCard) {
t.Fatal("isSignedCardType shouldn't return false for signed card types")
@ -156,20 +209,6 @@ var testCardsCleartext = []Card{
},
}
func TestClient_Encrypt(t *testing.T) {
c := newClient(newManager(Config{}), "")
c.userKeyRing = testPrivateKeyRing
cardEncrypted, err := c.EncryptAndSignCards(testCardsCleartext)
r.Nil(t, err)
// Result is always different, so the best way is to test it by decrypting again.
// Another test for decrypting will help us to be sure it's working.
cardCleartext, err := c.DecryptAndVerifyCards(cardEncrypted)
r.Nil(t, err)
r.Equal(t, testCardsCleartext[0].Data, cardCleartext[0].Data)
}
func TestClient_Decrypt(t *testing.T) {
c := newClient(newManager(Config{}), "")
c.userKeyRing = testPrivateKeyRing

View File

@ -36,8 +36,8 @@ type PMKey struct {
Fingerprint string
PrivateKey *crypto.Key
Primary int
Token *string `json:",omitempty"`
Signature *string `json:",omitempty"`
Token string
Signature string
}
type clearable []byte
@ -84,12 +84,12 @@ func (key PMKey) getPassphraseFromToken(kr *crypto.KeyRing) (passphrase []byte,
return nil, errors.New("no user key was provided")
}
msg, err := crypto.NewPGPMessageFromArmored(*key.Token)
msg, err := crypto.NewPGPMessageFromArmored(key.Token)
if err != nil {
return
}
sig, err := crypto.NewPGPSignatureFromArmored(*key.Signature)
sig, err := crypto.NewPGPSignatureFromArmored(key.Signature)
if err != nil {
return
}
@ -137,7 +137,7 @@ func (keys *PMKeys) UnlockAll(passphrase []byte, userKey *crypto.KeyRing) (kr *c
for _, key := range *keys {
var secret []byte
if key.Token == nil || key.Signature == nil {
if key.Token == "" || key.Signature == "" {
secret = passphrase
} else if secret, err = key.getPassphraseFromToken(userKey); err != nil {
return
@ -166,10 +166,6 @@ func (keys *PMKeys) UnlockAll(passphrase []byte, userKey *crypto.KeyRing) (kr *c
// ErrNoKeyringAvailable represents an error caused by a keyring being nil or having no entities.
var ErrNoKeyringAvailable = errors.New("no keyring available")
func (c *client) encrypt(plain string, signer *crypto.KeyRing) (armored string, err error) {
return encrypt(c.userKeyRing, plain, signer)
}
func encrypt(encrypter *crypto.KeyRing, plain string, signer *crypto.KeyRing) (armored string, err error) {
if encrypter == nil {
return "", ErrNoKeyringAvailable
@ -209,18 +205,6 @@ func decrypt(decrypter *crypto.KeyRing, armored string) (plainBody []byte, err e
return plainMessage.GetBinary(), nil
}
func (c *client) sign(plain string) (armoredSignature string, err error) {
if c.userKeyRing == nil {
return "", ErrNoKeyringAvailable
}
plainMessage := crypto.NewPlainMessageFromString(plain)
pgpSignature, err := c.userKeyRing.SignDetached(plainMessage)
if err != nil {
return
}
return pgpSignature.GetArmored()
}
func (c *client) verify(plain, amroredSignature string) (err error) {
plainMessage := crypto.NewPlainMessageFromString(plain)
pgpSignature, err := crypto.NewPGPSignatureFromArmored(amroredSignature)

View File

@ -22,7 +22,7 @@ import (
"testing"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func loadPMKeys(jsonKeys string) (keys *PMKeys) {
@ -31,6 +31,7 @@ func loadPMKeys(jsonKeys string) (keys *PMKeys) {
}
func TestPMKeys_GetKeyRingAndUnlock(t *testing.T) {
r := require.New(t)
addrKeysWithTokens := loadPMKeys(readTestFile("keyring_addressKeysWithTokens_JSON", false))
addrKeysWithoutTokens := loadPMKeys(readTestFile("keyring_addressKeysWithoutTokens_JSON", false))
addrKeysPrimaryHasToken := loadPMKeys(readTestFile("keyring_addressKeysPrimaryHasToken_JSON", false))
@ -42,7 +43,7 @@ func TestPMKeys_GetKeyRingAndUnlock(t *testing.T) {
}
userKey, err := crypto.NewKeyRing(key)
assert.NoError(t, err, "Expected not to receive an error unlocking user key")
r.NoError(err, "Expected not to receive an error unlocking user key")
type args struct {
userKeyring *crypto.KeyRing
@ -77,9 +78,7 @@ func TestPMKeys_GetKeyRingAndUnlock(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
kr, err := tt.keys.UnlockAll(tt.args.passphrase, tt.args.userKeyring) // nolint[scopelint]
if !assert.NoError(t, err) {
return
}
r.NoError(err)
// assert at least one key has been decrypted
atLeastOneDecrypted := false
@ -96,7 +95,21 @@ func TestPMKeys_GetKeyRingAndUnlock(t *testing.T) {
}
}
assert.True(t, atLeastOneDecrypted)
r.True(atLeastOneDecrypted)
})
}
}
func TestGopenpgpEncryptAttachment(t *testing.T) {
r := require.New(t)
wantMessage := crypto.NewPlainMessage([]byte(testAttachmentCleartext))
pgpSplitMessage, err := testPublicKeyRing.EncryptAttachment(wantMessage, "")
r.NoError(err)
haveMessage, err := testPrivateKeyRing.DecryptAttachment(pgpSplitMessage)
r.NoError(err)
r.Equal(wantMessage.Data, haveMessage.Data)
}

View File

@ -59,6 +59,7 @@ func newManager(cfg Config) *manager {
// wrapped in JSON. If error is returned, `handleRequestFailure` is called,
// otherwise `handleRequestSuccess` is called.
m.rc.SetError(&Error{})
m.rc.OnAfterResponse(updateTime)
m.rc.OnAfterResponse(m.catchAPIError)
m.rc.OnAfterResponse(m.handleRequestSuccess)
m.rc.OnError(m.handleRequestFailure)

View File

@ -22,7 +22,7 @@ import (
"encoding/base64"
"time"
"github.com/ProtonMail/proton-bridge/pkg/srp"
"github.com/ProtonMail/go-srp"
)
func (m *manager) NewClient(uid, acc, ref string, exp time.Time) Client {
@ -44,7 +44,7 @@ func (m *manager) NewClientWithRefresh(ctx context.Context, uid, ref string) (Cl
return c.withAuth(auth.AccessToken, auth.RefreshToken, expiresIn(auth.ExpiresIn)), auth, nil
}
func (m *manager) NewClientWithLogin(ctx context.Context, username, password string) (Client, *Auth, error) {
func (m *manager) NewClientWithLogin(ctx context.Context, username string, password []byte) (Client, *Auth, error) {
log.Trace("New client with login")
info, err := m.getAuthInfo(ctx, GetAuthInfoReq{Username: username})
@ -52,12 +52,12 @@ func (m *manager) NewClientWithLogin(ctx context.Context, username, password str
return nil, nil, err
}
srpAuth, err := srp.NewSrpAuth(info.Version, username, password, info.Salt, info.Modulus, info.ServerEphemeral)
srpAuth, err := srp.NewAuth(info.Version, username, password, info.Salt, info.Modulus, info.ServerEphemeral)
if err != nil {
return nil, nil, err
}
proofs, err := srpAuth.GenerateSrpProofs(2048)
proofs, err := srpAuth.GenerateProofs(2048)
if err != nil {
return nil, nil, err
}

View File

@ -29,7 +29,7 @@ import (
type Manager interface {
NewClient(string, string, string, time.Time) Client
NewClientWithRefresh(context.Context, string, string) (Client, *AuthRefresh, error)
NewClientWithLogin(context.Context, string, string) (Client, *Auth, error)
NewClientWithLogin(context.Context, string, []byte) (Client, *Auth, error)
DownloadAndVerify(kr *crypto.KeyRing, url, sig string) ([]byte, error)
ReportBug(context.Context, ReportBugReq) error

View File

@ -27,6 +27,7 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/mail"
"net/url"
@ -34,9 +35,11 @@ import (
"strconv"
"strings"
"github.com/ProtonMail/go-crypto/openpgp"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/go-resty/resty/v2"
"github.com/sirupsen/logrus"
"golang.org/x/crypto/openpgp/armor"
"golang.org/x/crypto/openpgp/packet"
)
@ -293,6 +296,54 @@ func (m *Message) Decrypt(kr *crypto.KeyRing) ([]byte, error) {
return body, nil
}
type Signature struct {
Hash string
Data []byte
}
func (m *Message) ExtractSignatures(kr *crypto.KeyRing) ([]Signature, error) {
var entities openpgp.EntityList
for _, key := range kr.GetKeys() {
entities = append(entities, key.GetEntity())
}
p, err := armor.Decode(strings.NewReader(m.Body))
if err != nil {
return nil, err
}
msg, err := openpgp.ReadMessage(p.Body, entities, nil, nil)
if err != nil {
return nil, err
}
if _, err := ioutil.ReadAll(msg.UnverifiedBody); err != nil {
return nil, err
}
if !msg.IsSigned {
return nil, nil
}
signatures := make([]Signature, 0, len(msg.UnverifiedSignatures))
for _, signature := range msg.UnverifiedSignatures {
buf := new(bytes.Buffer)
if err := signature.Serialize(buf); err != nil {
return nil, err
}
signatures = append(signatures, Signature{
Hash: signature.Hash.String(),
Data: buf.Bytes(),
})
}
return signatures, nil
}
func (m *Message) decryptLegacy(kr *crypto.KeyRing) (dec []byte, err error) {
randomKeyStart := strings.Index(m.Body, RandomKeyHeader) + len(RandomKeyHeader)
randomKeyEnd := strings.Index(m.Body, RandomKeyTail)

View File

@ -17,42 +17,42 @@ import (
logrus "github.com/sirupsen/logrus"
)
// MockClient is a mock of Client interface
// MockClient is a mock of Client interface.
type MockClient struct {
ctrl *gomock.Controller
recorder *MockClientMockRecorder
}
// MockClientMockRecorder is the mock recorder for MockClient
// MockClientMockRecorder is the mock recorder for MockClient.
type MockClientMockRecorder struct {
mock *MockClient
}
// NewMockClient creates a new mock instance
// NewMockClient creates a new mock instance.
func NewMockClient(ctrl *gomock.Controller) *MockClient {
mock := &MockClient{ctrl: ctrl}
mock.recorder = &MockClientMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockClient) EXPECT() *MockClientMockRecorder {
return m.recorder
}
// AddAuthRefreshHandler mocks base method
// AddAuthRefreshHandler mocks base method.
func (m *MockClient) AddAuthRefreshHandler(arg0 pmapi.AuthRefreshHandler) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "AddAuthRefreshHandler", arg0)
}
// AddAuthRefreshHandler indicates an expected call of AddAuthRefreshHandler
// AddAuthRefreshHandler indicates an expected call of AddAuthRefreshHandler.
func (mr *MockClientMockRecorder) AddAuthRefreshHandler(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddAuthRefreshHandler", reflect.TypeOf((*MockClient)(nil).AddAuthRefreshHandler), arg0)
}
// Addresses mocks base method
// Addresses mocks base method.
func (m *MockClient) Addresses() pmapi.AddressList {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Addresses")
@ -60,13 +60,13 @@ func (m *MockClient) Addresses() pmapi.AddressList {
return ret0
}
// Addresses indicates an expected call of Addresses
// Addresses indicates an expected call of Addresses.
func (mr *MockClientMockRecorder) Addresses() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Addresses", reflect.TypeOf((*MockClient)(nil).Addresses))
}
// Auth2FA mocks base method
// Auth2FA mocks base method.
func (m *MockClient) Auth2FA(arg0 context.Context, arg1 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Auth2FA", arg0, arg1)
@ -74,13 +74,13 @@ func (m *MockClient) Auth2FA(arg0 context.Context, arg1 string) error {
return ret0
}
// Auth2FA indicates an expected call of Auth2FA
// Auth2FA indicates an expected call of Auth2FA.
func (mr *MockClientMockRecorder) Auth2FA(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Auth2FA", reflect.TypeOf((*MockClient)(nil).Auth2FA), arg0, arg1)
}
// AuthDelete mocks base method
// AuthDelete mocks base method.
func (m *MockClient) AuthDelete(arg0 context.Context) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AuthDelete", arg0)
@ -88,13 +88,13 @@ func (m *MockClient) AuthDelete(arg0 context.Context) error {
return ret0
}
// AuthDelete indicates an expected call of AuthDelete
// AuthDelete indicates an expected call of AuthDelete.
func (mr *MockClientMockRecorder) AuthDelete(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthDelete", reflect.TypeOf((*MockClient)(nil).AuthDelete), arg0)
}
// AuthSalt mocks base method
// AuthSalt mocks base method.
func (m *MockClient) AuthSalt(arg0 context.Context) (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AuthSalt", arg0)
@ -103,13 +103,13 @@ func (m *MockClient) AuthSalt(arg0 context.Context) (string, error) {
return ret0, ret1
}
// AuthSalt indicates an expected call of AuthSalt
// AuthSalt indicates an expected call of AuthSalt.
func (mr *MockClientMockRecorder) AuthSalt(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthSalt", reflect.TypeOf((*MockClient)(nil).AuthSalt), arg0)
}
// CountMessages mocks base method
// CountMessages mocks base method.
func (m *MockClient) CountMessages(arg0 context.Context, arg1 string) ([]*pmapi.MessagesCount, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CountMessages", arg0, arg1)
@ -118,13 +118,13 @@ func (m *MockClient) CountMessages(arg0 context.Context, arg1 string) ([]*pmapi.
return ret0, ret1
}
// CountMessages indicates an expected call of CountMessages
// CountMessages indicates an expected call of CountMessages.
func (mr *MockClientMockRecorder) CountMessages(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CountMessages", reflect.TypeOf((*MockClient)(nil).CountMessages), arg0, arg1)
}
// CreateAttachment mocks base method
// CreateAttachment mocks base method.
func (m *MockClient) CreateAttachment(arg0 context.Context, arg1 *pmapi.Attachment, arg2, arg3 io.Reader) (*pmapi.Attachment, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateAttachment", arg0, arg1, arg2, arg3)
@ -133,13 +133,13 @@ func (m *MockClient) CreateAttachment(arg0 context.Context, arg1 *pmapi.Attachme
return ret0, ret1
}
// CreateAttachment indicates an expected call of CreateAttachment
// CreateAttachment indicates an expected call of CreateAttachment.
func (mr *MockClientMockRecorder) CreateAttachment(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateAttachment", reflect.TypeOf((*MockClient)(nil).CreateAttachment), arg0, arg1, arg2, arg3)
}
// CreateDraft mocks base method
// CreateDraft mocks base method.
func (m *MockClient) CreateDraft(arg0 context.Context, arg1 *pmapi.Message, arg2 string, arg3 int) (*pmapi.Message, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateDraft", arg0, arg1, arg2, arg3)
@ -148,13 +148,13 @@ func (m *MockClient) CreateDraft(arg0 context.Context, arg1 *pmapi.Message, arg2
return ret0, ret1
}
// CreateDraft indicates an expected call of CreateDraft
// CreateDraft indicates an expected call of CreateDraft.
func (mr *MockClientMockRecorder) CreateDraft(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateDraft", reflect.TypeOf((*MockClient)(nil).CreateDraft), arg0, arg1, arg2, arg3)
}
// CreateLabel mocks base method
// CreateLabel mocks base method.
func (m *MockClient) CreateLabel(arg0 context.Context, arg1 *pmapi.Label) (*pmapi.Label, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateLabel", arg0, arg1)
@ -163,13 +163,13 @@ func (m *MockClient) CreateLabel(arg0 context.Context, arg1 *pmapi.Label) (*pmap
return ret0, ret1
}
// CreateLabel indicates an expected call of CreateLabel
// CreateLabel indicates an expected call of CreateLabel.
func (mr *MockClientMockRecorder) CreateLabel(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateLabel", reflect.TypeOf((*MockClient)(nil).CreateLabel), arg0, arg1)
}
// CurrentUser mocks base method
// CurrentUser mocks base method.
func (m *MockClient) CurrentUser(arg0 context.Context) (*pmapi.User, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CurrentUser", arg0)
@ -178,13 +178,13 @@ func (m *MockClient) CurrentUser(arg0 context.Context) (*pmapi.User, error) {
return ret0, ret1
}
// CurrentUser indicates an expected call of CurrentUser
// CurrentUser indicates an expected call of CurrentUser.
func (mr *MockClientMockRecorder) CurrentUser(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CurrentUser", reflect.TypeOf((*MockClient)(nil).CurrentUser), arg0)
}
// DecryptAndVerifyCards mocks base method
// DecryptAndVerifyCards mocks base method.
func (m *MockClient) DecryptAndVerifyCards(arg0 []pmapi.Card) ([]pmapi.Card, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DecryptAndVerifyCards", arg0)
@ -193,13 +193,13 @@ func (m *MockClient) DecryptAndVerifyCards(arg0 []pmapi.Card) ([]pmapi.Card, err
return ret0, ret1
}
// DecryptAndVerifyCards indicates an expected call of DecryptAndVerifyCards
// DecryptAndVerifyCards indicates an expected call of DecryptAndVerifyCards.
func (mr *MockClientMockRecorder) DecryptAndVerifyCards(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DecryptAndVerifyCards", reflect.TypeOf((*MockClient)(nil).DecryptAndVerifyCards), arg0)
}
// DeleteLabel mocks base method
// DeleteLabel mocks base method.
func (m *MockClient) DeleteLabel(arg0 context.Context, arg1 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteLabel", arg0, arg1)
@ -207,13 +207,13 @@ func (m *MockClient) DeleteLabel(arg0 context.Context, arg1 string) error {
return ret0
}
// DeleteLabel indicates an expected call of DeleteLabel
// DeleteLabel indicates an expected call of DeleteLabel.
func (mr *MockClientMockRecorder) DeleteLabel(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteLabel", reflect.TypeOf((*MockClient)(nil).DeleteLabel), arg0, arg1)
}
// DeleteMessages mocks base method
// DeleteMessages mocks base method.
func (m *MockClient) DeleteMessages(arg0 context.Context, arg1 []string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteMessages", arg0, arg1)
@ -221,13 +221,13 @@ func (m *MockClient) DeleteMessages(arg0 context.Context, arg1 []string) error {
return ret0
}
// DeleteMessages indicates an expected call of DeleteMessages
// DeleteMessages indicates an expected call of DeleteMessages.
func (mr *MockClientMockRecorder) DeleteMessages(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteMessages", reflect.TypeOf((*MockClient)(nil).DeleteMessages), arg0, arg1)
}
// EmptyFolder mocks base method
// EmptyFolder mocks base method.
func (m *MockClient) EmptyFolder(arg0 context.Context, arg1, arg2 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "EmptyFolder", arg0, arg1, arg2)
@ -235,13 +235,13 @@ func (m *MockClient) EmptyFolder(arg0 context.Context, arg1, arg2 string) error
return ret0
}
// EmptyFolder indicates an expected call of EmptyFolder
// EmptyFolder indicates an expected call of EmptyFolder.
func (mr *MockClientMockRecorder) EmptyFolder(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EmptyFolder", reflect.TypeOf((*MockClient)(nil).EmptyFolder), arg0, arg1, arg2)
}
// GetAddresses mocks base method
// GetAddresses mocks base method.
func (m *MockClient) GetAddresses(arg0 context.Context) (pmapi.AddressList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetAddresses", arg0)
@ -250,13 +250,13 @@ func (m *MockClient) GetAddresses(arg0 context.Context) (pmapi.AddressList, erro
return ret0, ret1
}
// GetAddresses indicates an expected call of GetAddresses
// GetAddresses indicates an expected call of GetAddresses.
func (mr *MockClientMockRecorder) GetAddresses(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAddresses", reflect.TypeOf((*MockClient)(nil).GetAddresses), arg0)
}
// GetAttachment mocks base method
// GetAttachment mocks base method.
func (m *MockClient) GetAttachment(arg0 context.Context, arg1 string) (io.ReadCloser, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetAttachment", arg0, arg1)
@ -265,13 +265,13 @@ func (m *MockClient) GetAttachment(arg0 context.Context, arg1 string) (io.ReadCl
return ret0, ret1
}
// GetAttachment indicates an expected call of GetAttachment
// GetAttachment indicates an expected call of GetAttachment.
func (mr *MockClientMockRecorder) GetAttachment(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAttachment", reflect.TypeOf((*MockClient)(nil).GetAttachment), arg0, arg1)
}
// GetContactByID mocks base method
// GetContactByID mocks base method.
func (m *MockClient) GetContactByID(arg0 context.Context, arg1 string) (pmapi.Contact, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetContactByID", arg0, arg1)
@ -280,13 +280,13 @@ func (m *MockClient) GetContactByID(arg0 context.Context, arg1 string) (pmapi.Co
return ret0, ret1
}
// GetContactByID indicates an expected call of GetContactByID
// GetContactByID indicates an expected call of GetContactByID.
func (mr *MockClientMockRecorder) GetContactByID(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetContactByID", reflect.TypeOf((*MockClient)(nil).GetContactByID), arg0, arg1)
}
// GetContactEmailByEmail mocks base method
// GetContactEmailByEmail mocks base method.
func (m *MockClient) GetContactEmailByEmail(arg0 context.Context, arg1 string, arg2, arg3 int) ([]pmapi.ContactEmail, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetContactEmailByEmail", arg0, arg1, arg2, arg3)
@ -295,13 +295,13 @@ func (m *MockClient) GetContactEmailByEmail(arg0 context.Context, arg1 string, a
return ret0, ret1
}
// GetContactEmailByEmail indicates an expected call of GetContactEmailByEmail
// GetContactEmailByEmail indicates an expected call of GetContactEmailByEmail.
func (mr *MockClientMockRecorder) GetContactEmailByEmail(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetContactEmailByEmail", reflect.TypeOf((*MockClient)(nil).GetContactEmailByEmail), arg0, arg1, arg2, arg3)
}
// GetEvent mocks base method
// GetEvent mocks base method.
func (m *MockClient) GetEvent(arg0 context.Context, arg1 string) (*pmapi.Event, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetEvent", arg0, arg1)
@ -310,13 +310,13 @@ func (m *MockClient) GetEvent(arg0 context.Context, arg1 string) (*pmapi.Event,
return ret0, ret1
}
// GetEvent indicates an expected call of GetEvent
// GetEvent indicates an expected call of GetEvent.
func (mr *MockClientMockRecorder) GetEvent(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEvent", reflect.TypeOf((*MockClient)(nil).GetEvent), arg0, arg1)
}
// GetMailSettings mocks base method
// GetMailSettings mocks base method.
func (m *MockClient) GetMailSettings(arg0 context.Context) (pmapi.MailSettings, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetMailSettings", arg0)
@ -325,13 +325,13 @@ func (m *MockClient) GetMailSettings(arg0 context.Context) (pmapi.MailSettings,
return ret0, ret1
}
// GetMailSettings indicates an expected call of GetMailSettings
// GetMailSettings indicates an expected call of GetMailSettings.
func (mr *MockClientMockRecorder) GetMailSettings(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMailSettings", reflect.TypeOf((*MockClient)(nil).GetMailSettings), arg0)
}
// GetMessage mocks base method
// GetMessage mocks base method.
func (m *MockClient) GetMessage(arg0 context.Context, arg1 string) (*pmapi.Message, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetMessage", arg0, arg1)
@ -340,13 +340,13 @@ func (m *MockClient) GetMessage(arg0 context.Context, arg1 string) (*pmapi.Messa
return ret0, ret1
}
// GetMessage indicates an expected call of GetMessage
// GetMessage indicates an expected call of GetMessage.
func (mr *MockClientMockRecorder) GetMessage(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMessage", reflect.TypeOf((*MockClient)(nil).GetMessage), arg0, arg1)
}
// GetPublicKeysForEmail mocks base method
// GetPublicKeysForEmail mocks base method.
func (m *MockClient) GetPublicKeysForEmail(arg0 context.Context, arg1 string) ([]pmapi.PublicKey, bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetPublicKeysForEmail", arg0, arg1)
@ -356,13 +356,13 @@ func (m *MockClient) GetPublicKeysForEmail(arg0 context.Context, arg1 string) ([
return ret0, ret1, ret2
}
// GetPublicKeysForEmail indicates an expected call of GetPublicKeysForEmail
// GetPublicKeysForEmail indicates an expected call of GetPublicKeysForEmail.
func (mr *MockClientMockRecorder) GetPublicKeysForEmail(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPublicKeysForEmail", reflect.TypeOf((*MockClient)(nil).GetPublicKeysForEmail), arg0, arg1)
}
// Import mocks base method
// Import mocks base method.
func (m *MockClient) Import(arg0 context.Context, arg1 pmapi.ImportMsgReqs) ([]*pmapi.ImportMsgRes, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Import", arg0, arg1)
@ -371,13 +371,13 @@ func (m *MockClient) Import(arg0 context.Context, arg1 pmapi.ImportMsgReqs) ([]*
return ret0, ret1
}
// Import indicates an expected call of Import
// Import indicates an expected call of Import.
func (mr *MockClientMockRecorder) Import(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Import", reflect.TypeOf((*MockClient)(nil).Import), arg0, arg1)
}
// IsUnlocked mocks base method
// IsUnlocked mocks base method.
func (m *MockClient) IsUnlocked() bool {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "IsUnlocked")
@ -385,13 +385,13 @@ func (m *MockClient) IsUnlocked() bool {
return ret0
}
// IsUnlocked indicates an expected call of IsUnlocked
// IsUnlocked indicates an expected call of IsUnlocked.
func (mr *MockClientMockRecorder) IsUnlocked() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsUnlocked", reflect.TypeOf((*MockClient)(nil).IsUnlocked))
}
// KeyRingForAddressID mocks base method
// KeyRingForAddressID mocks base method.
func (m *MockClient) KeyRingForAddressID(arg0 string) (*crypto.KeyRing, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "KeyRingForAddressID", arg0)
@ -400,13 +400,13 @@ func (m *MockClient) KeyRingForAddressID(arg0 string) (*crypto.KeyRing, error) {
return ret0, ret1
}
// KeyRingForAddressID indicates an expected call of KeyRingForAddressID
// KeyRingForAddressID indicates an expected call of KeyRingForAddressID.
func (mr *MockClientMockRecorder) KeyRingForAddressID(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KeyRingForAddressID", reflect.TypeOf((*MockClient)(nil).KeyRingForAddressID), arg0)
}
// LabelMessages mocks base method
// LabelMessages mocks base method.
func (m *MockClient) LabelMessages(arg0 context.Context, arg1 []string, arg2 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "LabelMessages", arg0, arg1, arg2)
@ -414,13 +414,13 @@ func (m *MockClient) LabelMessages(arg0 context.Context, arg1 []string, arg2 str
return ret0
}
// LabelMessages indicates an expected call of LabelMessages
// LabelMessages indicates an expected call of LabelMessages.
func (mr *MockClientMockRecorder) LabelMessages(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LabelMessages", reflect.TypeOf((*MockClient)(nil).LabelMessages), arg0, arg1, arg2)
}
// ListLabels mocks base method
// ListLabels mocks base method.
func (m *MockClient) ListLabels(arg0 context.Context) ([]*pmapi.Label, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ListLabels", arg0)
@ -429,13 +429,13 @@ func (m *MockClient) ListLabels(arg0 context.Context) ([]*pmapi.Label, error) {
return ret0, ret1
}
// ListLabels indicates an expected call of ListLabels
// ListLabels indicates an expected call of ListLabels.
func (mr *MockClientMockRecorder) ListLabels(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListLabels", reflect.TypeOf((*MockClient)(nil).ListLabels), arg0)
}
// ListMessages mocks base method
// ListMessages mocks base method.
func (m *MockClient) ListMessages(arg0 context.Context, arg1 *pmapi.MessagesFilter) ([]*pmapi.Message, int, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ListMessages", arg0, arg1)
@ -445,13 +445,13 @@ func (m *MockClient) ListMessages(arg0 context.Context, arg1 *pmapi.MessagesFilt
return ret0, ret1, ret2
}
// ListMessages indicates an expected call of ListMessages
// ListMessages indicates an expected call of ListMessages.
func (mr *MockClientMockRecorder) ListMessages(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListMessages", reflect.TypeOf((*MockClient)(nil).ListMessages), arg0, arg1)
}
// MarkMessagesRead mocks base method
// MarkMessagesRead mocks base method.
func (m *MockClient) MarkMessagesRead(arg0 context.Context, arg1 []string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "MarkMessagesRead", arg0, arg1)
@ -459,13 +459,13 @@ func (m *MockClient) MarkMessagesRead(arg0 context.Context, arg1 []string) error
return ret0
}
// MarkMessagesRead indicates an expected call of MarkMessagesRead
// MarkMessagesRead indicates an expected call of MarkMessagesRead.
func (mr *MockClientMockRecorder) MarkMessagesRead(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MarkMessagesRead", reflect.TypeOf((*MockClient)(nil).MarkMessagesRead), arg0, arg1)
}
// MarkMessagesUnread mocks base method
// MarkMessagesUnread mocks base method.
func (m *MockClient) MarkMessagesUnread(arg0 context.Context, arg1 []string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "MarkMessagesUnread", arg0, arg1)
@ -473,13 +473,13 @@ func (m *MockClient) MarkMessagesUnread(arg0 context.Context, arg1 []string) err
return ret0
}
// MarkMessagesUnread indicates an expected call of MarkMessagesUnread
// MarkMessagesUnread indicates an expected call of MarkMessagesUnread.
func (mr *MockClientMockRecorder) MarkMessagesUnread(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MarkMessagesUnread", reflect.TypeOf((*MockClient)(nil).MarkMessagesUnread), arg0, arg1)
}
// ReloadKeys mocks base method
// ReloadKeys mocks base method.
func (m *MockClient) ReloadKeys(arg0 context.Context, arg1 []byte) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReloadKeys", arg0, arg1)
@ -487,13 +487,13 @@ func (m *MockClient) ReloadKeys(arg0 context.Context, arg1 []byte) error {
return ret0
}
// ReloadKeys indicates an expected call of ReloadKeys
// ReloadKeys indicates an expected call of ReloadKeys.
func (mr *MockClientMockRecorder) ReloadKeys(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReloadKeys", reflect.TypeOf((*MockClient)(nil).ReloadKeys), arg0, arg1)
}
// ReorderAddresses mocks base method
// ReorderAddresses mocks base method.
func (m *MockClient) ReorderAddresses(arg0 context.Context, arg1 []string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReorderAddresses", arg0, arg1)
@ -501,13 +501,13 @@ func (m *MockClient) ReorderAddresses(arg0 context.Context, arg1 []string) error
return ret0
}
// ReorderAddresses indicates an expected call of ReorderAddresses
// ReorderAddresses indicates an expected call of ReorderAddresses.
func (mr *MockClientMockRecorder) ReorderAddresses(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReorderAddresses", reflect.TypeOf((*MockClient)(nil).ReorderAddresses), arg0, arg1)
}
// SendMessage mocks base method
// SendMessage mocks base method.
func (m *MockClient) SendMessage(arg0 context.Context, arg1 string, arg2 *pmapi.SendMessageReq) (*pmapi.Message, *pmapi.Message, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SendMessage", arg0, arg1, arg2)
@ -517,13 +517,13 @@ func (m *MockClient) SendMessage(arg0 context.Context, arg1 string, arg2 *pmapi.
return ret0, ret1, ret2
}
// SendMessage indicates an expected call of SendMessage
// SendMessage indicates an expected call of SendMessage.
func (mr *MockClientMockRecorder) SendMessage(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendMessage", reflect.TypeOf((*MockClient)(nil).SendMessage), arg0, arg1, arg2)
}
// UnlabelMessages mocks base method
// UnlabelMessages mocks base method.
func (m *MockClient) UnlabelMessages(arg0 context.Context, arg1 []string, arg2 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UnlabelMessages", arg0, arg1, arg2)
@ -531,13 +531,13 @@ func (m *MockClient) UnlabelMessages(arg0 context.Context, arg1 []string, arg2 s
return ret0
}
// UnlabelMessages indicates an expected call of UnlabelMessages
// UnlabelMessages indicates an expected call of UnlabelMessages.
func (mr *MockClientMockRecorder) UnlabelMessages(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UnlabelMessages", reflect.TypeOf((*MockClient)(nil).UnlabelMessages), arg0, arg1, arg2)
}
// Unlock mocks base method
// Unlock mocks base method.
func (m *MockClient) Unlock(arg0 context.Context, arg1 []byte) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Unlock", arg0, arg1)
@ -545,13 +545,13 @@ func (m *MockClient) Unlock(arg0 context.Context, arg1 []byte) error {
return ret0
}
// Unlock indicates an expected call of Unlock
// Unlock indicates an expected call of Unlock.
func (mr *MockClientMockRecorder) Unlock(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Unlock", reflect.TypeOf((*MockClient)(nil).Unlock), arg0, arg1)
}
// UpdateLabel mocks base method
// UpdateLabel mocks base method.
func (m *MockClient) UpdateLabel(arg0 context.Context, arg1 *pmapi.Label) (*pmapi.Label, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateLabel", arg0, arg1)
@ -560,13 +560,13 @@ func (m *MockClient) UpdateLabel(arg0 context.Context, arg1 *pmapi.Label) (*pmap
return ret0, ret1
}
// UpdateLabel indicates an expected call of UpdateLabel
// UpdateLabel indicates an expected call of UpdateLabel.
func (mr *MockClientMockRecorder) UpdateLabel(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateLabel", reflect.TypeOf((*MockClient)(nil).UpdateLabel), arg0, arg1)
}
// UpdateUser mocks base method
// UpdateUser mocks base method.
func (m *MockClient) UpdateUser(arg0 context.Context) (*pmapi.User, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateUser", arg0)
@ -575,72 +575,72 @@ func (m *MockClient) UpdateUser(arg0 context.Context) (*pmapi.User, error) {
return ret0, ret1
}
// UpdateUser indicates an expected call of UpdateUser
// UpdateUser indicates an expected call of UpdateUser.
func (mr *MockClientMockRecorder) UpdateUser(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUser", reflect.TypeOf((*MockClient)(nil).UpdateUser), arg0)
}
// MockManager is a mock of Manager interface
// MockManager is a mock of Manager interface.
type MockManager struct {
ctrl *gomock.Controller
recorder *MockManagerMockRecorder
}
// MockManagerMockRecorder is the mock recorder for MockManager
// MockManagerMockRecorder is the mock recorder for MockManager.
type MockManagerMockRecorder struct {
mock *MockManager
}
// NewMockManager creates a new mock instance
// NewMockManager creates a new mock instance.
func NewMockManager(ctrl *gomock.Controller) *MockManager {
mock := &MockManager{ctrl: ctrl}
mock.recorder = &MockManagerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockManager) EXPECT() *MockManagerMockRecorder {
return m.recorder
}
// AddConnectionObserver mocks base method
// AddConnectionObserver mocks base method.
func (m *MockManager) AddConnectionObserver(arg0 pmapi.ConnectionObserver) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "AddConnectionObserver", arg0)
}
// AddConnectionObserver indicates an expected call of AddConnectionObserver
// AddConnectionObserver indicates an expected call of AddConnectionObserver.
func (mr *MockManagerMockRecorder) AddConnectionObserver(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddConnectionObserver", reflect.TypeOf((*MockManager)(nil).AddConnectionObserver), arg0)
}
// AllowProxy mocks base method
// AllowProxy mocks base method.
func (m *MockManager) AllowProxy() {
m.ctrl.T.Helper()
m.ctrl.Call(m, "AllowProxy")
}
// AllowProxy indicates an expected call of AllowProxy
// AllowProxy indicates an expected call of AllowProxy.
func (mr *MockManagerMockRecorder) AllowProxy() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AllowProxy", reflect.TypeOf((*MockManager)(nil).AllowProxy))
}
// DisallowProxy mocks base method
// DisallowProxy mocks base method.
func (m *MockManager) DisallowProxy() {
m.ctrl.T.Helper()
m.ctrl.Call(m, "DisallowProxy")
}
// DisallowProxy indicates an expected call of DisallowProxy
// DisallowProxy indicates an expected call of DisallowProxy.
func (mr *MockManagerMockRecorder) DisallowProxy() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DisallowProxy", reflect.TypeOf((*MockManager)(nil).DisallowProxy))
}
// DownloadAndVerify mocks base method
// DownloadAndVerify mocks base method.
func (m *MockManager) DownloadAndVerify(arg0 *crypto.KeyRing, arg1, arg2 string) ([]byte, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DownloadAndVerify", arg0, arg1, arg2)
@ -649,13 +649,13 @@ func (m *MockManager) DownloadAndVerify(arg0 *crypto.KeyRing, arg1, arg2 string)
return ret0, ret1
}
// DownloadAndVerify indicates an expected call of DownloadAndVerify
// DownloadAndVerify indicates an expected call of DownloadAndVerify.
func (mr *MockManagerMockRecorder) DownloadAndVerify(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DownloadAndVerify", reflect.TypeOf((*MockManager)(nil).DownloadAndVerify), arg0, arg1, arg2)
}
// NewClient mocks base method
// NewClient mocks base method.
func (m *MockManager) NewClient(arg0, arg1, arg2 string, arg3 time.Time) pmapi.Client {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "NewClient", arg0, arg1, arg2, arg3)
@ -663,14 +663,14 @@ func (m *MockManager) NewClient(arg0, arg1, arg2 string, arg3 time.Time) pmapi.C
return ret0
}
// NewClient indicates an expected call of NewClient
// NewClient indicates an expected call of NewClient.
func (mr *MockManagerMockRecorder) NewClient(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewClient", reflect.TypeOf((*MockManager)(nil).NewClient), arg0, arg1, arg2, arg3)
}
// NewClientWithLogin mocks base method
func (m *MockManager) NewClientWithLogin(arg0 context.Context, arg1, arg2 string) (pmapi.Client, *pmapi.Auth, error) {
// NewClientWithLogin mocks base method.
func (m *MockManager) NewClientWithLogin(arg0 context.Context, arg1 string, arg2 []byte) (pmapi.Client, *pmapi.Auth, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "NewClientWithLogin", arg0, arg1, arg2)
ret0, _ := ret[0].(pmapi.Client)
@ -679,13 +679,13 @@ func (m *MockManager) NewClientWithLogin(arg0 context.Context, arg1, arg2 string
return ret0, ret1, ret2
}
// NewClientWithLogin indicates an expected call of NewClientWithLogin
// NewClientWithLogin indicates an expected call of NewClientWithLogin.
func (mr *MockManagerMockRecorder) NewClientWithLogin(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewClientWithLogin", reflect.TypeOf((*MockManager)(nil).NewClientWithLogin), arg0, arg1, arg2)
}
// NewClientWithRefresh mocks base method
// NewClientWithRefresh mocks base method.
func (m *MockManager) NewClientWithRefresh(arg0 context.Context, arg1, arg2 string) (pmapi.Client, *pmapi.AuthRefresh, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "NewClientWithRefresh", arg0, arg1, arg2)
@ -695,13 +695,13 @@ func (m *MockManager) NewClientWithRefresh(arg0 context.Context, arg1, arg2 stri
return ret0, ret1, ret2
}
// NewClientWithRefresh indicates an expected call of NewClientWithRefresh
// NewClientWithRefresh indicates an expected call of NewClientWithRefresh.
func (mr *MockManagerMockRecorder) NewClientWithRefresh(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewClientWithRefresh", reflect.TypeOf((*MockManager)(nil).NewClientWithRefresh), arg0, arg1, arg2)
}
// ReportBug mocks base method
// ReportBug mocks base method.
func (m *MockManager) ReportBug(arg0 context.Context, arg1 pmapi.ReportBugReq) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReportBug", arg0, arg1)
@ -709,13 +709,13 @@ func (m *MockManager) ReportBug(arg0 context.Context, arg1 pmapi.ReportBugReq) e
return ret0
}
// ReportBug indicates an expected call of ReportBug
// ReportBug indicates an expected call of ReportBug.
func (mr *MockManagerMockRecorder) ReportBug(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReportBug", reflect.TypeOf((*MockManager)(nil).ReportBug), arg0, arg1)
}
// SendSimpleMetric mocks base method
// SendSimpleMetric mocks base method.
func (m *MockManager) SendSimpleMetric(arg0 context.Context, arg1, arg2, arg3 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SendSimpleMetric", arg0, arg1, arg2, arg3)
@ -723,55 +723,55 @@ func (m *MockManager) SendSimpleMetric(arg0 context.Context, arg1, arg2, arg3 st
return ret0
}
// SendSimpleMetric indicates an expected call of SendSimpleMetric
// SendSimpleMetric indicates an expected call of SendSimpleMetric.
func (mr *MockManagerMockRecorder) SendSimpleMetric(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendSimpleMetric", reflect.TypeOf((*MockManager)(nil).SendSimpleMetric), arg0, arg1, arg2, arg3)
}
// SetCookieJar mocks base method
// SetCookieJar mocks base method.
func (m *MockManager) SetCookieJar(arg0 http.CookieJar) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "SetCookieJar", arg0)
}
// SetCookieJar indicates an expected call of SetCookieJar
// SetCookieJar indicates an expected call of SetCookieJar.
func (mr *MockManagerMockRecorder) SetCookieJar(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetCookieJar", reflect.TypeOf((*MockManager)(nil).SetCookieJar), arg0)
}
// SetLogging mocks base method
// SetLogging mocks base method.
func (m *MockManager) SetLogging(arg0 *logrus.Entry, arg1 bool) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "SetLogging", arg0, arg1)
}
// SetLogging indicates an expected call of SetLogging
// SetLogging indicates an expected call of SetLogging.
func (mr *MockManagerMockRecorder) SetLogging(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetLogging", reflect.TypeOf((*MockManager)(nil).SetLogging), arg0, arg1)
}
// SetRetryCount mocks base method
// SetRetryCount mocks base method.
func (m *MockManager) SetRetryCount(arg0 int) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "SetRetryCount", arg0)
}
// SetRetryCount indicates an expected call of SetRetryCount
// SetRetryCount indicates an expected call of SetRetryCount.
func (mr *MockManagerMockRecorder) SetRetryCount(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetRetryCount", reflect.TypeOf((*MockManager)(nil).SetRetryCount), arg0)
}
// SetTransport mocks base method
// SetTransport mocks base method.
func (m *MockManager) SetTransport(arg0 http.RoundTripper) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "SetTransport", arg0)
}
// SetTransport indicates an expected call of SetTransport
// SetTransport indicates an expected call of SetTransport.
func (mr *MockManagerMockRecorder) SetTransport(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetTransport", reflect.TypeOf((*MockManager)(nil).SetTransport), arg0)

View File

@ -20,13 +20,14 @@ package pmapi
import (
"encoding/base64"
"github.com/jameskeane/bcrypt"
"github.com/ProtonMail/go-srp"
"github.com/pkg/errors"
)
func HashMailboxPassword(password, salt string) ([]byte, error) {
// HashMailboxPassword expectects 128bit long salt encoded by standard base64.
func HashMailboxPassword(password []byte, salt string) ([]byte, error) {
if salt == "" {
return []byte(password), nil
return password, nil
}
decodedSalt, err := base64.StdEncoding.DecodeString(salt)
@ -34,15 +35,10 @@ func HashMailboxPassword(password, salt string) ([]byte, error) {
return nil, errors.Wrap(err, "failed to decode salt")
}
encodedSalt := base64.NewEncoding("./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789").WithPadding(base64.NoPadding).EncodeToString(decodedSalt)
hashResult, err := bcrypt.Hash(password, "$2y$10$"+encodedSalt)
hash, err := srp.MailboxPassword(password, decodedSalt)
if err != nil {
return nil, errors.Wrap(err, "failed to bcrypt-hash password")
return nil, errors.Wrap(err, "failed to hash password")
}
if len(hashResult) != 60 {
return nil, errors.New("pmapi: invalid mailbox password hash")
}
return []byte(hashResult[len(hashResult)-31:]), nil
return hash[len(hash)-31:], nil
}

View File

@ -15,38 +15,30 @@
// 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
package pmapi
import (
"fmt"
"testing"
"github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/config/useragent"
"github.com/ProtonMail/proton-bridge/internal/serverutil/mocks"
imapserver "github.com/emersion/go-imap/server"
"github.com/stretchr/testify/require"
)
func TestIMAPServerTurnOffAndOnAgain(t *testing.T) {
func TestMailboxPassword(t *testing.T) {
// wantHash was generated with passprase and salt defined below. It
// should not change when changing implementation of the function.
wantHash := []byte("B5nwpsJQSTJ16ldr64Vdq6oeCCn32Fi")
// Valid salt is 128bit long (16bytes)
// $echo aaaabbbbccccdddd | base64
salt := "YWFhYWJiYmJjY2NjZGRkZAo="
passphrase := []byte("random")
r := require.New(t)
ts := mocks.NewTestServer(12345)
_, err := HashMailboxPassword(passphrase, "badsalt")
r.Error(err)
server := imapserver.New(nil)
server.Addr = fmt.Sprintf("%v:%v", bridge.Host, ts.WantPort)
s := &imapServer{
panicHandler: ts.PanicHandler,
server: server,
port: ts.WantPort,
eventListener: ts.EventListener,
userAgent: useragent.New(),
}
s.isRunning.Store(false)
r.True(ts.IsPortFree())
go s.ListenAndServe()
ts.RunServerTests(r)
haveHash, err := HashMailboxPassword(passphrase, salt)
r.NoError(err)
r.Equal(wantHash, haveHash)
}

View File

@ -23,6 +23,7 @@ import (
"strconv"
"time"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/go-resty/resty/v2"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
@ -71,6 +72,16 @@ func (m *manager) catchAPIError(_ *resty.Client, res *resty.Response) error {
return err
}
func updateTime(_ *resty.Client, res *resty.Response) error {
if date, err := time.Parse(time.RFC1123, res.Header().Get("Date")); err != nil {
log.WithError(err).Warning("Cannot parse header date")
} else {
crypto.UpdateTime(date.Unix())
}
return nil
}
func catchRetryAfter(_ *resty.Client, res *resty.Response) (time.Duration, error) {
if res.StatusCode() == http.StatusTooManyRequests {
if after := res.Header().Get("Retry-After"); after != "" {

View File

@ -1,107 +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 srp
import (
"bytes"
"crypto/md5" //nolint[gosec]
"crypto/sha512"
"encoding/base64"
"encoding/hex"
"errors"
"strings"
"github.com/jameskeane/bcrypt"
)
// BCryptHash function bcrypt algorithm to hash password with salt.
func BCryptHash(password string, salt string) (string, error) {
return bcrypt.Hash(password, salt)
}
// ExpandHash extends the byte data for SRP flow.
func ExpandHash(data []byte) []byte {
part0 := sha512.Sum512(append(data, 0))
part1 := sha512.Sum512(append(data, 1))
part2 := sha512.Sum512(append(data, 2))
part3 := sha512.Sum512(append(data, 3))
return bytes.Join([][]byte{
part0[:],
part1[:],
part2[:],
part3[:],
}, []byte{})
}
// HashPassword returns the hash of password argument. Based on version number
// following arguments are used in addition to password:
// * 0, 1, 2: userName and modulus
// * 3, 4: salt and modulus.
func HashPassword(authVersion int, password, userName string, salt, modulus []byte) ([]byte, error) {
switch authVersion {
case 4, 3:
return hashPasswordVersion3(password, salt, modulus)
case 2:
return hashPasswordVersion2(password, userName, modulus)
case 1:
return hashPasswordVersion1(password, userName, modulus)
case 0:
return hashPasswordVersion0(password, userName, modulus)
default:
return nil, errors.New("pmapi: unsupported auth version")
}
}
// CleanUserName returns the input string in lower-case without characters `_`,
// `.` and `-`.
func CleanUserName(userName string) string {
userName = strings.ReplaceAll(userName, "-", "")
userName = strings.ReplaceAll(userName, ".", "")
userName = strings.ReplaceAll(userName, "_", "")
return strings.ToLower(userName)
}
func hashPasswordVersion3(password string, salt, modulus []byte) (res []byte, err error) {
encodedSalt := base64.NewEncoding("./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789").WithPadding(base64.NoPadding).EncodeToString(append(salt, []byte("proton")...))
crypted, err := BCryptHash(password, "$2y$10$"+encodedSalt)
if err != nil {
return
}
return ExpandHash(append([]byte(crypted), modulus...)), nil
}
func hashPasswordVersion2(password, userName string, modulus []byte) (res []byte, err error) {
return hashPasswordVersion1(password, CleanUserName(userName), modulus)
}
func hashPasswordVersion1(password, userName string, modulus []byte) (res []byte, err error) {
prehashed := md5.Sum([]byte(strings.ToLower(userName))) //nolint[gosec]
encodedSalt := hex.EncodeToString(prehashed[:])
crypted, err := BCryptHash(password, "$2y$10$"+encodedSalt)
if err != nil {
return
}
return ExpandHash(append([]byte(crypted), modulus...)), nil
}
func hashPasswordVersion0(password, userName string, modulus []byte) (res []byte, err error) {
prehashed := sha512.Sum512([]byte(password))
return hashPasswordVersion1(base64.StdEncoding.EncodeToString(prehashed[:]), userName, modulus)
}

View File

@ -1,219 +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 srp
import (
"bytes"
"crypto/rand"
"encoding/base64"
"errors"
"math/big"
"golang.org/x/crypto/openpgp"
"golang.org/x/crypto/openpgp/clearsign"
)
//nolint[gochecknoglobals]
var (
ErrDataAfterModulus = errors.New("pm-srp: extra data after modulus")
ErrInvalidSignature = errors.New("pm-srp: invalid modulus signature")
RandReader = rand.Reader
)
// Store random reader in a variable to be able to overwrite it in tests
// Amored pubkey for modulus verification.
const modulusPubkey = `-----BEGIN PGP PUBLIC KEY BLOCK-----
xjMEXAHLgxYJKwYBBAHaRw8BAQdAFurWXXwjTemqjD7CXjXVyKf0of7n9Ctm
L8v9enkzggHNEnByb3RvbkBzcnAubW9kdWx1c8J3BBAWCgApBQJcAcuDBgsJ
BwgDAgkQNQWFxOlRjyYEFQgKAgMWAgECGQECGwMCHgEAAPGRAP9sauJsW12U
MnTQUZpsbJb53d0Wv55mZIIiJL2XulpWPQD/V6NglBd96lZKBmInSXX/kXat
Sv+y0io+LR8i2+jV+AbOOARcAcuDEgorBgEEAZdVAQUBAQdAeJHUz1c9+KfE
kSIgcBRE3WuXC4oj5a2/U3oASExGDW4DAQgHwmEEGBYIABMFAlwBy4MJEDUF
hcTpUY8mAhsMAAD/XQD8DxNI6E78meodQI+wLsrKLeHn32iLvUqJbVDhfWSU
WO4BAMcm1u02t4VKw++ttECPt+HUgPUq5pqQWe5Q2cW4TMsE
=Y4Mw
-----END PGP PUBLIC KEY BLOCK-----`
// ReadClearSignedMessage reads the clear text from signed message and verifies
// signature. There must be no data appended after signed message in input string.
// The message must be sign by key corresponding to `modulusPubkey`.
func ReadClearSignedMessage(signedMessage string) (string, error) {
modulusBlock, rest := clearsign.Decode([]byte(signedMessage))
if len(rest) != 0 {
return "", ErrDataAfterModulus
}
modulusKeyring, err := openpgp.ReadArmoredKeyRing(bytes.NewReader([]byte(modulusPubkey)))
if err != nil {
return "", errors.New("pm-srp: can not read modulus pubkey")
}
_, err = openpgp.CheckDetachedSignature(modulusKeyring, bytes.NewReader(modulusBlock.Bytes), modulusBlock.ArmoredSignature.Body, nil)
if err != nil {
return "", ErrInvalidSignature
}
return string(modulusBlock.Bytes), nil
}
// SrpProofs object.
type SrpProofs struct { //nolint[golint]
ClientProof, ClientEphemeral, ExpectedServerProof []byte
}
// SrpAuth stores byte data for the calculation of SRP proofs.
type SrpAuth struct { //nolint[golint]
Modulus, ServerEphemeral, HashedPassword []byte
}
// NewSrpAuth creates new SrpAuth from strings input. Salt and server ephemeral are in
// base64 format. Modulus is base64 with signature attached. The signature is
// verified against server key. The version controls password hash algorithm.
func NewSrpAuth(version int, username, password, salt, signedModulus, serverEphemeral string) (auth *SrpAuth, err error) {
data := &SrpAuth{}
// Modulus
var modulus string
modulus, err = ReadClearSignedMessage(signedModulus)
if err != nil {
return
}
data.Modulus, err = base64.StdEncoding.DecodeString(modulus)
if err != nil {
return
}
// Password
var decodedSalt []byte
if version >= 3 {
decodedSalt, err = base64.StdEncoding.DecodeString(salt)
if err != nil {
return
}
}
data.HashedPassword, err = HashPassword(version, password, username, decodedSalt, data.Modulus)
if err != nil {
return
}
// Server ephermeral
data.ServerEphemeral, err = base64.StdEncoding.DecodeString(serverEphemeral)
if err != nil {
return
}
return data, nil
}
// GenerateSrpProofs calculates SPR proofs.
func (s *SrpAuth) GenerateSrpProofs(length int) (res *SrpProofs, err error) { //nolint[funlen]
toInt := func(arr []byte) *big.Int {
var reversed = make([]byte, len(arr))
for i := 0; i < len(arr); i++ {
reversed[len(arr)-i-1] = arr[i]
}
return big.NewInt(0).SetBytes(reversed)
}
fromInt := func(num *big.Int) []byte {
var arr = num.Bytes()
var reversed = make([]byte, length/8)
for i := 0; i < len(arr); i++ {
reversed[len(arr)-i-1] = arr[i]
}
return reversed
}
generator := big.NewInt(2)
multiplier := toInt(ExpandHash(append(fromInt(generator), s.Modulus...)))
modulus := toInt(s.Modulus)
serverEphemeral := toInt(s.ServerEphemeral)
hashedPassword := toInt(s.HashedPassword)
modulusMinusOne := big.NewInt(0).Sub(modulus, big.NewInt(1))
if modulus.BitLen() != length {
return nil, errors.New("pm-srp: SRP modulus has incorrect size")
}
multiplier = multiplier.Mod(multiplier, modulus)
if multiplier.Cmp(big.NewInt(1)) <= 0 || multiplier.Cmp(modulusMinusOne) >= 0 {
return nil, errors.New("pm-srp: SRP multiplier is out of bounds")
}
if generator.Cmp(big.NewInt(1)) <= 0 || generator.Cmp(modulusMinusOne) >= 0 {
return nil, errors.New("pm-srp: SRP generator is out of bounds")
}
if serverEphemeral.Cmp(big.NewInt(1)) <= 0 || serverEphemeral.Cmp(modulusMinusOne) >= 0 {
return nil, errors.New("pm-srp: SRP server ephemeral is out of bounds")
}
// Check primality
// Doing exponentiation here is faster than a full call to ProbablyPrime while
// still perfectly accurate by Pocklington's theorem
if big.NewInt(0).Exp(big.NewInt(2), modulusMinusOne, modulus).Cmp(big.NewInt(1)) != 0 {
return nil, errors.New("pm-srp: SRP modulus is not prime")
}
// Check safe primality
if !big.NewInt(0).Rsh(modulus, 1).ProbablyPrime(10) {
return nil, errors.New("pm-srp: SRP modulus is not a safe prime")
}
var clientSecret, clientEphemeral, scramblingParam *big.Int
for {
for {
clientSecret, err = rand.Int(RandReader, modulusMinusOne)
if err != nil {
return
}
if clientSecret.Cmp(big.NewInt(int64(length*2))) > 0 { // Very likely
break
}
}
clientEphemeral = big.NewInt(0).Exp(generator, clientSecret, modulus)
scramblingParam = toInt(ExpandHash(append(fromInt(clientEphemeral), fromInt(serverEphemeral)...)))
if scramblingParam.Cmp(big.NewInt(0)) != 0 { // Very likely
break
}
}
subtracted := big.NewInt(0).Sub(serverEphemeral, big.NewInt(0).Mod(big.NewInt(0).Mul(big.NewInt(0).Exp(generator, hashedPassword, modulus), multiplier), modulus))
if subtracted.Cmp(big.NewInt(0)) < 0 {
subtracted.Add(subtracted, modulus)
}
exponent := big.NewInt(0).Mod(big.NewInt(0).Add(big.NewInt(0).Mul(scramblingParam, hashedPassword), clientSecret), modulusMinusOne)
sharedSession := big.NewInt(0).Exp(subtracted, exponent, modulus)
clientProof := ExpandHash(bytes.Join([][]byte{fromInt(clientEphemeral), fromInt(serverEphemeral), fromInt(sharedSession)}, []byte{}))
serverProof := ExpandHash(bytes.Join([][]byte{fromInt(clientEphemeral), clientProof, fromInt(sharedSession)}, []byte{}))
return &SrpProofs{ClientEphemeral: fromInt(clientEphemeral), ClientProof: clientProof, ExpectedServerProof: serverProof}, nil
}
// GenerateVerifier verifier for update pwds and create accounts.
func (s *SrpAuth) GenerateVerifier(length int) ([]byte, error) {
return nil, errors.New("pm-srp: the client doesn't need SRP GenerateVerifier")
}

View File

@ -1,111 +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 srp
import (
"bytes"
"encoding/base64"
"math/rand"
"testing"
)
const (
testServerEphemeral = "l13IQSVFBEV0ZZREuRQ4ZgP6OpGiIfIjbSDYQG3Yp39FkT2B/k3n1ZhwqrAdy+qvPPFq/le0b7UDtayoX4aOTJihoRvifas8Hr3icd9nAHqd0TUBbkZkT6Iy6UpzmirCXQtEhvGQIdOLuwvy+vZWh24G2ahBM75dAqwkP961EJMh67/I5PA5hJdQZjdPT5luCyVa7BS1d9ZdmuR0/VCjUOdJbYjgtIH7BQoZs+KacjhUN8gybu+fsycvTK3eC+9mCN2Y6GdsuCMuR3pFB0RF9eKae7cA6RbJfF1bjm0nNfWLXzgKguKBOeF3GEAsnCgK68q82/pq9etiUDizUlUBcA=="
testServerProof = "ffYFIhnhZJAflFJr9FfXbtdsBLkDGH+TUR5sj98wg0iVHyIhIVT6BeZD8tZA75tYlz7uYIanswweB3bjrGfITXfxERgQysQSoPUB284cX4VQm1IfTB/9LPma618MH8OULNluXVu2eizPWnvIn9VLXCaIX+38Xd6xOjmCQgfkpJy3Sh3ndikjqNCGWiKyvERVJi0nTmpAbHmcdeEp1K++ZRbebRhm2d018o/u4H2gu+MF39Hx12zMzEGNMwkNkgKSEQYlqmj57S6tW9JuB30zVZFnw6Krftg1QfJR6zCT1/J57OGp0A/7X/lC6Xz/I33eJvXOpG9GCRCbNiozFg9IXQ=="
testClientProof = "8dQtp6zIeEmu3D93CxPdEiCWiAE86uDmK33EpxyqReMwUrm/bTL+zCkWa/X7QgLNrt2FBAriyROhz5TEONgZq/PqZnBEBym6Rvo708KHu6S4LFdZkVc0+lgi7yQpNhU8bqB0BCqdSWd3Fjd3xbOYgO7/vnFK+p9XQZKwEh2RmGv97XHwoxefoyXK6BB+VVMkELd4vL7vdqBiOBU3ufOlSp+0XBMVltQ4oi5l1y21pzOA9cw5WTPIPMcQHffNFq/rReHYnqbBqiLlSLyw6K0PcVuN3bvr3rVYfdS1CsM/Rv1DzXlBUl39B2j82y6hdyGcTeplGyAnAcu0CimvynKBvQ=="
testModulus = "W2z5HBi8RvsfYzZTS7qBaUxxPhsfHJFZpu3Kd6s1JafNrCCH9rfvPLrfuqocxWPgWDH2R8neK7PkNvjxto9TStuY5z7jAzWRvFWN9cQhAKkdWgy0JY6ywVn22+HFpF4cYesHrqFIKUPDMSSIlWjBVmEJZ/MusD44ZT29xcPrOqeZvwtCffKtGAIjLYPZIEbZKnDM1Dm3q2K/xS5h+xdhjnndhsrkwm9U9oyA2wxzSXFL+pdfj2fOdRwuR5nW0J2NFrq3kJjkRmpO/Genq1UW+TEknIWAb6VzJJJA244K/H8cnSx2+nSNZO3bbo6Ys228ruV9A8m6DhxmS+bihN3ttQ=="
testModulusClearSign = `-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA256
W2z5HBi8RvsfYzZTS7qBaUxxPhsfHJFZpu3Kd6s1JafNrCCH9rfvPLrfuqocxWPgWDH2R8neK7PkNvjxto9TStuY5z7jAzWRvFWN9cQhAKkdWgy0JY6ywVn22+HFpF4cYesHrqFIKUPDMSSIlWjBVmEJZ/MusD44ZT29xcPrOqeZvwtCffKtGAIjLYPZIEbZKnDM1Dm3q2K/xS5h+xdhjnndhsrkwm9U9oyA2wxzSXFL+pdfj2fOdRwuR5nW0J2NFrq3kJjkRmpO/Genq1UW+TEknIWAb6VzJJJA244K/H8cnSx2+nSNZO3bbo6Ys228ruV9A8m6DhxmS+bihN3ttQ==
-----BEGIN PGP SIGNATURE-----
Version: ProtonMail
Comment: https://protonmail.com
wl4EARYIABAFAlwB1j0JEDUFhcTpUY8mAAD8CgEAnsFnF4cF0uSHKkXa1GIa
GO86yMV4zDZEZcDSJo0fgr8A/AlupGN9EdHlsrZLmTA1vhIx+rOgxdEff28N
kvNM7qIK
=q6vu
-----END PGP SIGNATURE-----`
)
func init() {
// Only for tests, replace the default random reader by something that always
// return the same thing
RandReader = rand.New(rand.NewSource(42))
}
func TestReadClearSigned(t *testing.T) {
cleartext, err := ReadClearSignedMessage(testModulusClearSign)
if err != nil {
t.Fatal("Expected no error but have ", err)
}
if cleartext != testModulus {
t.Fatalf("Expected message\n\t'%s'\nbut have\n\t'%s'", testModulus, cleartext)
}
lastChar := len(testModulusClearSign)
wrongSignature := testModulusClearSign[:lastChar-100]
wrongSignature += "c"
wrongSignature += testModulusClearSign[lastChar-99:]
_, err = ReadClearSignedMessage(wrongSignature)
if err != ErrInvalidSignature {
t.Fatal("Expected the ErrInvalidSignature but have ", err)
}
wrongSignature = testModulusClearSign + "data after modulus"
_, err = ReadClearSignedMessage(wrongSignature)
if err != ErrDataAfterModulus {
t.Fatal("Expected the ErrDataAfterModulus but have ", err)
}
}
func TestSRPauth(t *testing.T) {
srp, err := NewSrpAuth(4, "bridgetest", "test", "yKlc5/CvObfoiw==", testModulusClearSign, testServerEphemeral)
if err != nil {
t.Fatal("Expected no error but have ", err)
}
proofs, err := srp.GenerateSrpProofs(2048)
if err != nil {
t.Fatal("Expected no error but have ", err)
}
expectedProof, err := base64.StdEncoding.DecodeString(testServerProof)
if err != nil {
t.Fatal("Expected no error but have ", err)
}
if !bytes.Equal(proofs.ExpectedServerProof, expectedProof) {
t.Fatalf("Expected server proof\n\t'%s'\nbut have\n\t'%s'",
testServerProof,
base64.StdEncoding.EncodeToString(proofs.ExpectedServerProof),
)
}
expectedProof, err = base64.StdEncoding.DecodeString(testClientProof)
if err != nil {
t.Fatal("Expected no error but have ", err)
}
if !bytes.Equal(proofs.ClientProof, expectedProof) {
t.Fatalf("Expected client proof\n\t'%s'\nbut have\n\t'%s'",
testClientProof,
base64.StdEncoding.EncodeToString(proofs.ClientProof),
)
}
}

View File

@ -1,3 +1,84 @@
## v1.8.9
- 2021-09-01
### Fixed
- Fixed an issues with incorrect handling of 401 server error leading to random Bridge logouts
- Changed encoding of message/rfc822 - to better handle sending of the .msg files
- Fixed crash within RFC822 builder for invalid or empty headers
- Fixed crash within RFC822 builder for header with key length > 76 chars
## v1.8.7
- 2021-06-22
### New
- Updated crypto-libraries to gopenpgp/v2 v2.1.10
### Fixed
- Fixed IMAP/SMTP restart in Bridge to mitigate connection issues
- Fixed unknown charset error for 'combined' messages
- Implemented a long-term fix for 'invalid or missing message signature' error
## v1.8.5
- 2021-06-11
### New
- Updated golang Secure Remote Password Protocol
- Updated crypto-libraries to gopenpgp/v2 v2.1.9
- Implemented new message parser (for imports from external accounts)
### Fixed
- Bridge not to strip PGP signatures of incoming clear text messages
- Import of messages with malformed MIME header
- Improved parsing of message headers
- Fetching bodies of non-multipart messages
- Sync and performance improvements
## v1.8.3
- 2021-05-27
### Fixed
- A bug with sending encrypted emails to external contacts
## v1.8.2
- 2021-05-21
### Fixed
- Hotfix for error during bug reporting
## v1.8.1
- 2021-05-19
### Fixed
- Hotfix for crash when listing empty folder
## v1.8.0
- 2021-05-10
### New
- Implemented connection manager to improve performance during weak connection, better handling of connection loss and other connectivity issues
- Prompt profile installation during Apple Mail auto-configuration on MacOS Big Sur
### Fixed
- Bugs with building of message bodies/headers
- Incorrect naming format of some of the attachments
## v1.7.1
- 2021-04-27

View File

@ -1,3 +1,66 @@
## v1.8.7
- 2021-06-24
### New
- Updated golang Secure Remote Password Protocol
- Updated crypto-libraries to gopenpgp/v2 v2.1.10
- Implemented new message parser (for imports from external accounts)
### Fixed
- Fixed IMAP/SMTP restart in Bridge to mitigate connection issues
- Fixed unknown charset error for 'combined' messages
- Implemented a long-term fix for 'invalid or missing message signature' error
- Bridge not to strip PGP signatures of incoming clear text messages
- Import of messages with malformed MIME header
- Improved parsing of message headers
- Fetching bodies of non-multipart messages
- Sync and performance improvements
## v1.8.3
- 2021-05-27
### Fixed
- A bug with sending encrypted emails to external contacts
## v1.8.2
- 2021-05-21
### Fixed
- Hotfix for error during bug reporting
## v1.8.1
- 2021-05-19
### Fixed
- Hotfix for crash when listing empty folder
## v1.8.0
- 2021-05-17
### New
- Refactor of message builder to achieve greater RFC compliance
- Implemented connection manager to improve performance during weak connection, better handling of connection loss and other connectivity issues
- Increased the number of message fetchers to allow more parallel requests - performance improvement
- Log changes for easier debugging (update-related)
- Prompt profile installation during Apple Mail auto-configuration on MacOS Big Sur
### Fixed
- Bugs with building of message bodies/headers
- Incorrect naming format of some of the attachments
- Removed html-wrappig of non-decriptable messages - to facilitate decryption outside Bridge and/or allow to store such messages as they are
- Tray icon issues with multiple displays on MacOS
## v1.6.9
- 2021-04-01

View File

@ -1,3 +1,11 @@
## v1.3.3
- 2021-05-17
### Fixed
- Fixed potential security vulnerability related to rpath
- Improved parsing of embedded messages
## v1.3.1
- 2021-03-11

View File

@ -1,7 +1,7 @@
.PHONY: check-go check-godog install-godog test test-bridge test-ie test-live test-live-bridge test-live-ie test-stage test-debug test-live-debug bench
export GO111MODULE=on
export BRIDGE_VERSION:=1.5.5+integrationtests
export BRIDGE_VERSION:=1.8.9+integrationtests
export VERBOSITY?=fatal
export TEST_DATA=testdata
export TEST_APP?=bridge

View File

@ -177,12 +177,12 @@ func (a *TestAccount) EnsureAddress(addressOrAddressTestID string) string {
return addressOrAddressTestID
}
func (a *TestAccount) Password() string {
return a.password
func (a *TestAccount) Password() []byte {
return []byte(a.password)
}
func (a *TestAccount) MailboxPassword() string {
return a.mailboxPassword
func (a *TestAccount) MailboxPassword() []byte {
return []byte(a.mailboxPassword)
}
func (a *TestAccount) IsTwoFAEnabled() bool {

View File

@ -29,6 +29,9 @@ const (
)
func FeatureContext(s *godog.Suite) {
s.BeforeSuite(context.BeforeRun)
s.AfterSuite(context.AfterRun)
s.BeforeScenario(beforeScenario)
s.AfterScenario(afterScenario)

View File

@ -35,7 +35,7 @@ func benchTestContext() (*context.TestContext, *mocks.IMAPClient) {
panic("account " + username + " does not exist")
}
_ = ctx.GetPMAPIController().AddUser(account.User(), account.Addresses(), account.Password(), account.IsTwoFAEnabled())
_ = ctx.GetPMAPIController().AddUser(account)
if err := ctx.LoginUser(account.Username(), account.Password(), account.MailboxPassword()); err != nil {
panic(err)
}

View File

@ -94,8 +94,6 @@ type TestContext struct {
// New returns a new test TestContext.
func New(app string) *TestContext {
setLogrusVerbosityFromEnv()
listener := listener.New()
pmapiController, clientManager := newPMAPIController(app, listener)

View File

@ -51,7 +51,7 @@ func (c *fakeCredStore) List() (userIDs []string, err error) {
return keys, nil
}
func (c *fakeCredStore) Add(userID, userName, uid, ref, mailboxPassword string, emails []string) (*credentials.Credentials, error) {
func (c *fakeCredStore) Add(userID, userName, uid, ref string, mailboxPassword []byte, emails []string) (*credentials.Credentials, error) {
bridgePassword := bridgePassword
if c, ok := c.credentials[userID]; ok {
bridgePassword = c.BridgePassword
@ -80,7 +80,7 @@ func (c *fakeCredStore) UpdateEmails(userID string, emails []string) (*credentia
return c.credentials[userID], nil
}
func (c *fakeCredStore) UpdatePassword(userID, password string) (*credentials.Credentials, error) {
func (c *fakeCredStore) UpdatePassword(userID string, password []byte) (*credentials.Credentials, error) {
creds, err := c.Get(userID)
if err != nil {
return nil, err
@ -100,7 +100,7 @@ func (c *fakeCredStore) UpdateToken(userID, uid, ref string) (*credentials.Crede
func (c *fakeCredStore) Logout(userID string) (*credentials.Credentials, error) {
c.credentials[userID].APIToken = ""
c.credentials[userID].MailboxPassword = ""
c.credentials[userID].MailboxPassword = []byte{}
return c.credentials[userID], nil
}

40
test/context/globals.go Normal file
View File

@ -0,0 +1,40 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.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 context
import (
"os"
"github.com/ProtonMail/proton-bridge/test/liveapi"
)
// BeforeRun does necessary setup.
func BeforeRun() {
setLogrusVerbosityFromEnv()
if os.Getenv(EnvName) == EnvLive {
liveapi.SetupPersistentClients()
}
}
// AfterRun does necessary cleanup.
func AfterRun() {
if os.Getenv(EnvName) == EnvLive {
liveapi.CleanupPersistentClients()
}
}

View File

@ -23,6 +23,7 @@ import (
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/ProtonMail/proton-bridge/test/accounts"
"github.com/ProtonMail/proton-bridge/test/fakeapi"
"github.com/ProtonMail/proton-bridge/test/liveapi"
)
@ -30,7 +31,8 @@ import (
type PMAPIController interface {
TurnInternetConnectionOff()
TurnInternetConnectionOn()
AddUser(user *pmapi.User, addresses *pmapi.AddressList, password string, twoFAEnabled bool) error
GetAuthClient(username string) pmapi.Client
AddUser(account *accounts.TestAccount) error
AddUserLabel(username string, label *pmapi.Label) error
GetLabelIDs(username string, labelNames []string) ([]string, error)
AddUserMessage(username string, message *pmapi.Message) (string, error)

View File

@ -24,9 +24,10 @@ import (
"path/filepath"
"time"
"github.com/ProtonMail/go-srp"
"github.com/ProtonMail/proton-bridge/internal/store"
"github.com/ProtonMail/proton-bridge/internal/users"
"github.com/ProtonMail/proton-bridge/pkg/srp"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
)
@ -37,7 +38,7 @@ func (ctx *TestContext) GetUsers() *users.Users {
}
// LoginUser logs in the user with the given username, password, and mailbox password.
func (ctx *TestContext) LoginUser(username, password, mailboxPassword string) error {
func (ctx *TestContext) LoginUser(username string, password, mailboxPassword []byte) error {
srp.RandReader = rand.New(rand.NewSource(42)) //nolint[gosec] It is OK to use weaker random number generator here
client, auth, err := ctx.users.Login(username, password)
@ -61,6 +62,27 @@ func (ctx *TestContext) LoginUser(username, password, mailboxPassword string) er
return nil
}
// FinishLogin prevents authentication if not necessary.
func (ctx *TestContext) FinishLogin(client pmapi.Client, mailboxPassword []byte) error {
type currentAuthGetter interface {
GetCurrentAuth() *pmapi.Auth
}
c, ok := client.(currentAuthGetter)
if c == nil || !ok {
return errors.New("cannot get current auth tokens from client")
}
user, err := ctx.users.FinishLogin(client, c.GetCurrentAuth(), mailboxPassword)
if err != nil {
return errors.Wrap(err, "failed to finish login")
}
ctx.addCleanupChecked(user.Logout, "Logging out user")
return nil
}
// GetUser retrieves the bridge user matching the given query string.
func (ctx *TestContext) GetUser(username string) (*users.User, error) {
return ctx.users.GetUser(username)

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