diff options
43 files changed, 636 insertions, 325 deletions
diff --git a/Vagrantfile b/Vagrantfile index c3f3b08a..6f1f1d1f 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -27,14 +27,11 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| # see https://leap.se/en/docs/platform/details/development#Verify.vagrantbox.download # for details - config.vm.box = "leap-wheezy" + config.vm.box = "boxcutter/debian80" config.vbguest.auto_update = false config.vm.define "source", primary: true do |source| - source.vm.provider :virtualbox do |v, override| - override.vm.box_url = "https://downloads.leap.se/platform/vagrant/virtualbox/leap-wheezy.box" - end source.vm.provider "libvirt" do |v, override| override.vm.box_url = "https://downloads.leap.se/platform/vagrant/libvirt/leap-wheezy.box" end @@ -49,10 +46,7 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| # until https://github.com/pixelated-project/pixelated-user-agent/issues/226 is not fixed, # we depend on a debian testing box - config.vm.box = "leap-wheezy" - deb.vm.provider "virtualbox" do |v, override| - override.vm.box_url = "https://downloads.leap.se/platform/vagrant/virtualbox/leap-wheezy.box" - end + config.vm.box = "boxcutter/debian80" deb.vm.provider "libvirt" do |v, override| override.vm.box_url = "https://downloads.leap.se/platform/vagrant/libvirt/leap-wheezy.box" end @@ -63,15 +57,6 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| end end - config.vm.define "hackday", autostart: false do |hackday| - config.vm.box = "hackday-pixelated-user-agent" - hackday.vm.provision "puppet" do |puppet| - puppet.manifests_path = "provisioning/manifests" - puppet.module_path = "provisioning/modules" - puppet.manifest_file = "hackday.pp" - end - end - if /mswin|mingw/ =~ RUBY_PLATFORM config.vm.synced_folder ".", "/vagrant", type: "rsync", rsync__exclude: ".git/" end diff --git a/provisioning/build-hackday-box.sh b/provisioning/build-hackday-box.sh deleted file mode 100755 index 858fc40b..00000000 --- a/provisioning/build-hackday-box.sh +++ /dev/null @@ -1,52 +0,0 @@ -#!/bin/bash -# -# Copyright (c) 2014 ThoughtWorks, Inc. -# -# Pixelated is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Pixelated 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 Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with Pixelated. If not, see <http://www.gnu.org/licenses/>. - -PROVISIONING_PATH=$(dirname $0) -USER_AGENT_DIR="$PROVISIONING_PATH/.." - -PACKAGE_FILE="hackday-pixelated-user-agent.box" -BOX_NAME="hackaday-pixelated-user-agent" - -pushd $USER_AGENT_DIR - -# clear old boxes -vagrant destroy -f source -vagrant destroy -f hackday -vagrant box remove $BOX_NAME -rm "$USER_AGENT_DIR/$PACKAGE_FILE" - -# build from scratch -vagrant up source && \ -vagrant package --output="$USER_AGENT_DIR/$PACKAGE_FILE" && \ -vagrant box add $BOX_NAME $PACKAGE_FILE - -SUCCESS=$? - -popd - -if [ $SUCCESS -eq 0 ] ; then - echo - echo "Created hackday box $PACKAGE_FILE" - echo "If you want to test it, run" - echo - echo "vagrant up hackday" -else - echo "Error while building hackday box" 1>&2 -fi - -exit $SUCCESS - diff --git a/provisioning/manifests/hackday.pp b/provisioning/manifests/hackday.pp deleted file mode 100644 index 87a72728..00000000 --- a/provisioning/manifests/hackday.pp +++ /dev/null @@ -1 +0,0 @@ -class { '::pixelated::hackday_dev_env': } diff --git a/provisioning/modules/pixelated/files/activate_custom_node_modules.sh b/provisioning/modules/pixelated/files/activate_custom_node_modules.sh index 2e15a42b..f46b67e1 100755 --- a/provisioning/modules/pixelated/files/activate_custom_node_modules.sh +++ b/provisioning/modules/pixelated/files/activate_custom_node_modules.sh @@ -28,13 +28,10 @@ fi source /home/vagrant/user-agent-venv/bin/activate cd /vagrant/service -echo "running python setup.py develop" -python setup.py develop >/dev/null -echo "running bower install" -cd /vagrant/web-ui/ -node_modules/.bin/bower install >/dev/null +echo "running python setup" +./go setuppy > /dev/null +echo "running js setup" +./go setupjs > /dev/null -echo "running go build" -LC_ALL=en_US.UTF-8 ./go build cd /vagrant diff --git a/provisioning/modules/pixelated/manifests/hackday_dev_env.pp b/provisioning/modules/pixelated/manifests/hackday_dev_env.pp deleted file mode 100644 index 78de0bba..00000000 --- a/provisioning/modules/pixelated/manifests/hackday_dev_env.pp +++ /dev/null @@ -1,14 +0,0 @@ -# -class pixelated::hackday_dev_env { - file { '/home/vagrant/.activate_custom_node_modules.sh': - owner => 'vagrant', - mode => '0600', - source => 'puppet:///modules/pixelated/activate_custom_node_modules.sh', - } - exec { 'add_custom_node_modules_to_bashrc': - command => "/bin/bash -c 'echo \"source /home/vagrant/.activate_custom_node_modules.sh\" >> /home/vagrant/.bashrc'", - unless => "/bin/grep \"source /home/vagrant/.activate_custom_node_modules.sh\" /home/vagrant/.bashrc", - user => 'vagrant', - require => File['/home/vagrant/.activate_custom_node_modules.sh'] - } -} diff --git a/provisioning/modules/pixelated/manifests/source.pp b/provisioning/modules/pixelated/manifests/source.pp index d4e9754a..3d7ca037 100644 --- a/provisioning/modules/pixelated/manifests/source.pp +++ b/provisioning/modules/pixelated/manifests/source.pp @@ -7,6 +7,7 @@ class pixelated::source { 'python-dev', 'python-virtualenv', 'libffi-dev', + 'libssl-dev', 'g++', 'ruby-dev', 'libsqlite3-dev', diff --git a/service/pixelated/adapter/services/mail_service.py b/service/pixelated/adapter/services/mail_service.py index 03889f82..656bb4bc 100644 --- a/service/pixelated/adapter/services/mail_service.py +++ b/service/pixelated/adapter/services/mail_service.py @@ -97,6 +97,11 @@ class MailService(object): trashed_mail = self.mailboxes.move_to_trash(mail_id) self.search_engine.index_mail(trashed_mail) + def recover_mail(self, mail_id): + mail = self.mail(mail_id) + recovered_mail = self.mailboxes.move_to_inbox(mail_id) + self.search_engine.index_mail(recovered_mail) + def delete_permanent(self, mail_id): mail = self.mail(mail_id) self.search_engine.remove_from_index(mail_id) diff --git a/service/pixelated/adapter/services/mailboxes.py b/service/pixelated/adapter/services/mailboxes.py index a7a3a591..e9fe6ce5 100644 --- a/service/pixelated/adapter/services/mailboxes.py +++ b/service/pixelated/adapter/services/mailboxes.py @@ -49,8 +49,14 @@ class Mailboxes(object): return [self._create_or_get(leap_mailbox_name) for leap_mailbox_name in self.account.mailboxes] def move_to_trash(self, mail_id): + return self._move_to(mail_id, self.trash()) + + def move_to_inbox(self, mail_id): + return self._move_to(mail_id, self.inbox()) + + def _move_to(self, mail_id, mailbox): mail = self.querier.mail(mail_id) - mail.set_mailbox(self.trash().mailbox_name) + mail.set_mailbox(mailbox.mailbox_name) mail.save() return mail diff --git a/service/pixelated/certificates/dev.pixelated-project.org.ca.crt b/service/pixelated/certificates/dev.pixelated-project.org.ca.crt index 8f2219a7..22ccb662 100644 --- a/service/pixelated/certificates/dev.pixelated-project.org.ca.crt +++ b/service/pixelated/certificates/dev.pixelated-project.org.ca.crt @@ -1,33 +1,33 @@ -----BEGIN CERTIFICATE----- -MIIFozCCA4ugAwIBAgIBATANBgkqhkiG9w0BAQ0FADBkMRYwFAYDVQQKDA1kZXYu -cGl4ZWxhdGVkMSowKAYDVQQLDCFodHRwczovL2Rldi5waXhlbGF0ZWQtcHJvamVj -dC5vcmcxHjAcBgNVBAMMFWRldi5waXhlbGF0ZWQgUm9vdCBDQTAeFw0xNTAxMjAw -MDAwMDBaFw0yNTAxMjAwMDAwMDBaMGQxFjAUBgNVBAoMDWRldi5waXhlbGF0ZWQx -KjAoBgNVBAsMIWh0dHBzOi8vZGV2LnBpeGVsYXRlZC1wcm9qZWN0Lm9yZzEeMBwG -A1UEAwwVZGV2LnBpeGVsYXRlZCBSb290IENBMIICIjANBgkqhkiG9w0BAQEFAAOC -Ag8AMIICCgKCAgEAnhlrDaixEGkzugWvwp1pPq4UUq8lWSLe173IHRvuElckYmEy -qTG/UpHx7ZAG/6OLNoCavOO4SEZvYGfGKRpqWGAm8O1acFc7S5GSOJcQQ5UO6bj8 -/32x3/wQAm85T1yUYAgJG0EWExheKMkQVXsRrXuYPsJJ6t3R33nQMUouJCY2toUb -Nu2NAhCZSNxL/77akwznYwtOS2aupznH4nzsxI70y9WlUcSIqlISJW9S65CMnIVq -xgtAxDZ+XOoYrS7Ihh87Dge8eWaUaTOTf+TgyLwxO0v3r64/tjnAV+jDzQsQKnhw -HNNOi2X7E85DzdicB+HOz8mP7G4LiLeCUGGiKQ4CcG7LCMBVcaUWpig7TBQT1PgL -JGwqSw98TD5+81mgU4sjC85HRngGRtoeWkjkLE5jXntFd6S7sUUctJ1CIMJN+W2P -D3g0TQp2mX8OY9zhyX87Lbc/YPY3+OHHtlKm/Gey+LplfWG65i7sPhzWYO/8gmML -nZeuwK4nm21aT3ugonnKiMetmFNDlMH2JxLg+pmw2BnwJe8jNYD6i8wDHTb6mYxU -iMJyU7KPIWXOznNTVwn3c2CBijzfmsbtuyswk7RePpXhHlLzgsw0tb0OlnO3EQKQ -7O4FLZO5erpa5ANb9j4/9MYJlfXQHPvhI2dsr3yv893/7fEK0AeuguqLQMMCAwEA -AaNgMF4wHQYDVR0OBBYEFGuiVuLgAlxTj8o7g+Nl8MhODLb1MA4GA1UdDwEB/wQE -AwICBDAMBgNVHRMEBTADAQH/MB8GA1UdIwQYMBaAFGuiVuLgAlxTj8o7g+Nl8MhO -DLb1MA0GCSqGSIb3DQEBDQUAA4ICAQBXd2dgIe4DCKS9BECJoJAanf73cmzgAcUc -dfW2MhppAe3CscL8N4MJSxgJz+HUjmoNNUHpNLxRuigkarYqJxhLh1YvoyfvLJ/l -8+FvHsb3w3S4T1fDhmdnMp04niFUgDvDFgjaeVVg9WVelmAIKsGZZhNMmqqScS+T -uJuSIT/TZmLEcT2QqPVPZEFfT6IsZRgdgKpRw237bhP8TayzSvpsFbwUuVNXbOEc -IPj/rGdzAJWTWC+EVdVBKw3bcNrYoXEkWEtlchsw8uhHuAYOCaf2t5WjZVOwKmQ5 -o9vFP2yWNg1+OxKnHkJupnUbLVCx1qoRJ5M5f7zNOoyr9G6VEb6v+TlFVfSWqXQs -gj/Iy3q4Y9rgdA/4u6V3LdjJgZSFfSHKePeHZuGqKTnY9MxLwARkRfte3/TNFJLe -GdIOaKe236Rf28PYfinBqujhqhlJKWPVJSF1idR0F0jcWXRTPb3bR+0YP0tJXLtS -ICzsgv92LOkUjet1vcJrrLoJMnA27MqZNzClzP+Lrh1JtxA8w8wXTNal6oQbw8sZ -iO1+sca3wifjr4Fh/63ka52dTLDQB++CKkR7vffq8tBUoILxJ0vmiFcVdWWrjJnN -qdAVWM7CTclo/kscqoYDobIusaw/pouyyIdLYHPROlGIObxcr6jBlOvPoPn5gHzi -UBl/Dd7yAQ== +MIIFqzCCA5OgAwIBAgIBATANBgkqhkiG9w0BAQ0FADBoMRowGAYDVQQKDBFQaXhl +bGF0ZWQgUHJvamVjdDEmMCQGA1UECwwdaHR0cHM6Ly9waXhlbGF0ZWQtcHJvamVj +dC5vcmcxIjAgBgNVBAMMGVBpeGVsYXRlZCBQcm9qZWN0IFJvb3QgQ0EwHhcNMTUw +NDAxMDAwMDAwWhcNMjUwNDAxMDAwMDAwWjBoMRowGAYDVQQKDBFQaXhlbGF0ZWQg +UHJvamVjdDEmMCQGA1UECwwdaHR0cHM6Ly9waXhlbGF0ZWQtcHJvamVjdC5vcmcx +IjAgBgNVBAMMGVBpeGVsYXRlZCBQcm9qZWN0IFJvb3QgQ0EwggIiMA0GCSqGSIb3 +DQEBAQUAA4ICDwAwggIKAoICAQDIKlYvQciyw1gq7NgyXxo4OPnlB6cyjLb0vr6V +cnY00F3pwa3cuOASYtZ5p4IMGod1fZoBUlGFbxay0NO/FyOlPVc3FaF1cL5oYurl +a/koAjAihGMGEjsc2gcdODJOHoidgZG4a4WUCVfXP54QFkG6tXve1uWnZAt7W6Z8 +M4DobBqh4HE8aAn1vSIdOltKvuH6Jz9+O2SZdXczEKuHM/Zv8o5qI4+3CKQ/enLS +WUfqZx217zJw4xax4kTsZxaU56XkdXGjAoqKmP3am8sAfSPyt5J6yPvKzUM2hbyQ +v/ZR3AzKX6E5w/kPWfrjr+ABPtMEbcUEQYTR5Ic/46umzseEEUTHaeBhn5XZfan/ +K3+iOXHgukuNX5BF28D10RKrIWutOr6pMWXtFNLsmSvbii6SYg4sr80RhPsox3Lf +QuO36aH/jrkmyeEgTVVyniPUYLeg/IU+lQzOQpoHR7FiS2/39QP9HNbO7kQzi2Fh +aFQW35sRRZuz+Gt38y0DObKOwEZaR2NM2kIHiR6cYWP9fYCekOfJi7rp0hfxcO+J +o+kVoZM53SS0QJGzHnKYo+hHAy01Hj9Kh7N269rruZSjDXsWF/eZp5oYQeItBolq +J3Va8VyQk12Pd+4vcIERWNx8JCBDnYY1+sRBsrsH1mSMRQxOkbOhlx0y64dRNQRa +GV4xxQIDAQABo2AwXjAdBgNVHQ4EFgQUdXb2FIbWofTZ00R/OoQQu2K1mscwDgYD +VR0PAQH/BAQDAgIEMAwGA1UdEwQFMAMBAf8wHwYDVR0jBBgwFoAUdXb2FIbWofTZ +00R/OoQQu2K1mscwDQYJKoZIhvcNAQENBQADggIBAKR6XuNIZGWccQtVxuOKRuNY +5KE5N2Gqi4erN1XkJvOD60an2F6aYjFUN1ZZfq31Oi57GHZ8lwPKZZuAn/pn2vQG +1YpGFXndnoFkKDGb3XVRMHMtFIcV/2u2rX8zfRmZGpHvgSZo+9g4PN2jCipKkuki +pVnLjO7SZi4/AAuzlFPBUHE441xJI6Hl3lLKFezLD4akxrBpFbW+rBOSFupxyBcD +hedH0MyHY6HUQLL4Mm2tobouFStUcBZT0wGEeh/tlaJKAhk/kzryle4IZcqpv01a +6BfsyO9kRABc4b9w26tvd93utgXkpmwni+I31QSFVH/s8oU1pYvrpHO2MQf5rE7X +gyVvUaXSNETE2Nj1/Bp0Bbk/A2IyzOyoIALU+5LBOzgnW8K+Ojg9FQSRuIFZF+fw +VRruRFMziKtFBYaTclWX9mf+tn3MFSe8FcIcpMa0yXu9pDtQxZeeFG/TbseE0VjS +20HHEPnA6YIKbi7vg7LZP+dZoPBc30tJeG/fDCo6EDmFhddtDJpmnFPnr3OVf1AC +B90gGoXQ2RJsP8ZLkYlMn5JYKQbXLQSf806eMYJr3Mo6IBOR+o9dxAyBtEBgAUG6 +jgfKRzGizhHA3LDbxUlJt1k16SU58QBY3p8FHwjbjon7GDEOhSNOtuHAHUc83sHH +SC1pIYpuR4H4lTW5fuyF -----END CERTIFICATE----- diff --git a/service/pixelated/certificates/dev.wazokazi.is.ca.crt b/service/pixelated/certificates/dev.wazokazi.is.ca.crt deleted file mode 100644 index 0fda59da..00000000 --- a/service/pixelated/certificates/dev.wazokazi.is.ca.crt +++ /dev/null @@ -1,33 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIFnzCCA4egAwIBAgIBATANBgkqhkiG9w0BAQ0FADBiMRowGAYDVQQKDBFQaXhl -bGF0ZWRfU3RhZ2luZzEgMB4GA1UECwwXaHR0cHM6Ly9kZXYud2F6b2themkuaXMx -IjAgBgNVBAMMGVBpeGVsYXRlZF9TdGFnaW5nIFJvb3QgQ0EwHhcNMTQxMDIyMDAw -MDAwWhcNMjQxMDIyMDAwMDAwWjBiMRowGAYDVQQKDBFQaXhlbGF0ZWRfU3RhZ2lu -ZzEgMB4GA1UECwwXaHR0cHM6Ly9kZXYud2F6b2themkuaXMxIjAgBgNVBAMMGVBp -eGVsYXRlZF9TdGFnaW5nIFJvb3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw -ggIKAoICAQC541dNmfzVEzLZ0LdV5ASG1STjB9NgoPpCily1bjjWhnglhNHEAfvP -kj2uEGXJip/ndJB/RV4QSqqxgoq8aHfhIspYLZ0IG5pIFnjdIMB5brqpkkf5N2wY -tSFy2gkspx/hk6xnEGyxDQntraY5rqFRLjCAE+cwk9kbqlm8iRrPApMDODizsNKF -8sTW/gC7U/LAkTvzZdnCUg4i72SmBUnZmudXiTiRTnvf2gmdCrPY2tpEguyWh+NB -qeig6wju0NGZl7+y7MnshteEcJ9IMiao88SRvL8B4pK8gA60ix+06QIPmuh8wQac -+rCB1+3sjeOSrBVPXzQkgLWqu/rT551AyKAEQsCm6SKPs/PhcLaZPLfA/onuZMdO -NL8l1DeRyyAXQYZgRMM4P0PUEmVfhV/xmkR62bhuQ8hvXV4lmhRSPqB5Whb0PNJ4 -SuGJTL6hu1b+zNBFqGl4EX/itYLS6anqmR8cIgallfkTOUgunOOUwzoj8hVDrbJF -6cWld0PmtFgUh5vaIpKZ+ygOLEosVqBVZzzN4ObEh+MfIxpzYZJ2ylB2r8DVzDxq -55G0f7LZHL31WcJwVF4lfmz1rx8us83DCWXJcVpxgJpoo/+Z+hyiUFf/XCMNYXn9 -vgZ1+bO7k/zUXGf+rnCgTxQkGTPZ95CCZ1nIMl7RgIJrSirBnlV5hQIDAQABo2Aw -XjAdBgNVHQ4EFgQUCSvehLZHfFbYbEhsNfWin8KNf6MwDgYDVR0PAQH/BAQDAgIE -MAwGA1UdEwQFMAMBAf8wHwYDVR0jBBgwFoAUCSvehLZHfFbYbEhsNfWin8KNf6Mw -DQYJKoZIhvcNAQENBQADggIBAJRD90EJ+zUOFA3Ky7NdNao3JXNLNIVsXTO5FAAM -ZPMeoSKIdKES5kc0vKlswP6RT7tBY5EH6sdjFMGEPK9glgxI1An4ozmgBUndOrql -mab9Ep+9ckfof8Yg6foETkpg6YhdWLbIHs98ajiTY6bozW1jJaRceVf57JLycqcO -FuEJ1fHRMqy2CY96g/Z4bMqkkYkVJKkyv7NIHg//DvRvboB4h+hO0MRB6rPAFC63 -6wxpGYhH4LHWRGS54IKwQxsOxpX38VM67Zj0XSEIx77b2E45unRXoZRwCBEWJlcv -5QWPCDxJgi0hDH1Fer+vQlcjB6/Kzg8AiVD15qGSNMLcjzn2Z334m0ErcYFI/c3A -VAMzCBR40mJx6r2zHUYbo+rR23huRWuFgeIdeeOJnJzpb+GwuGHlCdPl7mb7G8lp -R/U6J+GSUWs55quasE3VimDtX6eqh94HakGuq4n596GsIhYu6YHVSAf/4uTenpDk -ux2qIoSjZlarhoUoayRti4/HcP2a0w604jmyFLPNTi59J/xz/wwc7z8xwAT9DZ6F -4yxCC+u/eePbQY0USTRlrNIF9hi9DeJOxr/lTDZ8iXMxN820QiixH0XRQPrGEMcp -oY0FiYBJHweeaSEqz3AYB6updct7Qi0yS4g1uE8M9m+6SNzmSVpQWmZPr7iM5Cua -e0TX ------END CERTIFICATE----- diff --git a/service/pixelated/certificates/example.wazokazi.is.ca.crt b/service/pixelated/certificates/example.wazokazi.is.ca.crt deleted file mode 100644 index 515a12c4..00000000 --- a/service/pixelated/certificates/example.wazokazi.is.ca.crt +++ /dev/null @@ -1,32 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIFkzCCA3ugAwIBAgIBATANBgkqhkiG9w0BAQ0FADBcMRUwEwYDVQQKDAxMRUFQ -X0V4YW1wbGUxJDAiBgNVBAsMG2h0dHBzOi8vZXhhbXBsZS53YXpva2F6aS5pczEd -MBsGA1UEAwwUTEVBUF9FeGFtcGxlIFJvb3QgQ0EwHhcNMTQxMDMxMDAwMDAwWhcN -MjQxMDMxMDAwMDAwWjBcMRUwEwYDVQQKDAxMRUFQX0V4YW1wbGUxJDAiBgNVBAsM -G2h0dHBzOi8vZXhhbXBsZS53YXpva2F6aS5pczEdMBsGA1UEAwwUTEVBUF9FeGFt -cGxlIFJvb3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCsbVir -UZrbwQ6MLPY8i9KrYpCnFnYPTCZez6kfK0S9+5IVNltZlbedYPN/Mjr3ngdo4tSl -fZK6bk3UX1JhL6GkMMXD8Nk/yckGwaV9IbLdNI5Ihu3ot/SYIV8Bst19+hWUFbtN -Qgwlkyla2ZAqH6pIL7Hc5vED0ouVN5CBErYxsm/+mj5p69BtCZoR0QaKWURF2rTx -COb+xrhMRmqg/JPcsEtK/OqgIplkbjsHfxsknutFE6l/FbFvxc0sSmaMyd6fCVM4 -y9ivZXXh376klYPMuO0fXkGd79gSbWGldcX3MVbFNfwp2SxPXc/zP8/ntg0kqN4R -hASAvsT1bMG9/elXmU5XGouaJ0x5utK9/49qzfKiGyu/hW/ihQsGMx6I05EGxnsL -NFfUooySUqy6bB9BliHgyBaHHToNMJH2LHiyYFANWT6rPWAMH7VKEohNMRxu5IRK -U0E5OwSXu4yI5JU4bviuGSe93V5OOmO6GeIxVupij5i/1XtJ1Z5skuBcq7b7C2/6 -zNKoEu/wE9lbtjPNl7JwIYjaWRLpI+bMDPWjBSDSLCkHXPuxOCWFggGMEPOI4d3J -5hp22UGQxqq7z86ZTxPfx9Ce9oUbl1NOkuNznc0XVjFcglHqz4/75ogNP0dmra+P -GmPoAetOlKOIXxbBOQeSJr4WAgQK3GECb3uvQQIDAQABo2AwXjAdBgNVHQ4EFgQU -ti27+d3zDOJiJeQyx6il6YzZkKowDgYDVR0PAQH/BAQDAgIEMAwGA1UdEwQFMAMB -Af8wHwYDVR0jBBgwFoAUti27+d3zDOJiJeQyx6il6YzZkKowDQYJKoZIhvcNAQEN -BQADggIBABMGDact+sxOf8Yyt+mzckf1uANEw3TAM81ztxuiwYA6/quNvBISdjS6 -PyyZ8S3U+sjJA5FQW1SDd4IbuRFK2Y2Hmeto1uIFAxjQj/jDdW26Wj/FkBmWnCQd -L2/CSU1MXitLuxHE8SaUWrw+W7I/Ga5t+9VRxDI0XNF+OAEPwvp6JequZKIW+OcT -4fU7z4xu2QYSPRVzliJ9fVxb4jllG6kLNLnqkVTQNiOJ8sVbALh1tJSSFGd6ZGNB -8ePrWkPOyYMD6J+zy6uqDTUipaXuVIgA7mPTyu0XKqWO3tZXyakITlxnHgMYqpfE -XNkGArsMNXV3ktp+HCpGVO46+9nMzSPIBxMfpgpeq3ommBov3pANT0S5IrBiyo/T -VjUkWz3D1ymRyK0SWrHZSOq9rUS+tMjKxwmhFTBn1cR7PItWB9pNiVt/Oz0z1ecM -Ij4pMb/jPvV2H6crMjMVSPzbazTE/S6AEl0beZ/o44Frz1aXvuMwgtG6fUFZma4X -6exBWOmqF96qkPH2I0olHIbOTXHz+uBKJnFaSg09RT+AZfFutJ2DkVK7NJgOdyjf -QQcDfkfeRz/A6mI5wjm/UwZRWWytBuUDjU4ZaqxH9AlmzXsJHoYYr0YgTEYlm16o -eGTyR4QlaLlwTg1CvZgHTKxpI/LKn1T4aMHHvd92W2fT7pq4PWlH ------END CERTIFICATE----- diff --git a/service/pixelated/maintenance.py b/service/pixelated/maintenance.py index 8c20e097..b4e616a7 100644 --- a/service/pixelated/maintenance.py +++ b/service/pixelated/maintenance.py @@ -1,4 +1,4 @@ -# + # Copyright (c) 2014 ThoughtWorks, Inc. # # Pixelated is free software: you can redistribute it and/or modify @@ -20,9 +20,10 @@ import sys import json import argparse import email +import re from os.path import join -from mailbox import mboxMessage +from mailbox import mboxMessage, Maildir from pixelated.config.app import App from pixelated.config import app_factory from pixelated.config.args import parser_add_default_arguments @@ -39,6 +40,7 @@ from twisted.internet.threads import deferToThread from leap.mail.imap.memorystore import MemoryStore from leap.mail.imap.soledadstore import SoledadStore +from leap.mail.imap.fields import WithMsgFields from leap.common.events import register, unregister, events_pb2 as proto # monkey patching some specifics @@ -50,51 +52,26 @@ import pixelated.support.ext_keymanager_fetch_key import pixelated.support.ext_requests_urllib3 -def delete_all_mails(args): - leap_session, soledad = args - generation, docs = soledad.get_all_docs() - - for doc in docs: - if doc.content.get('type', None) in ['head', 'cnt', 'flags']: - soledad.delete_doc(doc) - - return args - - -@defer.inlineCallbacks -def load_mails(args, mail_paths): - leap_session, soledad = args - account = leap_session.account - - for path in mail_paths: - print 'Loading mails from %s' % path - for root, dirs, files in os.walk(path): - mbx = account.getMailbox('INBOX') - for f in files: - with open(join(root, f), 'r') as fp: - m = email.message_from_file(fp) - flags = ("\\RECENT",) - r = yield mbx.addMessage(m.as_string(), flags=flags, notify_on_disk=False) - print 'Added message %s' % m.get('subject') - print m.as_string() - - defer.returnValue(args) - return - +def initialize(): + args = parse_args() + app = App() -def dump_soledad(args): - leap_session, soledad = args + init_logging(args) + init_leap_cert(args) - generation, docs = soledad.get_all_docs() + if args.dispatcher or args.dispatcher_stdin: + config_dispatcher(app, args) + else: + config_user_agent(app, args) - for doc in docs: - print doc - print '\n' + init_events_server() + execute_command = create_execute_command(args, app) - return args + reactor.callWhenRunning(execute_command) + reactor.run() -def initialize(): +def parse_args(): parser = argparse.ArgumentParser(description='pixelated maintenance') parser_add_default_arguments(parser) subparsers = parser.add_subparsers(help='commands', dest='command') @@ -105,19 +82,10 @@ def initialize(): subparsers.add_parser('dump-soledad', help='dump the soledad database') subparsers.add_parser('sync', help='sync the soledad database') - args = parser.parse_args() - app = App() - - init_logging(args) - init_leap_cert(args) - - if args.dispatcher or args.dispatcher_stdin: - config_dispatcher(app, args) - else: - config_user_agent(app, args) + return parser.parse_args() - init_events_server() +def create_execute_command(args, app): def execute_command(): def init_soledad(): @@ -135,26 +103,109 @@ def initialize(): return args - d = deferToThread(init_soledad) - d.addCallback(get_soledad_handle) - d.addCallback(soledad_sync) - if args.command == 'reset': - d.addCallback(delete_all_mails) - elif args.command == 'load-mails': - d.addCallback(load_mails, args.file) - elif args.command == 'dump-soledad': - d.addCallback(dump_soledad) - elif args.command == 'sync': - # nothing to do here, sync is already part of the chain - pass + tearDown = defer.Deferred() + + prepare = deferToThread(init_soledad) + prepare.addCallback(get_soledad_handle) + prepare.addCallback(soledad_sync) + add_command_callback(args, prepare, tearDown) + tearDown.addCallback(soledad_sync) + tearDown.addCallback(shutdown) + tearDown.addErrback(shutdown_on_error) + + return execute_command + + +def add_command_callback(args, prepareDeferred, finalizeDeferred): + if args.command == 'reset': + prepareDeferred.addCallback(delete_all_mails) + prepareDeferred.addCallback(flush_to_soledad, finalizeDeferred) + elif args.command == 'load-mails': + prepareDeferred.addCallback(load_mails, args.file) + prepareDeferred.addCallback(flush_to_soledad, finalizeDeferred) + elif args.command == 'dump-soledad': + prepareDeferred.addCallback(dump_soledad) + prepareDeferred.chainDeferred(finalizeDeferred) + elif args.command == 'sync': + # nothing to do here, sync is already part of the chain + prepareDeferred.chainDeferred(finalizeDeferred) + else: + print 'Unsupported command: %s' % args.command + prepareDeferred.chainDeferred(finalizeDeferred) + + return finalizeDeferred + + +def delete_all_mails(args): + leap_session, soledad = args + generation, docs = soledad.get_all_docs() + + for doc in docs: + if doc.content.get('type', None) in ['head', 'cnt', 'flags']: + soledad.delete_doc(doc) + + return args + + +def add_mail_folder(account, maildir, folder_name, deferreds): + mbx = account.getMailbox(folder_name) + for mail in maildir: + flags = (WithMsgFields.RECENT_FLAG,) if mail.get_subdir() == 'new' else () + if 'S' in mail.get_flags(): + flags = (WithMsgFields.SEEN_FLAG,) + flags + if 'R' in mail.get_flags(): + flags = (WithMsgFields.ANSWERED_FLAG,) + flags + + deferreds.append(mbx.addMessage(mail.as_string(), flags=flags, notify_on_disk=False)) + + +@defer.inlineCallbacks +def load_mails(args, mail_paths): + leap_session, soledad = args + account = leap_session.account + + deferreds = [] + + for path in mail_paths: + maildir = Maildir(path, factory=None) + add_mail_folder(account, maildir, 'INBOX', deferreds) + for mail_folder_name in maildir.list_folders(): + mail_folder = maildir.get_folder(mail_folder_name) + add_mail_folder(account, mail_folder, mail_folder_name, deferreds) + + yield defer.DeferredList(deferreds) + defer.returnValue(args) + + +def flush_to_soledad(args, finalize): + leap_session, soledad = args + account = leap_session.account + memstore = account._memstore + permanent_store = memstore._permanent_store + + d = memstore.write_messages(permanent_store) + + def check_flushed(args): + if memstore.is_writing: + reactor.callLater(1, check_flushed, args) else: - print 'Unsupported command: %s' % args.command - d.addCallback(soledad_sync) - d.addCallback(shutdown) - d.addErrback(shutdown_on_error) + finalize.callback((leap_session, soledad)) - reactor.callWhenRunning(execute_command) - reactor.run() + d.addCallback(check_flushed) + + return args + + +def dump_soledad(args): + leap_session, soledad = args + + generation, docs = soledad.get_all_docs() + + for doc in docs: + print doc + print '\n' + + return args def shutdown(args): @@ -163,7 +214,7 @@ def shutdown(args): def shutdown_on_error(error): print error - reactor.stop() + shutdown(None) -if __name__ == 'main': +if __name__ == '__main__': initialize() diff --git a/service/pixelated/resources/mails_resource.py b/service/pixelated/resources/mails_resource.py index c057031a..3822abd3 100644 --- a/service/pixelated/resources/mails_resource.py +++ b/service/pixelated/resources/mails_resource.py @@ -53,6 +53,20 @@ class MailsDeleteResource(Resource): return respond_json(None, request) +class MailsRecoverResource(Resource): + isLeaf = True + + def __init__(self, mail_service): + Resource.__init__(self) + self._mail_service = mail_service + + def render_POST(self, request): + idents = json.loads(request.content.read())['idents'] + for ident in idents: + self._mail_service.recover_mail(ident) + return respond_json(None, request) + + class MailsResource(Resource): def _register_smtp_error_handler(self): @@ -66,6 +80,7 @@ class MailsResource(Resource): def __init__(self, mail_service, draft_service): Resource.__init__(self) self.putChild('delete', MailsDeleteResource(mail_service)) + self.putChild('recover', MailsRecoverResource(mail_service)) self.putChild('read', MailsReadResource(mail_service)) self.putChild('unread', MailsUnreadResource(mail_service)) diff --git a/service/test/mailsets/common/cur/mbox00000002:2,S b/service/test/mailsets/common/cur/mbox00000002:2,S new file mode 100644 index 00000000..2881e352 --- /dev/null +++ b/service/test/mailsets/common/cur/mbox00000002:2,S @@ -0,0 +1,19 @@ +Subject: A read mail +From: bob@try.pixelated-project.org +To: alice@try.pixelated-project.org +X-Pixelated-encryption-status: true +Date: Tue, 08 Apr 2015 18:32:05 +0000 (UTC) + +Q: How many hardware engineers does it take to change a lightbulb? +A: None. We'll fix it in software. + +Q: How many system programmers does it take to change a light bulb? +A: None. The application can work around it. + +Q: How many software engineers does it take to change a lightbulb? +A: None. We'll document it in the manual. + +Q: How many tech writers does it take to change a lightbulb? +A: None. The user can figure it out. + + diff --git a/service/test/mailsets/common/cur/mbox00000003:2,S b/service/test/mailsets/common/cur/mbox00000003:2,S new file mode 100644 index 00000000..36c6706a --- /dev/null +++ b/service/test/mailsets/common/cur/mbox00000003:2,S @@ -0,0 +1,17 @@ +Subject: Scott's Laws +From: scott@try.pixelated-project.org +To: alice@try.pixelated-project.org +X-Pixelated-encryption-status: true +Date: Tue, 09 Apr 2015 23:42:05 +0000 (UTC) + +Scott's First Law: + No matter what goes wrong, it will probably look right. + +Scott's Second Law: + When an error has been detected and corrected, it will be found + to have been wrong in the first place. +Corollary: + After the correction has been found in error, it will be + impossible to fit the original quantity back into the + equation. + diff --git a/service/test/mailsets/common/cur/mbox00000004:2,S b/service/test/mailsets/common/cur/mbox00000004:2,S new file mode 100644 index 00000000..db3be01d --- /dev/null +++ b/service/test/mailsets/common/cur/mbox00000004:2,S @@ -0,0 +1,9 @@ +Subject: Progress is only made on alternate Fridays +From: bob@try.pixelated-project.org +To: alice@try.pixelated-project.org +X-Pixelated-encryption-status: true +Date: Tue, 10 Apr 2015 03:15:38 +0000 (UTC) + +Weinberg's First Law: + Progress is only made on alternate Fridays. + diff --git a/service/test/mailsets/common/cur/mbox00000005:2,S b/service/test/mailsets/common/cur/mbox00000005:2,S new file mode 100644 index 00000000..e02718f1 --- /dev/null +++ b/service/test/mailsets/common/cur/mbox00000005:2,S @@ -0,0 +1,12 @@ +Subject: bypassing trouble +From: oliver@try.pixelated-project.org +To: alice@try.pixelated-project.org +X-Pixelated-encryption-status: true +Date: Tue, 04 Apr 2015 16:41:39 +0000 (UTC) + +If I had a formula for bypassing trouble, I would not pass it around. +Trouble creates a capacity to handle it. I don't say embrace trouble; that's +as bad as treating it as an enemy. But I do say meet it as a friend, for +you'll see a lot of it and you had better be on speaking terms with it. + -- Oliver Wendell Holmes, Jr. + diff --git a/service/test/mailsets/common/new/mbox00000000:2, b/service/test/mailsets/common/new/mbox00000000:2, new file mode 100644 index 00000000..a35e63f0 --- /dev/null +++ b/service/test/mailsets/common/new/mbox00000000:2, @@ -0,0 +1,12 @@ +Subject: Hey Alice +From: bob@try.pixelated-project.org +To: alice@try.pixelated-project.org +X-TW-Pixelated-Tags: cia +X-Pixelated-encryption-status: true +Date: Tue, 01 Apr 2015 08:43:27 +0000 (UTC) + +Yesterday upon the stair +I met a man who wasn't there. +He wasn't there again today -- +I think he's from the CIA. + diff --git a/service/test/mailsets/common/new/mbox00000001 b/service/test/mailsets/common/new/mbox00000001 new file mode 100644 index 00000000..e1c6ba8b --- /dev/null +++ b/service/test/mailsets/common/new/mbox00000001 @@ -0,0 +1,22 @@ +Subject: Garbage collection works +From: bob@try.pixelated-project.org +To: alice@try.pixelated-project.org +X-TW-Pixelated-Tags: garbage +X-Pixelated-encryption-status: true +Date: Tue, 05 Apr 2015 13:52:05 +0000 (UTC) + +=== ALL USERS PLEASE NOTE ======================== + +The garbage collector now works. In addition a new, experimental garbage +collection algorithm has been installed. With SI:%DSK-GC-QLX-BITS set to 17, +(NOT the default) the old garbage collection algorithm remains in force; when +virtual storage is filled, the machine cold boots itself. With SI:%DSK-GC- +QLX-BITS set to 23, the new garbage collector is enabled. Unlike most garbage +collectors, the new gc starts its mark phase from the mind of the user, rather +than from the obarray. This allows the garbage collection of significantly +more Qs. As the garbage collector runs, it may ask you something like "Do you +remember what SI:RDTBL-TRANS does?", and if you can't give a reasonable answer +in thirty seconds, the symbol becomes a candidate for GCing. The variable +SI:%GC-QLX-LUSER-TM governs how long the GC waits before timing out the user. + + diff --git a/service/test/mailsets/common/tmp/.keep b/service/test/mailsets/common/tmp/.keep new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/service/test/mailsets/common/tmp/.keep diff --git a/service/test/unit/adapter/test_mail_service.py b/service/test/unit/adapter/test_mail_service.py index 34fec708..f4b89f57 100644 --- a/service/test/unit/adapter/test_mail_service.py +++ b/service/test/unit/adapter/test_mail_service.py @@ -101,3 +101,13 @@ class TestMailService(unittest.TestCase): self.mail_service.delete_mail(1) verify(self.mailboxes).move_to_trash(1) + + def test_recover_mail(self): + mail_to_recover = PixelatedMail.from_soledad(*leap_mail(), soledad_querier=None) + when(self.mail_service).mail(1).thenReturn(mail_to_recover) + when(self.mailboxes).move_to_inbox(1).thenReturn(mail_to_recover) + + self.mail_service.recover_mail(1) + + verify(self.mailboxes).move_to_inbox(1) + verify(self.search_engine).index_mail(mail_to_recover) diff --git a/service/test/unit/adapter/test_mailboxes.py b/service/test/unit/adapter/test_mailboxes.py new file mode 100644 index 00000000..5b4548eb --- /dev/null +++ b/service/test/unit/adapter/test_mailboxes.py @@ -0,0 +1,40 @@ +# +# Copyright (c) 2014 ThoughtWorks, Inc. +# +# Pixelated is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pixelated 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with Pixelated. If not, see <http://www.gnu.org/licenses/>. +import unittest + +from pixelated.adapter.model.mail import PixelatedMail +from pixelated.adapter.services.mailboxes import Mailboxes +from mockito import mock, when, verify +from test.support import test_helper +from mock import MagicMock + + +class PixelatedMailboxesTest(unittest.TestCase): + def setUp(self): + self.querier = mock() + self.search_engine = mock() + self.account = MagicMock() + self.mailboxes = Mailboxes(self.account, self.querier, self.search_engine) + + def test_move_to_inbox(self): + mail = PixelatedMail.from_soledad(*test_helper.leap_mail(), soledad_querier=self.querier) + when(self.querier).mail(1).thenReturn(mail) + when(mail).save().thenReturn(None) + + mail.set_mailbox('TRASH') + recovered_mail = self.mailboxes.move_to_inbox(1) + self.assertEquals('INBOX', recovered_mail.mailbox_name) + verify(mail).save() diff --git a/service/test/unit/fixtures/mailset/cur/.keep b/service/test/unit/fixtures/mailset/cur/.keep new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/service/test/unit/fixtures/mailset/cur/.keep diff --git a/service/test/unit/fixtures/mailset/mbox00000000 b/service/test/unit/fixtures/mailset/new/mbox00000000 index 3d01c203..3d01c203 100644 --- a/service/test/unit/fixtures/mailset/mbox00000000 +++ b/service/test/unit/fixtures/mailset/new/mbox00000000 diff --git a/service/test/unit/fixtures/mailset/mbox00000001 b/service/test/unit/fixtures/mailset/new/mbox00000001 index fc76bba2..fc76bba2 100644 --- a/service/test/unit/fixtures/mailset/mbox00000001 +++ b/service/test/unit/fixtures/mailset/new/mbox00000001 diff --git a/service/test/unit/fixtures/mailset/tmp/.keep b/service/test/unit/fixtures/mailset/tmp/.keep new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/service/test/unit/fixtures/mailset/tmp/.keep diff --git a/service/test/unit/maintenance/test_commands.py b/service/test/unit/maintenance/test_commands.py index f23655d8..6f993106 100644 --- a/service/test/unit/maintenance/test_commands.py +++ b/service/test/unit/maintenance/test_commands.py @@ -19,74 +19,96 @@ import email from pixelated.maintenance import delete_all_mails, load_mails from pixelated.bitmask_libraries.session import LeapSession from leap.mail.imap.account import SoledadBackedAccount +from leap.mail.imap.fields import WithMsgFields from leap.soledad.client import Soledad from leap.soledad.common.document import SoledadDocument from mock import MagicMock, ANY from os.path import join, dirname +from twisted.internet import defer, reactor class TestCommands(unittest.TestCase): - def setUp(self): - self.leap_session = MagicMock(spec=LeapSession) - self.soledad = MagicMock(spec=Soledad) - self.account = MagicMock(spec=SoledadBackedAccount) - self.mailbox = MagicMock() - self.leap_session.account = self.account - self.account.getMailbox.return_value = self.mailbox + def setUp(self): + self.leap_session = MagicMock(spec=LeapSession) + self.soledad = MagicMock(spec=Soledad) + self.account = MagicMock(spec=SoledadBackedAccount) + self.mailbox = MagicMock() + self.leap_session.account = self.account + self.account.getMailbox.return_value = self.mailbox - self.args = (self.leap_session, self.soledad) + self.args = (self.leap_session, self.soledad) - def test_delete_all_mails_supports_empty_doclist(self): - self.soledad.get_all_docs.return_value = (1, []) + def test_delete_all_mails_supports_empty_doclist(self): + self.soledad.get_all_docs.return_value = (1, []) - delete_all_mails(self.args) + delete_all_mails(self.args) - self.assertFalse(self.soledad.delete_doc.called) + self.assertFalse(self.soledad.delete_doc.called) - def test_delete_all_mails(self): - doc = MagicMock(spec=SoledadDocument) - doc.content = {'type': 'head'} - self.soledad.get_all_docs.return_value = (1, [doc]) + def test_delete_all_mails(self): + doc = MagicMock(spec=SoledadDocument) + doc.content = {'type': 'head'} + self.soledad.get_all_docs.return_value = (1, [doc]) - delete_all_mails(self.args) + delete_all_mails(self.args) - self.soledad.delete_doc.assert_called_once_with(doc) + self.soledad.delete_doc.assert_called_once_with(doc) - def test_only_mail_documents_are_deleted(self): - docs = self._create_docs_of_type(['head', 'cnt', 'flags', 'mbx', 'foo', None]) - self.soledad.get_all_docs.return_value = (1, docs) + def test_only_mail_documents_are_deleted(self): + docs = self._create_docs_of_type(['head', 'cnt', 'flags', 'mbx', 'foo', None]) + self.soledad.get_all_docs.return_value = (1, docs) - delete_all_mails(self.args) + delete_all_mails(self.args) - for doc in docs: - if doc.content['type'] in ['head', 'cnt', 'flags']: - self.soledad.delete_doc.assert_any_call(doc) - self.assertEqual(3, len(self.soledad.delete_doc.mock_calls)) + for doc in docs: + if doc.content['type'] in ['head', 'cnt', 'flags']: + self.soledad.delete_doc.assert_any_call(doc) + self.assertEqual(3, len(self.soledad.delete_doc.mock_calls)) - def _create_docs_of_type(self, type_list): - return [self._create_doc_type(t) for t in type_list] + def _create_docs_of_type(self, type_list): + return [self._create_doc_type(t) for t in type_list] - def _create_doc_type(self, doc_type): - doc = MagicMock(spec=SoledadDocument) - doc.content = {'type': doc_type} - return doc + def _create_doc_type(self, doc_type): + doc = MagicMock(spec=SoledadDocument) + doc.content = {'type': doc_type} + return doc - def test_load_mails_empty_path_list(self): - load_mails(self.args, []) + def test_load_mails_empty_path_list(self): + load_mails(self.args, []) - self.assertFalse(self.mailbox.called) + self.assertFalse(self.mailbox.called) - def test_load_mails_adds_mails(self): - mail_root = join(dirname(__file__), '..', 'fixtures', 'mailset') + def test_load_mails_adds_mails(self): + # given + mail_root = join(dirname(__file__), '..', 'fixtures', 'mailset') + firstMailDeferred = defer.Deferred() + secondMailDeferred = defer.Deferred() + self.mailbox.addMessage.side_effect = [firstMailDeferred, secondMailDeferred] - foo = load_mails(self.args, [mail_root]) + # when + d = load_mails(self.args, [mail_root]) + # then + def assert_mails_added(_): self.assertTrue(self.mailbox.addMessage.called) - self.mailbox.addMessage.assert_any_call(self._mail_content(join(mail_root, 'mbox00000000')), flags=("\\RECENT",), notify_on_disk=False) - self.mailbox.addMessage.assert_any_call(self._mail_content(join(mail_root, 'mbox00000001')), flags=("\\RECENT",), notify_on_disk=False) + self.mailbox.addMessage.assert_any_call(self._mail_content(join(mail_root, 'new', 'mbox00000000')), flags=(WithMsgFields.RECENT_FLAG,), notify_on_disk=False) + self.mailbox.addMessage.assert_any_call(self._mail_content(join(mail_root, 'new', 'mbox00000001')), flags=(WithMsgFields.RECENT_FLAG,), notify_on_disk=False) - def _mail_content(self, mail_file): - with open(mail_file, 'r') as fp: - m = email.message_from_file(fp) - return m.as_string() + def error_callack(err): + print err + self.assertTrue(False) + + d.addCallback(assert_mails_added) + d.addErrback(error_callack) + + # trigger callbacks for both mails + reactor.callLater(0, firstMailDeferred.callback, None) + reactor.callLater(0, secondMailDeferred.callback, None) + + return d + + def _mail_content(self, mail_file): + with open(mail_file, 'r') as fp: + m = email.message_from_file(fp) + return m.as_string() diff --git a/web-ui/app/js/mail_list_actions/ui/mail_list_actions.js b/web-ui/app/js/mail_list_actions/ui/mail_list_actions.js index 8987a7d4..2c9c699f 100644 --- a/web-ui/app/js/mail_list_actions/ui/mail_list_actions.js +++ b/web-ui/app/js/mail_list_actions/ui/mail_list_actions.js @@ -28,6 +28,7 @@ define( 'mail_list_actions/ui/toggle_check_all_trigger', 'mail_list_actions/ui/pagination_trigger', 'mail_list_actions/ui/delete_many_trigger', + 'mail_list_actions/ui/recover_many_trigger', 'mail_list_actions/ui/mark_many_as_read_trigger', 'mail_list_actions/ui/mark_as_unread_trigger' ], @@ -43,6 +44,7 @@ define( toggleCheckAllMailTrigger, paginationTrigger, deleteManyTrigger, + recoverManyTrigger, markManyAsReadTrigger, markAsUnreadTrigger ) { @@ -51,14 +53,13 @@ define( function mailsActions() { this.render = function() { - this.$node.html(templates.mailActions.actionsBox({ - txtDeleteButton: this.getTxtDeleteButton() - })); + this.$node.html(this.getActionsBoxTemplate()); refreshTrigger.attachTo('#refresh-trigger'); composeTrigger.attachTo('#compose-trigger'); toggleCheckAllMailTrigger.attachTo('#toggle-check-all-emails'); paginationTrigger.attachTo('#pagination-trigger'); deleteManyTrigger.attachTo('#delete-selected'); + recoverManyTrigger.attachTo('#recover-selected'); markManyAsReadTrigger.attachTo('#mark-selected-as-read'); markAsUnreadTrigger.attachTo('#mark-selected-as-unread'); refresher.attachTo(document); @@ -68,19 +69,19 @@ define( return this.attr.currentTag || urlParams.getTag(); }; - this.getTxtDeleteButton = function() { - if(this.getCurrentTag() === 'trash') { - return 'Delete permanently'; - } else { - return 'Delete'; - } - }; - this.updateCurrentTag = function (ev, data) { this.attr.currentTag = data.tag; this.render(); }; + this.getActionsBoxTemplate = function () { + if(this.getCurrentTag() === 'trash') { + return templates.mailActions.trashActionsBox(); + } else { + return templates.mailActions.actionsBox(); + } + }; + this.after('initialize', function () { this.on(document, events.ui.tag.select, this.updateCurrentTag); this.render(); diff --git a/web-ui/app/js/mail_list_actions/ui/recover_many_trigger.js b/web-ui/app/js/mail_list_actions/ui/recover_many_trigger.js new file mode 100644 index 00000000..e0a32094 --- /dev/null +++ b/web-ui/app/js/mail_list_actions/ui/recover_many_trigger.js @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2014 ThoughtWorks, Inc. + * + * Pixelated is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Pixelated 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Pixelated. If not, see <http://www.gnu.org/licenses/>. + */ +define( + [ + 'flight/lib/component', + 'views/templates', + 'mixins/with_enable_disable_on_event', + 'page/events' + ], + + function(defineComponent, templates, withEnableDisableOnEvent, events) { + 'use strict'; + + return defineComponent(recoverManyTrigger, withEnableDisableOnEvent(events.ui.mails.hasMailsChecked)); + + function recoverManyTrigger() { + this.defaultAttrs({}); + + this.getMailsToRecover = function(event) { + this.trigger(document, events.ui.mail.wantChecked, this.$node); + }; + + this.recoverManyEmails = function (event, data) { + this.trigger(document, events.ui.mail.recoverMany, data); + }; + + this.after('initialize', function () { + this.on('click', this.getMailsToRecover); + this.on(events.ui.mail.hereChecked, this.recoverManyEmails); + }); + } + } +); diff --git a/web-ui/app/js/page/default.js b/web-ui/app/js/page/default.js index bbff271f..1571202e 100644 --- a/web-ui/app/js/page/default.js +++ b/web-ui/app/js/page/default.js @@ -27,6 +27,7 @@ define( 'mail_view/data/mail_sender', 'services/mail_service', 'services/delete_service', + 'services/recover_service', 'tags/ui/tag_list', 'tags/data/tags', 'page/router', @@ -56,6 +57,7 @@ define( mailSender, mailService, deleteService, + recoverService, tagList, tags, router, @@ -89,6 +91,7 @@ define( mailService.attachTo(document); deleteService.attachTo(document); + recoverService.attachTo(document); tags.attachTo(document); tagList.attachTo('#tag-list'); diff --git a/web-ui/app/js/page/events.js b/web-ui/app/js/page/events.js index f1f426f7..cf1b29ad 100644 --- a/web-ui/app/js/page/events.js +++ b/web-ui/app/js/page/events.js @@ -55,6 +55,7 @@ define(function () { updateSelected: 'ui:mail:updateSelected', delete: 'ui:mail:delete', deleteMany: 'ui:mail:deleteMany', + recoverMany: 'ui:mail:recoverMany', wantChecked: 'ui:mail:wantChecked', hereChecked: 'ui:mail:hereChecked', checked: 'ui:mail:checked', @@ -108,6 +109,7 @@ define(function () { unread: 'mail:unread', delete: 'mail:delete', deleteMany: 'mail:deleteMany', + recoverMany: 'mail:recoverMany', deleted: 'mail:deleted', saveDraft: 'draft:save', draftSaved: 'draft:saved', diff --git a/web-ui/app/js/services/mail_service.js b/web-ui/app/js/services/mail_service.js index 1fa41619..04194964 100644 --- a/web-ui/app/js/services/mail_service.js +++ b/web-ui/app/js/services/mail_service.js @@ -123,6 +123,16 @@ define( }, this); }; + this.triggerRecovered = function (dataToRecover) { + return _.bind(function () { + var mails = dataToRecover.mails || [dataToRecover.mail]; + + this.refreshMails(); + this.trigger(document, events.ui.userAlerts.displayMessage, { message: dataToRecover.successMessage}); + this.trigger(document, events.ui.mails.uncheckAll); + }, this); + }; + this.deleteMail = function (ev, data) { monitoredAjax(this, '/mail/' + data.mail.ident, {type: 'DELETE'}) @@ -145,6 +155,21 @@ define( .fail(this.errorMessage(i18n('Could not delete emails'))); }; + this.recoverManyMails = function (ev, data) { + var dataToRecover = data; + var mailIdents = _.map(data.mails, function (mail) { + return mail.ident; + }); + + monitoredAjax(this, '/mails/recover', { + type: 'POST', + dataType: 'json', + contentType: 'application/json; charset=utf-8', + data: JSON.stringify({idents: mailIdents}) + }).done(this.triggerRecovered(dataToRecover)) + .fail(this.errorMessage(i18n('Could not move emails to inbox'))); + }; + function compileQuery(data) { var query = 'tag:"' + that.attr.currentTag + '"'; @@ -273,6 +298,7 @@ define( this.on(document, events.mail.unread, this.unreadMail); this.on(document, events.mail.delete, this.deleteMail); this.on(document, events.mail.deleteMany, this.deleteManyMails); + this.on(document, events.mail.recoverMany, this.recoverManyMails); this.on(document, events.search.perform, this.newSearch); this.on(document, events.ui.tag.selected, this.fetchByTag); this.on(document, events.ui.tag.select, this.fetchByTag); diff --git a/web-ui/app/js/services/recover_service.js b/web-ui/app/js/services/recover_service.js new file mode 100644 index 00000000..62fcf1f9 --- /dev/null +++ b/web-ui/app/js/services/recover_service.js @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2014 ThoughtWorks, Inc. + * + * Pixelated is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Pixelated 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Pixelated. If not, see <http://www.gnu.org/licenses/>. + */ + +define(['flight/lib/component', 'page/events', 'views/i18n'], function (defineComponent, events, i18n) { + 'use strict'; + + return defineComponent(function() { + + this.recoverManyEmails = function (event, data) { + var emails = _.values(data.checkedMails); + + this.trigger(document, events.mail.recoverMany, { + mails: emails, + successMessage: i18n('Your messages were moved to inbox!') + }); + + }; + + this.after('initialize', function () { + this.on(document, events.ui.mail.recoverMany, this.recoverManyEmails); + }); + + }); +}); diff --git a/web-ui/app/js/views/templates.js b/web-ui/app/js/views/templates.js index 64e269ca..5e991a20 100644 --- a/web-ui/app/js/views/templates.js +++ b/web-ui/app/js/views/templates.js @@ -46,6 +46,7 @@ define(['hbs/templates'], function (templates) { }, mailActions: { actionsBox: window.Pixelated['app/templates/mail_actions/actions_box.hbs'], + trashActionsBox: window.Pixelated['app/templates/mail_actions/trash_actions_box.hbs'], composeTrigger: window.Pixelated['app/templates/mail_actions/compose_trigger.hbs'], refreshTrigger: window.Pixelated['app/templates/mail_actions/refresh_trigger.hbs'], paginationTrigger: window.Pixelated['app/templates/mail_actions/pagination_trigger.hbs'] diff --git a/web-ui/app/locales/en-us/translation.json b/web-ui/app/locales/en-us/translation.json index 330e38b6..4dd90e90 100644 --- a/web-ui/app/locales/en-us/translation.json +++ b/web-ui/app/locales/en-us/translation.json @@ -46,12 +46,12 @@ "encrypted encryption-error": "Unable to decrypt", "encrypted encryption-valid": "Encrypted", "not-encrypted": "Not Encrypted", - "signed": "Certified sender", - "signed signature-revoked": "Sender could not be securely identifiedw", - "signed signature-expired": "Sender could not be securely identified", - "signed signature-not-trusted": "Sender and/or message cannot be trusted", - "signed signature-unknown": "Sender and/or message cannot be trusted", - "not-signed": "Uncertified sender", + "signed": "Verified sender", + "signed signature-revoked": "Unreliable signature", + "signed signature-expired": "Unreliable signature", + "signed signature-not-trusted": "Unreliable signature", + "signed signature-unknown": "Unreliable signature", + "not-signed": "Not signed", "send-button": "Send", "sending-mail": "Sending...", "draft-button": "Save Draft", diff --git a/web-ui/app/scss/_compose.scss b/web-ui/app/scss/_compose.scss index c026b866..acff745d 100644 --- a/web-ui/app/scss/_compose.scss +++ b/web-ui/app/scss/_compose.scss @@ -24,7 +24,8 @@ // COMPOSE PANE #compose-box, #draft-box, #reply-box { - margin: 0 0 50px 10px; + margin: 5px 0 50px 30px; + padding: 0; .input-container { border-bottom: 1px solid #DDD; padding: 1px; @@ -44,7 +45,6 @@ &#subject { font-size: 1.6875rem; line-height: 1.4; - margin-top: 26px; } } textarea { @@ -85,6 +85,10 @@ } } + button.close-mail-button { + margin: 1px; + } + @include recipients; } diff --git a/web-ui/app/scss/_read.scss b/web-ui/app/scss/_read.scss index 7235df72..d621f672 100644 --- a/web-ui/app/scss/_read.scss +++ b/web-ui/app/scss/_read.scss @@ -32,6 +32,13 @@ height: 27px; margin-right: 3px; } + + .full-view-header { + display:inline-block; + padding-top: 5px; + width:95%; + flex-shrink:1; + } } h3 { margin-bottom: 0; diff --git a/web-ui/app/templates/mail_actions/actions_box.hbs b/web-ui/app/templates/mail_actions/actions_box.hbs index 628a6240..b6dc2f53 100644 --- a/web-ui/app/templates/mail_actions/actions_box.hbs +++ b/web-ui/app/templates/mail_actions/actions_box.hbs @@ -1,6 +1,6 @@ <li><input type="checkbox" id="toggle-check-all-emails"/></li> <li><input type="button" id="mark-selected-as-read" value="{{t 'Mark as read'}}" disabled="disabled"/></li> <li><input type="button" id="mark-selected-as-unread" value="{{t 'Mark as unread'}}" disabled="disabled"/></li> -<li><input type="button" id="delete-selected" value="{{t txtDeleteButton}}" disabled="disabled"/></li> +<li><input type="button" id="delete-selected" value="{{t 'Delete'}}" disabled="disabled"/></li> <li id="pagination-trigger" class="right"></li> <li id="refresh-trigger" class="right"></li> diff --git a/web-ui/app/templates/mail_actions/trash_actions_box.hbs b/web-ui/app/templates/mail_actions/trash_actions_box.hbs new file mode 100644 index 00000000..7852bd6f --- /dev/null +++ b/web-ui/app/templates/mail_actions/trash_actions_box.hbs @@ -0,0 +1,5 @@ +<li><input type="checkbox" id="toggle-check-all-emails"/></li> +<li><input type="button" id="delete-selected" value="{{t 'Delete permanently'}}" disabled="disabled"/></li> +<li><input type="button" id="recover-selected" value="{{t 'Move to Inbox'}}" disabled="disabled"/></li> +<li id="pagination-trigger" class="right"></li> +<li id="refresh-trigger" class="right"></li> diff --git a/web-ui/app/templates/mails/full_view.hbs b/web-ui/app/templates/mails/full_view.hbs index a5c41121..77994860 100644 --- a/web-ui/app/templates/mails/full_view.hbs +++ b/web-ui/app/templates/mails/full_view.hbs @@ -8,7 +8,7 @@ </button> - <div style="display:inline-block;padding-top: 5px;width:95%;flex-shrink:1" > + <div class="full-view-header"> <div class="column large-12 no-padding security-status"> {{#if signatureStatus}} diff --git a/web-ui/test/spec/mail_list_actions/ui/mail_list_actions.spec.js b/web-ui/test/spec/mail_list_actions/ui/mail_list_actions.spec.js index 7f7ba64a..d8917ed9 100644 --- a/web-ui/test/spec/mail_list_actions/ui/mail_list_actions.spec.js +++ b/web-ui/test/spec/mail_list_actions/ui/mail_list_actions.spec.js @@ -28,6 +28,24 @@ describeComponent('mail_list_actions/ui/mail_list_actions', function () { expect(this.component.$node.html()).toMatch('<li><input type="button" id="delete-selected" value="Delete permanently" disabled="disabled"></li>'); }); + + it('should render move to inbox if on trash', function () { + var urlParams = require('page/router/url_params'); + spyOn(urlParams, 'getTag').and.returnValue('trash'); + + this.setupComponent(); + + expect(this.component.$node.html()).toMatch('<li><input type="button" id="recover-selected" value="Move to Inbox" disabled="disabled"></li>'); + }); + + it('should not render move to inbox if on trash', function () { + var urlParams = require('page/router/url_params'); + spyOn(urlParams, 'getTag').and.returnValue('inbox'); + + this.setupComponent(); + + expect(this.component.$node.html()).not.toMatch('<li><input type="button" id="recover-selected" value="Move to Inbox" disabled="disabled"></li>'); + }); }); }); diff --git a/web-ui/test/spec/services/mail_service.spec.js b/web-ui/test/spec/services/mail_service.spec.js index 1a5cb9f7..7fb2bfda 100644 --- a/web-ui/test/spec/services/mail_service.spec.js +++ b/web-ui/test/spec/services/mail_service.spec.js @@ -174,6 +174,42 @@ describeComponent('services/mail_service', function () { expect(spyEvent).toHaveBeenTriggeredOnAndWith(document, {message: i18n('Could not delete email')} ); }); + it('will try to recover a message when requested to', function() { + var spyAjax = spyOn($, 'ajax').and.returnValue($.Deferred()); + this.component.trigger(Pixelated.events.mail.recoverMany, {mails: [{ident: '43'}, {ident: '44'}]}); + expect(spyAjax).toHaveBeenCalled(); + expect(spyAjax.calls.mostRecent().args[0]).toEqual('/mails/recover'); + expect(spyAjax.calls.mostRecent().args[1].type).toEqual('POST'); + expect(spyAjax.calls.all()[0].args[1].data).toEqual(JSON.stringify({ idents: ['43', '44'] } )); + }); + + describe('when successfuly recovers emails', function () { + var displayMessageEvent, uncheckAllEvent, mailsRecoveredEvent; + + beforeEach(function () { + displayMessageEvent = spyOnEvent(document, Pixelated.events.ui.userAlerts.displayMessage); + uncheckAllEvent = spyOnEvent(document, Pixelated.events.ui.mails.uncheckAll); + spyOn(this.component, 'refreshMails'); + + this.component.triggerRecovered({ + successMessage: 'A success message', + mails: {1: 'email 1', 2: 'email 2'} + })(); + }); + + it('will trigger that a message has been recovered when it is done recovering', function() { + expect(this.component.refreshMails).toHaveBeenCalled(); + }); + + it('displays a success message', function () { + expect(displayMessageEvent).toHaveBeenTriggeredOnAndWith(document, {message: 'A success message'}); + }); + + it('unchecks all checked mails', function () { + expect(uncheckAllEvent).toHaveBeenTriggeredOn(document); + }); + }); + it('triggers mails:available with received mails and keeps that tag as the current tag', function() { var eventSpy = spyOnEvent(document, Pixelated.events.mails.available); diff --git a/web-ui/test/spec/services/recover_service.spec.js b/web-ui/test/spec/services/recover_service.spec.js new file mode 100644 index 00000000..86fe9f87 --- /dev/null +++ b/web-ui/test/spec/services/recover_service.spec.js @@ -0,0 +1,32 @@ +describeComponent('services/recover_service', function () { + 'use strict'; + + var i18n; + + beforeEach( function () { + this.setupComponent(); + i18n = require('views/i18n'); + }); + + var mail1 = { + ident: 42, + isInTrash: function() { return false; } + }; + + var mail2 = { + ident: 34, + isInTrash: function() { return true; } + }; + + it('moves selected emails from trash back to inbox', function () { + var mailRecoverManyEvent = spyOnEvent(document, Pixelated.events.mail.recoverMany); + this.component.trigger(document, Pixelated.events.ui.mail.recoverMany, {checkedMails: {mail1: mail1, mail2: mail2}}); + + var expectedRecoverManyEventData = { + mails: [mail1, mail2], + successMessage: i18n('Your messages were moved to inbox!') + }; + + expect(mailRecoverManyEvent).toHaveBeenTriggeredOnAndWith(document, expectedRecoverManyEventData); + }); +}); |