summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--service/pixelated/account_recovery.py28
-rw-r--r--service/pixelated/assets/recovery.mail.en-US28
-rw-r--r--service/pixelated/assets/recovery.mail.pt-BR26
-rw-r--r--service/pixelated/resources/backup_account_resource.py7
-rw-r--r--service/pixelated/resources/login_resource.py11
-rw-r--r--service/pixelated/support/language.py24
-rw-r--r--service/test/unit/resources/test_backup_account_resource.py7
-rw-r--r--service/test/unit/resources/test_login_resource.py23
-rw-r--r--service/test/unit/support/test_language.py40
-rw-r--r--service/test/unit/test_account_recovery.py51
10 files changed, 177 insertions, 68 deletions
diff --git a/service/pixelated/account_recovery.py b/service/pixelated/account_recovery.py
index 723d4048..f84aea1a 100644
--- a/service/pixelated/account_recovery.py
+++ b/service/pixelated/account_recovery.py
@@ -15,25 +15,26 @@
# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
import pkg_resources
+import binascii
from twisted.internet.defer import inlineCallbacks, returnValue
from twisted.logger import Logger
from twisted.mail import smtp
-from email.mime.text import MIMEText
-
-from pixelated.resources.account_recovery_resource import AccountRecoveryResource
+from email import message_from_string
+from email.utils import formatdate
log = Logger()
class AccountRecovery(object):
- def __init__(self, session, soledad, smtp_config, backup_email, domain):
+ def __init__(self, session, soledad, smtp_config, backup_email, domain, language='en-US'):
self._bonafide_session = session
self._soledad = soledad
self._smtp_config = smtp_config
self._backup_email = backup_email
self._domain = domain
+ self._language = language
@inlineCallbacks
def update_recovery_code(self):
@@ -56,10 +57,7 @@ class AccountRecovery(object):
log.info('Sending mail containing the user\'s recovery code')
sender = 'team@{}'.format(self._domain)
- msg = MIMEText(self._get_recovery_mail(code))
- msg['Subject'] = 'Recovery Code'
- msg['From'] = sender
- msg['To'] = backup_email
+ msg = self._get_recovery_mail(code, sender, backup_email)
try:
send_mail_result = yield smtp.sendmail(
@@ -72,15 +70,15 @@ class AccountRecovery(object):
log.error('Failed trying to send the email with the recovery code')
raise e
- def _get_recovery_mail(self, code, language='en-US'):
+ def _get_recovery_mail(self, code, sender, backup_email):
recovery_mail = pkg_resources.resource_filename(
'pixelated.assets',
- 'recovery.mail.%s' % (language))
-
- account_recovery_url = '{}/{}'.format(self._domain, AccountRecoveryResource.BASE_URL)
+ 'recovery.mail.%s' % (self._language))
with open(recovery_mail) as mail_template_file:
- return mail_template_file.read().format(
+ return message_from_string(mail_template_file.read().format(
domain=self._domain,
- recovery_code=code,
- account_recovery_url=account_recovery_url)
+ recovery_code=binascii.hexlify(code),
+ backup_email=backup_email,
+ sender=sender,
+ date=formatdate(localtime=True)))
diff --git a/service/pixelated/assets/recovery.mail.en-US b/service/pixelated/assets/recovery.mail.en-US
index d17f8a28..e7a09f1b 100644
--- a/service/pixelated/assets/recovery.mail.en-US
+++ b/service/pixelated/assets/recovery.mail.en-US
@@ -1,20 +1,28 @@
+From: {sender}
+Date: {date}
+Subject: Recovery Code for {domain}
+To: {backup_email}
+Content-Type: text/plain; charset=UTF-8
+
Hello,
-You are receiving this email because you registered at a Pixelated provider, on {domain}.
-In case you ever forget your password, you can access this link {account_recovery_url} and put the following recovery code:
+You are receiving this message because you registered an email account at https://{domain}.
+If you ever forget your password, you'll need the code below to recover it. Save it! It is the only way to access to your account again.
{recovery_code}
-This code is the only way to recover access to your account in case you lose your password.
-Be careful and keep it safe!!!
+Save this message or write this code in a safe place.
+--
Why is this so important?
-Pixelated is an email client that respects your privacy and uses PGP Encryption to do so.
-Your password also gives you access to your keys, so if you forget it you will lose access to your account and the ability to decrypt your messages.
-We understand that forgetting passwords is a common thing, so we developed a more secure way to recover access to your account, therefore, a little bit more annoying ;)
-This code is half of a big code to recover your account, the other half is with the account administrator. In case you forget your password, use this code and your administrator code to recover access to your account. It's like those locks with two keys :)
-You will only succeed if you have both codes, so, never hurts to ask again: SAVE THIS CODE!
+Pixelated is an email client that respects your privacy and uses PGP Encryption to do so. Your password also gives you access to your keys, so if you forget it you will lose access to your account and the ability to read your messages.
+Forgetting passwords is a common thing, so we developed a more secure way to recover access to your account.
+
+1) This code is half of a big code to recover your account.
+2) The other half is with the account administrator.
+3) In case you forget your password, use this code and your administrator code to recover access to your account. It's like those locks with two keys :)
+
-PS: If you didn't create an account at {domain}, please ignore this email.
+PS: If you didn't create an account at https://{domain}, please ignore this email.
diff --git a/service/pixelated/assets/recovery.mail.pt-BR b/service/pixelated/assets/recovery.mail.pt-BR
new file mode 100644
index 00000000..558c6905
--- /dev/null
+++ b/service/pixelated/assets/recovery.mail.pt-BR
@@ -0,0 +1,26 @@
+From: {sender}
+Date: {date}
+Subject: Codigo de Recuperacao de {domain}
+To: {backup_email}
+Content-Type: text/plain; charset=UTF-8
+
+Olá,
+
+Você está recebendo isso porque você registrou um email no https://{domain}.
+Guarde o código abaixo para se um dia esquecer sua senha. Ele é a única forma de recuperar uma senha para acessar sua conta outra vez:
+
+{recovery_code}
+
+Salve essa mensagem ou anote o código em um lugar seguro.
+
+--
+Por que isso é importante?
+
+O Pixelated é um cliente de email que respeita sua privacidade e usa criptografia PGP para isso. Sua senha também dá acesso às suas chaves, então se você esquecê-la você perderá acesso a sua conta e a habilidade de ler suas mensagens. Esquecer a senha é algo comum, por isso desenvolvemos uma forma mais segura de recuperar sua conta.
+
+1) Esse código é uma metade de um código necessário para recuperar a conta.
+2) A outra metade está com o administrador da conta.
+3) Se você esquecer a senha, use esse código e o do administrador para recuperar acesso a conta. É como se fosse um cadeado com duas chaves :)
+
+
+PS: Se você não criou uma conta no site https://{domain}, por favor ignore esse email.
diff --git a/service/pixelated/resources/backup_account_resource.py b/service/pixelated/resources/backup_account_resource.py
index ec3e9dee..94129122 100644
--- a/service/pixelated/resources/backup_account_resource.py
+++ b/service/pixelated/resources/backup_account_resource.py
@@ -25,6 +25,7 @@ from twisted.web.template import Element, XMLFile, renderElement
from pixelated.resources import BaseResource
from pixelated.resources import get_protected_static_folder
from pixelated.account_recovery import AccountRecovery
+from pixelated.support.language import parse_accept_language
class BackupAccountPage(Element):
@@ -56,7 +57,8 @@ class BackupAccountResource(BaseResource):
self.soledad(request),
self._service(request, '_leap_session').smtp_config,
self._get_backup_email(request),
- self._leap_provider.server_name)
+ self._leap_provider.server_name,
+ language=self._get_language(request))
def update_response(response):
request.setResponseCode(NO_CONTENT)
@@ -72,3 +74,6 @@ class BackupAccountResource(BaseResource):
def _get_backup_email(self, request):
return json.loads(request.content.getvalue()).get('backupEmail')
+
+ def _get_language(self, request):
+ return parse_accept_language(request.getAllHeaders())
diff --git a/service/pixelated/resources/login_resource.py b/service/pixelated/resources/login_resource.py
index 45942ea6..5b0b70d0 100644
--- a/service/pixelated/resources/login_resource.py
+++ b/service/pixelated/resources/login_resource.py
@@ -22,6 +22,8 @@ from pixelated.config.leap import BootstrapUserServices
from pixelated.resources import BaseResource, UnAuthorizedResource, IPixelatedSession
from pixelated.resources.account_recovery_resource import AccountRecoveryResource
from pixelated.resources import get_public_static_folder, respond_json
+from pixelated.support.language import parse_accept_language
+
from twisted.cred.error import UnauthorizedLogin
from twisted.internet import defer
from twisted.logger import Logger
@@ -36,15 +38,6 @@ from twisted.web.template import Element, XMLFile, renderElement, renderer
log = Logger()
-def parse_accept_language(all_headers):
- accepted_languages = ['pt-BR', 'en-US']
- languages = all_headers.get('accept-language', '').split(';')[0]
- for language in accepted_languages:
- if language in languages:
- return language
- return 'pt-BR'
-
-
class DisclaimerElement(Element):
loader = XMLFile(FilePath(os.path.join(get_public_static_folder(), '_login_disclaimer_banner.html')))
diff --git a/service/pixelated/support/language.py b/service/pixelated/support/language.py
new file mode 100644
index 00000000..cd455f89
--- /dev/null
+++ b/service/pixelated/support/language.py
@@ -0,0 +1,24 @@
+#
+# Copyright (c) 2017 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/>.
+
+
+def parse_accept_language(all_headers):
+ accepted_languages = ['pt-BR', 'en-US']
+ languages = all_headers.get('accept-language', '').split(';')[0]
+ for language in accepted_languages:
+ if language in languages:
+ return language
+ return 'en-US'
diff --git a/service/test/unit/resources/test_backup_account_resource.py b/service/test/unit/resources/test_backup_account_resource.py
index 220e3909..e16fa0e1 100644
--- a/service/test/unit/resources/test_backup_account_resource.py
+++ b/service/test/unit/resources/test_backup_account_resource.py
@@ -44,8 +44,10 @@ class TestBackupAccountResource(unittest.TestCase):
d.addCallback(assert_200_when_user_logged_in)
return d
+ @patch('pixelated.resources.backup_account_resource.parse_accept_language')
@patch('pixelated.resources.backup_account_resource.AccountRecovery')
- def test_post_updates_recovery_code(self, mock_account_recovery_init):
+ def test_post_updates_recovery_code(self, mock_account_recovery_init, mock_language):
+ mock_language.return_value = 'pt-BR'
mock_account_recovery = MagicMock()
mock_account_recovery_init.return_value = mock_account_recovery
mock_account_recovery.update_recovery_code.return_value = defer.succeed("Success")
@@ -61,7 +63,8 @@ class TestBackupAccountResource(unittest.TestCase):
self.resource.soledad(request),
self.resource._service(request, '_leap_session').smtp_config,
self.resource._get_backup_email(request),
- self.leap_provider.server_name)
+ self.leap_provider.server_name,
+ language='pt-BR')
mock_account_recovery.update_recovery_code.assert_called()
d.addCallback(assert_update_recovery_code_called)
diff --git a/service/test/unit/resources/test_login_resource.py b/service/test/unit/resources/test_login_resource.py
index eaaba1d4..03d61758 100644
--- a/service/test/unit/resources/test_login_resource.py
+++ b/service/test/unit/resources/test_login_resource.py
@@ -24,30 +24,9 @@ from twisted.trial import unittest
from twisted.web.test.requesthelper import DummyRequest
from pixelated.resources.login_resource import LoginResource, LoginStatusResource
-from pixelated.resources.login_resource import parse_accept_language
from test.unit.resources import DummySite
-class TestParseAcceptLanguage(unittest.TestCase):
- def test_parse_pt_br_simple(self):
- all_headers = {
- 'accept-language': 'pt-BR,pt;q=0.8,en-US;q=0.5,en;q=0.3'}
- parsed_language = parse_accept_language(all_headers)
- self.assertEqual('pt-BR', parsed_language)
-
- def test_parse_en_us_simple(self):
- all_headers = {
- 'accept-language': 'en-US,en;q=0.8,en-US;q=0.5,en;q=0.3'}
- parsed_language = parse_accept_language(all_headers)
- self.assertEqual('en-US', parsed_language)
-
- def test_parse_pt_br_as_default(self):
- all_headers = {
- 'accept-language': 'de-DE,de;q=0.8,en-US;q=0.5,en;q=0.3'}
- parsed_language = parse_accept_language(all_headers)
- self.assertEqual('pt-BR', parsed_language)
-
-
class TestLoginResource(unittest.TestCase):
def setUp(self):
self.services_factory = mock()
@@ -252,7 +231,7 @@ class TestLoginPOST(unittest.TestCase):
d = self.web.get(self.request)
def assert_login_setup_service_for_user(_):
- mock_user_bootstrap_setup.assert_called_once_with(self.user_auth, self.password, 'pt-BR')
+ mock_user_bootstrap_setup.assert_called_once_with(self.user_auth, self.password, 'en-US')
d.addCallback(assert_login_setup_service_for_user)
return d
diff --git a/service/test/unit/support/test_language.py b/service/test/unit/support/test_language.py
new file mode 100644
index 00000000..b84f3a23
--- /dev/null
+++ b/service/test/unit/support/test_language.py
@@ -0,0 +1,40 @@
+#
+# Copyright (c) 2017 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/>.
+
+
+from twisted.trial import unittest
+
+from pixelated.support.language import parse_accept_language
+
+
+class TestParseAcceptLanguage(unittest.TestCase):
+ def test_parse_pt_br_simple(self):
+ all_headers = {
+ 'accept-language': 'pt-BR,pt;q=0.8,en-US;q=0.5,en;q=0.3'}
+ parsed_language = parse_accept_language(all_headers)
+ self.assertEqual('pt-BR', parsed_language)
+
+ def test_parse_en_us_simple(self):
+ all_headers = {
+ 'accept-language': 'en-US,en;q=0.8,en-US;q=0.5,en;q=0.3'}
+ parsed_language = parse_accept_language(all_headers)
+ self.assertEqual('en-US', parsed_language)
+
+ def test_parse_en_us_as_default(self):
+ all_headers = {
+ 'accept-language': 'de-DE,de;q=0.8,en-US;q=0.5,en;q=0.3'}
+ parsed_language = parse_accept_language(all_headers)
+ self.assertEqual('en-US', parsed_language)
diff --git a/service/test/unit/test_account_recovery.py b/service/test/unit/test_account_recovery.py
index b0edc466..4dc9621f 100644
--- a/service/test/unit/test_account_recovery.py
+++ b/service/test/unit/test_account_recovery.py
@@ -50,25 +50,58 @@ class AccountRecoveryTest(unittest.TestCase):
yield self.account_recovery.update_recovery_code()
self.mock_bonafide_session.update_recovery_code.assert_called_once_with(self.generated_code)
+ @defer.inlineCallbacks
+ def test_creates_recovery_code(self):
+ when(self.account_recovery)._send_mail(ANY).thenReturn(defer.succeed(None))
+ yield self.account_recovery.update_recovery_code()
+ self.mock_soledad.create_recovery_code.assert_called_once()
+
+ @patch('pixelated.account_recovery.smtp.sendmail')
+ @patch('pixelated.account_recovery.pkg_resources.resource_filename')
+ @defer.inlineCallbacks
+ def test_default_email_template(self, mock_resource, mock_sendmail):
+ mock_sendmail.return_value = defer.succeed(None)
+
+ with patch('pixelated.account_recovery.open', mock_open(read_data=''), create=True):
+ yield self.account_recovery.update_recovery_code()
+ mock_resource.assert_called_once_with('pixelated.assets',
+ 'recovery.mail.en-US')
+
+ @patch('pixelated.account_recovery.smtp.sendmail')
+ @patch('pixelated.account_recovery.pkg_resources.resource_filename')
+ @defer.inlineCallbacks
+ def test_portuguese_email_template(self, mock_resource, mock_sendmail):
+ self.account_recovery = AccountRecovery(
+ self.mock_bonafide_session,
+ self.mock_soledad,
+ self.mock_smtp_config,
+ self.backup_email,
+ self.domain,
+ language='pt-BR')
+ mock_sendmail.return_value = defer.succeed(None)
+
+ with patch('pixelated.account_recovery.open', mock_open(read_data=''), create=True):
+ yield self.account_recovery.update_recovery_code()
+ mock_resource.assert_called_once_with('pixelated.assets',
+ 'recovery.mail.pt-BR')
+
+ @patch('pixelated.account_recovery.formatdate')
@patch('pixelated.account_recovery.smtp.sendmail')
@patch('pixelated.account_recovery.pkg_resources.resource_filename')
@defer.inlineCallbacks
- def test_send_recovery_code_by_email(self, mock_resource, mock_sendmail):
+ def test_send_recovery_code_by_email(self, mock_resource, mock_sendmail, mock_formatdate):
mock_sendmail.return_value = defer.succeed(None)
+ mock_formatdate.return_value = 'Sat, 21 Mar 2015 19:30:09 -0300'
sender = 'team@{}'.format(self.domain)
- mock_file_content = '{domain}, {recovery_code}, {account_recovery_url}'
- recovery_code_email = 'test.com, 4645a2f8997e5d0d, test.com/account-recovery'
- msg = MIMEText(recovery_code_email)
- msg['Subject'] = 'Recovery Code'
- msg['From'] = sender
- msg['To'] = self.backup_email
+ mock_file_content = '{backup_email}, {sender}, {date}, {domain}, {recovery_code}'
+ recovery_code_email = '\ntest@test.com, team@test.com, Sat, 21 Mar 2015 19:30:09 -0300, test.com, 34363435613266383939376535643064'
with patch('pixelated.account_recovery.open', mock_open(read_data=mock_file_content), create=True):
- yield self.account_recovery._send_mail(self.generated_code, self.backup_email)
+ yield self.account_recovery.update_recovery_code()
mock_sendmail.assert_called_with(
self.mock_smtp_config.remote_smtp_host,
sender,
[self.backup_email],
- msg.as_string())
+ recovery_code_email)