summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Vagrantfile19
-rwxr-xr-xprovisioning/build-hackday-box.sh52
-rw-r--r--provisioning/manifests/hackday.pp1
-rwxr-xr-xprovisioning/modules/pixelated/files/activate_custom_node_modules.sh11
-rw-r--r--provisioning/modules/pixelated/manifests/hackday_dev_env.pp14
-rw-r--r--provisioning/modules/pixelated/manifests/source.pp1
-rw-r--r--service/pixelated/adapter/services/mail_service.py5
-rw-r--r--service/pixelated/adapter/services/mailboxes.py8
-rw-r--r--service/pixelated/certificates/dev.pixelated-project.org.ca.crt62
-rw-r--r--service/pixelated/certificates/dev.wazokazi.is.ca.crt33
-rw-r--r--service/pixelated/certificates/example.wazokazi.is.ca.crt32
-rw-r--r--service/pixelated/maintenance.py195
-rw-r--r--service/pixelated/resources/mails_resource.py15
-rw-r--r--service/test/mailsets/common/cur/mbox00000002:2,S19
-rw-r--r--service/test/mailsets/common/cur/mbox00000003:2,S17
-rw-r--r--service/test/mailsets/common/cur/mbox00000004:2,S9
-rw-r--r--service/test/mailsets/common/cur/mbox00000005:2,S12
-rw-r--r--service/test/mailsets/common/new/mbox00000000:2,12
-rw-r--r--service/test/mailsets/common/new/mbox0000000122
-rw-r--r--service/test/mailsets/common/tmp/.keep0
-rw-r--r--service/test/unit/adapter/test_mail_service.py10
-rw-r--r--service/test/unit/adapter/test_mailboxes.py40
-rw-r--r--service/test/unit/fixtures/mailset/cur/.keep0
-rw-r--r--service/test/unit/fixtures/mailset/new/mbox00000000 (renamed from service/test/unit/fixtures/mailset/mbox00000000)0
-rw-r--r--service/test/unit/fixtures/mailset/new/mbox00000001 (renamed from service/test/unit/fixtures/mailset/mbox00000001)0
-rw-r--r--service/test/unit/fixtures/mailset/tmp/.keep0
-rw-r--r--service/test/unit/maintenance/test_commands.py110
-rw-r--r--web-ui/app/js/mail_list_actions/ui/mail_list_actions.js23
-rw-r--r--web-ui/app/js/mail_list_actions/ui/recover_many_trigger.js47
-rw-r--r--web-ui/app/js/page/default.js3
-rw-r--r--web-ui/app/js/page/events.js2
-rw-r--r--web-ui/app/js/services/mail_service.js26
-rw-r--r--web-ui/app/js/services/recover_service.js38
-rw-r--r--web-ui/app/js/views/templates.js1
-rw-r--r--web-ui/app/locales/en-us/translation.json12
-rw-r--r--web-ui/app/scss/_compose.scss8
-rw-r--r--web-ui/app/scss/_read.scss7
-rw-r--r--web-ui/app/templates/mail_actions/actions_box.hbs2
-rw-r--r--web-ui/app/templates/mail_actions/trash_actions_box.hbs5
-rw-r--r--web-ui/app/templates/mails/full_view.hbs2
-rw-r--r--web-ui/test/spec/mail_list_actions/ui/mail_list_actions.spec.js18
-rw-r--r--web-ui/test/spec/services/mail_service.spec.js36
-rw-r--r--web-ui/test/spec/services/recover_service.spec.js32
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);
+ });
+});