summaryrefslogtreecommitdiff
path: root/service/pixelated
diff options
context:
space:
mode:
Diffstat (limited to 'service/pixelated')
-rw-r--r--service/pixelated/account_recovery.py56
-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/account_recovery_resource.py87
-rw-r--r--service/pixelated/resources/backup_account_resource.py25
-rw-r--r--service/pixelated/resources/login_resource.py14
-rw-r--r--service/pixelated/resources/root_resource.py4
-rw-r--r--service/pixelated/support/language.py24
8 files changed, 245 insertions, 19 deletions
diff --git a/service/pixelated/account_recovery.py b/service/pixelated/account_recovery.py
index 234bb1fe..58242a7d 100644
--- a/service/pixelated/account_recovery.py
+++ b/service/pixelated/account_recovery.py
@@ -13,23 +13,73 @@
#
# 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 pkg_resources
+import binascii
+from email import message_from_string
+
from twisted.internet.defer import inlineCallbacks, returnValue
from twisted.logger import Logger
+from twisted.mail import smtp
+
+from pixelated.support import date
log = Logger()
class AccountRecovery(object):
- def __init__(self, session, soledad):
+ 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):
+ log.info('Updating user\'s recovery code')
+
try:
code = self._soledad.create_recovery_code()
response = yield self._bonafide_session.update_recovery_code(code)
+ yield self._send_mail(code, self._backup_email)
+
returnValue(response)
+
+ except Exception as e:
+ log.error('Something went wrong when trying to save the recovery code')
+ log.error(e)
+ raise e
+
+ @inlineCallbacks
+ def _send_mail(self, code, backup_email):
+ log.info('Sending mail containing the user\'s recovery code')
+
+ sender = 'team@{}'.format(self._domain)
+ msg = self._get_recovery_mail(code, sender, backup_email)
+
+ try:
+ send_mail_result = yield smtp.sendmail(
+ str(self._smtp_config.remote_smtp_host),
+ sender,
+ [backup_email],
+ msg.as_string())
+ returnValue(send_mail_result)
except Exception as e:
- log.warn('Something went wrong when trying to save the recovery code')
- raise
+ log.error('Failed trying to send the email with the recovery code')
+ raise e
+
+ def _get_recovery_mail(self, code, sender, backup_email):
+ email_date = date.mail_date_now()
+ recovery_mail = pkg_resources.resource_filename(
+ 'pixelated.assets',
+ 'recovery.mail.%s' % (self._language))
+
+ with open(recovery_mail) as mail_template_file:
+ return message_from_string(mail_template_file.read().format(
+ domain=self._domain,
+ recovery_code=binascii.hexlify(code),
+ backup_email=backup_email,
+ sender=sender,
+ date=email_date))
diff --git a/service/pixelated/assets/recovery.mail.en-US b/service/pixelated/assets/recovery.mail.en-US
new file mode 100644
index 00000000..e7a09f1b
--- /dev/null
+++ b/service/pixelated/assets/recovery.mail.en-US
@@ -0,0 +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 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}
+
+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 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 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/account_recovery_resource.py b/service/pixelated/resources/account_recovery_resource.py
new file mode 100644
index 00000000..209a7693
--- /dev/null
+++ b/service/pixelated/resources/account_recovery_resource.py
@@ -0,0 +1,87 @@
+#
+# 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/>.
+
+import os
+import json
+
+from twisted.python.filepath import FilePath
+from twisted.web.http import OK, INTERNAL_SERVER_ERROR
+from twisted.web.template import Element, XMLFile, renderElement
+from twisted.web.server import NOT_DONE_YET
+from twisted.internet import defer
+from twisted.logger import Logger
+
+from pixelated.resources import BaseResource
+from pixelated.resources import get_public_static_folder
+
+log = Logger()
+
+
+class InvalidPasswordError(Exception):
+ pass
+
+
+class AccountRecoveryPage(Element):
+ loader = XMLFile(FilePath(os.path.join(get_public_static_folder(), 'account_recovery.html')))
+
+ def __init__(self):
+ super(AccountRecoveryPage, self).__init__()
+
+
+class AccountRecoveryResource(BaseResource):
+ BASE_URL = 'account-recovery'
+ isLeaf = True
+
+ def __init__(self, services_factory):
+ BaseResource.__init__(self, services_factory)
+
+ def render_GET(self, request):
+ request.setResponseCode(OK)
+ return self._render_template(request)
+
+ def _render_template(self, request):
+ site = AccountRecoveryPage()
+ return renderElement(request, site)
+
+ def render_POST(self, request):
+ def success_response(response):
+ request.setResponseCode(OK)
+ request.finish()
+
+ def error_response(failure):
+ log.warn(failure)
+ request.setResponseCode(INTERNAL_SERVER_ERROR)
+ request.finish()
+
+ d = self._handle_post(request)
+ d.addCallbacks(success_response, error_response)
+ return NOT_DONE_YET
+
+ def _get_post_form(self, request):
+ return json.loads(request.content.getvalue())
+
+ def _validate_password(self, password, confirm_password):
+ return password == confirm_password and len(password) >= 8 and len(password) <= 9999
+
+ def _handle_post(self, request):
+ form = self._get_post_form(request)
+ password = form.get('password')
+ confirm_password = form.get('confirmPassword')
+
+ if not self._validate_password(password, confirm_password):
+ return defer.fail(InvalidPasswordError('The user entered an invalid password or confirmation'))
+
+ return defer.succeed('Done!')
diff --git a/service/pixelated/resources/backup_account_resource.py b/service/pixelated/resources/backup_account_resource.py
index b752b4c7..94129122 100644
--- a/service/pixelated/resources/backup_account_resource.py
+++ b/service/pixelated/resources/backup_account_resource.py
@@ -15,16 +15,18 @@
# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
import os
-from xml.sax import SAXParseException
+import json
-from pixelated.resources import BaseResource
from twisted.python.filepath import FilePath
-from pixelated.resources import get_protected_static_folder
-from pixelated.account_recovery import AccountRecovery
from twisted.web.http import OK, NO_CONTENT, INTERNAL_SERVER_ERROR
from twisted.web.server import NOT_DONE_YET
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):
loader = XMLFile(FilePath(os.path.join(get_protected_static_folder(), 'backup_account.html')))
@@ -36,9 +38,10 @@ class BackupAccountPage(Element):
class BackupAccountResource(BaseResource):
isLeaf = True
- def __init__(self, services_factory, authenticator):
+ def __init__(self, services_factory, authenticator, leap_provider):
BaseResource.__init__(self, services_factory)
self._authenticator = authenticator
+ self._leap_provider = leap_provider
def render_GET(self, request):
request.setResponseCode(OK)
@@ -51,7 +54,11 @@ class BackupAccountResource(BaseResource):
def render_POST(self, request):
account_recovery = AccountRecovery(
self._authenticator.bonafide_session,
- self.soledad(request))
+ self.soledad(request),
+ self._service(request, '_leap_session').smtp_config,
+ self._get_backup_email(request),
+ self._leap_provider.server_name,
+ language=self._get_language(request))
def update_response(response):
request.setResponseCode(NO_CONTENT)
@@ -64,3 +71,9 @@ class BackupAccountResource(BaseResource):
d = account_recovery.update_recovery_code()
d.addCallbacks(update_response, error_response)
return NOT_DONE_YET
+
+ 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 3e1200d7..5b0b70d0 100644
--- a/service/pixelated/resources/login_resource.py
+++ b/service/pixelated/resources/login_resource.py
@@ -20,7 +20,10 @@ from xml.sax import SAXParseException
from pixelated.authentication import Authenticator
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
@@ -35,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')))
@@ -101,6 +95,8 @@ class LoginResource(BaseResource):
return self
if path == 'status':
return LoginStatusResource(self._services_factory)
+ if path == AccountRecoveryResource.BASE_URL:
+ return AccountRecoveryResource(self._services_factory)
if not self.is_logged_in(request):
return UnAuthorizedResource()
return NoResource()
diff --git a/service/pixelated/resources/root_resource.py b/service/pixelated/resources/root_resource.py
index 10d57c6f..896bc24b 100644
--- a/service/pixelated/resources/root_resource.py
+++ b/service/pixelated/resources/root_resource.py
@@ -23,6 +23,7 @@ from pixelated.resources import BaseResource, UnAuthorizedResource, UnavailableR
from pixelated.resources import get_public_static_folder, get_protected_static_folder
from pixelated.resources.attachments_resource import AttachmentsResource
from pixelated.resources.sandbox_resource import SandboxResource
+from pixelated.resources.account_recovery_resource import AccountRecoveryResource
from pixelated.resources.backup_account_resource import BackupAccountResource
from pixelated.resources.contacts_resource import ContactsResource
from pixelated.resources.features_resource import FeaturesResource
@@ -91,7 +92,8 @@ class RootResource(BaseResource):
def initialize(self, provider=None, disclaimer_banner=None, authenticator=None):
self._child_resources.add('assets', File(self._protected_static_folder))
- self._child_resources.add('backup-account', BackupAccountResource(self._services_factory, authenticator))
+ self._child_resources.add(AccountRecoveryResource.BASE_URL, AccountRecoveryResource(self._services_factory))
+ self._child_resources.add('backup-account', BackupAccountResource(self._services_factory, authenticator, provider))
self._child_resources.add('sandbox', SandboxResource(self._protected_static_folder))
self._child_resources.add('keys', KeysResource(self._services_factory))
self._child_resources.add(AttachmentsResource.BASE_URL, AttachmentsResource(self._services_factory))
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'