Compare commits

...

93 Commits

Author SHA1 Message Date
fe926cbd57 IE release notes and GODT-738 2020-09-23 13:50:08 +02:00
e01747e3b9 Merge branch 'release/forth' into devel 2020-09-23 13:10:11 +02:00
85220848d0 Update total even if its zero 2020-09-23 09:24:58 +02:00
70f91ae55b notes and build v1.4.0 2020-09-21 13:29:33 +02:00
a73b30ed9e Better naming 2020-09-18 10:25:14 +02:00
7337f78d4a PMAPI target - parallel upload 2020-09-18 10:25:14 +02:00
9b5da91f7c Fix: Yahoo not supporting TLS1.3 GODT-730 2020-09-18 07:53:53 +00:00
c7669b950f fix: gitignore should also ignore ie build files 2020-09-17 14:33:46 +02:00
b3ed8d51a7 fix: version check for catalina 2020-09-17 11:35:05 +00:00
60b7d980f4 Fix integration test - deleting from All Mail 2020-09-17 10:19:55 +00:00
abf2238e6f Wrap imap-id with backend caller 2020-09-17 08:59:28 +00:00
b4a358c084 User agent detected by fake IMAP extension instead of AUTH callback 2020-09-17 08:59:28 +00:00
3606a0ab9f QA build with option to change API URL by ENV variable 2020-09-17 08:30:31 +00:00
c5665d0dd7 Unsilent errors reading mbox files 2020-09-16 15:51:08 +02:00
d6464c0048 Fixes after rebase 2020-09-16 09:51:58 +00:00
5496a26f73 Finish tests for moving without MOVE support 2020-09-16 09:51:57 +00:00
ec9a799fe9 test - move like outlook - GODT-536 2020-09-16 09:51:57 +00:00
730abadfc3 Do not allow deleting messages from All Mail 2020-09-16 09:51:57 +00:00
60e1548685 log both timeouts in update send 2020-09-16 09:51:57 +00:00
7430c7f1f5 Timeout for sending IMAP update 2020-09-16 09:51:57 +00:00
6671b78799 Simplified integration tests 2020-09-16 09:51:57 +00:00
c7578cf53c \Deleted flag support finish 2020-09-16 09:51:57 +00:00
66e04dd5ed Implement deleted flag GODT-461 2020-09-16 09:51:57 +00:00
803353e300 Tests for deleted flag GODT-496 2020-09-16 09:51:57 +00:00
f3773c9d78 I/E measurements 2020-09-16 09:29:13 +00:00
41ac61bbe8 fix: less spammy go-message logs 2020-09-15 09:37:29 +00:00
0d3d6747ac fix: grammar in gui 2020-09-15 08:51:02 +00:00
eaa9a458c4 test: use actual broken eml 2020-09-15 06:31:45 +00:00
46e5cb9c83 test: use message.Parse for fakeapi import parser 2020-09-15 06:31:45 +00:00
dc5387a512 fix: bug report window title 2020-09-15 08:04:51 +02:00
4b7c234e78 feat: strip comments from addresses 2020-09-14 14:46:44 +02:00
5bca6fc3cf chore: tidy up before merge 2020-09-14 14:19:35 +02:00
97b64ebb70 fix: credits and release notes 2020-09-11 11:41:03 +02:00
9b3cc9dc34 feat: convert content type in html meta tags 2020-09-11 11:41:03 +02:00
afeed4a801 feat: use upstream go-message 2020-09-11 11:41:03 +02:00
dd70b30f76 fix: don't use full pk fingerprint, only use first 8 chars 2020-09-11 11:41:03 +02:00
3e8e3c912b fix: don't doubly apply 822 texwrapper 2020-09-11 11:41:03 +02:00
5d0e3f36b4 fix: unhandled charset in header 2020-09-11 11:41:03 +02:00
da751a38e3 fix: public key names and content types 2020-09-11 11:41:03 +02:00
f9af17dd9b fix: allow unknown encodings during initial parse 2020-09-11 11:41:03 +02:00
f622ecf678 feat: logging throughout parser 2020-09-11 11:41:03 +02:00
475e673b87 feat: add logging for encoding detection 2020-09-11 11:41:03 +02:00
3916ddc8e4 fix: allow overriding sign via contact settings if set 2020-09-11 11:41:03 +02:00
ef2ace0afe fix: always check charset before utf8 validity 2020-09-11 11:41:03 +02:00
b5d3737a7e fix: sign not overriding global 2020-09-11 11:41:03 +02:00
d872d77cf5 fix: draft mime type instead of composermode 2020-09-11 11:41:03 +02:00
1f17628399 fix: unequal number of rich/plain parts 2020-09-11 11:41:03 +02:00
4ab8f7d6b5 fix: pubkey should not be collected as attachment 2020-09-11 11:41:03 +02:00
fa5f4acdac docs: add docstring for buildBodies 2020-09-11 11:41:03 +02:00
642666fa59 docs: add docstrings for walker/visitor handlers/rules 2020-09-11 11:41:03 +02:00
a2cf5374b9 feat: more efficient regexp use in parser 2020-09-11 11:41:03 +02:00
6a7a77fc51 refactor: tidier encoding detection 2020-09-11 11:41:03 +02:00
f4dfadce52 feat: attach public key 2020-09-11 11:41:03 +02:00
9ba08e5edb refactor: remove dead code 2020-09-11 11:41:03 +02:00
9821b5bbc2 feat: recreate message with parser's writer 2020-09-11 11:41:03 +02:00
5343a6fc0f fix: fallback to detecting charset if cannot handle specified one 2020-09-11 11:41:03 +02:00
180c6699e0 fix: don't select multipart/alternative if length is 0 2020-09-11 11:41:03 +02:00
7d1b0d0a40 docs: changelog 2020-09-11 11:41:03 +02:00
caff73d06c docs: add HELP about 7bit filter 2020-09-11 11:41:03 +02:00
f4d073b4cf test: ignore weird test for now 2020-09-11 11:41:02 +02:00
65d8b382d0 fix: panic when no params available 2020-09-11 11:41:02 +02:00
0e7e13211b refactor: don't reconstruct mimeBody 2020-09-11 11:41:02 +02:00
7e1af9ff4e fix: linter issues 2020-09-11 11:41:02 +02:00
37186846db feat: wrap attachment lines as per rfc822 2020-09-11 11:41:02 +02:00
a5a61c9428 feat: set attachment headers 2020-09-11 11:41:02 +02:00
ea01c155da feat: handle foreign encodings 2020-09-11 11:41:02 +02:00
f4374a02da refactor: tidy a bit 2020-09-11 11:41:02 +02:00
0d4d95360f feat: set header 2020-09-11 11:41:02 +02:00
f88071b2ca feat: parse date 2020-09-11 11:41:02 +02:00
e01a523ae3 feat: pull out most things as attachments 2020-09-11 11:41:02 +02:00
c6b18b45b5 feat: better handling of multipart messages 2020-09-11 11:41:02 +02:00
a7da66ccbc feat: enter and exit handlers 2020-09-11 11:41:02 +02:00
8bd74c5edc feat: set mime type 2020-09-11 11:41:02 +02:00
2b36d3ab7b feat: attach public key 2020-09-11 11:41:02 +02:00
45b863f931 feat: parse most header values 2020-09-11 11:41:02 +02:00
953150cfdb feat: add part getter 2020-09-11 11:41:02 +02:00
6ea3fc1963 feat: initial parser exposing walker/writer 2020-09-11 11:41:02 +02:00
7207a5d59e docs: changelog 2020-09-11 09:08:19 +00:00
dd2264da6f fix: notify of unencrypted recipient 2020-09-11 09:08:19 +00:00
9261b6337e docs: changelog 2020-09-11 10:48:27 +02:00
4f6e8c30c7 fix: use correct package type for signed inline 2020-09-11 10:29:02 +02:00
614a00eac1 Update release date for Congo in Changelog 2020-09-09 12:23:42 +02:00
de58c7a905 Cookies for Import-Export 2020-09-09 09:09:35 +02:00
2e439e17cf Remove unused scope methods 2020-09-09 06:21:02 +00:00
f73aeec97f Update changelog 2020-09-08 08:43:05 +00:00
8a7b4bb919 Improve user agent 2020-09-08 08:43:05 +00:00
78fd73ee2a Merge branch 'release/congo' into devel 2020-09-08 09:37:05 +02:00
33bf64cc4e Fix hover on links in popups 2020-09-04 10:43:59 +02:00
bb1d27a5be Do not ignore errors 2020-09-03 14:36:12 +02:00
9218598140 Update routes to API v4 2020-08-31 07:42:20 +00:00
af89931f05 Hardcoded version 2020-08-27 13:59:07 +02:00
84147a2cb0 Fix flaky tests 2020-08-25 10:20:49 +02:00
2269a9edb7 Pause event loop while FETCHing to prevetn EXPUNGE 2020-08-24 08:26:31 +00:00
156 changed files with 4767 additions and 2633 deletions

3
.gitignore vendored
View File

@ -29,6 +29,7 @@ frontend/qml/*.qmlc
# Build files
bridge_darwin_*.tgz
cmd/Desktop-Bridge/deploy
cmd/Import-Export/deploy
internal/frontend/qt*/moc.cpp
internal/frontend/qt*/moc.go
internal/frontend/qt*/moc.h
@ -43,4 +44,4 @@ internal/frontend/rcc.qrc
internal/frontend/rcc_cgo_*.go
vendor-cache/
/main.go
/main.go

View File

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

View File

@ -4,12 +4,46 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
## Unreleased
## [IE 0.2.x] Congo
## [IE 1.1.x] Danube (v1.1.0 beta 2020-09-XX)
### Fixed
* GODT-703 Import-Export showed always at least one total message.
* GODT-738 Fix for mbox files with long lines.
## [Bridge 1.4.x] Forth (v1.4.0 beta 2020-09-XX)
### Added
* GODT-682 Persistent anonymous API cookies for Import-Export.
* GODT-357 Use go-message to make a better message parser.
* GODT-720 Time measurement of progress for Import-Export.
### Changed
* GODT-511 User agent format changed.
* Unsilent errors reading mbox files.
* GODT-692 QA build with option to change API URL by ENV variable.
* GODT-704 User agent detected by fake IMAP extension instead of AUTH callback (some clients use LOGIN instead of AUTH).
* GODT-695 Parallel upload for ProtonMail target.
### Removed
* GODT-519 Unused AUTH scope parsing methods.
### Fixed
* GODT-698 Use correct package type for signed PGP/Inline messages.
* Generic bug report window title.
* Fix missing check for unencrypted recipients during sending.
* Version checking for catalina.
* GODT-730 Limit maximal TLS version for Yahoo IMAP server.
## [IE 1.0.x] Congo (v1.0.0 live 2020-09-08)
### Added
* GODT-633 Persistent anonymous API cookies for better load balancing and abuse detection.
* GODT-461 Add support for `\Deleted` flag.
### Changed
* GODT-462 Pausing event loop while FETCHing to prevent EXPUNGE
* Wait for unilateral response to be delivered
* GODT-409 Set flags have to replace all flags.
* GODT-531 Better way to add trusted certificate in macOS.
* Bumped golangci-lint to v1.29.0
@ -36,6 +70,8 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
* golang.org/x/text v0.3.2 -> v0.3.3
* Set first-start to false in bridge, not in frontend.
* GODT-400 Refactor sendingInfo.
* GODT-513 Update routes to API v4.
* GODT-551 Do not ignore errors during message flagging.
* GODT-380 Adding IE GUI to Bridge repo and building
* BR: extend functionality of PopupDialog
* BR: makefile APP_VERSION instead of BRIDGE_VERSION
@ -49,11 +85,14 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
* IE: Removed `onLoginFinished`
* Structure for transfer rules in QML
* GODT-213 Convert panics from message parser to error.
* GODT-585 Do not allow deleting messages from All Mail.
### Fixed
* GODT-655 Fix date picker with automatic Windows DST
* GODT-454 Fix send on closed channel when receiving unencrypted send confirmation from GUI.
* GODT-597 Duplicate sending when draft creation takes too long
* GODT-634 Hover on links in popups.
## [v1.3.x] Emma (v1.3.2 beta 2020-08-04, v1.3.3 beta 2020-08-06, v1.3.3 live 2020-08-12)

View File

@ -9,8 +9,9 @@ TARGET_OS?=${GOOS}
## Build
.PHONY: build build-ie build-nogui build-ie-nogui check-has-go
# Keep version hardcoded so app build works also without Git repository.
BRIDGE_APP_VERSION?=1.4.0-git
IE_APP_VERSION?=1.0.0-git
IE_APP_VERSION?=1.1.0-git
APP_VERSION:=${BRIDGE_APP_VERSION}
SRC_ICO:=logo.ico
SRC_ICNS:=Bridge.icns

View File

@ -21,9 +21,11 @@ import (
"runtime/pprof"
"github.com/ProtonMail/proton-bridge/internal/cmd"
"github.com/ProtonMail/proton-bridge/internal/cookies"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/frontend"
"github.com/ProtonMail/proton-bridge/internal/importexport"
"github.com/ProtonMail/proton-bridge/internal/preferences"
"github.com/ProtonMail/proton-bridge/internal/updates"
"github.com/ProtonMail/proton-bridge/internal/users/credentials"
"github.com/ProtonMail/proton-bridge/pkg/config"
@ -36,6 +38,10 @@ import (
)
const (
// cacheVersion is used for cache files such as lock, or preferences.
// Different number will drop old files and create new ones.
cacheVersion = "c11"
appName = "importExport"
appNameDash = "import-export-app"
)
@ -58,7 +64,7 @@ func main() {
// IMPORTANT: ***Read the comments before CHANGING the order ***
func run(context *cli.Context) (contextError error) { // nolint[funlen]
// We need to have config instance to setup a logs, panic handler, etc ...
cfg := config.New(appName, constants.Version, constants.Revision, "")
cfg := config.New(appName, constants.Version, constants.Revision, cacheVersion)
// We want to know about any problem. Our PanicHandler calls sentry which is
// not dependent on anything else. If that fails, it tries to create crash
@ -132,6 +138,16 @@ func run(context *cli.Context) (contextError error) { // nolint[funlen]
// implementation depending on whether build flag pmapi_prod is used or not.
cm.SetRoundTripper(cfg.GetRoundTripper(cm, eventListener))
pref := preferences.New(cfg)
// Cookies must be persisted across restarts.
jar, err := cookies.NewCookieJar(pref)
if err != nil {
logrus.WithError(err).Warn("Could not create cookie jar")
} else {
cm.SetCookieJar(jar)
}
importexportInstance := importexport.New(cfg, panicHandler, eventListener, cm, credentialsStore)
// Decide about frontend mode before initializing rest of import-export.

14
go.mod
View File

@ -13,16 +13,17 @@ require (
require (
github.com/0xAX/notificator v0.0.0-20191016112426-3962a5ea8da1
github.com/Masterminds/semver/v3 v3.1.0
github.com/ProtonMail/go-appdir v1.1.0
github.com/ProtonMail/go-apple-mobileconfig v0.0.0-20160701194735-7ea9927a11f6
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-vcard v0.0.0-20180326232728-33aaa0a0c8a5
github.com/ProtonMail/gopenpgp/v2 v2.0.1
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
github.com/allan-simon/go-singleinstance v0.0.0-20160830203053-79edcfdc2dfc
github.com/andybalholm/cascadia v1.2.0
github.com/certifi/gocertifi v0.0.0-20200211180108-c7c1fbc02894 // indirect
github.com/chzyer/logex v1.1.10 // indirect
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect
@ -35,7 +36,7 @@ require (
github.com/emersion/go-imap-specialuse v0.0.0-20200722111535-598ff00e4075
github.com/emersion/go-imap-unselect v0.0.0-20171113212723-b985794e5f26
github.com/emersion/go-mbox v1.0.0
github.com/emersion/go-message v0.11.1
github.com/emersion/go-message v0.12.1-0.20200903165315-e1abe21f389a
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21
github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe
github.com/emersion/go-vcard v0.0.0-20190105225839-8856043f13c5 // indirect
@ -46,11 +47,9 @@ require (
github.com/golang/mock v1.4.4
github.com/google/go-cmp v0.5.1
github.com/google/uuid v1.1.1
github.com/go-delve/delve v1.4.1 // indirect
github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c // indirect
github.com/hashicorp/go-multierror v1.1.0
github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7
github.com/jhillyerd/enmime v0.8.1
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0
github.com/keybase/go-keychain v0.0.0-20200502122510-cda31fe0c86d
github.com/logrusorgru/aurora v2.0.3+incompatible
@ -60,13 +59,11 @@ require (
github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce
github.com/olekukonko/tablewriter v0.0.4 // indirect
github.com/pkg/errors v0.9.1
github.com/psampaz/go-mod-outdated v0.6.0 // indirect
github.com/sirupsen/logrus v1.6.0
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
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-20200603231648-26cdb75b6f22 // indirect
github.com/therecipe/qt/internal/binding/files/docs/5.13.0 v0.0.0-20200603231648-26cdb75b6f22 // indirect
github.com/twinj/uuid v1.0.0 // indirect
github.com/urfave/cli v1.22.4
go.etcd.io/bbolt v1.3.5
@ -77,7 +74,8 @@ require (
replace (
github.com/docker/docker-credential-helpers => github.com/ProtonMail/docker-credential-helpers v1.1.0
github.com/emersion/go-imap => github.com/jameshoulahan/go-imap v0.0.0-20200728140727-d57327f48843
github.com/emersion/go-imap => github.com/ProtonMail/go-imap v0.0.0-20200828124548-d04b0dc1f399
github.com/emersion/go-mbox => github.com/ProtonMail/mbox v0.0.0-20200918064939-909a18c9af45
github.com/emersion/go-smtp => github.com/ProtonMail/go-smtp v0.0.0-20181206232543-8261df20d309
github.com/jameskeane/bcrypt => github.com/ProtonMail/bcrypt v0.0.0-20170924085257-7509ea014998
golang.org/x/crypto => github.com/ProtonMail/crypto v0.0.0-20200416114516-1fa7f403fb9c

93
go.sum
View File

@ -1,6 +1,9 @@
github.com/0xAX/notificator v0.0.0-20191016112426-3962a5ea8da1 h1:j9HaafapDbPbGRDku6e/HRs6KBMcKHiWcm1/9Sbxnl4=
github.com/0xAX/notificator v0.0.0-20191016112426-3962a5ea8da1/go.mod h1:NtXa9WwQsukMHZpjNakTTz0LArxvGYdPA9CjIcUSZ6s=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
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-20200416114516-1fa7f403fb9c h1:DAvlgde2Stu18slmjwikiMPs/CKPV35wSvmJS34z0FU=
@ -13,6 +16,8 @@ github.com/ProtonMail/go-apple-mobileconfig v0.0.0-20160701194735-7ea9927a11f6 h
github.com/ProtonMail/go-apple-mobileconfig v0.0.0-20160701194735-7ea9927a11f6/go.mod h1:EtDfBMIDWmVe4viZCuBTEfe3OIIo0ghbpOaAZVO+hVg=
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-imap v0.0.0-20200828124548-d04b0dc1f399 h1:wBo/Xgb/Dn2loU47D+PJaOoIZ67i3AqYp51gLn8YE5U=
github.com/ProtonMail/go-imap v0.0.0-20200828124548-d04b0dc1f399/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=
@ -23,25 +28,24 @@ github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5 h1:Uga1DHFN4GU
github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5/go.mod h1:oeP9CMN+ajWp5jKp1kue5daJNwMMxLF+ujPaUIoJWlA=
github.com/ProtonMail/gopenpgp/v2 v2.0.1 h1:x0uvDhry5WzoHeJO4J3dgMLhG4Z9PeBJ2O+sDOY0LcU=
github.com/ProtonMail/gopenpgp/v2 v2.0.1/go.mod h1:wQQCJo7DURO6S9VwH+kSDEYs/B63yZnAEfGlOg8YNBY=
github.com/ProtonMail/mbox v0.0.0-20200918064939-909a18c9af45 h1:GDh55hDI2sNiirDqEWV8b6EB729u78Qxu3nKF970n6g=
github.com/ProtonMail/mbox v0.0.0-20200918064939-909a18c9af45/go.mod h1:Yp9IVuuOYLEuMv4yjgDHvhb5mHOcYH6x92Oas3QqEZI=
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/abiosoft/ishell v2.0.0+incompatible h1:zpwIuEHc37EzrsIYah3cpevrIc8Oma7oZPxr03tlmmw=
github.com/abiosoft/ishell v2.0.0+incompatible/go.mod h1:HQR9AqF2R3P4XXpMpI0NAzgHf/aS6+zVXRj14cVk9qg=
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db h1:CjPUSXOiYptLbTdr1RceuZgSFDQ7U15ITERUGrUORx8=
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db/go.mod h1:rB3B4rKii8V21ydCbIzH5hZiCQE7f5E9SzUb/ZZx530=
github.com/allan-simon/go-singleinstance v0.0.0-20160830203053-79edcfdc2dfc h1:mZca0/HZ/XWXP9txkfdl2GH6mUzBqAlyJz3u5Lg8fuA=
github.com/allan-simon/go-singleinstance v0.0.0-20160830203053-79edcfdc2dfc/go.mod h1:qqsTQiwdyqxU05iDCsi0oN3P4nrVxAmn8xCtODDSf/U=
github.com/andybalholm/cascadia v1.2.0 h1:vuRCkM5Ozh/BfmsaTm26kbjm0mIOM3yS5Ek/F5h18aE=
github.com/andybalholm/cascadia v1.2.0/go.mod h1:YCyR8vOZT9aZ1CHEd8ap0gMVm2aFgxBp0T0eFw1RUQY=
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a h1:MISbI8sU/PSK/ztvmWKFcI7UGb5/HQT7B+i3a2myKgI=
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a/go.mod h1:2GxOXOlEPAMFPfp014mK1SWq8G8BN8o7/dfYqJrVGn8=
github.com/andybalholm/cascadia v1.1.0 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5zzsLTo=
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/certifi/gocertifi v0.0.0-20200211180108-c7c1fbc02894 h1:JLaf/iINcLyjwbtTsCJjc6rtlASgHeIJPrB6QmwURnA=
github.com/certifi/gocertifi v0.0.0-20200211180108-c7c1fbc02894/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA=
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/cosiner/argv v0.0.0-20170225145430-13bacc38a0a5/go.mod h1:p/NrK5tF6ICIly4qwEDsf6VDirFiWWz0FenfYBwJaKQ=
github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
@ -66,9 +70,9 @@ github.com/emersion/go-imap-unselect v0.0.0-20171113212723-b985794e5f26 h1:FiSb8
github.com/emersion/go-imap-unselect v0.0.0-20171113212723-b985794e5f26/go.mod h1:+gnnZx3Mg3MnCzZrv0eZdp5puxXQUgGT/6N6L7ShKfM=
github.com/emersion/go-mbox v1.0.0 h1:HN6aKbyqmgIfK9fS/gen+NRr2wXLSxZXWfdAIAnzQPc=
github.com/emersion/go-mbox v1.0.0/go.mod h1:Yp9IVuuOYLEuMv4yjgDHvhb5mHOcYH6x92Oas3QqEZI=
github.com/emersion/go-message v0.11.1 h1:0C/S4JIXDTSfXB1vpqdimAYyK4+79fgEAMQ0dSL+Kac=
github.com/emersion/go-message v0.11.1/go.mod h1:C4jnca5HOTo4bGN9YdqNQM9sITuT3Y0K6bSUw9RklvY=
github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b h1:uhWtEWBHgop1rqEk2klKaxPAkVDCXexai6hSuRQ7Nvs=
github.com/emersion/go-message v0.12.1-0.20200903165315-e1abe21f389a h1:3C6qIGgPr1qAT0ikRD5NbyKpME/iHCDeXhpv/JJsFsE=
github.com/emersion/go-message v0.12.1-0.20200903165315-e1abe21f389a/go.mod h1:kYIioST9GDHte9/BRWgi93rpqbDuFftMjKSMaXS8ABo=
github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
@ -84,25 +88,12 @@ github.com/getsentry/raven-go v0.2.0 h1:no+xWJRb5ZI7eE8TWgIq1jLulQiIoLG0IfYxv5JY
github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
github.com/go-resty/resty/v2 v2.3.0 h1:JOOeAvjSlapTT92p8xiS19Zxev1neGikoHsXJeOq8So=
github.com/go-resty/resty/v2 v2.3.0/go.mod h1:UpN9CgLZNsv4e9XG50UU8xdI0F43UQ4HmxLBDwaroHU=
github.com/go-delve/delve v1.4.1 h1:kZs0umEv+VKnK84kY9/ZXWrakdLTeRTyYjFdgLelZCQ=
github.com/go-delve/delve v1.4.1/go.mod h1:vmy6iObn7zg8FQ5KOCIe6TruMNsqpoZO8uMiRea+97k=
github.com/go-resty/resty/v2 v2.2.0 h1:vgZ1cdblp8Aw4jZj3ZsKh6yKAlMg3CHMrqFSFFd+jgY=
github.com/go-resty/resty/v2 v2.2.0/go.mod h1:nYW/8rxqQCmI3bPz9Fsmjbr2FBjGuR2Mzt6kDh3zZ7w=
github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw=
github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/gogs/chardet v0.0.0-20150115103509-2404f7772561 h1:aBzukfDxQlCTVS0NBUjI5YA3iVeaZ9Tb5PxNrrIP1xs=
github.com/gogs/chardet v0.0.0-20150115103509-2404f7772561/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14=
github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/golang/mock v1.4.3 h1:GV+pQPG/EUUbkh47niozDcADz6go/dUwhVzdUQHIVRw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-dap v0.2.0/go.mod h1:5q8aYQFnHOAZEMP+6vmq25HKYAEwE+LF5yh7JKrrhSQ=
github.com/gopherjs/gopherjs v0.0.0-20190411002643-bd77b112433e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c h1:7lF+Vz0LqiRidnzC1Oq86fpX1q/iEv2KJdrCtttYjT4=
github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
@ -112,22 +103,14 @@ github.com/hashicorp/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
github.com/jameshoulahan/go-imap v0.0.0-20200728140727-d57327f48843 h1:suxlO4AC4E4bjueAsL0m+qp8kmkxRWMGj+5bBU/KJ8g=
github.com/jameshoulahan/go-imap v0.0.0-20200728140727-d57327f48843/go.mod h1:yKASt+C3ZiDAiCSssxg9caIckWF/JG7ZQTO7GAmvicU=
github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jaytaylor/html2text v0.0.0-20190408195923-01ec452cbe43/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7 h1:g0fAGBisHaEQ0TRq1iBvemFRf+8AEWEmBESSiWB3Vsc=
github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
github.com/jhillyerd/enmime v0.8.1 h1:Kz4xj3sJJ4Ju8e+w/7v9H4Matv5ijPgv7UkhPf+C15I=
github.com/jhillyerd/enmime v0.8.1/go.mod h1:MBHs3ugk03NGjMM6PuRynlKf+HA5eSillZ+TRCm73AE=
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA=
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
github.com/keybase/go-keychain v0.0.0-20200502122510-cda31fe0c86d h1:gVjhBCfVGl32RIBooOANzfw+0UqX8HU+yPlMv8vypcg=
github.com/keybase/go-keychain v0.0.0-20200502122510-cda31fe0c86d/go.mod h1:W6EbaYmb4RldPn0N3gvVHjY1wmU59kbymhW9NATWhwY=
github.com/keybase/go.dbus v0.0.0-20200324223359-a94be52c0b03/go.mod h1:a8clEhrrGV/d76/f9r2I41BwANMihfZYV9C223vaxqE=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@ -137,61 +120,41 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8=
github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381 h1:bqDmpDG49ZRnB5PcgP0RXtQvnMSgIF14M7CBd2shtXs=
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
github.com/martinlindhe/base36 v1.0.0 h1:eYsumTah144C0A8P1T/AVSUk5ZoLnhfYFM3OGQxB52A=
github.com/martinlindhe/base36 v1.0.0/go.mod h1:+AtEs8xrBpCeYgSLoY/aJ6Wf37jtBuR0s35750M27+8=
github.com/mattn/go-colorable v0.0.0-20170327083344-ded68f7a9561/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/miekg/dns v1.1.30 h1:Qww6FseFn8PRfw07jueqIXqodm0JKiiKuK0DeXSqfyo=
github.com/miekg/dns v1.1.30/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
github.com/miekg/dns v1.1.29 h1:xHBEhR+t5RzcFJjBLJlax2daXOrTYtr9z4WdKEfWFzg=
github.com/miekg/dns v1.1.29/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
github.com/myesui/uuid v1.0.0 h1:xCBmH4l5KuvLYc5L7AS7SZg9/jKdIFubM7OVoLqaQUI=
github.com/myesui/uuid v1.0.0/go.mod h1:2CDfNgU0LR8mIdO8vdWd8i9gWWxLlcoIGGpSNgafq84=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce h1:RPclfga2SEJmgMmz2k+Mg7cowZ8yv4Trqw9UsJby758=
github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce/go.mod h1:uFMI8w+ref4v2r9jz+c9i1IfIttS/OkmLfrk1jne5hs=
github.com/olekukonko/tablewriter v0.0.1 h1:b3iUnf1v+ppJiOfNX4yxxqfWKMQPZR5yoh8urCTFX88=
github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8=
github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA=
github.com/peterh/liner v0.0.0-20170317030525-88609521dc4b/go.mod h1:xIteQHvHuaLYG9IFj6mSxM0fCKrs34IrEQUhOYuGPHc=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/psampaz/go-mod-outdated v0.6.0 h1:DXS6rdsz4rpezbPsckQflqrYSEBvsF5GAmUWP+UvnQo=
github.com/psampaz/go-mod-outdated v0.6.0/go.mod h1:r78NYWd1z+F9Zdsfy70svgXOz363B08BWnTyFSgEESs=
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca h1:NugYot0LIVPxTvN8n+Kvkn6TrbMyxQiuvKdEwFdR9vI=
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
github.com/spf13/cobra v0.0.0-20170417170307-b6cb39589372/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/pflag v0.0.0-20170417173400-9e4c21054fa1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@ -201,40 +164,25 @@ github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoH
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
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-20200126204426-5074eb6d8c41 h1:yBVcrpbaQYJBdKT2pxTdlL4hBE/eM4UPcyj9YpyvSok=
github.com/therecipe/qt v0.0.0-20200126204426-5074eb6d8c41/go.mod h1:SUUR2j3aE1z6/g76SdD6NwACEpvCxb3fvG82eKbD6us=
github.com/therecipe/qt v0.0.0-20200603231648-26cdb75b6f22 h1:UrNr8EZueA1eREFmG5gVHBeeOuwW2GbzI9VfdB5uK+c=
github.com/therecipe/qt/internal/binding/files/docs v0.0.0-20191019224306-1097424d656c h1:/VhcwU7WuFEVgDHZ9V8PIYAyYqQ6KNxFUjBMOf2aFZM=
github.com/therecipe/qt/internal/binding/files/docs/5.12.0 v0.0.0-20200603231648-26cdb75b6f22 h1:FumuOkCw78iheUI3eIYhAgtsj/0HQBAib/jXk1cslJw=
github.com/therecipe/qt/internal/binding/files/docs/5.12.0 v0.0.0-20200603231648-26cdb75b6f22/go.mod h1:7m8PDYDEtEVqfjoUQc2UrFqhG0CDmoVJjRlQxexndFc=
github.com/therecipe/qt/internal/binding/files/docs/5.13.0 v0.0.0-20200603231648-26cdb75b6f22 h1:aYzTBQ/hC6FtbaRnyylxlhbSGMPnyD5lAzVO3Ae6emA=
github.com/therecipe/qt/internal/binding/files/docs/5.13.0 v0.0.0-20200603231648-26cdb75b6f22/go.mod h1:mH55Ek7AZcdns5KPp99O0bg+78el64YCYWHiQKrOdt4=
github.com/twinj/uuid v1.0.0 h1:fzz7COZnDrXGTAOHGuUGYd6sG+JMq+AoE7+Jlu0przk=
github.com/twinj/uuid v1.0.0/go.mod h1:mMgcE1RHFUFqe5AfiwlINXisXfDGro23fWdPUfOMjRY=
github.com/urfave/cli v1.22.4 h1:u7tSpNPPswAFymm8IehJhy4uJMlUuU/GmqSkvJ1InXA=
github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0=
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
github.com/urfave/cli v1.22.3 h1:FpNT6zq26xNpHZy08emi755QwzLPs6Pukqjlc7RfOMU=
github.com/urfave/cli v1.22.3/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk=
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.starlark.net v0.0.0-20190702223751-32f345186213/go.mod h1:c1/X6cHgvdXj6pUlmWKMkuqRnW4K8x2vwt6JAaaircg=
golang.org/x/arch v0.0.0-20190927153633-4e8777c89be4/go.mod h1:flIaEI6LNU6xOCD5PaJvn9wGP0agmIOqjrtsKGRguv4=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190420063019-afa5a82059c6/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/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-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
@ -246,41 +194,28 @@ golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5h
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=
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 h1:LfCXLvNmTYH9kEmVgqbnsWfruoXZIrh4YBgqVHtDvw0=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
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-20191127201027-ecd32218bd7f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425 h1:VvQyQJN0tSuecqgcIxMWnnfG5kSmgy9KZR9sW3W5QeA=
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/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=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/stretchr/testify.v1 v1.2.2 h1:yhQC6Uy5CqibAIlk1wlusa/MJ3iAN49/BsR/dCCKz3M=
gopkg.in/stretchr/testify.v1 v1.2.2/go.mod h1:QI5V/q6UbPmuhtm10CaFZxED9NreB8PnFYN9JcR6TxU=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

View File

@ -15,8 +15,8 @@
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// Code generated by ./credits.sh at Fri 04 Sep 2020 01:57:36 PM CEST. DO NOT EDIT.
// Code generated by ./credits.sh at Wed Sep 16 16:48:58 CEST 2020. DO NOT EDIT.
package bridge
const Credits = "github.com/0xAX/notificator;github.com/abiosoft/ishell;github.com/abiosoft/readline;github.com/allan-simon/go-singleinstance;github.com/andybalholm/cascadia;github.com/certifi/gocertifi;github.com/chzyer/logex;github.com/chzyer/test;github.com/cucumber/godog;github.com/docker/docker-credential-helpers;github.com/emersion/go-imap;github.com/emersion/go-imap-appendlimit;github.com/emersion/go-imap-idle;github.com/emersion/go-imap-move;github.com/emersion/go-imap-quota;github.com/emersion/go-imap-specialuse;github.com/emersion/go-imap-unselect;github.com/emersion/go-mbox;github.com/emersion/go-message;github.com/emersion/go-sasl;github.com/emersion/go-smtp;github.com/emersion/go-textwrapper;github.com/emersion/go-vcard;github.com/fatih/color;github.com/flynn-archive/go-shlex;github.com/getsentry/raven-go;github.com/go-delve/delve;github.com/golang/mock;github.com/google/go-cmp;github.com/google/uuid;github.com/gopherjs/gopherjs;github.com/go-resty/resty/v2;github.com/hashicorp/go-multierror;github.com/jameshoulahan/go-imap;github.com/jameskeane/bcrypt;github.com/jaytaylor/html2text;github.com/jhillyerd/enmime;github.com/kardianos/osext;github.com/keybase/go-keychain;github.com/logrusorgru/aurora;github.com/mattn/go-runewidth;github.com/miekg/dns;github.com/myesui/uuid;github.com/nsf/jsondiff;github.com/olekukonko/tablewriter;github.com/pkg/errors;github.com/ProtonMail/bcrypt;github.com/ProtonMail/crypto;github.com/ProtonMail/docker-credential-helpers;github.com/ProtonMail/go-appdir;github.com/ProtonMail/go-apple-mobileconfig;github.com/ProtonMail/go-autostart;github.com/ProtonMail/go-imap-id;github.com/ProtonMail/gopenpgp/v2;github.com/ProtonMail/go-smtp;github.com/ProtonMail/go-vcard;github.com/psampaz/go-mod-outdated;github.com/sirupsen/logrus;github.com/skratchdot/open-golang;github.com/stretchr/testify;github.com/therecipe/qt;github.com/twinj/uuid;github.com/urfave/cli;go.etcd.io/bbolt;golang.org/x/crypto;golang.org/x/net;golang.org/x/text;gopkg.in/stretchr/testify.v1;;Font Awesome 4.7.0;;Qt 5.13 by Qt group;"
const Credits = "github.com/0xAX/notificator;github.com/ProtonMail/bcrypt;github.com/ProtonMail/crypto;github.com/ProtonMail/docker-credential-helpers;github.com/ProtonMail/go-appdir;github.com/ProtonMail/go-apple-mobileconfig;github.com/ProtonMail/go-autostart;github.com/ProtonMail/go-imap;github.com/ProtonMail/go-imap-id;github.com/ProtonMail/go-smtp;github.com/ProtonMail/go-vcard;github.com/ProtonMail/gopenpgp/v2;github.com/PuerkitoBio/goquery;github.com/abiosoft/ishell;github.com/abiosoft/readline;github.com/allan-simon/go-singleinstance;github.com/certifi/gocertifi;github.com/chzyer/logex;github.com/chzyer/test;github.com/cucumber/godog;github.com/docker/docker-credential-helpers;github.com/emersion/go-imap;github.com/emersion/go-imap-appendlimit;github.com/emersion/go-imap-idle;github.com/emersion/go-imap-move;github.com/emersion/go-imap-quota;github.com/emersion/go-imap-specialuse;github.com/emersion/go-imap-unselect;github.com/emersion/go-mbox;github.com/emersion/go-message;github.com/emersion/go-sasl;github.com/emersion/go-smtp;github.com/emersion/go-textwrapper;github.com/emersion/go-vcard;github.com/fatih/color;github.com/flynn-archive/go-shlex;github.com/getsentry/raven-go;github.com/go-resty/resty/v2;github.com/golang/mock;github.com/google/go-cmp;github.com/google/uuid;github.com/gopherjs/gopherjs;github.com/hashicorp/go-multierror;github.com/jameskeane/bcrypt;github.com/jaytaylor/html2text;github.com/kardianos/osext;github.com/keybase/go-keychain;github.com/logrusorgru/aurora;github.com/mattn/go-runewidth;github.com/miekg/dns;github.com/myesui/uuid;github.com/nsf/jsondiff;github.com/olekukonko/tablewriter;github.com/pkg/errors;github.com/sirupsen/logrus;github.com/skratchdot/open-golang;github.com/ssor/bom;github.com/stretchr/testify;github.com/therecipe/qt;github.com/twinj/uuid;github.com/urfave/cli;go.etcd.io/bbolt;golang.org/x/crypto;golang.org/x/net;golang.org/x/text;gopkg.in/stretchr/testify.v1;;Font Awesome 4.7.0;;Qt 5.13 by Qt group;"

View File

@ -15,17 +15,21 @@
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// Code generated by ./release-notes.sh at 'Fri 04 Sep 2020 01:57:36 PM CEST'. DO NOT EDIT.
// Code generated by ./release-notes.sh at 'Mon Sep 21 01:29:10 PM CEST 2020'. DO NOT EDIT.
package bridge
const ReleaseNotes = `Improvements to Alternative Routing: Version two of this feature is now more resilient to unstable internet connections, which results in a smoother experience using this feature. Also includes fixes to previous implementation of Alternative Routing when first starting the application or when turning it off.
Email parsing improvements: Improved detection of email encodings embedded in html/xml in addition to message header; add a fallback option if encoding is not specified and decoding as UTF8 fails (ISO-8859-1) ; tweaked logic of parsing "References" header.
User interaction improvements: Some smaller improvements in specific cases to make the interaction with Proton Bridge clearer for the user
Code updates & maintenance: Migrated to GopenPGP v2, updates to GoIMAPv1, increased bbolt version to 1.3.5 and various improvements regarding extensibility and maintainability for upcoming work.
General stability improvements: Improvements to the behavior of the application under various unstable internet conditions.
const ReleaseNotes = `Bulletproofing against any potential data loss and/or duplication
Performance improvements for handling attachments and non-standard formatting
Better stability of the message parser
Additional foreign encoding support for outgoing messages
Complete refactor of the way messages are parsed to simplify code maintenance
• Improved User-Agent detection
• Added MacOS Big Sur compatibility
• Added persistent anonymous API cookies
`
const ReleaseFixedBugs = `• Fixed a slew of smaller bugs and some conditions which could cause the application to crash.
The full changelog can be found at https://github.com/ProtonMail/proton-bridge/blob/master/Changelog.md
const ReleaseFixedBugs = `• Fixed rare mail loss when moving from Spam folder
Limited log size
• Fixed Linux font issues (mouse hover).
`

View File

@ -79,7 +79,7 @@ func (f *frontendCLI) loginAccount(c *ishell.Context) { // nolint[funlen]
return
}
_, err = client.Auth2FA(twoFactor, auth)
err = client.Auth2FA(twoFactor, auth)
if err != nil {
f.processAPIError(err)
return

View File

@ -126,7 +126,7 @@ func (f *frontendCLI) loginAccount(c *ishell.Context) { // nolint[funlen]
return
}
_, err = client.Auth2FA(twoFactor, auth)
err = client.Auth2FA(twoFactor, auth)
if err != nil {
f.processAPIError(err)
return

View File

@ -354,7 +354,7 @@ Window {
} else {
return qsTr('A new version of Bridge is available.<br>
Check <a href="%1">release notes</a> to learn what is new in %2.<br>
You can continue with the update or download and install new version manually from<br><br>
You can continue with the update or download and install the new version manually from<br><br>
<a href="%3">%3</a>',
"Message for update in Win/Mac").arg("releaseNotes").arg(go.newversion).arg(go.landingPage)
}

View File

@ -274,15 +274,15 @@ Window {
}
} else {
if (go.goos=="linux") {
return qsTr('New version of %1 is available.<br>
return qsTr('A new version of %1 is available.<br>
Check <a href="%2">release notes</a> to learn what is new in %3.<br>
Use your package manager to update or download and install new version manually from<br><br>
Use your package manager to update or download and install new the version manually from<br><br>
<a href="%4">%4</a>',
"Message for update in Linux").arg(go.programTitle).arg("releaseNotes").arg(go.newversion).arg(go.landingPage)
} else {
return qsTr('New version of %1 is available.<br>
return qsTr('A new version of %1 is available.<br>
Check <a href="%2">release notes</a> to learn what is new in %3.<br>
You can continue with update or download and install new version manually from<br><br>
You can continue with update or download and install new the version manually from<br><br>
<a href="%4">%4</a>',
"Message for update in Win/Mac").arg(go.programTitle).arg("releaseNotes").arg(go.newversion).arg(go.landingPage)
}

View File

@ -106,6 +106,7 @@ Rectangle {
}
MouseArea {
anchors.fill: mainText
cursorShape: mainText.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
acceptedButtons: Qt.NoButton
}

View File

@ -39,7 +39,7 @@ Window {
color : "transparent"
flags : Qt.Window | Qt.Dialog | Qt.FramelessWindowHint
title : "ProtonMail Bridge - Bug report"
title : "Bug report"
visible : false
WindowTitleBar {

View File

@ -208,7 +208,7 @@ func (a *Accounts) Auth2FA(twoFacAuth string) int {
if a.auth == nil || a.authClient == nil {
err = fmt.Errorf("missing authentication in auth2FA %p %p", a.auth, a.authClient)
} else {
_, err = a.authClient.Auth2FA(twoFacAuth, a.auth)
err = a.authClient.Auth2FA(twoFacAuth, a.auth)
}
if a.showLoginError(err, "auth2FA") {

View File

@ -338,9 +338,7 @@ func (f *FrontendQt) setProgressManager(progress *transfer.Progress) {
break
}
failed, imported, _, _, total := progress.GetCounts()
if total != 0 {
f.Qml.SetTotal(int(total))
}
f.Qml.SetTotal(int(total))
f.Qml.SetProgressFails(int(failed))
f.Qml.SetProgressDescription(progress.PauseReason())
if total > 0 {

View File

@ -164,7 +164,7 @@ func (s *FrontendQt) auth2FA(twoFacAuth string) int {
if s.auth == nil || s.authClient == nil {
err = fmt.Errorf("missing authentication in auth2FA %p %p", s.auth, s.authClient)
} else {
_, err = s.authClient.Auth2FA(twoFacAuth, s.auth)
err = s.authClient.Auth2FA(twoFacAuth, s.auth)
}
if s.showLoginError(err, "auth2FA") {

View File

@ -23,7 +23,6 @@ import (
"sync"
"time"
imapid "github.com/ProtonMail/go-imap-id"
"github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/pkg/listener"
@ -45,9 +44,6 @@ type imapBackend struct {
users map[string]*imapUser
usersLocker sync.Locker
lastMailClient imapid.ID
lastMailClientLocker sync.Locker
imapCache map[string]map[string]string
imapCachePath string
imapCacheLock *sync.RWMutex
@ -87,9 +83,6 @@ func newIMAPBackend(
users: map[string]*imapUser{},
usersLocker: &sync.Mutex{},
lastMailClient: imapid.ID{imapid.FieldName: clientNone},
lastMailClientLocker: &sync.Mutex{},
imapCachePath: cfg.GetIMAPCachePath(),
imapCacheLock: &sync.RWMutex{},
}
@ -164,7 +157,9 @@ func (ib *imapBackend) Login(_ *imap.ConnInfo, username, password string) (goIMA
if err := imapUser.user.CheckBridgeLogin(password); err != nil {
log.WithError(err).Error("Could not check bridge password")
_ = imapUser.Logout()
if err := imapUser.Logout(); err != nil {
log.WithError(err).Warn("Could not logout user after unsuccessful login check")
}
// Apple Mail sometimes generates a lot of requests very quickly.
// It's therefore good to have a timeout after a bad login so that we can slow
// those requests down a little bit.
@ -192,23 +187,6 @@ func (ib *imapBackend) CreateMessageLimit() *uint32 {
return nil
}
func (ib *imapBackend) setLastMailClient(id imapid.ID) {
ib.lastMailClientLocker.Lock()
defer ib.lastMailClientLocker.Unlock()
if name, ok := id[imapid.FieldName]; ok && ib.lastMailClient[imapid.FieldName] != name {
ib.lastMailClient = imapid.ID{}
for k, v := range id {
ib.lastMailClient[k] = v
}
log.Warn("Mail Client ID changed to ", ib.lastMailClient)
ib.bridge.SetCurrentClient(
ib.lastMailClient[imapid.FieldName],
ib.lastMailClient[imapid.FieldVersion],
)
}
}
// monitorDisconnectedUsers removes users when it receives a close connection event for them.
func (ib *imapBackend) monitorDisconnectedUsers() {
ch := make(chan string)

View File

@ -80,7 +80,7 @@ func (ib *imapBackend) removeFromCache(userID, label, toRemove string) {
func (ib *imapBackend) getCacheList(userID, label string) (list string) {
if err := ib.loadIMAPCache(); err != nil {
log.Warn("Could not load cache: ", err)
log.WithError(err).Warn("Could not load cache")
}
ib.imapCacheLock.Lock()
@ -97,7 +97,9 @@ func (ib *imapBackend) getCacheList(userID, label string) (list string) {
ib.imapCacheLock.Unlock()
_ = ib.saveIMAPCache()
if err := ib.saveIMAPCache(); err != nil {
log.WithError(err).Warn("Could not save cache")
}
return
}

View File

@ -0,0 +1,90 @@
// Copyright (c) 2020 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 id
import (
imapid "github.com/ProtonMail/go-imap-id"
imapserver "github.com/emersion/go-imap/server"
)
type currentClientSetter interface {
SetCurrentClient(name, version string)
}
// Extension for IMAP server
type extension struct {
extID imapserver.ConnExtension
setter currentClientSetter
}
func (ext *extension) Capabilities(conn imapserver.Conn) []string {
return ext.extID.Capabilities(conn)
}
func (ext *extension) Command(name string) imapserver.HandlerFactory {
newIDHandler := ext.extID.Command(name)
if newIDHandler == nil {
return nil
}
return func() imapserver.Handler {
if hdlrID, ok := newIDHandler().(*imapid.Handler); ok {
return &handler{
hdlrID: hdlrID,
setter: ext.setter,
}
}
return nil
}
}
func (ext *extension) NewConn(conn imapserver.Conn) imapserver.Conn {
return ext.extID.NewConn(conn)
}
type handler struct {
hdlrID *imapid.Handler
setter currentClientSetter
}
func (hdlr *handler) Parse(fields []interface{}) error {
return hdlr.hdlrID.Parse(fields)
}
func (hdlr *handler) Handle(conn imapserver.Conn) error {
err := hdlr.hdlrID.Handle(conn)
if err == nil {
id := hdlr.hdlrID.Command.ID
hdlr.setter.SetCurrentClient(
id[imapid.FieldName],
id[imapid.FieldVersion],
)
}
return err
}
// NewExtension returns extension which is adding RFC2871 ID capability, with
// direct interface to set information about email client to backend.
func NewExtension(serverID imapid.ID, setter currentClientSetter) imapserver.Extension {
if conExtID, ok := imapid.NewExtension(serverID).(imapserver.ConnExtension); ok {
return &extension{
extID: conExtID,
setter: setter,
}
}
return nil
}

View File

@ -22,12 +22,6 @@ import "github.com/sirupsen/logrus"
const (
fetchMessagesWorkers = 5 // In how many workers to fetch message (group list on IMAP).
fetchAttachmentsWorkers = 5 // In how many workers to fetch attachments (for one message).
clientAppleMail = "Mac OS X Mail" //nolint[deadcode]
clientThunderbird = "Thunderbird" //nolint[deadcode]
clientOutlookMac = "Microsoft Outlook for Mac" //nolint[deadcode]
clientOutlookWin = "Microsoft Outlook" //nolint[deadcode]
clientNone = ""
)
var (

View File

@ -173,9 +173,8 @@ func (im *imapMailbox) Check() error {
// Expunge permanently removes all messages that have the \Deleted flag set
// from the currently selected mailbox.
// Our messages do not have \Deleted flag, nothing to do here.
func (im *imapMailbox) Expunge() error {
return nil
return im.storeMailbox.RemoveDeleted()
}
func (im *imapMailbox) ListQuotas() ([]string, error) {

View File

@ -37,7 +37,6 @@ import (
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/emersion/go-imap"
"github.com/hashicorp/go-multierror"
enmime "github.com/jhillyerd/enmime"
"github.com/pkg/errors"
openpgperrors "golang.org/x/crypto/openpgp/errors"
)
@ -221,6 +220,9 @@ func (im *imapMailbox) getMessage(storeMessage storeMessageProvider, items []ima
}
case imap.FetchFlags:
msg.Flags = message.GetFlags(m)
if storeMessage.IsMarkedDeleted() {
msg.Flags = append(msg.Flags, imap.DeletedFlag)
}
case imap.FetchInternalDate:
msg.InternalDate = time.Unix(m.Time, 0)
case imap.FetchRFC822Size:
@ -238,26 +240,30 @@ func (im *imapMailbox) getMessage(storeMessage storeMessageProvider, items []ima
return nil, err
}
default:
s := item
var section *imap.BodySectionName
if section, err = imap.ParseBodySectionName(s); err != nil {
err = nil // Ignore error
break
}
var literal imap.Literal
if literal, err = im.getMessageBodySection(storeMessage, section); err != nil {
if err = im.getLiteralForSection(item, msg, storeMessage); err != nil {
return
}
msg.Body[section] = literal
}
}
return msg, err
}
func (im *imapMailbox) getLiteralForSection(itemSection imap.FetchItem, msg *imap.Message, storeMessage storeMessageProvider) error {
section, err := imap.ParseBodySectionName(itemSection)
if err != nil { // Ignore error
return nil
}
var literal imap.Literal
if literal, err = im.getMessageBodySection(storeMessage, section); err != nil {
return err
}
msg.Body[section] = literal
return nil
}
func (im *imapMailbox) getBodyStructure(storeMessage storeMessageProvider) (
structure *message.BodyStructure,
bodyReader *bytes.Reader, err error,
@ -446,17 +452,6 @@ func (im *imapMailbox) writeMessageBody(w io.Writer, m *pmapi.Message) (err erro
return
}
func (im *imapMailbox) writeAndParseMIMEBody(m *pmapi.Message) (mime *enmime.Envelope, err error) { //nolint[unused]
b := &bytes.Buffer{}
if err = im.writeMessageBody(b, m); err != nil {
return
}
mime, err = enmime.ReadEnvelope(b)
return
}
func (im *imapMailbox) writeAttachmentBody(w io.Writer, m *pmapi.Message, att *pmapi.Attachment) (err error) {
// Retrieve encrypted attachment.
r, err := im.user.client().GetAttachment(att.ID)

View File

@ -57,7 +57,7 @@ func (im *imapMailbox) UpdateMessagesFlags(uid bool, seqSet *imap.SeqSet, operat
return im.addOrRemoveFlags(operation, messageIDs, flags)
}
func (im *imapMailbox) setFlags(messageIDs, flags []string) error {
func (im *imapMailbox) setFlags(messageIDs, flags []string) error { //nolint
seen := false
flagged := false
deleted := false
@ -77,19 +77,33 @@ func (im *imapMailbox) setFlags(messageIDs, flags []string) error {
}
if seen {
_ = im.storeMailbox.MarkMessagesRead(messageIDs)
if err := im.storeMailbox.MarkMessagesRead(messageIDs); err != nil {
return err
}
} else {
_ = im.storeMailbox.MarkMessagesUnread(messageIDs)
if err := im.storeMailbox.MarkMessagesUnread(messageIDs); err != nil {
return err
}
}
if flagged {
_ = im.storeMailbox.MarkMessagesStarred(messageIDs)
if err := im.storeMailbox.MarkMessagesStarred(messageIDs); err != nil {
return err
}
} else {
_ = im.storeMailbox.MarkMessagesUnstarred(messageIDs)
if err := im.storeMailbox.MarkMessagesUnstarred(messageIDs); err != nil {
return err
}
}
if deleted {
_ = im.storeMailbox.DeleteMessages(messageIDs)
if err := im.storeMailbox.MarkMessagesDeleted(messageIDs); err != nil {
return err
}
} else {
if err := im.storeMailbox.MarkMessagesUndeleted(messageIDs); err != nil {
return err
}
}
spamMailbox, err := im.storeAddress.GetMailbox("Spam")
@ -97,9 +111,13 @@ func (im *imapMailbox) setFlags(messageIDs, flags []string) error {
return err
}
if spam {
_ = spamMailbox.LabelMessages(messageIDs)
if err := spamMailbox.LabelMessages(messageIDs); err != nil {
return err
}
} else {
_ = spamMailbox.UnlabelMessages(messageIDs)
if err := spamMailbox.UnlabelMessages(messageIDs); err != nil {
return err
}
}
return nil
@ -111,22 +129,36 @@ func (im *imapMailbox) addOrRemoveFlags(operation imap.FlagsOp, messageIDs, flag
case imap.SeenFlag:
switch operation {
case imap.AddFlags:
_ = im.storeMailbox.MarkMessagesRead(messageIDs)
if err := im.storeMailbox.MarkMessagesRead(messageIDs); err != nil {
return err
}
case imap.RemoveFlags:
_ = im.storeMailbox.MarkMessagesUnread(messageIDs)
if err := im.storeMailbox.MarkMessagesUnread(messageIDs); err != nil {
return err
}
}
case imap.FlaggedFlag:
switch operation {
case imap.AddFlags:
_ = im.storeMailbox.MarkMessagesStarred(messageIDs)
if err := im.storeMailbox.MarkMessagesStarred(messageIDs); err != nil {
return err
}
case imap.RemoveFlags:
_ = im.storeMailbox.MarkMessagesUnstarred(messageIDs)
if err := im.storeMailbox.MarkMessagesUnstarred(messageIDs); err != nil {
return err
}
}
case imap.DeletedFlag:
if operation == imap.RemoveFlags {
break // Nothing to do, no message has the \Deleted flag.
switch operation {
case imap.AddFlags:
if err := im.storeMailbox.MarkMessagesDeleted(messageIDs); err != nil {
return err
}
case imap.RemoveFlags:
if err := im.storeMailbox.MarkMessagesUndeleted(messageIDs); err != nil {
return err
}
}
_ = im.storeMailbox.DeleteMessages(messageIDs)
case imap.AnsweredFlag, imap.DraftFlag, imap.RecentFlag:
// Not supported.
case message.AppleMailJunkFlag, message.ThunderbirdJunkFlag:
@ -140,9 +172,13 @@ func (im *imapMailbox) addOrRemoveFlags(operation imap.FlagsOp, messageIDs, flag
// No label removal is necessary because Spam and Inbox are both exclusive labels so the backend
// will automatically take care of label removal.
case imap.AddFlags:
_ = storeMailbox.LabelMessages(messageIDs)
if err := storeMailbox.LabelMessages(messageIDs); err != nil {
return err
}
case imap.RemoveFlags:
_ = storeMailbox.UnlabelMessages(messageIDs)
if err := storeMailbox.UnlabelMessages(messageIDs); err != nil {
return err
}
}
}
}
@ -186,6 +222,20 @@ func (im *imapMailbox) labelMessages(uid bool, seqSet *imap.SeqSet, targetLabel
return err
}
deletedIDs := []string{}
allDeletedIDs, err := im.storeMailbox.GetDeletedAPIIDs()
if err != nil {
log.WithError(err).Warn("Problem to get deleted API IDs")
} else {
for _, messageID := range messageIDs {
for _, deletedID := range allDeletedIDs {
if messageID == deletedID {
deletedIDs = append(deletedIDs, deletedID)
}
}
}
}
// Label messages first to not lose them. If message is only in trash and we unlabel
// it, it will be removed completely and we cannot label it back.
if err := targetStoreMailbox.LabelMessages(messageIDs); err != nil {
@ -197,6 +247,13 @@ func (im *imapMailbox) labelMessages(uid bool, seqSet *imap.SeqSet, targetLabel
}
}
// Preserve \Deleted flag at target location.
if len(deletedIDs) > 0 {
if err := targetStoreMailbox.MarkMessagesDeleted(deletedIDs); err != nil {
log.WithError(err).Warn("Problem to preserve deleted flag for copied messages")
}
}
targetSeqSet := targetStoreMailbox.GetUIDList(messageIDs)
return uidplus.CopyResponse(targetStoreMailbox.UIDValidity(), sourceSeqSet, targetSeqSet)
}
@ -321,6 +378,9 @@ func (im *imapMailbox) SearchMessages(isUID bool, criteria *imap.SearchCriteria)
if !m.Has(pmapi.FlagOpened) {
messageFlagsMap[imap.RecentFlag] = true
}
if storeMessage.IsMarkedDeleted() {
messageFlagsMap[imap.DeletedFlag] = true
}
flagMatch := true
for _, flag := range criteria.WithFlags {
@ -383,6 +443,12 @@ func (im *imapMailbox) ListMessages(isUID bool, seqSet *imap.SeqSet, items []ima
im.panicHandler.HandlePanic()
}()
// EXPUNGE cannot be sent during listing and can come only from
// the event loop, so we prevent any server side update to avoid
// the problem.
im.storeUser.PauseEventLoop(true)
defer im.storeUser.PauseEventLoop(false)
var markAsReadIDs []string
markAsReadMutex := &sync.Mutex{}

View File

@ -28,6 +28,7 @@ import (
imapid "github.com/ProtonMail/go-imap-id"
"github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/imap/id"
"github.com/ProtonMail/proton-bridge/internal/imap/uidplus"
"github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/emersion/go-imap"
@ -60,34 +61,12 @@ func NewIMAPServer(debugClient, debugServer bool, port int, tls *tls.Config, ima
s.UpgradeError = imapBackend.upgradeError
serverID := imapid.ID{
imapid.FieldName: "ProtonMail",
imapid.FieldName: "ProtonMail Bridge",
imapid.FieldVendor: "Proton Technologies AG",
imapid.FieldSupportURL: "https://protonmail.com/support",
}
s.EnableAuth(sasl.Login, func(conn imapserver.Conn) sasl.Server {
conn.Server().ForEachConn(func(candidate imapserver.Conn) {
if id, ok := candidate.(imapid.Conn); ok {
if conn.Context() == candidate.Context() {
// ID is not available right at the beginning of the connection.
// Clients send ID quickly after AUTH. We need to wait for it.
go func() {
start := time.Now()
for {
if id.ID() != nil {
imapBackend.setLastMailClient(id.ID())
break
}
if time.Since(start) > 10*time.Second {
break
}
time.Sleep(100 * time.Millisecond)
}
}()
}
}
})
return sasl.NewLoginServer(func(address, password string) error {
user, err := conn.Server().Backend.Login(nil, address, password)
if err != nil {
@ -105,7 +84,7 @@ func NewIMAPServer(debugClient, debugServer bool, port int, tls *tls.Config, ima
imapidle.NewExtension(),
imapmove.NewExtension(),
imapspecialuse.NewExtension(),
imapid.NewExtension(serverID),
id.NewExtension(serverID, imapBackend.bridge),
imapquota.NewExtension(),
imapappendlimit.NewExtension(),
imapunselect.NewExtension(),

View File

@ -41,6 +41,8 @@ type storeUserProvider interface {
attachedPublicKey,
attachedPublicKeyName string,
parentID string) (*pmapi.Message, []*pmapi.Attachment, error)
PauseEventLoop(bool)
}
type storeAddressProvider interface {
@ -68,6 +70,7 @@ type storeMailboxProvider interface {
GetAPIIDsFromSequenceRange(start, stop uint32) ([]string, error)
GetLatestAPIID() (string, error)
GetNextUID() (uint32, error)
GetDeletedAPIIDs() ([]string, error)
GetCounts() (dbTotal, dbUnread, dbUnreadSeqNum uint, err error)
GetUIDList(apiIDs []string) *uidplus.OrderedSeq
GetUIDByHeader(header *mail.Header) uint32
@ -81,8 +84,10 @@ type storeMailboxProvider interface {
MarkMessagesUnread(apiID []string) error
MarkMessagesStarred(apiID []string) error
MarkMessagesUnstarred(apiID []string) error
MarkMessagesDeleted(apiID []string) error
MarkMessagesUndeleted(apiID []string) error
ImportMessage(msg *pmapi.Message, body []byte, labelIDs []string) error
DeleteMessages(apiID []string) error
RemoveDeleted() error
}
type storeMessageProvider interface {
@ -90,6 +95,7 @@ type storeMessageProvider interface {
UID() (uint32, error)
SequenceNumber() (uint32, error)
Message() *pmapi.Message
IsMarkedDeleted() bool
SetSize(int64) error
SetContentTypeAndHeader(string, mail.Header) error

View File

@ -25,6 +25,7 @@
package uidplus
import (
"errors"
"fmt"
"github.com/emersion/go-imap"
@ -113,18 +114,43 @@ func (os *OrderedSeq) String() string {
return out
}
// UIDExpunge implements server.Handler but has no effect because Bridge is not
// using EXPUNGE at all. The message is deleted right after it was flagged as
// \Deleted Bridge should simply ignore this command with empty `OK` response.
//
// If not implemented it would cause harmless IMAP error.
//
// This overrides the standard EXPUNGE functionality.
type UIDExpunge struct{}
// UIDExpunge implements server.Handler but Bridge is not supporting
// UID EXPUNGE with specific UIDs.
type UIDExpunge struct {
expunge *server.Expunge
}
func (e *UIDExpunge) Parse(fields []interface{}) error { log.Traceln("parse", fields); return nil }
func (e *UIDExpunge) Handle(conn server.Conn) error { log.Traceln("handle"); return nil }
func (e *UIDExpunge) UidHandle(conn server.Conn) error { log.Traceln("uid handle"); return nil } //nolint[golint]
func newUIDExpunge() *UIDExpunge {
return &UIDExpunge{expunge: &server.Expunge{}}
}
func (e *UIDExpunge) Parse(fields []interface{}) error {
if len(fields) < 1 {
return e.expunge.Parse(fields)
}
// RFC4315#section-2.1
// The UID EXPUNGE command permanently removes all messages that both
// have the \Deleted flag set and have a UID that is included in the
// specified sequence set from the currently selected mailbox. If a
// message either does not have the \Deleted flag set or has a UID
// that is not included in the specified sequence set, it is not
// affected.
//
// Current implementation supports only deletion of all messages
// marked as deleted. It will probably need mailbox interface change:
// ExpungeUIDs(seqSet). Not sure how to combine with original
// e.expunge.Handle().
return errors.New("UID EXPUNGE with UIDs is not supported")
}
func (e *UIDExpunge) Handle(conn server.Conn) error {
return e.expunge.Handle(conn)
}
func (e *UIDExpunge) UidHandle(conn server.Conn) error { //nolint[golint]
return e.expunge.Handle(conn)
}
type extension struct{}
@ -143,7 +169,7 @@ func (ext *extension) Capabilities(c server.Conn) []string {
func (ext *extension) Command(name string) server.HandlerFactory {
if name == "EXPUNGE" {
return func() server.Handler {
return &UIDExpunge{}
return newUIDExpunge()
}
}

View File

@ -15,8 +15,8 @@
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// Code generated by ./credits.sh at Fri 04 Sep 2020 01:57:36 PM CEST. DO NOT EDIT.
// Code generated by ./credits.sh at Wed Sep 23 01:34:10 PM CEST 2020. DO NOT EDIT.
package importexport
const Credits = "github.com/0xAX/notificator;github.com/abiosoft/ishell;github.com/abiosoft/readline;github.com/allan-simon/go-singleinstance;github.com/andybalholm/cascadia;github.com/certifi/gocertifi;github.com/chzyer/logex;github.com/chzyer/test;github.com/cucumber/godog;github.com/docker/docker-credential-helpers;github.com/emersion/go-imap;github.com/emersion/go-imap-appendlimit;github.com/emersion/go-imap-idle;github.com/emersion/go-imap-move;github.com/emersion/go-imap-quota;github.com/emersion/go-imap-specialuse;github.com/emersion/go-imap-unselect;github.com/emersion/go-mbox;github.com/emersion/go-message;github.com/emersion/go-sasl;github.com/emersion/go-smtp;github.com/emersion/go-textwrapper;github.com/emersion/go-vcard;github.com/fatih/color;github.com/flynn-archive/go-shlex;github.com/getsentry/raven-go;github.com/go-delve/delve;github.com/golang/mock;github.com/google/go-cmp;github.com/google/uuid;github.com/gopherjs/gopherjs;github.com/go-resty/resty/v2;github.com/hashicorp/go-multierror;github.com/jameshoulahan/go-imap;github.com/jameskeane/bcrypt;github.com/jaytaylor/html2text;github.com/jhillyerd/enmime;github.com/kardianos/osext;github.com/keybase/go-keychain;github.com/logrusorgru/aurora;github.com/mattn/go-runewidth;github.com/miekg/dns;github.com/myesui/uuid;github.com/nsf/jsondiff;github.com/olekukonko/tablewriter;github.com/pkg/errors;github.com/ProtonMail/bcrypt;github.com/ProtonMail/crypto;github.com/ProtonMail/docker-credential-helpers;github.com/ProtonMail/go-appdir;github.com/ProtonMail/go-apple-mobileconfig;github.com/ProtonMail/go-autostart;github.com/ProtonMail/go-imap-id;github.com/ProtonMail/gopenpgp/v2;github.com/ProtonMail/go-smtp;github.com/ProtonMail/go-vcard;github.com/psampaz/go-mod-outdated;github.com/sirupsen/logrus;github.com/skratchdot/open-golang;github.com/stretchr/testify;github.com/therecipe/qt;github.com/twinj/uuid;github.com/urfave/cli;go.etcd.io/bbolt;golang.org/x/crypto;golang.org/x/net;golang.org/x/text;gopkg.in/stretchr/testify.v1;;Font Awesome 4.7.0;;Qt 5.13 by Qt group;"
const Credits = "github.com/0xAX/notificator;github.com/abiosoft/ishell;github.com/abiosoft/readline;github.com/allan-simon/go-singleinstance;github.com/certifi/gocertifi;github.com/chzyer/logex;github.com/chzyer/test;github.com/cucumber/godog;github.com/docker/docker-credential-helpers;github.com/emersion/go-imap;github.com/emersion/go-imap-appendlimit;github.com/emersion/go-imap-idle;github.com/emersion/go-imap-move;github.com/emersion/go-imap-quota;github.com/emersion/go-imap-specialuse;github.com/emersion/go-imap-unselect;github.com/emersion/go-mbox;github.com/emersion/go-message;github.com/emersion/go-sasl;github.com/emersion/go-smtp;github.com/emersion/go-textwrapper;github.com/emersion/go-vcard;github.com/fatih/color;github.com/flynn-archive/go-shlex;github.com/getsentry/raven-go;github.com/golang/mock;github.com/google/go-cmp;github.com/google/uuid;github.com/gopherjs/gopherjs;github.com/go-resty/resty/v2;github.com/hashicorp/go-multierror;github.com/jameskeane/bcrypt;github.com/jaytaylor/html2text;github.com/kardianos/osext;github.com/keybase/go-keychain;github.com/logrusorgru/aurora;github.com/Masterminds/semver/v3;github.com/mattn/go-runewidth;github.com/miekg/dns;github.com/myesui/uuid;github.com/nsf/jsondiff;github.com/olekukonko/tablewriter;github.com/pkg/errors;github.com/ProtonMail/bcrypt;github.com/ProtonMail/crypto;github.com/ProtonMail/docker-credential-helpers;github.com/ProtonMail/go-appdir;github.com/ProtonMail/go-apple-mobileconfig;github.com/ProtonMail/go-autostart;github.com/ProtonMail/go-imap;github.com/ProtonMail/go-imap-id;github.com/ProtonMail/gopenpgp/v2;github.com/ProtonMail/go-smtp;github.com/ProtonMail/go-vcard;github.com/ProtonMail/mbox;github.com/PuerkitoBio/goquery;github.com/sirupsen/logrus;github.com/skratchdot/open-golang;github.com/ssor/bom;github.com/stretchr/testify;github.com/therecipe/qt;github.com/twinj/uuid;github.com/urfave/cli;go.etcd.io/bbolt;golang.org/x/crypto;golang.org/x/net;golang.org/x/text;gopkg.in/stretchr/testify.v1;;Font Awesome 4.7.0;;Qt 5.13 by Qt group;"

View File

@ -15,19 +15,17 @@
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// Code generated by ./release-notes.sh at 'Fri 04 Sep 2020 01:57:36 PM CEST'. DO NOT EDIT.
// Code generated by ./release-notes.sh at 'Wed Sep 23 01:31:53 PM CEST 2020'. DO NOT EDIT.
package importexport
const ReleaseNotes = `***Note: If you were using the Import-Export app before, you need to uninstall the older version and log in again to the new version***
Complete code refactor in preparation of stable and open source release of the Import-Export app
• Increased number of supported mail providers by changing the way the folder structures are handled (NIL hierarchy delimiter)
Improved handling for unstable internet and pause and resume behavior
const ReleaseNotes = `• Speed up import by implementing parallel processing (parallel fetch, encrypt and upload of messages)
• Optimising the initial fetch of messages from external accounts
Better handling of attachments and non-standard formatting
• Improved stability of the message parser
Added persistent anonymous API cookies
`
const ReleaseFixedBugs = `Fixed rare cases where the application freezes when starting/stopping imports
Allowed current date to be included in the selected date range for both import and export
• Improved manual update process
• Limit space usage by on device application logs
const ReleaseFixedBugs = `Import from mbox files with long lines
Improvements to import from Yahoo accounts
`

View File

@ -95,9 +95,8 @@ func (b *sendPreferencesBuilder) shouldEncrypt() bool {
return false
}
func (b *sendPreferencesBuilder) withSign() {
v := true
b.sign = &v
func (b *sendPreferencesBuilder) withSign(sign bool) {
b.sign = &sign
}
func (b *sendPreferencesBuilder) withSignDefault() {
@ -192,7 +191,7 @@ func (b *sendPreferencesBuilder) build() (p SendPreferences) {
p.Scheme = pmapi.PGPMIMEPackage
}
case b.shouldSign() && !b.shouldEncrypt():
case b.shouldSign() && !b.shouldEncrypt() && b.getScheme() == pgpMIME:
p.Scheme = pmapi.ClearMIMEPackage
default:
@ -258,7 +257,7 @@ func (b *sendPreferencesBuilder) setInternalPGPSettings(
// We always encrypt and sign internal mail.
b.withEncrypt(true)
b.withSign()
b.withSign(true)
// We use a custom scheme for internal messages.
b.withScheme(pmInternal)
@ -369,7 +368,7 @@ func (b *sendPreferencesBuilder) setExternalPGPSettingsWithWKDKeys(
// We always encrypt and sign external mail if WKD keys are present.
b.withEncrypt(true)
b.withSign()
b.withSign(true)
// If the contact has a specific Scheme preference, we set it (otherwise we
// leave it unset to allow it to be filled in with the default value later).
@ -402,9 +401,13 @@ func (b *sendPreferencesBuilder) setExternalPGPSettingsWithoutWKDKeys(
) (err error) {
b.withEncrypt(vCardData.Encrypt)
if vCardData.SignIsSet {
b.withSign(vCardData.Sign)
}
// Sign must be enabled whenever encrypt is.
if vCardData.Sign || vCardData.Encrypt {
b.withSign()
if vCardData.Encrypt {
b.withSign(true)
}
// If the contact has a specific Scheme preference, we set it (otherwise we
@ -475,7 +478,7 @@ func (b *sendPreferencesBuilder) setEncryptionPreferences(mailSettings pmapi.Mai
}
if b.shouldEncrypt() {
b.withSign()
b.withSign(true)
}
// If undefined, default to the user mail setting "Default PGP scheme".
@ -495,12 +498,7 @@ func (b *sendPreferencesBuilder) setEncryptionPreferences(mailSettings pmapi.Mai
if b.shouldSign() && b.getScheme() == pgpInline {
b.withMIMEType("text/plain")
} else {
switch mailSettings.ComposerMode {
case pmapi.ComposerModeNormal:
b.withMIMETypeDefault("text/html")
case pmapi.ComposerModePlain:
b.withMIMETypeDefault("text/plain")
}
b.withMIMETypeDefault(mailSettings.DraftMIMEType)
}
}

View File

@ -51,7 +51,7 @@ func TestPreferencesBuilder(t *testing.T) {
contactMeta: &ContactMetadata{},
receivedKeys: []pmapi.PublicKey{{PublicKey: testPublicKey}},
isInternal: true,
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, ComposerMode: pmapi.ComposerModeNormal},
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, DraftMIMEType: "text/html"},
wantEncrypt: true,
wantSign: true,
@ -66,7 +66,7 @@ func TestPreferencesBuilder(t *testing.T) {
contactMeta: &ContactMetadata{MIMEType: "text/plain"},
receivedKeys: []pmapi.PublicKey{{PublicKey: testPublicKey}},
isInternal: true,
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, ComposerMode: pmapi.ComposerModeNormal},
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, DraftMIMEType: "text/html"},
wantEncrypt: true,
wantSign: true,
@ -81,7 +81,7 @@ func TestPreferencesBuilder(t *testing.T) {
contactMeta: &ContactMetadata{Keys: []string{testContactKey}},
receivedKeys: []pmapi.PublicKey{{PublicKey: testPublicKey}},
isInternal: true,
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, ComposerMode: pmapi.ComposerModeNormal},
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, DraftMIMEType: "text/html"},
wantEncrypt: true,
wantSign: true,
@ -97,7 +97,7 @@ func TestPreferencesBuilder(t *testing.T) {
contactMeta: &ContactMetadata{Keys: []string{testOtherContactKey}},
receivedKeys: []pmapi.PublicKey{{PublicKey: testPublicKey}},
isInternal: true,
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, ComposerMode: pmapi.ComposerModeNormal},
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, DraftMIMEType: "text/html"},
wantEncrypt: true,
wantSign: true,
@ -112,7 +112,7 @@ func TestPreferencesBuilder(t *testing.T) {
contactMeta: &ContactMetadata{},
receivedKeys: []pmapi.PublicKey{{PublicKey: testPublicKey}},
isInternal: false,
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, ComposerMode: pmapi.ComposerModeNormal},
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, DraftMIMEType: "text/html"},
wantEncrypt: true,
wantSign: true,
@ -127,7 +127,7 @@ func TestPreferencesBuilder(t *testing.T) {
contactMeta: &ContactMetadata{MIMEType: "text/plain"},
receivedKeys: []pmapi.PublicKey{{PublicKey: testPublicKey}},
isInternal: false,
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, ComposerMode: pmapi.ComposerModeNormal},
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, DraftMIMEType: "text/html"},
wantEncrypt: true,
wantSign: true,
@ -142,7 +142,7 @@ func TestPreferencesBuilder(t *testing.T) {
contactMeta: &ContactMetadata{},
receivedKeys: []pmapi.PublicKey{{PublicKey: testPublicKey}},
isInternal: false,
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPInlinePackage, ComposerMode: pmapi.ComposerModeNormal},
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPInlinePackage, DraftMIMEType: "text/html"},
wantEncrypt: true,
wantSign: true,
@ -157,7 +157,7 @@ func TestPreferencesBuilder(t *testing.T) {
contactMeta: &ContactMetadata{Scheme: pgpInline},
receivedKeys: []pmapi.PublicKey{{PublicKey: testPublicKey}},
isInternal: false,
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, ComposerMode: pmapi.ComposerModeNormal},
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, DraftMIMEType: "text/html"},
wantEncrypt: true,
wantSign: true,
@ -172,7 +172,7 @@ func TestPreferencesBuilder(t *testing.T) {
contactMeta: &ContactMetadata{Scheme: pgpMIME},
receivedKeys: []pmapi.PublicKey{{PublicKey: testPublicKey}},
isInternal: false,
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPInlinePackage, ComposerMode: pmapi.ComposerModeNormal},
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPInlinePackage, DraftMIMEType: "text/html"},
wantEncrypt: true,
wantSign: true,
@ -187,7 +187,7 @@ func TestPreferencesBuilder(t *testing.T) {
contactMeta: &ContactMetadata{Keys: []string{testContactKey}},
receivedKeys: []pmapi.PublicKey{{PublicKey: testPublicKey}},
isInternal: false,
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, ComposerMode: pmapi.ComposerModeNormal},
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, DraftMIMEType: "text/html"},
wantEncrypt: true,
wantSign: true,
@ -203,7 +203,7 @@ func TestPreferencesBuilder(t *testing.T) {
contactMeta: &ContactMetadata{Keys: []string{testOtherContactKey}},
receivedKeys: []pmapi.PublicKey{{PublicKey: testPublicKey}},
isInternal: false,
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, ComposerMode: pmapi.ComposerModeNormal},
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, DraftMIMEType: "text/html"},
wantEncrypt: true,
wantSign: true,
@ -218,7 +218,7 @@ func TestPreferencesBuilder(t *testing.T) {
contactMeta: &ContactMetadata{},
receivedKeys: []pmapi.PublicKey{},
isInternal: false,
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, ComposerMode: pmapi.ComposerModeNormal},
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, DraftMIMEType: "text/html"},
wantEncrypt: false,
wantSign: false,
@ -232,7 +232,7 @@ func TestPreferencesBuilder(t *testing.T) {
contactMeta: &ContactMetadata{MIMEType: "text/plain"},
receivedKeys: []pmapi.PublicKey{},
isInternal: false,
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, ComposerMode: pmapi.ComposerModeNormal},
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, DraftMIMEType: "text/html"},
wantEncrypt: false,
wantSign: false,
@ -243,10 +243,10 @@ func TestPreferencesBuilder(t *testing.T) {
{
name: "external with sign enabled",
contactMeta: &ContactMetadata{Sign: true},
contactMeta: &ContactMetadata{Sign: true, SignIsSet: true},
receivedKeys: []pmapi.PublicKey{},
isInternal: false,
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, ComposerMode: pmapi.ComposerModeNormal},
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, DraftMIMEType: "text/html"},
wantEncrypt: false,
wantSign: true,
@ -260,7 +260,7 @@ func TestPreferencesBuilder(t *testing.T) {
contactMeta: &ContactMetadata{Keys: []string{testContactKey}},
receivedKeys: []pmapi.PublicKey{},
isInternal: false,
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, ComposerMode: pmapi.ComposerModeNormal},
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, DraftMIMEType: "text/html"},
wantEncrypt: false,
wantSign: false,
@ -272,10 +272,10 @@ func TestPreferencesBuilder(t *testing.T) {
{
name: "external with pinned contact public key, encrypted and signed",
contactMeta: &ContactMetadata{Keys: []string{testContactKey}, Encrypt: true, Sign: true},
contactMeta: &ContactMetadata{Keys: []string{testContactKey}, Encrypt: true, Sign: true, SignIsSet: true},
receivedKeys: []pmapi.PublicKey{},
isInternal: false,
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, ComposerMode: pmapi.ComposerModeNormal},
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, DraftMIMEType: "text/html"},
wantEncrypt: true,
wantSign: true,
@ -287,10 +287,10 @@ func TestPreferencesBuilder(t *testing.T) {
{
name: "external with pinned contact public key, encrypted and signed using contact-specific pgp-inline",
contactMeta: &ContactMetadata{Keys: []string{testContactKey}, Encrypt: true, Sign: true, Scheme: pgpInline},
contactMeta: &ContactMetadata{Keys: []string{testContactKey}, Encrypt: true, Sign: true, Scheme: pgpInline, SignIsSet: true},
receivedKeys: []pmapi.PublicKey{},
isInternal: false,
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, ComposerMode: pmapi.ComposerModeNormal},
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, DraftMIMEType: "text/html"},
wantEncrypt: true,
wantSign: true,
@ -302,10 +302,10 @@ func TestPreferencesBuilder(t *testing.T) {
{
name: "external with pinned contact public key, encrypted and signed using global pgp-inline",
contactMeta: &ContactMetadata{Keys: []string{testContactKey}, Encrypt: true, Sign: true},
contactMeta: &ContactMetadata{Keys: []string{testContactKey}, Encrypt: true, Sign: true, SignIsSet: true},
receivedKeys: []pmapi.PublicKey{},
isInternal: false,
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPInlinePackage, ComposerMode: pmapi.ComposerModeNormal},
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPInlinePackage, DraftMIMEType: "text/html"},
wantEncrypt: true,
wantSign: true,

View File

@ -21,6 +21,7 @@ package smtp
import (
"encoding/base64"
"fmt"
"io"
"mime"
"net/mail"
@ -179,11 +180,12 @@ func (su *smtpUser) Send(from string, to []string, messageReader io.Reader) (err
return err
}
attachedPublicKeyName = "publickey - " + kr.GetIdentities()[0].Name
attachedPublicKeyName = fmt.Sprintf("publickey - %v - %v", kr.GetIdentities()[0].Name, firstKey.GetFingerprint()[:8])
}
message, mimeBody, plainBody, attReaders, err := message.Parse(messageReader, attachedPublicKey, attachedPublicKeyName)
if err != nil {
log.WithError(err).Error("Failed to parse message")
return
}
clearBody := message.Body
@ -290,6 +292,9 @@ func (su *smtpUser) Send(from string, to []string, messageReader io.Reader) (err
}
sendPreferences, err := su.getSendPreferences(email, message.MIMEType, mailSettings)
if !sendPreferences.Encrypt {
containsUnencryptedRecipients = true
}
if err != nil {
return err
}
@ -359,7 +364,9 @@ func (su *smtpUser) Send(from string, to []string, messageReader io.Reader) (err
return errors.New("error decoding subject message " + message.Header.Get("Subject"))
}
if !su.continueSendingUnencryptedMail(subject) {
_ = su.client().DeleteMessages([]string{message.ID})
if err := su.client().DeleteMessages([]string{message.ID}); err != nil {
log.WithError(err).Warn("Failed to delete canceled messages")
}
return errors.New("sending was canceled by user")
}
}

View File

@ -27,13 +27,13 @@ import (
)
type ContactMetadata struct {
Email string
Keys []string
Scheme string
Sign bool
SignMissing bool
Encrypt bool
MIMEType string
Email string
Keys []string
Scheme string
Sign bool
SignIsSet bool
Encrypt bool
MIMEType string
}
const (
@ -72,22 +72,22 @@ func GetContactMetadataFromVCards(cards []pmapi.Card, email string) (contactMeta
// Warn: ParseBool treats 1, T, True, true as true and 0, F, Fale, false as false.
// However PMEL declares 'true' is true, 'false' is false. every other string is true
encrypt, _ := strconv.ParseBool(parsedCard.GetValueByGroup(FieldPMEncrypt, group))
var sign, signMissing bool
var sign, signIsSet bool
if len(parsedCard[FieldPMSign]) == 0 {
signMissing = true
signIsSet = false
} else {
sign, _ = strconv.ParseBool(parsedCard.GetValueByGroup(FieldPMSign, group))
signMissing = false
signIsSet = true
}
mimeType := parsedCard.GetValueByGroup(FieldPMMIMEType, group)
return &ContactMetadata{
Email: email,
Keys: keys,
Scheme: scheme,
Sign: sign,
SignMissing: signMissing,
Encrypt: encrypt,
MIMEType: mimeType,
Email: email,
Keys: keys,
Scheme: scheme,
Sign: sign,
SignIsSet: signIsSet,
Encrypt: encrypt,
MIMEType: mimeType,
}, nil
}
return &ContactMetadata{}, nil

View File

@ -45,7 +45,9 @@ func (c *Cache) getEventID(userID string) string {
c.lock.Lock()
defer c.lock.Unlock()
_ = c.loadCache()
if err := c.loadCache(); err != nil {
log.WithError(err).Warn("Problem to load store cache")
}
if c.cache == nil {
c.cache = map[string]map[string]string{}

View File

@ -48,18 +48,26 @@ func (store *Store) imapNotice(address, notice string) {
store.imapSendUpdate(update)
}
func (store *Store) imapUpdateMessage(address, mailboxName string, uid, sequenceNumber uint32, msg *pmapi.Message) {
func (store *Store) imapUpdateMessage(
address, mailboxName string,
uid, sequenceNumber uint32,
msg *pmapi.Message, hasDeletedFlag bool,
) {
store.log.WithFields(logrus.Fields{
"address": address,
"mailbox": mailboxName,
"seqNum": sequenceNumber,
"uid": uid,
"flags": message.GetFlags(msg),
"deleted": hasDeletedFlag,
}).Trace("IDLE update")
update := new(imapBackend.MessageUpdate)
update.Update = imapBackend.NewUpdate(address, mailboxName)
update.Message = imap.NewMessage(sequenceNumber, []imap.FetchItem{imap.FetchFlags, imap.FetchUid})
update.Message.Flags = message.GetFlags(msg)
if hasDeletedFlag {
update.Message.Flags = append(update.Message.Flags, imap.DeletedFlag)
}
update.Message.Uid = uid
store.imapSendUpdate(update)
}
@ -114,10 +122,22 @@ func (store *Store) imapSendUpdate(update imapBackend.Update) {
return
}
done := update.Done()
go func() {
// This timeout is to not keep running many blocked goroutines.
// In case nothing listens to this channel, this thread should stop.
select {
case store.imapUpdates <- update:
case <-time.After(1 * time.Second):
store.log.Warn("IMAP update could not be sent (timeout).")
}
}()
// This timeout is to not block IMAP backend by wait for IMAP client.
select {
case <-done:
case <-time.After(1 * time.Second):
store.log.Error("Could not send IMAP update (timeout)")
store.log.Warn("IMAP update could not be delivered (timeout).")
return
case store.imapUpdates <- update:
}
}

View File

@ -38,7 +38,8 @@ type eventLoop struct {
pollCh chan chan struct{}
stopCh chan struct{}
notifyStopCh chan struct{}
isRunning bool
isRunning bool // The whole event loop is running.
isTickerPaused bool // The periodic loop is paused (but the event loop itself is still running).
hasInternet bool
pollCounter int
@ -59,6 +60,7 @@ func newEventLoop(cache *Cache, store *Store, user BridgeUser, events listener.L
currentEventID: cache.getEventID(user.ID()),
pollCh: make(chan chan struct{}),
isRunning: false,
isTickerPaused: false,
log: eventLog,
@ -68,10 +70,6 @@ func newEventLoop(cache *Cache, store *Store, user BridgeUser, events listener.L
}
}
func (loop *eventLoop) IsRunning() bool {
return loop.isRunning
}
func (loop *eventLoop) client() pmapi.Client {
return loop.store.client()
}
@ -156,6 +154,10 @@ func (loop *eventLoop) loop() {
close(loop.notifyStopCh)
return
case <-t.C:
if loop.isTickerPaused {
loop.log.Trace("Event loop paused, skipping")
continue
}
// Randomise periodic calls within range pollInterval ± pollSpread to reduces potential load spikes on API.
time.Sleep(time.Duration(rand.Intn(2*int(pollIntervalSpread.Milliseconds()))) * time.Millisecond)
case eventProcessedCh = <-loop.pollCh:

View File

@ -24,6 +24,7 @@ import (
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@ -102,10 +103,13 @@ func TestEventLoopUpdateMessageFromLoop(t *testing.T) {
// Event loop runs in goroutine and will be stopped by deferred mock clearing.
go m.store.eventLoop.start()
require.Eventually(t, func() bool {
msg, err := m.store.getMessageFromDB("msg1")
var err error
assert.Eventually(t, func() bool {
var msg *pmapi.Message
msg, err = m.store.getMessageFromDB("msg1")
return err == nil && msg.Subject == newSubject
}, time.Second, 10*time.Millisecond)
require.NoError(t, err)
}
func TestEventLoopUpdateMessage(t *testing.T) {

View File

@ -41,7 +41,7 @@ type Mailbox struct {
}
func newMailbox(storeAddress *Address, labelID, labelPrefix, labelName, color string) (mb *Mailbox, err error) {
_ = storeAddress.store.db.Update(func(tx *bolt.Tx) error {
err = storeAddress.store.db.Update(func(tx *bolt.Tx) error {
mb, err = txNewMailbox(tx, storeAddress, labelID, labelPrefix, labelName, color)
return err
})
@ -238,6 +238,17 @@ func (storeMailbox *Mailbox) txGetAPIIDsBucket(tx *bolt.Tx) *bolt.Bucket {
return storeMailbox.txGetBucket(tx).Bucket(apiIDsBucket)
}
// txGetDeletedIDsBucket returns the bucket with messagesID marked as deleted
func (storeMailbox *Mailbox) txGetDeletedIDsBucket(tx *bolt.Tx) *bolt.Bucket {
// There should be no error since it _...returns an error if the bucket
// name is blank, or if the bucket name is too long._
bucket, err := storeMailbox.txGetBucket(tx).CreateBucketIfNotExists(deletedIDsBucket)
if err != nil || bucket == nil {
storeMailbox.log.WithError(err).Error("Cannot create or get bucket with deleted IDs.")
}
return bucket
}
// txGetBucket returns the bucket of mailbox containing mapping buckets.
func (storeMailbox *Mailbox) txGetBucket(tx *bolt.Tx) *bolt.Bucket {
return tx.Bucket(mailboxesBucket).Bucket(storeMailbox.getBucketName())

View File

@ -129,7 +129,11 @@ func (storeMailbox *Mailbox) getUID(apiID string) (uid uint32, err error) {
}
func (storeMailbox *Mailbox) txGetUID(tx *bolt.Tx, apiID string) (uint32, error) {
b := storeMailbox.txGetAPIIDsBucket(tx)
return storeMailbox.txGetUIDFromBucket(storeMailbox.txGetAPIIDsBucket(tx), apiID)
}
// txGetUIDFromBucket expects pointer to API bucket.
func (storeMailbox *Mailbox) txGetUIDFromBucket(b *bolt.Bucket, apiID string) (uint32, error) {
v := b.Get([]byte(apiID))
if v == nil {
return 0, ErrNoSuchAPIID
@ -137,6 +141,19 @@ func (storeMailbox *Mailbox) txGetUID(tx *bolt.Tx, apiID string) (uint32, error)
return btoi(v), nil
}
// GetDeletedAPIIDs returns API IDs in this mailbox for message ID.
func (storeMailbox *Mailbox) GetDeletedAPIIDs() (apiIDs []string, err error) {
err = storeMailbox.db().Update(func(tx *bolt.Tx) error {
b := storeMailbox.txGetDeletedIDsBucket(tx)
c := b.Cursor()
for k, _ := c.First(); k != nil; k, _ = c.Next() {
apiIDs = append(apiIDs, string(k))
}
return nil
})
return
}
// getSequenceNumber returns IMAP sequence number in the mailbox for the message with the given API ID `apiID`.
func (storeMailbox *Mailbox) getSequenceNumber(apiID string) (seqNum uint32, err error) {
err = storeMailbox.db().View(func(tx *bolt.Tx) error {

View File

@ -24,6 +24,8 @@ import (
bolt "go.etcd.io/bbolt"
)
// ErrAllMailOpNotAllowed is error user when user tries to do unsupported
// operation on All Mail folder
var ErrAllMailOpNotAllowed = errors.New("operation not allowed for 'All Mail' folder")
// GetMessage returns the `pmapi.Message` struct wrapped in `StoreMessage`
@ -96,11 +98,8 @@ func (storeMailbox *Mailbox) LabelMessages(apiIDs []string) error {
// It has to be propagated to all the same messages in all mailboxes.
// The propagation is processed by the event loop.
func (storeMailbox *Mailbox) UnlabelMessages(apiIDs []string) error {
log.WithFields(logrus.Fields{
"messages": apiIDs,
"label": storeMailbox.labelID,
"mailbox": storeMailbox.Name,
}).Trace("Unlabeling messages")
storeMailbox.log.WithField("messages", apiIDs).
Trace("Unlabeling messages")
if storeMailbox.labelID == pmapi.AllMailLabel {
return ErrAllMailOpNotAllowed
}
@ -129,6 +128,9 @@ func (storeMailbox *Mailbox) MarkMessagesRead(apiIDs []string) error {
ids = append(ids, apiID)
}
}
if len(ids) == 0 {
return nil
}
return storeMailbox.client().MarkMessagesRead(ids)
}
@ -170,54 +172,63 @@ func (storeMailbox *Mailbox) MarkMessagesUnstarred(apiIDs []string) error {
return storeMailbox.client().UnlabelMessages(apiIDs, pmapi.StarredLabel)
}
// DeleteMessages deletes messages.
// If the mailbox is All Mail or All Sent, it does nothing.
// If the mailbox is Trash or Spam and message is not in any other mailbox, messages is deleted.
// In all other cases the message is only removed from the mailbox.
func (storeMailbox *Mailbox) DeleteMessages(apiIDs []string) error {
// MarkMessagesDeleted adds local flag \Deleted. This is not propagated to API
// until RemoveDeleted is called
func (storeMailbox *Mailbox) MarkMessagesDeleted(apiIDs []string) error {
log.WithFields(logrus.Fields{
"messages": apiIDs,
"label": storeMailbox.labelID,
"mailbox": storeMailbox.Name,
}).Trace("Deleting messages")
}).Trace("Marking messages as deleted")
if storeMailbox.labelID == pmapi.AllMailLabel {
return ErrAllMailOpNotAllowed
}
return storeMailbox.store.db.Update(func(tx *bolt.Tx) error {
return storeMailbox.txMarkMessagesAsDeleted(tx, apiIDs, true)
})
}
// MarkMessagesUndeleted removes local flag \Deleted. This is not propagated to
// API.
func (storeMailbox *Mailbox) MarkMessagesUndeleted(apiIDs []string) error {
log.WithFields(logrus.Fields{
"messages": apiIDs,
"label": storeMailbox.labelID,
"mailbox": storeMailbox.Name,
}).Trace("Marking messages as undeleted")
if storeMailbox.labelID == pmapi.AllMailLabel {
return ErrAllMailOpNotAllowed
}
return storeMailbox.store.db.Update(func(tx *bolt.Tx) error {
return storeMailbox.txMarkMessagesAsDeleted(tx, apiIDs, false)
})
}
// RemoveDeleted sends request to API to remove message from mailbox.
// If the mailbox is All Mail or All Sent, it does nothing.
// If the mailbox is Trash or Spam and message is not in any other mailbox, messages is deleted.
// In all other cases the message is only removed from the mailbox.
func (storeMailbox *Mailbox) RemoveDeleted() error {
storeMailbox.log.Trace("Deleting messages")
apiIDs, err := storeMailbox.GetDeletedAPIIDs()
if err != nil {
return err
}
if len(apiIDs) == 0 {
storeMailbox.log.Debug("List to expunge is empty")
return nil
}
defer storeMailbox.pollNow()
switch storeMailbox.labelID {
case pmapi.AllMailLabel, pmapi.AllSentLabel:
break
case pmapi.TrashLabel, pmapi.SpamLabel:
messageIDsToDelete := []string{}
messageIDsToUnlabel := []string{}
for _, apiID := range apiIDs {
msg, err := storeMailbox.store.getMessageFromDB(apiID)
if err != nil {
return err
}
otherLabels := false
// If the message has any custom label, we don't want to delete it, only remove trash/spam label.
for _, label := range msg.LabelIDs {
if label != pmapi.SpamLabel && label != pmapi.TrashLabel && label != pmapi.AllMailLabel && label != pmapi.AllSentLabel && label != pmapi.DraftLabel && label != pmapi.AllDraftsLabel {
otherLabels = true
break
}
}
if otherLabels {
messageIDsToUnlabel = append(messageIDsToUnlabel, apiID)
} else {
messageIDsToDelete = append(messageIDsToDelete, apiID)
}
}
if len(messageIDsToUnlabel) > 0 {
if err := storeMailbox.client().UnlabelMessages(messageIDsToUnlabel, storeMailbox.labelID); err != nil {
log.WithError(err).Warning("Cannot unlabel before deleting")
}
}
if len(messageIDsToDelete) > 0 {
if err := storeMailbox.client().DeleteMessages(messageIDsToDelete); err != nil {
return err
}
if err := storeMailbox.deleteFromTrashOrSpam(apiIDs); err != nil {
return err
}
case pmapi.DraftLabel:
if err := storeMailbox.client().DeleteMessages(apiIDs); err != nil {
@ -231,6 +242,50 @@ func (storeMailbox *Mailbox) DeleteMessages(apiIDs []string) error {
return nil
}
// deleteFromTrashOrSpam will remove messages from API forever. If messages
// still has some custom label the message will not be deleted. Instead it will
// be removed from Trash or Spam.
func (storeMailbox *Mailbox) deleteFromTrashOrSpam(apiIDs []string) error {
l := storeMailbox.log.WithField("messages", apiIDs)
l.Trace("Deleting messages from trash")
messageIDsToDelete := []string{}
messageIDsToUnlabel := []string{}
for _, apiID := range apiIDs {
msg, err := storeMailbox.store.getMessageFromDB(apiID)
if err != nil {
return err
}
otherLabels := false
// If the message has any custom label, we don't want to delete it, only remove trash/spam label.
for _, label := range msg.LabelIDs {
if label != pmapi.SpamLabel && label != pmapi.TrashLabel && label != pmapi.AllMailLabel && label != pmapi.AllSentLabel && label != pmapi.DraftLabel && label != pmapi.AllDraftsLabel {
otherLabels = true
break
}
}
if otherLabels {
messageIDsToUnlabel = append(messageIDsToUnlabel, apiID)
} else {
messageIDsToDelete = append(messageIDsToDelete, apiID)
}
}
if len(messageIDsToUnlabel) > 0 {
if err := storeMailbox.client().UnlabelMessages(messageIDsToUnlabel, storeMailbox.labelID); err != nil {
l.WithError(err).Warning("Cannot unlabel before deleting")
}
}
if len(messageIDsToDelete) > 0 {
if err := storeMailbox.client().DeleteMessages(messageIDsToDelete); err != nil {
return err
}
}
return nil
}
func (storeMailbox *Mailbox) txSkipAndRemoveFromMailbox(tx *bolt.Tx, msg *pmapi.Message) (skipAndRemove bool) {
defer func() {
if skipAndRemove {
@ -270,7 +325,7 @@ 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 *bolt.Bucket
var apiBucket, imapBucket, deletedBucket *bolt.Bucket
for _, msg := range msgs {
if storeMailbox.txSkipAndRemoveFromMailbox(tx, msg) {
continue
@ -289,12 +344,15 @@ func (storeMailbox *Mailbox) txCreateOrUpdateMessages(tx *bolt.Tx, msgs []*pmapi
}
} else {
uidb := apiBucket.Get([]byte(msg.ID))
if uidb != nil {
if imapBucket == nil {
imapBucket = storeMailbox.txGetIMAPIDsBucket(tx)
}
seqNum, seqErr := storeMailbox.txGetSequenceNumberOfUID(imapBucket, uidb)
if deletedBucket == nil {
deletedBucket = storeMailbox.txGetDeletedIDsBucket(tx)
}
isMarkedAsDeleted := deletedBucket.Get([]byte(msg.ID)) != nil
if seqErr == nil {
storeMailbox.store.imapUpdateMessage(
storeMailbox.storeAddress.address,
@ -302,6 +360,7 @@ func (storeMailbox *Mailbox) txCreateOrUpdateMessages(tx *bolt.Tx, msgs []*pmapi
btoi(uidb),
seqNum,
msg,
isMarkedAsDeleted,
)
}
continue
@ -335,6 +394,7 @@ func (storeMailbox *Mailbox) txCreateOrUpdateMessages(tx *bolt.Tx, msgs []*pmapi
uid,
seqNum,
msg,
false, // new message is never marked as deleted
)
shouldSendMailboxUpdate = true
}
@ -359,6 +419,7 @@ func (storeMailbox *Mailbox) txDeleteMessage(tx *bolt.Tx, apiID string) error {
}
imapBucket := storeMailbox.txGetIMAPIDsBucket(tx)
deletedBucket := storeMailbox.txGetDeletedIDsBucket(tx)
seqNum, seqNumErr := storeMailbox.txGetSequenceNumberOfUID(imapBucket, uidb)
if seqNumErr != nil {
@ -373,6 +434,10 @@ func (storeMailbox *Mailbox) txDeleteMessage(tx *bolt.Tx, apiID string) error {
return errors.Wrap(err, "cannot delete from API bucket")
}
if err := deletedBucket.Delete(apiIDb); err != nil {
return errors.Wrap(err, "cannot delete from mark-as-deleted bucket")
}
if seqNumErr == nil {
storeMailbox.store.imapDeleteMessage(
storeMailbox.storeAddress.address,
@ -401,3 +466,50 @@ func (storeMailbox *Mailbox) txMailboxStatusUpdate(tx *bolt.Tx) error {
)
return nil
}
func (storeMailbox *Mailbox) txMarkMessagesAsDeleted(tx *bolt.Tx, apiIDs []string, markAsDeleted bool) error {
// Load all buckets before looping over apiIDs
metaBucket := tx.Bucket(metadataBucket)
apiBucket := storeMailbox.txGetAPIIDsBucket(tx)
uidBucket := storeMailbox.txGetIMAPIDsBucket(tx)
deletedBucket := storeMailbox.txGetDeletedIDsBucket(tx)
for _, apiID := range apiIDs {
if markAsDeleted {
if err := deletedBucket.Put([]byte(apiID), []byte{1}); err != nil {
return err
}
} else {
if err := deletedBucket.Delete([]byte(apiID)); err != nil {
return err
}
}
msg, err := storeMailbox.store.txGetMessageFromBucket(metaBucket, apiID)
if err != nil {
return err
}
uid, err := storeMailbox.txGetUIDFromBucket(apiBucket, apiID)
if err != nil {
return err
}
seqNum, err := storeMailbox.txGetSequenceNumberOfUID(uidBucket, itob(uid))
if err != nil {
return err
}
// In order to send flags in format
// S: * 2 FETCH (FLAGS (\Deleted \Seen))
storeMailbox.store.imapUpdateMessage(
storeMailbox.storeAddress.address,
storeMailbox.labelName,
uid,
seqNum,
msg,
markAsDeleted,
)
}
return nil
}

View File

@ -62,6 +62,21 @@ func (message *Message) Message() *pmapi.Message {
return message.msg
}
// IsMarkedDeleted returns true if message is marked as deleted for specific
// mailbox
func (message *Message) IsMarkedDeleted() bool {
isMarkedAsDeleted := false
err := message.storeMailbox.db().Update(func(tx *bolt.Tx) error {
isMarkedAsDeleted = message.storeMailbox.txGetDeletedIDsBucket(tx).Get([]byte(message.msg.ID)) != nil
return nil
})
if err != nil {
message.storeMailbox.log.WithError(err).Error("Not able to retrieve deleted mark, assuming false.")
return false
}
return isMarkedAsDeleted
}
// SetSize updates the information about size of decrypted message which can be
// used for IMAP. This should not trigger any IMAP update.
// NOTE: The size from the server corresponds to pure body bytes. Hence it

View File

@ -70,6 +70,8 @@ var (
// * {imapUID} -> string messageID
// * api_ids
// * {messageID} -> uint32 imapUID
// * deleted_ids (can be missing or have no keys)
// * {messageID} -> true
metadataBucket = []byte("metadata") //nolint[gochecknoglobals]
countsBucket = []byte("counts") //nolint[gochecknoglobals]
addressInfoBucket = []byte("address_info") //nolint[gochecknoglobals]
@ -78,6 +80,7 @@ var (
mailboxesBucket = []byte("mailboxes") //nolint[gochecknoglobals]
imapIDsBucket = []byte("imap_ids") //nolint[gochecknoglobals]
apiIDsBucket = []byte("api_ids") //nolint[gochecknoglobals]
deletedIDsBucket = []byte("deleted_ids") //nolint[gochecknoglobals]
mboxVersionBucket = []byte("mailboxes_version") //nolint[gochecknoglobals]
// ErrNoSuchAPIID when mailbox does not have API ID.
@ -348,6 +351,18 @@ func (store *Store) addAddress(address, addressID string, labels []*pmapi.Label)
return
}
// PauseEventLoop sets whether the ticker is periodically polling or not.
func (store *Store) PauseEventLoop(pause bool) {
store.lock.Lock()
defer store.lock.Unlock()
store.log.WithField("pause", pause).Info("Pausing event loop")
if store.eventLoop != nil {
store.eventLoop.isTickerPaused = pause
}
}
// Close stops the event loop and closes the database to free the file.
func (store *Store) Close() error {
store.lock.Lock()

View File

@ -26,6 +26,10 @@ import (
bolt "go.etcd.io/bbolt"
)
func (loop *eventLoop) IsRunning() bool {
return loop.isRunning
}
// TestSync triggers a sync of the store.
func (store *Store) TestSync() {
store.lock.Lock()
@ -102,11 +106,13 @@ func txDumpMailsFactory(tb assert.TestingT) func(tx *bolt.Tx) error {
err := mailboxes.ForEach(func(mboxName, mboxData []byte) error {
fmt.Println("mbox:", string(mboxName))
b := mailboxes.Bucket(mboxName).Bucket(imapIDsBucket)
deletedMailboxes := mailboxes.Bucket(mboxName).Bucket(deletedIDsBucket)
c := b.Cursor()
i := 0
for imapID, apiID := c.First(); imapID != nil; imapID, apiID = c.Next() {
i++
fmt.Println(" ", i, "imap", btoi(imapID), "api", string(apiID))
isDeleted := deletedMailboxes != nil && deletedMailboxes.Get(apiID) != nil
fmt.Println(" ", i, "imap", btoi(imapID), "api", string(apiID), "isDeleted", isDeleted)
data := metadata.Get(apiID)
if !assert.NotNil(tb, data) {
continue

View File

@ -210,7 +210,9 @@ func (store *Store) deleteMailboxEvent(labelID string) error {
store.lock.Lock()
defer store.lock.Unlock()
_ = store.removeMailboxCount(labelID)
if err := store.removeMailboxCount(labelID); err != nil {
log.WithError(err).Warn("Problem to remove mailbox counts while deleting mailbox")
}
for _, a := range store.addresses {
if err := a.deleteMailboxEvent(labelID); err != nil {

View File

@ -63,7 +63,7 @@ func (store *Store) CreateDraft(
attachmentReaders = append(attachmentReaders, strings.NewReader(attachedPublicKey))
publicKeyAttachment := &pmapi.Attachment{
Name: attachedPublicKeyName + ".asc",
MIMEType: "application/pgp-key",
MIMEType: "application/pgp-keys",
Header: textproto.MIMEHeader{},
}
attachments = append(attachments, publicKeyAttachment)
@ -143,8 +143,10 @@ func (store *Store) getMessageFromDB(apiID string) (msg *pmapi.Message, err erro
}
func (store *Store) txGetMessage(tx *bolt.Tx, apiID string) (*pmapi.Message, error) {
b := tx.Bucket(metadataBucket)
return store.txGetMessageFromBucket(tx.Bucket(metadataBucket), apiID)
}
func (store *Store) txGetMessageFromBucket(b *bolt.Bucket, apiID string) (*pmapi.Message, error) {
msgb := b.Get([]byte(apiID))
if msgb == nil {
return nil, ErrNoSuchAPIID

View File

@ -62,11 +62,20 @@ func (p *Progress) update() {
return
}
// In case no one listens for an update, do not block the progress.
select {
case p.updateCh <- struct{}{}:
case <-time.After(100 * time.Millisecond):
}
// In case no one listens for an update, do not block the whole progress.
go func() {
defer func() {
// updateCh can be closed at the end of progress which is fine.
if r := recover(); r != nil {
log.WithField("r", r).Warn("Failed to send update")
}
}()
select {
case p.updateCh <- struct{}{}:
case <-time.After(5 * time.Millisecond):
}
}()
}
// finish should be called as the last call once everything is done.

View File

@ -31,6 +31,8 @@ type IMAPProvider struct {
addr string
client *imapClient.Client
timeIt *timeIt
}
// NewIMAPProvider returns new IMAPProvider.
@ -39,6 +41,8 @@ func NewIMAPProvider(username, password, host, port string) (*IMAPProvider, erro
username: username,
password: password,
addr: net.JoinHostPort(host, port),
timeIt: newTimeIt("imap"),
}
if err := p.auth(); err != nil {

View File

@ -40,6 +40,9 @@ func (p *IMAPProvider) TransferTo(rules transferRules, progress *Progress, ch ch
log.Info("Started transfer from IMAP to channel")
defer log.Info("Finished transfer from IMAP to channel")
p.timeIt.clear()
defer p.timeIt.logResults()
imapMessageInfoMap := p.loadMessageInfoMap(rules, progress)
for rule := range rules.iterateActiveRules() {
@ -78,6 +81,9 @@ func (p *IMAPProvider) loadMessageInfoMap(rules transferRules, progress *Progres
}
func (p *IMAPProvider) loadMessagesInfo(rule *Rule, progress *Progress, uidValidity, count uint32) map[string]imapMessageInfo {
p.timeIt.start("load", rule.SourceMailbox.Name)
defer p.timeIt.stop("load", rule.SourceMailbox.Name)
messagesInfo := map[string]imapMessageInfo{}
pageStart := uint32(1)
@ -199,13 +205,18 @@ func (p *IMAPProvider) exportMessages(rule *Rule, progress *Progress, ch chan<-
progress.messageExported(id, body, err)
if err == nil {
msg := p.exportMessage(rule, id, imapMessage, body)
p.timeIt.stop("fetch", rule.SourceMailbox.Name)
ch <- msg
p.timeIt.start("fetch", rule.SourceMailbox.Name)
}
}
p.timeIt.start("fetch", rule.SourceMailbox.Name)
progress.callWrap(func() error {
return p.uidFetch(rule.SourceMailbox.Name, seqSet, items, processMessageCallback)
})
p.timeIt.stop("fetch", rule.SourceMailbox.Name)
}
func (p *IMAPProvider) exportMessage(rule *Rule, id string, imapMessage *imap.Message, body []byte) Message {

View File

@ -18,7 +18,9 @@
package transfer
import (
"crypto/tls"
"net"
"strings"
"time"
imapID "github.com/ProtonMail/go-imap-id"
@ -146,7 +148,19 @@ func (p *IMAPProvider) auth() error { //nolint[funlen]
if host == "127.0.0.1" {
client, err = imapClient.Dial(p.addr)
} else {
client, err = imapClient.DialTLS(p.addr, nil)
// IMAP.mail.yahoo.com have problem with golang TLS1.3
// implementation with weird behaviour i.e. Yahoo
// no error during dial or handshake but server logs out right
// after successful login leaving no time to perform any
// action. It was discovered that limiting to maximum TLS
// version 1.2 for yahoo servers is working solution.
var tlsConf *tls.Config
if strings.Contains(strings.ToLower(host), "yahoo") {
log.Warning("Yahoo server detected: limiting maximal TLS version to 1.2.")
tlsConf = &tls.Config{MaxVersion: tls.VersionTLS12}
}
client, err = imapClient.DialTLS(p.addr, tlsConf)
}
if err != nil {
return ErrIMAPConnection{imapError{Err: err, Message: "failed to connect to server"}}

View File

@ -99,7 +99,10 @@ func (p *MBOXProvider) updateCount(rule *Rule, progress *Progress, filePath stri
count := 0
for {
_, err := mboxReader.NextMessage()
if err != nil {
if err == io.EOF {
break
} else if err != nil {
progress.fatal(err)
break
}
count++

View File

@ -33,8 +33,10 @@ type PMAPIProvider struct {
addressID string
keyRing *crypto.KeyRing
importMsgReqMap map[string]*pmapi.ImportMsgReq // Key is msg transfer ID.
importMsgReqSize int
nextImportRequests map[string]*pmapi.ImportMsgReq // Key is msg transfer ID.
nextImportRequestsSize int
timeIt *timeIt
}
// NewPMAPIProvider returns new PMAPIProvider.
@ -45,8 +47,10 @@ func NewPMAPIProvider(config *pmapi.ClientConfig, clientManager ClientManager, u
userID: userID,
addressID: addressID,
importMsgReqMap: map[string]*pmapi.ImportMsgReq{},
importMsgReqSize: 0,
nextImportRequests: map[string]*pmapi.ImportMsgReq{},
nextImportRequestsSize: 0,
timeIt: newTimeIt("pmapi"),
}
if addressID != "" {

View File

@ -34,6 +34,9 @@ func (p *PMAPIProvider) TransferTo(rules transferRules, progress *Progress, ch c
log.Info("Started transfer from PMAPI to channel")
defer log.Info("Finished transfer from PMAPI to channel")
p.timeIt.clear()
defer p.timeIt.logResults()
// TransferTo cannot end sooner than loadCounts goroutine because
// loadCounts writes to channel in progress which would be closed.
// That can happen for really small accounts.
@ -147,6 +150,9 @@ func (p *PMAPIProvider) exportMessage(rule *Rule, progress *Progress, pmapiMsgID
return err
})
p.timeIt.start("build", msgID)
defer p.timeIt.stop("build", msgID)
msgBuilder := pkgMessage.NewBuilder(p.client(), msg)
msgBuilder.EncryptedToHTML = false
_, body, err := msgBuilder.BuildMessage()

View File

@ -22,6 +22,7 @@ import (
"fmt"
"io"
"io/ioutil"
"sync"
pkgMessage "github.com/ProtonMail/proton-bridge/pkg/message"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
@ -32,6 +33,7 @@ import (
const (
pmapiImportBatchMaxItems = 10
pmapiImportBatchMaxSize = 25 * 1000 * 1000 // 25 MB
pmapiImportWorkers = 4 // To keep memory under 1 GB.
)
// DefaultMailboxes returns the default mailboxes for default rules if no other is found.
@ -72,10 +74,16 @@ func (p *PMAPIProvider) TransferFrom(rules transferRules, progress *Progress, ch
log.Info("Started transfer from channel to PMAPI")
defer log.Info("Finished transfer from channel to PMAPI")
p.timeIt.clear()
defer p.timeIt.logResults()
// Cache has to be cleared before each transfer to not contain
// old stuff from previous cancelled run.
p.importMsgReqMap = map[string]*pmapi.ImportMsgReq{}
p.importMsgReqSize = 0
p.nextImportRequests = map[string]*pmapi.ImportMsgReq{}
p.nextImportRequestsSize = 0
preparedImportRequestsCh := make(chan map[string]*pmapi.ImportMsgReq)
wg := p.startImportWorkers(progress, preparedImportRequestsCh)
for msg := range ch {
if progress.shouldStop() {
@ -85,13 +93,15 @@ func (p *PMAPIProvider) TransferFrom(rules transferRules, progress *Progress, ch
if p.isMessageDraft(msg) {
p.transferDraft(rules, progress, msg)
} else {
p.transferMessage(rules, progress, msg)
p.transferMessage(rules, progress, msg, preparedImportRequestsCh)
}
}
if len(p.importMsgReqMap) > 0 {
p.importMessages(progress)
if len(p.nextImportRequests) > 0 {
preparedImportRequestsCh <- p.nextImportRequests
}
close(preparedImportRequestsCh)
wg.Wait()
}
func (p *PMAPIProvider) isMessageDraft(msg Message) bool {
@ -114,7 +124,10 @@ func (p *PMAPIProvider) importDraft(msg Message, globalMailbox *Mailbox) (string
return "", errors.Wrap(err, "failed to parse message")
}
if err := message.Encrypt(p.keyRing, nil); err != nil {
p.timeIt.start("encrypt", msg.ID)
err = message.Encrypt(p.keyRing, nil)
p.timeIt.stop("encrypt", msg.ID)
if err != nil {
return "", errors.Wrap(err, "failed to encrypt draft")
}
@ -125,7 +138,7 @@ func (p *PMAPIProvider) importDraft(msg Message, globalMailbox *Mailbox) (string
attachments := message.Attachments
message.Attachments = nil
draft, err := p.createDraft(message, "", pmapi.DraftActionReply)
draft, err := p.createDraft(msg.ID, message, "", pmapi.DraftActionReply)
if err != nil {
return "", errors.Wrap(err, "failed to create draft")
}
@ -140,13 +153,15 @@ func (p *PMAPIProvider) importDraft(msg Message, globalMailbox *Mailbox) (string
return "", errors.Wrap(err, "failed to sign attachment")
}
p.timeIt.start("encrypt", msg.ID)
r = bytes.NewReader(attachmentBody)
encReader, err := attachment.Encrypt(p.keyRing, r)
p.timeIt.stop("encrypt", msg.ID)
if err != nil {
return "", errors.Wrap(err, "failed to encrypt attachment")
}
_, err = p.createAttachment(attachment, encReader, sigReader)
_, err = p.createAttachment(msg.ID, attachment, encReader, sigReader)
if err != nil {
return "", errors.Wrap(err, "failed to create attachment")
}
@ -155,7 +170,7 @@ func (p *PMAPIProvider) importDraft(msg Message, globalMailbox *Mailbox) (string
return draft.ID, nil
}
func (p *PMAPIProvider) transferMessage(rules transferRules, progress *Progress, msg Message) {
func (p *PMAPIProvider) transferMessage(rules transferRules, progress *Progress, msg Message, preparedImportRequestsCh chan map[string]*pmapi.ImportMsgReq) {
importMsgReq, err := p.generateImportMsgReq(msg, rules.globalMailbox)
if err != nil {
progress.messageImported(msg.ID, "", err)
@ -163,11 +178,13 @@ func (p *PMAPIProvider) transferMessage(rules transferRules, progress *Progress,
}
importMsgReqSize := len(importMsgReq.Body)
if p.importMsgReqSize+importMsgReqSize > pmapiImportBatchMaxSize || len(p.importMsgReqMap) == pmapiImportBatchMaxItems {
p.importMessages(progress)
if p.nextImportRequestsSize+importMsgReqSize > pmapiImportBatchMaxSize || len(p.nextImportRequests) == pmapiImportBatchMaxItems {
preparedImportRequestsCh <- p.nextImportRequests
p.nextImportRequests = map[string]*pmapi.ImportMsgReq{}
p.nextImportRequestsSize = 0
}
p.importMsgReqMap[msg.ID] = importMsgReq
p.importMsgReqSize += importMsgReqSize
p.nextImportRequests[msg.ID] = importMsgReq
p.nextImportRequestsSize += importMsgReqSize
}
func (p *PMAPIProvider) generateImportMsgReq(msg Message, globalMailbox *Mailbox) (*pmapi.ImportMsgReq, error) {
@ -176,7 +193,9 @@ func (p *PMAPIProvider) generateImportMsgReq(msg Message, globalMailbox *Mailbox
return nil, errors.Wrap(err, "failed to parse message")
}
p.timeIt.start("encrypt", msg.ID)
body, err := p.encryptMessage(message, attachmentReaders)
p.timeIt.stop("encrypt", msg.ID)
if err != nil {
return nil, errors.Wrap(err, "failed to encrypt message")
}
@ -208,6 +227,9 @@ func (p *PMAPIProvider) generateImportMsgReq(msg Message, globalMailbox *Mailbox
}
func (p *PMAPIProvider) parseMessage(msg Message) (m *pmapi.Message, r []io.Reader, err error) {
p.timeIt.start("parse", msg.ID)
defer p.timeIt.stop("parse", msg.ID)
// Old message parser is panicking in some cases.
// Instead of crashing we try to convert to regular error.
defer func() {
@ -254,26 +276,39 @@ func computeMessageFlags(labels []string) (flag int64) {
return flag
}
func (p *PMAPIProvider) importMessages(progress *Progress) {
func (p *PMAPIProvider) startImportWorkers(progress *Progress, preparedImportRequestsCh chan map[string]*pmapi.ImportMsgReq) *sync.WaitGroup {
var wg sync.WaitGroup
wg.Add(pmapiImportWorkers)
for i := 0; i < pmapiImportWorkers; i++ {
go func() {
for importRequests := range preparedImportRequestsCh {
p.importMessages(progress, importRequests)
}
wg.Done()
}()
}
return &wg
}
func (p *PMAPIProvider) importMessages(progress *Progress, importRequests map[string]*pmapi.ImportMsgReq) {
if progress.shouldStop() {
return
}
importMsgIDs := []string{}
importMsgRequests := []*pmapi.ImportMsgReq{}
for msgID, req := range p.importMsgReqMap {
for msgID, req := range importRequests {
importMsgIDs = append(importMsgIDs, msgID)
importMsgRequests = append(importMsgRequests, req)
}
log.WithField("msgIDs", importMsgIDs).WithField("size", p.importMsgReqSize).Debug("Importing messages")
results, err := p.importRequest(importMsgRequests)
log.WithField("msgIDs", importMsgIDs).Trace("Importing messages")
results, err := p.importRequest(importMsgIDs[0], importMsgRequests)
// In case the whole request failed, try to import every message one by one.
if err != nil || len(results) == 0 {
log.WithError(err).Warning("Importing messages failed, trying one by one")
for msgID, req := range p.importMsgReqMap {
importedID, err := p.importMessage(progress, req)
for msgID, req := range importRequests {
importedID, err := p.importMessage(msgID, progress, req)
progress.messageImported(msgID, importedID, err)
}
return
@ -285,20 +320,17 @@ func (p *PMAPIProvider) importMessages(progress *Progress) {
if result.Error != nil {
log.WithError(result.Error).WithField("msg", msgID).Warning("Importing message failed, trying alone")
req := importMsgRequests[index]
importedID, err := p.importMessage(progress, req)
importedID, err := p.importMessage(msgID, progress, req)
progress.messageImported(msgID, importedID, err)
} else {
progress.messageImported(msgID, result.MessageID, nil)
}
}
p.importMsgReqMap = map[string]*pmapi.ImportMsgReq{}
p.importMsgReqSize = 0
}
func (p *PMAPIProvider) importMessage(progress *Progress, req *pmapi.ImportMsgReq) (importedID string, importedErr error) {
func (p *PMAPIProvider) importMessage(msgSourceID string, progress *Progress, req *pmapi.ImportMsgReq) (importedID string, importedErr error) {
progress.callWrap(func() error {
results, err := p.importRequest([]*pmapi.ImportMsgReq{req})
results, err := p.importRequest(msgSourceID, []*pmapi.ImportMsgReq{req})
if err != nil {
return errors.Wrap(err, "failed to import messages")
}

View File

@ -18,6 +18,7 @@
package transfer
import (
"fmt"
"io"
"time"
@ -71,6 +72,10 @@ func (p *PMAPIProvider) tryReconnect() error {
func (p *PMAPIProvider) listMessages(filter *pmapi.MessagesFilter) (messages []*pmapi.Message, count int, err error) {
err = p.ensureConnection(func() error {
key := fmt.Sprintf("%s_%d", filter.LabelID, filter.Page)
p.timeIt.start("listing", key)
defer p.timeIt.stop("listing", key)
messages, count, err = p.client().ListMessages(filter)
return err
})
@ -79,30 +84,42 @@ func (p *PMAPIProvider) listMessages(filter *pmapi.MessagesFilter) (messages []*
func (p *PMAPIProvider) getMessage(msgID string) (message *pmapi.Message, err error) {
err = p.ensureConnection(func() error {
p.timeIt.start("download", msgID)
defer p.timeIt.stop("download", msgID)
message, err = p.client().GetMessage(msgID)
return err
})
return
}
func (p *PMAPIProvider) importRequest(req []*pmapi.ImportMsgReq) (res []*pmapi.ImportMsgRes, err error) {
func (p *PMAPIProvider) importRequest(msgSourceID string, req []*pmapi.ImportMsgReq) (res []*pmapi.ImportMsgRes, err error) {
err = p.ensureConnection(func() error {
p.timeIt.start("upload", msgSourceID)
defer p.timeIt.stop("upload", msgSourceID)
res, err = p.client().Import(req)
return err
})
return
}
func (p *PMAPIProvider) createDraft(message *pmapi.Message, parent string, action int) (draft *pmapi.Message, err error) {
func (p *PMAPIProvider) createDraft(msgSourceID string, message *pmapi.Message, parent string, action int) (draft *pmapi.Message, err error) {
err = p.ensureConnection(func() error {
p.timeIt.start("upload", msgSourceID)
defer p.timeIt.stop("upload", msgSourceID)
draft, err = p.client().CreateDraft(message, parent, action)
return err
})
return
}
func (p *PMAPIProvider) createAttachment(att *pmapi.Attachment, r io.Reader, sig io.Reader) (created *pmapi.Attachment, err error) {
func (p *PMAPIProvider) createAttachment(msgSourceID string, att *pmapi.Attachment, r io.Reader, sig io.Reader) (created *pmapi.Attachment, err error) {
err = p.ensureConnection(func() error {
p.timeIt.start("upload", msgSourceID)
defer p.timeIt.stop("upload", msgSourceID)
created, err = p.client().CreateAttachment(att, r, sig)
return err
})

View File

@ -0,0 +1,80 @@
// Copyright (c) 2020 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 transfer
import (
"sync"
"time"
)
type timeIt struct {
lock sync.Locker
name string
groups map[string]int64
ongoing map[string]time.Time
}
func newTimeIt(name string) *timeIt {
return &timeIt{
lock: &sync.Mutex{},
name: name,
groups: map[string]int64{},
ongoing: map[string]time.Time{},
}
}
func (t *timeIt) clear() {
t.lock.Lock()
defer t.lock.Unlock()
t.groups = map[string]int64{}
t.ongoing = map[string]time.Time{}
}
func (t *timeIt) start(group, id string) {
t.lock.Lock()
defer t.lock.Unlock()
t.ongoing[group+"/"+id] = time.Now()
}
func (t *timeIt) stop(group, id string) {
endTime := time.Now()
t.lock.Lock()
defer t.lock.Unlock()
startTime, ok := t.ongoing[group+"/"+id]
if !ok {
log.WithField("group", group).WithField("id", id).Error("Stop called before start")
return
}
delete(t.ongoing, group+"/"+id)
diff := endTime.Sub(startTime).Milliseconds()
t.groups[group] += diff
}
func (t *timeIt) logResults() {
t.lock.Lock()
defer t.lock.Unlock()
// Print also ongoing to be sure that nothing was left out.
// Basically ongoing should be empty.
log.WithField("name", t.name).WithField("result", t.groups).WithField("ongoing", t.ongoing).Debug("Time measurement")
}

View File

@ -181,7 +181,10 @@ func (t *Transfer) Start() *Progress {
reportFile := newFileReport(t.logDir, t.id)
progress := newProgress(log, reportFile)
ch := make(chan Message)
// Small queue to prevent having idle source while target is blocked.
// E.g., when upload to PM is in progress, we can in meantime download
// the next batch from remote IMAP server.
ch := make(chan Message, 10)
go func() {
defer t.panicHandler.HandlePanic()

View File

@ -21,6 +21,7 @@ import (
"mime"
"net/mail"
"net/textproto"
"regexp"
"strings"
"time"
@ -141,74 +142,41 @@ func GetAttachmentHeader(att *pmapi.Attachment) textproto.MIMEHeader {
return h
}
// ========= Header parsing and sanitizing functions =========
var reEmailComment = regexp.MustCompile("[(][^)]*[)]") // nolint[gochecknoglobals]
func parseHeader(h mail.Header) (m *pmapi.Message, err error) { //nolint[unparam]
m = pmapi.NewMessage()
if subject, err := pmmime.DecodeHeader(h.Get("Subject")); err == nil {
m.Subject = subject
}
if addrs, err := sanitizeAddressList(h, "From"); err == nil && len(addrs) > 0 {
m.Sender = addrs[0]
}
if addrs, err := sanitizeAddressList(h, "Reply-To"); err == nil && len(addrs) > 0 {
m.ReplyTos = addrs
}
if addrs, err := sanitizeAddressList(h, "To"); err == nil {
m.ToList = addrs
}
if addrs, err := sanitizeAddressList(h, "Cc"); err == nil {
m.CCList = addrs
}
if addrs, err := sanitizeAddressList(h, "Bcc"); err == nil {
m.BCCList = addrs
}
m.Time = 0
if t, err := h.Date(); err == nil && !t.IsZero() {
m.Time = t.Unix()
}
m.Header = h
return
// parseAddressComment removes the comments completely even though they should be allowed
// http://tools.wordtothewise.com/rfc/822
// NOTE: This should be supported in go>1.10 but it seems it's not ¯\_(ツ)_/¯
func parseAddressComment(raw string) string {
return reEmailComment.ReplaceAllString(raw, "")
}
func sanitizeAddressList(h mail.Header, field string) (addrs []*mail.Address, err error) {
raw := h.Get(field)
if raw == "" {
err = mail.ErrHeaderNotPresent
return
}
var decoded string
decoded, err = pmmime.DecodeHeader(raw)
if err != nil {
return
}
addrs, err = mail.ParseAddressList(parseAddressComment(decoded))
func parseAddressList(val string) (addrs []*mail.Address, err error) {
addrs, err = mail.ParseAddressList(parseAddressComment(val))
if err == nil {
if addrs == nil {
addrs = []*mail.Address{}
}
return
}
// Probably missing encoding error -- try to at least parse addresses in brackets.
addrStr := h.Get(field)
first := strings.Index(addrStr, "<")
last := strings.LastIndex(addrStr, ">")
first := strings.Index(val, "<")
last := strings.LastIndex(val, ">")
if first < 0 || last < 0 || first >= last {
return
}
var addrList []string
open := first
for open < last && 0 <= open {
addrStr = addrStr[open:]
close := strings.Index(addrStr, ">")
addrList = append(addrList, addrStr[:close+1])
addrStr = addrStr[close:]
open = strings.Index(addrStr, "<")
last = strings.LastIndex(addrStr, ">")
val = val[open:]
close := strings.Index(val, ">")
addrList = append(addrList, val[:close+1])
val = val[close:]
open = strings.Index(val, "<")
last = strings.LastIndex(val, ">")
}
addrStr = strings.Join(addrList, ", ")
//
return mail.ParseAddressList(addrStr)
val = strings.Join(addrList, ", ")
return mail.ParseAddressList(val)
}

View File

@ -1,76 +0,0 @@
// Copyright (c) 2020 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"
"errors"
escape "html"
"strings"
"github.com/andybalholm/cascadia"
"golang.org/x/net/html"
)
func plaintextToHTML(text string) (output string) {
text = escape.EscapeString(text)
text = strings.Replace(text, "\n\r", "<br>", -1)
text = strings.Replace(text, "\r\n", "<br>", -1)
text = strings.Replace(text, "\n", "<br>", -1)
text = strings.Replace(text, "\r", "<br>", -1)
return "<div>" + text + "</div>"
}
func stripHTML(input string) (stripped string, err error) {
reader := strings.NewReader(input)
doc, _ := html.Parse(reader)
body := cascadia.MustCompile("body").MatchFirst(doc)
if body == nil {
err = errors.New("failed to find necessary html element")
return
}
var buf1 bytes.Buffer
if err = html.Render(&buf1, body); err != nil {
stripped = input
return
}
stripped = buf1.String()
// Handle double body tags edge case.
if strings.Index(stripped, "<body") == 0 {
startIndex := strings.Index(stripped, ">")
if startIndex < 5 {
return
}
stripped = stripped[startIndex+1:]
// Closing body tag is optional.
closingIndex := strings.Index(stripped, "</body>")
if closingIndex > -1 {
stripped = stripped[:closingIndex]
}
}
return
}
func addOuterHTMLTags(input string) (output string) {
return "<html><head></head><body>" + input + "</body></html>"
}
func makeEmbeddedImageHTML(cid, name string) (output string) {
return "<img class=\"proton-embedded\" alt=\"" + name + "\" src=\"cid:" + cid + "\">"
}

View File

@ -19,455 +19,497 @@ package message
import (
"bytes"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"fmt"
"io"
"io/ioutil"
"math/rand"
"mime"
"mime/quotedprintable"
"net/mail"
"net/textproto"
"regexp"
"strconv"
"strings"
"github.com/ProtonMail/proton-bridge/pkg/message/parser"
pmmime "github.com/ProtonMail/proton-bridge/pkg/mime"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/emersion/go-message"
"github.com/jaytaylor/html2text"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
func parseAttachment(filename string, mediaType string, h textproto.MIMEHeader) (att *pmapi.Attachment) {
if decoded, err := pmmime.DecodeHeader(filename); err == nil {
filename = decoded
}
if filename == "" {
ext, err := mime.ExtensionsByType(mediaType)
if err == nil && len(ext) > 0 {
filename = "attachment" + ext[0]
}
func Parse(r io.Reader, key, keyName string) (m *pmapi.Message, mimeBody, plainBody string, attReaders []io.Reader, err error) {
logrus.Trace("Parsing message")
p, err := parser.New(r)
if err != nil {
err = errors.Wrap(err, "failed to create new parser")
return
}
att = &pmapi.Attachment{
Name: filename,
MIMEType: mediaType,
Header: h,
if err = convertForeignEncodings(p); err != nil {
err = errors.Wrap(err, "failed to convert foreign encodings")
return
}
headerContentID := strings.Trim(h.Get("Content-Id"), " <>")
m = pmapi.NewMessage()
if headerContentID != "" {
att.ContentID = headerContentID
if err = parseMessageHeader(m, p.Root().Header); err != nil {
err = errors.Wrap(err, "failed to parse message header")
return
}
return
if m.Attachments, attReaders, err = collectAttachments(p); err != nil {
err = errors.Wrap(err, "failed to collect attachments")
return
}
if m.Body, plainBody, err = buildBodies(p); err != nil {
err = errors.Wrap(err, "failed to build bodies")
return
}
if m.MIMEType, err = determineMIMEType(p); err != nil {
err = errors.Wrap(err, "failed to determine mime type")
return
}
// We only attach the public key manually to the MIME body for
// signed/encrypted external recipients. It's not important for it to be
// collected as an attachment; that's already done when we upload the draft.
if key != "" {
attachPublicKey(p.Root(), key, keyName)
}
mimeBodyBuffer := new(bytes.Buffer)
if err = p.NewWriter().Write(mimeBodyBuffer); err != nil {
err = errors.Wrap(err, "failed to write out mime message")
return
}
return m, mimeBodyBuffer.String(), plainBody, attReaders, nil
}
var reEmailComment = regexp.MustCompile("[(][^)]*[)]") //nolint[gochecknoglobals]
func convertForeignEncodings(p *parser.Parser) error {
logrus.Trace("Converting foreign encodings")
// parseAddressComment removes the comments completely even though they should be allowed
// http://tools.wordtothewise.com/rfc/822
// NOTE: This should be supported in go>1.10 but it seems it's not ¯\_(ツ)_/¯
func parseAddressComment(raw string) string {
return reEmailComment.ReplaceAllString(raw, "")
}
// Some clients incorrectly format messages with embedded attachments to have a format like
// I. text/plain II. attachment III. text/plain
// which we need to convert to a single HTML part with an embedded attachment.
func combineParts(m *pmapi.Message, parts []io.Reader, headers []textproto.MIMEHeader, convertPlainToHTML bool, atts *[]io.Reader) (isHTML bool, err error) { //nolint[funlen]
isHTML = true
foundText := false
for i := len(parts) - 1; i >= 0; i-- {
part := parts[i]
h := headers[i]
disp, dispParams, _ := pmmime.ParseMediaType(h.Get("Content-Disposition"))
d := pmmime.DecodeContentEncoding(part, h.Get("Content-Transfer-Encoding"))
if d == nil {
log.Warnf("Unsupported Content-Transfer-Encoding '%v'", h.Get("Content-Transfer-Encoding"))
d = part
}
contentType := h.Get("Content-Type")
if contentType == "" {
contentType = "text/plain"
}
mediaType, params, _ := pmmime.ParseMediaType(contentType)
if strings.HasPrefix(mediaType, "text/") && mediaType != "text/calendar" && disp != "attachment" {
// This is text.
var b []byte
if b, err = ioutil.ReadAll(d); err != nil {
continue
return p.NewWalker().
RegisterContentTypeHandler("text/html", func(p *parser.Part) error {
if err := p.ConvertToUTF8(); err != nil {
return err
}
b, err = pmmime.DecodeCharset(b, contentType)
return p.ConvertMetaCharset()
}).
RegisterContentTypeHandler("text/.*", func(p *parser.Part) error {
return p.ConvertToUTF8()
}).
RegisterDefaultHandler(func(p *parser.Part) error {
t, _, _ := p.Header.ContentType()
logrus.WithField("type", t).Trace("Not converting part to utf-8")
return nil
}).
Walk()
}
func collectAttachments(p *parser.Parser) ([]*pmapi.Attachment, []io.Reader, error) {
var (
atts []*pmapi.Attachment
data []io.Reader
err error
)
w := p.NewWalker().
RegisterContentDispositionHandler("attachment", func(p *parser.Part) error {
att, err := parseAttachment(p.Header)
if err != nil {
log.Warn("Decode charset error: ", err)
return false, err
}
contents := string(b)
if strings.Contains(mediaType, "text/plain") && len(contents) > 0 {
if !convertPlainToHTML {
isHTML = false
} else {
contents = plaintextToHTML(contents)
}
} else if strings.Contains(mediaType, "text/html") && len(contents) > 0 {
contents, err = stripHTML(contents)
if err != nil {
return isHTML, err
}
}
m.Body = contents + m.Body
foundText = true
} else {
// This is an attachment.
filename := dispParams["filename"]
if filename == "" {
// Using "name" in Content-Type is discouraged.
filename = params["name"]
}
if filename == "" && mediaType == "text/calendar" {
filename = "event.ics"
return err
}
att := parseAttachment(filename, mediaType, h)
atts = append(atts, att)
data = append(data, bytes.NewReader(p.Body))
b := &bytes.Buffer{}
if d == nil {
continue
}
if _, err = io.Copy(b, d); err != nil {
continue
}
if foundText && att.ContentID == "" && strings.Contains(mediaType, "image") {
// Treat this as an inline attachment even though it is not marked as one.
hasher := sha256.New()
_, _ = hasher.Write([]byte(att.Name + strconv.Itoa(b.Len())))
bytes := hasher.Sum(nil)
cid := hex.EncodeToString(bytes) + "@protonmail.com"
att.ContentID = cid
embeddedHTML := makeEmbeddedImageHTML(cid, att.Name)
m.Body = embeddedHTML + m.Body
return nil
}).
RegisterContentTypeHandler("text/calendar", func(p *parser.Part) error {
att, err := parseAttachment(p.Header)
if err != nil {
return err
}
m.Attachments = append(m.Attachments, att)
*atts = append(*atts, b)
}
}
if isHTML {
m.Body = addOuterHTMLTags(m.Body)
}
return isHTML, nil
}
atts = append(atts, att)
data = append(data, bytes.NewReader(p.Body))
func checkHeaders(headers []textproto.MIMEHeader) bool {
foundAttachment := false
return nil
}).
RegisterContentTypeHandler("text/.*", func(p *parser.Part) error {
return nil
}).
RegisterDefaultHandler(func(p *parser.Part) error {
if len(p.Children()) > 0 {
return nil
}
for i := 0; i < len(headers); i++ {
h := headers[i]
att, err := parseAttachment(p.Header)
if err != nil {
return err
}
mediaType, _, _ := pmmime.ParseMediaType(h.Get("Content-Type"))
atts = append(atts, att)
data = append(data, bytes.NewReader(p.Body))
if !strings.HasPrefix(mediaType, "text/") {
foundAttachment = true
} else if foundAttachment {
// This means that there is a text part after the first attachment,
// so we will have to convert the body from plain->HTML.
return true
}
}
return false
}
return nil
})
// ============================== 7bit Filter ==========================
// For every MIME part in the tree that has "8bit" or "binary" content
// transfer encoding: transcode it to "quoted-printable".
type SevenBitFilter struct {
target pmmime.VisitAcceptor
}
func NewSevenBitFilter(targetAccepter pmmime.VisitAcceptor) *SevenBitFilter {
return &SevenBitFilter{
target: targetAccepter,
}
}
func decodePart(partReader io.Reader, header textproto.MIMEHeader) (decodedPart io.Reader) {
decodedPart = pmmime.DecodeContentEncoding(partReader, header.Get("Content-Transfer-Encoding"))
if decodedPart == nil {
log.Warnf("Unsupported Content-Transfer-Encoding '%v'", header.Get("Content-Transfer-Encoding"))
decodedPart = partReader
}
return
}
func (sd SevenBitFilter) Accept(partReader io.Reader, header textproto.MIMEHeader, hasPlainSibling bool, isFirst, isLast bool) error {
cte := strings.ToLower(header.Get("Content-Transfer-Encoding"))
if isFirst && pmmime.IsLeaf(header) && cte != "quoted-printable" && cte != "base64" && cte != "7bit" {
decodedPart := decodePart(partReader, header)
filteredHeader := textproto.MIMEHeader{}
for k, v := range header {
filteredHeader[k] = v
}
filteredHeader.Set("Content-Transfer-Encoding", "quoted-printable")
filteredBuffer := &bytes.Buffer{}
decodedSlice, _ := ioutil.ReadAll(decodedPart)
w := quotedprintable.NewWriter(filteredBuffer)
if _, err := w.Write(decodedSlice); err != nil {
log.Errorf("cannot write quotedprintable from %q: %v", cte, err)
}
if err := w.Close(); err != nil {
log.Errorf("cannot close quotedprintable from %q: %v", cte, err)
}
_ = sd.target.Accept(filteredBuffer, filteredHeader, hasPlainSibling, true, isLast)
} else {
_ = sd.target.Accept(partReader, header, hasPlainSibling, isFirst, isLast)
}
return nil
}
// =================== HTML Only convertor ==================================
// In any part of MIME tree structure, replace standalone text/html with
// multipart/alternative containing both text/html and text/plain.
type HTMLOnlyConvertor struct {
target pmmime.VisitAcceptor
}
func NewHTMLOnlyConvertor(targetAccepter pmmime.VisitAcceptor) *HTMLOnlyConvertor {
return &HTMLOnlyConvertor{
target: targetAccepter,
}
}
func randomBoundary() string {
buf := make([]byte, 30)
// We specifically use `math/rand` here to allow the generator to be seeded for test purposes.
// The random numbers need not be cryptographically secure; we are simply generating random part boundaries.
if _, err := rand.Read(buf); err != nil { // nolint[gosec]
panic(err)
if err = w.Walk(); err != nil {
return nil, nil, err
}
return fmt.Sprintf("%x", buf)
return atts, data, nil
}
func (hoc HTMLOnlyConvertor) Accept(partReader io.Reader, header textproto.MIMEHeader, hasPlainSiblings bool, isFirst, isLast bool) error {
mediaType, _, err := pmmime.ParseMediaType(header.Get("Content-Type"))
if isFirst && err == nil && mediaType == "text/html" && !hasPlainSiblings {
multiPartHeaders := make(textproto.MIMEHeader)
for k, v := range header {
multiPartHeaders[k] = v
}
boundary := randomBoundary()
multiPartHeaders.Set("Content-Type", "multipart/alternative; boundary=\""+boundary+"\"")
childCte := header.Get("Content-Transfer-Encoding")
// buildBodies collects all text/html and text/plain parts and returns two bodies,
// - a rich text body (in which html is allowed), and
// - a plaintext body (in which html is converted to plaintext).
//
// text/html parts are converted to plaintext in order to build the plaintext body,
// unless there is already a plaintext part provided via multipart/alternative,
// in which case the provided alternative is chosen.
func buildBodies(p *parser.Parser) (richBody, plainBody string, err error) {
richParts, err := collectBodyParts(p, "text/html")
if err != nil {
return
}
_ = hoc.target.Accept(partReader, multiPartHeaders, false, true, false)
plainParts, err := collectBodyParts(p, "text/plain")
if err != nil {
return
}
partData, _ := ioutil.ReadAll(partReader)
richBuilder, plainBuilder := strings.Builder{}, strings.Builder{}
htmlChildHeaders := make(textproto.MIMEHeader)
htmlChildHeaders.Set("Content-Transfer-Encoding", childCte)
htmlChildHeaders.Set("Content-Type", "text/html")
htmlReader := bytes.NewReader(partData)
_ = hoc.target.Accept(htmlReader, htmlChildHeaders, false, true, false)
for _, richPart := range richParts {
_, _ = richBuilder.Write(richPart.Body)
}
_ = hoc.target.Accept(partReader, multiPartHeaders, hasPlainSiblings, false, false)
for _, plainPart := range plainParts {
_, _ = plainBuilder.Write(getPlainBody(plainPart))
}
plainChildHeaders := make(textproto.MIMEHeader)
plainChildHeaders.Set("Content-Transfer-Encoding", childCte)
plainChildHeaders.Set("Content-Type", "text/plain")
unHtmlized, err := html2text.FromReader(bytes.NewReader(partData))
return richBuilder.String(), plainBuilder.String(), nil
}
// collectBodyParts collects all body parts in the parse tree, preferring
// parts of the given content type if alternatives exist.
func collectBodyParts(p *parser.Parser, preferredContentType string) (parser.Parts, error) {
v := p.
NewVisitor(func(p *parser.Part, visit parser.Visit) (interface{}, error) {
childParts, err := collectChildParts(p, visit)
if err != nil {
return nil, err
}
return joinChildParts(childParts), nil
}).
RegisterRule("multipart/alternative", func(p *parser.Part, visit parser.Visit) (interface{}, error) {
childParts, err := collectChildParts(p, visit)
if err != nil {
return nil, err
}
return bestChoice(childParts, preferredContentType), nil
}).
RegisterRule("text/plain", func(p *parser.Part, visit parser.Visit) (interface{}, error) {
disp, _, err := p.Header.ContentDisposition()
if err != nil {
disp = ""
}
if disp == "attachment" {
return parser.Parts{}, nil
}
return parser.Parts{p}, nil
}).
RegisterRule("text/html", func(p *parser.Part, visit parser.Visit) (interface{}, error) {
disp, _, err := p.Header.ContentDisposition()
if err != nil {
disp = ""
}
if disp == "attachment" {
return parser.Parts{}, nil
}
return parser.Parts{p}, nil
})
res, err := v.Visit()
if err != nil {
return nil, err
}
return res.(parser.Parts), nil
}
func collectChildParts(p *parser.Part, visit parser.Visit) ([]parser.Parts, error) {
childParts := []parser.Parts{}
for _, child := range p.Children() {
res, err := visit(child)
if err != nil {
unHtmlized = string(partData)
return nil, err
}
plainReader := strings.NewReader(unHtmlized)
_ = hoc.target.Accept(plainReader, plainChildHeaders, false, true, true)
_ = hoc.target.Accept(partReader, multiPartHeaders, hasPlainSiblings, false, true)
} else {
_ = hoc.target.Accept(partReader, header, hasPlainSiblings, isFirst, isLast)
childParts = append(childParts, res.(parser.Parts))
}
return nil
return childParts, nil
}
// ======= Public Key Attacher ========
func joinChildParts(childParts []parser.Parts) parser.Parts {
res := parser.Parts{}
type PublicKeyAttacher struct {
target pmmime.VisitAcceptor
attachedPublicKey string
attachedPublicKeyName string
appendToMultipart bool
depth int
for _, parts := range childParts {
res = append(res, parts...)
}
return res
}
func NewPublicKeyAttacher(targetAccepter pmmime.VisitAcceptor, attachedPublicKey, attachedPublicKeyName string) *PublicKeyAttacher {
return &PublicKeyAttacher{
target: targetAccepter,
attachedPublicKey: attachedPublicKey,
attachedPublicKeyName: attachedPublicKeyName,
appendToMultipart: false,
depth: 0,
}
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
func split(input string, sliceLength int) string {
processed := input
result := ""
for len(processed) > 0 {
cutPoint := min(sliceLength, len(processed))
part := processed[0:cutPoint]
result = result + part + "\n"
processed = processed[cutPoint:]
}
return result
}
func createKeyAttachment(publicKey, publicKeyName string) (headers textproto.MIMEHeader, contents io.Reader) {
attachmentHeaders := make(textproto.MIMEHeader)
attachmentHeaders.Set("Content-Type", "application/pgp-key; name=\""+publicKeyName+"\"")
attachmentHeaders.Set("Content-Transfer-Encoding", "base64")
attachmentHeaders.Set("Content-Disposition", "attachment; filename=\""+publicKeyName+".asc.pgp\"")
buffer := &bytes.Buffer{}
w := base64.NewEncoder(base64.StdEncoding, buffer)
_, _ = w.Write([]byte(publicKey))
_ = w.Close()
return attachmentHeaders, strings.NewReader(split(buffer.String(), 73))
}
func (pka *PublicKeyAttacher) Accept(partReader io.Reader, header textproto.MIMEHeader, hasPlainSiblings bool, isFirst, isLast bool) error {
if isFirst && !pmmime.IsLeaf(header) {
pka.depth++
}
if isLast && !pmmime.IsLeaf(header) {
defer func() {
pka.depth--
}()
}
isRoot := (header.Get("From") != "")
// NOTE: This should also work for unspecified Content-Type (in which case us-ascii text/plain is assumed)!
mediaType, _, err := pmmime.ParseMediaType(header.Get("Content-Type"))
if isRoot && isFirst && err == nil && pka.attachedPublicKey != "" { //nolint[gocritic]
if strings.HasPrefix(mediaType, "multipart/mixed") {
pka.appendToMultipart = true
_ = pka.target.Accept(partReader, header, hasPlainSiblings, isFirst, isLast)
} else {
// Create two siblings with attachment in the case toplevel is not multipart/mixed.
multiPartHeaders := make(textproto.MIMEHeader)
for k, v := range header {
multiPartHeaders[k] = v
}
boundary := randomBoundary()
multiPartHeaders.Set("Content-Type", "multipart/mixed; boundary=\""+boundary+"\"")
multiPartHeaders.Del("Content-Transfer-Encoding")
_ = pka.target.Accept(partReader, multiPartHeaders, false, true, false)
originalHeader := make(textproto.MIMEHeader)
originalHeader.Set("Content-Type", header.Get("Content-Type"))
if header.Get("Content-Transfer-Encoding") != "" {
originalHeader.Set("Content-Transfer-Encoding", header.Get("Content-Transfer-Encoding"))
}
_ = pka.target.Accept(partReader, originalHeader, false, true, false)
_ = pka.target.Accept(partReader, multiPartHeaders, hasPlainSiblings, false, false)
attachmentHeaders, attachmentReader := createKeyAttachment(pka.attachedPublicKey, pka.attachedPublicKeyName)
_ = pka.target.Accept(attachmentReader, attachmentHeaders, false, true, true)
_ = pka.target.Accept(partReader, multiPartHeaders, hasPlainSiblings, false, true)
func bestChoice(childParts []parser.Parts, preferredContentType string) parser.Parts {
// If one of the parts has preferred content type, use that.
for i := len(childParts) - 1; i >= 0; i-- {
if allPartsHaveContentType(childParts[i], preferredContentType) {
return childParts[i]
}
} else if isLast && pka.depth == 1 && pka.attachedPublicKey != "" {
_ = pka.target.Accept(partReader, header, hasPlainSiblings, isFirst, false)
attachmentHeaders, attachmentReader := createKeyAttachment(pka.attachedPublicKey, pka.attachedPublicKeyName)
_ = pka.target.Accept(attachmentReader, attachmentHeaders, hasPlainSiblings, true, true)
_ = pka.target.Accept(partReader, header, hasPlainSiblings, isFirst, true)
} else {
_ = pka.target.Accept(partReader, header, hasPlainSiblings, isFirst, isLast)
}
return nil
// Otherwise, choose the last one.
return childParts[len(childParts)-1]
}
// ======= Parser ==========
func Parse(r io.Reader, attachedPublicKey, attachedPublicKeyName string) (m *pmapi.Message, mimeBody string, plainContents string, atts []io.Reader, err error) {
secondReader := new(bytes.Buffer)
_, _ = secondReader.ReadFrom(r)
mimeBody = secondReader.String()
mm, err := mail.ReadMessage(secondReader)
if err != nil {
return
func allPartsHaveContentType(parts parser.Parts, contentType string) bool {
if len(parts) == 0 {
return false
}
if m, err = parseHeader(mm.Header); err != nil {
return
for _, part := range parts {
t, _, err := part.Header.ContentType()
if err != nil {
return false
}
if t != contentType {
return false
}
}
h := textproto.MIMEHeader(m.Header)
mmBodyData, err := ioutil.ReadAll(mm.Body)
if err != nil {
return
return true
}
func determineMIMEType(p *parser.Parser) (string, error) {
var isHTML bool
w := p.NewWalker().
RegisterContentTypeHandler("text/html", func(p *parser.Part) (err error) {
isHTML = true
return
})
if err := w.Walk(); err != nil {
return "", err
}
printAccepter := pmmime.NewMIMEPrinter()
publicKeyAttacher := NewPublicKeyAttacher(printAccepter, attachedPublicKey, attachedPublicKeyName)
sevenBitFilter := NewSevenBitFilter(publicKeyAttacher)
plainTextCollector := pmmime.NewPlainTextCollector(sevenBitFilter)
htmlOnlyConvertor := NewHTMLOnlyConvertor(plainTextCollector)
visitor := pmmime.NewMimeVisitor(htmlOnlyConvertor)
err = pmmime.VisitAll(bytes.NewReader(mmBodyData), h, visitor)
/*
err = visitor.VisitAll(h, bytes.NewReader(mmBodyData))
*/
if err != nil {
return
}
mimeBody = printAccepter.String()
plainContents = plainTextCollector.GetPlainText()
parts, headers, err := pmmime.GetAllChildParts(bytes.NewReader(mmBodyData), h)
if err != nil {
return
}
convertPlainToHTML := checkHeaders(headers)
isHTML, err := combineParts(m, parts, headers, convertPlainToHTML, &atts)
if isHTML {
m.MIMEType = "text/html"
} else {
m.MIMEType = "text/plain"
return "text/html", nil
}
return m, mimeBody, plainContents, atts, err
return "text/plain", nil
}
// getPlainBody returns the body of the given part, converting html to
// plaintext where possible.
func getPlainBody(part *parser.Part) []byte {
contentType, _, err := part.Header.ContentType()
if err != nil {
return part.Body
}
switch contentType {
case "text/html":
text, err := html2text.FromReader(bytes.NewReader(part.Body))
if err != nil {
return part.Body
}
return []byte(text)
default:
return part.Body
}
}
func attachPublicKey(p *parser.Part, key, keyName string) {
h := message.Header{}
h.Set("Content-Type", fmt.Sprintf(`application/pgp-keys; name="%v.asc"; filename="%v.asc"`, keyName, keyName))
h.Set("Content-Disposition", fmt.Sprintf(`attachment; name="%v.asc"; filename="%v.asc"`, keyName, keyName))
h.Set("Content-Transfer-Encoding", "base64")
p.AddChild(&parser.Part{
Header: h,
Body: []byte(key),
})
}
// NOTE: We should use our own ParseAddressList here.
func parseMessageHeader(m *pmapi.Message, h message.Header) error { // nolint[funlen]
mimeHeader, err := toMailHeader(h)
if err != nil {
return err
}
m.Header = mimeHeader
if err := forEachDecodedHeaderField(h, func(key, val string) error {
switch strings.ToLower(key) {
case "subject":
m.Subject = val
case "from":
sender, err := parseAddressList(val)
if err != nil {
return err
}
if len(sender) > 0 {
m.Sender = sender[0]
}
case "to":
toList, err := parseAddressList(val)
if err != nil {
return err
}
m.ToList = toList
case "reply-to":
replyTos, err := parseAddressList(val)
if err != nil {
return err
}
m.ReplyTos = replyTos
case "cc":
ccList, err := parseAddressList(val)
if err != nil {
return err
}
m.CCList = ccList
case "bcc":
bccList, err := parseAddressList(val)
if err != nil {
return err
}
m.BCCList = bccList
case "date":
date, err := mail.ParseDate(val)
if err != nil {
return err
}
m.Time = date.Unix()
}
return nil
}); err != nil {
return err
}
return nil
}
func parseAttachment(h message.Header) (*pmapi.Attachment, error) {
att := &pmapi.Attachment{}
mimeHeader, err := toMIMEHeader(h)
if err != nil {
return nil, err
}
att.Header = mimeHeader
mimeType, _, err := h.ContentType()
if err != nil {
return nil, err
}
att.MIMEType = mimeType
_, dispParams, dispErr := h.ContentDisposition()
if dispErr != nil {
ext, err := mime.ExtensionsByType(att.MIMEType)
if err != nil {
return nil, err
}
if len(ext) > 0 {
att.Name = "attachment" + ext[0]
}
} else {
att.Name = dispParams["filename"]
if att.Name == "" {
att.Name = "attachment.bin"
}
}
att.ContentID = strings.Trim(h.Get("Content-Id"), " <>")
return att, nil
}
func forEachDecodedHeaderField(h message.Header, fn func(string, string) error) error {
fields := h.Fields()
for fields.Next() {
text, err := fields.Text()
if err != nil {
if !message.IsUnknownCharset(err) {
return err
}
if text, err = pmmime.DecodeHeader(fields.Value()); err != nil {
return err
}
}
if err := fn(fields.Key(), text); err != nil {
return err
}
}
return nil
}
func toMailHeader(h message.Header) (mail.Header, error) {
mimeHeader := make(mail.Header)
if err := forEachDecodedHeaderField(h, func(key, val string) error {
mimeHeader[key] = []string{val}
return nil
}); err != nil {
return nil, err
}
return mimeHeader, nil
}
func toMIMEHeader(h message.Header) (textproto.MIMEHeader, error) {
mimeHeader := make(textproto.MIMEHeader)
if err := forEachDecodedHeaderField(h, func(key, val string) error {
mimeHeader[key] = []string{val}
return nil
}); err != nil {
return nil, err
}
return mimeHeader, nil
}

View File

@ -0,0 +1,57 @@
// Copyright (c) 2020 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 parser
import "regexp"
type HandlerFunc func(*Part) error
type handler struct {
typeRegExp, dispRegExp *regexp.Regexp
fn HandlerFunc
}
func (h *handler) matchPart(p *Part) bool {
return h.matchType(p) || h.matchDisp(p)
}
func (h *handler) matchType(p *Part) bool {
if h.typeRegExp == nil {
return false
}
t, _, err := p.Header.ContentType()
if err != nil {
t = ""
}
return h.typeRegExp.MatchString(t)
}
func (h *handler) matchDisp(p *Part) bool {
if h.dispRegExp == nil {
return false
}
disp, _, err := p.Header.ContentDisposition()
if err != nil {
disp = ""
}
return h.dispRegExp.MatchString(disp)
}

View File

@ -0,0 +1,154 @@
// Copyright (c) 2020 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 parser
import (
"io"
"io/ioutil"
"github.com/emersion/go-message"
)
type Parser struct {
stack []*Part
root *Part
}
func New(r io.Reader) (*Parser, error) {
p := new(Parser)
entity, err := message.Read(r)
if err != nil && !message.IsUnknownCharset(err) {
return nil, err
}
if err := p.parseEntity(entity); err != nil {
return nil, err
}
return p, nil
}
func (p *Parser) NewWalker() *Walker {
return newWalker(p.root)
}
func (p *Parser) NewVisitor(defaultRule VisitorRule) *Visitor {
return newVisitor(p.root, defaultRule)
}
func (p *Parser) NewWriter() *Writer {
return newWriter(p.root)
}
func (p *Parser) Root() *Part {
return p.root
}
// Section returns the message part referred to by the given section. A section
// is zero or more integers. For example, section 1.2.3 will return the third
// part of the second part of the first part of the message.
func (p *Parser) Section(section []int) (part *Part, err error) {
part = p.root
for _, n := range section {
if part, err = part.Child(n); err != nil {
return
}
}
return
}
func (p *Parser) beginPart() {
p.stack = append(p.stack, &Part{})
}
func (p *Parser) endPart() {
var part *Part
p.stack, part = p.stack[:len(p.stack)-1], p.stack[len(p.stack)-1]
if len(p.stack) > 0 {
p.top().children = append(p.top().children, part)
} else {
p.root = part
}
}
func (p *Parser) top() *Part {
if len(p.stack) == 0 {
return nil
}
return p.stack[len(p.stack)-1]
}
func (p *Parser) withHeader(h message.Header) {
p.top().Header = h
}
func (p *Parser) withBody(bytes []byte) {
p.top().Body = bytes
}
func (p *Parser) parseEntity(e *message.Entity) error {
p.beginPart()
defer p.endPart()
p.withHeader(e.Header)
if mr := e.MultipartReader(); mr != nil {
return p.parseMultipart(mr)
}
return p.parsePart(e)
}
func (p *Parser) parsePart(e *message.Entity) (err error) {
bytes, err := ioutil.ReadAll(e.Body)
if err != nil {
return
}
p.withBody(bytes)
return
}
func (p *Parser) parseMultipart(r message.MultipartReader) (err error) {
for {
var child *message.Entity
if child, err = r.NextPart(); err != nil && !message.IsUnknownCharset(err) {
return ignoreEOF(err)
}
if err = p.parseEntity(child); err != nil {
return
}
}
}
func ignoreEOF(err error) error {
if err == io.EOF {
return nil
}
return err
}

View File

@ -15,11 +15,40 @@
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// +build pmapi_dev
package parser
package pmapi
import (
"io"
"io/ioutil"
"os"
"path/filepath"
"testing"
func init() {
rootURL = "dev.protonmail.com/api"
rootScheme = "https"
"github.com/stretchr/testify/require"
)
func newTestParser(t *testing.T, msg string) *Parser {
p, err := New(getFileReader(msg))
require.NoError(t, err)
return p
}
func getFileReader(filename string) io.ReadCloser {
f, err := os.Open(filepath.Join("testdata", filename))
if err != nil {
panic(err)
}
return f
}
func getFileAsString(filename string) string {
b, err := ioutil.ReadAll(getFileReader(filename))
if err != nil {
panic(err)
}
return string(b)
}

188
pkg/message/parser/part.go Normal file
View File

@ -0,0 +1,188 @@
// Copyright (c) 2020 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 parser
import (
"bytes"
"errors"
"mime"
"unicode/utf8"
pmmime "github.com/ProtonMail/proton-bridge/pkg/mime"
"github.com/PuerkitoBio/goquery"
"github.com/emersion/go-message"
"github.com/sirupsen/logrus"
"golang.org/x/net/html"
"golang.org/x/net/html/charset"
"golang.org/x/text/encoding"
)
type Parts []*Part
type Part struct {
Header message.Header
Body []byte
children Parts
}
func (p *Part) Child(n int) (part *Part, err error) {
if len(p.children) < n {
return nil, errors.New("no such part")
}
return p.children[n-1], nil
}
func (p *Part) Children() Parts {
return p.children
}
func (p *Part) AddChild(child *Part) {
if p.isMultipartMixed() {
p.children = append(p.children, child)
} else {
root := &Part{
Header: getContentHeaders(p.Header),
Body: p.Body,
children: p.children,
}
p.Body = nil
p.children = Parts{root, child}
stripContentHeaders(&p.Header)
p.Header.Set("Content-Type", "multipart/mixed")
}
}
func (p *Part) ConvertToUTF8() error {
logrus.Trace("Converting part to utf-8")
t, params, err := p.Header.ContentType()
if err != nil {
return err
}
decoder := selectSuitableDecoder(p, t, params)
if p.Body, err = decoder.Bytes(p.Body); err != nil {
return err
}
if params == nil {
params = make(map[string]string)
}
params["charset"] = "UTF-8"
p.Header.SetContentType(t, params)
return nil
}
func (p *Part) ConvertMetaCharset() error {
doc, err := html.Parse(bytes.NewReader(p.Body))
if err != nil {
return err
}
goquery.NewDocumentFromNode(doc).Find("meta").Each(func(n int, sel *goquery.Selection) {
if val, ok := sel.Attr("content"); ok {
t, params, err := mime.ParseMediaType(val)
if err != nil {
return
}
params["charset"] = "UTF-8"
sel.SetAttr("content", mime.FormatMediaType(t, params))
}
if _, ok := sel.Attr("charset"); ok {
sel.SetAttr("charset", "UTF-8")
}
})
buf := new(bytes.Buffer)
if err := html.Render(buf, doc); err != nil {
return err
}
p.Body = buf.Bytes()
return nil
}
func selectSuitableDecoder(p *Part, t string, params map[string]string) *encoding.Decoder {
if charset, ok := params["charset"]; ok {
logrus.WithField("charset", charset).Trace("The part has a specified charset")
if decoder, err := pmmime.SelectDecoder(charset); err == nil {
logrus.Trace("The charset is known; decoder has been selected")
return decoder
}
logrus.Warn("The charset is unknown; no decoder could be selected")
}
if utf8.Valid(p.Body) {
logrus.Trace("The part is already valid utf-8, returning noop encoder")
return encoding.Nop.NewDecoder()
}
encoding, name, _ := charset.DetermineEncoding(p.Body, t)
logrus.WithField("name", name).Warn("Determined encoding by reading body")
return encoding.NewDecoder()
}
func (p *Part) is7BitClean() bool {
for _, b := range p.Body {
if b > 1<<7 {
return false
}
}
return true
}
func (p *Part) isMultipartMixed() bool {
t, _, err := p.Header.ContentType()
if err != nil {
return false
}
return t == "multipart/mixed"
}
func getContentHeaders(header message.Header) message.Header {
var res message.Header
res.Set("Content-Type", header.Get("Content-Type"))
res.Set("Content-Disposition", header.Get("Content-Disposition"))
res.Set("Content-Transfer-Encoding", header.Get("Content-Transfer-Encoding"))
return res
}
func stripContentHeaders(header *message.Header) {
header.Del("Content-Type")
header.Del("Content-Disposition")
header.Del("Content-Transfer-Encoding")
}

View File

@ -0,0 +1,73 @@
// Copyright (c) 2020 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 parser
import (
"strconv"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestPart(t *testing.T) {
p := newTestParser(t, "complex_structure.eml")
wantParts := map[string]string{
"": "multipart/mixed",
"1": "text/plain",
"2": "application/octet-stream",
"3": "multipart/mixed",
"3.1": "text/plain",
"3.2": "application/octet-stream",
"4": "multipart/mixed",
"4.1": "image/gif",
"4.2": "multipart/mixed",
"4.2.1": "text/plain",
"4.2.2": "multipart/alternative",
"4.2.2.1": "text/plain",
"4.2.2.2": "text/html",
}
for partNumber, wantContType := range wantParts {
part, err := p.Section(getSectionNumber(partNumber))
require.NoError(t, err)
contType, _, err := part.Header.ContentType()
require.NoError(t, err)
assert.Equal(t, wantContType, contType)
}
}
func getSectionNumber(s string) (part []int) {
if s == "" {
return
}
for _, number := range strings.Split(s, ".") {
i64, err := strconv.ParseInt(number, 10, 64)
if err != nil {
panic(err)
}
part = append(part, int(i64))
}
return
}

View File

@ -0,0 +1,91 @@
Subject: Sample mail
From: John Doe <jdoe@machine.example>
To: Mary Smith <mary@example.net>
Date: Fri, 21 Nov 1997 09:55:06 -0600
Content-Type: multipart/mixed; boundary="0000MAIN"
main summary
--0000MAIN
Content-Type: text/plain
1. main message
--0000MAIN
Content-Type: application/octet-stream
Content-Disposition: inline; filename="main_signature.sig"
Content-Transfer-Encoding: base64
aWYgeW91IGFyZSByZWFkaW5nIHRoaXMsIGhpIQ==
--0000MAIN
Subject: Inside mail 3
From: Mary Smith <mary@example.net>
To: John Doe <jdoe@machine.example>
Date: Fri, 20 Nov 1997 09:55:06 -0600
Content-Type: multipart/mixed; boundary="0003MSG"
3. message summary
--0003MSG
Content-Type: text/plain
3.1 message text
--0003MSG
Content-Type: application/octet-stream
Content-Disposition: attachment; filename="msg_3_signature.sig"
Content-Transfer-Encoding: base64
aWYgeW91IGFyZSByZWFkaW5nIHRoaXMsIGhpIQ==
--0003MSG--
--0000MAIN
Content-Type: multipart/mixed; boundary="0004ATTACH"
4 attach summary
--0004ATTACH
Content-Type: image/gif
Content-Disposition: attachment; filename="att4.1_gif.sig"
Content-Transfer-Encoding: base64
aWYgeW91IGFyZSByZWFkaW5nIHRoaXMsIGhpIQ==
--0004ATTACH
Subject: Inside mail 4.2
From: Mary Smith <mary@example.net>
To: John Doe <jdoe@machine.example>
Date: Fri, 10 Nov 1997 09:55:06 -0600
Content-Type: multipart/mixed; boundary="0042MSG"
4.2 message summary
--0042MSG
Content-Type: text/plain
4.2.1 message text
--0042MSG
Content-Type: multipart/alternative; boundary="0422ALTER"
4.2.2 alternative summary
--0422ALTER
Content-Type: text/plain
4.2.2.1 plain text
--0422ALTER
Content-Type: text/html
<h1>4.2.2.2 html text</h1>
--0422ALTER--
--0042MSG--
--0004ATTACH--
--0000MAIN--

View File

@ -0,0 +1,36 @@
To: pmbridgeietest@outlook.com
From: schizofrenic <schizofrenic@pm.me>
Subject: aoeuaoeu
Message-ID: <7dc32b61-b9cf-f2d3-8ec5-10e5b4a33ec1@pm.me>
Date: Thu, 30 Jul 2020 13:35:24 +0200
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:68.0)
Gecko/20100101 Thunderbird/68.11.0
MIME-Version: 1.0
Content-Type: multipart/alternative;
boundary="------------22BC647264E52252E386881A"
Content-Language: en-US
This is a multi-part message in MIME format.
--------------22BC647264E52252E386881A
Content-Type: text/plain; charset=utf-8; format=flowed
Content-Transfer-Encoding: 7bit
*aoeuaoeu*
--------------22BC647264E52252E386881A
Content-Type: text/html; charset=utf-8
Content-Transfer-Encoding: 7bit
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
</head>
<body>
<p><b>aoeuaoeu</b><br>
</p>
</body>
</html>
--------------22BC647264E52252E386881A--

View File

@ -0,0 +1,9 @@
From: Sender <sender@pm.me>
To: Receiver <receiver@pm.me>
Content-Type: multipart/mixed; boundary=longrandomstring
--longrandomstring
Content-Type: text/html
<html><body>This is body of <b>HTML mail</b> with attachment</body></html>
--longrandomstring--

View File

@ -1,17 +1,16 @@
Content-Type: multipart/mixed; boundary=longrandomstring
From: Sender <sender@pm.me>
To: Receiver <receiver@pm.me>
Mime-Version: 1.0
From: Sender <sender@pm.me>
To: Receiver <receiver@pm.me>
Content-Type: multipart/mixed; boundary=longrandomstring
This is a multi-part message in MIME format.
--longrandomstring
Content-Transfer-Encoding: quoted-printable
Content-Type: text/html
body
<html><body>This is body of <b>HTML mail</b> with attachment</body></html>
--longrandomstring
Content-Transfer-Encoding: base64
Content-Type: application/octet-stream
Content-Type: application/octet-stream
Content-Transfer-Encoding: base64
Content-Disposition: attachment
aWYgeW91IGFyZSByZWFkaW5nIHRoaXMsIGhpIQ==
--longrandomstring--
.

View File

@ -0,0 +1,81 @@
// Copyright (c) 2020 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 parser
import "regexp"
type Visitor struct {
root *Part
rules []*visitorRule
defaultRule VisitorRule
}
func newVisitor(root *Part, defaultRule VisitorRule) *Visitor {
return &Visitor{
root: root,
defaultRule: defaultRule,
}
}
type Visit func(*Part) (interface{}, error)
type VisitorRule func(*Part, Visit) (interface{}, error)
type visitorRule struct {
re *regexp.Regexp
fn VisitorRule
}
// RegisterRule defines what to do when visiting a part whose content type
// matches the given regular expression.
// If a part matches multiple rules, the one registered first will be used.
func (v *Visitor) RegisterRule(contentTypeRegex string, fn VisitorRule) *Visitor {
v.rules = append(v.rules, &visitorRule{
re: regexp.MustCompile(contentTypeRegex),
fn: fn,
})
return v
}
func (v *Visitor) Visit() (interface{}, error) {
return v.visit(v.root)
}
func (v *Visitor) visit(p *Part) (interface{}, error) {
t, _, err := p.Header.ContentType()
if err != nil {
return nil, err
}
if rule := v.getRuleForContentType(t); rule != nil {
return rule.fn(p, v.visit)
}
return v.defaultRule(p, v.visit)
}
func (v *Visitor) getRuleForContentType(contentType string) *visitorRule {
for _, rule := range v.rules {
if rule.re.MatchString(contentType) {
return rule
}
}
return nil
}

View File

@ -0,0 +1,93 @@
// Copyright (c) 2020 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 parser
import "regexp"
type Walker struct {
root *Part
handlers []*handler
defaultHandler HandlerFunc
}
func newWalker(root *Part) *Walker {
return &Walker{
root: root,
defaultHandler: func(*Part) error { return nil },
}
}
func (w *Walker) Walk() (err error) {
return w.walkOverPart(w.root)
}
func (w *Walker) walkOverPart(p *Part) error {
if err := w.getHandlerFunc(p)(p); err != nil {
return err
}
for _, child := range p.children {
if err := w.walkOverPart(child); err != nil {
return err
}
}
return nil
}
// RegisterDefaultHandler registers a handler that will be called on every part
// that doesn't match a registered content type/disposition handler.
func (w *Walker) RegisterDefaultHandler(fn HandlerFunc) *Walker {
w.defaultHandler = fn
return w
}
// RegisterContentTypeHandler registers a handler that will be called when a
// part's content type matches the given regular expression.
// If a part matches multiple handlers, the one registered first will be chosen.
func (w *Walker) RegisterContentTypeHandler(typeRegExp string, fn HandlerFunc) *Walker {
w.handlers = append(w.handlers, &handler{
typeRegExp: regexp.MustCompile(typeRegExp),
fn: fn,
})
return w
}
// RegisterContentDispositionHandler registers a handler that will be called
// when a part's content disposition matches the given regular expression.
// If a part matches multiple handlers, the one registered first will be chosen.
func (w *Walker) RegisterContentDispositionHandler(dispRegExp string, fn HandlerFunc) *Walker {
w.handlers = append(w.handlers, &handler{
dispRegExp: regexp.MustCompile(dispRegExp),
fn: fn,
})
return w
}
func (w *Walker) getHandlerFunc(p *Part) HandlerFunc {
for _, handler := range w.handlers {
if handler.matchPart(p) {
return handler.fn
}
}
return w.defaultHandler
}

View File

@ -0,0 +1,98 @@
// Copyright (c) 2020 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 parser
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestWalker(t *testing.T) {
p := newTestParser(t, "text_html_octet_attachment.eml")
allBodies := [][]byte{}
walker := p.NewWalker().
RegisterDefaultHandler(func(p *Part) (err error) {
if p.Body != nil {
allBodies = append(allBodies, p.Body)
}
return
})
assert.NoError(t, walker.Walk())
assert.ElementsMatch(t, [][]byte{
[]byte("<html><body>This is body of <b>HTML mail</b> with attachment</body></html>"),
[]byte("if you are reading this, hi!"),
}, allBodies)
}
func TestWalkerTypeHandler(t *testing.T) {
p := newTestParser(t, "text_html_octet_attachment.eml")
html := [][]byte{}
walker := p.NewWalker().
RegisterContentTypeHandler("text/html", func(p *Part) (err error) {
html = append(html, p.Body)
return
})
assert.NoError(t, walker.Walk())
assert.ElementsMatch(t, [][]byte{
[]byte("<html><body>This is body of <b>HTML mail</b> with attachment</body></html>"),
}, html)
}
func TestWalkerDispositionHandler(t *testing.T) {
p := newTestParser(t, "text_html_octet_attachment.eml")
attachments := [][]byte{}
walker := p.NewWalker().
RegisterContentDispositionHandler("attachment", func(p *Part) (err error) {
attachments = append(attachments, p.Body)
return
})
assert.NoError(t, walker.Walk())
assert.ElementsMatch(t, [][]byte{
[]byte("if you are reading this, hi!"),
}, attachments)
}
func TestWalkerDispositionAndTypeHandler_TypeDefinedFirst(t *testing.T) {
p := newTestParser(t, "text_html_octet_attachment.eml")
var typeCalled, dispCalled bool
walker := p.NewWalker().
RegisterContentTypeHandler("application/octet-stream", func(p *Part) (err error) {
typeCalled = true
return
}).
RegisterContentDispositionHandler("attachment", func(p *Part) (err error) {
dispCalled = true
return
})
assert.NoError(t, walker.Walk())
assert.True(t, typeCalled)
assert.False(t, dispCalled)
}

View File

@ -0,0 +1,84 @@
// Copyright (c) 2020 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 parser
import (
"io"
"github.com/emersion/go-message"
)
type Writer struct {
root *Part
}
func newWriter(root *Part) *Writer {
return &Writer{
root: root,
}
}
func (w *Writer) Write(ww io.Writer) error {
if !w.root.is7BitClean() {
w.root.Header.Add("Content-Transfer-Encoding", "base64")
}
msgWriter, err := message.CreateWriter(ww, w.root.Header)
if err != nil {
return err
}
if err := w.write(msgWriter, w.root); err != nil {
return err
}
return msgWriter.Close()
}
func (w *Writer) write(writer *message.Writer, p *Part) error {
if len(p.children) > 0 {
for _, child := range p.children {
if err := w.writeAsChild(writer, child); err != nil {
return err
}
}
}
if _, err := writer.Write(p.Body); err != nil {
return err
}
return nil
}
func (w *Writer) writeAsChild(writer *message.Writer, p *Part) error {
if !p.is7BitClean() {
p.Header.Add("Content-Transfer-Encoding", "base64")
}
childWriter, err := writer.CreatePart(p.Header)
if err != nil {
return err
}
if err := w.write(childWriter, p); err != nil {
return err
}
return childWriter.Close()
}

View File

@ -15,11 +15,27 @@
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// +build pmapi_nopin
package parser
package pmapi
import (
"bytes"
"strings"
"testing"
func init() {
// This config disables TLS cert checking.
checkTLSCerts = false
"github.com/stretchr/testify/assert"
)
func TestParserWrite(t *testing.T) {
p := newTestParser(t, "text_html_octet_attachment.eml")
w := p.NewWriter()
buf := new(bytes.Buffer)
assert.NoError(t, w.Write(buf))
assert.Equal(t, getFileAsString("text_html_octet_attachment.eml"), crlf(buf.String()))
}
func crlf(s string) string {
return strings.ReplaceAll(s, "\r\n", "\n")
}

View File

@ -21,16 +21,420 @@ import (
"image/png"
"io"
"io/ioutil"
"math/rand"
"net/mail"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/text/encoding/charmap"
)
func TestParseTextPlain(t *testing.T) {
f := getFileReader("text_plain.eml")
m, _, plainBody, attReaders, err := Parse(f, "", "")
require.NoError(t, err)
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
assert.Equal(t, "body", m.Body)
assert.Equal(t, "body", plainBody)
assert.Len(t, attReaders, 0)
}
func TestParseTextPlainUTF8(t *testing.T) {
f := getFileReader("text_plain_utf8.eml")
m, _, plainBody, attReaders, err := Parse(f, "", "")
require.NoError(t, err)
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
assert.Equal(t, "body", m.Body)
assert.Equal(t, "body", plainBody)
assert.Len(t, attReaders, 0)
}
func TestParseTextPlainLatin1(t *testing.T) {
f := getFileReader("text_plain_latin1.eml")
m, _, plainBody, attReaders, err := Parse(f, "", "")
require.NoError(t, err)
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
assert.Equal(t, "ééééééé", m.Body)
assert.Equal(t, "ééééééé", plainBody)
assert.Len(t, attReaders, 0)
}
func TestParseTextPlainUTF8Subject(t *testing.T) {
f := getFileReader("text_plain_utf8_subject.eml")
m, _, plainBody, attReaders, err := Parse(f, "", "")
require.NoError(t, err)
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
assert.Equal(t, `汉字汉字汉`, m.Subject)
assert.Equal(t, "body", m.Body)
assert.Equal(t, "body", plainBody)
assert.Len(t, attReaders, 0)
}
func TestParseTextPlainLatin2Subject(t *testing.T) {
f := getFileReader("text_plain_latin2_subject.eml")
m, _, plainBody, attReaders, err := Parse(f, "", "")
require.NoError(t, err)
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
assert.Equal(t, `If you can read this you understand the example.`, m.Subject)
assert.Equal(t, "body", m.Body)
assert.Equal(t, "body", plainBody)
assert.Len(t, attReaders, 0)
}
func TestParseTextPlainUnknownCharsetIsActuallyLatin1(t *testing.T) {
f := getFileReader("text_plain_unknown_latin1.eml")
m, _, plainBody, attReaders, err := Parse(f, "", "")
require.NoError(t, err)
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
assert.Equal(t, "ééééééé", m.Body)
assert.Equal(t, "ééééééé", plainBody)
assert.Len(t, attReaders, 0)
}
func TestParseTextPlainUnknownCharsetIsActuallyLatin2(t *testing.T) {
f := getFileReader("text_plain_unknown_latin2.eml")
m, _, plainBody, attReaders, err := Parse(f, "", "")
require.NoError(t, err)
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
// The file contains latin2-encoded text, but we will assume it is latin1
// and decode it as such. This will lead to corruption.
latin2, _ := charmap.ISO8859_2.NewEncoder().Bytes([]byte("řšřšřš"))
expect, _ := charmap.ISO8859_1.NewDecoder().Bytes(latin2)
assert.NotEqual(t, []byte("řšřšřš"), expect)
assert.Equal(t, string(expect), m.Body)
assert.Equal(t, string(expect), plainBody)
assert.Len(t, attReaders, 0)
}
func TestParseTextPlainAlready7Bit(t *testing.T) {
f := getFileReader("text_plain_7bit.eml")
m, _, plainBody, attReaders, err := Parse(f, "", "")
require.NoError(t, err)
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
assert.Equal(t, "body", m.Body)
assert.Equal(t, "body", plainBody)
assert.Len(t, attReaders, 0)
}
func TestParseTextPlainWithOctetAttachment(t *testing.T) {
f := getFileReader("text_plain_octet_attachment.eml")
m, _, plainBody, attReaders, err := Parse(f, "", "")
require.NoError(t, err)
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
assert.Equal(t, "body", m.Body)
assert.Equal(t, "body", plainBody)
require.Len(t, attReaders, 1)
assert.Equal(t, readerToString(attReaders[0]), "if you are reading this, hi!")
}
func TestParseTextPlainWithOctetAttachmentGoodFilename(t *testing.T) {
f := getFileReader("text_plain_octet_attachment_good_2231_filename.eml")
m, _, plainBody, attReaders, err := Parse(f, "", "")
require.NoError(t, err)
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
assert.Equal(t, "body", m.Body)
assert.Equal(t, "body", plainBody)
assert.Len(t, attReaders, 1)
assert.Equal(t, readerToString(attReaders[0]), "if you are reading this, hi!")
assert.Equal(t, "😁😂.txt", m.Attachments[0].Name)
}
func TestParseTextPlainWithOctetAttachmentBadFilename(t *testing.T) {
f := getFileReader("text_plain_octet_attachment_bad_2231_filename.eml")
m, _, plainBody, attReaders, err := Parse(f, "", "")
require.NoError(t, err)
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
assert.Equal(t, "body", m.Body)
assert.Equal(t, "body", plainBody)
assert.Len(t, attReaders, 1)
assert.Equal(t, readerToString(attReaders[0]), "if you are reading this, hi!")
assert.Equal(t, "attachment.bin", m.Attachments[0].Name)
}
func TestParseTextPlainWithPlainAttachment(t *testing.T) {
f := getFileReader("text_plain_plain_attachment.eml")
m, _, plainBody, attReaders, err := Parse(f, "", "")
require.NoError(t, err)
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
assert.Equal(t, "body", m.Body)
assert.Equal(t, "body", plainBody)
require.Len(t, attReaders, 1)
assert.Equal(t, readerToString(attReaders[0]), "attachment")
}
func TestParseTextPlainWithImageInline(t *testing.T) {
f := getFileReader("text_plain_image_inline.eml")
m, _, plainBody, attReaders, err := Parse(f, "", "")
require.NoError(t, err)
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
assert.Equal(t, "body", m.Body)
assert.Equal(t, "body", plainBody)
// The inline image is an 8x8 mic-dropping gopher.
require.Len(t, attReaders, 1)
img, err := png.DecodeConfig(attReaders[0])
require.NoError(t, err)
assert.Equal(t, 8, img.Width)
assert.Equal(t, 8, img.Height)
}
func TestParseWithMultipleTextParts(t *testing.T) {
f := getFileReader("multiple_text_parts.eml")
m, _, plainBody, attReaders, err := Parse(f, "", "")
require.NoError(t, err)
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
assert.Equal(t, "body\nsome other part of the message", m.Body)
assert.Equal(t, "body\nsome other part of the message", plainBody)
assert.Len(t, attReaders, 0)
}
func TestParseTextHTML(t *testing.T) {
f := getFileReader("text_html.eml")
m, _, plainBody, attReaders, err := Parse(f, "", "")
require.NoError(t, err)
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
assert.Equal(t, "<html><head></head><body>This is body of <b>HTML mail</b> without attachment</body></html>", m.Body)
assert.Equal(t, "This is body of *HTML mail* without attachment", plainBody)
assert.Len(t, attReaders, 0)
}
func TestParseTextHTMLAlready7Bit(t *testing.T) {
f := getFileReader("text_html_7bit.eml")
m, _, plainBody, attReaders, err := Parse(f, "", "")
assert.NoError(t, err)
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
assert.Equal(t, "<html><head></head><body>This is body of <b>HTML mail</b> without attachment</body></html>", m.Body)
assert.Equal(t, "This is body of *HTML mail* without attachment", plainBody)
assert.Len(t, attReaders, 0)
}
func TestParseTextHTMLWithOctetAttachment(t *testing.T) {
f := getFileReader("text_html_octet_attachment.eml")
m, _, plainBody, attReaders, err := Parse(f, "", "")
require.NoError(t, err)
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
assert.Equal(t, "<html><head></head><body>This is body of <b>HTML mail</b> with attachment</body></html>", m.Body)
assert.Equal(t, "This is body of *HTML mail* with attachment", plainBody)
require.Len(t, attReaders, 1)
assert.Equal(t, readerToString(attReaders[0]), "if you are reading this, hi!")
}
func TestParseTextHTMLWithPlainAttachment(t *testing.T) {
f := getFileReader("text_html_plain_attachment.eml")
m, _, plainBody, attReaders, err := Parse(f, "", "")
require.NoError(t, err)
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
// BAD: plainBody should not be empty!
assert.Equal(t, "<html><head></head><body>This is body of <b>HTML mail</b> with attachment</body></html>", m.Body)
assert.Equal(t, "This is body of *HTML mail* with attachment", plainBody)
require.Len(t, attReaders, 1)
assert.Equal(t, readerToString(attReaders[0]), "attachment")
}
func TestParseTextHTMLWithImageInline(t *testing.T) {
f := getFileReader("text_html_image_inline.eml")
m, _, plainBody, attReaders, err := Parse(f, "", "")
assert.NoError(t, err)
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
assert.Equal(t, "<html><head></head><body>This is body of <b>HTML mail</b> with attachment</body></html>", m.Body)
assert.Equal(t, "This is body of *HTML mail* with attachment", plainBody)
// The inline image is an 8x8 mic-dropping gopher.
require.Len(t, attReaders, 1)
img, err := png.DecodeConfig(attReaders[0])
require.NoError(t, err)
assert.Equal(t, 8, img.Width)
assert.Equal(t, 8, img.Height)
}
func TestParseWithAttachedPublicKey(t *testing.T) {
f := getFileReader("text_plain.eml")
m, _, plainBody, attReaders, err := Parse(f, "publickey", "publickeyname")
require.NoError(t, err)
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
assert.Equal(t, "body", m.Body)
assert.Equal(t, "body", plainBody)
// The pubkey should not be collected as an attachment.
// We upload the pubkey when creating the draft.
require.Len(t, attReaders, 0)
}
func TestParseTextHTMLWithEmbeddedForeignEncoding(t *testing.T) {
f := getFileReader("text_html_embedded_foreign_encoding.eml")
m, _, plainBody, attReaders, err := Parse(f, "", "")
require.NoError(t, err)
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
assert.Equal(t, `<html><head><meta charset="UTF-8"/></head><body>latin2 řšřš</body></html>`, m.Body)
assert.Equal(t, `latin2 řšřš`, plainBody)
assert.Len(t, attReaders, 0)
}
func TestParseMultipartAlternative(t *testing.T) {
f := getFileReader("multipart_alternative.eml")
m, _, plainBody, _, err := Parse(f, "", "")
require.NoError(t, err)
assert.Equal(t, `"schizofrenic" <schizofrenic@pm.me>`, m.Sender.String())
assert.Equal(t, `<pmbridgeietest@outlook.com>`, m.ToList[0].String())
assert.Equal(t, `<html><head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8"/>
</head>
<body>
<b>aoeuaoeu</b>
</body></html>`, m.Body)
assert.Equal(t, "*aoeuaoeu*\n\n", plainBody)
}
func TestParseMultipartAlternativeNested(t *testing.T) {
f := getFileReader("multipart_alternative_nested.eml")
m, _, plainBody, _, err := Parse(f, "", "")
require.NoError(t, err)
assert.Equal(t, `"schizofrenic" <schizofrenic@pm.me>`, m.Sender.String())
assert.Equal(t, `<pmbridgeietest@outlook.com>`, m.ToList[0].String())
assert.Equal(t, `<html><head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8"/>
</head>
<body>
<b>multipart 2.2</b>
</body></html>`, m.Body)
assert.Equal(t, "*multipart 2.1*\n\n", plainBody)
}
func getFileReader(filename string) io.Reader {
f, err := os.Open(filepath.Join("testdata", filename))
if err != nil {
panic(err)
}
return f
}
func readerToString(r io.Reader) string {
b, err := ioutil.ReadAll(r)
if err != nil {
panic(err)
}
return string(b)
}
func TestRFC822AddressFormat(t *testing.T) { //nolint[funlen]
tests := []struct {
address string
@ -99,414 +503,11 @@ func TestRFC822AddressFormat(t *testing.T) { //nolint[funlen]
}
for _, data := range tests {
uncommented := parseAddressComment(data.address)
result, err := mail.ParseAddressList(uncommented)
if err != nil {
t.Errorf("Can not parse '%s' created from '%s': %v", uncommented, data.address, err)
}
if len(result) != len(data.expected) {
t.Errorf("Wrong parsing of '%s' created from '%s': expected '%s' but have '%+v'", uncommented, data.address, data.expected, result)
}
result, err := parseAddressList(data.address)
assert.NoError(t, err)
assert.Len(t, result, len(data.expected))
for i, result := range result {
if data.expected[i] != result.String() {
t.Errorf("Wrong parsing\nof %q\ncreated from %q:\nexpected %q\nbut have %q", uncommented, data.address, data.expected, result.String())
}
assert.Equal(t, data.expected[i], result.String())
}
}
}
func f(filename string) io.ReadCloser {
f, err := os.Open(filepath.Join("testdata", filename))
if err != nil {
panic(err)
}
return f
}
func s(filename string) string {
b, err := ioutil.ReadAll(f(filename))
if err != nil {
panic(err)
}
return string(b)
}
func readerToString(r io.Reader) string {
b, err := ioutil.ReadAll(r)
if err != nil {
panic(err)
}
return string(b)
}
func TestParseMessageTextPlain(t *testing.T) {
f := f("text_plain.eml")
defer func() { _ = f.Close() }()
m, mimeBody, plainContents, atts, err := Parse(f, "", "")
assert.NoError(t, err)
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
assert.Equal(t, "body", m.Body)
assert.Equal(t, s("text_plain.mime"), mimeBody)
assert.Equal(t, "body", plainContents)
assert.Len(t, atts, 0)
}
func TestParseMessageTextPlainUTF8(t *testing.T) {
f := f("text_plain_utf8.eml")
defer func() { _ = f.Close() }()
m, mimeBody, plainContents, atts, err := Parse(f, "", "")
assert.NoError(t, err)
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
assert.Equal(t, "body", m.Body)
assert.Equal(t, s("text_plain_utf8.mime"), mimeBody)
assert.Equal(t, "body", plainContents)
assert.Len(t, atts, 0)
}
func TestParseMessageTextPlainLatin1(t *testing.T) {
f := f("text_plain_latin1.eml")
defer func() { _ = f.Close() }()
m, mimeBody, plainContents, atts, err := Parse(f, "", "")
assert.NoError(t, err)
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
assert.Equal(t, "ééééééé", m.Body)
assert.Equal(t, s("text_plain_latin1.mime"), mimeBody)
assert.Equal(t, "ééééééé", plainContents)
assert.Len(t, atts, 0)
}
func TestParseMessageTextPlainUnknownCharsetIsActuallyLatin1(t *testing.T) {
f := f("text_plain_unknown_latin1.eml")
defer func() { _ = f.Close() }()
m, mimeBody, plainContents, atts, err := Parse(f, "", "")
assert.NoError(t, err)
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
assert.Equal(t, "ééééééé", m.Body)
assert.Equal(t, s("text_plain_unknown_latin1.mime"), mimeBody)
assert.Equal(t, "ééééééé", plainContents)
assert.Len(t, atts, 0)
}
func TestParseMessageTextPlainUnknownCharsetIsActuallyLatin2(t *testing.T) {
f := f("text_plain_unknown_latin2.eml")
defer func() { _ = f.Close() }()
m, mimeBody, plainContents, atts, err := Parse(f, "", "")
assert.NoError(t, err)
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
// The file contains latin2-encoded text, but we will assume it is latin1
// and decode it as such. This will lead to corruption.
latin2, _ := charmap.ISO8859_2.NewEncoder().Bytes([]byte("řšřšřš"))
expect, _ := charmap.ISO8859_1.NewDecoder().Bytes(latin2)
assert.NotEqual(t, []byte("řšřšřš"), expect)
assert.Equal(t, string(expect), m.Body)
assert.Equal(t, s("text_plain_unknown_latin2.mime"), mimeBody)
assert.Equal(t, string(expect), plainContents)
assert.Len(t, atts, 0)
}
func TestParseMessageTextPlainAlready7Bit(t *testing.T) {
f := f("text_plain_7bit.eml")
defer func() { _ = f.Close() }()
m, mimeBody, plainContents, atts, err := Parse(f, "", "")
assert.NoError(t, err)
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
assert.Equal(t, "body", m.Body)
assert.Equal(t, s("text_plain_7bit.mime"), mimeBody)
assert.Equal(t, "body", plainContents)
assert.Len(t, atts, 0)
}
func TestParseMessageTextPlainWithOctetAttachment(t *testing.T) {
f := f("text_plain_octet_attachment.eml")
defer func() { _ = f.Close() }()
m, mimeBody, plainContents, atts, err := Parse(f, "", "")
assert.NoError(t, err)
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
assert.Equal(t, "body", m.Body)
assert.Equal(t, s("text_plain_octet_attachment.mime"), mimeBody)
assert.Equal(t, "body", plainContents)
assert.Len(t, atts, 1)
assert.Equal(t, readerToString(atts[0]), "if you are reading this, hi!")
}
func TestParseMessageTextPlainWithOctetAttachmentGoodFilename(t *testing.T) {
f := f("text_plain_octet_attachment_good_2231_filename.eml")
defer func() { _ = f.Close() }()
m, mimeBody, plainContents, atts, err := Parse(f, "", "")
assert.NoError(t, err)
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
assert.Equal(t, "body", m.Body)
assert.Equal(t, s("text_plain_octet_attachment_good_2231_filename.mime"), mimeBody)
assert.Equal(t, "body", plainContents)
assert.Len(t, atts, 1)
assert.Equal(t, readerToString(atts[0]), "if you are reading this, hi!")
assert.Equal(t, "😁😂.txt", m.Attachments[0].Name)
}
func TestParseMessageTextPlainWithOctetAttachmentBadFilename(t *testing.T) {
f := f("text_plain_octet_attachment_bad_2231_filename.eml")
defer func() { _ = f.Close() }()
m, mimeBody, plainContents, atts, err := Parse(f, "", "")
assert.NoError(t, err)
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
assert.Equal(t, "body", m.Body)
assert.Equal(t, s("text_plain_octet_attachment_bad_2231_filename.mime"), mimeBody)
assert.Equal(t, "body", plainContents)
assert.Len(t, atts, 1)
assert.Equal(t, readerToString(atts[0]), "if you are reading this, hi!")
assert.Equal(t, "attachment.bin", m.Attachments[0].Name)
}
func TestParseMessageTextPlainWithPlainAttachment(t *testing.T) {
f := f("text_plain_plain_attachment.eml")
defer func() { _ = f.Close() }()
m, mimeBody, plainContents, atts, err := Parse(f, "", "")
assert.NoError(t, err)
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
assert.Equal(t, "body", m.Body)
assert.Equal(t, s("text_plain_plain_attachment.mime"), mimeBody)
assert.Equal(t, "body", plainContents)
assert.Len(t, atts, 1)
assert.Equal(t, readerToString(atts[0]), "attachment")
}
func TestParseMessageTextPlainWithImageInline(t *testing.T) {
f := f("text_plain_image_inline.eml")
defer func() { _ = f.Close() }()
m, mimeBody, plainContents, atts, err := Parse(f, "", "")
assert.NoError(t, err)
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
assert.Equal(t, "body", m.Body)
assert.Equal(t, s("text_plain_image_inline.mime"), mimeBody)
assert.Equal(t, "body", plainContents)
// The inline image is an 8x8 mic-dropping gopher.
assert.Len(t, atts, 1)
img, err := png.DecodeConfig(atts[0])
assert.NoError(t, err)
assert.Equal(t, 8, img.Width)
assert.Equal(t, 8, img.Height)
}
func TestParseMessageWithMultipleTextParts(t *testing.T) {
f := f("multiple_text_parts.eml")
defer func() { _ = f.Close() }()
m, mimeBody, plainContents, atts, err := Parse(f, "", "")
assert.NoError(t, err)
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
assert.Equal(t, "body\nsome other part of the message", m.Body)
assert.Equal(t, s("multiple_text_parts.mime"), mimeBody)
assert.Equal(t, "body\nsome other part of the message", plainContents)
assert.Len(t, atts, 0)
}
func TestParseMessageTextHTML(t *testing.T) {
rand.Seed(0)
f := f("text_html.eml")
defer func() { _ = f.Close() }()
m, mimeBody, plainContents, atts, err := Parse(f, "", "")
assert.NoError(t, err)
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
assert.Equal(t, "<html><head></head><body>This is body of <b>HTML mail</b> without attachment</body></html>", m.Body)
assert.Equal(t, s("text_html.mime"), mimeBody)
assert.Equal(t, "This is body of *HTML mail* without attachment", plainContents)
assert.Len(t, atts, 0)
}
func TestParseMessageTextHTMLAlready7Bit(t *testing.T) {
rand.Seed(0)
f := f("text_html_7bit.eml")
defer func() { _ = f.Close() }()
m, mimeBody, plainContents, atts, err := Parse(f, "", "")
assert.NoError(t, err)
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
assert.Equal(t, "<html><head></head><body>This is body of <b>HTML mail</b> without attachment</body></html>", m.Body)
assert.Equal(t, s("text_html_7bit.mime"), mimeBody)
assert.Equal(t, "This is body of *HTML mail* without attachment", plainContents)
assert.Len(t, atts, 0)
}
func TestParseMessageTextHTMLWithOctetAttachment(t *testing.T) {
rand.Seed(0)
f := f("text_html_octet_attachment.eml")
defer func() { _ = f.Close() }()
m, mimeBody, plainContents, atts, err := Parse(f, "", "")
assert.NoError(t, err)
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
assert.Equal(t, "<html><head></head><body>This is body of <b>HTML mail</b> with attachment</body></html>", m.Body)
assert.Equal(t, s("text_html_octet_attachment.mime"), mimeBody)
assert.Equal(t, "This is body of *HTML mail* with attachment", plainContents)
assert.Len(t, atts, 1)
assert.Equal(t, readerToString(atts[0]), "if you are reading this, hi!")
}
// NOTE: Enable when bug is fixed.
func _TestParseMessageTextHTMLWithPlainAttachment(t *testing.T) { // nolint[deadcode]
rand.Seed(0)
f := f("text_html_plain_attachment.eml")
defer func() { _ = f.Close() }()
m, mimeBody, plainContents, atts, err := Parse(f, "", "")
assert.NoError(t, err)
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
// BAD: plainContents should not be empty!
assert.Equal(t, "<html><head></head><body>This is body of <b>HTML mail</b> with attachment</body></html>", m.Body)
assert.Equal(t, s("text_html_plain_attachment.mime"), mimeBody)
assert.Equal(t, "This is body of *HTML mail* with attachment", plainContents)
assert.Len(t, atts, 1)
assert.Equal(t, readerToString(atts[0]), "attachment")
}
func TestParseMessageTextHTMLWithImageInline(t *testing.T) {
rand.Seed(0)
f := f("text_html_image_inline.eml")
defer func() { _ = f.Close() }()
m, mimeBody, plainContents, atts, err := Parse(f, "", "")
assert.NoError(t, err)
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
assert.Equal(t, "<html><head></head><body>This is body of <b>HTML mail</b> with attachment</body></html>", m.Body)
assert.Equal(t, s("text_html_image_inline.mime"), mimeBody)
assert.Equal(t, "This is body of *HTML mail* with attachment", plainContents)
// The inline image is an 8x8 mic-dropping gopher.
assert.Len(t, atts, 1)
img, err := png.DecodeConfig(atts[0])
assert.NoError(t, err)
assert.Equal(t, 8, img.Width)
assert.Equal(t, 8, img.Height)
}
// NOTE: Enable when bug is fixed.
func _TestParseMessageWithAttachedPublicKey(t *testing.T) { // nolint[deadcode]
f := f("text_plain.eml")
defer func() { _ = f.Close() }()
// BAD: Public Key is not attached unless Content-Type is specified (not required)!
m, mimeBody, plainContents, atts, err := Parse(f, "publickey", "publickeyname")
assert.NoError(t, err)
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
assert.Equal(t, "body", m.Body)
assert.Equal(t, s("text_plain_pubkey.mime"), mimeBody)
assert.Equal(t, "body", plainContents)
// BAD: Public key not available as an attachment!
assert.Len(t, atts, 1)
}
// NOTE: Enable when bug is fixed.
func _TestParseMessageTextHTMLWithEmbeddedForeignEncoding(t *testing.T) { // nolint[deadcode]
rand.Seed(0)
f := f("text_html_embedded_foreign_encoding.eml")
defer func() { _ = f.Close() }()
m, mimeBody, plainContents, atts, err := Parse(f, "", "")
assert.NoError(t, err)
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
// BAD: Bridge does not detect the charset specified in the <meta> tag of the html.
assert.Equal(t, `<html><head><meta charset="ISO-8859-2"></head><body>latin2 řšřš</body></html>`, m.Body)
assert.Equal(t, s("text_html_embedded_foreign_encoding.mime"), mimeBody)
assert.Equal(t, `latin2 řšřš`, plainContents)
assert.Len(t, atts, 0)
}

View File

@ -28,7 +28,13 @@ import (
"github.com/stretchr/testify/require"
)
var enableDebug = false // nolint[global]
func debug(msg string, v ...interface{}) {
if !enableDebug {
return
}
_, file, line, _ := runtime.Caller(1)
fmt.Printf("%s:%d: \033[2;33m"+msg+"\033[0;39m\n", append([]interface{}{filepath.Base(file), line}, v...)...)
}

View File

@ -0,0 +1,30 @@
To: pmbridgeietest@outlook.com
From: schizofrenic <schizofrenic@pm.me>
Subject: aoeuaoeu
Date: Thu, 30 Jul 2020 13:35:24 +0200
MIME-Version: 1.0
Content-Type: multipart/alternative; boundary="------------22BC647264E52252E386881A"
Content-Language: en-US
This is a multi-part message in MIME format.
--------------22BC647264E52252E386881A
Content-Type: text/plain; charset=utf-8; format=flowed
Content-Transfer-Encoding: 7bit
*aoeuaoeu*
--------------22BC647264E52252E386881A
Content-Type: text/html; charset=utf-8
Content-Transfer-Encoding: 7bit
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
</head>
<body>
<b>aoeuaoeu</b>
</body>
</html>
--------------22BC647264E52252E386881A--

View File

@ -0,0 +1,64 @@
To: pmbridgeietest@outlook.com
From: schizofrenic <schizofrenic@pm.me>
Subject: aoeuaoeu
Date: Thu, 30 Jul 2020 13:35:24 +0200
MIME-Version: 1.0
Content-Type: multipart/alternative; boundary="------------abcdefghijklmnopqrstuvwx"
Content-Language: en-US
This is a multi-part message in MIME format.
--------------abcdefghijklmnopqrstuvwx
Content-Type: multipart/alternative; boundary="------------22BC647264E52252E386881A"
Content-Language: en-US
This is a multi-part message in MIME format.
--------------22BC647264E52252E386881A
Content-Type: text/plain; charset=utf-8; format=flowed
Content-Transfer-Encoding: 7bit
*multipart 1.1*
--------------22BC647264E52252E386881A
Content-Type: text/html; charset=utf-8
Content-Transfer-Encoding: 7bit
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
</head>
<body>
<b>multipart 1.2</b>
</body>
</html>
--------------22BC647264E52252E386881A--
--------------abcdefghijklmnopqrstuvwx
Content-Type: multipart/alternative; boundary="------------22BC647264E52252E386881B"
Content-Language: en-US
This is a multi-part message in MIME format.
--------------22BC647264E52252E386881B
Content-Type: text/plain; charset=utf-8; format=flowed
Content-Transfer-Encoding: 7bit
*multipart 2.1*
--------------22BC647264E52252E386881B
Content-Type: text/html; charset=utf-8
Content-Transfer-Encoding: 7bit
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
</head>
<body>
<b>multipart 2.2</b>
</body>
</html>
--------------22BC647264E52252E386881B--
--------------abcdefghijklmnopqrstuvwx--

File diff suppressed because it is too large Load Diff

View File

@ -1,17 +0,0 @@
Content-Type: multipart/mixed; boundary=longrandomstring
From: Sender <sender@pm.me>
To: Receiver <receiver@pm.me>
This is a multi-part message in MIME format.
--longrandomstring
Content-Transfer-Encoding: quoted-printable
body
--longrandomstring
Content-Transfer-Encoding: quoted-printable
some other part of the message
--longrandomstring--
.

View File

@ -1,19 +0,0 @@
Content-Type: multipart/alternative; boundary="0194fdc2fa2ffcc041d3ff12045b73c86e4ff95ff662a5eee82abdf44a2d"
From: Sender <sender@pm.me>
To: Receiver <receiver@pm.me>
This is a multi-part message in MIME format.
--0194fdc2fa2ffcc041d3ff12045b73c86e4ff95ff662a5eee82abdf44a2d
Content-Transfer-Encoding: quoted-printable
Content-Type: text/html
<html><body>This is body of <b>HTML mail</b> without attachment</body></htm=
l>
--0194fdc2fa2ffcc041d3ff12045b73c86e4ff95ff662a5eee82abdf44a2d
Content-Transfer-Encoding: quoted-printable
Content-Type: text/plain
This is body of *HTML mail* without attachment
--0194fdc2fa2ffcc041d3ff12045b73c86e4ff95ff662a5eee82abdf44a2d--
.

View File

@ -1,19 +0,0 @@
Content-Transfer-Encoding: 7bit
Content-Type: multipart/alternative; boundary="0194fdc2fa2ffcc041d3ff12045b73c86e4ff95ff662a5eee82abdf44a2d"
From: Sender <sender@pm.me>
To: Receiver <receiver@pm.me>
This is a multi-part message in MIME format.
--0194fdc2fa2ffcc041d3ff12045b73c86e4ff95ff662a5eee82abdf44a2d
Content-Transfer-Encoding: 7bit
Content-Type: text/html
<html><body>This is body of <b>HTML mail</b> without attachment</body></html>
--0194fdc2fa2ffcc041d3ff12045b73c86e4ff95ff662a5eee82abdf44a2d
Content-Transfer-Encoding: 7bit
Content-Type: text/plain
This is body of *HTML mail* without attachment
--0194fdc2fa2ffcc041d3ff12045b73c86e4ff95ff662a5eee82abdf44a2d--
.

View File

@ -2,4 +2,4 @@ From: Sender <sender@pm.me>
To: Receiver <receiver@pm.me>
Content-Type: text/html
<html><head><meta charset="ISO-8859-2"></head><body>latin2 <20><><EFBFBD><EFBFBD></body></html>
<html><head><meta charset="ISO-8859-2"></head><body>latin2 <20><><EFBFBD><EFBFBD></body></html>

View File

@ -1,52 +0,0 @@
Content-Type: multipart/mixed; boundary=longrandomstring
From: Sender <sender@pm.me>
To: Receiver <receiver@pm.me>
This is a multi-part message in MIME format.
--longrandomstring
Content-Type: multipart/alternative; boundary="0194fdc2fa2ffcc041d3ff12045b73c86e4ff95ff662a5eee82abdf44a2d"
This is a multi-part message in MIME format.
--0194fdc2fa2ffcc041d3ff12045b73c86e4ff95ff662a5eee82abdf44a2d
Content-Transfer-Encoding: quoted-printable
Content-Type: text/html
<html><body>This is body of <b>HTML mail</b> with attachment</body></html>
--0194fdc2fa2ffcc041d3ff12045b73c86e4ff95ff662a5eee82abdf44a2d
Content-Transfer-Encoding: quoted-printable
Content-Type: text/plain
This is body of *HTML mail* with attachment
--0194fdc2fa2ffcc041d3ff12045b73c86e4ff95ff662a5eee82abdf44a2d--
.
--longrandomstring
Content-Disposition: inline
Content-Transfer-Encoding: base64
Content-Type: image/png
iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAABGdBTUEAALGPC/xhBQAAACBjSFJ
NAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAhGVYSWZNTQAqAAAACAAFAR
IAAwAAAAEAAQAAARoABQAAAAEAAABKARsABQAAAAEAAABSASgAAwAAAAEAAgAAh2kABAAAAAEAA
ABaAAAAAAAAASwAAAABAAABLAAAAAEAA6ABAAMAAAABAAEAAKACAAQAAAABAAAACKADAAQAAAAB
AAAACAAAAAAAXWZ6AAAACXBIWXMAAC4jAAAuIwF4pT92AAACZmlUWHRYTUw6Y29tLmFkb2JlLnh
tcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIE
NvcmUgNS40LjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5O
TkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91
dD0iIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4
wLyIKICAgICAgICAgICAgeG1sbnM6ZXhpZj0iaHR0cDovL25zLmFkb2JlLmNvbS9leGlmLzEuMC
8iPgogICAgICAgICA8dGlmZjpPcmllbnRhdGlvbj4xPC90aWZmOk9yaWVudGF0aW9uPgogICAgI
CAgICA8dGlmZjpSZXNvbHV0aW9uVW5pdD4yPC90aWZmOlJlc29sdXRpb25Vbml0PgogICAgICAg
ICA8ZXhpZjpDb2xvclNwYWNlPjE8L2V4aWY6Q29sb3JTcGFjZT4KICAgICAgICAgPGV4aWY6UGl
4ZWxYRGltZW5zaW9uPjE2PC9leGlmOlBpeGVsWERpbWVuc2lvbj4KICAgICAgICAgPGV4aWY6UG
l4ZWxZRGltZW5zaW9uPjE2PC9leGlmOlBpeGVsWURpbWVuc2lvbj4KICAgICAgPC9yZGY6RGVzY
3JpcHRpb24+CiAgIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+CgZBD4sAAAEISURBVBgZY2CAAO5F
x07Zz96xZ0Pn4lXqIKGGhgYmsFTHvAWdW6/dvnb89Yf/B5+9/r/y9IXzbVPahCH6/jMysfAJygo
JC2r++/T619Mb139J8HIb8Gs5hYMUzJ+/gJ1Jmo9H6c+L5wz3bt5iEeLmYOHn42fQ4vyacqGNQS
0xMfEHc7Cvl6CYho4rh5jUPyYefqafLKyMbH9+/d28/dFfdWtfDaZvTy7Zvv72nYGZkeEvw98/f
5j//2P4yCvxq/nU7zVs//8yM2gzMMitOnnu5cUff/8ff/v5/5Xf///vuHBhJcSRDAws9aEMr38c
W7XjNgvzexZ2rn9vbjx/IXl/M9iLM2fOZAUAKCZv7dU+UgAAAAAASUVORK5CYII=
--longrandomstring--
.

View File

@ -1,31 +0,0 @@
Content-Type: multipart/mixed; boundary=longrandomstring
From: Sender <sender@pm.me>
To: Receiver <receiver@pm.me>
This is a multi-part message in MIME format.
--longrandomstring
Content-Type: multipart/alternative; boundary="0194fdc2fa2ffcc041d3ff12045b73c86e4ff95ff662a5eee82abdf44a2d"
This is a multi-part message in MIME format.
--0194fdc2fa2ffcc041d3ff12045b73c86e4ff95ff662a5eee82abdf44a2d
Content-Transfer-Encoding: quoted-printable
Content-Type: text/html
<html><body>This is body of <b>HTML mail</b> with attachment</body></html>
--0194fdc2fa2ffcc041d3ff12045b73c86e4ff95ff662a5eee82abdf44a2d
Content-Transfer-Encoding: quoted-printable
Content-Type: text/plain
This is body of *HTML mail* with attachment
--0194fdc2fa2ffcc041d3ff12045b73c86e4ff95ff662a5eee82abdf44a2d--
.
--longrandomstring
Content-Transfer-Encoding: base64
Content-Type: application/octet-stream
aWYgeW91IGFyZSByZWFkaW5nIHRoaXMsIGhpIQ==
--longrandomstring--
.

View File

@ -1,5 +0,0 @@
Content-Transfer-Encoding: quoted-printable
From: Sender <sender@pm.me>
To: Receiver <receiver@pm.me>
body

View File

@ -1,38 +0,0 @@
Content-Type: multipart/related; boundary=longrandomstring
From: Sender <sender@pm.me>
To: Receiver <receiver@pm.me>
This is a multi-part message in MIME format.
--longrandomstring
Content-Transfer-Encoding: quoted-printable
body
--longrandomstring
Content-Disposition: inline
Content-Transfer-Encoding: base64
Content-Type: image/png
iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAABGdBTUEAALGPC/xhBQAAACBjSFJ
NAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAhGVYSWZNTQAqAAAACAAFAR
IAAwAAAAEAAQAAARoABQAAAAEAAABKARsABQAAAAEAAABSASgAAwAAAAEAAgAAh2kABAAAAAEAA
ABaAAAAAAAAASwAAAABAAABLAAAAAEAA6ABAAMAAAABAAEAAKACAAQAAAABAAAACKADAAQAAAAB
AAAACAAAAAAAXWZ6AAAACXBIWXMAAC4jAAAuIwF4pT92AAACZmlUWHRYTUw6Y29tLmFkb2JlLnh
tcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIE
NvcmUgNS40LjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5O
TkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91
dD0iIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4
wLyIKICAgICAgICAgICAgeG1sbnM6ZXhpZj0iaHR0cDovL25zLmFkb2JlLmNvbS9leGlmLzEuMC
8iPgogICAgICAgICA8dGlmZjpPcmllbnRhdGlvbj4xPC90aWZmOk9yaWVudGF0aW9uPgogICAgI
CAgICA8dGlmZjpSZXNvbHV0aW9uVW5pdD4yPC90aWZmOlJlc29sdXRpb25Vbml0PgogICAgICAg
ICA8ZXhpZjpDb2xvclNwYWNlPjE8L2V4aWY6Q29sb3JTcGFjZT4KICAgICAgICAgPGV4aWY6UGl
4ZWxYRGltZW5zaW9uPjE2PC9leGlmOlBpeGVsWERpbWVuc2lvbj4KICAgICAgICAgPGV4aWY6UG
l4ZWxZRGltZW5zaW9uPjE2PC9leGlmOlBpeGVsWURpbWVuc2lvbj4KICAgICAgPC9yZGY6RGVzY
3JpcHRpb24+CiAgIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+CgZBD4sAAAEISURBVBgZY2CAAO5F
x07Zz96xZ0Pn4lXqIKGGhgYmsFTHvAWdW6/dvnb89Yf/B5+9/r/y9IXzbVPahCH6/jMysfAJygo
JC2r++/T619Mb139J8HIb8Gs5hYMUzJ+/gJ1Jmo9H6c+L5wz3bt5iEeLmYOHn42fQ4vyacqGNQS
0xMfEHc7Cvl6CYho4rh5jUPyYefqafLKyMbH9+/d28/dFfdWtfDaZvTy7Zvv72nYGZkeEvw98/f
5j//2P4yCvxq/nU7zVs//8yM2gzMMitOnnu5cUff/8ff/v5/5Xf///vuHBhJcSRDAws9aEMr38c
W7XjNgvzexZ2rn9vbjx/IXl/M9iLM2fOZAUAKCZv7dU+UgAAAAAASUVORK5CYII=
--longrandomstring--
.

View File

@ -1,6 +0,0 @@
Content-Transfer-Encoding: quoted-printable
Content-Type: text/plain; charset=ISO-8859-1
From: Sender <sender@pm.me>
To: Receiver <receiver@pm.me>
=E9=E9=E9=E9=E9=E9=E9

View File

@ -0,0 +1,6 @@
From: Sender <sender@pm.me>
To: Receiver <receiver@pm.me>
Subject: =?ISO-8859-1?B?SWYgeW91IGNhbiByZWFkIHRoaXMgeW8=?=
=?ISO-8859-2?B?dSB1bmRlcnN0YW5kIHRoZSBleGFtcGxlLg==?=
body

View File

@ -1,18 +0,0 @@
Content-Type: multipart/mixed; boundary=longrandomstring
From: Sender <sender@pm.me>
To: Receiver <receiver@pm.me>
This is a multi-part message in MIME format.
--longrandomstring
Content-Transfer-Encoding: quoted-printable
body
--longrandomstring
Content-Disposition: attachment; filename*=utf-8'%F0%9F%98%81%F0%9F%98%82.txt
Content-Transfer-Encoding: base64
Content-Type: application/octet-stream
aWYgeW91IGFyZSByZWFkaW5nIHRoaXMsIGhpIQ==
--longrandomstring--
.

View File

@ -1,18 +0,0 @@
Content-Type: multipart/mixed; boundary=longrandomstring
From: Sender <sender@pm.me>
To: Receiver <receiver@pm.me>
This is a multi-part message in MIME format.
--longrandomstring
Content-Transfer-Encoding: quoted-printable
body
--longrandomstring
Content-Disposition: attachment; filename*=utf-8''%F0%9F%98%81%F0%9F%98%82.txt
Content-Transfer-Encoding: base64
Content-Type: application/octet-stream
aWYgeW91IGFyZSByZWFkaW5nIHRoaXMsIGhpIQ==
--longrandomstring--
.

View File

@ -1,17 +0,0 @@
Content-Type: multipart/mixed; boundary=longrandomstring
From: Sender <sender@pm.me>
To: Receiver <receiver@pm.me>
This is a multi-part message in MIME format.
--longrandomstring
Content-Transfer-Encoding: quoted-printable
body
--longrandomstring
Content-Disposition: attachment
Content-Transfer-Encoding: quoted-printable
attachment
--longrandomstring--
.

View File

@ -1,6 +0,0 @@
Content-Transfer-Encoding: quoted-printable
Content-Type: text/plain
From: Sender <sender@pm.me>
To: Receiver <receiver@pm.me>
=E9=E9=E9=E9=E9=E9=E9

View File

@ -1,6 +0,0 @@
Content-Transfer-Encoding: quoted-printable
Content-Type: text/plain
From: Sender <sender@pm.me>
To: Receiver <receiver@pm.me>
=F8=B9=F8=B9=F8=B9

View File

@ -1,6 +0,0 @@
Content-Transfer-Encoding: quoted-printable
Content-Type: text/plain; charset=utf-8
From: Sender <sender@pm.me>
To: Receiver <receiver@pm.me>
body

View File

@ -1,5 +1,5 @@
Content-Transfer-Encoding: 7bit
From: Sender <sender@pm.me>
To: Receiver <receiver@pm.me>
From: Sender <sender@pm.me>
To: Receiver <receiver@pm.me>
Subject: =?UTF-8?B?5rGJ5a2X5rGJ5a2X5rGJ?=
body

View File

@ -21,13 +21,10 @@ import (
"fmt"
"io"
"mime"
"mime/quotedprintable"
"regexp"
"strings"
"unicode/utf8"
"encoding/base64"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"golang.org/x/net/html/charset"
@ -37,7 +34,7 @@ import (
var wordDec = &mime.WordDecoder{
CharsetReader: func(charset string, input io.Reader) (io.Reader, error) {
dec, err := selectDecoder(charset)
dec, err := SelectDecoder(charset)
if err != nil {
return nil, err
}
@ -166,7 +163,7 @@ func getEncoding(charset string) (enc encoding.Encoding, err error) {
return
}
func selectDecoder(charset string) (decoder *encoding.Decoder, err error) {
func SelectDecoder(charset string) (decoder *encoding.Decoder, err error) {
var enc encoding.Encoding
lcharset := strings.Trim(strings.ToLower(charset), " \t\r\n")
switch lcharset {
@ -211,7 +208,7 @@ func DecodeCharset(original []byte, contentType string) ([]byte, error) {
}
if charset, ok := params["charset"]; ok {
decoder, err := selectDecoder(charset)
decoder, err := SelectDecoder(charset)
if err != nil {
return original, errors.Wrap(err, "unknown charset was specified")
}
@ -246,19 +243,6 @@ func DecodeCharset(original []byte, contentType string) ([]byte, error) {
return decoded, nil
}
// DecodeContentEncoding wraps the reader with decoder based on content encoding.
func DecodeContentEncoding(r io.Reader, contentEncoding string) (d io.Reader) {
switch strings.ToLower(contentEncoding) {
case "quoted-printable":
d = quotedprintable.NewReader(r)
case "base64":
d = base64.NewDecoder(base64.StdEncoding, r)
case "7bit", "8bit", "binary", "": // Nothing to do
d = r
}
return
}
// ParseMediaType from MIME doesn't support RFC2231 for non asci / utf8 encodings so we have to pre-parse it.
func ParseMediaType(v string) (mediatype string, params map[string]string, err error) {
v, _ = changeEncodingAndKeepLastParamDefinition(v)

View File

@ -1,479 +0,0 @@
// Copyright (c) 2020 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 pmmime
import (
"bytes"
"io"
"io/ioutil"
"mime/multipart"
"net/http"
"net/mail"
"net/textproto"
"regexp"
"strings"
log "github.com/sirupsen/logrus"
)
// VisitAcceptor decides what to do with part which is processed.
// It is used by MIMEVisitor.
type VisitAcceptor interface {
Accept(partReader io.Reader, header textproto.MIMEHeader, hasPlainSibling bool, isFirst, isLast bool) (err error)
}
func VisitAll(part io.Reader, h textproto.MIMEHeader, accepter VisitAcceptor) (err error) {
mediaType, _, err := getContentType(h)
if err != nil {
return
}
return accepter.Accept(part, h, mediaType == "text/plain", true, true)
}
func IsLeaf(h textproto.MIMEHeader) bool {
return !strings.HasPrefix(h.Get("Content-Type"), "multipart/")
}
// MIMEVisitor is main object to parse (visit) and process (accept) all parts of MIME message.
type MimeVisitor struct {
target VisitAcceptor
}
// Accept reads part recursively if needed.
// hasPlainSibling is there when acceptor want to check alternatives.
func (mv *MimeVisitor) Accept(part io.Reader, h textproto.MIMEHeader, hasPlainSibling bool, isFirst, isLast bool) (err error) {
if !isFirst {
return
}
parentMediaType, params, err := getContentType(h)
if err != nil {
return
}
if err = mv.target.Accept(part, h, hasPlainSibling, true, false); err != nil {
return
}
if !IsLeaf(h) {
var multiparts []io.Reader
var multipartHeaders []textproto.MIMEHeader
if multiparts, multipartHeaders, err = GetMultipartParts(part, params); err != nil {
return
}
hasPlainChild := false
for _, header := range multipartHeaders {
mediaType, _, _ := getContentType(header)
if mediaType == "text/plain" {
hasPlainChild = true
}
}
if hasPlainSibling && parentMediaType == "multipart/related" {
hasPlainChild = true
}
for i, p := range multiparts {
if err = mv.Accept(p, multipartHeaders[i], hasPlainChild, true, true); err != nil {
return
}
if err = mv.target.Accept(part, h, hasPlainSibling, false, i == (len(multiparts)-1)); err != nil {
return
}
}
}
return
}
// NewMIMEVisitor returns a new mime visitor initialised with an acceptor.
func NewMimeVisitor(targetAccepter VisitAcceptor) *MimeVisitor {
return &MimeVisitor{targetAccepter}
}
func GetAllChildParts(part io.Reader, h textproto.MIMEHeader) (parts []io.Reader, headers []textproto.MIMEHeader, err error) {
mediaType, params, err := getContentType(h)
if err != nil {
return
}
if strings.HasPrefix(mediaType, "multipart/") {
var multiparts []io.Reader
var multipartHeaders []textproto.MIMEHeader
if multiparts, multipartHeaders, err = GetMultipartParts(part, params); err != nil {
return
}
if strings.Contains(mediaType, "alternative") {
var chosenPart io.Reader
var chosenHeader textproto.MIMEHeader
if chosenPart, chosenHeader, err = pickAlternativePart(multiparts, multipartHeaders); err != nil {
return
}
var childParts []io.Reader
var childHeaders []textproto.MIMEHeader
if childParts, childHeaders, err = GetAllChildParts(chosenPart, chosenHeader); err != nil {
return
}
parts = append(parts, childParts...)
headers = append(headers, childHeaders...)
} else {
for i, p := range multiparts {
var childParts []io.Reader
var childHeaders []textproto.MIMEHeader
if childParts, childHeaders, err = GetAllChildParts(p, multipartHeaders[i]); err != nil {
return
}
parts = append(parts, childParts...)
headers = append(headers, childHeaders...)
}
}
} else {
parts = append(parts, part)
headers = append(headers, h)
}
return
}
func GetMultipartParts(r io.Reader, params map[string]string) (parts []io.Reader, headers []textproto.MIMEHeader, err error) {
mr := multipart.NewReader(r, params["boundary"])
parts = []io.Reader{}
headers = []textproto.MIMEHeader{}
var p *multipart.Part
for {
p, err = mr.NextPart()
if err == io.EOF {
err = nil
break
}
if err != nil {
return
}
b, _ := ioutil.ReadAll(p)
buffer := bytes.NewBuffer(b)
parts = append(parts, buffer)
headers = append(headers, p.Header)
}
return
}
func pickAlternativePart(parts []io.Reader, headers []textproto.MIMEHeader) (part io.Reader, h textproto.MIMEHeader, err error) {
for i, h := range headers {
mediaType, _, err := getContentType(h)
if err != nil {
continue
}
if strings.HasPrefix(mediaType, "multipart/") {
return parts[i], headers[i], nil
}
}
for i, h := range headers {
mediaType, _, err := getContentType(h)
if err != nil {
continue
}
if mediaType == "text/html" {
return parts[i], headers[i], nil
}
}
for i, h := range headers {
mediaType, _, err := getContentType(h)
if err != nil {
continue
}
if mediaType == "text/plain" {
return parts[i], headers[i], nil
}
}
// If we get all the way here, part will be nil.
return
}
// "Parse address comment" as defined in http://tools.wordtothewise.com/rfc/822
// FIXME: Does not work for address groups.
// NOTE: This should be removed for go>1.10 (please check).
func parseAddressComment(raw string) string {
parsed := []string{}
for _, item := range regexp.MustCompile("[,;]").Split(raw, -1) {
re := regexp.MustCompile("[(][^)]*[)]")
comments := strings.Join(re.FindAllString(item, -1), " ")
comments = strings.Replace(comments, "(", "", -1)
comments = strings.Replace(comments, ")", "", -1)
withoutComments := re.ReplaceAllString(item, "")
addr, err := mail.ParseAddress(withoutComments)
if err != nil {
continue
}
if addr.Name == "" {
addr.Name = comments
}
parsed = append(parsed, addr.String())
}
return strings.Join(parsed, ", ")
}
func decodePart(partReader io.Reader, header textproto.MIMEHeader) (decodedPart io.Reader) {
decodedPart = DecodeContentEncoding(partReader, header.Get("Content-Transfer-Encoding"))
if decodedPart == nil {
log.Warnf("Unsupported Content-Transfer-Encoding '%v'", header.Get("Content-Transfer-Encoding"))
decodedPart = partReader
}
return
}
// Assume 'text/plain' if missing.
func getContentType(header textproto.MIMEHeader) (mediatype string, params map[string]string, err error) {
contentType := header.Get("Content-Type")
if contentType == "" {
contentType = "text/plain"
}
return ParseMediaType(contentType)
}
// ===================== MIME Printer ===================================
// Simply print resulting MIME tree into text form.
// TODO move this to file mime_printer.go.
type stack []string
func (s stack) Push(v string) stack {
return append(s, v)
}
func (s stack) Pop() (stack, string) {
l := len(s)
return s[:l-1], s[l-1]
}
func (s stack) Peek() string {
return s[len(s)-1]
}
type MIMEPrinter struct {
result *bytes.Buffer
boundaryStack stack
}
func NewMIMEPrinter() (pd *MIMEPrinter) {
return &MIMEPrinter{
result: bytes.NewBuffer([]byte("")),
boundaryStack: stack{},
}
}
func (pd *MIMEPrinter) Accept(partReader io.Reader, header textproto.MIMEHeader, hasPlainSibling bool, isFirst, isLast bool) (err error) {
if isFirst {
http.Header(header).Write(pd.result)
pd.result.Write([]byte("\n"))
if IsLeaf(header) {
pd.result.ReadFrom(partReader)
} else {
_, params, _ := getContentType(header)
boundary := params["boundary"]
pd.boundaryStack = pd.boundaryStack.Push(boundary)
pd.result.Write([]byte("\nThis is a multi-part message in MIME format.\n--" + boundary + "\n"))
}
} else {
if !isLast {
pd.result.Write([]byte("\n--" + pd.boundaryStack.Peek() + "\n"))
} else {
var boundary string
pd.boundaryStack, boundary = pd.boundaryStack.Pop()
pd.result.Write([]byte("\n--" + boundary + "--\n.\n"))
}
}
return nil
}
func (pd *MIMEPrinter) String() string {
return pd.result.String()
}
// ======================== PlainText Collector =========================
// Collect contents of all non-attachment text/plain parts and return it as a string.
// TODO move this to file collector_plaintext.go.
type PlainTextCollector struct {
target VisitAcceptor
plainTextContents *bytes.Buffer
}
func NewPlainTextCollector(targetAccepter VisitAcceptor) *PlainTextCollector {
return &PlainTextCollector{
target: targetAccepter,
plainTextContents: bytes.NewBuffer([]byte("")),
}
}
func (ptc *PlainTextCollector) Accept(partReader io.Reader, header textproto.MIMEHeader, hasPlainSibling bool, isFirst, isLast bool) (err error) {
if isFirst {
if IsLeaf(header) {
mediaType, _, _ := getContentType(header)
disp, _, _ := ParseMediaType(header.Get("Content-Disposition"))
if mediaType == "text/plain" && disp != "attachment" {
partData, _ := ioutil.ReadAll(partReader)
decodedPart := decodePart(bytes.NewReader(partData), header)
if buffer, err := ioutil.ReadAll(decodedPart); err == nil {
buffer, err = DecodeCharset(buffer, header.Get("Content-Type"))
if err != nil {
log.Warnln("Decode charset error:", err)
return err
}
ptc.plainTextContents.Write(buffer)
}
err = ptc.target.Accept(bytes.NewReader(partData), header, hasPlainSibling, isFirst, isLast)
return
}
}
}
err = ptc.target.Accept(partReader, header, hasPlainSibling, isFirst, isLast)
return
}
func (ptc PlainTextCollector) GetPlainText() string {
return ptc.plainTextContents.String()
}
// ======================== Body Collector ==============
// Collect contents of all non-attachment parts and return it as a string.
// TODO move this to file collector_body.go.
type BodyCollector struct {
target VisitAcceptor
htmlBodyBuffer *bytes.Buffer
plainBodyBuffer *bytes.Buffer
htmlHeaderBuffer *bytes.Buffer
plainHeaderBuffer *bytes.Buffer
hasHtml bool
}
func NewBodyCollector(targetAccepter VisitAcceptor) *BodyCollector {
return &BodyCollector{
target: targetAccepter,
htmlBodyBuffer: bytes.NewBuffer([]byte("")),
plainBodyBuffer: bytes.NewBuffer([]byte("")),
htmlHeaderBuffer: bytes.NewBuffer([]byte("")),
plainHeaderBuffer: bytes.NewBuffer([]byte("")),
}
}
func (bc *BodyCollector) Accept(partReader io.Reader, header textproto.MIMEHeader, hasPlainSibling bool, isFirst, isLast bool) (err error) {
// TODO: Collect html and plaintext - if there's html with plain sibling don't include plain/text.
if isFirst {
if IsLeaf(header) {
mediaType, _, _ := getContentType(header)
disp, _, _ := ParseMediaType(header.Get("Content-Disposition"))
if disp != "attachment" {
partData, _ := ioutil.ReadAll(partReader)
decodedPart := decodePart(bytes.NewReader(partData), header)
if buffer, err := ioutil.ReadAll(decodedPart); err == nil {
buffer, err = DecodeCharset(buffer, header.Get("Content-Type"))
if err != nil {
log.Warnln("Decode charset error:", err)
return err
}
if mediaType == "text/html" {
bc.hasHtml = true
http.Header(header).Write(bc.htmlHeaderBuffer)
bc.htmlBodyBuffer.Write(buffer)
} else if mediaType == "text/plain" {
http.Header(header).Write(bc.plainHeaderBuffer)
bc.plainBodyBuffer.Write(buffer)
}
}
err = bc.target.Accept(bytes.NewReader(partData), header, hasPlainSibling, isFirst, isLast)
return
}
}
}
err = bc.target.Accept(partReader, header, hasPlainSibling, isFirst, isLast)
return
}
func (bc *BodyCollector) GetBody() (string, string) {
if bc.hasHtml {
return bc.htmlBodyBuffer.String(), "text/html"
} else {
return bc.plainBodyBuffer.String(), "text/plain"
}
}
func (bc *BodyCollector) GetHeaders() string {
if bc.hasHtml {
return bc.htmlHeaderBuffer.String()
} else {
return bc.plainHeaderBuffer.String()
}
}
// ======================== Attachments Collector ==============
// Collect contents of all attachment parts and return them as a string.
// TODO move this to file collector_attachment.go.
type AttachmentsCollector struct {
target VisitAcceptor
attBuffers []string
attHeaders []string
}
func NewAttachmentsCollector(targetAccepter VisitAcceptor) *AttachmentsCollector {
return &AttachmentsCollector{
target: targetAccepter,
attBuffers: []string{},
attHeaders: []string{},
}
}
func (ac *AttachmentsCollector) Accept(partReader io.Reader, header textproto.MIMEHeader, hasPlainSibling bool, isFirst, isLast bool) (err error) {
if isFirst {
if IsLeaf(header) {
mediaType, _, _ := getContentType(header)
disp, _, _ := ParseMediaType(header.Get("Content-Disposition"))
if (mediaType != "text/html" && mediaType != "text/plain") || disp == "attachment" {
partData, _ := ioutil.ReadAll(partReader)
decodedPart := decodePart(bytes.NewReader(partData), header)
if buffer, err := ioutil.ReadAll(decodedPart); err == nil {
buffer, err = DecodeCharset(buffer, header.Get("Content-Type"))
if err != nil {
log.Warnln("Decode charset error:", err)
return err
}
headerBuf := new(bytes.Buffer)
http.Header(header).Write(headerBuf)
ac.attHeaders = append(ac.attHeaders, headerBuf.String())
ac.attBuffers = append(ac.attBuffers, string(buffer))
}
err = ac.target.Accept(bytes.NewReader(partData), header, hasPlainSibling, isFirst, isLast)
return
}
}
}
err = ac.target.Accept(partReader, header, hasPlainSibling, isFirst, isLast)
return
}
func (ac AttachmentsCollector) GetAttachments() []string {
return ac.attBuffers
}
func (ac AttachmentsCollector) GetAttHeaders() []string {
return ac.attHeaders
}

View File

@ -1,231 +0,0 @@
// Copyright (c) 2020 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 pmmime
import (
"bytes"
"fmt"
"io/ioutil"
"net/mail"
"net/textproto"
"strings"
"testing"
)
func minimalParse(mimeBody string) (readBody string, plainContents string, err error) {
mm, err := mail.ReadMessage(strings.NewReader(mimeBody))
if err != nil {
return
}
h := textproto.MIMEHeader(mm.Header)
mmBodyData, err := ioutil.ReadAll(mm.Body)
if err != nil {
return
}
printAccepter := NewMIMEPrinter()
plainTextCollector := NewPlainTextCollector(printAccepter)
visitor := NewMimeVisitor(plainTextCollector)
err = VisitAll(bytes.NewReader(mmBodyData), h, visitor)
readBody = printAccepter.String()
plainContents = plainTextCollector.GetPlainText()
return readBody, plainContents, err
}
func androidParse(mimeBody string) (body, headers string, atts, attHeaders []string, err error) {
mm, err := mail.ReadMessage(strings.NewReader(mimeBody))
if err != nil {
return
}
h := textproto.MIMEHeader(mm.Header)
mmBodyData, err := ioutil.ReadAll(mm.Body)
if err != nil {
return
}
printAccepter := NewMIMEPrinter()
bodyCollector := NewBodyCollector(printAccepter)
attachmentsCollector := NewAttachmentsCollector(bodyCollector)
mimeVisitor := NewMimeVisitor(attachmentsCollector)
err = VisitAll(bytes.NewReader(mmBodyData), h, mimeVisitor)
body, _ = bodyCollector.GetBody()
headers = bodyCollector.GetHeaders()
atts = attachmentsCollector.GetAttachments()
attHeaders = attachmentsCollector.GetAttHeaders()
return
}
func TestParseBoundaryIsEmpty(t *testing.T) {
testMessage :=
`Date: Sun, 10 Mar 2019 11:10:06 -0600
In-Reply-To: <abcbase64@protonmail.com>
X-Original-To: enterprise@protonmail.com
References: <abc64@unicoderns.com> <abc63@protonmail.com> <abc64@protonmail.com> <abc65@mail.gmail.com> <abc66@protonmail.com>
To: "ProtonMail" <enterprise@protonmail.com>
X-Pm-Origin: external
Delivered-To: enterprise@protonmail.com
Content-Type: multipart/mixed; boundary=ac7e36bd45425e70b4dab2128f34172e4dc3f9ff2eeb47e909267d4252794ec7
Reply-To: XYZ <xyz@xyz.com>
Mime-Version: 1.0
Subject: Encrypted Message
Return-Path: <xyz@xyz.com>
From: XYZ <xyz@xyz.com>
X-Pm-ConversationID-Id: gNX9bDPLmBgFZ-C3Tdlb628cas1Xl0m4dql5nsWzQAEI-WQv0ytfwPR4-PWELEK0_87XuFOgetc239Y0pjPYHQ==
X-Pm-Date: Sun, 10 Mar 2019 18:10:06 +0100
Message-Id: <68c11e46-e611-d9e4-edc1-5ec96bac77cc@unicoderns.com>
X-Pm-Transfer-Encryption: TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)
X-Pm-External-Id: <68c11e46-e611-d9e4-edc1-5ec96bac77cc@unicoderns.com>
X-Pm-Internal-Id: _iJ8ETxcqXTSK8IzCn0qFpMUTwvRf-xJUtldRA1f6yHdmXjXzKleG3F_NLjZL3FvIWVHoItTxOuuVXcukwwW3g==
Openpgp: preference=signencrypt
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Thunderbird/60.4.0
X-Pm-Content-Encryption: end-to-end
--ac7e36bd45425e70b4dab2128f34172e4dc3f9ff2eeb47e909267d4252794ec7
Content-Disposition: inline
Content-Transfer-Encoding: quoted-printable
Content-Type: multipart/mixed; charset=utf-8
Content-Type: multipart/mixed; boundary="xnAIW3Turb9YQZ2rXc2ZGZH45WepHIZyy";
protected-headers="v1"
From: XYZ <xyz@xyz.com>
To: "ProtonMail" <enterprise@protonmail.com>
Subject: Encrypted Message
Message-ID: <68c11e46-e611-d9e4-edc1-5ec96bac77cc@unicoderns.com>
References: <abc64@unicoderns.com> <abc63@protonmail.com> <abc64@protonmail.com> <abc65@mail.gmail.com> <abc66@protonmail.com>
In-Reply-To: <abcbase64@protonmail.com>
--xnAIW3Turb9YQZ2rXc2ZGZH45WepHIZyy
Content-Type: text/rfc822-headers; protected-headers="v1"
Content-Disposition: inline
From: XYZ <xyz@xyz.com>
To: ProtonMail <enterprise@protonmail.com>
Subject: Re: Encrypted Message
--xnAIW3Turb9YQZ2rXc2ZGZH45WepHIZyy
Content-Type: multipart/alternative;
boundary="------------F9E5AA6D49692F51484075E3"
Content-Language: en-US
This is a multi-part message in MIME format.
--------------F9E5AA6D49692F51484075E3
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable
Hi ...
--------------F9E5AA6D49692F51484075E3
Content-Type: text/html; charset=utf-8
Content-Transfer-Encoding: quoted-printable
<html>
<head>
</head>
<body text=3D"#000000" bgcolor=3D"#FFFFFF">
<p>Hi .. </p>
</body>
</html>
--------------F9E5AA6D49692F51484075E3--
--xnAIW3Turb9YQZ2rXc2ZGZH45WepHIZyy--
--ac7e36bd45425e70b4dab2128f34172e4dc3f9ff2eeb47e909267d4252794ec7--
`
body, content, err := minimalParse(testMessage)
if err == nil {
t.Fatal("should have error but is", err)
}
t.Log("==BODY==")
t.Log(body)
t.Log("==CONTENT==")
t.Log(content)
}
func TestParse(t *testing.T) {
testMessage :=
`From: John Doe <example@example.com>
MIME-Version: 1.0
Content-Type: multipart/mixed;
boundary="XXXXboundary text"
This is a multipart message in MIME format.
--XXXXboundary text
Content-Type: text/plain; charset=utf-8
this is the body text
--XXXXboundary text
Content-Type: text/html; charset=utf-8
<html><body>this is the html body text</body></html>
--XXXXboundary text
Content-Type: text/plain; charset=utf-8
Content-Disposition: attachment;
filename="test.txt"
this is the attachment text
--XXXXboundary text--
`
body, heads, att, attHeads, err := androidParse(testMessage)
if err != nil {
t.Error("parse error", err)
}
fmt.Println("==BODY:")
fmt.Println(body)
fmt.Println("==BODY HEADERS:")
fmt.Println(heads)
fmt.Println("==ATTACHMENTS:")
fmt.Println(att)
fmt.Println("==ATTACHMENT HEADERS:")
fmt.Println(attHeads)
}
func TestParseAddressComment(t *testing.T) {
parsingExamples := map[string]string{
"": "",
"(Only Comment) here@pm.me": "\"Only Comment\" <here@pm.me>",
"Normal Name (With Comment) <here@pm.me>": "\"Normal Name\" <here@pm.me>",
"<Muhammed.(I am the greatest)Ali@(the)Vegas.WBA>": "\"I am the greatest the\" <Muhammed.Ali@Vegas.WBA>",
}
for raw, expected := range parsingExamples {
parsed := parseAddressComment(raw)
if expected != parsed {
t.Errorf("When parsing %q expected %q but have %q", raw, expected, parsed)
}
}
}

View File

@ -32,7 +32,7 @@ var (
testInput = []interface{}{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
wantOutput = []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
testProcessSleep = 100 // ms
runParallelTimeOverhead = 100 // ms
runParallelTimeOverhead = 150 // ms
)
func TestParallel(t *testing.T) {

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