1
0

Import/Export GUI

This commit is contained in:
Pavel Škoda
2020-06-23 15:35:54 +02:00
committed by Michal Horejsek
parent 1c10cc5065
commit 7e5e3d3dd4
50 changed files with 1793 additions and 692 deletions

View File

@ -94,9 +94,9 @@ build-ie-linux:
script: script:
- make build-ie - make build-ie
artifacts: artifacts:
name: "bridge-linux-$CI_COMMIT_SHORT_SHA" name: "ie-linux-$CI_COMMIT_SHORT_SHA"
paths: paths:
- bridge_*.tgz - ie_*.tgz
expire_in: 2 week expire_in: 2 week
build-darwin: build-darwin:
@ -145,9 +145,9 @@ build-ie-darwin:
script: script:
- make build-ie - make build-ie
artifacts: artifacts:
name: "bridge-darwin-$CI_COMMIT_SHORT_SHA" name: "ie-darwin-$CI_COMMIT_SHORT_SHA"
paths: paths:
- bridge_*.tgz - ie_*.tgz
expire_in: 2 week expire_in: 2 week
build-windows: build-windows:
@ -189,9 +189,9 @@ build-ie-windows:
- go mod download - go mod download
- TARGET_OS=windows make build-ie - TARGET_OS=windows make build-ie
artifacts: artifacts:
name: "bridge-windows-$CI_COMMIT_SHORT_SHA" name: "ie-windows-$CI_COMMIT_SHORT_SHA"
paths: paths:
- bridge_*.tgz - ie_*.tgz
expire_in: 2 week expire_in: 2 week
# Stage: MIRROR # Stage: MIRROR

View File

@ -47,6 +47,8 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
* GODT-554 Detect and notify about "bad certificate" IMAP TLS error. * GODT-554 Detect and notify about "bad certificate" IMAP TLS error.
* IMAP mailbox info update when new mailbox is created. * IMAP mailbox info update when new mailbox is created.
* GODT-72 Use ISO-8859-1 encoding if charset is not specified and it isn't UTF-8. * GODT-72 Use ISO-8859-1 encoding if charset is not specified and it isn't UTF-8.
* Structure for transfer rules in QML
* GODT-360 Detect charset embedded in html/xml.
### Changed ### Changed
* GODT-360 Detect charset embedded in html/xml. * GODT-360 Detect charset embedded in html/xml.

2
go.mod
View File

@ -46,6 +46,7 @@ require (
github.com/golang/mock v1.4.4 github.com/golang/mock v1.4.4
github.com/google/go-cmp v0.5.1 github.com/google/go-cmp v0.5.1
github.com/google/uuid v1.1.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/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c // indirect
github.com/hashicorp/go-multierror v1.1.0 github.com/hashicorp/go-multierror v1.1.0
github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7 github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7
@ -59,6 +60,7 @@ require (
github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce
github.com/olekukonko/tablewriter v0.0.4 // indirect github.com/olekukonko/tablewriter v0.0.4 // indirect
github.com/pkg/errors v0.9.1 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/sirupsen/logrus v1.6.0
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
github.com/stretchr/testify v1.6.1 github.com/stretchr/testify v1.6.1

42
go.sum
View File

@ -39,6 +39,9 @@ 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/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 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 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 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/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= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
@ -81,6 +84,10 @@ 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/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 h1:JOOeAvjSlapTT92p8xiS19Zxev1neGikoHsXJeOq8So=
github.com/go-resty/resty/v2 v2.3.0/go.mod h1:UpN9CgLZNsv4e9XG50UU8xdI0F43UQ4HmxLBDwaroHU= 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 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw=
github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= 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 h1:aBzukfDxQlCTVS0NBUjI5YA3iVeaZ9Tb5PxNrrIP1xs=
@ -91,6 +98,11 @@ 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/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 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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-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 h1:7lF+Vz0LqiRidnzC1Oq86fpX1q/iEv2KJdrCtttYjT4=
github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
@ -100,6 +112,10 @@ 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/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 h1:suxlO4AC4E4bjueAsL0m+qp8kmkxRWMGj+5bBU/KJ8g=
github.com/jameshoulahan/go-imap v0.0.0-20200728140727-d57327f48843/go.mod h1:yKASt+C3ZiDAiCSssxg9caIckWF/JG7ZQTO7GAmvicU= 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-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 h1:g0fAGBisHaEQ0TRq1iBvemFRf+8AEWEmBESSiWB3Vsc=
github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk= github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
@ -121,10 +137,14 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 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 h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8=
github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= 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 h1:eYsumTah144C0A8P1T/AVSUk5ZoLnhfYFM3OGQxB52A=
github.com/martinlindhe/base36 v1.0.0/go.mod h1:+AtEs8xrBpCeYgSLoY/aJ6Wf37jtBuR0s35750M27+8= 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 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 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.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 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
@ -135,6 +155,8 @@ github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/Qd
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 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 h1:Qww6FseFn8PRfw07jueqIXqodm0JKiiKuK0DeXSqfyo=
github.com/miekg/dns v1.1.30/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= 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 h1:xCBmH4l5KuvLYc5L7AS7SZg9/jKdIFubM7OVoLqaQUI=
github.com/myesui/uuid v1.0.0/go.mod h1:2CDfNgU0LR8mIdO8vdWd8i9gWWxLlcoIGGpSNgafq84= 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 h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
@ -145,11 +167,16 @@ github.com/olekukonko/tablewriter v0.0.1 h1:b3iUnf1v+ppJiOfNX4yxxqfWKMQPZR5yoh8u
github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= 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 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8=
github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA= 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.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 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 h1:NugYot0LIVPxTvN8n+Kvkn6TrbMyxQiuvKdEwFdR9vI=
@ -163,6 +190,8 @@ github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= 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 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= 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 h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM= 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= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@ -192,6 +221,12 @@ github.com/urfave/cli v1.22.4 h1:u7tSpNPPswAFymm8IehJhy4uJMlUuU/GmqSkvJ1InXA=
github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= 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 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0=
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= 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/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-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-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@ -211,6 +246,7 @@ 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-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-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-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-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 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -226,8 +262,10 @@ 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-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-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-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 h1:VvQyQJN0tSuecqgcIxMWnnfG5kSmgy9KZR9sW3W5QeA=
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 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-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 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@ -237,8 +275,12 @@ gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8X
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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 h1:yhQC6Uy5CqibAIlk1wlusa/MJ3iAN49/BsR/dCCKz3M=
gopkg.in/stretchr/testify.v1 v1.2.2/go.mod h1:QI5V/q6UbPmuhtm10CaFZxED9NreB8PnFYN9JcR6TxU= 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 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 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.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 h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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

@ -25,6 +25,7 @@ import (
"github.com/ProtonMail/proton-bridge/internal/metrics" "github.com/ProtonMail/proton-bridge/internal/metrics"
"github.com/ProtonMail/proton-bridge/internal/preferences" "github.com/ProtonMail/proton-bridge/internal/preferences"
"github.com/ProtonMail/proton-bridge/internal/users" "github.com/ProtonMail/proton-bridge/internal/users"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/ProtonMail/proton-bridge/pkg/listener" "github.com/ProtonMail/proton-bridge/pkg/listener"
logrus "github.com/sirupsen/logrus" logrus "github.com/sirupsen/logrus"
@ -130,15 +131,17 @@ func (b *Bridge) ReportBug(osType, osVersion, description, accountName, address,
defer c.Logout() defer c.Logout()
title := "[Bridge] Bug" title := "[Bridge] Bug"
if err := c.ReportBugWithEmailClient( report := pmapi.ReportReq{
osType, OS: osType,
osVersion, OSVersion: osVersion,
title, Browser: emailClient,
description, Title: title,
accountName, Description: description,
address, Username: accountName,
emailClient, Email: address,
); err != nil { }
if err := c.Report(report); err != nil {
log.Error("Reporting bug failed: ", err) log.Error("Reporting bug failed: ", err)
return err return err
} }

View File

@ -15,7 +15,7 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// Code generated by ./release-notes.sh at Wed 29 Jul 2020 07:07:28 AM CEST. DO NOT EDIT. // Code generated by ./release-notes.sh at 'Fri 07 Aug 2020 06:34:27 AM CEST'. DO NOT EDIT.
package bridge package bridge

View File

@ -38,7 +38,7 @@ Item {
property var allMonths : getMonthList(1,12) property var allMonths : getMonthList(1,12)
property var allDays : getDayList(1,31) property var allDays : getDayList(1,31)
property var enums : JSON.parse('{"pathOK":1,"pathEmptyPath":2,"pathWrongPath":4,"pathNotADir":8,"pathWrongPermissions":16,"pathDirEmpty":32,"errUnknownError":0,"errEventAPILogout":1,"errUpdateAPI":2,"errUpdateJSON":3,"errUserAuth":4,"errQApplication":18,"errEmailExportFailed":6,"errEmailExportMissing":7,"errNothingToImport":8,"errEmailImportFailed":12,"errDraftImportFailed":13,"errDraftLabelFailed":14,"errEncryptMessageAttachment":15,"errEncryptMessage":16,"errNoInternetWhileImport":17,"errUnlockUser":5,"errSourceMessageNotSelected":19,"errCannotParseMail":5000,"errWrongLoginOrPassword":5001,"errWrongServerPathOrPort":5002,"errWrongAuthMethod":5003,"errIMAPFetchFailed":5004,"errLocalSourceLoadFailed":1000,"errPMLoadFailed":1001,"errRemoteSourceLoadFailed":1002,"errLoadAccountList":1005,"errExit":1006,"errRetry":1007,"errAsk":1008,"errImportFailed":1009,"errCreateLabelFailed":1010,"errCreateFolderFailed":1011,"errUpdateLabelFailed":1012,"errUpdateFolderFailed":1013,"errFillFolderName":1014,"errSelectFolderColor":1015,"errNoInternet":1016,"folderTypeSystem":"","folderTypeLabel":"label","folderTypeFolder":"folder","folderTypeExternal":"external","progressInit":"init","progressLooping":"looping","statusNoInternet":"noInternet","statusCheckingInternet":"internetCheck","statusNewVersionAvailable":"oldVersion","statusUpToDate":"upToDate","statusForceUpdate":"forceupdate"}') property var enums : JSON.parse('{"pathOK":1,"pathEmptyPath":2,"pathWrongPath":4,"pathNotADir":8,"pathWrongPermissions":16,"pathDirEmpty":32,"errUnknownError":0,"errEventAPILogout":1,"errUpdateAPI":2,"errUpdateJSON":3,"errUserAuth":4,"errQApplication":18,"errEmailExportFailed":6,"errEmailExportMissing":7,"errNothingToImport":8,"errEmailImportFailed":12,"errDraftImportFailed":13,"errDraftLabelFailed":14,"errEncryptMessageAttachment":15,"errEncryptMessage":16,"errNoInternetWhileImport":17,"errUnlockUser":5,"errSourceMessageNotSelected":19,"errCannotParseMail":5000,"errWrongLoginOrPassword":5001,"errWrongServerPathOrPort":5002,"errWrongAuthMethod":5003,"errIMAPFetchFailed":5004,"errLocalSourceLoadFailed":1000,"errPMLoadFailed":1001,"errRemoteSourceLoadFailed":1002,"errLoadAccountList":1005,"errExit":1006,"errRetry":1007,"errAsk":1008,"errImportFailed":1009,"errCreateLabelFailed":1010,"errCreateFolderFailed":1011,"errUpdateLabelFailed":1012,"errUpdateFolderFailed":1013,"errFillFolderName":1014,"errSelectFolderColor":1015,"errNoInternet":1016,"folderTypeSystem":"system","folderTypeLabel":"label","folderTypeFolder":"folder","folderTypeExternal":"external","progressInit":"init","progressLooping":"looping","statusNoInternet":"noInternet","statusCheckingInternet":"internetCheck","statusNewVersionAvailable":"oldVersion","statusUpToDate":"upToDate","statusForceUpdate":"forceupdate"}')
IEStyle{} IEStyle{}
@ -396,7 +396,7 @@ Item {
onTriggered : go.runCheckVersion(false) onTriggered : go.runCheckVersion(false)
} }
property string areYouSureYouWantToQuit : qsTr("Tool does not finished all the jobs. Do you really want to quit?") property string areYouSureYouWantToQuit : qsTr("There are incomplete processes - some items are not yet transferred. Do you really want to stop and quit?")
// On start // On start
Component.onCompleted : { Component.onCompleted : {
// set spell messages // set spell messages

View File

@ -25,14 +25,15 @@ import ImportExportUI 1.0
Column { Column {
id: dateRange id: dateRange
property var structure : structureExternal property var structure : transferRules
property string sourceID : structureExternal.getID ( -1 ) property string sourceID : "-1"
property alias allDates : allDatesBox.checked property alias allDates : allDatesBox.checked
property alias inputDateFrom : inputDateFrom property alias inputDateFrom : inputDateFrom
property alias inputDateTo : inputDateTo property alias inputDateTo : inputDateTo
function setRange() {common.setRange()} function getRange() {common.getRange()}
function setRangeFromTo(from, to) {common.setRangeFromTo(from, to)}
function applyRange() {common.applyRange()} function applyRange() {common.applyRange()}
property var dropDownStyle : Style.dropDownLight property var dropDownStyle : Style.dropDownLight

View File

@ -34,7 +34,7 @@ Item {
property alias inputDateFrom : inputDateFrom property alias inputDateFrom : inputDateFrom
property alias inputDateTo : inputDateTo property alias inputDateTo : inputDateTo
function setRange() {common.setRange()} function getRange() {common.getRange()}
function applyRange() {common.applyRange()} function applyRange() {common.applyRange()}
*/ */
@ -43,11 +43,7 @@ Item {
inputDateTo.setDate((new Date()).getTime()) inputDateTo.setDate((new Date()).getTime())
} }
function setRange(){ // unix time in seconds function setRangeFromTo(folderFrom, folderTo){ // unix time in seconds
var folderFrom = dateRange.structure.getFrom(dateRange.sourceID)
if (folderFrom===undefined) folderFrom = 0
var folderTo = dateRange.structure.getTo(dateRange.sourceID)
if (folderTo===undefined) folderTo = 0
if ( folderFrom == 0 && folderTo ==0 ) { if ( folderFrom == 0 && folderTo ==0 ) {
dateRange.allDates = true dateRange.allDates = true
} else { } else {
@ -57,6 +53,15 @@ Item {
} }
} }
function getRange(){ // unix time in seconds
//console.log(" ==== GET RANGE === ")
//console.trace()
var folderFrom = dateRange.structure.globalFromDate
var folderTo = dateRange.structure.globalToDate
root.setRangeFromTo(folderFrom, folderTo)
}
function applyRange(){ // unix time is seconds function applyRange(){ // unix time is seconds
if (dateRange.allDates) structure.setFromToDate(dateRange.sourceID, 0, 0) if (dateRange.allDates) structure.setFromToDate(dateRange.sourceID, 0, 0)
else { else {
@ -67,15 +72,10 @@ Item {
} }
} }
Connections {
target: dateRange
onStructureChanged: setRange()
}
Component.onCompleted: { Component.onCompleted: {
inputDateFrom.updateRange(gui.netBday) inputDateFrom.updateRange(gui.netBday)
inputDateTo.updateRange(new Date()) inputDateTo.updateRange(new Date())
setRange() //getRange()
} }
} }

View File

@ -31,8 +31,10 @@ Rectangle {
property real padding : Style.dialog.spacing property real padding : Style.dialog.spacing
property bool down : popup.visible property bool down : popup.visible
property var structure : structureExternal property var structure : transferRules
property string sourceID : structureExternal.getID(-1) property string sourceID : ""
property int sourceFromDate : 0
property int sourceToDate : 0
color: Style.transparent color: Style.transparent
@ -145,7 +147,17 @@ Rectangle {
} }
} }
onAboutToShow : dateRangeInput.setRange() onAboutToShow : updateRange()
onAboutToHide : dateRangeInput.applyRange() onAboutToHide : dateRangeInput.applyRange()
} }
function updateRange() {
dateRangeInput.setRangeFromTo(root.sourceFromDate, root.sourceToDate)
}
Connections {
target:root
onSourceFromDateChanged: root.updateRange()
onSourceToDateChanged: root.updateRange()
}
} }

View File

@ -91,8 +91,6 @@ Dialog {
DateRange{ DateRange{
id: dateRangeInput id: dateRangeInput
structure: structurePM
sourceID: structurePM.getID(-1)
} }
OutputFormat { OutputFormat {
@ -142,7 +140,7 @@ Dialog {
id: buttonNext id: buttonNext
fa_icon: Style.fa.check fa_icon: Style.fa.check
text: qsTr("Export","todo") text: qsTr("Export","todo")
enabled: structurePM != 0 enabled: transferRules != 0
color_main: Style.dialog.background color_main: Style.dialog.background
color_minor: enabled ? Style.dialog.textBlue : Style.main.textDisabled color_minor: enabled ? Style.dialog.textBlue : Style.main.textDisabled
isOpaque: true isOpaque: true
@ -168,13 +166,17 @@ Dialog {
spacing: Style.main.rightMargin spacing: Style.main.rightMargin
AccessibleText { AccessibleText {
id: statusLabel id: statusLabel
text : qsTr("Exporting to:") text : qsTr("Status:")
font.pointSize: Style.main.iconSize * Style.pt font.pointSize: Style.main.iconSize * Style.pt
color : Style.main.text color : Style.main.text
} }
AccessibleText { AccessibleText {
anchors.baseline: statusLabel.baseline anchors.baseline: statusLabel.baseline
text : go.progressDescription == gui.enums.progressInit ? outputPathInput.path : go.progressDescription text : {
if (progressbarExport.isFinished) return qsTr("finished")
if (go.progressDescription == "") return qsTr("exporting")
return go.progressDescription
}
elide: Text.ElideMiddle elide: Text.ElideMiddle
width: progressbarExport.width - parent.spacing - statusLabel.width width: progressbarExport.width - parent.spacing - statusLabel.width
font.pointSize: Style.dialog.textSize * Style.pt font.pointSize: Style.dialog.textSize * Style.pt
@ -310,15 +312,17 @@ Dialog {
function check_inputs() { function check_inputs() {
if (currentIndex == 1) { if (currentIndex == 1) {
// at least one email to export // at least one email to export
if (structurePM.rowCount() == 0){ if (transferRules.rowCount() == 0){
errorPopup.show(qsTr("No emails found to export. Please try another address.", "todo")) errorPopup.show(qsTr("No emails found to export. Please try another address.", "todo"))
return false return false
} }
// at least one source selected // at least one source selected
if (!structurePM.atLeastOneSelected) { /*
errorPopup.show(qsTr("Please select at least one item to export.", "todo")) if (!transferRules.atLeastOneSelected) {
return false errorPopup.show(qsTr("Please select at least one item to export.", "todo"))
} return false
}
*/
// check path // check path
var folderCheck = go.checkPathStatus(outputPathInput.path) var folderCheck = go.checkPathStatus(outputPathInput.path)
switch (folderCheck) { switch (folderCheck) {
@ -364,7 +368,6 @@ Dialog {
errorPopup.buttonYes.visible = true errorPopup.buttonYes.visible = true
errorPopup.buttonNo.visible = true errorPopup.buttonNo.visible = true
errorPopup.buttonOkay.visible = false errorPopup.buttonOkay.visible = false
errorPopup.checkbox.text = root.msgClearUnfished
errorPopup.show ("Are you sure you want to cancel this export?") errorPopup.show ("Are you sure you want to cancel this export?")
} }
@ -374,10 +377,7 @@ Dialog {
case 0 : case 0 :
case 1 : root.hide(); break; case 1 : root.hide(); break;
case 2 : // progress bar case 2 : // progress bar
go.cancelProcess ( go.cancelProcess();
errorPopup.checkbox.text == root.msgClearUnfished &&
errorPopup.checkbox.checked
);
// no break // no break
default: default:
root.clear_status() root.clear_status()
@ -395,7 +395,7 @@ Dialog {
root.hide() root.hide()
break break
case 0: // loading structure case 0: // loading structure
dateRangeInput.setRange() dateRangeInput.getRange()
//no break //no break
default: default:
incrementCurrentIndex() incrementCurrentIndex()
@ -426,7 +426,7 @@ Dialog {
switch (currentIndex) { switch (currentIndex) {
case 0: case 0:
go.loadStructureForExport(root.address) go.loadStructureForExport(root.address)
sourceFoldersInput.hasItems = (structurePM.rowCount() > 0) sourceFoldersInput.hasItems = (transferRules.rowCount() > 0)
break break
case 2: case 2:
dateRangeInput.applyRange() dateRangeInput.applyRange()

View File

@ -327,6 +327,7 @@ Dialog {
iconText: Style.fa.refresh iconText: Style.fa.refresh
textColor: Style.main.textBlue textColor: Style.main.textBlue
onClicked: { onClicked: {
go.resetSource()
root.decrementCurrentIndex() root.decrementCurrentIndex()
timer.start() timer.start()
} }
@ -408,20 +409,13 @@ Dialog {
spacing: Style.main.rightMargin spacing: Style.main.rightMargin
AccessibleText { AccessibleText {
id: statusLabel id: statusLabel
text : qsTr("Importing from:") text : qsTr("Status:")
font.pointSize: Style.main.iconSize * Style.pt font.pointSize: Style.main.iconSize * Style.pt
color : Style.main.text color : Style.main.text
} }
AccessibleText { AccessibleText {
anchors.baseline: statusLabel.baseline anchors.baseline: statusLabel.baseline
text : { text : go.progressDescription == "" ? qsTr("importing") : go.progressDescription
var sourceFolder = root.isFromFile ? root.inputPath : inputEmail.text
if (go.progressDescription != gui.enums.progressInit && go.progress!=0) {
sourceFolder += "/"
sourceFolder += go.progressDescription
}
return sourceFolder
}
elide: Text.ElideMiddle elide: Text.ElideMiddle
width: progressbarImport.width - parent.spacing - statusLabel.width width: progressbarImport.width - parent.spacing - statusLabel.width
font.pointSize: Style.dialog.textSize * Style.pt font.pointSize: Style.dialog.textSize * Style.pt
@ -582,9 +576,9 @@ Dialog {
spacing : Style.dialog.heightSeparator spacing : Style.dialog.heightSeparator
Text { Text {
text: Style.fa.check_circle + " " + qsTr("Import completed successfully") text: go.progressDescription!="" ? qsTr("Import failed: %1").arg(go.progressDescription) : Style.fa.check_circle + " " + qsTr("Import completed successfully")
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
color: Style.main.textGreen color: go.progressDescription!="" ? Style.main.textRed : Style.main.textGreen
font.bold : true font.bold : true
font.family: Style.fontawesome.name font.family: Style.fontawesome.name
} }
@ -605,11 +599,7 @@ Dialog {
text : qsTr("View errors") text : qsTr("View errors")
color_main : Style.dialog.textBlue color_main : Style.dialog.textBlue
onClicked : { onClicked : {
if (go.importLogFileName=="") { go.loadImportReports()
console.log("onViewErrors: missing import log file name")
return
}
go.loadImportReports(go.importLogFileName)
reportList.show() reportList.show()
} }
} }
@ -619,10 +609,6 @@ Dialog {
text : qsTr("Report files") text : qsTr("Report files")
color_main : Style.dialog.textBlue color_main : Style.dialog.textBlue
onClicked : { onClicked : {
if (go.importLogFileName=="") {
console.log("onReportError: missing import log file name")
return
}
root.ask_send_report() root.ask_send_report()
} }
} }
@ -755,7 +741,6 @@ Dialog {
} }
function clear() { function clear() {
go.resetSource()
root.inputPath = "" root.inputPath = ""
clear_status() clear_status()
inputEmail.clear() inputEmail.clear()
@ -781,7 +766,7 @@ Dialog {
onClickedYes : { onClickedYes : {
if (errorPopup.msgID == "ask_send_report") { if (errorPopup.msgID == "ask_send_report") {
errorPopup.hide() errorPopup.hide()
root.report_sent(go.sendImportReport(root.address,go.importLogFileName)) root.report_sent(go.sendImportReport(root.address))
return return
} }
root.cancel() root.cancel()
@ -857,10 +842,13 @@ Dialog {
} }
break break
case 3: // import insturctions case 3: // import insturctions
if (!structureExternal.hasTarget()) { /*
errorPopup.show(qsTr("Nothing selected for import.")) console.log(" ====== TODO ======== ")
return false if (!structureExternal.hasTarget()) {
} errorPopup.show(qsTr("Nothing selected for import."))
return false
}
*/
break break
case 4: // import status case 4: // import status
} }
@ -880,7 +868,7 @@ Dialog {
root.hide() root.hide()
break break
case DialogImport.Page.Progress: case DialogImport.Page.Progress:
go.cancelProcess(false) go.cancelProcess()
root.currentIndex=3 root.currentIndex=3
root.clear_status() root.clear_status()
globalLabels.reset() globalLabels.reset()
@ -905,7 +893,7 @@ Dialog {
globalLabels.labelName, globalLabels.labelName,
globalLabels.labelColor, globalLabels.labelColor,
true, true,
structureExternal.getID(-1) "-1"
) )
if (!isOK) return if (!isOK) return
} }
@ -919,7 +907,8 @@ Dialog {
case DialogImport.Page.LoadingStructure: case DialogImport.Page.LoadingStructure:
globalLabels.reset() globalLabels.reset()
importInstructions.hasItems = (structureExternal.rowCount() > 0) // TODO_: importInstructions.hasItems = (structureExternal.rowCount() > 0)
importInstructions.hasItems = true
case DialogImport.Page.ImapSource: case DialogImport.Page.ImapSource:
default: default:
incrementCurrentIndex() incrementCurrentIndex()
@ -1008,7 +997,7 @@ Dialog {
case DialogImport.Page.SelectSourceType: case DialogImport.Page.SelectSourceType:
case DialogImport.Page.ImapSource: case DialogImport.Page.ImapSource:
case DialogImport.Page.SourceToTarget: case DialogImport.Page.SourceToTarget:
globalDateRange.setRange() globalDateRange.getRange()
break break
case DialogImport.Page.LoadingStructure: case DialogImport.Page.LoadingStructure:
go.setupAndLoadForImport( go.setupAndLoadForImport(

View File

@ -92,7 +92,7 @@ Rectangle {
clip : true clip : true
orientation : ListView.Vertical orientation : ListView.Vertical
boundsBehavior : Flickable.StopAtBounds boundsBehavior : Flickable.StopAtBounds
model : structurePM model : transferRules
cacheBuffer : 10000 cacheBuffer : 10000
anchors { anchors {
@ -125,27 +125,25 @@ Rectangle {
} }
delegate: FolderRowButton { delegate: FolderRowButton {
property variant modelData: model
width : root.width - 5*root.border.width width : root.width - 5*root.border.width
type : folderType type : modelData.type
color : folderColor folderIconColor : modelData.iconColor
title : folderName title : modelData.name
isSelected : isFolderSelected isSelected : modelData.isActive
onClicked : { onClicked : {
//console.log("Clicked", folderId, isSelected) //console.log("Clicked", folderId, isSelected)
structurePM.setFolderSelection(folderId,!isSelected) transferRules.setIsRuleActive(modelData.mboxID,!model.isActive)
} }
} }
section.property: "folderType" section.property: "type"
section.delegate: FolderRowButton { section.delegate: FolderRowButton {
isSection : true isSection : true
width : root.width - 5*root.border.width width : root.width - 5*root.border.width
title : gui.folderTypeTitle(section) title : gui.folderTypeTitle(section)
isSelected : { isSelected : section == gui.enums.folderTypeLabel ? transferRules.isLabelGroupSelected : transferRules.isFolderGroupSelected
//console.log("section selected changed: ", section) onClicked : transferRules.setIsGroupActive(section,!isSelected)
return section == gui.enums.folderTypeLabel ? structurePM.selectedLabels : structurePM.selectedFolders
}
onClicked : structurePM.selectType(section,!isSelected)
} }
} }
} }

View File

@ -26,9 +26,9 @@ AccessibleButton {
property bool isSection : false property bool isSection : false
property bool isSelected : false property bool isSelected : false
property string title : "N/A" property string title : "N/A"
property string type : "" property string type : ""
property color color : "black" property string folderIconColor : Style.main.textBlue
height : Style.exporting.rowHeight height : Style.exporting.rowHeight
padding : 0.0 padding : 0.0
@ -72,7 +72,7 @@ AccessibleButton {
left : checkbox.left left : checkbox.left
leftMargin : Style.dialog.fontSize + Style.exporting.leftMargin leftMargin : Style.dialog.fontSize + Style.exporting.leftMargin
} }
color : root.type==gui.enums.folderTypeSystem ? Style.main.textBlue : root.color color : root.type=="" ? Style.main.textBlue : root.folderIconColor
font { font {
family : Style.fontawesome.name family : Style.fontawesome.name
pointSize : Style.dialog.fontSize * Style.pt pointSize : Style.dialog.fontSize * Style.pt

View File

@ -39,7 +39,7 @@ Rectangle {
} }
property real iconWidth : nameWidth*0.3 property real iconWidth : nameWidth*0.3
property bool isSourceSelected: targetFolderID!="" property bool isSourceSelected: isActive
property string lastTargetFolder: "6" // Archive property string lastTargetFolder: "6" // Archive
property string lastTargetLabels: "" // no flag by default property string lastTargetLabels: "" // no flag by default
@ -71,7 +71,7 @@ Rectangle {
Text { Text {
id: folderIcon id: folderIcon
text : gui.folderIcon(folderName, gui.enums.folderTypeFolder) text : gui.folderIcon(name, gui.enums.folderTypeFolder)
anchors.verticalCenter : parent.verticalCenter anchors.verticalCenter : parent.verticalCenter
color: root.isSourceSelected ? Style.main.text : Style.main.textDisabled color: root.isSourceSelected ? Style.main.text : Style.main.textDisabled
font { font {
@ -81,7 +81,7 @@ Rectangle {
} }
Text { Text {
text : folderName text : name
width: nameWidth width: nameWidth
elide: Text.ElideRight elide: Text.ElideRight
anchors.verticalCenter : parent.verticalCenter anchors.verticalCenter : parent.verticalCenter
@ -102,24 +102,27 @@ Rectangle {
SelectFolderMenu { SelectFolderMenu {
id: selectFolder id: selectFolder
sourceID: folderId sourceID: mboxID
selectedIDs: targetFolderID targets: transferRules.targetFolders(mboxID)
width: nameWidth width: nameWidth
anchors.verticalCenter : parent.verticalCenter anchors.verticalCenter : parent.verticalCenter
enabled: root.isSourceSelected
onDoNotImport: root.toggleImport() onDoNotImport: root.toggleImport()
onImportToFolder: root.importToFolder(newTargetID) onImportToFolder: root.importToFolder(newTargetID)
} }
SelectLabelsMenu { SelectLabelsMenu {
sourceID: folderId sourceID: mboxID
selectedIDs: targetLabelIDs targets: transferRules.targetLabels(mboxID)
width: nameWidth width: nameWidth
anchors.verticalCenter : parent.verticalCenter anchors.verticalCenter : parent.verticalCenter
enabled: root.isSourceSelected enabled: root.isSourceSelected
onAddTargetLabel: { transferRules.addTargetID(sourceID, newTargetID) }
onRemoveTargetLabel: { transferRules.removeTargetID(sourceID, newTargetID) }
} }
LabelIconList { LabelIconList {
selectedIDs: targetLabelIDs colorList: labelColors=="" ? [] : labelColors.split(";")
width: iconWidth width: iconWidth
anchors.verticalCenter : parent.verticalCenter anchors.verticalCenter : parent.verticalCenter
enabled: root.isSourceSelected enabled: root.isSourceSelected
@ -127,38 +130,23 @@ Rectangle {
DateRangeMenu { DateRangeMenu {
id: dateRangeMenu id: dateRangeMenu
sourceID: folderId sourceID: mboxID
sourceFromDate: fromDate
sourceToDate: toDate
enabled: root.isSourceSelected enabled: root.isSourceSelected
anchors.verticalCenter : parent.verticalCenter anchors.verticalCenter : parent.verticalCenter
Component.onCompleted : dateRangeMenu.updateRange()
} }
} }
function importToFolder(newTargetID) { function importToFolder(newTargetID) {
if (root.isSourceSelected) { transferRules.addTargetID(mboxID,newTargetID)
structureExternal.setTargetFolderID(folderId,newTargetID)
} else {
lastTargetFolder = newTargetID
toggleImport()
}
} }
function toggleImport() { function toggleImport() {
if (root.isSourceSelected) { transferRules.setIsRuleActive(mboxID, !root.isSourceSelected)
lastTargetFolder = targetFolderID
lastTargetLabels = targetLabelIDs
structureExternal.setTargetFolderID(folderId,"")
return Qt.Unchecked
} else {
structureExternal.setTargetFolderID(folderId,lastTargetFolder)
var labelsSplit = lastTargetLabels.split(";")
for (var labelIndex in labelsSplit) {
var labelID = labelsSplit[labelIndex]
structureExternal.addTargetLabelID(folderId,labelID)
}
return Qt.Checked
}
} }
} }

View File

@ -50,7 +50,6 @@ Rectangle {
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
text: qsTr("No emails found for this source.","todo") text: qsTr("No emails found for this source.","todo")
} }
} }
anchors { anchors {
@ -70,7 +69,7 @@ Rectangle {
clip : true clip : true
orientation : ListView.Vertical orientation : ListView.Vertical
boundsBehavior : Flickable.StopAtBounds boundsBehavior : Flickable.StopAtBounds
model : structureExternal model : transferRules
cacheBuffer : 10000 cacheBuffer : 10000
delegate : ImportDelegate { delegate : ImportDelegate {
width: root.width width: root.width

View File

@ -25,8 +25,8 @@ import ImportExportUI 1.0
Row { Row {
id: dateRange id: dateRange
property var structure : structureExternal property var structure : transferRules
property string sourceID : structureExternal.getID ( -1 ) property string sourceID : "-1"
property alias allDates : allDatesBox.checked property alias allDates : allDatesBox.checked
property alias inputDateFrom : inputDateFrom property alias inputDateFrom : inputDateFrom
@ -34,7 +34,7 @@ Row {
property alias labelWidth: label.width property alias labelWidth: label.width
function setRange() {common.setRange()} function getRange() {common.getRange()}
function applyRange() {common.applyRange()} function applyRange() {common.applyRange()}
DateRangeFunctions {id:common} DateRangeFunctions {id:common}

View File

@ -26,42 +26,16 @@ Rectangle {
id: root id: root
width: Style.main.fontSize * 2 width: Style.main.fontSize * 2
height: metrics.height height: metrics.height
property string selectedIDs : "" property var colorList
color: "transparent" color: "transparent"
DelegateModel { DelegateModel {
id: selectedLabels id: selectedLabels
filterOnGroup: "selected" model : colorList
groups: DelegateModelGroup {
id: selected
name: "selected"
includeByDefault: true
}
model : structurePM
delegate : Text { delegate : Text {
text : metrics.text text : metrics.text
font : metrics.font font : metrics.font
color : folderColor===undefined ? "#000": folderColor color : modelData
}
}
function updateFilter() {
var selected = root.selectedIDs.split(";")
var rowCount = selectedLabels.items.count
//console.log(" log ::", root.selectedIDs, rowCount, selectedLabels.model)
// filter
for (var iItem = 0; iItem < rowCount; iItem++) {
var entry = selectedLabels.items.get(iItem);
//console.log(" log filter ", iItem, rowCount, entry.model.folderId, entry.model.folderType, selected[iSel], entry.inSelected )
for (var iSel in selected) {
entry.inSelected = (
entry.model.folderType == gui.enums.folderTypeLabel &&
entry.model.folderId == selected[iSel]
)
if (entry.inSelected) break // found match, skip rest
}
} }
} }
@ -77,7 +51,7 @@ Rectangle {
Row { Row {
anchors.left : root.left anchors.left : root.left
spacing : { spacing : {
var n = Math.max(2,selectedLabels.count) var n = Math.max(2,root.colorList.length)
var tagWidth = Math.max(1.0,metrics.width) var tagWidth = Math.max(1.0,metrics.width)
var space = Math.min(1*Style.px, (root.width - n*tagWidth)/(n-1)) // not more than 1px var space = Math.min(1*Style.px, (root.width - n*tagWidth)/(n-1)) // not more than 1px
space = Math.max(space,-tagWidth) // not less than tag width space = Math.max(space,-tagWidth) // not less than tag width
@ -88,9 +62,4 @@ Rectangle {
model: selectedLabels model: selectedLabels
} }
} }
Component.onCompleted: root.updateFilter()
onSelectedIDsChanged: root.updateFilter()
Connections { target: structurePM; onDataChanged:root.updateFilter() }
} }

View File

@ -26,19 +26,19 @@ ComboBox {
//fixme rounded //fixme rounded
height: Style.main.fontSize*2 //fixme height: Style.main.fontSize*2 //fixme
property string folderType: gui.enums.folderTypeFolder property string folderType: gui.enums.folderTypeFolder
property string selectedIDs
property string sourceID property string sourceID
property var targets
property bool isFolderType: root.folderType == gui.enums.folderTypeFolder property bool isFolderType: root.folderType == gui.enums.folderTypeFolder
property bool hasTarget: root.selectedIDs != ""
property bool below: true property bool below: true
signal doNotImport() signal doNotImport()
signal importToFolder(string newTargetID) signal importToFolder(string newTargetID)
signal addTargetLabel(string newTargetID)
signal removeTargetLabel(string newTargetID)
leftPadding: Style.dialog.spacing leftPadding: Style.dialog.spacing
onDownChanged : { onDownChanged : {
if (root.down) view.model.updateFilter()
root.below = popup.y>0 root.below = popup.y>0
} }
@ -58,30 +58,22 @@ ComboBox {
} }
displayText: { displayText: {
//console.trace() console.log("Target Menu current", view.currentItem, view.currentIndex)
//console.log("updatebox", view.currentIndex, root.hasTarget, root.selectedIDs, root.sourceID, root.folderType) if (view.currentIndex >= 0) {
if (!root.hasTarget) { if (!root.isFolderType) return Style.fa.tags + " " + qsTr("Add/Remove labels")
if (root.isFolderType) return qsTr("Do not import")
return qsTr("No labels selected")
}
if (!root.isFolderType) return Style.fa.tags + " " + qsTr("Add/Remove labels")
// We know here that it has a target and this is folder dropdown so we must find the first folder var tgtName = view.currentItem.folderName
var selSplit = root.selectedIDs.split(";") var tgtIcon = view.currentItem.folderIcon
for (var selIndex in selSplit) { var tgtColor = view.currentItem.folderColor
var selectedID = selSplit[selIndex]
var selectedType = structurePM.getType(selectedID) if (tgtIcon != Style.fa.folder_open) {
if (selectedType == gui.enums.folderTypeLabel) continue; // skip type::labele return tgtIcon + " " + tgtName
var selectedName = structurePM.getName(selectedID)
if (selectedName == "") continue; // empty name seems like wrong ID
var icon = gui.folderIcon(selectedName, selectedType)
if (selectedType == gui.enums.folderTypeSystem) {
return icon + " " + selectedName
} }
var iconColor = structurePM.getColor(selectedID)
return '<font color="'+iconColor+'">'+ icon + "</font> " + selectedName return '<font color="'+tgtColor+'">'+ tgtIcon + "</font> " + tgtName
} }
return "" if (root.isFolderType) return qsTr("No folder selected")
return qsTr("No labels selected")
} }
@ -116,7 +108,7 @@ ComboBox {
color: root.enabled && !root.down ? Style.main.textBlue : root.contentItem.color color: root.enabled && !root.down ? Style.main.textBlue : root.contentItem.color
} }
// Popup objects // Popup row
delegate: Rectangle { delegate: Rectangle {
id: thisDelegate id: thisDelegate
@ -127,22 +119,15 @@ ComboBox {
color: isHovered ? root.popup.hoverColor : root.popup.backColor color: isHovered ? root.popup.hoverColor : root.popup.backColor
property bool isSelected : isActive
property bool isSelected : { property string folderName: name
var selected = root.selectedIDs.split(";") property string folderIcon: gui.folderIcon(name,type)
for (var iSel in selected) { property string folderColor: (type == gui.enums.folderTypeLabel || type == gui.enums.folderTypeFolder) ? iconColor : root.popup.textColor
var sel = selected[iSel]
if (folderId == sel){
return true
}
}
return false
}
Text { Text {
id: targetIcon id: targetIcon
text: gui.folderIcon(folderName,folderType) text: thisDelegate.folderIcon
color : folderType != gui.enums.folderTypeSystem ? folderColor : root.popup.textColor color : thisDelegate.folderColor
anchors { anchors {
verticalCenter: parent.verticalCenter verticalCenter: parent.verticalCenter
left: parent.left left: parent.left
@ -157,6 +142,7 @@ ComboBox {
Text { Text {
id: targetName id: targetName
anchors { anchors {
verticalCenter: parent.verticalCenter verticalCenter: parent.verticalCenter
left: targetIcon.right left: targetIcon.right
@ -165,7 +151,7 @@ ComboBox {
rightMargin: Style.dialog.spacing rightMargin: Style.dialog.spacing
} }
text: folderName text: thisDelegate.folderName
color : root.popup.textColor color : root.popup.textColor
elide: Text.ElideRight elide: Text.ElideRight
@ -209,16 +195,15 @@ ComboBox {
onClicked: { onClicked: {
//console.log(" click delegate") //console.log(" click delegate")
if (root.isFolderType) { // don't update if selected if (root.isFolderType) { // don't update if selected
if (!thisDelegate.isSelected) {
root.importToFolder(folderId)
}
root.popup.close() root.popup.close()
} if (!isActive) {
if (root.folderType==gui.enums.folderTypeLabel) { root.importToFolder(mboxID)
if (thisDelegate.isSelected) { }
structureExternal.removeTargetLabelID(sourceID,folderId) } else {
if (isActive) {
root.removeTargetLabel(mboxID)
} else { } else {
structureExternal.addTargetLabelID(sourceID,folderId) root.addTargetLabel(mboxID)
} }
} }
} }
@ -295,14 +280,10 @@ ComboBox {
clip : true clip : true
anchors.fill : parent anchors.fill : parent
model : root.targets
delegate : root.delegate
section.property : "sectionName" currentIndex: view.model.selectedIndex
section.delegate : Text{text: sectionName}
model : FilterStructure {
filterOnGroup : root.folderType
delegate : root.delegate
}
} }
} }
@ -338,10 +319,7 @@ ComboBox {
onClicked : { onClicked : {
//console.log("click", addButton.text) //console.log("click", addButton.text)
var newName = "" var newName = name
if ( typeof folderName !== 'undefined' && !structurePM.hasFolderWithName (folderName) ) {
newName = folderName
}
winMain.popupFolderEdit.show(newName, "", "", root.folderType, sourceID) winMain.popupFolderEdit.show(newName, "", "", root.folderType, sourceID)
root.popup.close() root.popup.close()
} }

View File

@ -210,7 +210,7 @@ Window {
Component.onCompleted : { Component.onCompleted : {
testgui.winMain.x = 150 testgui.winMain.x = 350
testgui.winMain.y = 100 testgui.winMain.y = 100
} }
@ -230,7 +230,7 @@ Window {
} }
ListModel{ ListModel{
id: structureExternal id: structureExternalOFF
property var globalOptions: JSON.parse('{ "folderId" : "global--uniq" , "folderName" : "" , "folderColor" : "" , "folderType" : "" , "folderEntries" : 0, "fromDate": 0, "toDate": 0, "isFolderSelected" : false , "targetFolderID": "14" , "targetLabelIDs": ";20;29" }') property var globalOptions: JSON.parse('{ "folderId" : "global--uniq" , "folderName" : "" , "folderColor" : "" , "folderType" : "" , "folderEntries" : 0, "fromDate": 0, "toDate": 0, "isFolderSelected" : false , "targetFolderID": "14" , "targetLabelIDs": ";20;29" }')
@ -265,7 +265,7 @@ Window {
} }
ListModel{ ListModel{
id: structurePM id: structurePMOFF
// group selectors // group selectors
property bool selectedLabels : false property bool selectedLabels : false
@ -328,6 +328,7 @@ Window {
} }
} }
function setTypeSelected (model, folderType , toSelect ) { function setTypeSelected (model, folderType , toSelect ) {
console.log(" select type ", folderType, toSelect) console.log(" select type ", folderType, toSelect)
for (var i= -1; i<model.count; i++) { for (var i= -1; i<model.count; i++) {
@ -457,6 +458,355 @@ Window {
ListElement{ mailSubject : "Pop art is cool again" ; mailDate : "March 2 , 2019 12 : 00 : 22" ; inputFolder : "Archive" ; mailFrom : "me@me.me" ; errorMessage : "Something went wrong and import retry was not successful" ; } ListElement{ mailSubject : "Pop art is cool again" ; mailDate : "March 2 , 2019 12 : 00 : 22" ; inputFolder : "Archive" ; mailFrom : "me@me.me" ; errorMessage : "Something went wrong and import retry was not successful" ; }
ListElement{ mailSubject : "Check this cute kittens play volleyball on Copacabanana beach" ; mailDate : "March 2 , 2019 12 : 00 : 22" ; inputFolder : "Archive" ; mailFrom : "me@me.me" ; errorMessage : "Something went wrong and import retry was not successful" ; } ListElement{ mailSubject : "Check this cute kittens play volleyball on Copacabanana beach" ; mailDate : "March 2 , 2019 12 : 00 : 22" ; inputFolder : "Archive" ; mailFrom : "me@me.me" ; errorMessage : "Something went wrong and import retry was not successful" ; }
} }
// Transfer rules
ListModel {
id: transferRules
property var targets : new Object();
// test data for import
property var importRules : JSON.parse('[
{"isActive" : true , "mboxID" : "src1" , "fromDate" : 0 , "toDate" : 0 , "targetIDs" : [ "0" , "label1" ] } ,
{"isActive" : true , "mboxID" : "src2" , "fromDate" : 0 , "toDate" : 0 , "targetIDs" : [ "6" , "label2" ] } ,
{"isActive" : true , "mboxID" : "src3" , "fromDate" : 350000 , "toDate" : 5000000 , "targetIDs" : [ "folder1" ] } ,
{"isActive" : true , "mboxID" : "src4" , "fromDate" : 0 , "toDate" : 0 , "targetIDs" : [ "folder2" , "label1" , "label2" ] }
]')
property var selectedForExport : [ "0", "7", "folder1", "folde2", "label1", "label2", "label3"]
property var extMailboxes: JSON.parse('{
"src1": {"name" : "Source Inbox" , "type" : "external" , "color" : "#000"} ,
"src2": {"name" : "Source Sent" , "type" : "external" , "color" : "#000"} ,
"src3": {"name" : "Source Folder" , "type" : "external" , "color" : "#000"} ,
"src4": {"name" : "Source Trash" , "type" : "external" , "color" : "#000"}
}')
property var pmMailboxes : JSON.parse('{
"0": {"name" : "Inbox" , "type" : "system" , "color" : "#000"} ,
"3": {"name" : "Draft" , "type" : "system" , "color" : "#000"} ,
"6": {"name" : "Archive" , "type" : "system" , "color" : "#000"} ,
"5": {"name" : "All Mail" , "type" : "system" , "color" : "#000"} ,
"3": {"name" : "Trash" , "type" : "system" , "color" : "#000"} ,
"7": {"name" : "Sent" , "type" : "system" , "color" : "#000"} ,
"4": {"name" : "Spam" , "type" : "system" , "color" : "#000"} ,
"folder1": {"name": "Folder 1", "type":"folder", "color":"#57c"},
"folder2": {"name": "Folder 2", "type":"folder", "color":"#5c7"},
"folder3": {"name": "Folder 3", "type":"folder", "color":"#c57"},
"label1": {"name": "Label 1", "type":"label", "color":"#a5a"},
"label2": {"name": "Label 2", "type":"label", "color":"#5aa"},
"label3": {"name": "Label 3", "type":"label", "color":"#aa5"}
}')
ListElement{isActive : true ; mboxID : "source1" ; name : "Source folder 1" ; iconColor : "#cccccc" ; type : "external" ; fromDate : 0 ; toDate : 0 ; labelColors : "red ; green ; blue"}
ListElement{isActive : false ; mboxID : "source2" ; name : "Source folder 2" ; iconColor : "#cccccc" ; type : "external" ; fromDate : 300000 ; toDate : 15000000 ; labelColors : "red ; green ; blue"}
ListElement{isActive : true ; mboxID : "source3" ; name : "Source folder 4" ; iconColor : "#cccccc" ; type : "external" ; fromDate : 0 ; toDate : 0 ; labelColors : "red ; green ; blue"}
// TransferRules INTERFACE
// TransferRules properties
property int globalFromDate : 0 // 45000
property int globalToDate : 0 // 120000
property bool isLabelGroupSelected : false
property bool isFolderGroupSelected : false
// TransferRules default getters
// func (*TransferRules) count() int
// func (*TransferRules) roleNames() map[int] QByteArray
// func (*TransferRules) data(index, role) *QVariant
//
// Expected roles for TransferRules
//
// isActive bool
// mboxID string // constant
// name string // constant
// type string // constant, expected values: "label", "folder", ""
// iconColor string // constant
// fromDate int64
// toDate int64
// labelColors string // list of hex RGB strings delimited by `;`
// TransferRules custom getters
function targetFolders(sourceID) {
//ListElement{isActive: true; mboxID: "target1"; name: "Target system folder"; type: "system"; iconColor:"red"}
return getTargets(sourceID, "folder")
}
function targetLabels(sourceID) {
//ListElement{isActive: false; mboxID: "target3"; name: "Target custom label 1"; type: "label"; iconColor:"green"}
return getTargets(sourceID, "label")
}
// For target drop down menu (labels and folders) we need
// additional model TargetList (QAbstractListModel).
//
// func (*TransferRules) targetFolders(sourceID string) * QAbstractListModel
//
// There is no setter functions for this list all actions are
// handled by TransferRules.
//
// The target models have therefore only data functions and one property as interface:
//
// TargetList properties:
// property int selectedIndex : -1
//
// TargetList default getters
// func (*TargetList) count() int
// func (*TargetList) roleNames() map[int] QByteArray
// func (*TargetList) data(index, role) *QVariant
//
// Expected roles for TargetList
//
// isActive bool
// mboxID string // constant
// name string // constant
// type string // constant, expected values: "label", "folder", ""
// iconColor string // constant
//
// The tricky part here is the QAbstractListModel implemetation: it
// needs to return all targets of certain type and their index.
// Setters
function setIsRuleActive(srcID, isActive){
console.log("setIsRuleActive", srcID, isActive)
var groupLabelsSelected = true
var groupFoldersSelected = true
for (var i = 0; i < transferRules.count; i++) {
var rule = transferRules.get(i)
if (rule.mboxID ==srcID) rule.isActive = isActive;
if (!rule.isActive && rule.type == "label") groupLabelsSelected = false
if (!rule.isActive && rule.type == "folder") groupFoldersSelected = false
}
transferRules.isLabelGroupSelected = groupLabelsSelected
transferRules.isFolderGroupSelected = groupFoldersSelected
}
function setIsGroupActive(groupName,isActive){
console.log("setIsGroupActive", groupName, isActive)
var groupLabelsSelected = true
var groupFoldersSelected = true
for (var i = 0; i < transferRules.count; i++) {
var rule = transferRules.get(i)
if (rule.type == groupName) rule.isActive = isActive;
if (!rule.isActive && rule.type == "label") groupLabelsSelected = false
if (!rule.isActive && rule.type == "folder") groupFoldersSelected = false
}
transferRules.isLabelGroupSelected = groupLabelsSelected
transferRules.isFolderGroupSelected = groupFoldersSelected
}
function setFromToDate(srcID, fromDate, toDate){
console.log("setFromToDate", srcID, fromDate, toDate)
for (var i = 0; i < transferRules.count; i++) {
var rule = transferRules.get(i)
if (rule.mboxID ==srcID) {
rule.fromDate = fromDate
rule.toDate = toDate
}
}
}
function addTargetID(srcID, targetID){
console.log("addTargetID", srcID, targetID)
changeTargetID(srcID, targetID, true)
}
function removeTargetID(srcID, targetID){
console.log("removeTargetID", srcID, targetID)
changeTargetID(srcID, targetID, false)
}
// MOCK METHODS: NOT PART OF INTERFACE
Component.onCompleted: prepareImport()
// Fill model with import rules
function prepareImport() {
console.log(" ==== Prepare IMPORT ==== ")
console.trace()
transferRules.clear()
for (var ruleI in transferRules.importRules) {
var rule = transferRules.importRules[ruleI]
var src = transferRules.extMailboxes[rule.mboxID];
var labelColors = [];
for (var tid in rule.targetIDs) {
var targetID = rule.targetIDs[tid]
if (pmMailboxes[targetID].type == "label") {
labelColors.push(pmMailboxes[targetID]["color"])
}
}
transferRules.append({
"isActive" : rule.isActive,
"mboxID" : rule.mboxID,
"name" : src.name,
"type" : src.type,
"iconColor" : src["color"],
"fromDate" : rule.fromDate,
"toDate" : rule.toDate,
"labelColors" : labelColors.join(";"),
});
}
}
// Fill model with export rules
function prepareExport() {
console.log(" ==== Prepare EXPORT ==== ")
console.trace()
transferRules.clear()
var groupLabelsSelected = true
var groupFoldersSelected = true
for (var srcID in transferRules.pmMailboxes) {
var src = transferRules.pmMailboxes[srcID]
var isActive = transferRules.selectedForExport.find(function(mboxID){return mboxID == srcID}) !== undefined
transferRules.append({
"isActive" : isActive,
"mboxID" : srcID,
"name" : src.name,
"type" : (src.type == "system" ? "" : src["type"]),
"iconColor" : src["color"],
"fromDate" : 0,
"toDate" : 0,
"labelColors" : src["color"]
});
if (!isActive) {
if (src.type == "label") {
groupLabelsSelected = false
}
if (src.type == "folder") {
groupFoldersSelected = false
}
}
}
transferRules.isLabelGroupSelected = groupLabelsSelected
transferRules.isFolderGroupSelected = groupFoldersSelected
}
function getTargets(sourceID, type) {
console.log("get targets:", type, sourceID)
if (! (type+sourceID in transferRules.targets)){
var source;
for (var srcI in transferRules.importRules) {
source = transferRules.importRules[srcI]
if (source.mboxID == sourceID ) {
break
}
}
var model = Qt.createQmlObject ('import QtQuick 2.3; ListModel { property int selectedIndex: -1; }', transferRules);
var i = -1
for (var tgtID in transferRules.pmMailboxes) {
var tgt = transferRules.pmMailboxes[tgtID]
if (type == "label" && tgt.type != "label") continue
if (type != "label" && tgt.type == "label") continue
i++;
var isActive = false
for (var tid in source.targetIDs ) {
var selectedID = source.targetIDs[tid]
if (selectedID == tgtID) {
isActive = true
model.selectedIndex=i
break
}
}
var row = {
"isActive" : isActive,
"mboxID" : tgtID,
"name" : tgt.name,
"type" : tgt.type,
"iconColor" : tgt["color"]
};
model.append (row) ;
}
transferRules.targets[type+sourceID] = model;
}
return transferRules.targets[type+sourceID];
}
function changeTargetID(srcID, targetID, add){
console.log("change target ID ", srcID, targetID, add)
for (var targetsName in transferRules.targets) {
var targets = transferRules.targets[targetsName]
var areFolders = targetsName == "folder"+srcID
var areLabels = targetsName == "label"+srcID
if (areFolders || areLabels) {
console.log("matched targets ", targetsName, targets)
var deactivateOthers = false
var colorList = []
for (var i =0; i<targets.count; i++) {
var tgt = targets.get(i)
console.log(" tgt", i, tgt.mboxID, tgt.isActive)
if (tgt.mboxID == targetID) {
console.log(" matched tgt", i, tgt.mboxID)
if (areFolders && !add) {
console.exception("WRONG LOGIC: removing folder")
}
if (add) {
targets.selectedIndex=i
}
tgt.isActive = add
deactivateOthers = add && areFolders
console.log(" active ", tgt.isActive)
}
if (areLabels && tgt.isActive) {
colorList.push(tgt.iconColor)
console.log(" colors", i, colorList)
}
}
if (areLabels) {
if (colorList.length == 0){
targets.selectedIndex = -1
}
for (var i = 0; i<transferRules.count; i++) {
var rule = transferRules.get(i)
console.log (" are labels: color list", i, rule.mboxID )
if (rule.mboxID == srcID) {
rule.labelColors = colorList.join(";")
console.log("updated label color list", rule.labelColors)
break
}
}
}
if (deactivateOthers) {
for (var i =0; i<targets.count; i++) {
var tgt = targets.get(i)
if (tgt.mboxID != targetID) {
tgt.isActive = false
console.log(" deactivate ", tgt.mboxID, tgt.isActive)
}
}
}
}
}
}
}
} }
@ -497,6 +847,14 @@ Window {
signal toggleMainWin(int systX, int systY, int systW, int systH) signal toggleMainWin(int systX, int systY, int systW, int systH)
signal notifyHasNoKeychain()
signal notifyKeychainRebuild()
signal notifyAddressChangedLogout()
signal notifyAddressChanged()
signal notifyUpdate()
signal showWindow() signal showWindow()
signal showHelp() signal showHelp()
signal showQuit() signal showQuit()
@ -692,8 +1050,8 @@ Window {
go.animateProgressBar.resume() go.animateProgressBar.resume()
} }
function cancelProcess(clearUnfinished) { function cancelProcess() {
console.log("stopped at ", go.progress, " clearing unfinished", clearUnfinished) console.log("stopped at ", go.progress)
go.animateProgressBar.stop() go.animateProgressBar.stop()
} }
@ -718,6 +1076,7 @@ Window {
break; break;
case "loadStructureForExport" : case "loadStructureForExport" :
transferRules.prepareExport()
go.exportStructureLoadFinished(true) go.exportStructureLoadFinished(true)
break; break;

View File

@ -43,10 +43,10 @@ const (
// Constants for data map // Constants for data map
const ( const (
// Account info // Account info
Account = int(core.Qt__UserRole) + 1<<iota Account = int(core.Qt__UserRole) + 1 + iota // 256 + 1 = 257
Status Status // 258
Password Password // 259
Aliases Aliases // ...
IsExpanded IsExpanded
// Folder info // Folder info
FolderId FolderId
@ -65,4 +65,56 @@ const (
MailFrom MailFrom
InputFolder InputFolder
ErrorMessage ErrorMessage
// Transfer rules mbox
MboxSelectedIndex
MboxIsActive
MboxID
MboxName
MboxType
MboxColor
// Transfer Rules
RuleTargetLabelColors
RuleFromDate
RuleToDate
)
const (
// This should match enums in GuiIE.qml
errUnknownError = 0
errEventAPILogout = 1
errUpdateAPI = 2
errUpdateJSON = 3
errUserAuth = 4
errQApplication = 18
errEmailExportFailed = 6
errEmailExportMissing = 7
errNothingToImport = 8
errEmailImportFailed = 12
errDraftImportFailed = 13
errDraftLabelFailed = 14
errEncryptMessageAttachment = 15
errEncryptMessage = 16
errNoInternetWhileImport = 17
errUnlockUser = 5
errSourceMessageNotSelected = 19
errCannotParseMail = 5000
errWrongLoginOrPassword = 5001
errWrongServerPathOrPort = 5002
errWrongAuthMethod = 5003
errIMAPFetchFailed = 5004
errLocalSourceLoadFailed = 1000
errPMLoadFailed = 1001
errRemoteSourceLoadFailed = 1002
errLoadAccountList = 1005
errExit = 1006
errRetry = 1007
errAsk = 1008
errImportFailed = 1009
errCreateLabelFailed = 1010
errCreateFolderFailed = 1011
errUpdateLabelFailed = 1012
errUpdateFolderFailed = 1013
errFillFolderName = 1014
errSelectFolderColor = 1015
errNoInternet = 1016
) )

View File

@ -21,14 +21,10 @@ package qtie
import ( import (
qtcommon "github.com/ProtonMail/proton-bridge/internal/frontend/qt-common" qtcommon "github.com/ProtonMail/proton-bridge/internal/frontend/qt-common"
"github.com/ProtonMail/proton-bridge/internal/transfer"
"github.com/therecipe/qt/core" "github.com/therecipe/qt/core"
) )
// ErrorDetail stores information about email and error
type ErrorDetail struct {
MailSubject, MailDate, MailFrom, InputFolder, ErrorMessage string
}
func init() { func init() {
ErrorListModel_QRegisterMetaType() ErrorListModel_QRegisterMetaType()
} }
@ -42,11 +38,12 @@ type ErrorListModel struct {
_ map[int]*core.QByteArray `property:"roles"` _ map[int]*core.QByteArray `property:"roles"`
_ int `property:"count"` _ int `property:"count"`
Details []*ErrorDetail Progress *transfer.Progress
records []*transfer.MessageStatus
} }
func (s *ErrorListModel) init() { func (e *ErrorListModel) init() {
s.SetRoles(map[int]*core.QByteArray{ e.SetRoles(map[int]*core.QByteArray{
MailSubject: qtcommon.NewQByteArrayFromString("mailSubject"), MailSubject: qtcommon.NewQByteArrayFromString("mailSubject"),
MailDate: qtcommon.NewQByteArrayFromString("mailDate"), MailDate: qtcommon.NewQByteArrayFromString("mailDate"),
MailFrom: qtcommon.NewQByteArrayFromString("mailFrom"), MailFrom: qtcommon.NewQByteArrayFromString("mailFrom"),
@ -54,76 +51,50 @@ func (s *ErrorListModel) init() {
ErrorMessage: qtcommon.NewQByteArrayFromString("errorMessage"), ErrorMessage: qtcommon.NewQByteArrayFromString("errorMessage"),
}) })
// basic QAbstractListModel mehods // basic QAbstractListModel mehods
s.ConnectData(s.data) e.ConnectData(e.data)
s.ConnectRowCount(s.rowCount) e.ConnectRowCount(e.rowCount)
s.ConnectColumnCount(s.columnCount) e.ConnectColumnCount(e.columnCount)
s.ConnectRoleNames(s.roleNames) e.ConnectRoleNames(e.roleNames)
} }
func (s *ErrorListModel) data(index *core.QModelIndex, role int) *core.QVariant { func (e *ErrorListModel) data(index *core.QModelIndex, role int) *core.QVariant {
if !index.IsValid() { if !index.IsValid() {
return core.NewQVariant() return core.NewQVariant()
} }
if index.Row() >= len(s.Details) { if index.Row() >= len(e.records) {
return core.NewQVariant() return core.NewQVariant()
} }
var p = s.Details[index.Row()] var r = e.records[index.Row()]
switch role { switch role {
case MailSubject: case MailSubject:
return qtcommon.NewQVariantString(p.MailSubject) return qtcommon.NewQVariantString(r.Subject)
case MailDate: case MailDate:
return qtcommon.NewQVariantString(p.MailDate) return qtcommon.NewQVariantString(r.Time.String())
case MailFrom: case MailFrom:
return qtcommon.NewQVariantString(p.MailFrom) return qtcommon.NewQVariantString(r.From)
case InputFolder: case InputFolder:
return qtcommon.NewQVariantString(p.InputFolder) return qtcommon.NewQVariantString(r.SourceID)
case ErrorMessage: case ErrorMessage:
return qtcommon.NewQVariantString(p.ErrorMessage) return qtcommon.NewQVariantString(r.GetErrorMessage())
default: default:
return core.NewQVariant() return core.NewQVariant()
} }
} }
func (s *ErrorListModel) rowCount(parent *core.QModelIndex) int { return len(s.Details) } func (e *ErrorListModel) rowCount(parent *core.QModelIndex) int { return len(e.records) }
func (s *ErrorListModel) columnCount(parent *core.QModelIndex) int { return 1 } func (e *ErrorListModel) columnCount(parent *core.QModelIndex) int { return 1 }
func (s *ErrorListModel) roleNames() map[int]*core.QByteArray { return s.Roles() } func (e *ErrorListModel) roleNames() map[int]*core.QByteArray { return e.Roles() }
// Add more errors to list func (e *ErrorListModel) load() {
func (s *ErrorListModel) Add(more []*ErrorDetail) { if e.Progress == nil {
s.BeginInsertRows(core.NewQModelIndex(), len(s.Details), len(s.Details)) log.Error("Progress not connected")
s.Details = append(s.Details, more...) return
s.SetCount(len(s.Details)) }
s.EndInsertRows()
}
// Clear removes all items in model e.BeginResetModel()
func (s *ErrorListModel) Clear() { e.records = e.Progress.GetFailedMessages()
s.BeginRemoveRows(core.NewQModelIndex(), 0, len(s.Details)) e.EndResetModel()
s.Details = s.Details[0:0]
s.SetCount(len(s.Details))
s.EndRemoveRows()
}
func (s *ErrorListModel) load(importLogFileName string) {
/*
err := backend.LoopDetailsInFile(importLogFileName, func(d *backend.MessageDetails) {
if d.MessageID != "" { // imported ok
return
}
ed := &ErrorDetail{
MailSubject: d.Subject,
MailDate: d.Time,
MailFrom: d.From,
InputFolder: d.Folder,
ErrorMessage: d.Error,
}
s.Add([]*ErrorDetail{ed})
})
if err != nil {
log.Errorf("load import report from %q: %v", importLogFileName, err)
}
*/
} }

View File

@ -21,6 +21,7 @@ package qtie
import ( import (
"github.com/ProtonMail/proton-bridge/internal/transfer" "github.com/ProtonMail/proton-bridge/internal/transfer"
"github.com/pkg/errors"
) )
const ( const (
@ -29,10 +30,11 @@ const (
) )
func (f *FrontendQt) LoadStructureForExport(addressOrID string) { func (f *FrontendQt) LoadStructureForExport(addressOrID string) {
errCode := errUnknownError
var err error var err error
defer func() { defer func() {
if err != nil { if err != nil {
f.showError(err) f.showError(errCode, errors.Wrap(err, "failed to load structure for "+addressOrID))
f.Qml.ExportStructureLoadFinished(false) f.Qml.ExportStructureLoadFinished(false)
} else { } else {
f.Qml.ExportStructureLoadFinished(true) f.Qml.ExportStructureLoadFinished(true)
@ -40,20 +42,12 @@ func (f *FrontendQt) LoadStructureForExport(addressOrID string) {
}() }()
if f.transfer, err = f.ie.GetEMLExporter(addressOrID, ""); err != nil { if f.transfer, err = f.ie.GetEMLExporter(addressOrID, ""); err != nil {
// The only error can be problem to load PM user and address.
errCode = errPMLoadFailed
return return
} }
f.PMStructure.Clear() f.TransferRules.setTransfer(f.transfer)
sourceMailboxes, err := f.transfer.SourceMailboxes()
if err != nil {
return
}
for _, mbox := range sourceMailboxes {
rule := f.transfer.GetRule(mbox)
f.PMStructure.addEntry(newFolderInfo(mbox, rule))
}
f.PMStructure.transfer = f.transfer
} }
func (f *FrontendQt) StartExport(rootPath, login, fileType string, attachEncryptedBody bool) { func (f *FrontendQt) StartExport(rootPath, login, fileType string, attachEncryptedBody bool) {

View File

@ -65,11 +65,11 @@ type FrontendQt struct {
programVersion string // Program version programVersion string // Program version
buildVersion string // Program build version buildVersion string // Program build version
PMStructure *FolderStructure // Providing data for account labels and folders for ProtonMail account TransferRules *TransferRules
ExternalStructure *FolderStructure // Providing data for account labels and folders for MBOX, EML or external IMAP account ErrorList *ErrorListModel // Providing data for error reporting
ErrorList *ErrorListModel // Providing data for error reporting
transfer *transfer.Transfer transfer *transfer.Transfer
progress *transfer.Progress
notifyHasNoKeychain bool notifyHasNoKeychain bool
} }
@ -103,102 +103,99 @@ func New(
} }
// IsAppRestarting for Import-Export is always false i.e never restarts // IsAppRestarting for Import-Export is always false i.e never restarts
func (s *FrontendQt) IsAppRestarting() bool { func (f *FrontendQt) IsAppRestarting() bool {
return false return false
} }
// Loop function for Import-Export interface. It runs QtExecute in main thread // Loop function for Import-Export interface. It runs QtExecute in main thread
// with no additional function. // with no additional function.
func (s *FrontendQt) Loop(setupError error) (err error) { func (f *FrontendQt) Loop(setupError error) (err error) {
if setupError != nil { if setupError != nil {
s.notifyHasNoKeychain = true f.notifyHasNoKeychain = true
} }
go func() { go func() {
defer s.panicHandler.HandlePanic() defer f.panicHandler.HandlePanic()
s.watchEvents() f.watchEvents()
}() }()
err = s.QtExecute(func(s *FrontendQt) error { return nil }) err = f.QtExecute(func(f *FrontendQt) error { return nil })
return err return err
} }
func (s *FrontendQt) watchEvents() { func (f *FrontendQt) watchEvents() {
internetOffCh := qtcommon.MakeAndRegisterEvent(s.eventListener, events.InternetOffEvent) internetOffCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.InternetOffEvent)
internetOnCh := qtcommon.MakeAndRegisterEvent(s.eventListener, events.InternetOnEvent) internetOnCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.InternetOnEvent)
restartBridgeCh := qtcommon.MakeAndRegisterEvent(s.eventListener, events.RestartBridgeEvent) restartBridgeCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.RestartBridgeEvent)
addressChangedCh := qtcommon.MakeAndRegisterEvent(s.eventListener, events.AddressChangedEvent) addressChangedCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.AddressChangedEvent)
addressChangedLogoutCh := qtcommon.MakeAndRegisterEvent(s.eventListener, events.AddressChangedLogoutEvent) addressChangedLogoutCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.AddressChangedLogoutEvent)
logoutCh := qtcommon.MakeAndRegisterEvent(s.eventListener, events.LogoutEvent) logoutCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.LogoutEvent)
updateApplicationCh := qtcommon.MakeAndRegisterEvent(s.eventListener, events.UpgradeApplicationEvent) updateApplicationCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.UpgradeApplicationEvent)
newUserCh := qtcommon.MakeAndRegisterEvent(s.eventListener, events.UserRefreshEvent) newUserCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.UserRefreshEvent)
for { for {
select { select {
case <-internetOffCh: case <-internetOffCh:
s.Qml.SetConnectionStatus(false) f.Qml.SetConnectionStatus(false)
case <-internetOnCh: case <-internetOnCh:
s.Qml.SetConnectionStatus(true) f.Qml.SetConnectionStatus(true)
case <-restartBridgeCh: case <-restartBridgeCh:
s.Qml.SetIsRestarting(true) f.Qml.SetIsRestarting(true)
s.App.Quit() f.App.Quit()
case address := <-addressChangedCh: case address := <-addressChangedCh:
s.Qml.NotifyAddressChanged(address) f.Qml.NotifyAddressChanged(address)
case address := <-addressChangedLogoutCh: case address := <-addressChangedLogoutCh:
s.Qml.NotifyAddressChangedLogout(address) f.Qml.NotifyAddressChangedLogout(address)
case userID := <-logoutCh: case userID := <-logoutCh:
user, err := s.ie.GetUser(userID) user, err := f.ie.GetUser(userID)
if err != nil { if err != nil {
return return
} }
s.Qml.NotifyLogout(user.Username()) f.Qml.NotifyLogout(user.Username())
case <-updateApplicationCh: case <-updateApplicationCh:
s.Qml.ProcessFinished() f.Qml.ProcessFinished()
s.Qml.NotifyUpdate() f.Qml.NotifyUpdate()
case <-newUserCh: case <-newUserCh:
s.Qml.LoadAccounts() f.Qml.LoadAccounts()
} }
} }
} }
func (s *FrontendQt) qtSetupQmlAndStructures() { func (f *FrontendQt) qtSetupQmlAndStructures() {
s.App = widgets.NewQApplication(len(os.Args), os.Args) f.App = widgets.NewQApplication(len(os.Args), os.Args)
// view // view
s.View = qml.NewQQmlApplicationEngine(s.App) f.View = qml.NewQQmlApplicationEngine(f.App)
// Add Go-QML Import-Export // Add Go-QML Import-Export
s.Qml = NewGoQMLInterface(nil) f.Qml = NewGoQMLInterface(nil)
s.Qml.SetFrontend(s) // provides access f.Qml.SetFrontend(f) // provides access
s.View.RootContext().SetContextProperty("go", s.Qml) f.View.RootContext().SetContextProperty("go", f.Qml)
// Add AccountsModel // Add AccountsModel
s.Accounts.SetupAccounts(s.Qml, s.ie) f.Accounts.SetupAccounts(f.Qml, f.ie)
s.View.RootContext().SetContextProperty("accountsModel", s.Accounts.Model) f.View.RootContext().SetContextProperty("accountsModel", f.Accounts.Model)
// Add ProtonMail FolderStructure // Add TransferRules structure
s.PMStructure = NewFolderStructure(nil) f.TransferRules = NewTransferRules(nil)
s.View.RootContext().SetContextProperty("structurePM", s.PMStructure) f.View.RootContext().SetContextProperty("transferRules", f.TransferRules)
// Add external FolderStructure
s.ExternalStructure = NewFolderStructure(nil)
s.View.RootContext().SetContextProperty("structureExternal", s.ExternalStructure)
// Add error list modal // Add error list modal
s.ErrorList = NewErrorListModel(nil) f.ErrorList = NewErrorListModel(nil)
s.View.RootContext().SetContextProperty("errorList", s.ErrorList) f.View.RootContext().SetContextProperty("errorList", f.ErrorList)
s.Qml.ConnectLoadImportReports(s.ErrorList.load) f.Qml.ConnectLoadImportReports(f.ErrorList.load)
// Import path and load QML files // Import path and load QML files
s.View.AddImportPath("qrc:///") f.View.AddImportPath("qrc:///")
s.View.Load(core.NewQUrl3("qrc:/uiie.qml", 0)) f.View.Load(core.NewQUrl3("qrc:/uiie.qml", 0))
// TODO set the first start flag // TODO set the first start flag
log.Error("Get FirstStart: Not implemented") log.Error("Get FirstStart: Not implemented")
//if prefs.Get(prefs.FirstStart) == "true" { //if prefs.Get(prefs.FirstStart) == "true" {
if false { if false {
s.Qml.SetIsFirstStart(true) f.Qml.SetIsFirstStart(true)
} else { } else {
s.Qml.SetIsFirstStart(false) f.Qml.SetIsFirstStart(false)
} }
// Notify user about error during initialization. // Notify user about error during initialization.
if s.notifyHasNoKeychain { if f.notifyHasNoKeychain {
s.Qml.NotifyHasNoKeychain() f.Qml.NotifyHasNoKeychain()
} }
} }
@ -207,18 +204,18 @@ func (s *FrontendQt) qtSetupQmlAndStructures() {
// It is needed to have just one Qt application per program (at least per same // It is needed to have just one Qt application per program (at least per same
// thread). This functions reads the main user interface defined in QML files. // thread). This functions reads the main user interface defined in QML files.
// The files are appended to library by Qt-QRC. // The files are appended to library by Qt-QRC.
func (s *FrontendQt) QtExecute(Procedure func(*FrontendQt) error) error { func (f *FrontendQt) QtExecute(Procedure func(*FrontendQt) error) error {
qtcommon.QtSetupCoreAndControls(s.programName, s.programVersion) qtcommon.QtSetupCoreAndControls(f.programName, f.programVersion)
s.qtSetupQmlAndStructures() f.qtSetupQmlAndStructures()
// Check QML is loaded properly // Check QML is loaded properly
if len(s.View.RootObjects()) == 0 { if len(f.View.RootObjects()) == 0 {
//return errors.New(errors.ErrQApplication, "QML not loaded properly") //return errors.New(errors.ErrQApplication, "QML not loaded properly")
return errors.New("QML not loaded properly") return errors.New("QML not loaded properly")
} }
// Obtain main window (need for invoke method) // Obtain main window (need for invoke method)
s.MainWin = s.View.RootObjects()[0] f.MainWin = f.View.RootObjects()[0]
// Injected procedure for out-of-main-thread applications // Injected procedure for out-of-main-thread applications
if err := Procedure(s); err != nil { if err := Procedure(f); err != nil {
return err return err
} }
// Loop // Loop
@ -234,63 +231,55 @@ func (s *FrontendQt) QtExecute(Procedure func(*FrontendQt) error) error {
return nil return nil
} }
func (s *FrontendQt) openLogs() { func (f *FrontendQt) openLogs() {
go open.Run(s.config.GetLogDir()) go open.Run(f.config.GetLogDir())
} }
func (s *FrontendQt) openReport() { func (f *FrontendQt) openReport() {
go open.Run(s.Qml.ImportLogFileName()) go open.Run(f.Qml.ImportLogFileName())
} }
func (s *FrontendQt) openDownloadLink() { func (f *FrontendQt) openDownloadLink() {
go open.Run(s.updates.GetDownloadLink()) go open.Run(f.updates.GetDownloadLink())
} }
func (s *FrontendQt) sendImportReport(address, reportFile string) (isOK bool) { // sendImportReport sends an anonymized import or export report file to our customer support
/* func (f *FrontendQt) sendImportReport(address string) bool { // Todo_: Rename to sendReport?
accname := "[No account logged in]"
if s.Accounts.Count() > 0 {
accname = s.Accounts.get(0).Account()
}
basename := filepath.Base(reportFile)
req := pmapi.ReportReq{
OS: core.QSysInfo_ProductType(),
OSVersion: core.QSysInfo_PrettyProductName(),
Title: "[Import Export] Import report: " + basename,
Description: "Sending import report file in attachment.",
Username: accname,
Email: address,
}
report, err := os.Open(reportFile)
if err != nil {
log.Errorln("report file open:", err)
isOK = false
}
req.AddAttachment("log", basename, report)
c := pmapi.NewClient(backend.APIConfig, "import_reporter")
err = c.Report(req)
if err != nil {
log.Errorln("while sendReport:", err)
isOK = false
return
}
log.Infof("Report %q send successfully", basename)
isOK = true
*/
return false
}
// sendBug is almost idetical to bridge
func (s *FrontendQt) sendBug(description, emailClient, address string) (isOK bool) {
isOK = true
var accname = "No account logged in" var accname = "No account logged in"
if s.Accounts.Model.Count() > 0 { if f.Accounts.Model.Count() > 0 {
accname = s.Accounts.Model.Get(0).Account() accname = f.Accounts.Model.Get(0).Account()
} }
if err := s.ie.ReportBug(
if f.progress == nil {
log.Errorln("Failed to send process report: Missing progress")
return false
}
report := f.progress.GenerateBugReport()
if err := f.ie.ReportFile(
core.QSysInfo_ProductType(),
core.QSysInfo_PrettyProductName(),
accname,
address,
report,
); err != nil {
log.Errorln("Failed to send process report:", err)
return false
}
log.Info("Report send successfully")
return true
}
// sendBug sends a bug report described by user to our customer support
func (f *FrontendQt) sendBug(description, emailClient, address string) bool {
var accname = "No account logged in"
if f.Accounts.Model.Count() > 0 {
accname = f.Accounts.Model.Get(0).Account()
}
if err := f.ie.ReportBug(
core.QSysInfo_ProductType(), core.QSysInfo_ProductType(),
core.QSysInfo_PrettyProductName(), core.QSysInfo_PrettyProductName(),
description, description,
@ -299,41 +288,43 @@ func (s *FrontendQt) sendBug(description, emailClient, address string) (isOK boo
emailClient, emailClient,
); err != nil { ); err != nil {
log.Errorln("while sendBug:", err) log.Errorln("while sendBug:", err)
isOK = false return false
} }
return
return true
} }
// checkInternet is almost idetical to bridge // checkInternet is almost idetical to bridge
func (s *FrontendQt) checkInternet() { func (f *FrontendQt) checkInternet() {
s.Qml.SetConnectionStatus(s.ie.CheckConnection() == nil) f.Qml.SetConnectionStatus(f.ie.CheckConnection() == nil)
} }
func (s *FrontendQt) showError(err error) { func (f *FrontendQt) showError(code int, err error) {
code := 0 // TODO err.Code() f.Qml.SetErrorDescription(err.Error())
s.Qml.SetErrorDescription(err.Error())
log.WithField("code", code).Errorln(err.Error()) log.WithField("code", code).Errorln(err.Error())
s.Qml.NotifyError(code) f.Qml.NotifyError(code)
} }
func (s *FrontendQt) emitEvent(evType, msg string) { func (f *FrontendQt) emitEvent(evType, msg string) {
s.eventListener.Emit(evType, msg) f.eventListener.Emit(evType, msg)
} }
func (s *FrontendQt) setProgressManager(progress *transfer.Progress) { func (f *FrontendQt) setProgressManager(progress *transfer.Progress) {
s.Qml.ConnectPauseProcess(func() { progress.Pause("user") }) f.progress = progress
s.Qml.ConnectResumeProcess(progress.Resume) f.ErrorList.Progress = progress
s.Qml.ConnectCancelProcess(func(clearUnfinished bool) {
// TODO clear unfinished f.Qml.ConnectPauseProcess(func() { progress.Pause("paused") })
f.Qml.ConnectResumeProcess(progress.Resume)
f.Qml.ConnectCancelProcess(func() {
progress.Stop() progress.Stop()
}) })
go func() { go func() {
defer func() { defer func() {
s.Qml.DisconnectPauseProcess() f.Qml.DisconnectPauseProcess()
s.Qml.DisconnectResumeProcess() f.Qml.DisconnectResumeProcess()
s.Qml.DisconnectCancelProcess() f.Qml.DisconnectCancelProcess()
s.Qml.SetProgress(1) f.Qml.SetProgress(1)
}() }()
//TODO get log file (in old code it was here, but this is ugly place probably somewhere else) //TODO get log file (in old code it was here, but this is ugly place probably somewhere else)
@ -344,119 +335,123 @@ func (s *FrontendQt) setProgressManager(progress *transfer.Progress) {
} }
failed, imported, _, _, total := progress.GetCounts() failed, imported, _, _, total := progress.GetCounts()
if total != 0 { // udate total if total != 0 { // udate total
s.Qml.SetTotal(int(total)) f.Qml.SetTotal(int(total))
} }
s.Qml.SetProgressFails(int(failed)) f.Qml.SetProgressFails(int(failed))
s.Qml.SetProgressDescription(progress.PauseReason()) // TODO add description when changing folders? f.Qml.SetProgressDescription(progress.PauseReason()) // TODO add description when changing folders?
if total > 0 { if total > 0 {
newProgress := float32(imported+failed) / float32(total) newProgress := float32(imported+failed) / float32(total)
if newProgress >= 0 && newProgress != s.Qml.Progress() { if newProgress >= 0 && newProgress != f.Qml.Progress() {
s.Qml.SetProgress(newProgress) f.Qml.SetProgress(newProgress)
s.Qml.ProgressChanged(newProgress) f.Qml.ProgressChanged(newProgress)
} }
} }
} }
// TODO fatal error? if err := progress.GetFatalError(); err != nil {
f.Qml.SetProgressDescription(err.Error())
} else {
f.Qml.SetProgressDescription("")
}
}() }()
} }
// StartUpdate is identical to bridge // StartUpdate is identical to bridge
func (s *FrontendQt) StartUpdate() { func (f *FrontendQt) StartUpdate() {
progress := make(chan updates.Progress) progress := make(chan updates.Progress)
go func() { // Update progress in QML. go func() { // Update progress in QML.
defer s.panicHandler.HandlePanic() defer f.panicHandler.HandlePanic()
for current := range progress { for current := range progress {
s.Qml.SetProgress(current.Processed) f.Qml.SetProgress(current.Processed)
s.Qml.SetProgressDescription(strconv.Itoa(current.Description)) f.Qml.SetProgressDescription(strconv.Itoa(current.Description))
// Error happend // Error happend
if current.Err != nil { if current.Err != nil {
log.Error("update progress: ", current.Err) log.Error("update progress: ", current.Err)
s.Qml.UpdateFinished(true) f.Qml.UpdateFinished(true)
return return
} }
// Finished everything OK. // Finished everything OK.
if current.Description >= updates.InfoQuitApp { if current.Description >= updates.InfoQuitApp {
s.Qml.UpdateFinished(false) f.Qml.UpdateFinished(false)
time.Sleep(3 * time.Second) // Just notify. time.Sleep(3 * time.Second) // Just notify.
s.Qml.SetIsRestarting(current.Description == updates.InfoRestartApp) f.Qml.SetIsRestarting(current.Description == updates.InfoRestartApp)
s.App.Quit() f.App.Quit()
return return
} }
} }
}() }()
go func() { go func() {
defer s.panicHandler.HandlePanic() defer f.panicHandler.HandlePanic()
s.updates.StartUpgrade(progress) f.updates.StartUpgrade(progress)
}() }()
} }
// isNewVersionAvailable is identical to bridge // isNewVersionAvailable is identical to bridge
// return 0 when local version is fine // return 0 when local version is fine
// return 1 when new version is available // return 1 when new version is available
func (s *FrontendQt) isNewVersionAvailable(showMessage bool) { func (f *FrontendQt) isNewVersionAvailable(showMessage bool) {
go func() { go func() {
defer s.Qml.ProcessFinished() defer f.Qml.ProcessFinished()
isUpToDate, latestVersionInfo, err := s.updates.CheckIsUpToDate() isUpToDate, latestVersionInfo, err := f.updates.CheckIsUpToDate()
if err != nil { if err != nil {
log.Warnln("Cannot retrieve version info: ", err) log.Warnln("Cannot retrieve version info: ", err)
s.checkInternet() f.checkInternet()
return return
} }
s.Qml.SetConnectionStatus(true) // if we are here connection is ok f.Qml.SetConnectionStatus(true) // if we are here connection is ok
if isUpToDate { if isUpToDate {
s.Qml.SetUpdateState(StatusUpToDate) f.Qml.SetUpdateState(StatusUpToDate)
if showMessage { if showMessage {
s.Qml.NotifyVersionIsTheLatest() f.Qml.NotifyVersionIsTheLatest()
} }
return return
} }
s.Qml.SetNewversion(latestVersionInfo.Version) f.Qml.SetNewversion(latestVersionInfo.Version)
s.Qml.SetChangelog(latestVersionInfo.ReleaseNotes) f.Qml.SetChangelog(latestVersionInfo.ReleaseNotes)
s.Qml.SetBugfixes(latestVersionInfo.ReleaseFixedBugs) f.Qml.SetBugfixes(latestVersionInfo.ReleaseFixedBugs)
s.Qml.SetLandingPage(latestVersionInfo.LandingPage) f.Qml.SetLandingPage(latestVersionInfo.LandingPage)
s.Qml.SetDownloadLink(latestVersionInfo.GetDownloadLink()) f.Qml.SetDownloadLink(latestVersionInfo.GetDownloadLink())
s.Qml.SetUpdateState(StatusNewVersionAvailable) f.Qml.SetUpdateState(StatusNewVersionAvailable)
}() }()
} }
func (s *FrontendQt) resetSource() { func (f *FrontendQt) resetSource() {
if s.transfer != nil { if f.transfer != nil {
s.transfer.ResetRules() f.transfer.ResetRules()
if err := s.loadStructuresForImport(); err != nil { if err := f.loadStructuresForImport(); err != nil {
log.WithError(err).Error("Cannot reload structures after reseting rules.") log.WithError(err).Error("Cannot reload structures after reseting rules.")
} }
} }
} }
// getLocalVersionInfo is identical to bridge. // getLocalVersionInfo is identical to bridge.
func (s *FrontendQt) getLocalVersionInfo() { func (f *FrontendQt) getLocalVersionInfo() {
defer s.Qml.ProcessFinished() defer f.Qml.ProcessFinished()
localVersion := s.updates.GetLocalVersion() localVersion := f.updates.GetLocalVersion()
s.Qml.SetNewversion(localVersion.Version) f.Qml.SetNewversion(localVersion.Version)
s.Qml.SetChangelog(localVersion.ReleaseNotes) f.Qml.SetChangelog(localVersion.ReleaseNotes)
s.Qml.SetBugfixes(localVersion.ReleaseFixedBugs) f.Qml.SetBugfixes(localVersion.ReleaseFixedBugs)
} }
// LeastUsedColor is intended to return color for creating a new inbox or label. // LeastUsedColor is intended to return color for creating a new inbox or label.
func (s *FrontendQt) leastUsedColor() string { func (f *FrontendQt) leastUsedColor() string {
if s.transfer == nil { if f.transfer == nil {
log.Errorln("Getting least used color before transfer exist.") log.Errorln("Getting least used color before transfer exist.")
return "#7272a7" return "#7272a7"
} }
m, err := s.transfer.TargetMailboxes() m, err := f.transfer.TargetMailboxes()
if err != nil { if err != nil {
log.Errorln("Getting least used color:", err) log.Errorln("Getting least used color:", err)
s.showError(err) f.showError(errUnknownError, err)
} }
return transfer.LeastUsedColor(m) return transfer.LeastUsedColor(m)
} }
// createLabelOrFolder performs an IE target mailbox creation. // createLabelOrFolder performs an IE target mailbox creation.
func (s *FrontendQt) createLabelOrFolder(email, name, color string, isLabel bool, sourceID string) bool { func (f *FrontendQt) createLabelOrFolder(email, name, color string, isLabel bool, sourceID string) bool {
// Prepare new mailbox. // Prepare new mailbox.
m := transfer.Mailbox{ m := transfer.Mailbox{
Name: name, Name: name,
@ -466,32 +461,28 @@ func (s *FrontendQt) createLabelOrFolder(email, name, color string, isLabel bool
// Select least used color if no color given. // Select least used color if no color given.
if m.Color == "" { if m.Color == "" {
m.Color = s.leastUsedColor() m.Color = f.leastUsedColor()
} }
f.TransferRules.BeginResetModel()
defer f.TransferRules.EndResetModel()
// Create mailbox. // Create mailbox.
newLabel, err := s.transfer.CreateTargetMailbox(m) m, err := f.transfer.CreateTargetMailbox(m)
if err != nil { if err != nil {
log.Errorln("Folder/Label creating:", err) log.Errorln("Folder/Label creating:", err)
s.showError(err)
return false
}
// TODO: notify UI of newly added folders/labels
/*errc := s.PMStructure.Load(email, false)
if errc != nil {
s.showError(errc)
return false
}*/
if sourceID != "" {
if isLabel { if isLabel {
s.ExternalStructure.addTargetLabelID(sourceID, newLabel.ID) f.showError(errCreateLabelFailed, err)
} else { } else {
s.ExternalStructure.setTargetFolderID(sourceID, newLabel.ID) f.showError(errCreateFolderFailed, err)
} }
return false
} }
if sourceID == "-1" {
f.transfer.SetGlobalMailbox(&m)
} else {
f.TransferRules.addTargetID(sourceID, m.Hash())
}
return true return true
} }

View File

@ -19,14 +19,19 @@
package qtie package qtie
import "github.com/ProtonMail/proton-bridge/internal/transfer" import (
"github.com/pkg/errors"
"github.com/ProtonMail/proton-bridge/internal/transfer"
)
// wrapper for QML // wrapper for QML
func (f *FrontendQt) setupAndLoadForImport(isFromIMAP bool, sourcePath, sourceEmail, sourcePassword, sourceServer, sourcePort, targetAddress string) { func (f *FrontendQt) setupAndLoadForImport(isFromIMAP bool, sourcePath, sourceEmail, sourcePassword, sourceServer, sourcePort, targetAddress string) {
errCode := errUnknownError
var err error var err error
defer func() { defer func() {
if err != nil { if err != nil {
f.showError(err) f.showError(errCode, err)
f.Qml.ImportStructuresLoadFinished(false) f.Qml.ImportStructuresLoadFinished(false)
} else { } else {
f.Qml.ImportStructuresLoadFinished(true) f.Qml.ImportStructuresLoadFinished(true)
@ -36,11 +41,23 @@ func (f *FrontendQt) setupAndLoadForImport(isFromIMAP bool, sourcePath, sourceEm
if isFromIMAP { if isFromIMAP {
f.transfer, err = f.ie.GetRemoteImporter(targetAddress, sourceEmail, sourcePassword, sourceServer, sourcePort) f.transfer, err = f.ie.GetRemoteImporter(targetAddress, sourceEmail, sourcePassword, sourceServer, sourcePort)
if err != nil { if err != nil {
switch {
case errors.Is(err, &transfer.ErrIMAPConnection{}):
errCode = errWrongServerPathOrPort
case errors.Is(err, &transfer.ErrIMAPAuth{}):
errCode = errWrongLoginOrPassword
case errors.Is(err, &transfer.ErrIMAPAuthMethod{}):
errCode = errWrongAuthMethod
default:
errCode = errRemoteSourceLoadFailed
}
return return
} }
} else { } else {
f.transfer, err = f.ie.GetLocalImporter(targetAddress, sourcePath) f.transfer, err = f.ie.GetLocalImporter(targetAddress, sourcePath)
if err != nil { if err != nil {
// The only error can be problem to load PM user and address.
errCode = errPMLoadFailed
return return
} }
} }
@ -51,27 +68,7 @@ func (f *FrontendQt) setupAndLoadForImport(isFromIMAP bool, sourcePath, sourceEm
} }
func (f *FrontendQt) loadStructuresForImport() error { func (f *FrontendQt) loadStructuresForImport() error {
f.PMStructure.Clear() f.TransferRules.setTransfer(f.transfer)
targetMboxes, err := f.transfer.TargetMailboxes()
if err != nil {
return err
}
for _, mbox := range targetMboxes {
rule := &transfer.Rule{}
f.PMStructure.addEntry(newFolderInfo(mbox, rule))
}
f.ExternalStructure.Clear()
sourceMboxes, err := f.transfer.SourceMailboxes()
if err != nil {
return err
}
for _, mbox := range sourceMboxes {
rule := f.transfer.GetRule(mbox)
f.ExternalStructure.addEntry(newFolderInfo(mbox, rule))
}
f.ExternalStructure.transfer = f.transfer
return nil return nil
} }
@ -82,8 +79,9 @@ func (f *FrontendQt) StartImport(email string) { // TODO email not needed
f.Qml.SetProgress(0.0) f.Qml.SetProgress(0.0)
f.Qml.SetTotal(1) f.Qml.SetTotal(1)
f.Qml.SetImportLogFileName("") f.Qml.SetImportLogFileName("")
f.ErrorList.Clear()
progress := f.transfer.Start() progress := f.transfer.Start()
f.Qml.SetImportLogFileName(progress.FileReport())
f.setProgressManager(progress) f.setProgressManager(progress)
} }

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/>.
// +build !nogui
package qtie
import (
qtcommon "github.com/ProtonMail/proton-bridge/internal/frontend/qt-common"
"github.com/ProtonMail/proton-bridge/internal/transfer"
"github.com/sirupsen/logrus"
"github.com/therecipe/qt/core"
)
// MboxList is an interface between QML and targets for given rule.
type MboxList struct {
core.QAbstractListModel
containsFolders bool // Provides only folders if true. On the other hand provides only labels if false
transfer *transfer.Transfer
rule *transfer.Rule
log *logrus.Entry
_ int `property:"selectedIndex"`
_ func() `constructor:"init"`
}
func init() {
// This is needed so the type exists in QML files.
MboxList_QRegisterMetaType()
}
func newMboxList(t *TransferRules, rule *transfer.Rule, containsFolders bool) *MboxList {
m := NewMboxList(t)
m.BeginResetModel()
m.transfer = t.transfer
m.rule = rule
m.containsFolders = containsFolders
m.log = log.
WithField("rule", m.rule.SourceMailbox.Hash()).
WithField("folders", m.containsFolders)
m.EndResetModel()
m.itemsChanged(rule)
return m
}
func (m *MboxList) init() {
m.ConnectRowCount(m.rowCount)
m.ConnectRoleNames(m.roleNames)
m.ConnectData(m.data)
}
func (m *MboxList) rowCount(index *core.QModelIndex) int {
return len(m.targetMailboxes())
}
func (m *MboxList) roleNames() map[int]*core.QByteArray {
m.log.
WithField("isActive", MboxIsActive).
WithField("id", MboxID).
WithField("color", MboxColor).
Debug("role names")
return map[int]*core.QByteArray{
MboxIsActive: qtcommon.NewQByteArrayFromString("isActive"),
MboxID: qtcommon.NewQByteArrayFromString("mboxID"),
MboxName: qtcommon.NewQByteArrayFromString("name"),
MboxType: qtcommon.NewQByteArrayFromString("type"),
MboxColor: qtcommon.NewQByteArrayFromString("iconColor"),
}
}
func (m *MboxList) data(index *core.QModelIndex, role int) *core.QVariant {
allTargets := m.targetMailboxes()
i, valid := index.Row(), index.IsValid()
l := m.log.WithField("row", i).WithField("role", role)
l.Trace("called data()")
if !valid || i >= len(allTargets) {
l.WithField("row", i).Warning("Invalid index")
return core.NewQVariant()
}
if m.transfer == nil {
l.Warning("Requested mbox list data before transfer is connected")
return qtcommon.NewQVariantString("")
}
mbox := allTargets[i]
switch role {
case MboxIsActive:
for _, selectedMailbox := range m.rule.TargetMailboxes {
if selectedMailbox.Hash() == mbox.Hash() {
return qtcommon.NewQVariantBool(true)
}
}
return qtcommon.NewQVariantBool(false)
case MboxID:
return qtcommon.NewQVariantString(mbox.Hash())
case MboxName, int(core.Qt__DisplayRole):
return qtcommon.NewQVariantString(mbox.Name)
case MboxType:
t := "label"
if mbox.IsExclusive {
t = "folder"
}
return qtcommon.NewQVariantString(t)
case MboxColor:
return qtcommon.NewQVariantString(mbox.Color)
default:
l.Error("Requested mbox list data with unknown role")
return qtcommon.NewQVariantString("")
}
}
func (m *MboxList) targetMailboxes() []transfer.Mailbox {
if m.transfer == nil {
m.log.Warning("Requested target mailboxes before transfer is connected")
}
mailboxes, err := m.transfer.TargetMailboxes()
if err != nil {
m.log.WithError(err).Error("Unable to get target mailboxes")
}
return m.filter(mailboxes)
}
func (m *MboxList) filter(mailboxes []transfer.Mailbox) (filtered []transfer.Mailbox) {
for _, mailbox := range mailboxes {
if mailbox.IsExclusive == m.containsFolders {
filtered = append(filtered, mailbox)
}
}
return
}
func (m *MboxList) itemsChanged(rule *transfer.Rule) {
m.rule = rule
allTargets := m.targetMailboxes()
l := m.log.WithField("count", len(allTargets))
l.Trace("called itemChanged()")
defer func() {
l.WithField("selected", m.SelectedIndex()).Trace("index updated")
}()
// NOTE: Be careful with indices: If they are invalid the DataChanged
// signal will not be sent to QML e.g. `end == rowCount - 1`
if len(allTargets) > 0 {
begin := m.Index(0, 0, core.NewQModelIndex())
end := m.Index(len(allTargets)-1, 0, core.NewQModelIndex())
changedRoles := []int{MboxIsActive}
m.DataChanged(begin, end, changedRoles)
}
for index, targetMailbox := range allTargets {
for _, selectedTarget := range m.rule.TargetMailboxes {
if targetMailbox.Hash() == selectedTarget.Hash() {
m.SetSelectedIndex(index)
return
}
}
}
m.SetSelectedIndex(-1)
}

View File

@ -0,0 +1,377 @@
// 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/>.
// +build !nogui
package qtie
import (
qtcommon "github.com/ProtonMail/proton-bridge/internal/frontend/qt-common"
"github.com/ProtonMail/proton-bridge/internal/transfer"
"github.com/therecipe/qt/core"
)
// TransferRules is an interface between QML and transfer.
type TransferRules struct {
core.QAbstractListModel
transfer *transfer.Transfer
targetFoldersCache map[string]*MboxList
targetLabelsCache map[string]*MboxList
_ func() `constructor:"init"`
_ func(sourceID string) *MboxList `slot:"targetFolders,auto"`
_ func(sourceID string) *MboxList `slot:"targetLabels,auto"`
_ func(sourceID string, isActive bool) `slot:"setIsRuleActive,auto"`
_ func(groupName string, isActive bool) `slot:"setIsGroupActive,auto"`
_ func(sourceID string, fromDate int64, toDate int64) `slot:"setFromToDate,auto"`
_ func(sourceID string, targetID string) `slot:"addTargetID,auto"`
_ func(sourceID string, targetID string) `slot:"removeTargetID,auto"`
_ int `property:"globalFromDate"`
_ int `property:"globalToDate"`
_ bool `property:"isLabelGroupSelected"`
_ bool `property:"isFolderGroupSelected"`
}
func init() {
// This is needed so the type exists in QML files.
TransferRules_QRegisterMetaType()
}
func (t *TransferRules) init() {
log.Trace("Initializing transfer rules")
t.targetFoldersCache = make(map[string]*MboxList)
t.targetLabelsCache = make(map[string]*MboxList)
t.SetGlobalFromDate(0)
t.SetGlobalToDate(0)
t.ConnectRowCount(t.rowCount)
t.ConnectRoleNames(t.roleNames)
t.ConnectData(t.data)
}
func (t *TransferRules) rowCount(index *core.QModelIndex) int {
if t.transfer == nil {
return 0
}
return len(t.transfer.GetRules())
}
func (t *TransferRules) roleNames() map[int]*core.QByteArray {
return map[int]*core.QByteArray{
MboxIsActive: qtcommon.NewQByteArrayFromString("isActive"),
MboxID: qtcommon.NewQByteArrayFromString("mboxID"),
MboxName: qtcommon.NewQByteArrayFromString("name"),
MboxType: qtcommon.NewQByteArrayFromString("type"),
MboxColor: qtcommon.NewQByteArrayFromString("iconColor"),
RuleTargetLabelColors: qtcommon.NewQByteArrayFromString("labelColors"),
RuleFromDate: qtcommon.NewQByteArrayFromString("fromDate"),
RuleToDate: qtcommon.NewQByteArrayFromString("toDate"),
}
}
func (t *TransferRules) data(index *core.QModelIndex, role int) *core.QVariant {
i, valid := index.Row(), index.IsValid()
if !valid || i >= t.rowCount(index) {
log.WithField("row", i).Warning("Invalid index")
return core.NewQVariant()
}
log := log.WithField("row", i).WithField("role", role)
if t.transfer == nil {
log.Warning("Requested transfer rules data before transfer is connected")
return qtcommon.NewQVariantString("")
}
rule := t.transfer.GetRules()[i]
switch role {
case MboxIsActive:
return qtcommon.NewQVariantBool(rule.Active)
case MboxID:
return qtcommon.NewQVariantString(rule.SourceMailbox.Hash())
case MboxName:
return qtcommon.NewQVariantString(rule.SourceMailbox.Name)
case MboxType:
if rule.SourceMailbox.IsSystemFolder() {
return qtcommon.NewQVariantString(FolderTypeSystem)
}
if rule.SourceMailbox.IsExclusive {
return qtcommon.NewQVariantString(FolderTypeFolder)
}
return qtcommon.NewQVariantString(FolderTypeLabel)
case MboxColor:
return qtcommon.NewQVariantString(rule.SourceMailbox.Color)
case RuleTargetLabelColors:
colors := ""
for _, m := range rule.TargetMailboxes {
if m.IsExclusive {
continue
}
if colors != "" {
colors += ";"
}
colors += m.Color
}
return qtcommon.NewQVariantString(colors)
case RuleFromDate:
return qtcommon.NewQVariantLong(rule.FromTime)
case RuleToDate:
return qtcommon.NewQVariantLong(rule.ToTime)
default:
log.Error("Requested transfer rules data with unknown role")
return qtcommon.NewQVariantString("")
}
}
func (t *TransferRules) setTransfer(transfer *transfer.Transfer) {
log.Debug("Setting transfer")
t.BeginResetModel()
defer t.EndResetModel()
t.transfer = transfer
t.updateGroupSelection()
}
// Getters
func (t *TransferRules) targetFolders(sourceID string) *MboxList {
rule := t.getRule(sourceID)
if rule == nil {
return nil
}
if t.targetFoldersCache[sourceID] == nil {
log.WithField("source", sourceID).Debug("New target folder")
t.targetFoldersCache[sourceID] = newMboxList(t, rule, true)
}
return t.targetFoldersCache[sourceID]
}
func (t *TransferRules) targetLabels(sourceID string) *MboxList {
rule := t.getRule(sourceID)
if rule == nil {
return nil
}
if t.targetLabelsCache[sourceID] == nil {
log.WithField("source", sourceID).Debug("New target label")
t.targetLabelsCache[sourceID] = newMboxList(t, rule, false)
}
return t.targetLabelsCache[sourceID]
}
// Setters
func (t *TransferRules) setIsGroupActive(groupName string, isActive bool) {
wantExclusive := (groupName == FolderTypeLabel)
for _, rule := range t.transfer.GetRules() {
if rule.SourceMailbox.IsExclusive != wantExclusive {
continue
}
if rule.SourceMailbox.IsSystemFolder() {
continue
}
if rule.Active != isActive {
t.setIsRuleActive(rule.SourceMailbox.Hash(), isActive)
}
}
}
func (t *TransferRules) setIsRuleActive(sourceID string, isActive bool) {
log.WithField("source", sourceID).WithField("active", isActive).Trace("Setting rule as active/inactive")
rule := t.getRule(sourceID)
if rule == nil {
return
}
if isActive {
t.setRule(rule.SourceMailbox, rule.TargetMailboxes, rule.FromTime, rule.ToTime, []int{MboxIsActive})
} else {
t.unsetRule(rule.SourceMailbox)
}
}
func (t *TransferRules) setFromToDate(sourceID string, fromTime int64, toTime int64) {
log.WithField("source", sourceID).WithField("fromTime", fromTime).WithField("toTime", toTime).Trace("Setting from and to dates")
if sourceID == "-1" {
t.transfer.SetGlobalTimeLimit(fromTime, toTime)
return
}
rule := t.getRule(sourceID)
if rule == nil {
return
}
t.setRule(rule.SourceMailbox, rule.TargetMailboxes, fromTime, toTime, []int{RuleFromDate, RuleToDate})
}
func (t *TransferRules) addTargetID(sourceID string, targetID string) {
log.WithField("source", sourceID).WithField("target", targetID).Trace("Adding target")
rule := t.getRule(sourceID)
if rule == nil {
return
}
targetMailboxToAdd := t.getMailbox(t.transfer.TargetMailboxes, targetID)
if targetMailboxToAdd == nil {
return
}
newTargetMailboxes := []transfer.Mailbox{}
found := false
for _, targetMailbox := range rule.TargetMailboxes {
if targetMailbox.Hash() == targetMailboxToAdd.Hash() {
found = true
}
if !targetMailboxToAdd.IsExclusive || (targetMailboxToAdd.IsExclusive && !targetMailbox.IsExclusive) {
newTargetMailboxes = append(newTargetMailboxes, targetMailbox)
}
}
if !found {
newTargetMailboxes = append(newTargetMailboxes, *targetMailboxToAdd)
}
t.setRule(rule.SourceMailbox, newTargetMailboxes, rule.FromTime, rule.ToTime, []int{RuleTargetLabelColors})
}
func (t *TransferRules) removeTargetID(sourceID string, targetID string) {
log.WithField("source", sourceID).WithField("target", targetID).Trace("Removing target")
rule := t.getRule(sourceID)
if rule == nil {
return
}
targetMailboxToRemove := t.getMailbox(t.transfer.TargetMailboxes, targetID)
if targetMailboxToRemove == nil {
return
}
newTargetMailboxes := []transfer.Mailbox{}
for _, targetMailbox := range rule.TargetMailboxes {
if targetMailbox.Hash() != targetMailboxToRemove.Hash() {
newTargetMailboxes = append(newTargetMailboxes, targetMailbox)
}
}
t.setRule(rule.SourceMailbox, newTargetMailboxes, rule.FromTime, rule.ToTime, []int{RuleTargetLabelColors})
}
// Helpers
func (t *TransferRules) getRule(sourceID string) *transfer.Rule {
mailbox := t.getMailbox(t.transfer.SourceMailboxes, sourceID)
if mailbox == nil {
return nil
}
return t.transfer.GetRule(*mailbox)
}
func (t *TransferRules) getMailbox(mailboxesGetter func() ([]transfer.Mailbox, error), sourceID string) *transfer.Mailbox {
if t.transfer == nil {
log.Warn("Getting mailbox without avaiable transfer")
return nil
}
mailboxes, err := mailboxesGetter()
if err != nil {
log.WithError(err).Error("Failed to get source mailboxes")
return nil
}
for _, mailbox := range mailboxes {
if mailbox.Hash() == sourceID {
return &mailbox
}
}
log.WithField("source", sourceID).Error("Mailbox not found for source")
return nil
}
func (t *TransferRules) setRule(sourceMailbox transfer.Mailbox, targetMailboxes []transfer.Mailbox, fromTime, toTime int64, changedRoles []int) {
if err := t.transfer.SetRule(sourceMailbox, targetMailboxes, fromTime, toTime); err != nil {
log.WithError(err).WithField("source", sourceMailbox.Hash()).Error("Failed to set rule")
}
t.ruleChanged(sourceMailbox, changedRoles)
}
func (t *TransferRules) unsetRule(sourceMailbox transfer.Mailbox) {
t.transfer.UnsetRule(sourceMailbox)
t.ruleChanged(sourceMailbox, []int{MboxIsActive})
}
func (t *TransferRules) ruleChanged(sourceMailbox transfer.Mailbox, changedRoles []int) {
for row, rule := range t.transfer.GetRules() {
if rule.SourceMailbox.Hash() != sourceMailbox.Hash() {
continue
}
t.targetFolders(sourceMailbox.Hash()).itemsChanged(rule)
t.targetLabels(sourceMailbox.Hash()).itemsChanged(rule)
index := t.Index(row, 0, core.NewQModelIndex())
if !index.IsValid() || row >= t.rowCount(index) {
log.WithField("row", row).Warning("Invalid index")
return
}
t.DataChanged(index, index, changedRoles)
break
}
t.updateGroupSelection()
}
func (t *TransferRules) updateGroupSelection() {
areAllLabelsSelected, areAllFoldersSelected := true, true
for _, rule := range t.transfer.GetRules() {
if rule.Active {
continue
}
if rule.SourceMailbox.IsSystemFolder() {
continue
}
if rule.SourceMailbox.IsExclusive {
areAllFoldersSelected = false
} else {
areAllLabelsSelected = false
}
if !areAllLabelsSelected && !areAllFoldersSelected {
break
}
}
t.SetIsLabelGroupSelected(areAllLabelsSelected)
t.SetIsFolderGroupSelected(areAllFoldersSelected)
}

View File

@ -71,7 +71,7 @@ type GoQMLInterface struct {
_ func() `signal:"openManual"` _ func() `signal:"openManual"`
_ func(showMessage bool) `signal:"runCheckVersion"` _ func(showMessage bool) `signal:"runCheckVersion"`
_ func() `slot:"getLocalVersionInfo"` _ func() `slot:"getLocalVersionInfo"`
_ func(fname string) `slot:"loadImportReports"` _ func() `slot:"loadImportReports"`
_ func() `slot:"quit"` _ func() `slot:"quit"`
_ func() `slot:"loadAccounts"` _ func() `slot:"loadAccounts"`
@ -87,7 +87,7 @@ type GoQMLInterface struct {
_ func() string `slot:"getBackendVersion"` _ func() string `slot:"getBackendVersion"`
_ func(description, client, address string) bool `slot:"sendBug"` _ func(description, client, address string) bool `slot:"sendBug"`
_ func(address, fname string) bool `slot:"sendImportReport"` _ func(address string) bool `slot:"sendImportReport"`
_ func(address string) `slot:"loadStructureForExport"` _ func(address string) `slot:"loadStructureForExport"`
_ func() string `slot:"leastUsedColor"` _ func() string `slot:"leastUsedColor"`
_ func(username string, name string, color string, isLabel bool, sourceID string) bool `slot:"createLabelOrFolder"` _ func(username string, name string, color string, isLabel bool, sourceID string) bool `slot:"createLabelOrFolder"`
@ -104,13 +104,13 @@ type GoQMLInterface struct {
_ func(evType string, msg string) `signal:"emitEvent"` _ func(evType string, msg string) `signal:"emitEvent"`
_ func(tabIndex int, message string) `signal:"notifyBubble"` _ func(tabIndex int, message string) `signal:"notifyBubble"`
_ func() `signal:"bubbleClosed"` _ func() `signal:"bubbleClosed"`
_ func() `signal:"simpleErrorHappen"` _ func() `signal:"simpleErrorHappen"`
_ func() `signal:"askErrorHappen"` _ func() `signal:"askErrorHappen"`
_ func() `signal:"retryErrorHappen"` _ func() `signal:"retryErrorHappen"`
_ func() `signal:"pauseProcess"` _ func() `signal:"pauseProcess"`
_ func() `signal:"resumeProcess"` _ func() `signal:"resumeProcess"`
_ func(clearUnfinished bool) `signal:"cancelProcess"` _ func() `signal:"cancelProcess"`
_ func(iAccount int, prefRem bool) `slot:"deleteAccount"` _ func(iAccount int, prefRem bool) `slot:"deleteAccount"`
_ func(iAccount int) `slot:"logoutAccount"` _ func(iAccount int) `slot:"logoutAccount"`

View File

@ -114,6 +114,7 @@ type ImportExporter interface {
GetMBOXExporter(string, string) (*transfer.Transfer, error) GetMBOXExporter(string, string) (*transfer.Transfer, error)
SetCurrentOS(os string) SetCurrentOS(os string)
ReportBug(osType, osVersion, description, accountName, address, emailClient string) error ReportBug(osType, osVersion, description, accountName, address, emailClient string) error
ReportFile(osType, osVersion, accountName, address string, logdata []byte) error
} }
type importExportWrap struct { type importExportWrap struct {

View File

@ -15,8 +15,8 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// Code generated by ./credits.sh at Mon Jul 13 14:02:21 CEST 2020. DO NOT EDIT. // Code generated by ./credits.sh at Fri 07 Aug 2020 06:34:27 AM CEST. DO NOT EDIT.
package importexport package importexport
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-id;github.com/ProtonMail/go-smtp;github.com/ProtonMail/go-vcard;github.com/ProtonMail/gopenpgp/v2;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-resty/resty/v2;github.com/golang/mock;github.com/google/go-cmp;github.com/gopherjs/gopherjs;github.com/hashicorp/go-multierror;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/miekg/dns;github.com/myesui/uuid;github.com/nsf/jsondiff;github.com/pkg/errors;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/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/gopherjs/gopherjs;github.com/go-resty/resty/v2;github.com/hashicorp/go-multierror;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/miekg/dns;github.com/myesui/uuid;github.com/nsf/jsondiff;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;"

View File

@ -19,8 +19,11 @@
package importexport package importexport
import ( import (
"bytes"
"github.com/ProtonMail/proton-bridge/internal/transfer" "github.com/ProtonMail/proton-bridge/internal/transfer"
"github.com/ProtonMail/proton-bridge/internal/users" "github.com/ProtonMail/proton-bridge/internal/users"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/ProtonMail/proton-bridge/pkg/listener" "github.com/ProtonMail/proton-bridge/pkg/listener"
logrus "github.com/sirupsen/logrus" logrus "github.com/sirupsen/logrus"
@ -61,15 +64,17 @@ func (ie *ImportExport) ReportBug(osType, osVersion, description, accountName, a
defer c.Logout() defer c.Logout()
title := "[Import-Export] Bug" title := "[Import-Export] Bug"
if err := c.ReportBugWithEmailClient( report := pmapi.ReportReq{
osType, OS: osType,
osVersion, OSVersion: osVersion,
title, Browser: emailClient,
description, Title: title,
accountName, Description: description,
address, Username: accountName,
emailClient, Email: address,
); err != nil { }
if err := c.Report(report); err != nil {
log.Error("Reporting bug failed: ", err) log.Error("Reporting bug failed: ", err)
return err return err
} }
@ -79,6 +84,35 @@ func (ie *ImportExport) ReportBug(osType, osVersion, description, accountName, a
return nil return nil
} }
// ReportFile submits import report file
func (ie *ImportExport) ReportFile(osType, osVersion, accountName, address string, logdata []byte) error {
c := ie.clientManager.GetAnonymousClient()
defer c.Logout()
title := "[Import-Export] report file"
description := "An import/export report from the user swam down the river."
report := pmapi.ReportReq{
OS: osType,
OSVersion: osVersion,
Description: description,
Title: title,
Username: accountName,
Email: address,
}
report.AddAttachment("log", "report.log", bytes.NewReader(logdata))
if err := c.Report(report); err != nil {
log.Error("Sending report failed: ", err)
return err
}
log.Info("Report successfully sent")
return nil
}
// GetLocalImporter returns transferrer from local EML or MBOX structure to ProtonMail account. // GetLocalImporter returns transferrer from local EML or MBOX structure to ProtonMail account.
func (ie *ImportExport) GetLocalImporter(address, path string) (*transfer.Transfer, error) { func (ie *ImportExport) GetLocalImporter(address, path string) (*transfer.Transfer, error) {
source := transfer.NewLocalProvider(path) source := transfer.NewLocalProvider(path)
@ -130,7 +164,7 @@ func (ie *ImportExport) getPMAPIProvider(address string) (*transfer.PMAPIProvide
addressID, err := user.GetAddressID(address) addressID, err := user.GetAddressID(address)
if err != nil { if err != nil {
return nil, err log.WithError(err).Info("Address does not exist, using all addresses")
} }
return transfer.NewPMAPIProvider(ie.clientManager, user.ID(), addressID) return transfer.NewPMAPIProvider(ie.clientManager, user.ID(), addressID)

View File

@ -15,7 +15,7 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// Code generated by ./release-notes.sh at 'Thu Jun 25 10:06:16 CEST 2020'. DO NOT EDIT. // Code generated by ./release-notes.sh at 'Fri 07 Aug 2020 06:34:27 AM CEST'. DO NOT EDIT.
package importexport package importexport

View File

@ -5,10 +5,9 @@
package mocks package mocks
import ( import (
reflect "reflect"
pmapi "github.com/ProtonMail/proton-bridge/pkg/pmapi" pmapi "github.com/ProtonMail/proton-bridge/pkg/pmapi"
gomock "github.com/golang/mock/gomock" gomock "github.com/golang/mock/gomock"
reflect "reflect"
) )
// MockPanicHandler is a mock of PanicHandler interface // MockPanicHandler is a mock of PanicHandler interface

View File

@ -5,10 +5,9 @@
package mocks package mocks
import ( import (
gomock "github.com/golang/mock/gomock"
reflect "reflect" reflect "reflect"
time "time" time "time"
gomock "github.com/golang/mock/gomock"
) )
// MockListener is a mock of Listener interface // MockListener is a mock of Listener interface

View File

@ -33,6 +33,11 @@ type Mailbox struct {
IsExclusive bool IsExclusive bool
} }
// IsSystemFolder returns true when ID corresponds to PM system folder.
func (m Mailbox) IsSystemFolder() bool {
return pmapi.IsSystemLabel(m.ID)
}
// Hash returns unique identifier to be used for matching. // Hash returns unique identifier to be used for matching.
func (m Mailbox) Hash() string { func (m Mailbox) Hash() string {
return fmt.Sprintf("%x", sha256.Sum256([]byte(m.Name))) return fmt.Sprintf("%x", sha256.Sum256([]byte(m.Name)))

View File

@ -198,7 +198,7 @@ func (p *Progress) callWrap(callback func() error) {
break break
} }
p.Pause(err.Error()) p.Pause("paused due to " + err.Error())
} }
} }
@ -333,3 +333,11 @@ func (p *Progress) GenerateBugReport() []byte {
} }
return bugReport.getData() return bugReport.getData()
} }
func (p *Progress) FileReport() (path string) {
if r := p.fileReport; r != nil {
path = r.path
}
return
}

View File

@ -39,9 +39,13 @@ func (p *EMLProvider) ID() string {
// Mailboxes returns all available folder names from root of EML files. // Mailboxes returns all available folder names from root of EML files.
// In case the same folder name is used more than once (for example root/a/foo // In case the same folder name is used more than once (for example root/a/foo
// and root/b/foo), it's treated as the same folder. // and root/b/foo), it's treated as the same folder.
func (p *EMLProvider) Mailboxes(includeEmpty, includeAllMail bool) ([]Mailbox, error) { func (p *EMLProvider) Mailboxes(includeEmpty, includeAllMail bool) (mailboxes []Mailbox, err error) {
// Special case for exporting--we don't know the path before setup if finished.
if p.root == "" {
return
}
var folderNames []string var folderNames []string
var err error
if includeEmpty { if includeEmpty {
folderNames, err = getFolderNames(p.root) folderNames, err = getFolderNames(p.root)
} else { } else {
@ -51,7 +55,6 @@ func (p *EMLProvider) Mailboxes(includeEmpty, includeAllMail bool) ([]Mailbox, e
return nil, err return nil, err
} }
mailboxes := []Mailbox{}
for _, folderName := range folderNames { for _, folderName := range folderNames {
mailboxes = append(mailboxes, Mailbox{ mailboxes = append(mailboxes, Mailbox{
ID: "", ID: "",

View File

@ -0,0 +1,66 @@
// 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
// imapError is base for all IMAP errors.
type imapError struct {
Message string
Err error
}
func (e imapError) Error() string {
return e.Message + ": " + e.Err.Error()
}
func (e imapError) Unwrap() error {
return e.Err
}
func (e imapError) Cause() error {
return e.Err
}
// ErrIMAPConnection is error representing connection issues.
type ErrIMAPConnection struct {
imapError
}
func (e ErrIMAPConnection) Is(target error) bool {
_, ok := target.(*ErrIMAPConnection)
return ok
}
// ErrIMAPAuth is error representing authentication issues.
type ErrIMAPAuth struct {
imapError
}
func (e ErrIMAPAuth) Is(target error) bool {
_, ok := target.(*ErrIMAPAuth)
return ok
}
// ErrIMAPAuthMethod is error representing wrong auth method.
type ErrIMAPAuthMethod struct {
imapError
}
func (e ErrIMAPAuthMethod) Is(target error) bool {
_, ok := target.(*ErrIMAPAuthMethod)
return ok
}

View File

@ -137,7 +137,7 @@ func (p *IMAPProvider) auth() error { //nolint[funlen]
log.Info("Connecting to server") log.Info("Connecting to server")
if _, err := net.DialTimeout("tcp", p.addr, imapDialTimeout); err != nil { if _, err := net.DialTimeout("tcp", p.addr, imapDialTimeout); err != nil {
return errors.Wrap(err, "failed to dial server") return ErrIMAPConnection{imapError{Err: err, Message: "failed to dial server"}}
} }
var client *imapClient.Client var client *imapClient.Client
@ -149,7 +149,7 @@ func (p *IMAPProvider) auth() error { //nolint[funlen]
client, err = imapClient.DialTLS(p.addr, nil) client, err = imapClient.DialTLS(p.addr, nil)
} }
if err != nil { if err != nil {
return errors.Wrap(err, "failed to connect to server") return ErrIMAPConnection{imapError{Err: err, Message: "failed to connect to server"}}
} }
client.ErrorLog = &imapErrorLogger{logrus.WithField("pkg", "imap-client")} client.ErrorLog = &imapErrorLogger{logrus.WithField("pkg", "imap-client")}
@ -170,7 +170,7 @@ func (p *IMAPProvider) auth() error { //nolint[funlen]
capability, err := p.client.Capability() capability, err := p.client.Capability()
log.WithField("capability", capability).WithError(err).Debug("Server capability") log.WithField("capability", capability).WithError(err).Debug("Server capability")
if err != nil { if err != nil {
return errors.Wrap(err, "failed to get capabilities") return ErrIMAPConnection{imapError{Err: err, Message: "failed to get capabilities"}}
} }
// SASL AUTH PLAIN // SASL AUTH PLAIN
@ -178,7 +178,7 @@ func (p *IMAPProvider) auth() error { //nolint[funlen]
log.Debug("Trying plain auth") log.Debug("Trying plain auth")
authPlain := sasl.NewPlainClient("", p.username, p.password) authPlain := sasl.NewPlainClient("", p.username, p.password)
if err = p.client.Authenticate(authPlain); err != nil { if err = p.client.Authenticate(authPlain); err != nil {
return errors.Wrap(err, "plain auth failed") return ErrIMAPAuth{imapError{Err: err, Message: "plain auth failed"}}
} }
} }
@ -186,12 +186,12 @@ func (p *IMAPProvider) auth() error { //nolint[funlen]
if ok, _ := p.client.Support("IMAP4rev1"); p.client.State() == imap.NotAuthenticatedState && ok { if ok, _ := p.client.Support("IMAP4rev1"); p.client.State() == imap.NotAuthenticatedState && ok {
log.Debug("Trying login") log.Debug("Trying login")
if err = p.client.Login(p.username, p.password); err != nil { if err = p.client.Login(p.username, p.password); err != nil {
return errors.Wrap(err, "login failed") return ErrIMAPAuth{imapError{Err: err, Message: "login failed"}}
} }
} }
if p.client.State() == imap.NotAuthenticatedState { if p.client.State() == imap.NotAuthenticatedState {
return errors.New("unknown auth method") return ErrIMAPAuthMethod{imapError{Err: err, Message: "unknown auth method"}}
} }
log.Info("Logged in") log.Info("Logged in")

View File

@ -38,20 +38,24 @@ type PMAPIProvider struct {
// NewPMAPIProvider returns new PMAPIProvider. // NewPMAPIProvider returns new PMAPIProvider.
func NewPMAPIProvider(clientManager ClientManager, userID, addressID string) (*PMAPIProvider, error) { func NewPMAPIProvider(clientManager ClientManager, userID, addressID string) (*PMAPIProvider, error) {
keyRing, err := clientManager.GetClient(userID).KeyRingForAddressID(addressID) provider := &PMAPIProvider{
if err != nil {
return nil, errors.Wrap(err, "failed to get key ring")
}
return &PMAPIProvider{
clientManager: clientManager, clientManager: clientManager,
userID: userID, userID: userID,
addressID: addressID, addressID: addressID,
keyRing: keyRing,
importMsgReqMap: map[string]*pmapi.ImportMsgReq{}, importMsgReqMap: map[string]*pmapi.ImportMsgReq{},
importMsgReqSize: 0, importMsgReqSize: 0,
}, nil }
if addressID != "" {
keyRing, err := clientManager.GetClient(userID).KeyRingForAddressID(addressID)
if err != nil {
return nil, errors.Wrap(err, "failed to get key ring")
}
provider.keyRing = keyRing
}
return provider, nil
} }
func (p *PMAPIProvider) client() pmapi.Client { func (p *PMAPIProvider) client() pmapi.Client {
@ -86,7 +90,14 @@ func (p *PMAPIProvider) Mailboxes(includeEmpty, includeAllMail bool) ([]Mailbox,
} }
} }
mailboxes := getSystemMailboxes(includeAllMail) mailboxes := []Mailbox{}
for _, mailbox := range getSystemMailboxes(includeAllMail) {
if !includeEmpty && emptyLabelsMap[mailbox.ID] {
continue
}
mailboxes = append(mailboxes, mailbox)
}
for _, label := range sortedLabels { for _, label := range sortedLabels {
if !includeEmpty && emptyLabelsMap[label.ID] { if !includeEmpty && emptyLabelsMap[label.ID] {
continue continue

View File

@ -86,14 +86,15 @@ func (p *PMAPIProvider) transferTo(rule *Rule, progress *Progress, ch chan<- Mes
progress.callWrap(func() error { progress.callWrap(func() error {
desc := false desc := false
pmapiMessages, count, err := p.listMessages(&pmapi.MessagesFilter{ pmapiMessages, count, err := p.listMessages(&pmapi.MessagesFilter{
LabelID: rule.SourceMailbox.ID, AddressID: p.addressID,
Begin: rule.FromTime, LabelID: rule.SourceMailbox.ID,
End: rule.ToTime, Begin: rule.FromTime,
BeginID: nextID, End: rule.ToTime,
PageSize: pmapiListPageSize, BeginID: nextID,
Page: 0, PageSize: pmapiListPageSize,
Sort: "ID", Page: 0,
Desc: &desc, Sort: "ID",
Desc: &desc,
}) })
if err != nil { if err != nil {
return err return err

View File

@ -22,6 +22,7 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"sort"
"strings" "strings"
"time" "time"
@ -42,6 +43,11 @@ type transferRules struct {
// E.g., every message will be imported into this mailbox. // E.g., every message will be imported into this mailbox.
globalMailbox *Mailbox globalMailbox *Mailbox
// globalFromTime and globalToTime is applied to every rule right
// before the transfer (propagateGlobalTime has to be called).
globalFromTime int64
globalToTime int64
// skipEncryptedMessages determines whether message which cannot // skipEncryptedMessages determines whether message which cannot
// be decrypted should be exported or skipped. // be decrypted should be exported or skipped.
skipEncryptedMessages bool skipEncryptedMessages bool
@ -81,10 +87,18 @@ func (r *transferRules) setGlobalMailbox(mailbox *Mailbox) {
} }
func (r *transferRules) setGlobalTimeLimit(fromTime, toTime int64) { func (r *transferRules) setGlobalTimeLimit(fromTime, toTime int64) {
r.globalFromTime = fromTime
r.globalToTime = toTime
}
func (r *transferRules) propagateGlobalTime() {
if r.globalFromTime == 0 && r.globalToTime == 0 {
return
}
for _, rule := range r.rules { for _, rule := range r.rules {
if !rule.HasTimeLimit() { if !rule.HasTimeLimit() {
rule.FromTime = fromTime rule.FromTime = r.globalFromTime
rule.ToTime = toTime rule.ToTime = r.globalToTime
} }
} }
} }
@ -122,8 +136,9 @@ func (r *transferRules) setDefaultRules(sourceMailboxes []Mailbox, targetMailbox
} }
targetMailboxes := sourceMailbox.findMatchingMailboxes(targetMailboxes) targetMailboxes := sourceMailbox.findMatchingMailboxes(targetMailboxes)
if len(targetMailboxes) == 0 {
targetMailboxes = defaultCallback(sourceMailbox) if !containsExclusive(targetMailboxes) {
targetMailboxes = append(targetMailboxes, defaultCallback(sourceMailbox)...)
} }
active := true active := true
@ -147,10 +162,14 @@ func (r *transferRules) setDefaultRules(sourceMailboxes []Mailbox, targetMailbox
} }
} }
for _, rule := range r.rules { // There is no point showing rule which has no action (i.e., source mailbox
if !rule.Active { // is not available).
continue // A good reason to keep all rules and only deactivate them would be for
} // multiple imports from different sources with the same or similar enough
// mailbox setup to reuse configuration. That is very minor feature which
// can be implemented in more reasonable way by allowing users to save and
// load configurations.
for key, rule := range r.rules {
found := false found := false
for _, sourceMailbox := range sourceMailboxes { for _, sourceMailbox := range sourceMailboxes {
if sourceMailbox.Name == rule.SourceMailbox.Name { if sourceMailbox.Name == rule.SourceMailbox.Name {
@ -158,7 +177,7 @@ func (r *transferRules) setDefaultRules(sourceMailboxes []Mailbox, targetMailbox
} }
} }
if !found { if !found {
rule.Active = false delete(r.rules, key)
} }
} }
@ -216,6 +235,7 @@ func (r *transferRules) getRules() []*Rule {
for _, rule := range r.rules { for _, rule := range r.rules {
rules = append(rules, rule) rules = append(rules, rule)
} }
sort.Sort(byRuleOrder(rules))
return rules return rules
} }
@ -288,3 +308,59 @@ func (r *Rule) TargetMailboxNames() (names []string) {
} }
return return
} }
// byRuleOrder implements sort.Interface. Sort order:
// * System folders first (as defined in getSystemMailboxes).
// * Custom folders by name.
// * Custom labels by name.
type byRuleOrder []*Rule
func (a byRuleOrder) Len() int {
return len(a)
}
func (a byRuleOrder) Swap(i, j int) {
a[i], a[j] = a[j], a[i]
}
func (a byRuleOrder) Less(i, j int) bool {
if a[i].SourceMailbox.IsExclusive && !a[j].SourceMailbox.IsExclusive {
return true
}
if !a[i].SourceMailbox.IsExclusive && a[j].SourceMailbox.IsExclusive {
return false
}
iSystemIndex := -1
jSystemIndex := -1
for index, systemFolders := range getSystemMailboxes(true) {
if a[i].SourceMailbox.Name == systemFolders.Name {
iSystemIndex = index
}
if a[j].SourceMailbox.Name == systemFolders.Name {
jSystemIndex = index
}
}
if iSystemIndex != -1 && jSystemIndex == -1 {
return true
}
if iSystemIndex == -1 && jSystemIndex != -1 {
return false
}
if iSystemIndex != -1 && jSystemIndex != -1 {
return iSystemIndex < jSystemIndex
}
return a[i].SourceMailbox.Name < a[j].SourceMailbox.Name
}
// containsExclusive returns true if there is at least one exclusive mailbox.
func containsExclusive(mailboxes []Mailbox) bool {
for _, m := range mailboxes {
if m.IsExclusive {
return true
}
}
return false
}

View File

@ -86,6 +86,7 @@ func TestSetGlobalTimeLimit(t *testing.T) {
r.NoError(t, rules.setRule(mailboxB, []Mailbox{}, 0, 0)) r.NoError(t, rules.setRule(mailboxB, []Mailbox{}, 0, 0))
rules.setGlobalTimeLimit(30, 40) rules.setGlobalTimeLimit(30, 40)
rules.propagateGlobalTime()
r.Equal(t, map[string]*Rule{ r.Equal(t, map[string]*Rule{
mailboxA.Hash(): {Active: true, SourceMailbox: mailboxA, TargetMailboxes: []Mailbox{}, FromTime: 10, ToTime: 20}, mailboxA.Hash(): {Active: true, SourceMailbox: mailboxA, TargetMailboxes: []Mailbox{}, FromTime: 10, ToTime: 20},
@ -154,7 +155,6 @@ func TestSetDefaultRulesDeactivateMissing(t *testing.T) {
r.Equal(t, map[string]*Rule{ r.Equal(t, map[string]*Rule{
mailboxA.Hash(): {Active: true, SourceMailbox: mailboxA, TargetMailboxes: []Mailbox{mailboxB}, FromTime: 0, ToTime: 0}, mailboxA.Hash(): {Active: true, SourceMailbox: mailboxA, TargetMailboxes: []Mailbox{mailboxB}, FromTime: 0, ToTime: 0},
mailboxB.Hash(): {Active: false, SourceMailbox: mailboxB, TargetMailboxes: []Mailbox{mailboxB}, FromTime: 0, ToTime: 0},
}, rules.rules) }, rules.rules)
} }
@ -208,3 +208,40 @@ func generateTimeRule(from, to int64) Rule {
ToTime: to, ToTime: to,
} }
} }
func TestOrderRules(t *testing.T) {
wantMailboxOrder := []Mailbox{
{Name: "Inbox", IsExclusive: true},
{Name: "Drafts", IsExclusive: true},
{Name: "Sent", IsExclusive: true},
{Name: "Starred", IsExclusive: true},
{Name: "Archive", IsExclusive: true},
{Name: "Spam", IsExclusive: true},
{Name: "All Mail", IsExclusive: true},
{Name: "Folder A", IsExclusive: true},
{Name: "Folder B", IsExclusive: true},
{Name: "Folder C", IsExclusive: true},
{Name: "Label A", IsExclusive: false},
{Name: "Label B", IsExclusive: false},
{Name: "Label C", IsExclusive: false},
}
wantMailboxNames := []string{}
rules := map[string]*Rule{}
for _, mailbox := range wantMailboxOrder {
wantMailboxNames = append(wantMailboxNames, mailbox.Name)
rules[mailbox.Hash()] = &Rule{
SourceMailbox: mailbox,
}
}
transferRules := transferRules{
rules: rules,
}
gotMailboxNames := []string{}
for _, rule := range transferRules.getRules() {
gotMailboxNames = append(gotMailboxNames, rule.SourceMailbox.Name)
}
r.Equal(t, wantMailboxNames, gotMailboxNames)
}

View File

@ -31,12 +31,14 @@ var log = logrus.WithField("pkg", "transfer") //nolint[gochecknoglobals]
// Transfer is facade on top of import rules, progress manager and source // Transfer is facade on top of import rules, progress manager and source
// and target providers. This is the main object which should be used. // and target providers. This is the main object which should be used.
type Transfer struct { type Transfer struct {
panicHandler PanicHandler panicHandler PanicHandler
id string id string
dir string dir string
rules transferRules rules transferRules
source SourceProvider source SourceProvider
target TargetProvider target TargetProvider
sourceMboxCache []Mailbox
targetMboxCache []Mailbox
} }
// New creates Transfer for specific source and target. Usage: // New creates Transfer for specific source and target. Usage:
@ -127,23 +129,33 @@ func (t *Transfer) GetRules() []*Rule {
} }
// SourceMailboxes returns mailboxes available at source side. // SourceMailboxes returns mailboxes available at source side.
func (t *Transfer) SourceMailboxes() ([]Mailbox, error) { func (t *Transfer) SourceMailboxes() (m []Mailbox, err error) {
return t.source.Mailboxes(false, true) if t.sourceMboxCache == nil {
t.sourceMboxCache, err = t.source.Mailboxes(false, true)
}
return t.sourceMboxCache, err
} }
// TargetMailboxes returns mailboxes available at target side. // TargetMailboxes returns mailboxes available at target side.
func (t *Transfer) TargetMailboxes() ([]Mailbox, error) { func (t *Transfer) TargetMailboxes() (m []Mailbox, err error) {
return t.target.Mailboxes(true, false) if t.targetMboxCache == nil {
t.targetMboxCache, err = t.target.Mailboxes(true, false)
}
return t.targetMboxCache, err
} }
// CreateTargetMailbox creates mailbox in target provider. // CreateTargetMailbox creates mailbox in target provider.
func (t *Transfer) CreateTargetMailbox(mailbox Mailbox) (Mailbox, error) { func (t *Transfer) CreateTargetMailbox(mailbox Mailbox) (Mailbox, error) {
t.targetMboxCache = nil
return t.target.CreateMailbox(mailbox) return t.target.CreateMailbox(mailbox)
} }
// ChangeTarget changes the target. It is safe to change target for export, // ChangeTarget changes the target. It is safe to change target for export,
// must not be changed for import. Do not set after you started transfer. // must not be changed for import. Do not set after you started transfer.
func (t *Transfer) ChangeTarget(target TargetProvider) { func (t *Transfer) ChangeTarget(target TargetProvider) {
t.targetMboxCache = nil
t.target = target t.target = target
} }
@ -151,6 +163,7 @@ func (t *Transfer) ChangeTarget(target TargetProvider) {
func (t *Transfer) Start() *Progress { func (t *Transfer) Start() *Progress {
log.Debug("Transfer started") log.Debug("Transfer started")
t.rules.save() t.rules.save()
t.rules.propagateGlobalTime()
log := log.WithField("id", t.id) log := log.WithField("id", t.id)
reportFile := newFileReport(t.dir, t.id) reportFile := newFileReport(t.dir, t.id)

View File

@ -5,12 +5,11 @@
package mocks package mocks
import ( import (
reflect "reflect"
store "github.com/ProtonMail/proton-bridge/internal/store" store "github.com/ProtonMail/proton-bridge/internal/store"
credentials "github.com/ProtonMail/proton-bridge/internal/users/credentials" credentials "github.com/ProtonMail/proton-bridge/internal/users/credentials"
pmapi "github.com/ProtonMail/proton-bridge/pkg/pmapi" pmapi "github.com/ProtonMail/proton-bridge/pkg/pmapi"
gomock "github.com/golang/mock/gomock" gomock "github.com/golang/mock/gomock"
reflect "reflect"
) )
// MockConfiger is a mock of Configer interface // MockConfiger is a mock of Configer interface

View File

@ -173,26 +173,6 @@ func (c *client) Report(rep ReportReq) (err error) {
return res.Err() return res.Err()
} }
// ReportBug is old. Use Report instead.
func (c *client) ReportBug(os, osVersion, title, description, username, email string) (err error) {
return c.ReportBugWithEmailClient(os, osVersion, title, description, username, email, "")
}
// ReportBugWithEmailClient is old. Use Report instead.
func (c *client) ReportBugWithEmailClient(os, osVersion, title, description, username, email, emailClient string) (err error) {
bugReq := ReportReq{
OS: os,
OSVersion: osVersion,
Browser: emailClient,
Title: title,
Description: description,
Username: username,
Email: email,
}
return c.Report(bugReq)
}
// ReportCrash is old. Use sentry instead. // ReportCrash is old. Use sentry instead.
func (c *client) ReportCrash(stacktrace string) (err error) { func (c *client) ReportCrash(stacktrace string) (err error) {
crashReq := ReportReq{ crashReq := ReportReq{

View File

@ -27,19 +27,7 @@ import (
"testing" "testing"
) )
var testBugsReportReq = ReportReq{ var testBugReportReq = ReportReq{
OS: "Mac OSX",
OSVersion: "10.11.6",
Client: "demoapp",
ClientVersion: "GoPMAPI_1.0.14",
ClientType: 1,
Title: "Big Bug",
Description: "Cannot fetch new messages",
Username: "apple",
Email: "apple@gmail.com",
}
var testBugsReportReqWithEmailClient = ReportReq{
OS: "Mac OSX", OS: "Mac OSX",
OSVersion: "10.11.6", OSVersion: "10.11.6",
Browser: "AppleMail", Browser: "AppleMail",
@ -67,31 +55,6 @@ const testBugsBody = `{
const testAttachmentJSONZipped = "PK\x03\x04\x14\x00\b\x00\b\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\b\x00\x00\x00last.log\\Rَ\xaaH\x00}ﯨ\xf8r\x1f\xeeܖED;\xe9\ap\x03\x11\x11\x97\x0e8\x99L\xb0(\xa1\xa0\x16\x85b\x91I\xff\xfbD{\x99\xc9}\xab:K\x9d\xa4\xce\xf9\xe7\t\x00\x00z\xf6\xb4\xf7\x02z\xb7a\xe5\xd8\x04*V̭\x8d\xd1lvE}\xd6\xe3\x80\x1f\xd7nX\x9bI[\xa6\xe1a=\xd4a\xa8M\x97\xd9J\xf1F\xeb\x105U\xbd\xb0`XO\xce\xf1hu\x99q\xc3\xfe{\x11ߨ'-\v\x89Z\xa4\x9c5\xaf\xaf\xbd?>R\xd6\x11E\xf7\x1cX\xf0JpF#L\x9eE+\xbe\xe8\x1d\xee\ued2e\u007f\xde]\u06dd\xedo\x97\x87E\xa0V\xf4/$\xc2\xecK\xed\xa0\xdb&\x829\x12\xe5\x9do\xa0\xe9\x1a\xd2\x19\x1e\xf5`\x95гb\xf8\x89\x81\xb7\xa5G\x18\x95\xf3\x9d9\xe8\x93B\x17!\x1a^\xccr\xbb`\xb2\xb4\xb86\x87\xb4h\x0e\xda\xc6u<+\x9e$̓\x95\xccSo\xea\xa4\xdbH!\xe9g\x8b\xd4\b\xb3hܬ\xa6Wk\x14He\xae\x8aPU\xaa\xc1\xee$\xfbH\xb3\xab.I\f<\x89\x06q\xe3-3-\x99\xcdݽ\xe5v\x99\xedn\xac\xadn\xe8Rp=\xb4nJ\xed\xd5\r\x8d\xde\x06Ζ\xf6\xb3\x01\x94\xcb\xf6\xd4\x19r\xe1\xaa$4+\xeaW\xa6F\xfa0\x97\x9cD\f\x8e\xd7\xd6z\v,G\xf3e2\xd4\xe6V\xba\v\xb6\xd9\xe8\xca*\x16\x95V\xa4J\xfbp\xddmF\x8c\x9a\xc6\xc8Č-\xdb\v\xf6\xf5\xf9\x02*\x15e\x874\xc9\xe7\"\xa3\x1an\xabq}ˊq\x957\xd3\xfd\xa91\x82\xe0Lß\\\x17\x8e\x9e_\xed`\t\xe9~5̕\x03\x9a\f\xddN6\xa2\xc4\x17\xdb\xc9V\x1c~\x9e\xea\xbe\xda-xv\xed\x8b\xe2\xc8DŽS\x95E6\xf2\xc3H\x1d:HPx\xc9\x14\xbfɒ\xff\xea\xb4P\x14\xa3\xe2\xfe\xfd\x1f+z\x80\x903\x81\x98\xf8\x15\xa3\x12\x16\xf8\"0g\xf7~B^\xfd \x040T\xa3\x02\x9c\x10\xc1\xa8F\xa0I#\xf1\xa3\x04\x98\x01\x91\xe2\x12\xdc;\x06gL\xd0g\xc0\xe3\xbd\xf6\xd7}&\xa8轀?\xbfяy`X\xf0\x92\x9f\x05\xf0*A8ρ\xac=K\xff\xf3\xfe\xa6Z\xe1\x1a\x017\xc2\x04\f\x94g\xa9\xf7-\xfb\xebqz\u007fz\u007f\xfa7\x00\x00\xff\xffPK\a\b\xf5\\\v\xe5I\x02\x00\x00\r\x03\x00\x00PK\x01\x02\x14\x00\x14\x00\b\x00\b\x00\x00\x00\x00\x00\xf5\\\v\xe5I\x02\x00\x00\r\x03\x00\x00\b\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00last.logPK\x05\x06\x00\x00\x00\x00\x01\x00\x01\x006\x00\x00\x00\u007f\x02\x00\x00\x00\x00" //nolint[misspell] const testAttachmentJSONZipped = "PK\x03\x04\x14\x00\b\x00\b\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\b\x00\x00\x00last.log\\Rَ\xaaH\x00}ﯨ\xf8r\x1f\xeeܖED;\xe9\ap\x03\x11\x11\x97\x0e8\x99L\xb0(\xa1\xa0\x16\x85b\x91I\xff\xfbD{\x99\xc9}\xab:K\x9d\xa4\xce\xf9\xe7\t\x00\x00z\xf6\xb4\xf7\x02z\xb7a\xe5\xd8\x04*V̭\x8d\xd1lvE}\xd6\xe3\x80\x1f\xd7nX\x9bI[\xa6\xe1a=\xd4a\xa8M\x97\xd9J\xf1F\xeb\x105U\xbd\xb0`XO\xce\xf1hu\x99q\xc3\xfe{\x11ߨ'-\v\x89Z\xa4\x9c5\xaf\xaf\xbd?>R\xd6\x11E\xf7\x1cX\xf0JpF#L\x9eE+\xbe\xe8\x1d\xee\ued2e\u007f\xde]\u06dd\xedo\x97\x87E\xa0V\xf4/$\xc2\xecK\xed\xa0\xdb&\x829\x12\xe5\x9do\xa0\xe9\x1a\xd2\x19\x1e\xf5`\x95гb\xf8\x89\x81\xb7\xa5G\x18\x95\xf3\x9d9\xe8\x93B\x17!\x1a^\xccr\xbb`\xb2\xb4\xb86\x87\xb4h\x0e\xda\xc6u<+\x9e$̓\x95\xccSo\xea\xa4\xdbH!\xe9g\x8b\xd4\b\xb3hܬ\xa6Wk\x14He\xae\x8aPU\xaa\xc1\xee$\xfbH\xb3\xab.I\f<\x89\x06q\xe3-3-\x99\xcdݽ\xe5v\x99\xedn\xac\xadn\xe8Rp=\xb4nJ\xed\xd5\r\x8d\xde\x06Ζ\xf6\xb3\x01\x94\xcb\xf6\xd4\x19r\xe1\xaa$4+\xeaW\xa6F\xfa0\x97\x9cD\f\x8e\xd7\xd6z\v,G\xf3e2\xd4\xe6V\xba\v\xb6\xd9\xe8\xca*\x16\x95V\xa4J\xfbp\xddmF\x8c\x9a\xc6\xc8Č-\xdb\v\xf6\xf5\xf9\x02*\x15e\x874\xc9\xe7\"\xa3\x1an\xabq}ˊq\x957\xd3\xfd\xa91\x82\xe0Lß\\\x17\x8e\x9e_\xed`\t\xe9~5̕\x03\x9a\f\xddN6\xa2\xc4\x17\xdb\xc9V\x1c~\x9e\xea\xbe\xda-xv\xed\x8b\xe2\xc8DŽS\x95E6\xf2\xc3H\x1d:HPx\xc9\x14\xbfɒ\xff\xea\xb4P\x14\xa3\xe2\xfe\xfd\x1f+z\x80\x903\x81\x98\xf8\x15\xa3\x12\x16\xf8\"0g\xf7~B^\xfd \x040T\xa3\x02\x9c\x10\xc1\xa8F\xa0I#\xf1\xa3\x04\x98\x01\x91\xe2\x12\xdc;\x06gL\xd0g\xc0\xe3\xbd\xf6\xd7}&\xa8轀?\xbfяy`X\xf0\x92\x9f\x05\xf0*A8ρ\xac=K\xff\xf3\xfe\xa6Z\xe1\x1a\x017\xc2\x04\f\x94g\xa9\xf7-\xfb\xebqz\u007fz\u007f\xfa7\x00\x00\xff\xffPK\a\b\xf5\\\v\xe5I\x02\x00\x00\r\x03\x00\x00PK\x01\x02\x14\x00\x14\x00\b\x00\b\x00\x00\x00\x00\x00\xf5\\\v\xe5I\x02\x00\x00\r\x03\x00\x00\b\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00last.logPK\x05\x06\x00\x00\x00\x00\x01\x00\x01\x006\x00\x00\x00\u007f\x02\x00\x00\x00\x00" //nolint[misspell]
func TestClient_BugReport(t *testing.T) {
s, c := newTestServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Ok(t, checkMethodAndPath(r, "POST", "/reports/bug"))
Ok(t, isAuthReq(r, testUID, testAccessToken))
var bugsReportReq ReportReq
Ok(t, json.NewDecoder(r.Body).Decode(&bugsReportReq))
Equals(t, testBugsReportReq, bugsReportReq)
fmt.Fprint(w, testBugsBody)
}))
defer s.Close()
c.uid = testUID
c.accessToken = testAccessToken
Ok(t, c.ReportBug(
testBugsReportReq.OS,
testBugsReportReq.OSVersion,
testBugsReportReq.Title,
testBugsReportReq.Description,
testBugsReportReq.Username,
testBugsReportReq.Email,
))
}
func TestClient_BugReportWithAttachment(t *testing.T) { func TestClient_BugReportWithAttachment(t *testing.T) {
s, c := newTestServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { s, c := newTestServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Ok(t, checkMethodAndPath(r, "POST", "/reports/bug")) Ok(t, checkMethodAndPath(r, "POST", "/reports/bug"))
@ -100,15 +63,15 @@ func TestClient_BugReportWithAttachment(t *testing.T) {
Ok(t, r.ParseMultipartForm(10*1024)) Ok(t, r.ParseMultipartForm(10*1024))
for field, expected := range map[string]string{ for field, expected := range map[string]string{
"OS": testBugsReportReq.OS, "OS": testBugReportReq.OS,
"OSVersion": testBugsReportReq.OSVersion, "OSVersion": testBugReportReq.OSVersion,
"Client": testBugsReportReq.Client, "Client": testBugReportReq.Client,
"ClientVersion": testBugsReportReq.ClientVersion, "ClientVersion": testBugReportReq.ClientVersion,
"ClientType": fmt.Sprintf("%d", testBugsReportReq.ClientType), "ClientType": fmt.Sprintf("%d", testBugReportReq.ClientType),
"Title": testBugsReportReq.Title, "Title": testBugReportReq.Title,
"Description": testBugsReportReq.Description, "Description": testBugReportReq.Description,
"Username": testBugsReportReq.Username, "Username": testBugReportReq.Username,
"Email": testBugsReportReq.Email, "Email": testBugReportReq.Email,
} { } {
if r.PostFormValue(field) != expected { if r.PostFormValue(field) != expected {
t.Errorf("Field %q has %q but expected %q", field, r.PostFormValue(field), expected) t.Errorf("Field %q has %q but expected %q", field, r.PostFormValue(field), expected)
@ -129,20 +92,20 @@ func TestClient_BugReportWithAttachment(t *testing.T) {
c.uid = testUID c.uid = testUID
c.accessToken = testAccessToken c.accessToken = testAccessToken
rep := testBugsReportReq rep := testBugReportReq
rep.AddAttachment("log", "last.log", strings.NewReader(testAttachmentJSON)) rep.AddAttachment("log", "last.log", strings.NewReader(testAttachmentJSON))
Ok(t, c.Report(rep)) Ok(t, c.Report(rep))
} }
func TestClient_BugReportWithEmailClient(t *testing.T) { func TestClient_BugReport(t *testing.T) {
s, c := newTestServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { s, c := newTestServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Ok(t, checkMethodAndPath(r, "POST", "/reports/bug")) Ok(t, checkMethodAndPath(r, "POST", "/reports/bug"))
Ok(t, isAuthReq(r, testUID, testAccessToken)) Ok(t, isAuthReq(r, testUID, testAccessToken))
var bugsReportReq ReportReq var bugsReportReq ReportReq
Ok(t, json.NewDecoder(r.Body).Decode(&bugsReportReq)) Ok(t, json.NewDecoder(r.Body).Decode(&bugsReportReq))
Equals(t, testBugsReportReqWithEmailClient, bugsReportReq) Equals(t, testBugReportReq, bugsReportReq)
fmt.Fprint(w, testBugsBody) fmt.Fprint(w, testBugsBody)
})) }))
@ -150,15 +113,17 @@ func TestClient_BugReportWithEmailClient(t *testing.T) {
c.uid = testUID c.uid = testUID
c.accessToken = testAccessToken c.accessToken = testAccessToken
Ok(t, c.ReportBugWithEmailClient( r := ReportReq{
testBugsReportReqWithEmailClient.OS, OS: testBugReportReq.OS,
testBugsReportReqWithEmailClient.OSVersion, OSVersion: testBugReportReq.OSVersion,
testBugsReportReqWithEmailClient.Title, Browser: testBugReportReq.Browser,
testBugsReportReqWithEmailClient.Description, Title: testBugReportReq.Title,
testBugsReportReqWithEmailClient.Username, Description: testBugReportReq.Description,
testBugsReportReqWithEmailClient.Email, Username: testBugReportReq.Username,
testBugsReportReqWithEmailClient.Browser, Email: testBugReportReq.Email,
)) }
Ok(t, c.Report(r))
} }
func TestClient_BugsCrash(t *testing.T) { func TestClient_BugsCrash(t *testing.T) {

View File

@ -67,7 +67,7 @@ type Client interface {
DeleteLabel(labelID string) error DeleteLabel(labelID string) error
EmptyFolder(labelID string, addressID string) error EmptyFolder(labelID string, addressID string) error
ReportBugWithEmailClient(os, osVersion, title, description, username, email, emailClient string) error Report(report ReportReq) error
SendSimpleMetric(category, action, label string) error SendSimpleMetric(category, action, label string) error
GetMailSettings() (MailSettings, error) GetMailSettings() (MailSettings, error)

View File

@ -5,12 +5,11 @@
package mocks package mocks
import ( import (
io "io"
reflect "reflect"
crypto "github.com/ProtonMail/gopenpgp/v2/crypto" crypto "github.com/ProtonMail/gopenpgp/v2/crypto"
pmapi "github.com/ProtonMail/proton-bridge/pkg/pmapi" pmapi "github.com/ProtonMail/proton-bridge/pkg/pmapi"
gomock "github.com/golang/mock/gomock" gomock "github.com/golang/mock/gomock"
io "io"
reflect "reflect"
) )
// MockClient is a mock of Client interface // MockClient is a mock of Client interface
@ -601,18 +600,18 @@ func (mr *MockClientMockRecorder) ReorderAddresses(arg0 interface{}) *gomock.Cal
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReorderAddresses", reflect.TypeOf((*MockClient)(nil).ReorderAddresses), arg0) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReorderAddresses", reflect.TypeOf((*MockClient)(nil).ReorderAddresses), arg0)
} }
// ReportBugWithEmailClient mocks base method // Report mocks base method
func (m *MockClient) ReportBugWithEmailClient(arg0, arg1, arg2, arg3, arg4, arg5, arg6 string) error { func (m *MockClient) Report(arg0 pmapi.ReportReq) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReportBugWithEmailClient", arg0, arg1, arg2, arg3, arg4, arg5, arg6) ret := m.ctrl.Call(m, "Report", arg0)
ret0, _ := ret[0].(error) ret0, _ := ret[0].(error)
return ret0 return ret0
} }
// ReportBugWithEmailClient indicates an expected call of ReportBugWithEmailClient // Report indicates an expected call of Report
func (mr *MockClientMockRecorder) ReportBugWithEmailClient(arg0, arg1, arg2, arg3, arg4, arg5, arg6 interface{}) *gomock.Call { func (mr *MockClientMockRecorder) Report(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReportBugWithEmailClient", reflect.TypeOf((*MockClient)(nil).ReportBugWithEmailClient), arg0, arg1, arg2, arg3, arg4, arg5, arg6) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Report", reflect.TypeOf((*MockClient)(nil).Report), arg0)
} }
// SendMessage mocks base method // SendMessage mocks base method

View File

@ -23,16 +23,8 @@ import (
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
) )
func (api *FakePMAPI) ReportBugWithEmailClient(os, osVersion, title, description, username, email, emailClient string) error { func (api *FakePMAPI) Report(report pmapi.ReportReq) error {
return api.checkInternetAndRecordCall(POST, "/reports/bug", &pmapi.ReportReq{ return api.checkInternetAndRecordCall(POST, "/reports/bug", report)
OS: os,
OSVersion: osVersion,
Title: title,
Description: description,
Username: username,
Email: email,
Browser: emailClient,
})
} }
func (api *FakePMAPI) SendSimpleMetric(category, action, label string) error { func (api *FakePMAPI) SendSimpleMetric(category, action, label string) error {