Merge branch 'release/james' into devel

This commit is contained in:
Jakub 2021-05-28 10:18:46 +02:00
commit f3b197fa56
64 changed files with 1530 additions and 813 deletions

View File

@ -2,6 +2,16 @@
Changelog [format](http://keepachangelog.com/en/1.0.0/)
## [Bridge 1.8.4] James
## Added
GODT-1044: Lite parser for appended messages.
## 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
@ -26,11 +36,13 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
* GODT-1056 Check encrypted size of the message before upload.
* GODT-1143 Turn off SMTP server while no connection.
* GODT-1089 Explicitly open system preferences window on BigSur.
* GODT-35: Connection manager with resty.
### Fixed
* GODT-1159 SMTP server not restarting after restored internet.
* GODT-1146 Refactor handling of fetching BODY[HEADER] (and similar) regarding trailing newline.
* GODT-1152 Correctly resolve wildcard sequence/UID set.
* GODT-876 Set default from if empty for importing draft.
* Other: Avoid API jail.

View File

@ -10,7 +10,7 @@ TARGET_OS?=${GOOS}
.PHONY: build build-ie build-nogui build-ie-nogui build-launcher build-launcher-ie versioner hasher
# Keep version hardcoded so app build works also without Git repository.
BRIDGE_APP_VERSION?=1.8.3+git
BRIDGE_APP_VERSION?=1.8.4+git
IE_APP_VERSION?=1.3.3+git
APP_VERSION:=${BRIDGE_APP_VERSION}
SRC_ICO:=logo.ico

16
go.mod
View File

@ -7,8 +7,7 @@ go 1.13
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 (
@ -16,9 +15,10 @@ require (
github.com/Masterminds/semver/v3 v3.1.0
github.com/ProtonMail/go-autostart v0.0.0-20181114175602-c5272053443a
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 +27,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 +58,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

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

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

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

@ -0,0 +1,244 @@
// 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"
)
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"))
if err != nil {
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

@ -212,6 +212,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 +230,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 +239,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 +258,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 +276,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 +285,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

@ -82,6 +82,14 @@ 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 +116,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>

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

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

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

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

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