Compare commits

...

35 Commits

Author SHA1 Message Date
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
885fb95454 Other: Bridge James v1.8.2 2021-05-21 07:16:17 +02:00
629d6c5e4d GODT-1175: report bug 2021-05-20 15:24:43 +02:00
3f50bf66f4 Merge branch 'release/james' into devel 2021-05-20 08:45:23 +02:00
4072205709 Other: version bump and changelog Bridge James 1.8.2 2021-05-20 08:38:33 +02:00
233c55ab19 Other: release notes Bridge James v1.8.1 2021-05-20 08:21:56 +02:00
5d82c218ca GODT-1165: Handle UID FETCH with sequence range of empty mailbox 2021-05-19 16:17:19 +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
80 changed files with 2120 additions and 1020 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,17 +2,60 @@
Changelog [format](http://keepachangelog.com/en/1.0.0/)
## [Bridge 1.8.5] James
### Fixed
* GODT-1189: Draft created on Outlook is synced on web.
* GODT-1190: Fix some random crashes of Bridge on Windows.
* GODT-1191: Fix data loss of some drafts messages when restarting outlook on Windows.
## [Bridge 1.8.4] James
### Added
* GODT-1155: Update gopenpgp v2.1.9 and use go-srp.
* GODT-1044: Lite parser for appended messages.
* GODT-1183: Add test for getting contact emails by email
* GODT-1184: Preserve signatures in externally signed messages.
### Changed
* GODT-949: Ignore some InvalidMediaParameter errors in lite parser.
### Fixed
* GODT-1161: Guarantee order of responses when creating new message.
* GODT-1162: Fix wrong section 1 error when email has no MIME parts.
## [Bridge 1.8.3] James
### Fixed
* GODT-1182: Use correct contact route.
## [Bridge 1.8.2] James
### Fixed
* GODT-1175: Bug reporting.
## [Bridge 1.8.1] James
### Fixed
* GODT-1165: Handle UID FETCH with sequence range of empty mailbox.
## [Bridge 1.8.0] James
### Added
* GODT-1056 Check encrypted size of the message before upload.
* GODT-1143 Turn off SMTP server while no connection.
* GODT-1089 Explicitly open system preferences window on BigSur.
* GODT-35: Connection manager with resty.
### Fixed
* GODT-1159 SMTP server not restarting after restored internet.
* GODT-1146 Refactor handling of fetching BODY[HEADER] (and similar) regarding trailing newline.
* GODT-1152 Correctly resolve wildcard sequence/UID set.
* GODT-876 Set default from if empty for importing draft.
* Other: Avoid API jail.

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.0+git
BRIDGE_APP_VERSION?=1.8.5+git
IE_APP_VERSION?=1.3.3+git
APP_VERSION:=${BRIDGE_APP_VERSION}
SRC_ICO:=logo.ico

19
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.9
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
@ -59,18 +59,17 @@ require (
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
github.com/stretchr/testify v1.6.1
github.com/therecipe/qt v0.0.0-20200701200531-7f61353ee73e
github.com/therecipe/qt/internal/binding/files/docs/5.12.0 v0.0.0-20200904063919-c0c124a5770d // indirect
github.com/therecipe/qt/internal/binding/files/docs/5.13.0 v0.0.0-20200904063919-c0c124a5770d // indirect
github.com/urfave/cli/v2 v2.2.0
github.com/vmihailenco/msgpack/v5 v5.1.3
go.etcd.io/bbolt v1.3.5
golang.org/x/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/jameskeane/bcrypt => github.com/ProtonMail/bcrypt v0.0.0-20210511135022-227b4adcab57
)

46
go.sum
View File

@ -8,28 +8,28 @@ 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-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.9 h1:MdvkFBP8ldOHYOoaVct9LO+Zv5rl6VdeN1QurntRmkc=
github.com/ProtonMail/gopenpgp/v2 v2.1.9/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 +73,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=
@ -262,11 +260,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=
github.com/therecipe/qt/internal/binding/files/docs/5.13.0 v0.0.0-20200904063919-c0c124a5770d/go.mod h1:mH55Ek7AZcdns5KPp99O0bg+78el64YCYWHiQKrOdt4=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
@ -292,10 +285,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 +316,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 +329,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 +337,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,8 +354,8 @@ golang.org/x/tools v0.0.0-20190327201419-c70d86f8b7cf/go.mod h1:LCzVGOaR6xXOjkQ3
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190909214602-067311248421/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69 h1:yBHHx+XZqXJBm6Exke3N7V9gnlsyXxoCPEb1yVenjfk=
golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

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,113 +63,92 @@ 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 m.Sender == nil {
im.log.Warning("Append: Missing email sender. Will use main address")
m.Sender = &mail.Address{
Name: "",
Address: addr.Email,
}
}
// "Drafts" needs to call special API routes.
// Clients always append the whole message again and remove the old one.
if im.storeMailbox.LabelID() == pmapi.DraftLabel {
// Sender address needs to be sanitised (drafts need to match cases exactly).
m.Sender.Address = pmapi.ConstructAddress(m.Sender.Address, addr.Email)
draft, _, err := im.user.storeUser.CreateDraft(kr, m, readers, "", "", "")
if err != nil {
return errors.Wrap(err, "failed to create draft")
}
targetSeq := im.storeMailbox.GetUIDList([]string{draft.ID})
return uidplus.AppendResponse(im.storeMailbox.UIDValidity(), targetSeq)
return im.createDraftMessage(kr, addr.Email, body)
}
// 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)
m, _, _, _, err := message.Parse(bytes.NewReader(body))
if err != nil {
return err
}
// 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 m.Sender == nil {
m.Sender = &mail.Address{Address: addr.Email}
}
if user, err := im.user.backend.bridge.GetUser(pmapi.SanitizeEmail(m.Sender.Address)); err == nil && user.ID() == im.storeUser.UserID() {
logEntry := im.log.WithField("sender", m.Sender).WithField("extID", m.Header.Get("Message-Id")).WithField("date", date)
// If 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")
logEntry.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)
hdr, err := textproto.ReadHeader(bufio.NewReader(bytes.NewReader(body)))
if err != nil {
return err
}
// Avoid appending a message which is already on the server. Apply the new label instead.
// This always happens with Outlook because it uses APPEND instead of COPY.
internalID := hdr.Get("X-Pm-Internal-Id")
// In case there is a mail client which corrupts headers, try "References" too.
if internalID == "" {
if references := strings.Fields(hdr.Get("References")); len(references) > 0 {
if match := pmapi.RxInternalReferenceFormat.FindStringSubmatch(references[len(references)-1]); len(match) == 2 {
internalID = match[1]
}
}
}
if internalID != "" {
if msg, err := im.storeMailbox.GetMessage(internalID); err == nil {
if im.user.user.IsCombinedAddressMode() || im.storeAddress.AddressID() == msg.Message().AddressID {
return im.labelExistingMessage(msg.ID(), msg.IsMarkedDeleted())
}
}
}
return im.importMessage(kr, hdr, body, imapFlags, date)
}
func (im *imapMailbox) createDraftMessage(kr *crypto.KeyRing, email string, body []byte) error {
im.log.Info("Creating draft message")
m, _, _, readers, err := message.Parse(bytes.NewReader(body))
if err != nil {
return err
}
if m.Sender == nil {
m.Sender = &mail.Address{}
}
m.Sender.Address = pmapi.ConstructAddress(m.Sender.Address, email)
draft, _, err := im.user.storeUser.CreateDraft(kr, m, readers, "", "", "")
if err != nil {
return errors.Wrap(err, "failed to create draft")
}
return uidplus.AppendResponse(im.storeMailbox.UIDValidity(), im.storeMailbox.GetUIDList([]string{draft.ID}))
}
func (im *imapMailbox) labelExistingMessage(messageID string, isDeleted bool) error {
im.log.Info("Labelling existing message")
// IMAP clients can move message to local folder (setting \Deleted flag)
// and then move it back (IMAP client does not remember the message,
// so instead removing the flag it imports duplicate message).
@ -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
}
return uidplus.AppendResponse(im.storeMailbox.UIDValidity(), im.storeMailbox.GetUIDList([]string{messageID}))
}
func (im *imapMailbox) importMessage(m *pmapi.Message, readers []io.Reader, kr *crypto.KeyRing) (err error) {
body, err := message.BuildEncrypted(m, readers, kr)
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

@ -31,12 +31,12 @@ import (
"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"
@ -94,7 +94,7 @@ func NewIMAPServer(panicHandler panicHandler, debugClient, debugServer bool, por
})
s.Enable(
imapidle.NewExtension(),
idle.NewExtension(),
imapmove.NewExtension(),
id.NewExtension(serverID, userAgent),
imapquota.NewExtension(),

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

@ -72,7 +72,7 @@ func redirectInternetEventsToOneChannel(l listener.Listener) (isInternetOn chan
const (
recheckPortAfter = 50 * time.Millisecond
stopPortChecksAfter = 15 * time.Second
retryListnerAfter = 5 * time.Second
retryListenerAfter = 5 * time.Second
)
func monitorInternetConnection(s Server, l listener.Listener) {
@ -89,7 +89,7 @@ func monitorInternetConnection(s Server, l listener.Listener) {
// 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)
s.ListenRetryAndServe(10, retryListenerAfter)
}()
expectedIsPortFree = false
} else {

View File

@ -38,6 +38,11 @@ func (storeMailbox *Mailbox) GetAPIIDsFromUIDRange(start, stop uint32) (apiIDs [
b := storeMailbox.txGetIMAPIDsBucket(tx)
c := b.Cursor()
// GODT-1153 If the mailbox is empty we should reply BAD to client.
if uid, _ := c.Last(); uid == nil {
return nil
}
// If the start range is a wildcard, the range can only refer to the last message in the mailbox.
if start == 0 {
_, apiID := c.Last()
@ -74,6 +79,11 @@ func (storeMailbox *Mailbox) GetAPIIDsFromSequenceRange(start, stop uint32) (api
b := storeMailbox.txGetIMAPIDsBucket(tx)
c := b.Cursor()
// GODT-1153 If the mailbox is empty we should reply BAD to client.
if uid, _ := c.Last(); uid == nil {
return nil
}
// If the start range is a wildcard, the range can only refer to the last message in the mailbox.
if start == 0 {
_, apiID := c.Last()
@ -318,5 +328,10 @@ func (storeMailbox *Mailbox) GetUIDByHeader(header *mail.Header) (foundUID uint3
func (storeMailbox *Mailbox) txGetFinalUID(b *bolt.Bucket) uint32 {
uid, _ := b.Cursor().Last()
if uid == nil {
panic(errors.New("cannot get final UID of empty mailbox"))
}
return btoi(uid)
}

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,14 +420,18 @@ func (storeMailbox *Mailbox) txCreateOrUpdateMessages(tx *bolt.Tx, msgs []*pmapi
if err != nil {
return errors.Wrap(err, "cannot get sequence number from UID")
}
storeMailbox.store.notifyUpdateMessage(
storeMailbox.storeAddress.address,
storeMailbox.labelName,
uid,
seqNum,
msg,
false, // new message is never marked as deleted
)
updates = append(updates, func() {
storeMailbox.store.notifyUpdateMessage(
storeMailbox.storeAddress.address,
storeMailbox.labelName,
uid,
seqNum,
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

@ -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
@ -58,15 +58,15 @@ type Credentials struct {
func (s *Credentials) Marshal() string {
items := []string{
s.Name, // 0
s.Emails, // 1
s.APIToken, // 2
s.MailboxPassword, // 3
s.BridgePassword, // 4
s.Version, // 5
"", // 6
"", // 7
"", // 8
s.Name, // 0
s.Emails, // 1
s.APIToken, // 2
string(s.MailboxPassword), // 3
s.BridgePassword, // 4
s.Version, // 5
"", // 6
"", // 7
"", // 8
}
items[6] = fmt.Sprint(s.Timestamp)
@ -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

@ -39,7 +39,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 +108,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()

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

@ -108,7 +108,7 @@ func (m *MockCredentialsStorer) EXPECT() *MockCredentialsStorerMockRecorder {
}
// Add mocks base method
func (m *MockCredentialsStorer) Add(arg0, arg1, arg2, arg3, arg4 string, arg5 []string) (*credentials.Credentials, error) {
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)
@ -212,7 +212,7 @@ func (mr *MockCredentialsStorerMockRecorder) UpdateEmails(arg0, arg1 interface{}
}
// UpdatePassword mocks base method
func (m *MockCredentialsStorer) UpdatePassword(arg0, arg1 string) (*credentials.Credentials, error) {
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)

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

@ -133,7 +133,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{

View File

@ -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
}
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
return writeMultipartEncryptedRFC822(hdr, dec)
}
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,108 @@ 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)
header.SetContentType("multipart/signed", map[string]string{
"micalg": sig.Hash,
"protocol": "application/pgp-signature",
})
w, err := message.CreateWriter(buf, header)
if err != nil {
return nil, err
}
ent, err := message.Read(bytes.NewReader(body))
if err != nil {
return nil, err
}
bodyPart, err := w.CreatePart(ent.Header)
if err != nil {
return nil, err
}
bodyData, err := ioutil.ReadAll(ent.Body)
if err != nil {
return nil, err
}
if _, err := bodyPart.Write(bodyData); err != nil {
return nil, err
}
if err := bodyPart.Close(); 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 := w.CreatePart(sigHeader)
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 := sigPart.Close(); err != nil {
return nil, err
}
if err := w.Close(); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func writeMultipartEncryptedRFC822(header message.Header, body []byte) ([]byte, error) {
buf := new(bytes.Buffer)
ent, err := message.Read(bytes.NewReader(body))
if err != nil {
return nil, err
}
entFields := ent.Header.Fields()
for entFields.Next() {
header.Set(entFields.Key(), entFields.Value())
}
w, err := message.CreateWriter(buf, header)
if err != nil {
return nil, err
}
bodyData, err := ioutil.ReadAll(ent.Body)
if err != nil {
return nil, err
}
if _, err := w.Write(bodyData); err != nil {
return nil, err
}
if err := w.Close(); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func getMessageHeader(msg *pmapi.Message, opts JobOptions) message.Header { // nolint[funlen]
hdr := toMessageHeader(msg.Header)

View File

@ -87,16 +87,13 @@ 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!`))
}
@ -118,16 +115,13 @@ 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`))
@ -149,27 +143,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 +183,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 +225,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 +274,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 +324,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 +390,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)
}
}
}

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

@ -0,0 +1,127 @@
// 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"
"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
len(l) != 2 || // it doesn't have colon
(len(l) == 2 && !bytes.Equal(bytes.TrimSpace(l[0]), l[0])) // has white space in front of header field
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
}
var header textproto.Header
for _, line := range HeaderLines(rawHeader) {
if len(bytes.TrimSpace(line)) > 0 {
header.AddRaw(line)
}
}
return &header, body, nil
}
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
}

View File

@ -0,0 +1,81 @@
// 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)))
}

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

@ -104,7 +104,7 @@ func (bs *BodyStructure) parseAllChildSections(r io.Reader, currentPath []int, s
// If multipart, call getAllParts, else read to count lines.
if (strings.HasPrefix(mediaType, "multipart/") || mediaType == "message/rfc822") && params["boundary"] != "" {
newPath := append(currentPath, 1)
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--

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

@ -136,7 +136,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")

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

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

@ -27,17 +27,23 @@ func (m *manager) ReportBug(ctx context.Context, rep ReportBugReq) error {
rep.ClientType = EmailClientType
}
r := m.r(ctx)
if len(rep.Attachments) == 0 {
r = r.SetBody(rep)
} else {
r = r.SetMultipartFormData(rep.GetMultipartFormData())
for _, att := range rep.Attachments {
r = r.SetMultipartField(att.name, att.filename, "application/octet-stream", att.body)
}
if rep.Client == "" {
rep.Client = m.cfg.GetUserAgent()
}
if rep.ClientVersion == "" {
rep.ClientVersion = m.cfg.AppVersion
}
r := m.r(ctx).SetMultipartFormData(rep.GetMultipartFormData())
for _, att := range rep.Attachments {
r = r.SetMultipartField(att.name, att.filename, "application/octet-stream", att.body)
}
if _, err := wrapNoConnection(r.Post("/reports/bug")); err != nil {
return err
}
return nil
}

View File

@ -19,7 +19,6 @@ package pmapi
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
@ -85,22 +84,3 @@ func TestClient_BugReportWithAttachment(t *testing.T) {
err := cm.ReportBug(context.Background(), rep)
r.NoError(t, err)
}
func TestClient_BugReport(t *testing.T) {
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
r.NoError(t, checkMethodAndPath(req, "POST", "/reports/bug"))
var bugsReportReq ReportBugReq
r.NoError(t, json.NewDecoder(req.Body).Decode(&bugsReportReq))
r.Equal(t, testBugReportReq, bugsReportReq)
w.Header().Set("Content-Type", "application/json")
fmt.Fprint(w, testBugsBody)
}))
defer s.Close()
cm := newManager(newTestConfig(s.URL))
err := cm.ReportBug(context.Background(), testBugReportReq)
r.NoError(t, 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

@ -670,7 +670,7 @@ func (mr *MockManagerMockRecorder) NewClient(arg0, arg1, arg2, arg3 interface{})
}
// NewClientWithLogin mocks base method
func (m *MockManager) NewClientWithLogin(arg0 context.Context, arg1, arg2 string) (pmapi.Client, *pmapi.Auth, error) {
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)

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

@ -0,0 +1,44 @@
// 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 pmapi
import (
"testing"
"github.com/stretchr/testify/require"
)
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)
_, err := HashMailboxPassword(passphrase, "badsalt")
r.Error(err)
haveHash, err := HashMailboxPassword(passphrase, salt)
r.NoError(err)
r.Equal(wantHash, haveHash)
}

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,47 @@
## v1.8.3
- 2021-05-31
### New
- Improved moving messages from other accounts to ProtonMail - implemented new parser for processing such messages
- Performance improvements
### Fixed
- Sync issue with Microsoft Outlook (changed the order of processing requests)
- Fetching the bodies of non-multipart messages
## 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,37 @@
## 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

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

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

View File

@ -30,7 +30,7 @@ import (
type PMAPIController interface {
TurnInternetConnectionOff()
TurnInternetConnectionOn()
AddUser(user *pmapi.User, addresses *pmapi.AddressList, password string, twoFAEnabled bool) error
AddUser(user *pmapi.User, addresses *pmapi.AddressList, password []byte, twoFAEnabled bool) 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,9 @@ 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/pkg/errors"
"github.com/stretchr/testify/assert"
)
@ -37,7 +37,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)

View File

@ -61,7 +61,7 @@ func (ctl *Controller) ReorderAddresses(user *pmapi.User, addressIDs []string) e
return api.ReorderAddresses(context.Background(), addressIDs)
}
func (ctl *Controller) AddUser(user *pmapi.User, addresses *pmapi.AddressList, password string, twoFAEnabled bool) error {
func (ctl *Controller) AddUser(user *pmapi.User, addresses *pmapi.AddressList, password []byte, twoFAEnabled bool) error {
ctl.usersByUsername[user.Name] = &fakeUser{
user: user,
password: password,

View File

@ -18,6 +18,7 @@
package fakeapi
import (
"bytes"
"errors"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
@ -49,10 +50,10 @@ func (ctl *Controller) checkScope(uid string) bool {
return session.hasFullScope
}
func (ctl *Controller) createSessionIfAuthorized(username, password string) (*fakeSession, error) {
func (ctl *Controller) createSessionIfAuthorized(username string, password []byte) (*fakeSession, error) {
// get user
user, ok := ctl.usersByUsername[username]
if !ok || user.password != password {
if !ok || !bytes.Equal(user.password, password) {
return nil, errWrongNameOrPassword
}

View File

@ -21,6 +21,6 @@ import "github.com/ProtonMail/proton-bridge/pkg/pmapi"
type fakeUser struct {
user *pmapi.User
password string
password []byte
has2FA bool
}

View File

@ -94,7 +94,7 @@ func (m *fakePMAPIManager) NewClientWithRefresh(_ context.Context, uid, ref stri
return client, auth, nil
}
func (m *fakePMAPIManager) NewClientWithLogin(_ context.Context, username string, password string) (pmapi.Client, *pmapi.Auth, error) {
func (m *fakePMAPIManager) NewClientWithLogin(_ context.Context, username string, password []byte) (pmapi.Client, *pmapi.Auth, error) {
if err := m.controller.checkAndRecordCall(POST, "/auth/info", &pmapi.GetAuthInfoReq{Username: username}); err != nil {
return nil, nil, err
}

View File

@ -27,7 +27,7 @@ Feature: IMAP fetch messages
Then IMAP response is "OK"
And IMAP response has 5 messages
Scenario: Fetch first few messages of inbox
Scenario: Fetch first few messages of inbox by UID
Given there are 10 messages in mailbox "INBOX" for "user"
And there is IMAP client logged in as "user"
And there is IMAP client selected in "INBOX"
@ -108,12 +108,22 @@ Feature: IMAP fetch messages
And IMAP response has 10 messages
# This test is wrong! RFC says it should return "BAD" (GODT-1153).
Scenario: Fetch of empty mailbox
Scenario Outline: Fetch range of empty mailbox
Given there is IMAP client logged in as "user"
And there is IMAP client selected in "Folders/mbox"
When IMAP client fetches "1:*"
When IMAP client fetches "<range>"
Then IMAP response is "OK"
And IMAP response has 0 messages
When IMAP client fetches by UID "<range>"
Then IMAP response is "OK"
And IMAP response has 0 messages
Examples:
| range |
| 1 |
| 1,5,6 |
| 1:* |
| * |
Scenario: Fetch of big mailbox
Given there are 100 messages in mailbox "Folders/mbox" for "user"
@ -123,7 +133,26 @@ Feature: IMAP fetch messages
Then IMAP response is "OK"
And IMAP response has 100 messages
Scenario: Fetch of big mailbox by UID
Given there are 100 messages in mailbox "Folders/mbox" for "user"
And there is IMAP client logged in as "user"
And there is IMAP client selected in "Folders/mbox"
When IMAP client fetches by UID "1:*"
Then IMAP response is "OK"
And IMAP response has 100 messages
Scenario: Fetch returns also messages that are marked as deleted
Given there are messages in mailbox "Folders/mbox" for "user"
| from | to | subject | body | read | starred | deleted |
| john.doe@mail.com | user@pm.me | foo | hello | false | false | false |
| jane.doe@mail.com | name@pm.me | bar | world | true | true | true |
And there is IMAP client logged in as "user"
And there is IMAP client selected in "Folders/mbox"
When IMAP client fetches "1:*"
Then IMAP response is "OK"
And IMAP response has 2 message
Scenario: Fetch by UID returns also messages that are marked as deleted
Given there are messages in mailbox "Folders/mbox" for "user"
| from | to | subject | body | read | starred | deleted |
| john.doe@mail.com | user@pm.me | foo | hello | false | false | false |

View File

@ -19,6 +19,7 @@ Feature: IMAP import messages
"""
Then IMAP response is "OK"
# I could not find any RFC why this is not valid. But for now our parser is not able to process it.
@ignore
Scenario: Import message with attachment name encoded by RFC 2047 without quoting
When IMAP client imports message to "INBOX"
@ -118,6 +119,25 @@ Feature: IMAP import messages
And API mailbox "INBOX" for "user" has 0 message
And API mailbox "Sent" for "user" has 1 message
Scenario Outline: Import message without sender
When IMAP client imports message to "<mailbox>"
"""
To: Lionel Richie <lionel@richie.com>
Subject: RE: Hello, is it me you looking for?
Nope.
"""
Then IMAP response is "OK"
And API mailbox "<mailbox>" for "user" has 1 message
Examples:
| mailbox |
| Drafts |
| Archive |
| Sent |
Scenario: Import embedded message
When IMAP client imports message to "INBOX"
"""

View File

@ -9,6 +9,7 @@ Feature: Servers are closed when no internet
Then IMAP client "i1" is logged out
And SMTP client "s1" is logged out
Given the internet connection is restored
And 1 second pass
And there is IMAP client "i2" logged in as "user"
And there is SMTP client "s2" logged in as "user"
When IMAP client "i2" gets info of "INBOX"
@ -20,6 +21,7 @@ Feature: Servers are closed when no internet
Then IMAP client "i2" is logged out
And SMTP client "s2" is logged out
Given the internet connection is restored
And 1 second pass
And there is IMAP client "i3" logged in as "user"
And there is SMTP client "s3" logged in as "user"
When IMAP client "i3" gets info of "INBOX"

View File

@ -25,7 +25,7 @@ import (
"github.com/pkg/errors"
)
func (ctl *Controller) AddUser(user *pmapi.User, addresses *pmapi.AddressList, password string, twoFAEnabled bool) error {
func (ctl *Controller) AddUser(user *pmapi.User, addresses *pmapi.AddressList, password []byte, twoFAEnabled bool) error {
if twoFAEnabled {
return godog.ErrPending
}

View File

@ -45,7 +45,7 @@ func userLogsInWithBadPassword(bddUserID string) error {
if account == nil {
return godog.ErrPending
}
ctx.SetLastError(ctx.LoginUser(account.Username(), "you shall not pass!", "123"))
ctx.SetLastError(ctx.LoginUser(account.Username(), []byte("you shall not pass!"), []byte("123")))
return nil
}