diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 00ba87a8..bd5c1c5d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -94,9 +94,9 @@ build-ie-linux: script: - make build-ie artifacts: - name: "bridge-linux-$CI_COMMIT_SHORT_SHA" + name: "ie-linux-$CI_COMMIT_SHORT_SHA" paths: - - bridge_*.tgz + - ie_*.tgz expire_in: 2 week build-darwin: @@ -145,9 +145,9 @@ build-ie-darwin: script: - make build-ie artifacts: - name: "bridge-darwin-$CI_COMMIT_SHORT_SHA" + name: "ie-darwin-$CI_COMMIT_SHORT_SHA" paths: - - bridge_*.tgz + - ie_*.tgz expire_in: 2 week build-windows: @@ -189,9 +189,9 @@ build-ie-windows: - go mod download - TARGET_OS=windows make build-ie artifacts: - name: "bridge-windows-$CI_COMMIT_SHORT_SHA" + name: "ie-windows-$CI_COMMIT_SHORT_SHA" paths: - - bridge_*.tgz + - ie_*.tgz expire_in: 2 week # Stage: MIRROR diff --git a/Changelog.md b/Changelog.md index 05861d1c..7e6bf64b 100644 --- a/Changelog.md +++ b/Changelog.md @@ -47,6 +47,8 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/) * GODT-554 Detect and notify about "bad certificate" IMAP TLS error. * 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. +* Structure for transfer rules in QML +* GODT-360 Detect charset embedded in html/xml. ### Changed * GODT-360 Detect charset embedded in html/xml. diff --git a/go.mod b/go.mod index c499d754..a4c40251 100644 --- a/go.mod +++ b/go.mod @@ -46,6 +46,7 @@ require ( github.com/golang/mock v1.4.4 github.com/google/go-cmp v0.5.1 github.com/google/uuid v1.1.1 + github.com/go-delve/delve v1.4.1 // indirect github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c // indirect github.com/hashicorp/go-multierror v1.1.0 github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7 @@ -59,6 +60,7 @@ require ( github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce github.com/olekukonko/tablewriter v0.0.4 // indirect github.com/pkg/errors v0.9.1 + github.com/psampaz/go-mod-outdated v0.6.0 // indirect github.com/sirupsen/logrus v1.6.0 github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 github.com/stretchr/testify v1.6.1 diff --git a/go.sum b/go.sum index 79d0f39d..8ea5870e 100644 --- a/go.sum +++ b/go.sum @@ -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/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/cosiner/argv v0.0.0-20170225145430-13bacc38a0a5/go.mod h1:p/NrK5tF6ICIly4qwEDsf6VDirFiWWz0FenfYBwJaKQ= +github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= @@ -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/go-resty/resty/v2 v2.3.0 h1:JOOeAvjSlapTT92p8xiS19Zxev1neGikoHsXJeOq8So= github.com/go-resty/resty/v2 v2.3.0/go.mod h1:UpN9CgLZNsv4e9XG50UU8xdI0F43UQ4HmxLBDwaroHU= +github.com/go-delve/delve v1.4.1 h1:kZs0umEv+VKnK84kY9/ZXWrakdLTeRTyYjFdgLelZCQ= +github.com/go-delve/delve v1.4.1/go.mod h1:vmy6iObn7zg8FQ5KOCIe6TruMNsqpoZO8uMiRea+97k= +github.com/go-resty/resty/v2 v2.2.0 h1:vgZ1cdblp8Aw4jZj3ZsKh6yKAlMg3CHMrqFSFFd+jgY= +github.com/go-resty/resty/v2 v2.2.0/go.mod h1:nYW/8rxqQCmI3bPz9Fsmjbr2FBjGuR2Mzt6kDh3zZ7w= github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw= github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/gogs/chardet v0.0.0-20150115103509-2404f7772561 h1:aBzukfDxQlCTVS0NBUjI5YA3iVeaZ9Tb5PxNrrIP1xs= @@ -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/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/golang/mock v1.4.3 h1:GV+pQPG/EUUbkh47niozDcADz6go/dUwhVzdUQHIVRw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-dap v0.2.0/go.mod h1:5q8aYQFnHOAZEMP+6vmq25HKYAEwE+LF5yh7JKrrhSQ= github.com/gopherjs/gopherjs v0.0.0-20190411002643-bd77b112433e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c h1:7lF+Vz0LqiRidnzC1Oq86fpX1q/iEv2KJdrCtttYjT4= github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= @@ -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/jameshoulahan/go-imap v0.0.0-20200728140727-d57327f48843 h1:suxlO4AC4E4bjueAsL0m+qp8kmkxRWMGj+5bBU/KJ8g= github.com/jameshoulahan/go-imap v0.0.0-20200728140727-d57327f48843/go.mod h1:yKASt+C3ZiDAiCSssxg9caIckWF/JG7ZQTO7GAmvicU= +github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jaytaylor/html2text v0.0.0-20190408195923-01ec452cbe43/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk= github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7 h1:g0fAGBisHaEQ0TRq1iBvemFRf+8AEWEmBESSiWB3Vsc= github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk= @@ -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/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= +github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381 h1:bqDmpDG49ZRnB5PcgP0RXtQvnMSgIF14M7CBd2shtXs= +github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= github.com/martinlindhe/base36 v1.0.0 h1:eYsumTah144C0A8P1T/AVSUk5ZoLnhfYFM3OGQxB52A= github.com/martinlindhe/base36 v1.0.0/go.mod h1:+AtEs8xrBpCeYgSLoY/aJ6Wf37jtBuR0s35750M27+8= +github.com/mattn/go-colorable v0.0.0-20170327083344-ded68f7a9561/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= @@ -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/miekg/dns v1.1.30 h1:Qww6FseFn8PRfw07jueqIXqodm0JKiiKuK0DeXSqfyo= github.com/miekg/dns v1.1.30/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= +github.com/miekg/dns v1.1.29 h1:xHBEhR+t5RzcFJjBLJlax2daXOrTYtr9z4WdKEfWFzg= +github.com/miekg/dns v1.1.29/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= github.com/myesui/uuid v1.0.0 h1:xCBmH4l5KuvLYc5L7AS7SZg9/jKdIFubM7OVoLqaQUI= github.com/myesui/uuid v1.0.0/go.mod h1:2CDfNgU0LR8mIdO8vdWd8i9gWWxLlcoIGGpSNgafq84= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= @@ -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.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8= github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA= +github.com/peterh/liner v0.0.0-20170317030525-88609521dc4b/go.mod h1:xIteQHvHuaLYG9IFj6mSxM0fCKrs34IrEQUhOYuGPHc= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/psampaz/go-mod-outdated v0.6.0 h1:DXS6rdsz4rpezbPsckQflqrYSEBvsF5GAmUWP+UvnQo= +github.com/psampaz/go-mod-outdated v0.6.0/go.mod h1:r78NYWd1z+F9Zdsfy70svgXOz363B08BWnTyFSgEESs= +github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca h1:NugYot0LIVPxTvN8n+Kvkn6TrbMyxQiuvKdEwFdR9vI= @@ -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/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= +github.com/spf13/cobra v0.0.0-20170417170307-b6cb39589372/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/pflag v0.0.0-20170417173400-9e4c21054fa1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo= github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -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= go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0= go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= +github.com/urfave/cli v1.22.3 h1:FpNT6zq26xNpHZy08emi755QwzLPs6Pukqjlc7RfOMU= +github.com/urfave/cli v1.22.3/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.starlark.net v0.0.0-20190702223751-32f345186213/go.mod h1:c1/X6cHgvdXj6pUlmWKMkuqRnW4K8x2vwt6JAaaircg= +golang.org/x/arch v0.0.0-20190927153633-4e8777c89be4/go.mod h1:flIaEI6LNU6xOCD5PaJvn9wGP0agmIOqjrtsKGRguv4= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -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-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -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-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191127201027-ecd32218bd7f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191216052735-49a3e744a425 h1:VvQyQJN0tSuecqgcIxMWnnfG5kSmgy9KZR9sW3W5QeA= golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -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/stretchr/testify.v1 v1.2.2 h1:yhQC6Uy5CqibAIlk1wlusa/MJ3iAN49/BsR/dCCKz3M= gopkg.in/stretchr/testify.v1 v1.2.2/go.mod h1:QI5V/q6UbPmuhtm10CaFZxED9NreB8PnFYN9JcR6TxU= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/internal/bridge/bridge.go b/internal/bridge/bridge.go index 3d753afd..0961107e 100644 --- a/internal/bridge/bridge.go +++ b/internal/bridge/bridge.go @@ -25,6 +25,7 @@ import ( "github.com/ProtonMail/proton-bridge/internal/metrics" "github.com/ProtonMail/proton-bridge/internal/preferences" "github.com/ProtonMail/proton-bridge/internal/users" + "github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/listener" logrus "github.com/sirupsen/logrus" @@ -130,15 +131,17 @@ func (b *Bridge) ReportBug(osType, osVersion, description, accountName, address, defer c.Logout() title := "[Bridge] Bug" - if err := c.ReportBugWithEmailClient( - osType, - osVersion, - title, - description, - accountName, - address, - emailClient, - ); err != nil { + report := pmapi.ReportReq{ + OS: osType, + OSVersion: osVersion, + Browser: emailClient, + Title: title, + Description: description, + Username: accountName, + Email: address, + } + + if err := c.Report(report); err != nil { log.Error("Reporting bug failed: ", err) return err } diff --git a/internal/bridge/release_notes.go b/internal/bridge/release_notes.go index c3f9a024..ab69ebed 100644 --- a/internal/bridge/release_notes.go +++ b/internal/bridge/release_notes.go @@ -15,7 +15,7 @@ // You should have received a copy of the GNU General Public License // along with ProtonMail Bridge. If not, see . -// 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 diff --git a/internal/frontend/qml/GuiIE.qml b/internal/frontend/qml/GuiIE.qml index 04c117c4..bac8918e 100644 --- a/internal/frontend/qml/GuiIE.qml +++ b/internal/frontend/qml/GuiIE.qml @@ -38,7 +38,7 @@ Item { property var allMonths : getMonthList(1,12) 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{} @@ -396,7 +396,7 @@ Item { 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 Component.onCompleted : { // set spell messages diff --git a/internal/frontend/qml/ImportExportUI/DateRange.qml b/internal/frontend/qml/ImportExportUI/DateRange.qml index 26ebc016..d1decff5 100644 --- a/internal/frontend/qml/ImportExportUI/DateRange.qml +++ b/internal/frontend/qml/ImportExportUI/DateRange.qml @@ -25,14 +25,15 @@ import ImportExportUI 1.0 Column { id: dateRange - property var structure : structureExternal - property string sourceID : structureExternal.getID ( -1 ) + property var structure : transferRules + property string sourceID : "-1" property alias allDates : allDatesBox.checked property alias inputDateFrom : inputDateFrom property alias inputDateTo : inputDateTo - function setRange() {common.setRange()} + function getRange() {common.getRange()} + function setRangeFromTo(from, to) {common.setRangeFromTo(from, to)} function applyRange() {common.applyRange()} property var dropDownStyle : Style.dropDownLight diff --git a/internal/frontend/qml/ImportExportUI/DateRangeFunctions.qml b/internal/frontend/qml/ImportExportUI/DateRangeFunctions.qml index 8dceb497..aed413b9 100644 --- a/internal/frontend/qml/ImportExportUI/DateRangeFunctions.qml +++ b/internal/frontend/qml/ImportExportUI/DateRangeFunctions.qml @@ -34,7 +34,7 @@ Item { property alias inputDateFrom : inputDateFrom property alias inputDateTo : inputDateTo - function setRange() {common.setRange()} + function getRange() {common.getRange()} function applyRange() {common.applyRange()} */ @@ -43,11 +43,7 @@ Item { inputDateTo.setDate((new Date()).getTime()) } - function setRange(){ // 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 + function setRangeFromTo(folderFrom, folderTo){ // unix time in seconds if ( folderFrom == 0 && folderTo ==0 ) { dateRange.allDates = true } 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 if (dateRange.allDates) structure.setFromToDate(dateRange.sourceID, 0, 0) else { @@ -67,15 +72,10 @@ Item { } } - Connections { - target: dateRange - onStructureChanged: setRange() - } - Component.onCompleted: { inputDateFrom.updateRange(gui.netBday) inputDateTo.updateRange(new Date()) - setRange() + //getRange() } } diff --git a/internal/frontend/qml/ImportExportUI/DateRangeMenu.qml b/internal/frontend/qml/ImportExportUI/DateRangeMenu.qml index 4ef6853b..4382e8ab 100644 --- a/internal/frontend/qml/ImportExportUI/DateRangeMenu.qml +++ b/internal/frontend/qml/ImportExportUI/DateRangeMenu.qml @@ -31,8 +31,10 @@ Rectangle { property real padding : Style.dialog.spacing property bool down : popup.visible - property var structure : structureExternal - property string sourceID : structureExternal.getID(-1) + property var structure : transferRules + property string sourceID : "" + property int sourceFromDate : 0 + property int sourceToDate : 0 color: Style.transparent @@ -145,7 +147,17 @@ Rectangle { } } - onAboutToShow : dateRangeInput.setRange() + onAboutToShow : updateRange() onAboutToHide : dateRangeInput.applyRange() } + + function updateRange() { + dateRangeInput.setRangeFromTo(root.sourceFromDate, root.sourceToDate) + } + + Connections { + target:root + onSourceFromDateChanged: root.updateRange() + onSourceToDateChanged: root.updateRange() + } } diff --git a/internal/frontend/qml/ImportExportUI/DialogExport.qml b/internal/frontend/qml/ImportExportUI/DialogExport.qml index 2d44fb74..6ffd196d 100644 --- a/internal/frontend/qml/ImportExportUI/DialogExport.qml +++ b/internal/frontend/qml/ImportExportUI/DialogExport.qml @@ -91,8 +91,6 @@ Dialog { DateRange{ id: dateRangeInput - structure: structurePM - sourceID: structurePM.getID(-1) } OutputFormat { @@ -142,7 +140,7 @@ Dialog { id: buttonNext fa_icon: Style.fa.check text: qsTr("Export","todo") - enabled: structurePM != 0 + enabled: transferRules != 0 color_main: Style.dialog.background color_minor: enabled ? Style.dialog.textBlue : Style.main.textDisabled isOpaque: true @@ -168,13 +166,17 @@ Dialog { spacing: Style.main.rightMargin AccessibleText { id: statusLabel - text : qsTr("Exporting to:") + text : qsTr("Status:") font.pointSize: Style.main.iconSize * Style.pt color : Style.main.text } AccessibleText { 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 width: progressbarExport.width - parent.spacing - statusLabel.width font.pointSize: Style.dialog.textSize * Style.pt @@ -310,15 +312,17 @@ Dialog { function check_inputs() { if (currentIndex == 1) { // 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")) return false } // at least one source selected - if (!structurePM.atLeastOneSelected) { - errorPopup.show(qsTr("Please select at least one item to export.", "todo")) - return false - } + /* + if (!transferRules.atLeastOneSelected) { + errorPopup.show(qsTr("Please select at least one item to export.", "todo")) + return false + } + */ // check path var folderCheck = go.checkPathStatus(outputPathInput.path) switch (folderCheck) { @@ -364,7 +368,6 @@ Dialog { errorPopup.buttonYes.visible = true errorPopup.buttonNo.visible = true errorPopup.buttonOkay.visible = false - errorPopup.checkbox.text = root.msgClearUnfished errorPopup.show ("Are you sure you want to cancel this export?") } @@ -374,10 +377,7 @@ Dialog { case 0 : case 1 : root.hide(); break; case 2 : // progress bar - go.cancelProcess ( - errorPopup.checkbox.text == root.msgClearUnfished && - errorPopup.checkbox.checked - ); + go.cancelProcess(); // no break default: root.clear_status() @@ -395,7 +395,7 @@ Dialog { root.hide() break case 0: // loading structure - dateRangeInput.setRange() + dateRangeInput.getRange() //no break default: incrementCurrentIndex() @@ -426,7 +426,7 @@ Dialog { switch (currentIndex) { case 0: go.loadStructureForExport(root.address) - sourceFoldersInput.hasItems = (structurePM.rowCount() > 0) + sourceFoldersInput.hasItems = (transferRules.rowCount() > 0) break case 2: dateRangeInput.applyRange() diff --git a/internal/frontend/qml/ImportExportUI/DialogImport.qml b/internal/frontend/qml/ImportExportUI/DialogImport.qml index 380e3fd4..c4fd4c1b 100644 --- a/internal/frontend/qml/ImportExportUI/DialogImport.qml +++ b/internal/frontend/qml/ImportExportUI/DialogImport.qml @@ -327,6 +327,7 @@ Dialog { iconText: Style.fa.refresh textColor: Style.main.textBlue onClicked: { + go.resetSource() root.decrementCurrentIndex() timer.start() } @@ -408,20 +409,13 @@ Dialog { spacing: Style.main.rightMargin AccessibleText { id: statusLabel - text : qsTr("Importing from:") + text : qsTr("Status:") font.pointSize: Style.main.iconSize * Style.pt color : Style.main.text } AccessibleText { anchors.baseline: statusLabel.baseline - text : { - var sourceFolder = root.isFromFile ? root.inputPath : inputEmail.text - if (go.progressDescription != gui.enums.progressInit && go.progress!=0) { - sourceFolder += "/" - sourceFolder += go.progressDescription - } - return sourceFolder - } + text : go.progressDescription == "" ? qsTr("importing") : go.progressDescription elide: Text.ElideMiddle width: progressbarImport.width - parent.spacing - statusLabel.width font.pointSize: Style.dialog.textSize * Style.pt @@ -582,9 +576,9 @@ Dialog { spacing : Style.dialog.heightSeparator 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 - color: Style.main.textGreen + color: go.progressDescription!="" ? Style.main.textRed : Style.main.textGreen font.bold : true font.family: Style.fontawesome.name } @@ -605,11 +599,7 @@ Dialog { text : qsTr("View errors") color_main : Style.dialog.textBlue onClicked : { - if (go.importLogFileName=="") { - console.log("onViewErrors: missing import log file name") - return - } - go.loadImportReports(go.importLogFileName) + go.loadImportReports() reportList.show() } } @@ -619,10 +609,6 @@ Dialog { text : qsTr("Report files") color_main : Style.dialog.textBlue onClicked : { - if (go.importLogFileName=="") { - console.log("onReportError: missing import log file name") - return - } root.ask_send_report() } } @@ -755,7 +741,6 @@ Dialog { } function clear() { - go.resetSource() root.inputPath = "" clear_status() inputEmail.clear() @@ -781,7 +766,7 @@ Dialog { onClickedYes : { if (errorPopup.msgID == "ask_send_report") { errorPopup.hide() - root.report_sent(go.sendImportReport(root.address,go.importLogFileName)) + root.report_sent(go.sendImportReport(root.address)) return } root.cancel() @@ -857,10 +842,13 @@ Dialog { } break case 3: // import insturctions - if (!structureExternal.hasTarget()) { - errorPopup.show(qsTr("Nothing selected for import.")) - return false - } + /* + console.log(" ====== TODO ======== ") + if (!structureExternal.hasTarget()) { + errorPopup.show(qsTr("Nothing selected for import.")) + return false + } + */ break case 4: // import status } @@ -880,7 +868,7 @@ Dialog { root.hide() break case DialogImport.Page.Progress: - go.cancelProcess(false) + go.cancelProcess() root.currentIndex=3 root.clear_status() globalLabels.reset() @@ -905,7 +893,7 @@ Dialog { globalLabels.labelName, globalLabels.labelColor, true, - structureExternal.getID(-1) + "-1" ) if (!isOK) return } @@ -919,7 +907,8 @@ Dialog { case DialogImport.Page.LoadingStructure: globalLabels.reset() - importInstructions.hasItems = (structureExternal.rowCount() > 0) + // TODO_: importInstructions.hasItems = (structureExternal.rowCount() > 0) + importInstructions.hasItems = true case DialogImport.Page.ImapSource: default: incrementCurrentIndex() @@ -1008,7 +997,7 @@ Dialog { case DialogImport.Page.SelectSourceType: case DialogImport.Page.ImapSource: case DialogImport.Page.SourceToTarget: - globalDateRange.setRange() + globalDateRange.getRange() break case DialogImport.Page.LoadingStructure: go.setupAndLoadForImport( diff --git a/internal/frontend/qml/ImportExportUI/ExportStructure.qml b/internal/frontend/qml/ImportExportUI/ExportStructure.qml index 867f3ab5..ebac48af 100644 --- a/internal/frontend/qml/ImportExportUI/ExportStructure.qml +++ b/internal/frontend/qml/ImportExportUI/ExportStructure.qml @@ -92,7 +92,7 @@ Rectangle { clip : true orientation : ListView.Vertical boundsBehavior : Flickable.StopAtBounds - model : structurePM + model : transferRules cacheBuffer : 10000 anchors { @@ -125,27 +125,25 @@ Rectangle { } delegate: FolderRowButton { + property variant modelData: model width : root.width - 5*root.border.width - type : folderType - color : folderColor - title : folderName - isSelected : isFolderSelected + type : modelData.type + folderIconColor : modelData.iconColor + title : modelData.name + isSelected : modelData.isActive onClicked : { //console.log("Clicked", folderId, isSelected) - structurePM.setFolderSelection(folderId,!isSelected) + transferRules.setIsRuleActive(modelData.mboxID,!model.isActive) } } - section.property: "folderType" + section.property: "type" section.delegate: FolderRowButton { isSection : true width : root.width - 5*root.border.width title : gui.folderTypeTitle(section) - isSelected : { - //console.log("section selected changed: ", section) - return section == gui.enums.folderTypeLabel ? structurePM.selectedLabels : structurePM.selectedFolders - } - onClicked : structurePM.selectType(section,!isSelected) + isSelected : section == gui.enums.folderTypeLabel ? transferRules.isLabelGroupSelected : transferRules.isFolderGroupSelected + onClicked : transferRules.setIsGroupActive(section,!isSelected) } } } diff --git a/internal/frontend/qml/ImportExportUI/FolderRowButton.qml b/internal/frontend/qml/ImportExportUI/FolderRowButton.qml index 43e85554..a7c79db1 100644 --- a/internal/frontend/qml/ImportExportUI/FolderRowButton.qml +++ b/internal/frontend/qml/ImportExportUI/FolderRowButton.qml @@ -26,9 +26,9 @@ AccessibleButton { property bool isSection : false property bool isSelected : false - property string title : "N/A" - property string type : "" - property color color : "black" + property string title : "N/A" + property string type : "" + property string folderIconColor : Style.main.textBlue height : Style.exporting.rowHeight padding : 0.0 @@ -72,7 +72,7 @@ AccessibleButton { left : checkbox.left 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 { family : Style.fontawesome.name pointSize : Style.dialog.fontSize * Style.pt diff --git a/internal/frontend/qml/ImportExportUI/ImportDelegate.qml b/internal/frontend/qml/ImportExportUI/ImportDelegate.qml index ebb72d35..aa916186 100644 --- a/internal/frontend/qml/ImportExportUI/ImportDelegate.qml +++ b/internal/frontend/qml/ImportExportUI/ImportDelegate.qml @@ -39,7 +39,7 @@ Rectangle { } property real iconWidth : nameWidth*0.3 - property bool isSourceSelected: targetFolderID!="" + property bool isSourceSelected: isActive property string lastTargetFolder: "6" // Archive property string lastTargetLabels: "" // no flag by default @@ -71,7 +71,7 @@ Rectangle { Text { id: folderIcon - text : gui.folderIcon(folderName, gui.enums.folderTypeFolder) + text : gui.folderIcon(name, gui.enums.folderTypeFolder) anchors.verticalCenter : parent.verticalCenter color: root.isSourceSelected ? Style.main.text : Style.main.textDisabled font { @@ -81,7 +81,7 @@ Rectangle { } Text { - text : folderName + text : name width: nameWidth elide: Text.ElideRight anchors.verticalCenter : parent.verticalCenter @@ -102,24 +102,27 @@ Rectangle { SelectFolderMenu { id: selectFolder - sourceID: folderId - selectedIDs: targetFolderID + sourceID: mboxID + targets: transferRules.targetFolders(mboxID) width: nameWidth anchors.verticalCenter : parent.verticalCenter + enabled: root.isSourceSelected onDoNotImport: root.toggleImport() onImportToFolder: root.importToFolder(newTargetID) } SelectLabelsMenu { - sourceID: folderId - selectedIDs: targetLabelIDs + sourceID: mboxID + targets: transferRules.targetLabels(mboxID) width: nameWidth anchors.verticalCenter : parent.verticalCenter enabled: root.isSourceSelected + onAddTargetLabel: { transferRules.addTargetID(sourceID, newTargetID) } + onRemoveTargetLabel: { transferRules.removeTargetID(sourceID, newTargetID) } } LabelIconList { - selectedIDs: targetLabelIDs + colorList: labelColors=="" ? [] : labelColors.split(";") width: iconWidth anchors.verticalCenter : parent.verticalCenter enabled: root.isSourceSelected @@ -127,38 +130,23 @@ Rectangle { DateRangeMenu { id: dateRangeMenu - sourceID: folderId + sourceID: mboxID + sourceFromDate: fromDate + sourceToDate: toDate enabled: root.isSourceSelected anchors.verticalCenter : parent.verticalCenter + + Component.onCompleted : dateRangeMenu.updateRange() } } function importToFolder(newTargetID) { - if (root.isSourceSelected) { - structureExternal.setTargetFolderID(folderId,newTargetID) - } else { - lastTargetFolder = newTargetID - toggleImport() - } + transferRules.addTargetID(mboxID,newTargetID) } function toggleImport() { - if (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 - } + transferRules.setIsRuleActive(mboxID, !root.isSourceSelected) } - } diff --git a/internal/frontend/qml/ImportExportUI/ImportStructure.qml b/internal/frontend/qml/ImportExportUI/ImportStructure.qml index da94e46f..17956039 100644 --- a/internal/frontend/qml/ImportExportUI/ImportStructure.qml +++ b/internal/frontend/qml/ImportExportUI/ImportStructure.qml @@ -50,7 +50,6 @@ Rectangle { verticalAlignment: Text.AlignVCenter text: qsTr("No emails found for this source.","todo") } - } anchors { @@ -70,7 +69,7 @@ Rectangle { clip : true orientation : ListView.Vertical boundsBehavior : Flickable.StopAtBounds - model : structureExternal + model : transferRules cacheBuffer : 10000 delegate : ImportDelegate { width: root.width diff --git a/internal/frontend/qml/ImportExportUI/InlineDateRange.qml b/internal/frontend/qml/ImportExportUI/InlineDateRange.qml index 017b0eb4..8518048e 100644 --- a/internal/frontend/qml/ImportExportUI/InlineDateRange.qml +++ b/internal/frontend/qml/ImportExportUI/InlineDateRange.qml @@ -25,8 +25,8 @@ import ImportExportUI 1.0 Row { id: dateRange - property var structure : structureExternal - property string sourceID : structureExternal.getID ( -1 ) + property var structure : transferRules + property string sourceID : "-1" property alias allDates : allDatesBox.checked property alias inputDateFrom : inputDateFrom @@ -34,7 +34,7 @@ Row { property alias labelWidth: label.width - function setRange() {common.setRange()} + function getRange() {common.getRange()} function applyRange() {common.applyRange()} DateRangeFunctions {id:common} diff --git a/internal/frontend/qml/ImportExportUI/LabelIconList.qml b/internal/frontend/qml/ImportExportUI/LabelIconList.qml index bf6ab759..90c38858 100644 --- a/internal/frontend/qml/ImportExportUI/LabelIconList.qml +++ b/internal/frontend/qml/ImportExportUI/LabelIconList.qml @@ -26,42 +26,16 @@ Rectangle { id: root width: Style.main.fontSize * 2 height: metrics.height - property string selectedIDs : "" + property var colorList color: "transparent" - - DelegateModel { id: selectedLabels - filterOnGroup: "selected" - groups: DelegateModelGroup { - id: selected - name: "selected" - includeByDefault: true - } - model : structurePM + model : colorList delegate : Text { text : metrics.text font : metrics.font - color : folderColor===undefined ? "#000": folderColor - } - } - - 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 - } + color : modelData } } @@ -77,7 +51,7 @@ Rectangle { Row { anchors.left : root.left 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 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 @@ -88,9 +62,4 @@ Rectangle { model: selectedLabels } } - - Component.onCompleted: root.updateFilter() - onSelectedIDsChanged: root.updateFilter() - Connections { target: structurePM; onDataChanged:root.updateFilter() } } - diff --git a/internal/frontend/qml/ImportExportUI/SelectFolderMenu.qml b/internal/frontend/qml/ImportExportUI/SelectFolderMenu.qml index 5476aef4..2dc3b20d 100644 --- a/internal/frontend/qml/ImportExportUI/SelectFolderMenu.qml +++ b/internal/frontend/qml/ImportExportUI/SelectFolderMenu.qml @@ -26,19 +26,19 @@ ComboBox { //fixme rounded height: Style.main.fontSize*2 //fixme property string folderType: gui.enums.folderTypeFolder - property string selectedIDs property string sourceID + property var targets property bool isFolderType: root.folderType == gui.enums.folderTypeFolder - property bool hasTarget: root.selectedIDs != "" property bool below: true signal doNotImport() signal importToFolder(string newTargetID) + signal addTargetLabel(string newTargetID) + signal removeTargetLabel(string newTargetID) leftPadding: Style.dialog.spacing onDownChanged : { - if (root.down) view.model.updateFilter() root.below = popup.y>0 } @@ -58,30 +58,22 @@ ComboBox { } displayText: { - //console.trace() - //console.log("updatebox", view.currentIndex, root.hasTarget, root.selectedIDs, root.sourceID, root.folderType) - if (!root.hasTarget) { - if (root.isFolderType) return qsTr("Do not import") - return qsTr("No labels selected") - } - if (!root.isFolderType) return Style.fa.tags + " " + qsTr("Add/Remove labels") + console.log("Target Menu current", view.currentItem, view.currentIndex) + if (view.currentIndex >= 0) { + 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 selSplit = root.selectedIDs.split(";") - for (var selIndex in selSplit) { - var selectedID = selSplit[selIndex] - var selectedType = structurePM.getType(selectedID) - if (selectedType == gui.enums.folderTypeLabel) continue; // skip type::labele - 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 tgtName = view.currentItem.folderName + var tgtIcon = view.currentItem.folderIcon + var tgtColor = view.currentItem.folderColor + + if (tgtIcon != Style.fa.folder_open) { + return tgtIcon + " " + tgtName } - var iconColor = structurePM.getColor(selectedID) - return ''+ icon + " " + selectedName + + return ''+ tgtIcon + " " + 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 } - // Popup objects + // Popup row delegate: Rectangle { id: thisDelegate @@ -127,22 +119,15 @@ ComboBox { color: isHovered ? root.popup.hoverColor : root.popup.backColor - - property bool isSelected : { - var selected = root.selectedIDs.split(";") - for (var iSel in selected) { - var sel = selected[iSel] - if (folderId == sel){ - return true - } - } - return false - } + property bool isSelected : isActive + property string folderName: name + property string folderIcon: gui.folderIcon(name,type) + property string folderColor: (type == gui.enums.folderTypeLabel || type == gui.enums.folderTypeFolder) ? iconColor : root.popup.textColor Text { id: targetIcon - text: gui.folderIcon(folderName,folderType) - color : folderType != gui.enums.folderTypeSystem ? folderColor : root.popup.textColor + text: thisDelegate.folderIcon + color : thisDelegate.folderColor anchors { verticalCenter: parent.verticalCenter left: parent.left @@ -157,6 +142,7 @@ ComboBox { Text { id: targetName + anchors { verticalCenter: parent.verticalCenter left: targetIcon.right @@ -165,7 +151,7 @@ ComboBox { rightMargin: Style.dialog.spacing } - text: folderName + text: thisDelegate.folderName color : root.popup.textColor elide: Text.ElideRight @@ -209,16 +195,15 @@ ComboBox { onClicked: { //console.log(" click delegate") if (root.isFolderType) { // don't update if selected - if (!thisDelegate.isSelected) { - root.importToFolder(folderId) - } root.popup.close() - } - if (root.folderType==gui.enums.folderTypeLabel) { - if (thisDelegate.isSelected) { - structureExternal.removeTargetLabelID(sourceID,folderId) + if (!isActive) { + root.importToFolder(mboxID) + } + } else { + if (isActive) { + root.removeTargetLabel(mboxID) } else { - structureExternal.addTargetLabelID(sourceID,folderId) + root.addTargetLabel(mboxID) } } } @@ -295,14 +280,10 @@ ComboBox { clip : true anchors.fill : parent + model : root.targets + delegate : root.delegate - section.property : "sectionName" - section.delegate : Text{text: sectionName} - - model : FilterStructure { - filterOnGroup : root.folderType - delegate : root.delegate - } + currentIndex: view.model.selectedIndex } } @@ -338,10 +319,7 @@ ComboBox { onClicked : { //console.log("click", addButton.text) - var newName = "" - if ( typeof folderName !== 'undefined' && !structurePM.hasFolderWithName (folderName) ) { - newName = folderName - } + var newName = name winMain.popupFolderEdit.show(newName, "", "", root.folderType, sourceID) root.popup.close() } diff --git a/internal/frontend/qml/tst_GuiIE.qml b/internal/frontend/qml/tst_GuiIE.qml index 625edda0..c96f4ce1 100644 --- a/internal/frontend/qml/tst_GuiIE.qml +++ b/internal/frontend/qml/tst_GuiIE.qml @@ -210,7 +210,7 @@ Window { Component.onCompleted : { - testgui.winMain.x = 150 + testgui.winMain.x = 350 testgui.winMain.y = 100 } @@ -230,7 +230,7 @@ Window { } 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" }') @@ -265,7 +265,7 @@ Window { } ListModel{ - id: structurePM + id: structurePMOFF // group selectors property bool selectedLabels : false @@ -328,6 +328,7 @@ Window { } } + function setTypeSelected (model, folderType , toSelect ) { console.log(" select type ", folderType, toSelect) for (var i= -1; i= len(s.Details) { + if index.Row() >= len(e.records) { return core.NewQVariant() } - var p = s.Details[index.Row()] + var r = e.records[index.Row()] switch role { case MailSubject: - return qtcommon.NewQVariantString(p.MailSubject) + return qtcommon.NewQVariantString(r.Subject) case MailDate: - return qtcommon.NewQVariantString(p.MailDate) + return qtcommon.NewQVariantString(r.Time.String()) case MailFrom: - return qtcommon.NewQVariantString(p.MailFrom) + return qtcommon.NewQVariantString(r.From) case InputFolder: - return qtcommon.NewQVariantString(p.InputFolder) + return qtcommon.NewQVariantString(r.SourceID) case ErrorMessage: - return qtcommon.NewQVariantString(p.ErrorMessage) + return qtcommon.NewQVariantString(r.GetErrorMessage()) default: return core.NewQVariant() } } -func (s *ErrorListModel) rowCount(parent *core.QModelIndex) int { return len(s.Details) } -func (s *ErrorListModel) columnCount(parent *core.QModelIndex) int { return 1 } -func (s *ErrorListModel) roleNames() map[int]*core.QByteArray { return s.Roles() } +func (e *ErrorListModel) rowCount(parent *core.QModelIndex) int { return len(e.records) } +func (e *ErrorListModel) columnCount(parent *core.QModelIndex) int { return 1 } +func (e *ErrorListModel) roleNames() map[int]*core.QByteArray { return e.Roles() } -// Add more errors to list -func (s *ErrorListModel) Add(more []*ErrorDetail) { - s.BeginInsertRows(core.NewQModelIndex(), len(s.Details), len(s.Details)) - s.Details = append(s.Details, more...) - s.SetCount(len(s.Details)) - s.EndInsertRows() -} +func (e *ErrorListModel) load() { + if e.Progress == nil { + log.Error("Progress not connected") + return + } -// Clear removes all items in model -func (s *ErrorListModel) Clear() { - s.BeginRemoveRows(core.NewQModelIndex(), 0, len(s.Details)) - 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) - } - */ + e.BeginResetModel() + e.records = e.Progress.GetFailedMessages() + e.EndResetModel() } diff --git a/internal/frontend/qt-ie/export.go b/internal/frontend/qt-ie/export.go index d85a3cbb..e851ff02 100644 --- a/internal/frontend/qt-ie/export.go +++ b/internal/frontend/qt-ie/export.go @@ -21,6 +21,7 @@ package qtie import ( "github.com/ProtonMail/proton-bridge/internal/transfer" + "github.com/pkg/errors" ) const ( @@ -29,10 +30,11 @@ const ( ) func (f *FrontendQt) LoadStructureForExport(addressOrID string) { + errCode := errUnknownError var err error defer func() { if err != nil { - f.showError(err) + f.showError(errCode, errors.Wrap(err, "failed to load structure for "+addressOrID)) f.Qml.ExportStructureLoadFinished(false) } else { f.Qml.ExportStructureLoadFinished(true) @@ -40,20 +42,12 @@ func (f *FrontendQt) LoadStructureForExport(addressOrID string) { }() 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 } - f.PMStructure.Clear() - 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 + f.TransferRules.setTransfer(f.transfer) } func (f *FrontendQt) StartExport(rootPath, login, fileType string, attachEncryptedBody bool) { diff --git a/internal/frontend/qt-ie/frontend.go b/internal/frontend/qt-ie/frontend.go index 6b6305f5..10599904 100644 --- a/internal/frontend/qt-ie/frontend.go +++ b/internal/frontend/qt-ie/frontend.go @@ -65,11 +65,11 @@ type FrontendQt struct { programVersion string // Program version buildVersion string // Program build version - PMStructure *FolderStructure // Providing data for account labels and folders for ProtonMail account - ExternalStructure *FolderStructure // Providing data for account labels and folders for MBOX, EML or external IMAP account - ErrorList *ErrorListModel // Providing data for error reporting + TransferRules *TransferRules + ErrorList *ErrorListModel // Providing data for error reporting transfer *transfer.Transfer + progress *transfer.Progress notifyHasNoKeychain bool } @@ -103,102 +103,99 @@ func New( } // IsAppRestarting for Import-Export is always false i.e never restarts -func (s *FrontendQt) IsAppRestarting() bool { +func (f *FrontendQt) IsAppRestarting() bool { return false } // Loop function for Import-Export interface. It runs QtExecute in main thread // with no additional function. -func (s *FrontendQt) Loop(setupError error) (err error) { +func (f *FrontendQt) Loop(setupError error) (err error) { if setupError != nil { - s.notifyHasNoKeychain = true + f.notifyHasNoKeychain = true } go func() { - defer s.panicHandler.HandlePanic() - s.watchEvents() + defer f.panicHandler.HandlePanic() + f.watchEvents() }() - err = s.QtExecute(func(s *FrontendQt) error { return nil }) + err = f.QtExecute(func(f *FrontendQt) error { return nil }) return err } -func (s *FrontendQt) watchEvents() { - internetOffCh := qtcommon.MakeAndRegisterEvent(s.eventListener, events.InternetOffEvent) - internetOnCh := qtcommon.MakeAndRegisterEvent(s.eventListener, events.InternetOnEvent) - restartBridgeCh := qtcommon.MakeAndRegisterEvent(s.eventListener, events.RestartBridgeEvent) - addressChangedCh := qtcommon.MakeAndRegisterEvent(s.eventListener, events.AddressChangedEvent) - addressChangedLogoutCh := qtcommon.MakeAndRegisterEvent(s.eventListener, events.AddressChangedLogoutEvent) - logoutCh := qtcommon.MakeAndRegisterEvent(s.eventListener, events.LogoutEvent) - updateApplicationCh := qtcommon.MakeAndRegisterEvent(s.eventListener, events.UpgradeApplicationEvent) - newUserCh := qtcommon.MakeAndRegisterEvent(s.eventListener, events.UserRefreshEvent) +func (f *FrontendQt) watchEvents() { + internetOffCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.InternetOffEvent) + internetOnCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.InternetOnEvent) + restartBridgeCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.RestartBridgeEvent) + addressChangedCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.AddressChangedEvent) + addressChangedLogoutCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.AddressChangedLogoutEvent) + logoutCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.LogoutEvent) + updateApplicationCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.UpgradeApplicationEvent) + newUserCh := qtcommon.MakeAndRegisterEvent(f.eventListener, events.UserRefreshEvent) for { select { case <-internetOffCh: - s.Qml.SetConnectionStatus(false) + f.Qml.SetConnectionStatus(false) case <-internetOnCh: - s.Qml.SetConnectionStatus(true) + f.Qml.SetConnectionStatus(true) case <-restartBridgeCh: - s.Qml.SetIsRestarting(true) - s.App.Quit() + f.Qml.SetIsRestarting(true) + f.App.Quit() case address := <-addressChangedCh: - s.Qml.NotifyAddressChanged(address) + f.Qml.NotifyAddressChanged(address) case address := <-addressChangedLogoutCh: - s.Qml.NotifyAddressChangedLogout(address) + f.Qml.NotifyAddressChangedLogout(address) case userID := <-logoutCh: - user, err := s.ie.GetUser(userID) + user, err := f.ie.GetUser(userID) if err != nil { return } - s.Qml.NotifyLogout(user.Username()) + f.Qml.NotifyLogout(user.Username()) case <-updateApplicationCh: - s.Qml.ProcessFinished() - s.Qml.NotifyUpdate() + f.Qml.ProcessFinished() + f.Qml.NotifyUpdate() case <-newUserCh: - s.Qml.LoadAccounts() + f.Qml.LoadAccounts() } } } -func (s *FrontendQt) qtSetupQmlAndStructures() { - s.App = widgets.NewQApplication(len(os.Args), os.Args) +func (f *FrontendQt) qtSetupQmlAndStructures() { + f.App = widgets.NewQApplication(len(os.Args), os.Args) // view - s.View = qml.NewQQmlApplicationEngine(s.App) + f.View = qml.NewQQmlApplicationEngine(f.App) // Add Go-QML Import-Export - s.Qml = NewGoQMLInterface(nil) - s.Qml.SetFrontend(s) // provides access - s.View.RootContext().SetContextProperty("go", s.Qml) + f.Qml = NewGoQMLInterface(nil) + f.Qml.SetFrontend(f) // provides access + f.View.RootContext().SetContextProperty("go", f.Qml) + // Add AccountsModel - s.Accounts.SetupAccounts(s.Qml, s.ie) - s.View.RootContext().SetContextProperty("accountsModel", s.Accounts.Model) + f.Accounts.SetupAccounts(f.Qml, f.ie) + f.View.RootContext().SetContextProperty("accountsModel", f.Accounts.Model) - // Add ProtonMail FolderStructure - s.PMStructure = NewFolderStructure(nil) - s.View.RootContext().SetContextProperty("structurePM", s.PMStructure) - - // Add external FolderStructure - s.ExternalStructure = NewFolderStructure(nil) - s.View.RootContext().SetContextProperty("structureExternal", s.ExternalStructure) + // Add TransferRules structure + f.TransferRules = NewTransferRules(nil) + f.View.RootContext().SetContextProperty("transferRules", f.TransferRules) // Add error list modal - s.ErrorList = NewErrorListModel(nil) - s.View.RootContext().SetContextProperty("errorList", s.ErrorList) - s.Qml.ConnectLoadImportReports(s.ErrorList.load) + f.ErrorList = NewErrorListModel(nil) + f.View.RootContext().SetContextProperty("errorList", f.ErrorList) + f.Qml.ConnectLoadImportReports(f.ErrorList.load) // Import path and load QML files - s.View.AddImportPath("qrc:///") - s.View.Load(core.NewQUrl3("qrc:/uiie.qml", 0)) + f.View.AddImportPath("qrc:///") + f.View.Load(core.NewQUrl3("qrc:/uiie.qml", 0)) // TODO set the first start flag log.Error("Get FirstStart: Not implemented") //if prefs.Get(prefs.FirstStart) == "true" { if false { - s.Qml.SetIsFirstStart(true) + f.Qml.SetIsFirstStart(true) } else { - s.Qml.SetIsFirstStart(false) + f.Qml.SetIsFirstStart(false) } // Notify user about error during initialization. - if s.notifyHasNoKeychain { - s.Qml.NotifyHasNoKeychain() + if f.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 // thread). This functions reads the main user interface defined in QML files. // The files are appended to library by Qt-QRC. -func (s *FrontendQt) QtExecute(Procedure func(*FrontendQt) error) error { - qtcommon.QtSetupCoreAndControls(s.programName, s.programVersion) - s.qtSetupQmlAndStructures() +func (f *FrontendQt) QtExecute(Procedure func(*FrontendQt) error) error { + qtcommon.QtSetupCoreAndControls(f.programName, f.programVersion) + f.qtSetupQmlAndStructures() // 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("QML not loaded properly") } // 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 - if err := Procedure(s); err != nil { + if err := Procedure(f); err != nil { return err } // Loop @@ -234,63 +231,55 @@ func (s *FrontendQt) QtExecute(Procedure func(*FrontendQt) error) error { return nil } -func (s *FrontendQt) openLogs() { - go open.Run(s.config.GetLogDir()) +func (f *FrontendQt) openLogs() { + go open.Run(f.config.GetLogDir()) } -func (s *FrontendQt) openReport() { - go open.Run(s.Qml.ImportLogFileName()) +func (f *FrontendQt) openReport() { + go open.Run(f.Qml.ImportLogFileName()) } -func (s *FrontendQt) openDownloadLink() { - go open.Run(s.updates.GetDownloadLink()) +func (f *FrontendQt) openDownloadLink() { + go open.Run(f.updates.GetDownloadLink()) } -func (s *FrontendQt) sendImportReport(address, reportFile string) (isOK bool) { - /* - 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 +// sendImportReport sends an anonymized import or export report file to our customer support +func (f *FrontendQt) sendImportReport(address string) bool { // Todo_: Rename to sendReport? var accname = "No account logged in" - if s.Accounts.Model.Count() > 0 { - accname = s.Accounts.Model.Get(0).Account() + if f.Accounts.Model.Count() > 0 { + 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_PrettyProductName(), description, @@ -299,41 +288,43 @@ func (s *FrontendQt) sendBug(description, emailClient, address string) (isOK boo emailClient, ); err != nil { log.Errorln("while sendBug:", err) - isOK = false + return false } - return + + return true } // checkInternet is almost idetical to bridge -func (s *FrontendQt) checkInternet() { - s.Qml.SetConnectionStatus(s.ie.CheckConnection() == nil) +func (f *FrontendQt) checkInternet() { + f.Qml.SetConnectionStatus(f.ie.CheckConnection() == nil) } -func (s *FrontendQt) showError(err error) { - code := 0 // TODO err.Code() - s.Qml.SetErrorDescription(err.Error()) +func (f *FrontendQt) showError(code int, err error) { + f.Qml.SetErrorDescription(err.Error()) log.WithField("code", code).Errorln(err.Error()) - s.Qml.NotifyError(code) + f.Qml.NotifyError(code) } -func (s *FrontendQt) emitEvent(evType, msg string) { - s.eventListener.Emit(evType, msg) +func (f *FrontendQt) emitEvent(evType, msg string) { + f.eventListener.Emit(evType, msg) } -func (s *FrontendQt) setProgressManager(progress *transfer.Progress) { - s.Qml.ConnectPauseProcess(func() { progress.Pause("user") }) - s.Qml.ConnectResumeProcess(progress.Resume) - s.Qml.ConnectCancelProcess(func(clearUnfinished bool) { - // TODO clear unfinished +func (f *FrontendQt) setProgressManager(progress *transfer.Progress) { + f.progress = progress + f.ErrorList.Progress = progress + + f.Qml.ConnectPauseProcess(func() { progress.Pause("paused") }) + f.Qml.ConnectResumeProcess(progress.Resume) + f.Qml.ConnectCancelProcess(func() { progress.Stop() }) go func() { defer func() { - s.Qml.DisconnectPauseProcess() - s.Qml.DisconnectResumeProcess() - s.Qml.DisconnectCancelProcess() - s.Qml.SetProgress(1) + f.Qml.DisconnectPauseProcess() + f.Qml.DisconnectResumeProcess() + f.Qml.DisconnectCancelProcess() + f.Qml.SetProgress(1) }() //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() if total != 0 { // udate total - s.Qml.SetTotal(int(total)) + f.Qml.SetTotal(int(total)) } - s.Qml.SetProgressFails(int(failed)) - s.Qml.SetProgressDescription(progress.PauseReason()) // TODO add description when changing folders? + f.Qml.SetProgressFails(int(failed)) + f.Qml.SetProgressDescription(progress.PauseReason()) // TODO add description when changing folders? if total > 0 { newProgress := float32(imported+failed) / float32(total) - if newProgress >= 0 && newProgress != s.Qml.Progress() { - s.Qml.SetProgress(newProgress) - s.Qml.ProgressChanged(newProgress) + if newProgress >= 0 && newProgress != f.Qml.Progress() { + f.Qml.SetProgress(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 -func (s *FrontendQt) StartUpdate() { +func (f *FrontendQt) StartUpdate() { progress := make(chan updates.Progress) go func() { // Update progress in QML. - defer s.panicHandler.HandlePanic() + defer f.panicHandler.HandlePanic() for current := range progress { - s.Qml.SetProgress(current.Processed) - s.Qml.SetProgressDescription(strconv.Itoa(current.Description)) + f.Qml.SetProgress(current.Processed) + f.Qml.SetProgressDescription(strconv.Itoa(current.Description)) // Error happend if current.Err != nil { log.Error("update progress: ", current.Err) - s.Qml.UpdateFinished(true) + f.Qml.UpdateFinished(true) return } // Finished everything OK. if current.Description >= updates.InfoQuitApp { - s.Qml.UpdateFinished(false) + f.Qml.UpdateFinished(false) time.Sleep(3 * time.Second) // Just notify. - s.Qml.SetIsRestarting(current.Description == updates.InfoRestartApp) - s.App.Quit() + f.Qml.SetIsRestarting(current.Description == updates.InfoRestartApp) + f.App.Quit() return } } }() go func() { - defer s.panicHandler.HandlePanic() - s.updates.StartUpgrade(progress) + defer f.panicHandler.HandlePanic() + f.updates.StartUpgrade(progress) }() } // isNewVersionAvailable is identical to bridge // return 0 when local version is fine // return 1 when new version is available -func (s *FrontendQt) isNewVersionAvailable(showMessage bool) { +func (f *FrontendQt) isNewVersionAvailable(showMessage bool) { go func() { - defer s.Qml.ProcessFinished() - isUpToDate, latestVersionInfo, err := s.updates.CheckIsUpToDate() + defer f.Qml.ProcessFinished() + isUpToDate, latestVersionInfo, err := f.updates.CheckIsUpToDate() if err != nil { log.Warnln("Cannot retrieve version info: ", err) - s.checkInternet() + f.checkInternet() 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 { - s.Qml.SetUpdateState(StatusUpToDate) + f.Qml.SetUpdateState(StatusUpToDate) if showMessage { - s.Qml.NotifyVersionIsTheLatest() + f.Qml.NotifyVersionIsTheLatest() } return } - s.Qml.SetNewversion(latestVersionInfo.Version) - s.Qml.SetChangelog(latestVersionInfo.ReleaseNotes) - s.Qml.SetBugfixes(latestVersionInfo.ReleaseFixedBugs) - s.Qml.SetLandingPage(latestVersionInfo.LandingPage) - s.Qml.SetDownloadLink(latestVersionInfo.GetDownloadLink()) - s.Qml.SetUpdateState(StatusNewVersionAvailable) + f.Qml.SetNewversion(latestVersionInfo.Version) + f.Qml.SetChangelog(latestVersionInfo.ReleaseNotes) + f.Qml.SetBugfixes(latestVersionInfo.ReleaseFixedBugs) + f.Qml.SetLandingPage(latestVersionInfo.LandingPage) + f.Qml.SetDownloadLink(latestVersionInfo.GetDownloadLink()) + f.Qml.SetUpdateState(StatusNewVersionAvailable) }() } -func (s *FrontendQt) resetSource() { - if s.transfer != nil { - s.transfer.ResetRules() - if err := s.loadStructuresForImport(); err != nil { +func (f *FrontendQt) resetSource() { + if f.transfer != nil { + f.transfer.ResetRules() + if err := f.loadStructuresForImport(); err != nil { log.WithError(err).Error("Cannot reload structures after reseting rules.") } } } // getLocalVersionInfo is identical to bridge. -func (s *FrontendQt) getLocalVersionInfo() { - defer s.Qml.ProcessFinished() - localVersion := s.updates.GetLocalVersion() - s.Qml.SetNewversion(localVersion.Version) - s.Qml.SetChangelog(localVersion.ReleaseNotes) - s.Qml.SetBugfixes(localVersion.ReleaseFixedBugs) +func (f *FrontendQt) getLocalVersionInfo() { + defer f.Qml.ProcessFinished() + localVersion := f.updates.GetLocalVersion() + f.Qml.SetNewversion(localVersion.Version) + f.Qml.SetChangelog(localVersion.ReleaseNotes) + f.Qml.SetBugfixes(localVersion.ReleaseFixedBugs) } // LeastUsedColor is intended to return color for creating a new inbox or label. -func (s *FrontendQt) leastUsedColor() string { - if s.transfer == nil { +func (f *FrontendQt) leastUsedColor() string { + if f.transfer == nil { log.Errorln("Getting least used color before transfer exist.") return "#7272a7" } - m, err := s.transfer.TargetMailboxes() + m, err := f.transfer.TargetMailboxes() if err != nil { log.Errorln("Getting least used color:", err) - s.showError(err) + f.showError(errUnknownError, err) } return transfer.LeastUsedColor(m) } // 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. m := transfer.Mailbox{ Name: name, @@ -466,32 +461,28 @@ func (s *FrontendQt) createLabelOrFolder(email, name, color string, isLabel bool // Select least used color if no color given. if m.Color == "" { - m.Color = s.leastUsedColor() + m.Color = f.leastUsedColor() } + f.TransferRules.BeginResetModel() + defer f.TransferRules.EndResetModel() + // Create mailbox. - newLabel, err := s.transfer.CreateTargetMailbox(m) - + m, err := f.transfer.CreateTargetMailbox(m) if err != nil { 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 { - s.ExternalStructure.addTargetLabelID(sourceID, newLabel.ID) + f.showError(errCreateLabelFailed, err) } 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 } diff --git a/internal/frontend/qt-ie/import.go b/internal/frontend/qt-ie/import.go index 058dff58..6e80dda9 100644 --- a/internal/frontend/qt-ie/import.go +++ b/internal/frontend/qt-ie/import.go @@ -19,14 +19,19 @@ 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 func (f *FrontendQt) setupAndLoadForImport(isFromIMAP bool, sourcePath, sourceEmail, sourcePassword, sourceServer, sourcePort, targetAddress string) { + errCode := errUnknownError var err error defer func() { if err != nil { - f.showError(err) + f.showError(errCode, err) f.Qml.ImportStructuresLoadFinished(false) } else { f.Qml.ImportStructuresLoadFinished(true) @@ -36,11 +41,23 @@ func (f *FrontendQt) setupAndLoadForImport(isFromIMAP bool, sourcePath, sourceEm if isFromIMAP { f.transfer, err = f.ie.GetRemoteImporter(targetAddress, sourceEmail, sourcePassword, sourceServer, sourcePort) 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 } } else { f.transfer, err = f.ie.GetLocalImporter(targetAddress, sourcePath) if err != nil { + // The only error can be problem to load PM user and address. + errCode = errPMLoadFailed return } } @@ -51,27 +68,7 @@ func (f *FrontendQt) setupAndLoadForImport(isFromIMAP bool, sourcePath, sourceEm } func (f *FrontendQt) loadStructuresForImport() error { - f.PMStructure.Clear() - 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 + f.TransferRules.setTransfer(f.transfer) return nil } @@ -82,8 +79,9 @@ func (f *FrontendQt) StartImport(email string) { // TODO email not needed f.Qml.SetProgress(0.0) f.Qml.SetTotal(1) f.Qml.SetImportLogFileName("") - f.ErrorList.Clear() progress := f.transfer.Start() + + f.Qml.SetImportLogFileName(progress.FileReport()) f.setProgressManager(progress) } diff --git a/internal/frontend/qt-ie/mbox.go b/internal/frontend/qt-ie/mbox.go new file mode 100644 index 00000000..7f791c89 --- /dev/null +++ b/internal/frontend/qt-ie/mbox.go @@ -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 . + +// +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) +} diff --git a/internal/frontend/qt-ie/transfer_rules.go b/internal/frontend/qt-ie/transfer_rules.go new file mode 100644 index 00000000..8b88f94e --- /dev/null +++ b/internal/frontend/qt-ie/transfer_rules.go @@ -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 . + +// +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) +} diff --git a/internal/frontend/qt-ie/ui.go b/internal/frontend/qt-ie/ui.go index e70663ae..c9414de8 100644 --- a/internal/frontend/qt-ie/ui.go +++ b/internal/frontend/qt-ie/ui.go @@ -71,7 +71,7 @@ type GoQMLInterface struct { _ func() `signal:"openManual"` _ func(showMessage bool) `signal:"runCheckVersion"` _ func() `slot:"getLocalVersionInfo"` - _ func(fname string) `slot:"loadImportReports"` + _ func() `slot:"loadImportReports"` _ func() `slot:"quit"` _ func() `slot:"loadAccounts"` @@ -87,7 +87,7 @@ type GoQMLInterface struct { _ func() string `slot:"getBackendVersion"` _ 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() string `slot:"leastUsedColor"` _ 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(tabIndex int, message string) `signal:"notifyBubble"` - _ func() `signal:"bubbleClosed"` - _ func() `signal:"simpleErrorHappen"` - _ func() `signal:"askErrorHappen"` - _ func() `signal:"retryErrorHappen"` - _ func() `signal:"pauseProcess"` - _ func() `signal:"resumeProcess"` - _ func(clearUnfinished bool) `signal:"cancelProcess"` + _ func() `signal:"bubbleClosed"` + _ func() `signal:"simpleErrorHappen"` + _ func() `signal:"askErrorHappen"` + _ func() `signal:"retryErrorHappen"` + _ func() `signal:"pauseProcess"` + _ func() `signal:"resumeProcess"` + _ func() `signal:"cancelProcess"` _ func(iAccount int, prefRem bool) `slot:"deleteAccount"` _ func(iAccount int) `slot:"logoutAccount"` diff --git a/internal/frontend/types/types.go b/internal/frontend/types/types.go index f5781421..83d61b66 100644 --- a/internal/frontend/types/types.go +++ b/internal/frontend/types/types.go @@ -114,6 +114,7 @@ type ImportExporter interface { GetMBOXExporter(string, string) (*transfer.Transfer, error) SetCurrentOS(os string) ReportBug(osType, osVersion, description, accountName, address, emailClient string) error + ReportFile(osType, osVersion, accountName, address string, logdata []byte) error } type importExportWrap struct { diff --git a/internal/importexport/credits.go b/internal/importexport/credits.go index 311da61d..b10c5df9 100644 --- a/internal/importexport/credits.go +++ b/internal/importexport/credits.go @@ -15,8 +15,8 @@ // You should have received a copy of the GNU General Public License // along with ProtonMail Bridge. If not, see . -// 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 -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;" diff --git a/internal/importexport/importexport.go b/internal/importexport/importexport.go index ceaf7176..761c2992 100644 --- a/internal/importexport/importexport.go +++ b/internal/importexport/importexport.go @@ -19,8 +19,11 @@ package importexport import ( + "bytes" + "github.com/ProtonMail/proton-bridge/internal/transfer" "github.com/ProtonMail/proton-bridge/internal/users" + "github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/listener" logrus "github.com/sirupsen/logrus" @@ -61,15 +64,17 @@ func (ie *ImportExport) ReportBug(osType, osVersion, description, accountName, a defer c.Logout() title := "[Import-Export] Bug" - if err := c.ReportBugWithEmailClient( - osType, - osVersion, - title, - description, - accountName, - address, - emailClient, - ); err != nil { + report := pmapi.ReportReq{ + OS: osType, + OSVersion: osVersion, + Browser: emailClient, + Title: title, + Description: description, + Username: accountName, + Email: address, + } + + if err := c.Report(report); err != nil { log.Error("Reporting bug failed: ", err) return err } @@ -79,6 +84,35 @@ func (ie *ImportExport) ReportBug(osType, osVersion, description, accountName, a 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. func (ie *ImportExport) GetLocalImporter(address, path string) (*transfer.Transfer, error) { source := transfer.NewLocalProvider(path) @@ -130,7 +164,7 @@ func (ie *ImportExport) getPMAPIProvider(address string) (*transfer.PMAPIProvide addressID, err := user.GetAddressID(address) 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) diff --git a/internal/importexport/release_notes.go b/internal/importexport/release_notes.go index 71713e72..c922d763 100644 --- a/internal/importexport/release_notes.go +++ b/internal/importexport/release_notes.go @@ -15,7 +15,7 @@ // You should have received a copy of the GNU General Public License // along with ProtonMail Bridge. If not, see . -// 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 diff --git a/internal/store/mocks/mocks.go b/internal/store/mocks/mocks.go index 8fd5c2d6..467863be 100644 --- a/internal/store/mocks/mocks.go +++ b/internal/store/mocks/mocks.go @@ -5,10 +5,9 @@ package mocks import ( - reflect "reflect" - pmapi "github.com/ProtonMail/proton-bridge/pkg/pmapi" gomock "github.com/golang/mock/gomock" + reflect "reflect" ) // MockPanicHandler is a mock of PanicHandler interface diff --git a/internal/store/mocks/utils_mocks.go b/internal/store/mocks/utils_mocks.go index 940bf172..3f43bb43 100644 --- a/internal/store/mocks/utils_mocks.go +++ b/internal/store/mocks/utils_mocks.go @@ -5,10 +5,9 @@ package mocks import ( + gomock "github.com/golang/mock/gomock" reflect "reflect" time "time" - - gomock "github.com/golang/mock/gomock" ) // MockListener is a mock of Listener interface diff --git a/internal/transfer/mailbox.go b/internal/transfer/mailbox.go index 430db7c9..db300a54 100644 --- a/internal/transfer/mailbox.go +++ b/internal/transfer/mailbox.go @@ -33,6 +33,11 @@ type Mailbox struct { 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. func (m Mailbox) Hash() string { return fmt.Sprintf("%x", sha256.Sum256([]byte(m.Name))) diff --git a/internal/transfer/progress.go b/internal/transfer/progress.go index 28c37f38..0798bed5 100644 --- a/internal/transfer/progress.go +++ b/internal/transfer/progress.go @@ -198,7 +198,7 @@ func (p *Progress) callWrap(callback func() error) { break } - p.Pause(err.Error()) + p.Pause("paused due to " + err.Error()) } } @@ -333,3 +333,11 @@ func (p *Progress) GenerateBugReport() []byte { } return bugReport.getData() } + +func (p *Progress) FileReport() (path string) { + if r := p.fileReport; r != nil { + path = r.path + } + + return +} diff --git a/internal/transfer/provider_eml.go b/internal/transfer/provider_eml.go index 051afe1f..ec4e794b 100644 --- a/internal/transfer/provider_eml.go +++ b/internal/transfer/provider_eml.go @@ -39,9 +39,13 @@ func (p *EMLProvider) ID() string { // 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 // 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 err error if includeEmpty { folderNames, err = getFolderNames(p.root) } else { @@ -51,7 +55,6 @@ func (p *EMLProvider) Mailboxes(includeEmpty, includeAllMail bool) ([]Mailbox, e return nil, err } - mailboxes := []Mailbox{} for _, folderName := range folderNames { mailboxes = append(mailboxes, Mailbox{ ID: "", diff --git a/internal/transfer/provider_imap_errors.go b/internal/transfer/provider_imap_errors.go new file mode 100644 index 00000000..bbc2aefb --- /dev/null +++ b/internal/transfer/provider_imap_errors.go @@ -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 . + +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 +} diff --git a/internal/transfer/provider_imap_utils.go b/internal/transfer/provider_imap_utils.go index 36b3cb2c..1edfeaff 100644 --- a/internal/transfer/provider_imap_utils.go +++ b/internal/transfer/provider_imap_utils.go @@ -137,7 +137,7 @@ func (p *IMAPProvider) auth() error { //nolint[funlen] log.Info("Connecting to server") 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 @@ -149,7 +149,7 @@ func (p *IMAPProvider) auth() error { //nolint[funlen] client, err = imapClient.DialTLS(p.addr, 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")} @@ -170,7 +170,7 @@ func (p *IMAPProvider) auth() error { //nolint[funlen] capability, err := p.client.Capability() log.WithField("capability", capability).WithError(err).Debug("Server capability") if err != nil { - return errors.Wrap(err, "failed to get capabilities") + return ErrIMAPConnection{imapError{Err: err, Message: "failed to get capabilities"}} } // SASL AUTH PLAIN @@ -178,7 +178,7 @@ func (p *IMAPProvider) auth() error { //nolint[funlen] log.Debug("Trying plain auth") authPlain := sasl.NewPlainClient("", p.username, p.password) 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 { log.Debug("Trying login") 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 { - return errors.New("unknown auth method") + return ErrIMAPAuthMethod{imapError{Err: err, Message: "unknown auth method"}} } log.Info("Logged in") diff --git a/internal/transfer/provider_pmapi.go b/internal/transfer/provider_pmapi.go index 61f708de..3baa5993 100644 --- a/internal/transfer/provider_pmapi.go +++ b/internal/transfer/provider_pmapi.go @@ -38,20 +38,24 @@ type PMAPIProvider struct { // NewPMAPIProvider returns new PMAPIProvider. func NewPMAPIProvider(clientManager ClientManager, userID, addressID string) (*PMAPIProvider, error) { - keyRing, err := clientManager.GetClient(userID).KeyRingForAddressID(addressID) - if err != nil { - return nil, errors.Wrap(err, "failed to get key ring") - } - - return &PMAPIProvider{ + provider := &PMAPIProvider{ clientManager: clientManager, userID: userID, addressID: addressID, - keyRing: keyRing, importMsgReqMap: map[string]*pmapi.ImportMsgReq{}, 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 { @@ -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 { if !includeEmpty && emptyLabelsMap[label.ID] { continue diff --git a/internal/transfer/provider_pmapi_source.go b/internal/transfer/provider_pmapi_source.go index 6c383812..9a9c3e6f 100644 --- a/internal/transfer/provider_pmapi_source.go +++ b/internal/transfer/provider_pmapi_source.go @@ -86,14 +86,15 @@ func (p *PMAPIProvider) transferTo(rule *Rule, progress *Progress, ch chan<- Mes progress.callWrap(func() error { desc := false pmapiMessages, count, err := p.listMessages(&pmapi.MessagesFilter{ - LabelID: rule.SourceMailbox.ID, - Begin: rule.FromTime, - End: rule.ToTime, - BeginID: nextID, - PageSize: pmapiListPageSize, - Page: 0, - Sort: "ID", - Desc: &desc, + AddressID: p.addressID, + LabelID: rule.SourceMailbox.ID, + Begin: rule.FromTime, + End: rule.ToTime, + BeginID: nextID, + PageSize: pmapiListPageSize, + Page: 0, + Sort: "ID", + Desc: &desc, }) if err != nil { return err diff --git a/internal/transfer/rules.go b/internal/transfer/rules.go index 2bf03915..7cd067b2 100644 --- a/internal/transfer/rules.go +++ b/internal/transfer/rules.go @@ -22,6 +22,7 @@ import ( "fmt" "os" "path/filepath" + "sort" "strings" "time" @@ -42,6 +43,11 @@ type transferRules struct { // E.g., every message will be imported into this 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 // be decrypted should be exported or skipped. skipEncryptedMessages bool @@ -81,10 +87,18 @@ func (r *transferRules) setGlobalMailbox(mailbox *Mailbox) { } 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 { if !rule.HasTimeLimit() { - rule.FromTime = fromTime - rule.ToTime = toTime + rule.FromTime = r.globalFromTime + rule.ToTime = r.globalToTime } } } @@ -122,8 +136,9 @@ func (r *transferRules) setDefaultRules(sourceMailboxes []Mailbox, targetMailbox } targetMailboxes := sourceMailbox.findMatchingMailboxes(targetMailboxes) - if len(targetMailboxes) == 0 { - targetMailboxes = defaultCallback(sourceMailbox) + + if !containsExclusive(targetMailboxes) { + targetMailboxes = append(targetMailboxes, defaultCallback(sourceMailbox)...) } active := true @@ -147,10 +162,14 @@ func (r *transferRules) setDefaultRules(sourceMailboxes []Mailbox, targetMailbox } } - for _, rule := range r.rules { - if !rule.Active { - continue - } + // There is no point showing rule which has no action (i.e., source mailbox + // is not available). + // 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 for _, sourceMailbox := range sourceMailboxes { if sourceMailbox.Name == rule.SourceMailbox.Name { @@ -158,7 +177,7 @@ func (r *transferRules) setDefaultRules(sourceMailboxes []Mailbox, targetMailbox } } if !found { - rule.Active = false + delete(r.rules, key) } } @@ -216,6 +235,7 @@ func (r *transferRules) getRules() []*Rule { for _, rule := range r.rules { rules = append(rules, rule) } + sort.Sort(byRuleOrder(rules)) return rules } @@ -288,3 +308,59 @@ func (r *Rule) TargetMailboxNames() (names []string) { } 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 +} diff --git a/internal/transfer/rules_test.go b/internal/transfer/rules_test.go index f887a8ac..a1d93582 100644 --- a/internal/transfer/rules_test.go +++ b/internal/transfer/rules_test.go @@ -86,6 +86,7 @@ func TestSetGlobalTimeLimit(t *testing.T) { r.NoError(t, rules.setRule(mailboxB, []Mailbox{}, 0, 0)) rules.setGlobalTimeLimit(30, 40) + rules.propagateGlobalTime() r.Equal(t, map[string]*Rule{ 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{ 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) } @@ -208,3 +208,40 @@ func generateTimeRule(from, to int64) Rule { 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) +} diff --git a/internal/transfer/transfer.go b/internal/transfer/transfer.go index 2fda8f7e..566dfcb1 100644 --- a/internal/transfer/transfer.go +++ b/internal/transfer/transfer.go @@ -31,12 +31,14 @@ var log = logrus.WithField("pkg", "transfer") //nolint[gochecknoglobals] // Transfer is facade on top of import rules, progress manager and source // and target providers. This is the main object which should be used. type Transfer struct { - panicHandler PanicHandler - id string - dir string - rules transferRules - source SourceProvider - target TargetProvider + panicHandler PanicHandler + id string + dir string + rules transferRules + source SourceProvider + target TargetProvider + sourceMboxCache []Mailbox + targetMboxCache []Mailbox } // 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. -func (t *Transfer) SourceMailboxes() ([]Mailbox, error) { - return t.source.Mailboxes(false, true) +func (t *Transfer) SourceMailboxes() (m []Mailbox, err error) { + if t.sourceMboxCache == nil { + t.sourceMboxCache, err = t.source.Mailboxes(false, true) + } + return t.sourceMboxCache, err } // TargetMailboxes returns mailboxes available at target side. -func (t *Transfer) TargetMailboxes() ([]Mailbox, error) { - return t.target.Mailboxes(true, false) +func (t *Transfer) TargetMailboxes() (m []Mailbox, err error) { + if t.targetMboxCache == nil { + t.targetMboxCache, err = t.target.Mailboxes(true, false) + } + return t.targetMboxCache, err } // CreateTargetMailbox creates mailbox in target provider. func (t *Transfer) CreateTargetMailbox(mailbox Mailbox) (Mailbox, error) { + t.targetMboxCache = nil + return t.target.CreateMailbox(mailbox) } // 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. func (t *Transfer) ChangeTarget(target TargetProvider) { + t.targetMboxCache = nil + t.target = target } @@ -151,6 +163,7 @@ func (t *Transfer) ChangeTarget(target TargetProvider) { func (t *Transfer) Start() *Progress { log.Debug("Transfer started") t.rules.save() + t.rules.propagateGlobalTime() log := log.WithField("id", t.id) reportFile := newFileReport(t.dir, t.id) diff --git a/internal/users/mocks/mocks.go b/internal/users/mocks/mocks.go index dd087706..524d3273 100644 --- a/internal/users/mocks/mocks.go +++ b/internal/users/mocks/mocks.go @@ -5,12 +5,11 @@ package mocks import ( - reflect "reflect" - store "github.com/ProtonMail/proton-bridge/internal/store" credentials "github.com/ProtonMail/proton-bridge/internal/users/credentials" pmapi "github.com/ProtonMail/proton-bridge/pkg/pmapi" gomock "github.com/golang/mock/gomock" + reflect "reflect" ) // MockConfiger is a mock of Configer interface diff --git a/pkg/pmapi/bugs.go b/pkg/pmapi/bugs.go index b50b121c..ea792d6d 100644 --- a/pkg/pmapi/bugs.go +++ b/pkg/pmapi/bugs.go @@ -173,26 +173,6 @@ func (c *client) Report(rep ReportReq) (err error) { 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. func (c *client) ReportCrash(stacktrace string) (err error) { crashReq := ReportReq{ diff --git a/pkg/pmapi/bugs_test.go b/pkg/pmapi/bugs_test.go index c3336c20..13792000 100644 --- a/pkg/pmapi/bugs_test.go +++ b/pkg/pmapi/bugs_test.go @@ -27,19 +27,7 @@ import ( "testing" ) -var testBugsReportReq = 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{ +var testBugReportReq = ReportReq{ OS: "Mac OSX", OSVersion: "10.11.6", 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] -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) { s, c := newTestServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { Ok(t, checkMethodAndPath(r, "POST", "/reports/bug")) @@ -100,15 +63,15 @@ func TestClient_BugReportWithAttachment(t *testing.T) { Ok(t, r.ParseMultipartForm(10*1024)) for field, expected := range map[string]string{ - "OS": testBugsReportReq.OS, - "OSVersion": testBugsReportReq.OSVersion, - "Client": testBugsReportReq.Client, - "ClientVersion": testBugsReportReq.ClientVersion, - "ClientType": fmt.Sprintf("%d", testBugsReportReq.ClientType), - "Title": testBugsReportReq.Title, - "Description": testBugsReportReq.Description, - "Username": testBugsReportReq.Username, - "Email": testBugsReportReq.Email, + "OS": testBugReportReq.OS, + "OSVersion": testBugReportReq.OSVersion, + "Client": testBugReportReq.Client, + "ClientVersion": testBugReportReq.ClientVersion, + "ClientType": fmt.Sprintf("%d", testBugReportReq.ClientType), + "Title": testBugReportReq.Title, + "Description": testBugReportReq.Description, + "Username": testBugReportReq.Username, + "Email": testBugReportReq.Email, } { if 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.accessToken = testAccessToken - rep := testBugsReportReq + rep := testBugReportReq rep.AddAttachment("log", "last.log", strings.NewReader(testAttachmentJSON)) 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) { 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, testBugsReportReqWithEmailClient, bugsReportReq) + Equals(t, testBugReportReq, bugsReportReq) fmt.Fprint(w, testBugsBody) })) @@ -150,15 +113,17 @@ func TestClient_BugReportWithEmailClient(t *testing.T) { c.uid = testUID c.accessToken = testAccessToken - Ok(t, c.ReportBugWithEmailClient( - testBugsReportReqWithEmailClient.OS, - testBugsReportReqWithEmailClient.OSVersion, - testBugsReportReqWithEmailClient.Title, - testBugsReportReqWithEmailClient.Description, - testBugsReportReqWithEmailClient.Username, - testBugsReportReqWithEmailClient.Email, - testBugsReportReqWithEmailClient.Browser, - )) + r := ReportReq{ + OS: testBugReportReq.OS, + OSVersion: testBugReportReq.OSVersion, + Browser: testBugReportReq.Browser, + Title: testBugReportReq.Title, + Description: testBugReportReq.Description, + Username: testBugReportReq.Username, + Email: testBugReportReq.Email, + } + + Ok(t, c.Report(r)) } func TestClient_BugsCrash(t *testing.T) { diff --git a/pkg/pmapi/client_types.go b/pkg/pmapi/client_types.go index 538d82cb..168c9936 100644 --- a/pkg/pmapi/client_types.go +++ b/pkg/pmapi/client_types.go @@ -67,7 +67,7 @@ type Client interface { DeleteLabel(labelID 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 GetMailSettings() (MailSettings, error) diff --git a/pkg/pmapi/mocks/mocks.go b/pkg/pmapi/mocks/mocks.go index 11399d72..6c30a7cc 100644 --- a/pkg/pmapi/mocks/mocks.go +++ b/pkg/pmapi/mocks/mocks.go @@ -5,12 +5,11 @@ package mocks import ( - io "io" - reflect "reflect" - crypto "github.com/ProtonMail/gopenpgp/v2/crypto" pmapi "github.com/ProtonMail/proton-bridge/pkg/pmapi" gomock "github.com/golang/mock/gomock" + io "io" + reflect "reflect" ) // 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) } -// ReportBugWithEmailClient mocks base method -func (m *MockClient) ReportBugWithEmailClient(arg0, arg1, arg2, arg3, arg4, arg5, arg6 string) error { +// Report mocks base method +func (m *MockClient) Report(arg0 pmapi.ReportReq) error { 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) return ret0 } -// ReportBugWithEmailClient indicates an expected call of ReportBugWithEmailClient -func (mr *MockClientMockRecorder) ReportBugWithEmailClient(arg0, arg1, arg2, arg3, arg4, arg5, arg6 interface{}) *gomock.Call { +// Report indicates an expected call of Report +func (mr *MockClientMockRecorder) Report(arg0 interface{}) *gomock.Call { 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 diff --git a/test/fakeapi/reports.go b/test/fakeapi/reports.go index 7b8175eb..f0604eb4 100644 --- a/test/fakeapi/reports.go +++ b/test/fakeapi/reports.go @@ -23,16 +23,8 @@ import ( "github.com/ProtonMail/proton-bridge/pkg/pmapi" ) -func (api *FakePMAPI) ReportBugWithEmailClient(os, osVersion, title, description, username, email, emailClient string) error { - return api.checkInternetAndRecordCall(POST, "/reports/bug", &pmapi.ReportReq{ - OS: os, - OSVersion: osVersion, - Title: title, - Description: description, - Username: username, - Email: email, - Browser: emailClient, - }) +func (api *FakePMAPI) Report(report pmapi.ReportReq) error { + return api.checkInternetAndRecordCall(POST, "/reports/bug", report) } func (api *FakePMAPI) SendSimpleMetric(category, action, label string) error {