summaryrefslogtreecommitdiff
path: root/src/pixelated
diff options
context:
space:
mode:
Diffstat (limited to 'src/pixelated')
-rw-r--r--src/pixelated/__init__.py0
-rw-r--r--src/pixelated/adapter/__init__.py15
-rw-r--r--src/pixelated/adapter/listeners/__init__.py15
-rw-r--r--src/pixelated/adapter/listeners/mailbox_indexer_listener.py69
-rw-r--r--src/pixelated/adapter/mailstore/__init__.py20
-rw-r--r--src/pixelated/adapter/mailstore/body_parser.py68
-rw-r--r--src/pixelated/adapter/mailstore/leap_attachment_store.py67
-rw-r--r--src/pixelated/adapter/mailstore/leap_mailstore.py429
-rw-r--r--src/pixelated/adapter/mailstore/mailstore.py61
-rw-r--r--src/pixelated/adapter/mailstore/maintenance/__init__.py104
-rw-r--r--src/pixelated/adapter/mailstore/searchable_mailstore.py82
-rw-r--r--src/pixelated/adapter/model/__init__.py15
-rw-r--r--src/pixelated/adapter/model/mail.py235
-rw-r--r--src/pixelated/adapter/model/status.py43
-rw-r--r--src/pixelated/adapter/model/tag.py73
-rw-r--r--src/pixelated/adapter/search/__init__.py218
-rw-r--r--src/pixelated/adapter/search/contacts.py56
-rw-r--r--src/pixelated/adapter/search/index_storage_key.py42
-rw-r--r--src/pixelated/adapter/services/__init__.py15
-rw-r--r--src/pixelated/adapter/services/draft_service.py40
-rw-r--r--src/pixelated/adapter/services/feedback_service.py20
-rw-r--r--src/pixelated/adapter/services/mail_sender.py102
-rw-r--r--src/pixelated/adapter/services/mail_service.py152
-rw-r--r--src/pixelated/adapter/services/tag_service.py24
-rw-r--r--src/pixelated/adapter/welcome_mail.py29
-rw-r--r--src/pixelated/application.py222
-rw-r--r--src/pixelated/assets/Interstitial.html18
-rw-r--r--src/pixelated/assets/Interstitial.js58
-rw-r--r--src/pixelated/assets/__init__.py0
-rw-r--r--src/pixelated/assets/_login_disclaimer_banner.html9
-rw-r--r--src/pixelated/assets/favicon.pngbin0 -> 592 bytes
-rw-r--r--src/pixelated/assets/hive-bg.pngbin0 -> 3356 bytes
-rw-r--r--src/pixelated/assets/index.html9
-rw-r--r--src/pixelated/assets/jquery-2.1.3.min.js4
-rw-r--r--src/pixelated/assets/login.html36
-rw-r--r--src/pixelated/assets/normalize.min.css1
-rw-r--r--src/pixelated/assets/opensans.css69
-rw-r--r--src/pixelated/assets/pixelated-logo-orange.svg29
-rw-r--r--src/pixelated/assets/pixelated.css128
-rw-r--r--src/pixelated/assets/snap.svg-min.js20
-rw-r--r--src/pixelated/assets/welcome.mail72
-rw-r--r--src/pixelated/bitmask_libraries/__init__.py0
-rw-r--r--src/pixelated/bitmask_libraries/certs.py56
-rw-r--r--src/pixelated/bitmask_libraries/config.py46
-rw-r--r--src/pixelated/bitmask_libraries/nicknym.py63
-rw-r--r--src/pixelated/bitmask_libraries/provider.py171
-rw-r--r--src/pixelated/bitmask_libraries/session.py320
-rw-r--r--src/pixelated/bitmask_libraries/smtp.py24
-rw-r--r--src/pixelated/bitmask_libraries/soledad.py48
-rw-r--r--src/pixelated/certificates/__init__.py0
-rw-r--r--src/pixelated/certificates/unstable.pixelated-project.org.ca.crt33
-rw-r--r--src/pixelated/config/__init__.py0
-rw-r--r--src/pixelated/config/arguments.py98
-rw-r--r--src/pixelated/config/credentials.py54
-rw-r--r--src/pixelated/config/leap.py92
-rw-r--r--src/pixelated/config/logger.py37
-rw-r--r--src/pixelated/config/services.py84
-rw-r--r--src/pixelated/config/site.py32
-rw-r--r--src/pixelated/extensions/__init__.py0
-rw-r--r--src/pixelated/extensions/esmtp_sender_factory.py28
-rw-r--r--src/pixelated/extensions/protobuf_socket.py37
-rw-r--r--src/pixelated/extensions/requests_urllib3.py84
-rw-r--r--src/pixelated/extensions/shared_db.py16
-rw-r--r--src/pixelated/extensions/sqlcipher_wal.py24
-rw-r--r--src/pixelated/maintenance.py325
-rw-r--r--src/pixelated/register.py88
-rw-r--r--src/pixelated/resources/__init__.py118
-rw-r--r--src/pixelated/resources/attachments_resource.py113
-rw-r--r--src/pixelated/resources/auth.py150
-rw-r--r--src/pixelated/resources/contacts_resource.py44
-rw-r--r--src/pixelated/resources/features_resource.py52
-rw-r--r--src/pixelated/resources/feedback_resource.py31
-rw-r--r--src/pixelated/resources/keys_resource.py30
-rw-r--r--src/pixelated/resources/login_resource.py182
-rw-r--r--src/pixelated/resources/logout_resource.py45
-rw-r--r--src/pixelated/resources/mail_resource.py79
-rw-r--r--src/pixelated/resources/mails_resource.py239
-rw-r--r--src/pixelated/resources/root_resource.py150
-rw-r--r--src/pixelated/resources/sandbox_resource.py37
-rw-r--r--src/pixelated/resources/session.py39
-rw-r--r--src/pixelated/resources/tags_resource.py39
-rw-r--r--src/pixelated/resources/user_settings_resource.py45
-rw-r--r--src/pixelated/support/__init__.py81
-rw-r--r--src/pixelated/support/date.py29
-rw-r--r--src/pixelated/support/encrypted_file_storage.py153
-rw-r--r--src/pixelated/support/error_handler.py27
-rw-r--r--src/pixelated/support/functional.py37
-rw-r--r--src/pixelated/support/mail_generator.py157
-rw-r--r--src/pixelated/support/markov.py94
-rw-r--r--src/pixelated/support/replier.py35
-rw-r--r--src/pixelated/support/tls_adapter.py47
91 files changed, 6482 insertions, 0 deletions
diff --git a/src/pixelated/__init__.py b/src/pixelated/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/src/pixelated/__init__.py
diff --git a/src/pixelated/adapter/__init__.py b/src/pixelated/adapter/__init__.py
new file mode 100644
index 00000000..2756a319
--- /dev/null
+++ b/src/pixelated/adapter/__init__.py
@@ -0,0 +1,15 @@
+#
+# 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/>.
diff --git a/src/pixelated/adapter/listeners/__init__.py b/src/pixelated/adapter/listeners/__init__.py
new file mode 100644
index 00000000..2756a319
--- /dev/null
+++ b/src/pixelated/adapter/listeners/__init__.py
@@ -0,0 +1,15 @@
+#
+# 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/>.
diff --git a/src/pixelated/adapter/listeners/mailbox_indexer_listener.py b/src/pixelated/adapter/listeners/mailbox_indexer_listener.py
new file mode 100644
index 00000000..bde3b25f
--- /dev/null
+++ b/src/pixelated/adapter/listeners/mailbox_indexer_listener.py
@@ -0,0 +1,69 @@
+#
+# 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 PCULAR 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 logging
+from twisted.internet import defer
+
+
+logger = logging.getLogger(__name__)
+
+
+class MailboxIndexerListener(object):
+ """ Listens for new mails, keeping the index updated """
+
+ @classmethod
+ @defer.inlineCallbacks
+ def listen(cls, account, mailbox_name, mail_store, search_engine):
+ listener = MailboxIndexerListener(
+ mailbox_name, mail_store, search_engine)
+ mail_collection = yield account.get_collection_by_mailbox(mailbox_name)
+ mail_collection.addListener(listener)
+
+ defer.returnValue(listener)
+
+ def __init__(self, mailbox_name, mail_store, search_engine):
+ self.mailbox_name = mailbox_name
+ self.mail_store = mail_store
+ self.search_engine = search_engine
+
+ @defer.inlineCallbacks
+ def notify_new(self):
+ try:
+ indexed_idents = set(self.search_engine.search(
+ 'tag:' + self.mailbox_name.lower(), all_mails=True))
+ soledad_idents = yield self.mail_store.get_mailbox_mail_ids(self.mailbox_name)
+ soledad_idents = set(soledad_idents)
+
+ missing_idents = soledad_idents.difference(indexed_idents)
+
+ self.search_engine.index_mails((yield self.mail_store.get_mails(missing_idents, include_body=True)))
+ except Exception, e: # this is a event handler, don't let exceptions escape
+ logger.error(e)
+
+ def __eq__(self, other):
+ return other and other.mailbox_name == self.mailbox_name
+
+ def __hash__(self):
+ return self.mailbox_name.__hash__()
+
+ def __repr__(self):
+ return 'MailboxListener: ' + self.mailbox_name
+
+
+@defer.inlineCallbacks
+def listen_all_mailboxes(account, search_engine, mail_store):
+ mailboxes = yield account.list_all_mailbox_names()
+ for mailbox_name in mailboxes:
+ yield MailboxIndexerListener.listen(account, mailbox_name, mail_store, search_engine)
diff --git a/src/pixelated/adapter/mailstore/__init__.py b/src/pixelated/adapter/mailstore/__init__.py
new file mode 100644
index 00000000..978df45d
--- /dev/null
+++ b/src/pixelated/adapter/mailstore/__init__.py
@@ -0,0 +1,20 @@
+#
+# Copyright (c) 2015 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 pixelated.adapter.mailstore.mailstore import MailStore, underscore_uuid
+from pixelated.adapter.mailstore.leap_mailstore import LeapMailStore
+
+__all__ = ['MailStore', 'LeapMailStore', 'underscore_uuid']
diff --git a/src/pixelated/adapter/mailstore/body_parser.py b/src/pixelated/adapter/mailstore/body_parser.py
new file mode 100644
index 00000000..25e0c29a
--- /dev/null
+++ b/src/pixelated/adapter/mailstore/body_parser.py
@@ -0,0 +1,68 @@
+#
+# Copyright (c) 2015 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 email.parser import Parser
+import re
+import logging
+
+logger = logging.getLogger(__name__)
+
+
+def _parse_charset_header(content_type_and_charset_header, default_charset='us-ascii'):
+ try:
+ return re.compile('.*charset="?([a-zA-Z0-9-]+)"?', re.MULTILINE | re.DOTALL).match(content_type_and_charset_header).group(1)
+ except:
+ return default_charset
+
+
+class BodyParser(object):
+
+ def __init__(self, content, content_type='text/plain; charset="us-ascii"', content_transfer_encoding=None):
+ self._content = content
+ self._content_type = content_type
+ self._content_transfer_encoding = content_transfer_encoding
+
+ def parsed_content(self):
+ charset = _parse_charset_header(self._content_type)
+ text = self._serialize_for_parser(charset)
+
+ decoded_body = self._parse_and_decode(text)
+ return unicode(decoded_body, charset, errors='replace')
+
+ def _parse_and_decode(self, text):
+ parsed_body = Parser().parsestr(text)
+ decoded_body = self._unwrap_content_transfer_encoding(parsed_body)
+ return decoded_body
+
+ def _unwrap_content_transfer_encoding(self, parsed_body):
+ return parsed_body.get_payload(decode=True)
+
+ def _serialize_for_parser(self, charset):
+ text = u'Content-Type: %s\n' % self._content_type
+ if self._content_transfer_encoding is not None:
+ text += u'Content-Transfer-Encoding: %s\n' % self._content_transfer_encoding
+
+ text += u'\n'
+ encoded_text = text.encode(charset)
+ if isinstance(self._content, unicode):
+ try:
+ return encoded_text + self._content.encode(charset)
+ except UnicodeError, e:
+ logger.warn(
+ 'Failed to encode content for charset %s. Ignoring invalid chars: %s' % (charset, e))
+ return encoded_text + self._content.encode(charset, 'ignore')
+ else:
+ return encoded_text + self._content
diff --git a/src/pixelated/adapter/mailstore/leap_attachment_store.py b/src/pixelated/adapter/mailstore/leap_attachment_store.py
new file mode 100644
index 00000000..81893675
--- /dev/null
+++ b/src/pixelated/adapter/mailstore/leap_attachment_store.py
@@ -0,0 +1,67 @@
+
+import quopri
+import base64
+from email import encoders
+from leap.mail.adaptors.soledad import SoledadMailAdaptor, ContentDocWrapper
+from twisted.internet import defer
+from email.mime.nonmultipart import MIMENonMultipart
+from email.mime.multipart import MIMEMultipart
+from leap.mail.mail import Message
+
+
+class LeapAttachmentStore(object):
+
+ def __init__(self, soledad):
+ self.soledad = soledad
+
+ @defer.inlineCallbacks
+ def get_mail_attachment(self, attachment_id):
+ results = yield self.soledad.get_from_index('by-type-and-payloadhash', 'cnt', attachment_id) if attachment_id else []
+ if results:
+ content = ContentDocWrapper(**results[0].content)
+ defer.returnValue({'content-type': content.content_type, 'content': self._try_decode(
+ content.raw, content.content_transfer_encoding)})
+ else:
+ raise ValueError('No attachment with id %s found!' % attachment_id)
+
+ @defer.inlineCallbacks
+ def add_attachment(self, content, content_type):
+ cdoc = self._attachment_to_cdoc(content, content_type)
+ attachment_id = cdoc.phash
+ try:
+ yield self.get_mail_attachment(attachment_id)
+ except ValueError:
+ yield self.soledad.create_doc(cdoc.serialize(), doc_id=attachment_id)
+ defer.returnValue(attachment_id)
+
+ def _try_decode(self, raw, encoding):
+ encoding = encoding.lower()
+ if encoding == 'base64':
+ data = base64.decodestring(raw)
+ elif encoding == 'quoted-printable':
+ data = quopri.decodestring(raw)
+ else:
+ data = str(raw)
+
+ return bytearray(data)
+
+ def _attachment_to_cdoc(self, content, content_type, encoder=encoders.encode_base64):
+ major, sub = content_type.split('/')
+ attachment = MIMENonMultipart(major, sub)
+ attachment.set_payload(content)
+ encoder(attachment)
+ attachment.add_header('Content-Disposition',
+ 'attachment', filename='does_not_matter.txt')
+
+ pseudo_mail = MIMEMultipart()
+ pseudo_mail.attach(attachment)
+
+ tmp_mail = SoledadMailAdaptor().get_msg_from_string(
+ MessageClass=Message, raw_msg=pseudo_mail.as_string())
+
+ cdoc = tmp_mail.get_wrapper().cdocs[1]
+ return cdoc
+
+ def _calc_attachment_id_(self, content, content_type, encoder=encoders.encode_base64):
+ cdoc = self._attachment_to_cdoc(content, content_type, encoder)
+ return cdoc.phash
diff --git a/src/pixelated/adapter/mailstore/leap_mailstore.py b/src/pixelated/adapter/mailstore/leap_mailstore.py
new file mode 100644
index 00000000..e8f0c2a6
--- /dev/null
+++ b/src/pixelated/adapter/mailstore/leap_mailstore.py
@@ -0,0 +1,429 @@
+#
+# Copyright (c) 2015 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 re
+from email.header import decode_header
+from uuid import uuid4
+
+from leap.mail.adaptors.soledad import SoledadMailAdaptor
+from leap.mail.mail import Message
+from twisted.internet import defer
+from twisted.internet.defer import FirstError, DeferredList
+
+from pixelated.adapter.mailstore.body_parser import BodyParser
+from pixelated.adapter.mailstore.mailstore import MailStore, underscore_uuid
+from pixelated.adapter.model.mail import Mail, InputMail
+from pixelated.support.functional import to_unicode
+from pixelated.support import date
+
+
+MIME_PGP_KEY = 'application/pgp-keys'
+
+
+class AttachmentInfo(object):
+
+ def __init__(self, ident, name, encoding=None, ctype='application/octet-stream', size=0):
+ self.ident = ident
+ self.name = name
+ self.encoding = encoding
+ self.ctype = ctype
+ self.size = size
+
+ def __repr__(self):
+ return 'AttachmentInfo[%s, %s, %s]' % (self.ident, self.name, self.encoding)
+
+ def __str__(self):
+ return 'AttachmentInfo[%s, %s, %s]' % (self.ident, self.name, self.encoding)
+
+ def as_dict(self):
+ return {'ident': self.ident, 'name': self.name, 'encoding': self.encoding, 'size': self.size, 'content-type': self.ctype}
+
+
+class LeapMail(Mail):
+
+ def __init__(self, mail_id, mailbox_name, headers=None, tags=set(), flags=set(), body=None, attachments=[]):
+ self._mail_id = mail_id
+ self._mailbox_name = mailbox_name
+ self._headers = headers if headers is not None else {}
+ self._body = to_unicode(body)
+ self.tags = set(tags) # TODO test that asserts copy
+ self._flags = set(flags) # TODO test that asserts copy
+ self._attachments = attachments
+
+ @property
+ def headers(self):
+ cpy = dict(self._headers)
+ for name in set(self._headers.keys()).intersection(['To', 'Cc', 'Bcc']):
+ cpy[name] = [address.strip() for address in (
+ self._headers[name].split(',') if self._headers[name] else [])]
+
+ return cpy
+
+ @property
+ def ident(self):
+ return self._mail_id
+
+ @property
+ def mail_id(self):
+ return self._mail_id
+
+ @property
+ def body(self):
+ return self._body
+
+ @property
+ def flags(self):
+ return self._flags
+
+ @property
+ def mailbox_name(self):
+ return self._mailbox_name
+
+ @property
+ def security_casing(self):
+ casing = dict(imprints=self._signature_information(), locks=[])
+ if self._encrypted() == "decrypted":
+ casing["locks"] = [{"state": "valid"}]
+ return casing
+
+ def _encrypted(self):
+ return self.headers.get("X-Leap-Encryption", "false")
+
+ def _signature_information(self):
+ signature = self.headers.get("X-Leap-Signature", None)
+ if signature is None or signature.startswith("could not verify"):
+ return [{"state": "no_signature_information"}]
+ else:
+ if signature.startswith("valid"):
+ return [{"state": "valid", "seal": {"validity": "valid"}}]
+ else:
+ return []
+
+ @property
+ def raw(self):
+ result = u''
+ for k, v in self._headers.items():
+ content, encoding = decode_header(v)[0]
+ if encoding:
+ result += '%s: %s\n' % (k, unicode(content, encoding=encoding))
+ else:
+ result += '%s: %s\n' % (k, v)
+ result += '\n'
+
+ if self._body:
+ result = result + self._body
+
+ return result
+
+ def _remove_duplicates(self, values):
+ return list(set(values))
+
+ def _decoded_header_utf_8(self, header_value):
+ if isinstance(header_value, list):
+ return self._remove_duplicates([self._decoded_header_utf_8(v) for v in header_value])
+ elif header_value is not None:
+ def encode_chunk(content, encoding):
+ return unicode(content.strip(), encoding=encoding or 'ascii', errors='ignore')
+
+ try:
+ encoded_chunks = [encode_chunk(
+ content, encoding) for content, encoding in decode_header(header_value)]
+ # decode_header strips whitespaces on all chunks, joining over
+ # ' ' is only a workaround, not a proper fix
+ return ' '.join(encoded_chunks)
+ except UnicodeEncodeError:
+ return unicode(header_value.encode('ascii', errors='ignore'))
+
+ def as_dict(self):
+ return {
+ 'header': {k.lower(): self._decoded_header_utf_8(v) for k, v in self.headers.items()},
+ 'ident': self._mail_id,
+ 'tags': self.tags,
+ 'status': list(self.status),
+ 'body': self._body,
+ 'security_casing': self.security_casing,
+ 'textPlainBody': self._body,
+ 'mailbox': self._mailbox_name.lower(),
+ 'attachments': [attachment.as_dict() for attachment in self._attachments]
+ }
+
+ @staticmethod
+ def from_dict(mail_dict):
+ # TODO: implement this method and also write tests for it
+ headers = {key.capitalize(): value for key,
+ value in mail_dict.get('header', {}).items()}
+ headers['Date'] = date.mail_date_now()
+ body = mail_dict.get('body', '')
+ tags = set(mail_dict.get('tags', []))
+ status = set(mail_dict.get('status', []))
+ attachments = []
+
+ # mail_id, mailbox_name, headers=None, tags=set(), flags=set(),
+ # body=None, attachments=[]
+ return LeapMail(None, None, headers, tags, set(), body, attachments)
+
+
+def _extract_filename(headers, default_filename='UNNAMED'):
+ content_disposition = headers.get('Content-Disposition', '')
+ filename = _extract_filename_from_name_header_part(content_disposition)
+ if not filename:
+ filename = headers.get('Content-Description', '')
+ if not filename:
+ content_type = headers.get('Content-Type', '')
+ filename = _extract_filename_from_name_header_part(content_type)
+
+ if not filename:
+ filename = default_filename
+
+ return filename
+
+
+def _extract_filename_from_name_header_part(header_value):
+ match = re.compile('.*name=\"?(.*[^\"\'])').search(header_value)
+ filename = ''
+ if match:
+ filename = match.group(1)
+ return filename
+
+
+class LeapMailStore(MailStore):
+ __slots__ = ('soledad')
+
+ def __init__(self, soledad):
+ self.soledad = soledad
+
+ @defer.inlineCallbacks
+ def get_mail(self, mail_id, include_body=False):
+ message = yield self._fetch_msg_from_soledad(mail_id)
+ if not _is_empty_message(message):
+ leap_mail = yield self._leap_message_to_leap_mail(mail_id, message, include_body)
+ else:
+ leap_mail = None
+
+ defer.returnValue(leap_mail)
+
+ @defer.inlineCallbacks
+ def get_mails(self, mail_ids, gracefully_ignore_errors=False, include_body=False):
+ deferreds = []
+ for mail_id in mail_ids:
+ deferreds.append(self.get_mail(mail_id, include_body=include_body))
+
+ if gracefully_ignore_errors:
+ results = yield DeferredList(deferreds, consumeErrors=True)
+ defer.returnValue(
+ [mail for ok, mail in results if ok and mail is not None])
+ else:
+ result = yield defer.gatherResults(deferreds, consumeErrors=True)
+ defer.returnValue(result)
+
+ @defer.inlineCallbacks
+ def update_mail(self, mail):
+ message = yield self._fetch_msg_from_soledad(mail.mail_id)
+ message.get_wrapper().set_tags(tuple(mail.tags))
+ message.get_wrapper().set_flags(tuple(mail.flags))
+ # TODO assert this is yielded (otherwise asynchronous)
+ yield self._update_mail(message)
+
+ @defer.inlineCallbacks
+ def all_mails(self, gracefully_ignore_errors=False):
+ mdocs = yield self.soledad.get_from_index('by-type', 'meta')
+
+ mail_ids = map(lambda doc: doc.doc_id, mdocs)
+
+ mails = yield self.get_mails(mail_ids, gracefully_ignore_errors=gracefully_ignore_errors, include_body=True)
+ defer.returnValue(mails)
+
+ @defer.inlineCallbacks
+ def add_mailbox(self, mailbox_name):
+ mailbox = yield self._get_or_create_mailbox(mailbox_name)
+ defer.returnValue(mailbox)
+
+ @defer.inlineCallbacks
+ def get_mailbox_names(self):
+ mbox_map = set((yield self._mailbox_uuid_to_name_map()).values())
+
+ defer.returnValue(mbox_map.union({'INBOX'}))
+
+ @defer.inlineCallbacks
+ def _mailbox_uuid_to_name_map(self):
+ map = {}
+ mbox_docs = yield self.soledad.get_from_index('by-type', 'mbox')
+ for doc in mbox_docs:
+ map[underscore_uuid(doc.content.get('uuid'))
+ ] = doc.content.get('mbox')
+
+ defer.returnValue(map)
+
+ @defer.inlineCallbacks
+ def add_mail(self, mailbox_name, raw_msg):
+ mailbox = yield self._get_or_create_mailbox(mailbox_name)
+ message = SoledadMailAdaptor().get_msg_from_string(Message, raw_msg)
+ message.get_wrapper().set_mbox_uuid(mailbox.uuid)
+
+ yield SoledadMailAdaptor().create_msg(self.soledad, message)
+
+ # add behavious from insert_mdoc_id from mail.py
+ # TODO test that asserts include_body
+ mail = yield self._leap_message_to_leap_mail(message.get_wrapper().mdoc.doc_id, message, include_body=True)
+ defer.returnValue(mail)
+
+ @defer.inlineCallbacks
+ def delete_mail(self, mail_id):
+ message = yield self._fetch_msg_from_soledad(mail_id)
+ if message and message.get_wrapper().mdoc.doc_id:
+ yield message.get_wrapper().delete(self.soledad)
+ defer.returnValue(True)
+ defer.returnValue(False)
+
+ @defer.inlineCallbacks
+ def get_mailbox_mail_ids(self, mailbox_name):
+ mailbox = yield self._get_or_create_mailbox(mailbox_name)
+ fdocs = yield self.soledad.get_from_index('by-type-and-mbox-uuid', 'flags', underscore_uuid(mailbox.uuid))
+
+ mail_ids = map(lambda doc: _fdoc_id_to_mdoc_id(doc.doc_id), fdocs)
+
+ defer.returnValue(mail_ids)
+
+ @defer.inlineCallbacks
+ def delete_mailbox(self, mailbox_name):
+ mbx_wrapper = yield self._get_or_create_mailbox(mailbox_name)
+ yield SoledadMailAdaptor().delete_mbox(self.soledad, mbx_wrapper)
+
+ @defer.inlineCallbacks
+ def copy_mail_to_mailbox(self, mail_id, mailbox_name):
+ message = yield self._fetch_msg_from_soledad(mail_id, load_body=True)
+ mailbox = yield self._get_or_create_mailbox(mailbox_name)
+ copy_wrapper = yield message.get_wrapper().copy(self.soledad, mailbox.uuid)
+
+ leap_message = Message(copy_wrapper)
+
+ mail = yield self._leap_message_to_leap_mail(copy_wrapper.mdoc.doc_id, leap_message, include_body=False)
+
+ defer.returnValue(mail)
+
+ @defer.inlineCallbacks
+ def move_mail_to_mailbox(self, mail_id, mailbox_name):
+ mail_copy = yield self.copy_mail_to_mailbox(mail_id, mailbox_name)
+ yield self.delete_mail(mail_id)
+ defer.returnValue(mail_copy)
+
+ def _update_mail(self, message):
+ return message.get_wrapper().update(self.soledad)
+
+ @defer.inlineCallbacks
+ def _leap_message_to_leap_mail(self, mail_id, message, include_body):
+ if include_body:
+ # TODO use body from message if available
+ body = yield self._raw_message_body(message)
+ else:
+ body = None
+
+ # fetch mailbox name by mbox_uuid
+ mbox_uuid = message.get_wrapper().fdoc.mbox_uuid
+ mbox_name = yield self._mailbox_name_from_uuid(mbox_uuid)
+ attachments = self._extract_attachment_info_from(message)
+ attachments = self._filter_public_keys_from_attachments(attachments)
+ mail = LeapMail(mail_id, mbox_name, message.get_wrapper().hdoc.headers, set(message.get_tags()), set(
+ message.get_flags()), body=body, attachments=attachments) # TODO assert flags are passed on
+
+ defer.returnValue(mail)
+
+ def _filter_public_keys_from_attachments(self, attachments):
+ return filter(lambda attachment: attachment.ctype != MIME_PGP_KEY, attachments)
+
+ @defer.inlineCallbacks
+ def _raw_message_body(self, message):
+ content_doc = (yield message.get_wrapper().get_body(self.soledad))
+ parser = BodyParser('', content_type='text/plain',
+ content_transfer_encoding='UTF-8')
+ # It fix the problem when leap doesn'r found body_phash and returns
+ # empty string
+ if not isinstance(content_doc, str):
+ parser = BodyParser(content_doc.raw, content_type=content_doc.content_type,
+ content_transfer_encoding=content_doc.content_transfer_encoding)
+
+ defer.returnValue(parser.parsed_content())
+
+ @defer.inlineCallbacks
+ def _mailbox_name_from_uuid(self, uuid):
+ map = (yield self._mailbox_uuid_to_name_map())
+ defer.returnValue(map.get(uuid, ''))
+
+ @defer.inlineCallbacks
+ def _get_or_create_mailbox(self, mailbox_name):
+ mailbox_name_upper = mailbox_name.upper()
+ mbx = yield SoledadMailAdaptor().get_or_create_mbox(self.soledad, mailbox_name_upper)
+ if mbx.uuid is None:
+ mbx.uuid = str(uuid4())
+ yield mbx.update(self.soledad)
+ defer.returnValue(mbx)
+
+ def _fetch_msg_from_soledad(self, mail_id, load_body=False):
+ return SoledadMailAdaptor().get_msg_from_mdoc_id(Message, self.soledad, mail_id, get_cdocs=load_body)
+
+ @defer.inlineCallbacks
+ def _dump_soledad(self):
+ gen, docs = yield self.soledad.get_all_docs()
+ for doc in docs:
+ print '\n%s\n' % doc
+
+ def _extract_attachment_info_from(self, message):
+ wrapper = message.get_wrapper()
+ part_maps = wrapper.hdoc.part_map
+ return self._extract_part_map(part_maps)
+
+ def _is_attachment(self, part_map, headers):
+ disposition = headers.get('Content-Disposition', None)
+ content_type = part_map['ctype']
+
+ if 'multipart' in content_type:
+ return False
+
+ if 'text/plain' == content_type and ((disposition == 'inline') or (disposition is None)):
+ return False
+
+ return True
+
+ def _create_attachment_info_from(self, part_map, headers):
+ ident = part_map['phash']
+ name = _extract_filename(headers)
+ encoding = headers.get('Content-Transfer-Encoding', None)
+ ctype = part_map.get('ctype') or headers.get('Content-Type')
+ size = part_map.get('size', 0)
+
+ return AttachmentInfo(ident, name, encoding, ctype, size)
+
+ def _extract_part_map(self, part_maps):
+ result = []
+
+ for nr, part_map in part_maps.items():
+ if 'headers' in part_map and 'phash' in part_map:
+ headers = {header[0]: header[1]
+ for header in part_map['headers']}
+ if self._is_attachment(part_map, headers):
+ result.append(
+ self._create_attachment_info_from(part_map, headers))
+ if 'part_map' in part_map:
+ result += self._extract_part_map(part_map['part_map'])
+
+ return result
+
+
+def _is_empty_message(message):
+ return (message is None) or (message.get_wrapper().mdoc.doc_id is None)
+
+
+def _fdoc_id_to_mdoc_id(fdoc_id):
+ return 'M' + fdoc_id[1:]
diff --git a/src/pixelated/adapter/mailstore/mailstore.py b/src/pixelated/adapter/mailstore/mailstore.py
new file mode 100644
index 00000000..9aba6e62
--- /dev/null
+++ b/src/pixelated/adapter/mailstore/mailstore.py
@@ -0,0 +1,61 @@
+#
+# Copyright (c) 2015 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/>.
+
+
+class MailStore(object):
+
+ def get_mail(self, mail_id):
+ pass
+
+ def get_mail_attachment(self, attachment_id):
+ pass
+
+ def get_mails(self, mail_ids, gracefully_ignore_errors=False, include_body=False):
+ pass
+
+ def all_mails(self):
+ pass
+
+ def delete_mail(self, mail_id):
+ pass
+
+ def update_mail(self, mail):
+ pass
+
+ def add_mail(self, mailbox_name, mail):
+ pass
+
+ def get_mailbox_names(self):
+ pass
+
+ def add_mailbox(self, mailbox_name):
+ pass
+
+ def delete_mailbox(self, mailbox_name):
+ pass
+
+ def get_mailbox_mail_ids(self, mailbox_name):
+ pass
+
+ def copy_mail_to_mailbox(self, mail_id, mailbox_name):
+ pass
+
+ def move_mail_to_mailbox(self, mail_id, mailbox_name):
+ pass
+
+
+def underscore_uuid(uuid):
+ return uuid.replace('-', '_')
diff --git a/src/pixelated/adapter/mailstore/maintenance/__init__.py b/src/pixelated/adapter/mailstore/maintenance/__init__.py
new file mode 100644
index 00000000..02b38a10
--- /dev/null
+++ b/src/pixelated/adapter/mailstore/maintenance/__init__.py
@@ -0,0 +1,104 @@
+#
+# Copyright (c) 2015 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 leap.keymanager.keys import KEY_TYPE_KEY, KEY_PRIVATE_KEY, KEY_FINGERPRINT_KEY, KEY_ADDRESS_KEY
+from leap.keymanager.openpgp import OpenPGPKey
+
+from twisted.internet import defer
+import logging
+
+
+TYPE_OPENPGP_KEY = 'OpenPGPKey'
+TYPE_OPENPGP_ACTIVE = 'OpenPGPKey-active'
+
+KEY_DOC_TYPES = {TYPE_OPENPGP_ACTIVE, TYPE_OPENPGP_KEY}
+
+logger = logging.getLogger(__name__)
+
+
+def _is_key_doc(doc):
+ return doc.content.get(KEY_TYPE_KEY, None) in KEY_DOC_TYPES
+
+
+def _is_private_key_doc(doc):
+ return _is_key_doc(doc) and doc.content.get(KEY_PRIVATE_KEY, False)
+
+
+def _is_active_key_doc(doc):
+ return _is_key_doc(doc) and doc.content.get(KEY_TYPE_KEY, None) == TYPE_OPENPGP_ACTIVE
+
+
+def _is_public_key(doc):
+ return _is_key_doc(doc) and not doc.content.get(KEY_PRIVATE_KEY, False)
+
+
+def _key_fingerprint(doc):
+ return doc.content.get(KEY_FINGERPRINT_KEY, None)
+
+
+def _address(doc):
+ return doc.content.get(KEY_ADDRESS_KEY, None)
+
+
+class SoledadMaintenance(object):
+
+ def __init__(self, soledad):
+ self._soledad = soledad
+
+ @defer.inlineCallbacks
+ def repair(self):
+ _, docs = yield self._soledad.get_all_docs()
+
+ private_key_fingerprints = self._key_fingerprints_with_private_key(
+ docs)
+
+ for doc in docs:
+ if _is_key_doc(doc) and _key_fingerprint(doc) not in private_key_fingerprints:
+ logger.warn('Deleting doc %s for key %s of <%s>' %
+ (doc.doc_id, _key_fingerprint(doc), _address(doc)))
+ yield self._soledad.delete_doc(doc)
+
+ yield self._repair_missing_active_docs(docs, private_key_fingerprints)
+
+ @defer.inlineCallbacks
+ def _repair_missing_active_docs(self, docs, private_key_fingerprints):
+ missing = self._missing_active_docs(docs, private_key_fingerprints)
+ for fingerprint in missing:
+ emails = self._emails_for_key_fingerprint(docs, fingerprint)
+ for email in emails:
+ logger.warn('Re-creating active doc for key %s, email %s' %
+ (fingerprint, email))
+ yield self._soledad.create_doc_from_json(OpenPGPKey(email, fingerprint=fingerprint, private=False).get_active_json())
+
+ def _key_fingerprints_with_private_key(self, docs):
+ return [doc.content[KEY_FINGERPRINT_KEY] for doc in docs if _is_private_key_doc(doc)]
+
+ def _missing_active_docs(self, docs, private_key_fingerprints):
+ active_doc_ids = self._active_docs_for_key_fingerprint(docs)
+
+ return set([private_key_fingerprint for private_key_fingerprint in private_key_fingerprints if private_key_fingerprint not in active_doc_ids])
+
+ def _emails_for_key_fingerprint(self, docs, fingerprint):
+ for doc in docs:
+ if _is_private_key_doc(doc) and _key_fingerprint(doc) == fingerprint:
+ email = _address(doc)
+ if email is None:
+ return []
+ if isinstance(email, list):
+ return email
+ return [email]
+
+ def _active_docs_for_key_fingerprint(self, docs):
+ return [doc.content[KEY_FINGERPRINT_KEY] for doc in docs if _is_active_key_doc(doc) and _is_public_key(doc)]
diff --git a/src/pixelated/adapter/mailstore/searchable_mailstore.py b/src/pixelated/adapter/mailstore/searchable_mailstore.py
new file mode 100644
index 00000000..e578e6a7
--- /dev/null
+++ b/src/pixelated/adapter/mailstore/searchable_mailstore.py
@@ -0,0 +1,82 @@
+#
+# Copyright (c) 2015 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.internet import defer
+from types import FunctionType
+from pixelated.adapter.mailstore import MailStore
+
+
+class SearchableMailStore(object): # implementes MailStore
+
+ def __init__(self, delegate, search_engine):
+ self._delegate = delegate
+ self._search_engine = search_engine
+
+ @classmethod
+ def _create_delegator(cls, method_name):
+ def delegator(self, *args, **kw):
+ return getattr(self._delegate, method_name)(*args, **kw)
+
+ setattr(cls, method_name, delegator)
+
+ @defer.inlineCallbacks
+ def add_mail(self, mailbox_name, mail):
+ stored_mail = yield self._delegate.add_mail(mailbox_name, mail)
+ self._search_engine.index_mail(stored_mail)
+ defer.returnValue(stored_mail)
+
+ @defer.inlineCallbacks
+ def delete_mail(self, mail_id):
+ removed = yield self._delegate.delete_mail(mail_id)
+ self._search_engine.remove_from_index(mail_id)
+ defer.returnValue(removed)
+
+ @defer.inlineCallbacks
+ def update_mail(self, mail):
+ yield self._delegate.update_mail(mail)
+ self._search_engine.index_mail(mail)
+
+ @defer.inlineCallbacks
+ def move_mail_to_mailbox(self, mail_id, mailbox_name):
+ moved_mail = yield self._delegate.move_mail_to_mailbox(mail_id, mailbox_name)
+ self._search_engine.remove_from_index(mail_id)
+ self._search_engine.index_mail(moved_mail)
+ defer.returnValue(moved_mail)
+
+ @defer.inlineCallbacks
+ def copy_mail_to_mailbox(self, mail_id, mailbox_name):
+ copied_mail = yield self._delegate.copy_mail_to_mailbox(mail_id, mailbox_name)
+ self._search_engine.index_mail(copied_mail)
+ defer.returnValue(copied_mail)
+
+ def delete_mailbox(self, mailbox_name):
+ raise NotImplementedError()
+
+ def __getattr__(self, name):
+ """
+ Acts like method missing. If a method of MailStore is not implemented in this class,
+ a delegate method is created.
+
+ :param name: attribute name
+ :return: method or attribute
+ """
+ methods = ([key for key, value in MailStore.__dict__.items()
+ if type(value) == FunctionType])
+
+ if name in methods:
+ SearchableMailStore._create_delegator(name)
+ return super(SearchableMailStore, self).__getattribute__(name)
+ else:
+ raise NotImplementedError('No attribute %s' % name)
diff --git a/src/pixelated/adapter/model/__init__.py b/src/pixelated/adapter/model/__init__.py
new file mode 100644
index 00000000..2756a319
--- /dev/null
+++ b/src/pixelated/adapter/model/__init__.py
@@ -0,0 +1,15 @@
+#
+# 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/>.
diff --git a/src/pixelated/adapter/model/mail.py b/src/pixelated/adapter/model/mail.py
new file mode 100644
index 00000000..7043af56
--- /dev/null
+++ b/src/pixelated/adapter/model/mail.py
@@ -0,0 +1,235 @@
+#
+# 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 PCULAR 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 re
+import logging
+from email import message_from_file
+from email.mime.text import MIMEText
+from email.header import Header
+from hashlib import sha256
+
+import binascii
+from email.MIMEMultipart import MIMEMultipart
+from email.mime.nonmultipart import MIMENonMultipart
+import leap.mail.walk as walk
+from pixelated.adapter.model.status import Status
+from pixelated.support import date
+
+
+logger = logging.getLogger(__name__)
+
+
+class Mail(object):
+
+ @property
+ def from_sender(self):
+ return self.headers['From']
+
+ @property
+ def to(self):
+ return self.headers['To']
+
+ @property
+ def cc(self):
+ return self.headers['Cc']
+
+ @property
+ def bcc(self):
+ return self.headers['Bcc']
+
+ @property
+ def subject(self):
+ return self.headers['Subject']
+
+ @property
+ def date(self):
+ return self.headers['Date']
+
+ @property
+ def status(self):
+ return Status.from_flags(self.flags)
+
+ @property
+ def flags(self):
+ return self.fdoc.content.get('flags')
+
+ @property
+ def mailbox_name(self):
+ # FIXME mbox is no longer available, instead we now have mbox_uuid
+ return self.fdoc.content.get('mbox', 'INBOX')
+
+ def _encode_header_value_list(self, header_value_list):
+ encoded_header_list = [self._encode_header_value(
+ v) for v in header_value_list]
+ return ', '.join(encoded_header_list)
+
+ def _encode_header_value(self, header_value):
+ if isinstance(header_value, unicode):
+ return str(Header(header_value, 'utf-8'))
+ return str(header_value)
+
+ def _add_message_content(self, mime_multipart, body_to_use=None):
+ body_to_use = body_to_use or self.body
+ if isinstance(body_to_use, list):
+ for part in body_to_use:
+ mime_multipart.attach(
+ MIMEText(part['raw'], part['content-type']))
+ else:
+ mime_multipart.attach(
+ MIMEText(body_to_use, 'plain', self._charset()))
+
+ def _add_body(self, mime):
+ body_to_use = getattr(self, 'body', None) or getattr(
+ self, 'text_plain_body', None)
+ self._add_message_content(mime, body_to_use)
+ self._add_attachments(mime)
+
+ def _generate_mime_multipart(self):
+ mime = MIMEMultipart()
+ self._add_headers(mime)
+ self._add_body(mime)
+ return mime
+
+ @property
+ def _mime_multipart(self):
+ self._mime = self._mime or self._generate_mime_multipart()
+ return self._mime
+
+ def _add_headers(self, mime):
+ for key, value in self.headers.items():
+ if isinstance(value, list):
+ mime[str(key)] = self._encode_header_value_list(value)
+ else:
+ mime[str(key)] = self._encode_header_value(value)
+
+ def _add_attachments(self, mime):
+ for attachment in getattr(self, '_attachments', []):
+ major, sub = attachment['content-type'].split('/')
+ attachment_mime = MIMENonMultipart(major, sub)
+ base64_attachment_file = binascii.b2a_base64(attachment['raw'])
+ attachment_mime.set_payload(base64_attachment_file)
+ attachment_mime[
+ 'Content-Disposition'] = 'attachment; filename="%s"' % attachment['name']
+ attachment_mime['Content-Transfer-Encoding'] = 'base64'
+ mime.attach(attachment_mime)
+
+ def _charset(self):
+ content_type = self.headers.get('content_type', {})
+ if 'charset' in content_type:
+ return self._parse_charset_header(content_type)
+ return 'utf-8'
+
+ def _parse_charset_header(self, charset_header, default_charset='utf-8'):
+ try:
+ return re.compile('.*charset=([a-zA-Z0-9-]+)', re.MULTILINE | re.DOTALL).match(charset_header).group(1)
+ except:
+ return default_charset
+
+ @property
+ def raw(self):
+ return self._mime_multipart.as_string()
+
+ def _get_chash(self):
+ return sha256(self.raw).hexdigest()
+
+
+class InputMail(Mail):
+
+ def __init__(self):
+ self._raw_message = None
+ self._fd = None
+ self._hd = None
+ self._bd = None
+ self._chash = None
+ self._mime = None
+ self.headers = {}
+ self.body = ''
+ self._status = []
+ self._attachments = []
+
+ @property
+ def ident(self):
+ return self._get_chash()
+
+ def _get_body_phash(self):
+ return walk.get_body_phash(self._mime_multipart)
+
+ def _add_predefined_headers(self, mime_multipart):
+ for header in ['To', 'Cc', 'Bcc']:
+ if self.headers.get(header):
+ mime_multipart[header] = ", ".join(self.headers[header])
+ for header in ['Subject', 'From']:
+ if self.headers.get(header):
+ mime_multipart[header] = self.headers[header]
+ mime_multipart['Date'] = self.headers['Date']
+
+ def to_mime_multipart(self):
+ mime = MIMEMultipart()
+ self._add_predefined_headers(mime)
+ self._add_body(mime)
+ return mime
+
+ def to_smtp_format(self):
+ mime_multipart = self.to_mime_multipart()
+ return mime_multipart.as_string()
+
+ @staticmethod
+ def delivery_error_template(delivery_address):
+ return InputMail.from_dict({
+ 'body': "Mail undelivered for %s" % delivery_address,
+ 'header': {
+ 'bcc': [],
+ 'cc': [],
+ 'subject': "Mail undelivered for %s" % delivery_address
+ }
+ })
+
+ @staticmethod
+ def from_dict(mail_dict, from_address):
+ input_mail = InputMail()
+ input_mail.headers = {
+ key.capitalize(): value for key, value in mail_dict.get('header', {}).items()}
+
+ input_mail.headers['Date'] = date.mail_date_now()
+ input_mail.headers['From'] = from_address
+
+ input_mail.body = mail_dict.get('body', '')
+ input_mail.tags = set(mail_dict.get('tags', []))
+ input_mail._status = set(mail_dict.get('status', []))
+ input_mail._attachments = mail_dict.get('attachments', [])
+ return input_mail
+
+ @staticmethod
+ def from_python_mail(mail):
+ input_mail = InputMail()
+ input_mail.headers = {unicode(key.capitalize()): unicode(
+ value) for key, value in mail.items()}
+ input_mail.headers[u'Date'] = unicode(date.mail_date_now())
+ input_mail.headers[u'To'] = [u'']
+
+ for payload in mail.get_payload():
+ input_mail._mime_multipart.attach(payload)
+ if payload.get_content_type() == 'text/plain':
+ input_mail.body = unicode(payload.as_string())
+ input_mail._mime = input_mail.to_mime_multipart()
+ return input_mail
+
+
+def welcome_mail():
+ current_path = os.path.dirname(os.path.abspath(__file__))
+ with open(os.path.join(current_path, '..', '..', 'assets', 'welcome.mail')) as mail_template_file:
+ mail_template = message_from_file(mail_template_file)
+ return InputMail.from_python_mail(mail_template)
diff --git a/src/pixelated/adapter/model/status.py b/src/pixelated/adapter/model/status.py
new file mode 100644
index 00000000..7101f4c0
--- /dev/null
+++ b/src/pixelated/adapter/model/status.py
@@ -0,0 +1,43 @@
+#
+# 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/>.
+
+
+class Status:
+
+ SEEN = u'\\Seen'
+ ANSWERED = u'\\Answered'
+ DELETED = u'\\Deleted'
+ RECENT = u'\\Recent'
+
+ FLAGS_TO_STATUSES = {
+ SEEN: 'read',
+ ANSWERED: 'replied',
+ RECENT: 'recent'
+ }
+
+ @staticmethod
+ def from_flag(flag):
+ return Status.FLAGS_TO_STATUSES[flag]
+
+ @staticmethod
+ def from_flags(flags):
+ return set(Status.from_flag(flag) for flag in flags if flag in Status.FLAGS_TO_STATUSES.keys())
+
+ @staticmethod
+ def to_flags(statuses):
+ statuses_to_flags = dict(
+ zip(Status.FLAGS_TO_STATUSES.values(), Status.FLAGS_TO_STATUSES.keys()))
+ return [statuses_to_flags[status] for status in statuses]
diff --git a/src/pixelated/adapter/model/tag.py b/src/pixelated/adapter/model/tag.py
new file mode 100644
index 00000000..ca62a1fe
--- /dev/null
+++ b/src/pixelated/adapter/model/tag.py
@@ -0,0 +1,73 @@
+#
+# 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 json
+
+
+class Tag(object):
+
+ @classmethod
+ def from_dict(cls, tag_dict):
+ tag = Tag(tag_dict['name'], tag_dict['default'])
+ tag.mails = set(tag_dict['mails'])
+ return tag
+
+ @classmethod
+ def from_json_string(cls, json_string):
+ tag_dict = json.loads(json_string)
+ tag_dict['mails'] = set(tag_dict['mails'])
+ return Tag.from_dict(tag_dict)
+
+ @property
+ def total(self):
+ return len(self.mails)
+
+ def __init__(self, name, default=False):
+ self.name = name.lower()
+ self.ident = self.name.__hash__()
+ self.default = default
+ self.mails = set()
+
+ def __eq__(self, other):
+ return self.name == other.name
+
+ def __hash__(self):
+ return self.name.__hash__()
+
+ def increment(self, mail_ident):
+ self.mails.add(mail_ident)
+
+ def decrement(self, mail_ident):
+ self.mails.discard(mail_ident)
+
+ def as_dict(self):
+ return {
+ 'name': self.name,
+ 'default': self.default,
+ 'ident': self.ident,
+ 'counts': {'total': self.total,
+ 'read': 0,
+ 'starred': 0,
+ 'replied': 0},
+ 'mails': list(self.mails)
+ }
+
+ def as_json_string(self):
+ tag_dict = self.as_dict()
+ return json.dumps(tag_dict)
+
+ def __repr__(self):
+ return self.name
diff --git a/src/pixelated/adapter/search/__init__.py b/src/pixelated/adapter/search/__init__.py
new file mode 100644
index 00000000..79f0b281
--- /dev/null
+++ b/src/pixelated/adapter/search/__init__.py
@@ -0,0 +1,218 @@
+#
+# 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/>.
+from pixelated.support.encrypted_file_storage import EncryptedFileStorage
+
+import os
+import re
+import dateutil.parser
+import time
+from pixelated.adapter.model.status import Status
+from pixelated.adapter.search.contacts import contacts_suggestions
+from whoosh.index import FileIndex
+from whoosh.fields import Schema, ID, KEYWORD, TEXT, NUMERIC, NGRAMWORDS
+from whoosh.qparser import QueryParser
+from whoosh.qparser import MultifieldParser
+from whoosh.writing import AsyncWriter
+from whoosh import sorting
+from pixelated.support.functional import unique, to_unicode
+import traceback
+from pixelated.support import date
+
+
+class SearchEngine(object):
+ DEFAULT_INDEX_HOME = os.path.join(os.environ['HOME'], '.leap')
+ DEFAULT_TAGS = ['inbox', 'sent', 'drafts', 'trash']
+
+ def __init__(self, key, user_home=DEFAULT_INDEX_HOME):
+ self.key = key
+ self.index_folder = os.path.join(user_home, 'search_index')
+ if not os.path.exists(self.index_folder):
+ os.makedirs(self.index_folder)
+ self._index = self._create_index()
+
+ def _add_to_tags(self, tags, group, skip_default_tags, count_type, query=None):
+ query_matcher = re.compile(
+ ".*%s.*" % query.lower()) if query else re.compile(".*")
+
+ for tag, count in group.iteritems():
+
+ if skip_default_tags and tag in self.DEFAULT_TAGS or not query_matcher.match(tag):
+ continue
+
+ if not tags.get(tag):
+ tags[tag] = {'ident': tag, 'name': tag, 'default': False, 'counts': {'total': 0, 'read': 0},
+ 'mails': []}
+ tags[tag]['counts'][count_type] += count
+
+ def _search_tag_groups(self, is_filtering_tags):
+ seen = None
+ query_parser = QueryParser('tag', self._index.schema)
+ options = {'limit': None, 'groupedby': sorting.FieldFacet(
+ 'tag', allow_overlap=True), 'maptype': sorting.Count}
+
+ with self._index.searcher() as searcher:
+ total = searcher.search(
+ query_parser.parse('*'), **options).groups()
+ if not is_filtering_tags:
+ seen = searcher.search(query_parser.parse(
+ "* AND flags:%s" % Status.SEEN), **options).groups()
+ return seen, total
+
+ def _init_tags_defaults(self):
+ tags = {}
+ for default_tag in self.DEFAULT_TAGS:
+ tags[default_tag] = {
+ 'ident': default_tag,
+ 'name': default_tag,
+ 'default': True,
+ 'counts': {
+ 'total': 0,
+ 'read': 0
+ },
+ 'mails': []
+ }
+ return tags
+
+ def _build_tags(self, seen, total, skip_default_tags, query):
+ tags = {}
+ if not skip_default_tags:
+ tags = self._init_tags_defaults()
+ self._add_to_tags(tags, total, skip_default_tags,
+ count_type='total', query=query)
+ if seen:
+ self._add_to_tags(tags, seen, skip_default_tags, count_type='read')
+ return tags.values()
+
+ def tags(self, query, skip_default_tags):
+ is_filtering_tags = True if query else False
+ seen, total = self._search_tag_groups(
+ is_filtering_tags=is_filtering_tags)
+ return self._build_tags(seen, total, skip_default_tags, query)
+
+ def _mail_schema(self):
+ return Schema(
+ ident=ID(stored=True, unique=True),
+ sender=ID(stored=False),
+ to=KEYWORD(stored=False, commas=True),
+ cc=KEYWORD(stored=False, commas=True),
+ bcc=KEYWORD(stored=False, commas=True),
+ subject=NGRAMWORDS(stored=False),
+ date=NUMERIC(stored=False, sortable=True, bits=64, signed=False),
+ body=NGRAMWORDS(stored=False),
+ tag=KEYWORD(stored=True, commas=True),
+ flags=KEYWORD(stored=True, commas=True),
+ raw=TEXT(stored=False))
+
+ def _create_index(self):
+ storage = EncryptedFileStorage(self.index_folder, self.key)
+ return FileIndex.create(storage, self._mail_schema(), indexname='mails')
+
+ def index_mail(self, mail):
+ if mail is not None:
+ with AsyncWriter(self._index) as writer:
+ self._index_mail(writer, mail)
+
+ def _index_mail(self, writer, mail):
+ mdict = mail.as_dict()
+ header = mdict['header']
+ tags = set(mdict.get('tags', {}))
+ tags.add(mail.mailbox_name.lower())
+
+ index_data = {
+ 'sender': self._empty_string_to_none(header.get('from', '')),
+ 'subject': self._empty_string_to_none(header.get('subject', '')),
+ 'date': self._format_utc_integer(header.get('date', date.mail_date_now())),
+ 'to': self._format_recipient(header, 'to'),
+ 'cc': self._format_recipient(header, 'cc'),
+ 'bcc': self._format_recipient(header, 'bcc'),
+ 'tag': u','.join(unique(tags)),
+ 'body': to_unicode(mdict.get('textPlainBody', mdict.get('body', ''))),
+ 'ident': unicode(mdict['ident']),
+ 'flags': unicode(','.join(unique(mail.flags))),
+ 'raw': unicode(mail.raw)
+ }
+
+ writer.update_document(**index_data)
+
+ def _format_utc_integer(self, date):
+ timetuple = dateutil.parser.parse(date).utctimetuple()
+ return time.strftime('%s', timetuple)
+
+ def _format_recipient(self, headers, name):
+ list = headers.get(name, [''])
+ return u','.join(list) if list else u''
+
+ def _empty_string_to_none(self, field_value):
+ if not field_value:
+ return None
+ else:
+ return field_value
+
+ def index_mails(self, mails, callback=None):
+ try:
+ with AsyncWriter(self._index) as writer:
+ for mail in mails:
+ self._index_mail(writer, mail)
+ if callback:
+ callback()
+ except Exception, e:
+ traceback.print_exc(e)
+ raise
+
+ def _search_with_options(self, options, query):
+ with self._index.searcher() as searcher:
+ query = QueryParser('raw', self._index.schema).parse(query)
+ results = searcher.search(query, **options)
+ return results
+
+ def search(self, query, window=25, page=1, all_mails=False):
+ query = self.prepare_query(query)
+ return self._search_all_mails(query) if all_mails else self._paginated_search_mails(query, window, page)
+
+ def _search_all_mails(self, query):
+ with self._index.searcher() as searcher:
+ sorting_facet = sorting.FieldFacet('date', reverse=True)
+ results = searcher.search(
+ query, sortedby=sorting_facet, reverse=True, limit=None)
+ return unique([mail['ident'] for mail in results])
+
+ def _paginated_search_mails(self, query, window, page):
+ page = int(page) if page is not None and int(page) > 1 else 1
+ window = int(window) if window is not None else 25
+
+ with self._index.searcher() as searcher:
+ tags_facet = sorting.FieldFacet(
+ 'tag', allow_overlap=True, maptype=sorting.Count)
+ sorting_facet = sorting.FieldFacet('date', reverse=True)
+ results = searcher.search_page(
+ query, page, pagelen=window, groupedby=tags_facet, sortedby=sorting_facet)
+ return unique([mail['ident'] for mail in results]), sum(results.results.groups().values())
+
+ def prepare_query(self, query):
+ query = (
+ query
+ .replace('-in:', 'AND NOT tag:')
+ .replace('in:all', '*')
+ )
+ return MultifieldParser(['body', 'subject', 'raw'], self._index.schema).parse(query)
+
+ def remove_from_index(self, mail_id):
+ with AsyncWriter(self._index) as writer:
+ writer.delete_by_term('ident', mail_id)
+
+ def contacts(self, query):
+ with self._index.searcher() as searcher:
+ return contacts_suggestions(query, searcher)
diff --git a/src/pixelated/adapter/search/contacts.py b/src/pixelated/adapter/search/contacts.py
new file mode 100644
index 00000000..733489b0
--- /dev/null
+++ b/src/pixelated/adapter/search/contacts.py
@@ -0,0 +1,56 @@
+#
+# 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/>.
+from email.utils import parseaddr
+from pixelated.support.functional import flatten
+from whoosh.qparser import QueryParser
+from whoosh import sorting
+from whoosh.query import Term
+
+
+def address_duplication_filter(contacts):
+ contacts_by_mail = dict()
+
+ for contact in contacts:
+ mail_address = extract_mail_address(contact)
+ current = contacts_by_mail.get(mail_address, '')
+ current = contact if len(contact) > len(current) else current
+ contacts_by_mail[mail_address] = current
+ return contacts_by_mail.values()
+
+
+def extract_mail_address(text):
+ return parseaddr(text)[1]
+
+
+def contacts_suggestions(query, searcher):
+ return address_duplication_filter(search_addresses(searcher, query)) if query else []
+
+
+def search_addresses(searcher, query):
+ restrict_q = Term("tag", "drafts") | Term("tag", "trash")
+ results = []
+ for field in ['to', 'cc', 'bcc', 'sender']:
+ query_parser = QueryParser(field, searcher.schema)
+ results.append(
+ searcher.search(
+ query_parser.parse("*%s* OR *%s*" % (query.title(), query)),
+ limit=None,
+ mask=restrict_q,
+ groupedby=sorting.FieldFacet(
+ field,
+ allow_overlap=True),
+ terms=True).matched_terms())
+ return [address[1] for address in flatten(results)]
diff --git a/src/pixelated/adapter/search/index_storage_key.py b/src/pixelated/adapter/search/index_storage_key.py
new file mode 100644
index 00000000..b2761849
--- /dev/null
+++ b/src/pixelated/adapter/search/index_storage_key.py
@@ -0,0 +1,42 @@
+#
+# Copyright (c) 2015 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 base64
+from twisted.internet import defer
+import os
+
+
+class SearchIndexStorageKey(object):
+ __slots__ = '_soledad'
+
+ def __init__(self, soledad):
+ self._soledad = soledad
+
+ @defer.inlineCallbacks
+ def get_or_create_key(self):
+ docs = yield self._soledad.get_from_index('by-type', 'index_key')
+
+ if len(docs):
+ key = docs[0].content['value']
+ else:
+ key = self._new_index_key()
+ yield self._store_key_in_soledad(key)
+ defer.returnValue(key)
+
+ def _new_index_key(self):
+ return os.urandom(64) # 32 for encryption, 32 for hmac
+
+ def _store_key_in_soledad(self, index_key):
+ return self._soledad.create_doc(dict(type='index_key', value=base64.encodestring(index_key)))
diff --git a/src/pixelated/adapter/services/__init__.py b/src/pixelated/adapter/services/__init__.py
new file mode 100644
index 00000000..2756a319
--- /dev/null
+++ b/src/pixelated/adapter/services/__init__.py
@@ -0,0 +1,15 @@
+#
+# 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/>.
diff --git a/src/pixelated/adapter/services/draft_service.py b/src/pixelated/adapter/services/draft_service.py
new file mode 100644
index 00000000..504d92db
--- /dev/null
+++ b/src/pixelated/adapter/services/draft_service.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/>.
+from twisted.internet import defer
+
+
+class DraftService(object):
+ __slots__ = '_mail_store'
+
+ def __init__(self, mail_store):
+ self._mail_store = mail_store
+
+ @defer.inlineCallbacks
+ def create_draft(self, input_mail):
+ mail = yield self._mail_store.add_mail('DRAFTS', input_mail.raw)
+ defer.returnValue(mail)
+
+ @defer.inlineCallbacks
+ def update_draft(self, ident, input_mail):
+ removed = yield self._mail_store.delete_mail(ident)
+ if removed:
+ new_draft = yield self.create_draft(input_mail)
+ defer.returnValue(new_draft)
+
+ def process_draft(self, ident, input_mail):
+ if ident:
+ return self.update_draft(ident, input_mail)
+ return self.create_draft(input_mail)
diff --git a/src/pixelated/adapter/services/feedback_service.py b/src/pixelated/adapter/services/feedback_service.py
new file mode 100644
index 00000000..5200a9ff
--- /dev/null
+++ b/src/pixelated/adapter/services/feedback_service.py
@@ -0,0 +1,20 @@
+import os
+import requests
+
+
+class FeedbackService(object):
+ FEEDBACK_URL = os.environ.get('FEEDBACK_URL')
+
+ def __init__(self, leap_session):
+ self.leap_session = leap_session
+
+ def open_ticket(self, feedback):
+ account_mail = self.leap_session.account_email()
+ data = {
+ "ticket[comments_attributes][0][body]": feedback,
+ "ticket[subject]": "Feedback user-agent from {0}".format(account_mail),
+ "ticket[email]": account_mail,
+ "ticket[regarding_user]": account_mail
+ }
+
+ return requests.post(self.FEEDBACK_URL, data=data, verify=False)
diff --git a/src/pixelated/adapter/services/mail_sender.py b/src/pixelated/adapter/services/mail_sender.py
new file mode 100644
index 00000000..4933ce4e
--- /dev/null
+++ b/src/pixelated/adapter/services/mail_sender.py
@@ -0,0 +1,102 @@
+#
+# 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/>.
+from StringIO import StringIO
+from email.utils import parseaddr
+from leap.mail.outgoing.service import OutgoingMail
+
+from twisted.internet.defer import Deferred, fail
+from twisted.mail.smtp import SMTPSenderFactory
+from twisted.internet import reactor, defer
+from pixelated.support.functional import flatten
+from twisted.mail.smtp import User
+
+
+class SMTPDownException(Exception):
+
+ def __init__(self):
+ Exception.__init__(self, "Couldn't send mail now, try again later.")
+
+
+NOT_NEEDED = None
+
+
+class MailSenderException(Exception):
+
+ def __init__(self, message, email_error_map):
+ super(MailSenderException, self).__init__(message, email_error_map)
+ self.email_error_map = email_error_map
+
+
+class MailSender(object):
+
+ def __init__(self, smtp_config, keymanager):
+ self._smtp_config = smtp_config
+ self._keymanager = keymanager
+
+ @defer.inlineCallbacks
+ def sendmail(self, mail):
+ recipients = flatten([mail.to, mail.cc, mail.bcc])
+
+ results = yield self._send_mail_to_all_recipients(mail, recipients)
+ all_succeeded = reduce(lambda a, b: a and b, [r[0] for r in results])
+
+ if not all_succeeded:
+ error_map = self._build_error_map(recipients, results)
+ raise MailSenderException(
+ 'Failed to send mail to all recipients', error_map)
+
+ defer.returnValue(all_succeeded)
+
+ def _send_mail_to_all_recipients(self, mail, recipients):
+ outgoing_mail = self._create_outgoing_mail()
+ bccs = mail.bcc
+ deferreds = []
+
+ for recipient in recipients:
+ self._define_bcc_field(mail, recipient, bccs)
+ smtp_recipient = self._create_twisted_smtp_recipient(recipient)
+ deferreds.append(outgoing_mail.send_message(
+ mail.to_smtp_format(), smtp_recipient))
+
+ return defer.DeferredList(deferreds, fireOnOneErrback=False, consumeErrors=True)
+
+ def _define_bcc_field(self, mail, recipient, bccs):
+ if recipient in bccs:
+ mail.headers['Bcc'] = [recipient]
+ else:
+ mail.headers['Bcc'] = []
+
+ def _build_error_map(self, recipients, results):
+ error_map = {}
+ for email, error in [(recipients[idx], r[1]) for idx, r in enumerate(results)]:
+ error_map[email] = error
+ return error_map
+
+ def _create_outgoing_mail(self):
+ return OutgoingMail(str(self._smtp_config.account_email),
+ self._keymanager,
+ self._smtp_config.cert_path,
+ self._smtp_config.cert_path,
+ str(self._smtp_config.remote_smtp_host),
+ int(self._smtp_config.remote_smtp_port))
+
+ def _create_twisted_smtp_recipient(self, recipient):
+ # TODO: Better is fix Twisted instead
+ recipient = self._remove_canonical_recipient(recipient)
+ return User(str(recipient), NOT_NEEDED, NOT_NEEDED, NOT_NEEDED)
+
+ def _remove_canonical_recipient(self, recipient):
+ return recipient.split('<')[1][0:-1] if '<' in recipient else recipient
diff --git a/src/pixelated/adapter/services/mail_service.py b/src/pixelated/adapter/services/mail_service.py
new file mode 100644
index 00000000..884a205a
--- /dev/null
+++ b/src/pixelated/adapter/services/mail_service.py
@@ -0,0 +1,152 @@
+#
+# 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/>.
+from email import encoders
+from email.mime.nonmultipart import MIMENonMultipart
+from email.mime.multipart import MIMEMultipart
+from leap.mail.mail import Message
+
+from twisted.internet import defer
+
+from pixelated.adapter.model.mail import InputMail
+from pixelated.adapter.model.status import Status
+from pixelated.adapter.services.tag_service import extract_reserved_tags
+from leap.mail.adaptors.soledad import SoledadMailAdaptor
+
+
+class MailService(object):
+
+ def __init__(self, mail_sender, mail_store, search_engine, account_email, attachment_store):
+ self.mail_store = mail_store
+ self.search_engine = search_engine
+ self.mail_sender = mail_sender
+ self.account_email = account_email
+ self.attachment_store = attachment_store
+
+ @defer.inlineCallbacks
+ def all_mails(self):
+ mails = yield self.mail_store.all_mails(gracefully_ignore_errors=True)
+ defer.returnValue(mails)
+
+ def save_attachment(self, content, content_type):
+ return self.attachment_store.add_attachment(content, content_type)
+
+ @defer.inlineCallbacks
+ def mails(self, query, window_size, page):
+ mail_ids, total = self.search_engine.search(query, window_size, page)
+
+ try:
+ mails = yield self.mail_store.get_mails(mail_ids)
+ defer.returnValue((mails, total))
+ except Exception, e:
+ import traceback
+ traceback.print_exc()
+ raise
+
+ @defer.inlineCallbacks
+ def update_tags(self, mail_id, new_tags):
+ new_tags = self._filter_white_space_tags(new_tags)
+ reserved_words = extract_reserved_tags(new_tags)
+ if len(reserved_words):
+ raise ValueError(
+ 'None of the following words can be used as tags: ' + ' '.join(reserved_words))
+ new_tags = self._favor_existing_tags_casing(new_tags)
+ mail = yield self.mail(mail_id)
+ mail.tags = set(new_tags)
+ yield self.mail_store.update_mail(mail)
+
+ defer.returnValue(mail)
+
+ def _filter_white_space_tags(self, tags):
+ return [tag.strip() for tag in tags if not tag.isspace()]
+
+ def _favor_existing_tags_casing(self, new_tags):
+ current_tags = [tag['name'] for tag in self.search_engine.tags(
+ query='', skip_default_tags=True)]
+ current_tags_lower = [tag.lower() for tag in current_tags]
+
+ def _use_current_casing(new_tag_lower):
+ return current_tags[current_tags_lower.index(new_tag_lower)]
+
+ return [_use_current_casing(new_tag.lower()) if new_tag.lower() in current_tags_lower else new_tag for new_tag in new_tags]
+
+ def mail(self, mail_id):
+ return self.mail_store.get_mail(mail_id, include_body=True)
+
+ def attachment(self, attachment_id):
+ return self.attachment_store.get_mail_attachment(attachment_id)
+
+ @defer.inlineCallbacks
+ def mail_exists(self, mail_id):
+ try:
+ mail = yield self.mail_store.get_mail(mail_id, include_body=False)
+ defer.returnValue(mail is not None)
+ except Exception, e:
+ defer.returnValue(False)
+
+ @defer.inlineCallbacks
+ def send_mail(self, content_dict):
+ mail = InputMail.from_dict(content_dict, self.account_email)
+ draft_id = content_dict.get('ident')
+ yield self.mail_sender.sendmail(mail)
+
+ sent_mail = yield self.move_to_sent(draft_id, mail)
+ defer.returnValue(sent_mail)
+
+ @defer.inlineCallbacks
+ def move_to_sent(self, last_draft_ident, mail):
+ if last_draft_ident:
+ try:
+ yield self.mail_store.delete_mail(last_draft_ident)
+ except Exception as error:
+ pass
+ sent_mail = yield self.mail_store.add_mail('SENT', mail.raw)
+ sent_mail.flags.add(Status.SEEN)
+ yield self.mail_store.update_mail(sent_mail)
+ defer.returnValue(sent_mail)
+
+ @defer.inlineCallbacks
+ def mark_as_read(self, mail_id):
+ mail = yield self.mail(mail_id)
+ mail.flags.add(Status.SEEN)
+ yield self.mail_store.update_mail(mail)
+
+ @defer.inlineCallbacks
+ def mark_as_unread(self, mail_id):
+ mail = yield self.mail(mail_id)
+ mail.flags.remove(Status.SEEN)
+ yield self.mail_store.update_mail(mail)
+
+ @defer.inlineCallbacks
+ def delete_mail(self, mail_id):
+ mail = yield self.mail(mail_id)
+ if mail is not None:
+ if mail.mailbox_name.upper() in (u'TRASH', u'DRAFTS'):
+ yield self.mail_store.delete_mail(mail_id)
+ else:
+ yield self.mail_store.move_mail_to_mailbox(mail_id, 'TRASH')
+
+ @defer.inlineCallbacks
+ def recover_mail(self, mail_id):
+ yield self.mail_store.move_mail_to_mailbox(mail_id, 'INBOX')
+
+ @defer.inlineCallbacks
+ def archive_mail(self, mail_id):
+ yield self.mail_store.add_mailbox('ARCHIVE')
+ yield self.mail_store.move_mail_to_mailbox(mail_id, 'ARCHIVE')
+
+ @defer.inlineCallbacks
+ def delete_permanent(self, mail_id):
+ yield self.mail_store.delete_mail(mail_id)
diff --git a/src/pixelated/adapter/services/tag_service.py b/src/pixelated/adapter/services/tag_service.py
new file mode 100644
index 00000000..ad94170b
--- /dev/null
+++ b/src/pixelated/adapter/services/tag_service.py
@@ -0,0 +1,24 @@
+#
+# 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/>.
+from pixelated.adapter.model.tag import Tag
+
+SPECIAL_TAGS = {Tag('inbox', True), Tag('sent', True), Tag(
+ 'drafts', True), Tag('trash', True), Tag('ALL', True)}
+
+
+def extract_reserved_tags(tags):
+ tags = [tag.lower() for tag in tags]
+ return {tag.name for tag in SPECIAL_TAGS if tag.name in tags}
diff --git a/src/pixelated/adapter/welcome_mail.py b/src/pixelated/adapter/welcome_mail.py
new file mode 100644
index 00000000..8e6e957b
--- /dev/null
+++ b/src/pixelated/adapter/welcome_mail.py
@@ -0,0 +1,29 @@
+#
+# Copyright (c) 2015 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 pkg_resources
+from email import message_from_file
+from pixelated.adapter.model.mail import InputMail
+
+
+def add_welcome_mail(mail_store):
+ welcome_mail = pkg_resources.resource_filename(
+ 'pixelated.assets', 'welcome.mail')
+
+ with open(welcome_mail) as mail_template_file:
+ mail_template = message_from_file(mail_template_file)
+
+ input_mail = InputMail.from_python_mail(mail_template)
+ mail_store.add_mail('INBOX', input_mail.raw)
diff --git a/src/pixelated/application.py b/src/pixelated/application.py
new file mode 100644
index 00000000..e24e388b
--- /dev/null
+++ b/src/pixelated/application.py
@@ -0,0 +1,222 @@
+#
+# Copyright (c) 2015 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 logging
+
+from OpenSSL import SSL
+from OpenSSL import crypto
+from leap.common.events import (server as events_server,
+ register, catalog as events)
+from twisted.cred import portal
+from twisted.cred.checkers import AllowAnonymousAccess
+from twisted.internet import defer
+from twisted.internet import reactor
+from twisted.internet import ssl
+
+from pixelated.adapter.welcome_mail import add_welcome_mail
+from pixelated.config import arguments
+from pixelated.config import logger
+from pixelated.config.leap import initialize_leap_single_user, init_monkeypatches, initialize_leap_provider
+from pixelated.config import services
+from pixelated.config.site import PixelatedSite
+from pixelated.resources.auth import LeapPasswordChecker, PixelatedRealm, PixelatedAuthSessionWrapper, SessionChecker
+from pixelated.resources.login_resource import LoginResource
+from pixelated.resources.root_resource import RootResource
+
+log = logging.getLogger(__name__)
+
+
+class ServicesFactory(object):
+
+ def __init__(self, mode):
+ self._services_by_user = {}
+ self.mode = mode
+
+ def is_logged_in(self, user_id):
+ return user_id in self._services_by_user
+
+ def services(self, user_id):
+ return self._services_by_user[user_id]
+
+ def log_out_user(self, user_id):
+ if self.is_logged_in(user_id):
+ _services = self._services_by_user[user_id]
+ _services.close()
+ del self._services_by_user[user_id]
+
+ def add_session(self, user_id, services):
+ self._services_by_user[user_id] = services
+
+ @defer.inlineCallbacks
+ def create_services_from(self, leap_session):
+ _services = services.Services(leap_session)
+ yield _services.setup()
+ self._services_by_user[leap_session.user_auth.uuid] = _services
+
+
+class SingleUserServicesFactory(object):
+
+ def __init__(self, mode):
+ self._services = None
+ self.mode = mode
+
+ def add_session(self, user_id, services):
+ self._services = services
+
+ def services(self, user_id):
+ return self._services
+
+
+class UserAgentMode(object):
+
+ def __init__(self, is_single_user):
+ self.is_single_user = is_single_user
+
+
+@defer.inlineCallbacks
+def start_user_agent_in_single_user_mode(root_resource, services_factory, leap_home, leap_session):
+ log.info('Bootstrap done, loading services for user %s' %
+ leap_session.user_auth.username)
+
+ _services = services.Services(leap_session)
+ yield _services.setup()
+
+ if leap_session.fresh_account:
+ yield add_welcome_mail(leap_session.mail_store)
+
+ services_factory.add_session(leap_session.user_auth.uuid, _services)
+
+ root_resource.initialize()
+
+ # soledad needs lots of threads
+ reactor.getThreadPool().adjustPoolsize(5, 15)
+ log.info('Done, the user agent is ready to be used')
+
+
+def _ssl_options(sslkey, sslcert):
+ with open(sslkey) as keyfile:
+ pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, keyfile.read())
+ with open(sslcert) as certfile:
+ cert = crypto.load_certificate(crypto.FILETYPE_PEM, certfile.read())
+
+ acceptable = ssl.AcceptableCiphers.fromOpenSSLCipherString(
+ u'ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:!RC4:HIGH:!MD5:!aNULL:!EDH')
+ options = ssl.CertificateOptions(privateKey=pkey,
+ certificate=cert,
+ method=SSL.TLSv1_2_METHOD,
+ acceptableCiphers=acceptable)
+ return options
+
+
+def _create_service_factory(args):
+ if args.single_user:
+ return SingleUserServicesFactory(UserAgentMode(is_single_user=True))
+ else:
+ return ServicesFactory(UserAgentMode(is_single_user=False))
+
+
+def initialize():
+ log.info('Starting the Pixelated user agent')
+ args = arguments.parse_user_agent_args()
+ logger.init(debug=args.debug)
+ services_factory = _create_service_factory(args)
+ resource = RootResource(services_factory)
+
+ deferred = _start_mode(args, resource, services_factory)
+
+ def _quit_on_error(failure):
+ failure.printTraceback()
+ reactor.stop()
+
+ def _register_shutdown_on_token_expire(leap_session):
+ register(events.SOLEDAD_INVALID_AUTH_TOKEN,
+ lambda *unused: reactor.stop())
+ return leap_session
+
+ deferred.addCallback(_register_shutdown_on_token_expire)
+ deferred.addErrback(_quit_on_error)
+
+ log.info('Running the reactor')
+
+ reactor.run()
+
+
+def _start_mode(args, resource, services_factory):
+ if services_factory.mode.is_single_user:
+ deferred = _start_in_single_user_mode(args, resource, services_factory)
+ else:
+ deferred = _start_in_multi_user_mode(args, resource, services_factory)
+ return deferred
+
+
+def _start_in_multi_user_mode(args, root_resource, services_factory):
+ if args.provider is None:
+ raise ValueError('provider name is required')
+
+ init_monkeypatches()
+ events_server.ensure_server()
+
+ config, provider = initialize_leap_provider(
+ args.provider, args.leap_provider_cert, args.leap_provider_cert_fingerprint, args.leap_home)
+ protected_resource = set_up_protected_resources(
+ root_resource, provider, services_factory, banner=args.banner)
+ start_site(args, protected_resource)
+ reactor.getThreadPool().adjustPoolsize(5, 15)
+ return defer.succeed(None)
+
+
+def set_up_protected_resources(root_resource, provider, services_factory, checker=None, banner=None):
+ if not checker:
+ checker = LeapPasswordChecker(provider)
+ session_checker = SessionChecker(services_factory)
+ anonymous_resource = LoginResource(
+ services_factory, disclaimer_banner=banner)
+
+ realm = PixelatedRealm(root_resource, anonymous_resource)
+ _portal = portal.Portal(
+ realm, [checker, session_checker, AllowAnonymousAccess()])
+
+ protected_resource = PixelatedAuthSessionWrapper(
+ _portal, root_resource, anonymous_resource, [])
+ anonymous_resource.set_portal(_portal)
+ root_resource.initialize(_portal, disclaimer_banner=banner)
+ return protected_resource
+
+
+def _start_in_single_user_mode(args, resource, services_factory):
+ start_site(args, resource)
+ deferred = initialize_leap_single_user(args.leap_provider_cert,
+ args.leap_provider_cert_fingerprint,
+ args.credentials_file,
+ args.organization_mode,
+ args.leap_home)
+ deferred.addCallback(
+ lambda leap_session: start_user_agent_in_single_user_mode(
+ resource,
+ services_factory,
+ args.leap_home,
+ leap_session))
+ return deferred
+
+
+def start_site(config, resource):
+ log.info('Starting the API on port %s' % config.port)
+ if config.sslkey and config.sslcert:
+ reactor.listenSSL(config.port, PixelatedSite(resource), _ssl_options(config.sslkey, config.sslcert),
+ interface=config.host)
+ else:
+ reactor.listenTCP(config.port, PixelatedSite(
+ resource), interface=config.host)
diff --git a/src/pixelated/assets/Interstitial.html b/src/pixelated/assets/Interstitial.html
new file mode 100644
index 00000000..bc6cc738
--- /dev/null
+++ b/src/pixelated/assets/Interstitial.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html style="border: 0px; padding: 0px; margin: 0px;">
+
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+ <script src="startup-assets/snap.svg-min.js"></script>
+ <script src="startup-assets/jquery-2.1.3.min.js"></script>
+ </head>
+
+ <body style="border: 0px; padding: 0px; margin: 0px;background-color: #808181">
+ <section id="hive-section" style="background-color: #808181;" name="hive-section">
+ <svg id="hive" style="width: 100%; height: 100%;"></svg>
+ </section>
+ <script src="startup-assets/Interstitial.js"></script>
+ </body>
+
+</html>
diff --git a/src/pixelated/assets/Interstitial.js b/src/pixelated/assets/Interstitial.js
new file mode 100644
index 00000000..ac5a789a
--- /dev/null
+++ b/src/pixelated/assets/Interstitial.js
@@ -0,0 +1,58 @@
+if ($('#hive').length) {
+ var hive = new Snap('#hive');
+ var img_width = $('#hive').width();
+ var left_pos = img_width * .5;
+
+ var pixelated = hive.path("M12.4,20.3v31.8l28,15.8l28-15.8V20.3l-28-15.8L12.4,20.3z M39.2,56.4l-16.3-9V27.9l16.3,9.3L39.2,56.4z M57.7,47.4l-16.1,9l0-19.2l16.1-9.4V47.4z M57.7,25.2L40.4,35.5L22.9,25.2l17.5-9.4L57.7,25.2z").transform("translate(319, 50)").attr("fill", "#908e8e");
+ var all = hive.group().transform("matrix(2, 0, 0, 2, -100, -100)");
+
+ var height = 50;
+ var width = 58;
+ var rows = (($(window).height() / height) / 2) + 1;
+ var cols = (($(window).width() / width) / 2) + 1;
+
+
+ for (var j = 0; j < rows; j++) {
+ for (var i = 0; i < cols; i++) {
+ x = i * width + (j%2*width/2);
+ y = j * height;
+ all.add(pixelated.clone().transform("translate("+x+","+y+")"));
+ }
+ }
+
+ all.add(pixelated);
+
+ var brightenLogo = function () {
+ var glowPosition = Math.floor(Math.random()*rows*cols);
+
+ all[glowPosition].animate({fill: "#FFF"}, 1000, function() {
+ darkenLogo(all[glowPosition]);
+ });
+ };
+
+ var darkenLogo = function (el) {
+ el.animate({fill: "#908e8e"}, 1000, brightenLogo);
+ };
+
+ brightenLogo();
+
+}
+
+$(function () {
+ var handler = setInterval(function () {
+ $.ajax({
+ method: 'GET',
+ url: '/'
+ }).success(function (data) {
+ if (/Pixelated Mail/g.test(data)) {
+ window.location="/";
+ }
+ });
+ }, 2000);
+
+ $('#hive-section').height($(window).height());
+
+ $(window).resize(function() {
+ window.location.reload(true);
+ });
+});
diff --git a/src/pixelated/assets/__init__.py b/src/pixelated/assets/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/src/pixelated/assets/__init__.py
diff --git a/src/pixelated/assets/_login_disclaimer_banner.html b/src/pixelated/assets/_login_disclaimer_banner.html
new file mode 100644
index 00000000..dfc63030
--- /dev/null
+++ b/src/pixelated/assets/_login_disclaimer_banner.html
@@ -0,0 +1,9 @@
+<div>
+ <ul class="accounts">
+ <h2>Some disclaimer</h2>
+ <li>
+ please supply the option --banner with an XML compatible file
+ <div>to override this default message</div>
+ </li>
+ </ul>
+</div>
diff --git a/src/pixelated/assets/favicon.png b/src/pixelated/assets/favicon.png
new file mode 100644
index 00000000..e14841c7
--- /dev/null
+++ b/src/pixelated/assets/favicon.png
Binary files differ
diff --git a/src/pixelated/assets/hive-bg.png b/src/pixelated/assets/hive-bg.png
new file mode 100644
index 00000000..77316967
--- /dev/null
+++ b/src/pixelated/assets/hive-bg.png
Binary files differ
diff --git a/src/pixelated/assets/index.html b/src/pixelated/assets/index.html
new file mode 100644
index 00000000..c095577e
--- /dev/null
+++ b/src/pixelated/assets/index.html
@@ -0,0 +1,9 @@
+<html>
+ <head>
+ <meta http-equiv="refresh" content="0;URL=/login">
+ </head>
+ <body bgcolor="#FFFFFF" text="#000000\">
+ <a href="/login">click here</a>
+ </body>
+</html>
+
diff --git a/src/pixelated/assets/jquery-2.1.3.min.js b/src/pixelated/assets/jquery-2.1.3.min.js
new file mode 100644
index 00000000..25714ed2
--- /dev/null
+++ b/src/pixelated/assets/jquery-2.1.3.min.js
@@ -0,0 +1,4 @@
+/*! jQuery v2.1.3 | (c) 2005, 2014 jQuery Foundation, Inc. | jquery.org/license */
+!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=c.slice,e=c.concat,f=c.push,g=c.indexOf,h={},i=h.toString,j=h.hasOwnProperty,k={},l=a.document,m="2.1.3",n=function(a,b){return new n.fn.init(a,b)},o=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,p=/^-ms-/,q=/-([\da-z])/gi,r=function(a,b){return b.toUpperCase()};n.fn=n.prototype={jquery:m,constructor:n,selector:"",length:0,toArray:function(){return d.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:d.call(this)},pushStack:function(a){var b=n.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a,b){return n.each(this,a,b)},map:function(a){return this.pushStack(n.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:c.sort,splice:c.splice},n.extend=n.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||n.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(a=arguments[h]))for(b in a)c=g[b],d=a[b],g!==d&&(j&&d&&(n.isPlainObject(d)||(e=n.isArray(d)))?(e?(e=!1,f=c&&n.isArray(c)?c:[]):f=c&&n.isPlainObject(c)?c:{},g[b]=n.extend(j,f,d)):void 0!==d&&(g[b]=d));return g},n.extend({expando:"jQuery"+(m+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===n.type(a)},isArray:Array.isArray,isWindow:function(a){return null!=a&&a===a.window},isNumeric:function(a){return!n.isArray(a)&&a-parseFloat(a)+1>=0},isPlainObject:function(a){return"object"!==n.type(a)||a.nodeType||n.isWindow(a)?!1:a.constructor&&!j.call(a.constructor.prototype,"isPrototypeOf")?!1:!0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?h[i.call(a)]||"object":typeof a},globalEval:function(a){var b,c=eval;a=n.trim(a),a&&(1===a.indexOf("use strict")?(b=l.createElement("script"),b.text=a,l.head.appendChild(b).parentNode.removeChild(b)):c(a))},camelCase:function(a){return a.replace(p,"ms-").replace(q,r)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b,c){var d,e=0,f=a.length,g=s(a);if(c){if(g){for(;f>e;e++)if(d=b.apply(a[e],c),d===!1)break}else for(e in a)if(d=b.apply(a[e],c),d===!1)break}else if(g){for(;f>e;e++)if(d=b.call(a[e],e,a[e]),d===!1)break}else for(e in a)if(d=b.call(a[e],e,a[e]),d===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(o,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(s(Object(a))?n.merge(c,"string"==typeof a?[a]:a):f.call(c,a)),c},inArray:function(a,b,c){return null==b?-1:g.call(b,a,c)},merge:function(a,b){for(var c=+b.length,d=0,e=a.length;c>d;d++)a[e++]=b[d];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,f=0,g=a.length,h=s(a),i=[];if(h)for(;g>f;f++)d=b(a[f],f,c),null!=d&&i.push(d);else for(f in a)d=b(a[f],f,c),null!=d&&i.push(d);return e.apply([],i)},guid:1,proxy:function(a,b){var c,e,f;return"string"==typeof b&&(c=a[b],b=a,a=c),n.isFunction(a)?(e=d.call(arguments,2),f=function(){return a.apply(b||this,e.concat(d.call(arguments)))},f.guid=a.guid=a.guid||n.guid++,f):void 0},now:Date.now,support:k}),n.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(a,b){h["[object "+b+"]"]=b.toLowerCase()});function s(a){var b=a.length,c=n.type(a);return"function"===c||n.isWindow(a)?!1:1===a.nodeType&&b?!0:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var t=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=hb(),z=hb(),A=hb(),B=function(a,b){return a===b&&(l=!0),0},C=1<<31,D={}.hasOwnProperty,E=[],F=E.pop,G=E.push,H=E.push,I=E.slice,J=function(a,b){for(var c=0,d=a.length;d>c;c++)if(a[c]===b)return c;return-1},K="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",L="[\\x20\\t\\r\\n\\f]",M="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",N=M.replace("w","w#"),O="\\["+L+"*("+M+")(?:"+L+"*([*^$|!~]?=)"+L+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+N+"))|)"+L+"*\\]",P=":("+M+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+O+")*)|.*)\\)|)",Q=new RegExp(L+"+","g"),R=new RegExp("^"+L+"+|((?:^|[^\\\\])(?:\\\\.)*)"+L+"+$","g"),S=new RegExp("^"+L+"*,"+L+"*"),T=new RegExp("^"+L+"*([>+~]|"+L+")"+L+"*"),U=new RegExp("="+L+"*([^\\]'\"]*?)"+L+"*\\]","g"),V=new RegExp(P),W=new RegExp("^"+N+"$"),X={ID:new RegExp("^#("+M+")"),CLASS:new RegExp("^\\.("+M+")"),TAG:new RegExp("^("+M.replace("w","w*")+")"),ATTR:new RegExp("^"+O),PSEUDO:new RegExp("^"+P),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+L+"*(even|odd|(([+-]|)(\\d*)n|)"+L+"*(?:([+-]|)"+L+"*(\\d+)|))"+L+"*\\)|)","i"),bool:new RegExp("^(?:"+K+")$","i"),needsContext:new RegExp("^"+L+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+L+"*((?:-\\d)?\\d*)"+L+"*\\)|)(?=[^-]|$)","i")},Y=/^(?:input|select|textarea|button)$/i,Z=/^h\d$/i,$=/^[^{]+\{\s*\[native \w/,_=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ab=/[+~]/,bb=/'|\\/g,cb=new RegExp("\\\\([\\da-f]{1,6}"+L+"?|("+L+")|.)","ig"),db=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},eb=function(){m()};try{H.apply(E=I.call(v.childNodes),v.childNodes),E[v.childNodes.length].nodeType}catch(fb){H={apply:E.length?function(a,b){G.apply(a,I.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function gb(a,b,d,e){var f,h,j,k,l,o,r,s,w,x;if((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,d=d||[],k=b.nodeType,"string"!=typeof a||!a||1!==k&&9!==k&&11!==k)return d;if(!e&&p){if(11!==k&&(f=_.exec(a)))if(j=f[1]){if(9===k){if(h=b.getElementById(j),!h||!h.parentNode)return d;if(h.id===j)return d.push(h),d}else if(b.ownerDocument&&(h=b.ownerDocument.getElementById(j))&&t(b,h)&&h.id===j)return d.push(h),d}else{if(f[2])return H.apply(d,b.getElementsByTagName(a)),d;if((j=f[3])&&c.getElementsByClassName)return H.apply(d,b.getElementsByClassName(j)),d}if(c.qsa&&(!q||!q.test(a))){if(s=r=u,w=b,x=1!==k&&a,1===k&&"object"!==b.nodeName.toLowerCase()){o=g(a),(r=b.getAttribute("id"))?s=r.replace(bb,"\\$&"):b.setAttribute("id",s),s="[id='"+s+"'] ",l=o.length;while(l--)o[l]=s+rb(o[l]);w=ab.test(a)&&pb(b.parentNode)||b,x=o.join(",")}if(x)try{return H.apply(d,w.querySelectorAll(x)),d}catch(y){}finally{r||b.removeAttribute("id")}}}return i(a.replace(R,"$1"),b,d,e)}function hb(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ib(a){return a[u]=!0,a}function jb(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function kb(a,b){var c=a.split("|"),e=a.length;while(e--)d.attrHandle[c[e]]=b}function lb(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||C)-(~a.sourceIndex||C);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function mb(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function nb(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function ob(a){return ib(function(b){return b=+b,ib(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function pb(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=gb.support={},f=gb.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=gb.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=g.documentElement,e=g.defaultView,e&&e!==e.top&&(e.addEventListener?e.addEventListener("unload",eb,!1):e.attachEvent&&e.attachEvent("onunload",eb)),p=!f(g),c.attributes=jb(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=jb(function(a){return a.appendChild(g.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=$.test(g.getElementsByClassName),c.getById=jb(function(a){return o.appendChild(a).id=u,!g.getElementsByName||!g.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c&&c.parentNode?[c]:[]}},d.filter.ID=function(a){var b=a.replace(cb,db);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(cb,db);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=$.test(g.querySelectorAll))&&(jb(function(a){o.appendChild(a).innerHTML="<a id='"+u+"'></a><select id='"+u+"-\f]' msallowcapture=''><option selected=''></option></select>",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+L+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+L+"*(?:value|"+K+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),jb(function(a){var b=g.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+L+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=$.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&jb(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",P)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=$.test(o.compareDocumentPosition),t=b||$.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===g||a.ownerDocument===v&&t(v,a)?-1:b===g||b.ownerDocument===v&&t(v,b)?1:k?J(k,a)-J(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,h=[a],i=[b];if(!e||!f)return a===g?-1:b===g?1:e?-1:f?1:k?J(k,a)-J(k,b):0;if(e===f)return lb(a,b);c=a;while(c=c.parentNode)h.unshift(c);c=b;while(c=c.parentNode)i.unshift(c);while(h[d]===i[d])d++;return d?lb(h[d],i[d]):h[d]===v?-1:i[d]===v?1:0},g):n},gb.matches=function(a,b){return gb(a,null,null,b)},gb.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(U,"='$1']"),!(!c.matchesSelector||!p||r&&r.test(b)||q&&q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return gb(b,n,null,[a]).length>0},gb.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},gb.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&D.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},gb.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},gb.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=gb.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=gb.selectors={cacheLength:50,createPseudo:ib,match:X,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(cb,db),a[3]=(a[3]||a[4]||a[5]||"").replace(cb,db),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||gb.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&gb.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return X.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&V.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(cb,db).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+L+")"+a+"("+L+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=gb.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(Q," ")+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h;if(q){if(f){while(p){l=b;while(l=l[p])if(h?l.nodeName.toLowerCase()===r:1===l.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){k=q[u]||(q[u]={}),j=k[a]||[],n=j[0]===w&&j[1],m=j[0]===w&&j[2],l=n&&q.childNodes[n];while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if(1===l.nodeType&&++m&&l===b){k[a]=[w,n,m];break}}else if(s&&(j=(b[u]||(b[u]={}))[a])&&j[0]===w)m=j[1];else while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if((h?l.nodeName.toLowerCase()===r:1===l.nodeType)&&++m&&(s&&((l[u]||(l[u]={}))[a]=[w,m]),l===b))break;return m-=e,m===d||m%d===0&&m/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||gb.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ib(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=J(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ib(function(a){var b=[],c=[],d=h(a.replace(R,"$1"));return d[u]?ib(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ib(function(a){return function(b){return gb(a,b).length>0}}),contains:ib(function(a){return a=a.replace(cb,db),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ib(function(a){return W.test(a||"")||gb.error("unsupported lang: "+a),a=a.replace(cb,db).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Z.test(a.nodeName)},input:function(a){return Y.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:ob(function(){return[0]}),last:ob(function(a,b){return[b-1]}),eq:ob(function(a,b,c){return[0>c?c+b:c]}),even:ob(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:ob(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:ob(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:ob(function(a,b,c){for(var d=0>c?c+b:c;++d<b;)a.push(d);return a})}},d.pseudos.nth=d.pseudos.eq;for(b in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})d.pseudos[b]=mb(b);for(b in{submit:!0,reset:!0})d.pseudos[b]=nb(b);function qb(){}qb.prototype=d.filters=d.pseudos,d.setFilters=new qb,g=gb.tokenize=function(a,b){var c,e,f,g,h,i,j,k=z[a+" "];if(k)return b?0:k.slice(0);h=a,i=[],j=d.preFilter;while(h){(!c||(e=S.exec(h)))&&(e&&(h=h.slice(e[0].length)||h),i.push(f=[])),c=!1,(e=T.exec(h))&&(c=e.shift(),f.push({value:c,type:e[0].replace(R," ")}),h=h.slice(c.length));for(g in d.filter)!(e=X[g].exec(h))||j[g]&&!(e=j[g](e))||(c=e.shift(),f.push({value:c,type:g,matches:e}),h=h.slice(c.length));if(!c)break}return b?h.length:h?gb.error(a):z(a,i).slice(0)};function rb(a){for(var b=0,c=a.length,d="";c>b;b++)d+=a[b].value;return d}function sb(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(i=b[u]||(b[u]={}),(h=i[d])&&h[0]===w&&h[1]===f)return j[2]=h[2];if(i[d]=j,j[2]=a(b,c,g))return!0}}}function tb(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function ub(a,b,c){for(var d=0,e=b.length;e>d;d++)gb(a,b[d],c);return c}function vb(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(!c||c(f,d,e))&&(g.push(f),j&&b.push(h));return g}function wb(a,b,c,d,e,f){return d&&!d[u]&&(d=wb(d)),e&&!e[u]&&(e=wb(e,f)),ib(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||ub(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:vb(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=vb(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?J(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=vb(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):H.apply(g,r)})}function xb(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=sb(function(a){return a===b},h,!0),l=sb(function(a){return J(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];f>i;i++)if(c=d.relative[a[i].type])m=[sb(tb(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return wb(i>1&&tb(m),i>1&&rb(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(R,"$1"),c,e>i&&xb(a.slice(i,e)),f>e&&xb(a=a.slice(e)),f>e&&rb(a))}m.push(c)}return tb(m)}function yb(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,m,o,p=0,q="0",r=f&&[],s=[],t=j,u=f||e&&d.find.TAG("*",k),v=w+=null==t?1:Math.random()||.1,x=u.length;for(k&&(j=g!==n&&g);q!==x&&null!=(l=u[q]);q++){if(e&&l){m=0;while(o=a[m++])if(o(l,g,h)){i.push(l);break}k&&(w=v)}c&&((l=!o&&l)&&p--,f&&r.push(l))}if(p+=q,c&&q!==p){m=0;while(o=b[m++])o(r,s,g,h);if(f){if(p>0)while(q--)r[q]||s[q]||(s[q]=F.call(i));s=vb(s)}H.apply(i,s),k&&!f&&s.length>0&&p+b.length>1&&gb.uniqueSort(i)}return k&&(w=v,j=t),r};return c?ib(f):f}return h=gb.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=xb(b[c]),f[u]?d.push(f):e.push(f);f=A(a,yb(e,d)),f.selector=a}return f},i=gb.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(cb,db),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=X.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(cb,db),ab.test(j[0].type)&&pb(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&rb(j),!a)return H.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,ab.test(a)&&pb(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=jb(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),jb(function(a){return a.innerHTML="<a href='#'></a>","#"===a.firstChild.getAttribute("href")})||kb("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&jb(function(a){return a.innerHTML="<input/>",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||kb("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),jb(function(a){return null==a.getAttribute("disabled")})||kb(K,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),gb}(a);n.find=t,n.expr=t.selectors,n.expr[":"]=n.expr.pseudos,n.unique=t.uniqueSort,n.text=t.getText,n.isXMLDoc=t.isXML,n.contains=t.contains;var u=n.expr.match.needsContext,v=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,w=/^.[^:#\[\.,]*$/;function x(a,b,c){if(n.isFunction(b))return n.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return n.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(w.test(b))return n.filter(b,a,c);b=n.filter(b,a)}return n.grep(a,function(a){return g.call(b,a)>=0!==c})}n.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?n.find.matchesSelector(d,a)?[d]:[]:n.find.matches(a,n.grep(b,function(a){return 1===a.nodeType}))},n.fn.extend({find:function(a){var b,c=this.length,d=[],e=this;if("string"!=typeof a)return this.pushStack(n(a).filter(function(){for(b=0;c>b;b++)if(n.contains(e[b],this))return!0}));for(b=0;c>b;b++)n.find(a,e[b],d);return d=this.pushStack(c>1?n.unique(d):d),d.selector=this.selector?this.selector+" "+a:a,d},filter:function(a){return this.pushStack(x(this,a||[],!1))},not:function(a){return this.pushStack(x(this,a||[],!0))},is:function(a){return!!x(this,"string"==typeof a&&u.test(a)?n(a):a||[],!1).length}});var y,z=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,A=n.fn.init=function(a,b){var c,d;if(!a)return this;if("string"==typeof a){if(c="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:z.exec(a),!c||!c[1]&&b)return!b||b.jquery?(b||y).find(a):this.constructor(b).find(a);if(c[1]){if(b=b instanceof n?b[0]:b,n.merge(this,n.parseHTML(c[1],b&&b.nodeType?b.ownerDocument||b:l,!0)),v.test(c[1])&&n.isPlainObject(b))for(c in b)n.isFunction(this[c])?this[c](b[c]):this.attr(c,b[c]);return this}return d=l.getElementById(c[2]),d&&d.parentNode&&(this.length=1,this[0]=d),this.context=l,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):n.isFunction(a)?"undefined"!=typeof y.ready?y.ready(a):a(n):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),n.makeArray(a,this))};A.prototype=n.fn,y=n(l);var B=/^(?:parents|prev(?:Until|All))/,C={children:!0,contents:!0,next:!0,prev:!0};n.extend({dir:function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&n(a).is(c))break;d.push(a)}return d},sibling:function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c}}),n.fn.extend({has:function(a){var b=n(a,this),c=b.length;return this.filter(function(){for(var a=0;c>a;a++)if(n.contains(this,b[a]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=u.test(a)||"string"!=typeof a?n(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&n.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?n.unique(f):f)},index:function(a){return a?"string"==typeof a?g.call(n(a),this[0]):g.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(n.unique(n.merge(this.get(),n(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function D(a,b){while((a=a[b])&&1!==a.nodeType);return a}n.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return n.dir(a,"parentNode")},parentsUntil:function(a,b,c){return n.dir(a,"parentNode",c)},next:function(a){return D(a,"nextSibling")},prev:function(a){return D(a,"previousSibling")},nextAll:function(a){return n.dir(a,"nextSibling")},prevAll:function(a){return n.dir(a,"previousSibling")},nextUntil:function(a,b,c){return n.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return n.dir(a,"previousSibling",c)},siblings:function(a){return n.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return n.sibling(a.firstChild)},contents:function(a){return a.contentDocument||n.merge([],a.childNodes)}},function(a,b){n.fn[a]=function(c,d){var e=n.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=n.filter(d,e)),this.length>1&&(C[a]||n.unique(e),B.test(a)&&e.reverse()),this.pushStack(e)}});var E=/\S+/g,F={};function G(a){var b=F[a]={};return n.each(a.match(E)||[],function(a,c){b[c]=!0}),b}n.Callbacks=function(a){a="string"==typeof a?F[a]||G(a):n.extend({},a);var b,c,d,e,f,g,h=[],i=!a.once&&[],j=function(l){for(b=a.memory&&l,c=!0,g=e||0,e=0,f=h.length,d=!0;h&&f>g;g++)if(h[g].apply(l[0],l[1])===!1&&a.stopOnFalse){b=!1;break}d=!1,h&&(i?i.length&&j(i.shift()):b?h=[]:k.disable())},k={add:function(){if(h){var c=h.length;!function g(b){n.each(b,function(b,c){var d=n.type(c);"function"===d?a.unique&&k.has(c)||h.push(c):c&&c.length&&"string"!==d&&g(c)})}(arguments),d?f=h.length:b&&(e=c,j(b))}return this},remove:function(){return h&&n.each(arguments,function(a,b){var c;while((c=n.inArray(b,h,c))>-1)h.splice(c,1),d&&(f>=c&&f--,g>=c&&g--)}),this},has:function(a){return a?n.inArray(a,h)>-1:!(!h||!h.length)},empty:function(){return h=[],f=0,this},disable:function(){return h=i=b=void 0,this},disabled:function(){return!h},lock:function(){return i=void 0,b||k.disable(),this},locked:function(){return!i},fireWith:function(a,b){return!h||c&&!i||(b=b||[],b=[a,b.slice?b.slice():b],d?i.push(b):j(b)),this},fire:function(){return k.fireWith(this,arguments),this},fired:function(){return!!c}};return k},n.extend({Deferred:function(a){var b=[["resolve","done",n.Callbacks("once memory"),"resolved"],["reject","fail",n.Callbacks("once memory"),"rejected"],["notify","progress",n.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return n.Deferred(function(c){n.each(b,function(b,f){var g=n.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&n.isFunction(a.promise)?a.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?n.extend(a,d):d}},e={};return d.pipe=d.then,n.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=d.call(arguments),e=c.length,f=1!==e||a&&n.isFunction(a.promise)?e:0,g=1===f?a:n.Deferred(),h=function(a,b,c){return function(e){b[a]=this,c[a]=arguments.length>1?d.call(arguments):e,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(e>1)for(i=new Array(e),j=new Array(e),k=new Array(e);e>b;b++)c[b]&&n.isFunction(c[b].promise)?c[b].promise().done(h(b,k,c)).fail(g.reject).progress(h(b,j,i)):--f;return f||g.resolveWith(k,c),g.promise()}});var H;n.fn.ready=function(a){return n.ready.promise().done(a),this},n.extend({isReady:!1,readyWait:1,holdReady:function(a){a?n.readyWait++:n.ready(!0)},ready:function(a){(a===!0?--n.readyWait:n.isReady)||(n.isReady=!0,a!==!0&&--n.readyWait>0||(H.resolveWith(l,[n]),n.fn.triggerHandler&&(n(l).triggerHandler("ready"),n(l).off("ready"))))}});function I(){l.removeEventListener("DOMContentLoaded",I,!1),a.removeEventListener("load",I,!1),n.ready()}n.ready.promise=function(b){return H||(H=n.Deferred(),"complete"===l.readyState?setTimeout(n.ready):(l.addEventListener("DOMContentLoaded",I,!1),a.addEventListener("load",I,!1))),H.promise(b)},n.ready.promise();var J=n.access=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===n.type(c)){e=!0;for(h in c)n.access(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,n.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(n(a),c)})),b))for(;i>h;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f};n.acceptData=function(a){return 1===a.nodeType||9===a.nodeType||!+a.nodeType};function K(){Object.defineProperty(this.cache={},0,{get:function(){return{}}}),this.expando=n.expando+K.uid++}K.uid=1,K.accepts=n.acceptData,K.prototype={key:function(a){if(!K.accepts(a))return 0;var b={},c=a[this.expando];if(!c){c=K.uid++;try{b[this.expando]={value:c},Object.defineProperties(a,b)}catch(d){b[this.expando]=c,n.extend(a,b)}}return this.cache[c]||(this.cache[c]={}),c},set:function(a,b,c){var d,e=this.key(a),f=this.cache[e];if("string"==typeof b)f[b]=c;else if(n.isEmptyObject(f))n.extend(this.cache[e],b);else for(d in b)f[d]=b[d];return f},get:function(a,b){var c=this.cache[this.key(a)];return void 0===b?c:c[b]},access:function(a,b,c){var d;return void 0===b||b&&"string"==typeof b&&void 0===c?(d=this.get(a,b),void 0!==d?d:this.get(a,n.camelCase(b))):(this.set(a,b,c),void 0!==c?c:b)},remove:function(a,b){var c,d,e,f=this.key(a),g=this.cache[f];if(void 0===b)this.cache[f]={};else{n.isArray(b)?d=b.concat(b.map(n.camelCase)):(e=n.camelCase(b),b in g?d=[b,e]:(d=e,d=d in g?[d]:d.match(E)||[])),c=d.length;while(c--)delete g[d[c]]}},hasData:function(a){return!n.isEmptyObject(this.cache[a[this.expando]]||{})},discard:function(a){a[this.expando]&&delete this.cache[a[this.expando]]}};var L=new K,M=new K,N=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,O=/([A-Z])/g;function P(a,b,c){var d;if(void 0===c&&1===a.nodeType)if(d="data-"+b.replace(O,"-$1").toLowerCase(),c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:N.test(c)?n.parseJSON(c):c}catch(e){}M.set(a,b,c)}else c=void 0;return c}n.extend({hasData:function(a){return M.hasData(a)||L.hasData(a)},data:function(a,b,c){return M.access(a,b,c)
+},removeData:function(a,b){M.remove(a,b)},_data:function(a,b,c){return L.access(a,b,c)},_removeData:function(a,b){L.remove(a,b)}}),n.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=M.get(f),1===f.nodeType&&!L.get(f,"hasDataAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=n.camelCase(d.slice(5)),P(f,d,e[d])));L.set(f,"hasDataAttrs",!0)}return e}return"object"==typeof a?this.each(function(){M.set(this,a)}):J(this,function(b){var c,d=n.camelCase(a);if(f&&void 0===b){if(c=M.get(f,a),void 0!==c)return c;if(c=M.get(f,d),void 0!==c)return c;if(c=P(f,d,void 0),void 0!==c)return c}else this.each(function(){var c=M.get(this,d);M.set(this,d,b),-1!==a.indexOf("-")&&void 0!==c&&M.set(this,a,b)})},null,b,arguments.length>1,null,!0)},removeData:function(a){return this.each(function(){M.remove(this,a)})}}),n.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=L.get(a,b),c&&(!d||n.isArray(c)?d=L.access(a,b,n.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=n.queue(a,b),d=c.length,e=c.shift(),f=n._queueHooks(a,b),g=function(){n.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return L.get(a,c)||L.access(a,c,{empty:n.Callbacks("once memory").add(function(){L.remove(a,[b+"queue",c])})})}}),n.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length<c?n.queue(this[0],a):void 0===b?this:this.each(function(){var c=n.queue(this,a,b);n._queueHooks(this,a),"fx"===a&&"inprogress"!==c[0]&&n.dequeue(this,a)})},dequeue:function(a){return this.each(function(){n.dequeue(this,a)})},clearQueue:function(a){return this.queue(a||"fx",[])},promise:function(a,b){var c,d=1,e=n.Deferred(),f=this,g=this.length,h=function(){--d||e.resolveWith(f,[f])};"string"!=typeof a&&(b=a,a=void 0),a=a||"fx";while(g--)c=L.get(f[g],a+"queueHooks"),c&&c.empty&&(d++,c.empty.add(h));return h(),e.promise(b)}});var Q=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,R=["Top","Right","Bottom","Left"],S=function(a,b){return a=b||a,"none"===n.css(a,"display")||!n.contains(a.ownerDocument,a)},T=/^(?:checkbox|radio)$/i;!function(){var a=l.createDocumentFragment(),b=a.appendChild(l.createElement("div")),c=l.createElement("input");c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),b.appendChild(c),k.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML="<textarea>x</textarea>",k.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var U="undefined";k.focusinBubbles="onfocusin"in a;var V=/^key/,W=/^(?:mouse|pointer|contextmenu)|click/,X=/^(?:focusinfocus|focusoutblur)$/,Y=/^([^.]*)(?:\.(.+)|)$/;function Z(){return!0}function $(){return!1}function _(){try{return l.activeElement}catch(a){}}n.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=L.get(a);if(r){c.handler&&(f=c,c=f.handler,e=f.selector),c.guid||(c.guid=n.guid++),(i=r.events)||(i=r.events={}),(g=r.handle)||(g=r.handle=function(b){return typeof n!==U&&n.event.triggered!==b.type?n.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(E)||[""],j=b.length;while(j--)h=Y.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o&&(l=n.event.special[o]||{},o=(e?l.delegateType:l.bindType)||o,l=n.event.special[o]||{},k=n.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&n.expr.match.needsContext.test(e),namespace:p.join(".")},f),(m=i[o])||(m=i[o]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,p,g)!==!1||a.addEventListener&&a.addEventListener(o,g,!1)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),n.event.global[o]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=L.hasData(a)&&L.get(a);if(r&&(i=r.events)){b=(b||"").match(E)||[""],j=b.length;while(j--)if(h=Y.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=n.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,m=i[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&q!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||n.removeEvent(a,o,r.handle),delete i[o])}else for(o in i)n.event.remove(a,o+b[j],c,d,!0);n.isEmptyObject(i)&&(delete r.handle,L.remove(a,"events"))}},trigger:function(b,c,d,e){var f,g,h,i,k,m,o,p=[d||l],q=j.call(b,"type")?b.type:b,r=j.call(b,"namespace")?b.namespace.split("."):[];if(g=h=d=d||l,3!==d.nodeType&&8!==d.nodeType&&!X.test(q+n.event.triggered)&&(q.indexOf(".")>=0&&(r=q.split("."),q=r.shift(),r.sort()),k=q.indexOf(":")<0&&"on"+q,b=b[n.expando]?b:new n.Event(q,"object"==typeof b&&b),b.isTrigger=e?2:3,b.namespace=r.join("."),b.namespace_re=b.namespace?new RegExp("(^|\\.)"+r.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=d),c=null==c?[b]:n.makeArray(c,[b]),o=n.event.special[q]||{},e||!o.trigger||o.trigger.apply(d,c)!==!1)){if(!e&&!o.noBubble&&!n.isWindow(d)){for(i=o.delegateType||q,X.test(i+q)||(g=g.parentNode);g;g=g.parentNode)p.push(g),h=g;h===(d.ownerDocument||l)&&p.push(h.defaultView||h.parentWindow||a)}f=0;while((g=p[f++])&&!b.isPropagationStopped())b.type=f>1?i:o.bindType||q,m=(L.get(g,"events")||{})[b.type]&&L.get(g,"handle"),m&&m.apply(g,c),m=k&&g[k],m&&m.apply&&n.acceptData(g)&&(b.result=m.apply(g,c),b.result===!1&&b.preventDefault());return b.type=q,e||b.isDefaultPrevented()||o._default&&o._default.apply(p.pop(),c)!==!1||!n.acceptData(d)||k&&n.isFunction(d[q])&&!n.isWindow(d)&&(h=d[k],h&&(d[k]=null),n.event.triggered=q,d[q](),n.event.triggered=void 0,h&&(d[k]=h)),b.result}},dispatch:function(a){a=n.event.fix(a);var b,c,e,f,g,h=[],i=d.call(arguments),j=(L.get(this,"events")||{})[a.type]||[],k=n.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=n.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,c=0;while((g=f.handlers[c++])&&!a.isImmediatePropagationStopped())(!a.namespace_re||a.namespace_re.test(g.namespace))&&(a.handleObj=g,a.data=g.data,e=((n.event.special[g.origType]||{}).handle||g.handler).apply(f.elem,i),void 0!==e&&(a.result=e)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&(!a.button||"click"!==a.type))for(;i!==this;i=i.parentNode||this)if(i.disabled!==!0||"click"!==a.type){for(d=[],c=0;h>c;c++)f=b[c],e=f.selector+" ",void 0===d[e]&&(d[e]=f.needsContext?n(e,this).index(i)>=0:n.find(e,this,null,[i]).length),d[e]&&d.push(f);d.length&&g.push({elem:i,handlers:d})}return h<b.length&&g.push({elem:this,handlers:b.slice(h)}),g},props:"altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(a,b){return null==a.which&&(a.which=null!=b.charCode?b.charCode:b.keyCode),a}},mouseHooks:{props:"button buttons clientX clientY offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(a,b){var c,d,e,f=b.button;return null==a.pageX&&null!=b.clientX&&(c=a.target.ownerDocument||l,d=c.documentElement,e=c.body,a.pageX=b.clientX+(d&&d.scrollLeft||e&&e.scrollLeft||0)-(d&&d.clientLeft||e&&e.clientLeft||0),a.pageY=b.clientY+(d&&d.scrollTop||e&&e.scrollTop||0)-(d&&d.clientTop||e&&e.clientTop||0)),a.which||void 0===f||(a.which=1&f?1:2&f?3:4&f?2:0),a}},fix:function(a){if(a[n.expando])return a;var b,c,d,e=a.type,f=a,g=this.fixHooks[e];g||(this.fixHooks[e]=g=W.test(e)?this.mouseHooks:V.test(e)?this.keyHooks:{}),d=g.props?this.props.concat(g.props):this.props,a=new n.Event(f),b=d.length;while(b--)c=d[b],a[c]=f[c];return a.target||(a.target=l),3===a.target.nodeType&&(a.target=a.target.parentNode),g.filter?g.filter(a,f):a},special:{load:{noBubble:!0},focus:{trigger:function(){return this!==_()&&this.focus?(this.focus(),!1):void 0},delegateType:"focusin"},blur:{trigger:function(){return this===_()&&this.blur?(this.blur(),!1):void 0},delegateType:"focusout"},click:{trigger:function(){return"checkbox"===this.type&&this.click&&n.nodeName(this,"input")?(this.click(),!1):void 0},_default:function(a){return n.nodeName(a.target,"a")}},beforeunload:{postDispatch:function(a){void 0!==a.result&&a.originalEvent&&(a.originalEvent.returnValue=a.result)}}},simulate:function(a,b,c,d){var e=n.extend(new n.Event,c,{type:a,isSimulated:!0,originalEvent:{}});d?n.event.trigger(e,null,b):n.event.dispatch.call(b,e),e.isDefaultPrevented()&&c.preventDefault()}},n.removeEvent=function(a,b,c){a.removeEventListener&&a.removeEventListener(b,c,!1)},n.Event=function(a,b){return this instanceof n.Event?(a&&a.type?(this.originalEvent=a,this.type=a.type,this.isDefaultPrevented=a.defaultPrevented||void 0===a.defaultPrevented&&a.returnValue===!1?Z:$):this.type=a,b&&n.extend(this,b),this.timeStamp=a&&a.timeStamp||n.now(),void(this[n.expando]=!0)):new n.Event(a,b)},n.Event.prototype={isDefaultPrevented:$,isPropagationStopped:$,isImmediatePropagationStopped:$,preventDefault:function(){var a=this.originalEvent;this.isDefaultPrevented=Z,a&&a.preventDefault&&a.preventDefault()},stopPropagation:function(){var a=this.originalEvent;this.isPropagationStopped=Z,a&&a.stopPropagation&&a.stopPropagation()},stopImmediatePropagation:function(){var a=this.originalEvent;this.isImmediatePropagationStopped=Z,a&&a.stopImmediatePropagation&&a.stopImmediatePropagation(),this.stopPropagation()}},n.each({mouseenter:"mouseover",mouseleave:"mouseout",pointerenter:"pointerover",pointerleave:"pointerout"},function(a,b){n.event.special[a]={delegateType:b,bindType:b,handle:function(a){var c,d=this,e=a.relatedTarget,f=a.handleObj;return(!e||e!==d&&!n.contains(d,e))&&(a.type=f.origType,c=f.handler.apply(this,arguments),a.type=b),c}}}),k.focusinBubbles||n.each({focus:"focusin",blur:"focusout"},function(a,b){var c=function(a){n.event.simulate(b,a.target,n.event.fix(a),!0)};n.event.special[b]={setup:function(){var d=this.ownerDocument||this,e=L.access(d,b);e||d.addEventListener(a,c,!0),L.access(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=L.access(d,b)-1;e?L.access(d,b,e):(d.removeEventListener(a,c,!0),L.remove(d,b))}}}),n.fn.extend({on:function(a,b,c,d,e){var f,g;if("object"==typeof a){"string"!=typeof b&&(c=c||b,b=void 0);for(g in a)this.on(g,b,c,a[g],e);return this}if(null==c&&null==d?(d=b,c=b=void 0):null==d&&("string"==typeof b?(d=c,c=void 0):(d=c,c=b,b=void 0)),d===!1)d=$;else if(!d)return this;return 1===e&&(f=d,d=function(a){return n().off(a),f.apply(this,arguments)},d.guid=f.guid||(f.guid=n.guid++)),this.each(function(){n.event.add(this,a,d,c,b)})},one:function(a,b,c,d){return this.on(a,b,c,d,1)},off:function(a,b,c){var d,e;if(a&&a.preventDefault&&a.handleObj)return d=a.handleObj,n(a.delegateTarget).off(d.namespace?d.origType+"."+d.namespace:d.origType,d.selector,d.handler),this;if("object"==typeof a){for(e in a)this.off(e,b,a[e]);return this}return(b===!1||"function"==typeof b)&&(c=b,b=void 0),c===!1&&(c=$),this.each(function(){n.event.remove(this,a,c,b)})},trigger:function(a,b){return this.each(function(){n.event.trigger(a,b,this)})},triggerHandler:function(a,b){var c=this[0];return c?n.event.trigger(a,b,c,!0):void 0}});var ab=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,bb=/<([\w:]+)/,cb=/<|&#?\w+;/,db=/<(?:script|style|link)/i,eb=/checked\s*(?:[^=]|=\s*.checked.)/i,fb=/^$|\/(?:java|ecma)script/i,gb=/^true\/(.*)/,hb=/^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g,ib={option:[1,"<select multiple='multiple'>","</select>"],thead:[1,"<table>","</table>"],col:[2,"<table><colgroup>","</colgroup></table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_default:[0,"",""]};ib.optgroup=ib.option,ib.tbody=ib.tfoot=ib.colgroup=ib.caption=ib.thead,ib.th=ib.td;function jb(a,b){return n.nodeName(a,"table")&&n.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function kb(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function lb(a){var b=gb.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function mb(a,b){for(var c=0,d=a.length;d>c;c++)L.set(a[c],"globalEval",!b||L.get(b[c],"globalEval"))}function nb(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(L.hasData(a)&&(f=L.access(a),g=L.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;d>c;c++)n.event.add(b,e,j[e][c])}M.hasData(a)&&(h=M.access(a),i=n.extend({},h),M.set(b,i))}}function ob(a,b){var c=a.getElementsByTagName?a.getElementsByTagName(b||"*"):a.querySelectorAll?a.querySelectorAll(b||"*"):[];return void 0===b||b&&n.nodeName(a,b)?n.merge([a],c):c}function pb(a,b){var c=b.nodeName.toLowerCase();"input"===c&&T.test(a.type)?b.checked=a.checked:("input"===c||"textarea"===c)&&(b.defaultValue=a.defaultValue)}n.extend({clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=n.contains(a.ownerDocument,a);if(!(k.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||n.isXMLDoc(a)))for(g=ob(h),f=ob(a),d=0,e=f.length;e>d;d++)pb(f[d],g[d]);if(b)if(c)for(f=f||ob(a),g=g||ob(h),d=0,e=f.length;e>d;d++)nb(f[d],g[d]);else nb(a,h);return g=ob(h,"script"),g.length>0&&mb(g,!i&&ob(a,"script")),h},buildFragment:function(a,b,c,d){for(var e,f,g,h,i,j,k=b.createDocumentFragment(),l=[],m=0,o=a.length;o>m;m++)if(e=a[m],e||0===e)if("object"===n.type(e))n.merge(l,e.nodeType?[e]:e);else if(cb.test(e)){f=f||k.appendChild(b.createElement("div")),g=(bb.exec(e)||["",""])[1].toLowerCase(),h=ib[g]||ib._default,f.innerHTML=h[1]+e.replace(ab,"<$1></$2>")+h[2],j=h[0];while(j--)f=f.lastChild;n.merge(l,f.childNodes),f=k.firstChild,f.textContent=""}else l.push(b.createTextNode(e));k.textContent="",m=0;while(e=l[m++])if((!d||-1===n.inArray(e,d))&&(i=n.contains(e.ownerDocument,e),f=ob(k.appendChild(e),"script"),i&&mb(f),c)){j=0;while(e=f[j++])fb.test(e.type||"")&&c.push(e)}return k},cleanData:function(a){for(var b,c,d,e,f=n.event.special,g=0;void 0!==(c=a[g]);g++){if(n.acceptData(c)&&(e=c[L.expando],e&&(b=L.cache[e]))){if(b.events)for(d in b.events)f[d]?n.event.remove(c,d):n.removeEvent(c,d,b.handle);L.cache[e]&&delete L.cache[e]}delete M.cache[c[M.expando]]}}}),n.fn.extend({text:function(a){return J(this,function(a){return void 0===a?n.text(this):this.empty().each(function(){(1===this.nodeType||11===this.nodeType||9===this.nodeType)&&(this.textContent=a)})},null,a,arguments.length)},append:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=jb(this,a);b.appendChild(a)}})},prepend:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=jb(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},remove:function(a,b){for(var c,d=a?n.filter(a,this):this,e=0;null!=(c=d[e]);e++)b||1!==c.nodeType||n.cleanData(ob(c)),c.parentNode&&(b&&n.contains(c.ownerDocument,c)&&mb(ob(c,"script")),c.parentNode.removeChild(c));return this},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(n.cleanData(ob(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return n.clone(this,a,b)})},html:function(a){return J(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!db.test(a)&&!ib[(bb.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(ab,"<$1></$2>");try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(n.cleanData(ob(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=arguments[0];return this.domManip(arguments,function(b){a=this.parentNode,n.cleanData(ob(this)),a&&a.replaceChild(b,this)}),a&&(a.length||a.nodeType)?this:this.remove()},detach:function(a){return this.remove(a,!0)},domManip:function(a,b){a=e.apply([],a);var c,d,f,g,h,i,j=0,l=this.length,m=this,o=l-1,p=a[0],q=n.isFunction(p);if(q||l>1&&"string"==typeof p&&!k.checkClone&&eb.test(p))return this.each(function(c){var d=m.eq(c);q&&(a[0]=p.call(this,c,d.html())),d.domManip(a,b)});if(l&&(c=n.buildFragment(a,this[0].ownerDocument,!1,this),d=c.firstChild,1===c.childNodes.length&&(c=d),d)){for(f=n.map(ob(c,"script"),kb),g=f.length;l>j;j++)h=c,j!==o&&(h=n.clone(h,!0,!0),g&&n.merge(f,ob(h,"script"))),b.call(this[j],h,j);if(g)for(i=f[f.length-1].ownerDocument,n.map(f,lb),j=0;g>j;j++)h=f[j],fb.test(h.type||"")&&!L.access(h,"globalEval")&&n.contains(i,h)&&(h.src?n._evalUrl&&n._evalUrl(h.src):n.globalEval(h.textContent.replace(hb,"")))}return this}}),n.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){n.fn[a]=function(a){for(var c,d=[],e=n(a),g=e.length-1,h=0;g>=h;h++)c=h===g?this:this.clone(!0),n(e[h])[b](c),f.apply(d,c.get());return this.pushStack(d)}});var qb,rb={};function sb(b,c){var d,e=n(c.createElement(b)).appendTo(c.body),f=a.getDefaultComputedStyle&&(d=a.getDefaultComputedStyle(e[0]))?d.display:n.css(e[0],"display");return e.detach(),f}function tb(a){var b=l,c=rb[a];return c||(c=sb(a,b),"none"!==c&&c||(qb=(qb||n("<iframe frameborder='0' width='0' height='0'/>")).appendTo(b.documentElement),b=qb[0].contentDocument,b.write(),b.close(),c=sb(a,b),qb.detach()),rb[a]=c),c}var ub=/^margin/,vb=new RegExp("^("+Q+")(?!px)[a-z%]+$","i"),wb=function(b){return b.ownerDocument.defaultView.opener?b.ownerDocument.defaultView.getComputedStyle(b,null):a.getComputedStyle(b,null)};function xb(a,b,c){var d,e,f,g,h=a.style;return c=c||wb(a),c&&(g=c.getPropertyValue(b)||c[b]),c&&(""!==g||n.contains(a.ownerDocument,a)||(g=n.style(a,b)),vb.test(g)&&ub.test(b)&&(d=h.width,e=h.minWidth,f=h.maxWidth,h.minWidth=h.maxWidth=h.width=g,g=c.width,h.width=d,h.minWidth=e,h.maxWidth=f)),void 0!==g?g+"":g}function yb(a,b){return{get:function(){return a()?void delete this.get:(this.get=b).apply(this,arguments)}}}!function(){var b,c,d=l.documentElement,e=l.createElement("div"),f=l.createElement("div");if(f.style){f.style.backgroundClip="content-box",f.cloneNode(!0).style.backgroundClip="",k.clearCloneStyle="content-box"===f.style.backgroundClip,e.style.cssText="border:0;width:0;height:0;top:0;left:-9999px;margin-top:1px;position:absolute",e.appendChild(f);function g(){f.style.cssText="-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;display:block;margin-top:1%;top:1%;border:1px;padding:1px;width:4px;position:absolute",f.innerHTML="",d.appendChild(e);var g=a.getComputedStyle(f,null);b="1%"!==g.top,c="4px"===g.width,d.removeChild(e)}a.getComputedStyle&&n.extend(k,{pixelPosition:function(){return g(),b},boxSizingReliable:function(){return null==c&&g(),c},reliableMarginRight:function(){var b,c=f.appendChild(l.createElement("div"));return c.style.cssText=f.style.cssText="-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;display:block;margin:0;border:0;padding:0",c.style.marginRight=c.style.width="0",f.style.width="1px",d.appendChild(e),b=!parseFloat(a.getComputedStyle(c,null).marginRight),d.removeChild(e),f.removeChild(c),b}})}}(),n.swap=function(a,b,c,d){var e,f,g={};for(f in b)g[f]=a.style[f],a.style[f]=b[f];e=c.apply(a,d||[]);for(f in b)a.style[f]=g[f];return e};var zb=/^(none|table(?!-c[ea]).+)/,Ab=new RegExp("^("+Q+")(.*)$","i"),Bb=new RegExp("^([+-])=("+Q+")","i"),Cb={position:"absolute",visibility:"hidden",display:"block"},Db={letterSpacing:"0",fontWeight:"400"},Eb=["Webkit","O","Moz","ms"];function Fb(a,b){if(b in a)return b;var c=b[0].toUpperCase()+b.slice(1),d=b,e=Eb.length;while(e--)if(b=Eb[e]+c,b in a)return b;return d}function Gb(a,b,c){var d=Ab.exec(b);return d?Math.max(0,d[1]-(c||0))+(d[2]||"px"):b}function Hb(a,b,c,d,e){for(var f=c===(d?"border":"content")?4:"width"===b?1:0,g=0;4>f;f+=2)"margin"===c&&(g+=n.css(a,c+R[f],!0,e)),d?("content"===c&&(g-=n.css(a,"padding"+R[f],!0,e)),"margin"!==c&&(g-=n.css(a,"border"+R[f]+"Width",!0,e))):(g+=n.css(a,"padding"+R[f],!0,e),"padding"!==c&&(g+=n.css(a,"border"+R[f]+"Width",!0,e)));return g}function Ib(a,b,c){var d=!0,e="width"===b?a.offsetWidth:a.offsetHeight,f=wb(a),g="border-box"===n.css(a,"boxSizing",!1,f);if(0>=e||null==e){if(e=xb(a,b,f),(0>e||null==e)&&(e=a.style[b]),vb.test(e))return e;d=g&&(k.boxSizingReliable()||e===a.style[b]),e=parseFloat(e)||0}return e+Hb(a,b,c||(g?"border":"content"),d,f)+"px"}function Jb(a,b){for(var c,d,e,f=[],g=0,h=a.length;h>g;g++)d=a[g],d.style&&(f[g]=L.get(d,"olddisplay"),c=d.style.display,b?(f[g]||"none"!==c||(d.style.display=""),""===d.style.display&&S(d)&&(f[g]=L.access(d,"olddisplay",tb(d.nodeName)))):(e=S(d),"none"===c&&e||L.set(d,"olddisplay",e?c:n.css(d,"display"))));for(g=0;h>g;g++)d=a[g],d.style&&(b&&"none"!==d.style.display&&""!==d.style.display||(d.style.display=b?f[g]||"":"none"));return a}n.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=xb(a,"opacity");return""===c?"1":c}}}},cssNumber:{columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":"cssFloat"},style:function(a,b,c,d){if(a&&3!==a.nodeType&&8!==a.nodeType&&a.style){var e,f,g,h=n.camelCase(b),i=a.style;return b=n.cssProps[h]||(n.cssProps[h]=Fb(i,h)),g=n.cssHooks[b]||n.cssHooks[h],void 0===c?g&&"get"in g&&void 0!==(e=g.get(a,!1,d))?e:i[b]:(f=typeof c,"string"===f&&(e=Bb.exec(c))&&(c=(e[1]+1)*e[2]+parseFloat(n.css(a,b)),f="number"),null!=c&&c===c&&("number"!==f||n.cssNumber[h]||(c+="px"),k.clearCloneStyle||""!==c||0!==b.indexOf("background")||(i[b]="inherit"),g&&"set"in g&&void 0===(c=g.set(a,c,d))||(i[b]=c)),void 0)}},css:function(a,b,c,d){var e,f,g,h=n.camelCase(b);return b=n.cssProps[h]||(n.cssProps[h]=Fb(a.style,h)),g=n.cssHooks[b]||n.cssHooks[h],g&&"get"in g&&(e=g.get(a,!0,c)),void 0===e&&(e=xb(a,b,d)),"normal"===e&&b in Db&&(e=Db[b]),""===c||c?(f=parseFloat(e),c===!0||n.isNumeric(f)?f||0:e):e}}),n.each(["height","width"],function(a,b){n.cssHooks[b]={get:function(a,c,d){return c?zb.test(n.css(a,"display"))&&0===a.offsetWidth?n.swap(a,Cb,function(){return Ib(a,b,d)}):Ib(a,b,d):void 0},set:function(a,c,d){var e=d&&wb(a);return Gb(a,c,d?Hb(a,b,d,"border-box"===n.css(a,"boxSizing",!1,e),e):0)}}}),n.cssHooks.marginRight=yb(k.reliableMarginRight,function(a,b){return b?n.swap(a,{display:"inline-block"},xb,[a,"marginRight"]):void 0}),n.each({margin:"",padding:"",border:"Width"},function(a,b){n.cssHooks[a+b]={expand:function(c){for(var d=0,e={},f="string"==typeof c?c.split(" "):[c];4>d;d++)e[a+R[d]+b]=f[d]||f[d-2]||f[0];return e}},ub.test(a)||(n.cssHooks[a+b].set=Gb)}),n.fn.extend({css:function(a,b){return J(this,function(a,b,c){var d,e,f={},g=0;if(n.isArray(b)){for(d=wb(a),e=b.length;e>g;g++)f[b[g]]=n.css(a,b[g],!1,d);return f}return void 0!==c?n.style(a,b,c):n.css(a,b)},a,b,arguments.length>1)},show:function(){return Jb(this,!0)},hide:function(){return Jb(this)},toggle:function(a){return"boolean"==typeof a?a?this.show():this.hide():this.each(function(){S(this)?n(this).show():n(this).hide()})}});function Kb(a,b,c,d,e){return new Kb.prototype.init(a,b,c,d,e)}n.Tween=Kb,Kb.prototype={constructor:Kb,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||"swing",this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(n.cssNumber[c]?"":"px")},cur:function(){var a=Kb.propHooks[this.prop];return a&&a.get?a.get(this):Kb.propHooks._default.get(this)},run:function(a){var b,c=Kb.propHooks[this.prop];return this.pos=b=this.options.duration?n.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):Kb.propHooks._default.set(this),this}},Kb.prototype.init.prototype=Kb.prototype,Kb.propHooks={_default:{get:function(a){var b;return null==a.elem[a.prop]||a.elem.style&&null!=a.elem.style[a.prop]?(b=n.css(a.elem,a.prop,""),b&&"auto"!==b?b:0):a.elem[a.prop]},set:function(a){n.fx.step[a.prop]?n.fx.step[a.prop](a):a.elem.style&&(null!=a.elem.style[n.cssProps[a.prop]]||n.cssHooks[a.prop])?n.style(a.elem,a.prop,a.now+a.unit):a.elem[a.prop]=a.now}}},Kb.propHooks.scrollTop=Kb.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},n.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2}},n.fx=Kb.prototype.init,n.fx.step={};var Lb,Mb,Nb=/^(?:toggle|show|hide)$/,Ob=new RegExp("^(?:([+-])=|)("+Q+")([a-z%]*)$","i"),Pb=/queueHooks$/,Qb=[Vb],Rb={"*":[function(a,b){var c=this.createTween(a,b),d=c.cur(),e=Ob.exec(b),f=e&&e[3]||(n.cssNumber[a]?"":"px"),g=(n.cssNumber[a]||"px"!==f&&+d)&&Ob.exec(n.css(c.elem,a)),h=1,i=20;if(g&&g[3]!==f){f=f||g[3],e=e||[],g=+d||1;do h=h||".5",g/=h,n.style(c.elem,a,g+f);while(h!==(h=c.cur()/d)&&1!==h&&--i)}return e&&(g=c.start=+g||+d||0,c.unit=f,c.end=e[1]?g+(e[1]+1)*e[2]:+e[2]),c}]};function Sb(){return setTimeout(function(){Lb=void 0}),Lb=n.now()}function Tb(a,b){var c,d=0,e={height:a};for(b=b?1:0;4>d;d+=2-b)c=R[d],e["margin"+c]=e["padding"+c]=a;return b&&(e.opacity=e.width=a),e}function Ub(a,b,c){for(var d,e=(Rb[b]||[]).concat(Rb["*"]),f=0,g=e.length;g>f;f++)if(d=e[f].call(c,b,a))return d}function Vb(a,b,c){var d,e,f,g,h,i,j,k,l=this,m={},o=a.style,p=a.nodeType&&S(a),q=L.get(a,"fxshow");c.queue||(h=n._queueHooks(a,"fx"),null==h.unqueued&&(h.unqueued=0,i=h.empty.fire,h.empty.fire=function(){h.unqueued||i()}),h.unqueued++,l.always(function(){l.always(function(){h.unqueued--,n.queue(a,"fx").length||h.empty.fire()})})),1===a.nodeType&&("height"in b||"width"in b)&&(c.overflow=[o.overflow,o.overflowX,o.overflowY],j=n.css(a,"display"),k="none"===j?L.get(a,"olddisplay")||tb(a.nodeName):j,"inline"===k&&"none"===n.css(a,"float")&&(o.display="inline-block")),c.overflow&&(o.overflow="hidden",l.always(function(){o.overflow=c.overflow[0],o.overflowX=c.overflow[1],o.overflowY=c.overflow[2]}));for(d in b)if(e=b[d],Nb.exec(e)){if(delete b[d],f=f||"toggle"===e,e===(p?"hide":"show")){if("show"!==e||!q||void 0===q[d])continue;p=!0}m[d]=q&&q[d]||n.style(a,d)}else j=void 0;if(n.isEmptyObject(m))"inline"===("none"===j?tb(a.nodeName):j)&&(o.display=j);else{q?"hidden"in q&&(p=q.hidden):q=L.access(a,"fxshow",{}),f&&(q.hidden=!p),p?n(a).show():l.done(function(){n(a).hide()}),l.done(function(){var b;L.remove(a,"fxshow");for(b in m)n.style(a,b,m[b])});for(d in m)g=Ub(p?q[d]:0,d,l),d in q||(q[d]=g.start,p&&(g.end=g.start,g.start="width"===d||"height"===d?1:0))}}function Wb(a,b){var c,d,e,f,g;for(c in a)if(d=n.camelCase(c),e=b[d],f=a[c],n.isArray(f)&&(e=f[1],f=a[c]=f[0]),c!==d&&(a[d]=f,delete a[c]),g=n.cssHooks[d],g&&"expand"in g){f=g.expand(f),delete a[d];for(c in f)c in a||(a[c]=f[c],b[c]=e)}else b[d]=e}function Xb(a,b,c){var d,e,f=0,g=Qb.length,h=n.Deferred().always(function(){delete i.elem}),i=function(){if(e)return!1;for(var b=Lb||Sb(),c=Math.max(0,j.startTime+j.duration-b),d=c/j.duration||0,f=1-d,g=0,i=j.tweens.length;i>g;g++)j.tweens[g].run(f);return h.notifyWith(a,[j,f,c]),1>f&&i?c:(h.resolveWith(a,[j]),!1)},j=h.promise({elem:a,props:n.extend({},b),opts:n.extend(!0,{specialEasing:{}},c),originalProperties:b,originalOptions:c,startTime:Lb||Sb(),duration:c.duration,tweens:[],createTween:function(b,c){var d=n.Tween(a,j.opts,b,c,j.opts.specialEasing[b]||j.opts.easing);return j.tweens.push(d),d},stop:function(b){var c=0,d=b?j.tweens.length:0;if(e)return this;for(e=!0;d>c;c++)j.tweens[c].run(1);return b?h.resolveWith(a,[j,b]):h.rejectWith(a,[j,b]),this}}),k=j.props;for(Wb(k,j.opts.specialEasing);g>f;f++)if(d=Qb[f].call(j,a,k,j.opts))return d;return n.map(k,Ub,j),n.isFunction(j.opts.start)&&j.opts.start.call(a,j),n.fx.timer(n.extend(i,{elem:a,anim:j,queue:j.opts.queue})),j.progress(j.opts.progress).done(j.opts.done,j.opts.complete).fail(j.opts.fail).always(j.opts.always)}n.Animation=n.extend(Xb,{tweener:function(a,b){n.isFunction(a)?(b=a,a=["*"]):a=a.split(" ");for(var c,d=0,e=a.length;e>d;d++)c=a[d],Rb[c]=Rb[c]||[],Rb[c].unshift(b)},prefilter:function(a,b){b?Qb.unshift(a):Qb.push(a)}}),n.speed=function(a,b,c){var d=a&&"object"==typeof a?n.extend({},a):{complete:c||!c&&b||n.isFunction(a)&&a,duration:a,easing:c&&b||b&&!n.isFunction(b)&&b};return d.duration=n.fx.off?0:"number"==typeof d.duration?d.duration:d.duration in n.fx.speeds?n.fx.speeds[d.duration]:n.fx.speeds._default,(null==d.queue||d.queue===!0)&&(d.queue="fx"),d.old=d.complete,d.complete=function(){n.isFunction(d.old)&&d.old.call(this),d.queue&&n.dequeue(this,d.queue)},d},n.fn.extend({fadeTo:function(a,b,c,d){return this.filter(S).css("opacity",0).show().end().animate({opacity:b},a,c,d)},animate:function(a,b,c,d){var e=n.isEmptyObject(a),f=n.speed(b,c,d),g=function(){var b=Xb(this,n.extend({},a),f);(e||L.get(this,"finish"))&&b.stop(!0)};return g.finish=g,e||f.queue===!1?this.each(g):this.queue(f.queue,g)},stop:function(a,b,c){var d=function(a){var b=a.stop;delete a.stop,b(c)};return"string"!=typeof a&&(c=b,b=a,a=void 0),b&&a!==!1&&this.queue(a||"fx",[]),this.each(function(){var b=!0,e=null!=a&&a+"queueHooks",f=n.timers,g=L.get(this);if(e)g[e]&&g[e].stop&&d(g[e]);else for(e in g)g[e]&&g[e].stop&&Pb.test(e)&&d(g[e]);for(e=f.length;e--;)f[e].elem!==this||null!=a&&f[e].queue!==a||(f[e].anim.stop(c),b=!1,f.splice(e,1));(b||!c)&&n.dequeue(this,a)})},finish:function(a){return a!==!1&&(a=a||"fx"),this.each(function(){var b,c=L.get(this),d=c[a+"queue"],e=c[a+"queueHooks"],f=n.timers,g=d?d.length:0;for(c.finish=!0,n.queue(this,a,[]),e&&e.stop&&e.stop.call(this,!0),b=f.length;b--;)f[b].elem===this&&f[b].queue===a&&(f[b].anim.stop(!0),f.splice(b,1));for(b=0;g>b;b++)d[b]&&d[b].finish&&d[b].finish.call(this);delete c.finish})}}),n.each(["toggle","show","hide"],function(a,b){var c=n.fn[b];n.fn[b]=function(a,d,e){return null==a||"boolean"==typeof a?c.apply(this,arguments):this.animate(Tb(b,!0),a,d,e)}}),n.each({slideDown:Tb("show"),slideUp:Tb("hide"),slideToggle:Tb("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(a,b){n.fn[a]=function(a,c,d){return this.animate(b,a,c,d)}}),n.timers=[],n.fx.tick=function(){var a,b=0,c=n.timers;for(Lb=n.now();b<c.length;b++)a=c[b],a()||c[b]!==a||c.splice(b--,1);c.length||n.fx.stop(),Lb=void 0},n.fx.timer=function(a){n.timers.push(a),a()?n.fx.start():n.timers.pop()},n.fx.interval=13,n.fx.start=function(){Mb||(Mb=setInterval(n.fx.tick,n.fx.interval))},n.fx.stop=function(){clearInterval(Mb),Mb=null},n.fx.speeds={slow:600,fast:200,_default:400},n.fn.delay=function(a,b){return a=n.fx?n.fx.speeds[a]||a:a,b=b||"fx",this.queue(b,function(b,c){var d=setTimeout(b,a);c.stop=function(){clearTimeout(d)}})},function(){var a=l.createElement("input"),b=l.createElement("select"),c=b.appendChild(l.createElement("option"));a.type="checkbox",k.checkOn=""!==a.value,k.optSelected=c.selected,b.disabled=!0,k.optDisabled=!c.disabled,a=l.createElement("input"),a.value="t",a.type="radio",k.radioValue="t"===a.value}();var Yb,Zb,$b=n.expr.attrHandle;n.fn.extend({attr:function(a,b){return J(this,n.attr,a,b,arguments.length>1)},removeAttr:function(a){return this.each(function(){n.removeAttr(this,a)})}}),n.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(a&&3!==f&&8!==f&&2!==f)return typeof a.getAttribute===U?n.prop(a,b,c):(1===f&&n.isXMLDoc(a)||(b=b.toLowerCase(),d=n.attrHooks[b]||(n.expr.match.bool.test(b)?Zb:Yb)),void 0===c?d&&"get"in d&&null!==(e=d.get(a,b))?e:(e=n.find.attr(a,b),null==e?void 0:e):null!==c?d&&"set"in d&&void 0!==(e=d.set(a,c,b))?e:(a.setAttribute(b,c+""),c):void n.removeAttr(a,b))
+},removeAttr:function(a,b){var c,d,e=0,f=b&&b.match(E);if(f&&1===a.nodeType)while(c=f[e++])d=n.propFix[c]||c,n.expr.match.bool.test(c)&&(a[d]=!1),a.removeAttribute(c)},attrHooks:{type:{set:function(a,b){if(!k.radioValue&&"radio"===b&&n.nodeName(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}}}),Zb={set:function(a,b,c){return b===!1?n.removeAttr(a,c):a.setAttribute(c,c),c}},n.each(n.expr.match.bool.source.match(/\w+/g),function(a,b){var c=$b[b]||n.find.attr;$b[b]=function(a,b,d){var e,f;return d||(f=$b[b],$b[b]=e,e=null!=c(a,b,d)?b.toLowerCase():null,$b[b]=f),e}});var _b=/^(?:input|select|textarea|button)$/i;n.fn.extend({prop:function(a,b){return J(this,n.prop,a,b,arguments.length>1)},removeProp:function(a){return this.each(function(){delete this[n.propFix[a]||a]})}}),n.extend({propFix:{"for":"htmlFor","class":"className"},prop:function(a,b,c){var d,e,f,g=a.nodeType;if(a&&3!==g&&8!==g&&2!==g)return f=1!==g||!n.isXMLDoc(a),f&&(b=n.propFix[b]||b,e=n.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){return a.hasAttribute("tabindex")||_b.test(a.nodeName)||a.href?a.tabIndex:-1}}}}),k.optSelected||(n.propHooks.selected={get:function(a){var b=a.parentNode;return b&&b.parentNode&&b.parentNode.selectedIndex,null}}),n.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){n.propFix[this.toLowerCase()]=this});var ac=/[\t\r\n\f]/g;n.fn.extend({addClass:function(a){var b,c,d,e,f,g,h="string"==typeof a&&a,i=0,j=this.length;if(n.isFunction(a))return this.each(function(b){n(this).addClass(a.call(this,b,this.className))});if(h)for(b=(a||"").match(E)||[];j>i;i++)if(c=this[i],d=1===c.nodeType&&(c.className?(" "+c.className+" ").replace(ac," "):" ")){f=0;while(e=b[f++])d.indexOf(" "+e+" ")<0&&(d+=e+" ");g=n.trim(d),c.className!==g&&(c.className=g)}return this},removeClass:function(a){var b,c,d,e,f,g,h=0===arguments.length||"string"==typeof a&&a,i=0,j=this.length;if(n.isFunction(a))return this.each(function(b){n(this).removeClass(a.call(this,b,this.className))});if(h)for(b=(a||"").match(E)||[];j>i;i++)if(c=this[i],d=1===c.nodeType&&(c.className?(" "+c.className+" ").replace(ac," "):"")){f=0;while(e=b[f++])while(d.indexOf(" "+e+" ")>=0)d=d.replace(" "+e+" "," ");g=a?n.trim(d):"",c.className!==g&&(c.className=g)}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):this.each(n.isFunction(a)?function(c){n(this).toggleClass(a.call(this,c,this.className,b),b)}:function(){if("string"===c){var b,d=0,e=n(this),f=a.match(E)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else(c===U||"boolean"===c)&&(this.className&&L.set(this,"__className__",this.className),this.className=this.className||a===!1?"":L.get(this,"__className__")||"")})},hasClass:function(a){for(var b=" "+a+" ",c=0,d=this.length;d>c;c++)if(1===this[c].nodeType&&(" "+this[c].className+" ").replace(ac," ").indexOf(b)>=0)return!0;return!1}});var bc=/\r/g;n.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=n.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,n(this).val()):a,null==e?e="":"number"==typeof e?e+="":n.isArray(e)&&(e=n.map(e,function(a){return null==a?"":a+""})),b=n.valHooks[this.type]||n.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=n.valHooks[e.type]||n.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(bc,""):null==c?"":c)}}}),n.extend({valHooks:{option:{get:function(a){var b=n.find.attr(a,"value");return null!=b?b:n.trim(n.text(a))}},select:{get:function(a){for(var b,c,d=a.options,e=a.selectedIndex,f="select-one"===a.type||0>e,g=f?null:[],h=f?e+1:d.length,i=0>e?h:f?e:0;h>i;i++)if(c=d[i],!(!c.selected&&i!==e||(k.optDisabled?c.disabled:null!==c.getAttribute("disabled"))||c.parentNode.disabled&&n.nodeName(c.parentNode,"optgroup"))){if(b=n(c).val(),f)return b;g.push(b)}return g},set:function(a,b){var c,d,e=a.options,f=n.makeArray(b),g=e.length;while(g--)d=e[g],(d.selected=n.inArray(d.value,f)>=0)&&(c=!0);return c||(a.selectedIndex=-1),f}}}}),n.each(["radio","checkbox"],function(){n.valHooks[this]={set:function(a,b){return n.isArray(b)?a.checked=n.inArray(n(a).val(),b)>=0:void 0}},k.checkOn||(n.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})}),n.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(a,b){n.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),n.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)},bind:function(a,b,c){return this.on(a,null,b,c)},unbind:function(a,b){return this.off(a,null,b)},delegate:function(a,b,c,d){return this.on(b,a,c,d)},undelegate:function(a,b,c){return 1===arguments.length?this.off(a,"**"):this.off(b,a||"**",c)}});var cc=n.now(),dc=/\?/;n.parseJSON=function(a){return JSON.parse(a+"")},n.parseXML=function(a){var b,c;if(!a||"string"!=typeof a)return null;try{c=new DOMParser,b=c.parseFromString(a,"text/xml")}catch(d){b=void 0}return(!b||b.getElementsByTagName("parsererror").length)&&n.error("Invalid XML: "+a),b};var ec=/#.*$/,fc=/([?&])_=[^&]*/,gc=/^(.*?):[ \t]*([^\r\n]*)$/gm,hc=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,ic=/^(?:GET|HEAD)$/,jc=/^\/\//,kc=/^([\w.+-]+:)(?:\/\/(?:[^\/?#]*@|)([^\/?#:]*)(?::(\d+)|)|)/,lc={},mc={},nc="*/".concat("*"),oc=a.location.href,pc=kc.exec(oc.toLowerCase())||[];function qc(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.toLowerCase().match(E)||[];if(n.isFunction(c))while(d=f[e++])"+"===d[0]?(d=d.slice(1)||"*",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function rc(a,b,c,d){var e={},f=a===mc;function g(h){var i;return e[h]=!0,n.each(a[h]||[],function(a,h){var j=h(b,c,d);return"string"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e["*"]&&g("*")}function sc(a,b){var c,d,e=n.ajaxSettings.flatOptions||{};for(c in b)void 0!==b[c]&&((e[c]?a:d||(d={}))[c]=b[c]);return d&&n.extend(!0,a,d),a}function tc(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while("*"===i[0])i.shift(),void 0===d&&(d=a.mimeType||b.getResponseHeader("Content-Type"));if(d)for(e in h)if(h[e]&&h[e].test(d)){i.unshift(e);break}if(i[0]in c)f=i[0];else{for(e in c){if(!i[0]||a.converters[e+" "+i[0]]){f=e;break}g||(g=e)}f=f||g}return f?(f!==i[0]&&i.unshift(f),c[f]):void 0}function uc(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.converters[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if("*"===f)f=i;else if("*"!==i&&i!==f){if(g=j[i+" "+f]||j["* "+f],!g)for(e in j)if(h=e.split(" "),h[1]===f&&(g=j[i+" "+h[0]]||j["* "+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a["throws"])b=g(b);else try{b=g(b)}catch(l){return{state:"parsererror",error:g?l:"No conversion from "+i+" to "+f}}}return{state:"success",data:b}}n.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:oc,type:"GET",isLocal:hc.test(pc[1]),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":nc,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":n.parseJSON,"text xml":n.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?sc(sc(a,n.ajaxSettings),b):sc(n.ajaxSettings,a)},ajaxPrefilter:qc(lc),ajaxTransport:qc(mc),ajax:function(a,b){"object"==typeof a&&(b=a,a=void 0),b=b||{};var c,d,e,f,g,h,i,j,k=n.ajaxSetup({},b),l=k.context||k,m=k.context&&(l.nodeType||l.jquery)?n(l):n.event,o=n.Deferred(),p=n.Callbacks("once memory"),q=k.statusCode||{},r={},s={},t=0,u="canceled",v={readyState:0,getResponseHeader:function(a){var b;if(2===t){if(!f){f={};while(b=gc.exec(e))f[b[1].toLowerCase()]=b[2]}b=f[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return 2===t?e:null},setRequestHeader:function(a,b){var c=a.toLowerCase();return t||(a=s[c]=s[c]||a,r[a]=b),this},overrideMimeType:function(a){return t||(k.mimeType=a),this},statusCode:function(a){var b;if(a)if(2>t)for(b in a)q[b]=[q[b],a[b]];else v.always(a[v.status]);return this},abort:function(a){var b=a||u;return c&&c.abort(b),x(0,b),this}};if(o.promise(v).complete=p.add,v.success=v.done,v.error=v.fail,k.url=((a||k.url||oc)+"").replace(ec,"").replace(jc,pc[1]+"//"),k.type=b.method||b.type||k.method||k.type,k.dataTypes=n.trim(k.dataType||"*").toLowerCase().match(E)||[""],null==k.crossDomain&&(h=kc.exec(k.url.toLowerCase()),k.crossDomain=!(!h||h[1]===pc[1]&&h[2]===pc[2]&&(h[3]||("http:"===h[1]?"80":"443"))===(pc[3]||("http:"===pc[1]?"80":"443")))),k.data&&k.processData&&"string"!=typeof k.data&&(k.data=n.param(k.data,k.traditional)),rc(lc,k,b,v),2===t)return v;i=n.event&&k.global,i&&0===n.active++&&n.event.trigger("ajaxStart"),k.type=k.type.toUpperCase(),k.hasContent=!ic.test(k.type),d=k.url,k.hasContent||(k.data&&(d=k.url+=(dc.test(d)?"&":"?")+k.data,delete k.data),k.cache===!1&&(k.url=fc.test(d)?d.replace(fc,"$1_="+cc++):d+(dc.test(d)?"&":"?")+"_="+cc++)),k.ifModified&&(n.lastModified[d]&&v.setRequestHeader("If-Modified-Since",n.lastModified[d]),n.etag[d]&&v.setRequestHeader("If-None-Match",n.etag[d])),(k.data&&k.hasContent&&k.contentType!==!1||b.contentType)&&v.setRequestHeader("Content-Type",k.contentType),v.setRequestHeader("Accept",k.dataTypes[0]&&k.accepts[k.dataTypes[0]]?k.accepts[k.dataTypes[0]]+("*"!==k.dataTypes[0]?", "+nc+"; q=0.01":""):k.accepts["*"]);for(j in k.headers)v.setRequestHeader(j,k.headers[j]);if(k.beforeSend&&(k.beforeSend.call(l,v,k)===!1||2===t))return v.abort();u="abort";for(j in{success:1,error:1,complete:1})v[j](k[j]);if(c=rc(mc,k,b,v)){v.readyState=1,i&&m.trigger("ajaxSend",[v,k]),k.async&&k.timeout>0&&(g=setTimeout(function(){v.abort("timeout")},k.timeout));try{t=1,c.send(r,x)}catch(w){if(!(2>t))throw w;x(-1,w)}}else x(-1,"No Transport");function x(a,b,f,h){var j,r,s,u,w,x=b;2!==t&&(t=2,g&&clearTimeout(g),c=void 0,e=h||"",v.readyState=a>0?4:0,j=a>=200&&300>a||304===a,f&&(u=tc(k,v,f)),u=uc(k,u,v,j),j?(k.ifModified&&(w=v.getResponseHeader("Last-Modified"),w&&(n.lastModified[d]=w),w=v.getResponseHeader("etag"),w&&(n.etag[d]=w)),204===a||"HEAD"===k.type?x="nocontent":304===a?x="notmodified":(x=u.state,r=u.data,s=u.error,j=!s)):(s=x,(a||!x)&&(x="error",0>a&&(a=0))),v.status=a,v.statusText=(b||x)+"",j?o.resolveWith(l,[r,x,v]):o.rejectWith(l,[v,x,s]),v.statusCode(q),q=void 0,i&&m.trigger(j?"ajaxSuccess":"ajaxError",[v,k,j?r:s]),p.fireWith(l,[v,x]),i&&(m.trigger("ajaxComplete",[v,k]),--n.active||n.event.trigger("ajaxStop")))}return v},getJSON:function(a,b,c){return n.get(a,b,c,"json")},getScript:function(a,b){return n.get(a,void 0,b,"script")}}),n.each(["get","post"],function(a,b){n[b]=function(a,c,d,e){return n.isFunction(c)&&(e=e||d,d=c,c=void 0),n.ajax({url:a,type:b,dataType:e,data:c,success:d})}}),n._evalUrl=function(a){return n.ajax({url:a,type:"GET",dataType:"script",async:!1,global:!1,"throws":!0})},n.fn.extend({wrapAll:function(a){var b;return n.isFunction(a)?this.each(function(b){n(this).wrapAll(a.call(this,b))}):(this[0]&&(b=n(a,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstElementChild)a=a.firstElementChild;return a}).append(this)),this)},wrapInner:function(a){return this.each(n.isFunction(a)?function(b){n(this).wrapInner(a.call(this,b))}:function(){var b=n(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=n.isFunction(a);return this.each(function(c){n(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(){return this.parent().each(function(){n.nodeName(this,"body")||n(this).replaceWith(this.childNodes)}).end()}}),n.expr.filters.hidden=function(a){return a.offsetWidth<=0&&a.offsetHeight<=0},n.expr.filters.visible=function(a){return!n.expr.filters.hidden(a)};var vc=/%20/g,wc=/\[\]$/,xc=/\r?\n/g,yc=/^(?:submit|button|image|reset|file)$/i,zc=/^(?:input|select|textarea|keygen)/i;function Ac(a,b,c,d){var e;if(n.isArray(b))n.each(b,function(b,e){c||wc.test(a)?d(a,e):Ac(a+"["+("object"==typeof e?b:"")+"]",e,c,d)});else if(c||"object"!==n.type(b))d(a,b);else for(e in b)Ac(a+"["+e+"]",b[e],c,d)}n.param=function(a,b){var c,d=[],e=function(a,b){b=n.isFunction(b)?b():null==b?"":b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};if(void 0===b&&(b=n.ajaxSettings&&n.ajaxSettings.traditional),n.isArray(a)||a.jquery&&!n.isPlainObject(a))n.each(a,function(){e(this.name,this.value)});else for(c in a)Ac(c,a[c],b,e);return d.join("&").replace(vc,"+")},n.fn.extend({serialize:function(){return n.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=n.prop(this,"elements");return a?n.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!n(this).is(":disabled")&&zc.test(this.nodeName)&&!yc.test(a)&&(this.checked||!T.test(a))}).map(function(a,b){var c=n(this).val();return null==c?null:n.isArray(c)?n.map(c,function(a){return{name:b.name,value:a.replace(xc,"\r\n")}}):{name:b.name,value:c.replace(xc,"\r\n")}}).get()}}),n.ajaxSettings.xhr=function(){try{return new XMLHttpRequest}catch(a){}};var Bc=0,Cc={},Dc={0:200,1223:204},Ec=n.ajaxSettings.xhr();a.attachEvent&&a.attachEvent("onunload",function(){for(var a in Cc)Cc[a]()}),k.cors=!!Ec&&"withCredentials"in Ec,k.ajax=Ec=!!Ec,n.ajaxTransport(function(a){var b;return k.cors||Ec&&!a.crossDomain?{send:function(c,d){var e,f=a.xhr(),g=++Bc;if(f.open(a.type,a.url,a.async,a.username,a.password),a.xhrFields)for(e in a.xhrFields)f[e]=a.xhrFields[e];a.mimeType&&f.overrideMimeType&&f.overrideMimeType(a.mimeType),a.crossDomain||c["X-Requested-With"]||(c["X-Requested-With"]="XMLHttpRequest");for(e in c)f.setRequestHeader(e,c[e]);b=function(a){return function(){b&&(delete Cc[g],b=f.onload=f.onerror=null,"abort"===a?f.abort():"error"===a?d(f.status,f.statusText):d(Dc[f.status]||f.status,f.statusText,"string"==typeof f.responseText?{text:f.responseText}:void 0,f.getAllResponseHeaders()))}},f.onload=b(),f.onerror=b("error"),b=Cc[g]=b("abort");try{f.send(a.hasContent&&a.data||null)}catch(h){if(b)throw h}},abort:function(){b&&b()}}:void 0}),n.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/(?:java|ecma)script/},converters:{"text script":function(a){return n.globalEval(a),a}}}),n.ajaxPrefilter("script",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type="GET")}),n.ajaxTransport("script",function(a){if(a.crossDomain){var b,c;return{send:function(d,e){b=n("<script>").prop({async:!0,charset:a.scriptCharset,src:a.url}).on("load error",c=function(a){b.remove(),c=null,a&&e("error"===a.type?404:200,a.type)}),l.head.appendChild(b[0])},abort:function(){c&&c()}}}});var Fc=[],Gc=/(=)\?(?=&|$)|\?\?/;n.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var a=Fc.pop()||n.expando+"_"+cc++;return this[a]=!0,a}}),n.ajaxPrefilter("json jsonp",function(b,c,d){var e,f,g,h=b.jsonp!==!1&&(Gc.test(b.url)?"url":"string"==typeof b.data&&!(b.contentType||"").indexOf("application/x-www-form-urlencoded")&&Gc.test(b.data)&&"data");return h||"jsonp"===b.dataTypes[0]?(e=b.jsonpCallback=n.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,h?b[h]=b[h].replace(Gc,"$1"+e):b.jsonp!==!1&&(b.url+=(dc.test(b.url)?"&":"?")+b.jsonp+"="+e),b.converters["script json"]=function(){return g||n.error(e+" was not called"),g[0]},b.dataTypes[0]="json",f=a[e],a[e]=function(){g=arguments},d.always(function(){a[e]=f,b[e]&&(b.jsonpCallback=c.jsonpCallback,Fc.push(e)),g&&n.isFunction(f)&&f(g[0]),g=f=void 0}),"script"):void 0}),n.parseHTML=function(a,b,c){if(!a||"string"!=typeof a)return null;"boolean"==typeof b&&(c=b,b=!1),b=b||l;var d=v.exec(a),e=!c&&[];return d?[b.createElement(d[1])]:(d=n.buildFragment([a],b,e),e&&e.length&&n(e).remove(),n.merge([],d.childNodes))};var Hc=n.fn.load;n.fn.load=function(a,b,c){if("string"!=typeof a&&Hc)return Hc.apply(this,arguments);var d,e,f,g=this,h=a.indexOf(" ");return h>=0&&(d=n.trim(a.slice(h)),a=a.slice(0,h)),n.isFunction(b)?(c=b,b=void 0):b&&"object"==typeof b&&(e="POST"),g.length>0&&n.ajax({url:a,type:e,dataType:"html",data:b}).done(function(a){f=arguments,g.html(d?n("<div>").append(n.parseHTML(a)).find(d):a)}).complete(c&&function(a,b){g.each(c,f||[a.responseText,b,a])}),this},n.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(a,b){n.fn[b]=function(a){return this.on(b,a)}}),n.expr.filters.animated=function(a){return n.grep(n.timers,function(b){return a===b.elem}).length};var Ic=a.document.documentElement;function Jc(a){return n.isWindow(a)?a:9===a.nodeType&&a.defaultView}n.offset={setOffset:function(a,b,c){var d,e,f,g,h,i,j,k=n.css(a,"position"),l=n(a),m={};"static"===k&&(a.style.position="relative"),h=l.offset(),f=n.css(a,"top"),i=n.css(a,"left"),j=("absolute"===k||"fixed"===k)&&(f+i).indexOf("auto")>-1,j?(d=l.position(),g=d.top,e=d.left):(g=parseFloat(f)||0,e=parseFloat(i)||0),n.isFunction(b)&&(b=b.call(a,c,h)),null!=b.top&&(m.top=b.top-h.top+g),null!=b.left&&(m.left=b.left-h.left+e),"using"in b?b.using.call(a,m):l.css(m)}},n.fn.extend({offset:function(a){if(arguments.length)return void 0===a?this:this.each(function(b){n.offset.setOffset(this,a,b)});var b,c,d=this[0],e={top:0,left:0},f=d&&d.ownerDocument;if(f)return b=f.documentElement,n.contains(b,d)?(typeof d.getBoundingClientRect!==U&&(e=d.getBoundingClientRect()),c=Jc(f),{top:e.top+c.pageYOffset-b.clientTop,left:e.left+c.pageXOffset-b.clientLeft}):e},position:function(){if(this[0]){var a,b,c=this[0],d={top:0,left:0};return"fixed"===n.css(c,"position")?b=c.getBoundingClientRect():(a=this.offsetParent(),b=this.offset(),n.nodeName(a[0],"html")||(d=a.offset()),d.top+=n.css(a[0],"borderTopWidth",!0),d.left+=n.css(a[0],"borderLeftWidth",!0)),{top:b.top-d.top-n.css(c,"marginTop",!0),left:b.left-d.left-n.css(c,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||Ic;while(a&&!n.nodeName(a,"html")&&"static"===n.css(a,"position"))a=a.offsetParent;return a||Ic})}}),n.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(b,c){var d="pageYOffset"===c;n.fn[b]=function(e){return J(this,function(b,e,f){var g=Jc(b);return void 0===f?g?g[c]:b[e]:void(g?g.scrollTo(d?a.pageXOffset:f,d?f:a.pageYOffset):b[e]=f)},b,e,arguments.length,null)}}),n.each(["top","left"],function(a,b){n.cssHooks[b]=yb(k.pixelPosition,function(a,c){return c?(c=xb(a,b),vb.test(c)?n(a).position()[b]+"px":c):void 0})}),n.each({Height:"height",Width:"width"},function(a,b){n.each({padding:"inner"+a,content:b,"":"outer"+a},function(c,d){n.fn[d]=function(d,e){var f=arguments.length&&(c||"boolean"!=typeof d),g=c||(d===!0||e===!0?"margin":"border");return J(this,function(b,c,d){var e;return n.isWindow(b)?b.document.documentElement["client"+a]:9===b.nodeType?(e=b.documentElement,Math.max(b.body["scroll"+a],e["scroll"+a],b.body["offset"+a],e["offset"+a],e["client"+a])):void 0===d?n.css(b,c,g):n.style(b,c,d,g)},b,f?d:void 0,f,null)}})}),n.fn.size=function(){return this.length},n.fn.andSelf=n.fn.addBack,"function"==typeof define&&define.amd&&define("jquery",[],function(){return n});var Kc=a.jQuery,Lc=a.$;return n.noConflict=function(b){return a.$===n&&(a.$=Lc),b&&a.jQuery===n&&(a.jQuery=Kc),n},typeof b===U&&(a.jQuery=a.$=n),n});
diff --git a/src/pixelated/assets/login.html b/src/pixelated/assets/login.html
new file mode 100644
index 00000000..ff103f03
--- /dev/null
+++ b/src/pixelated/assets/login.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<html xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1">
+<head>
+ <title>Pixelated - Login</title>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+ <link rel="icon" type="image/png" href="/startup-assets/favicon.png" />
+ <link rel="stylesheet" type="text/css" href="/startup-assets/normalize.min.css" />
+ <link rel="stylesheet" type="text/css" href="/startup-assets/pixelated.css" />
+ <link rel="stylesheet" type="text/css" href="/startup-assets/opensans.css" />
+</head>
+<body>
+<div class="content">
+ <div class="login">
+
+ <img class="logo" src="/startup-assets/pixelated-logo-orange.svg" alt="Pixelated logo"/>
+
+ <p t:render="error_msg" class="error" ></p>
+
+
+ <form class="standard" id="login_form" action="/login" method="post">
+ <input type="text" name="username" id="email" class="text-field" placeholder="username" tabindex="1"
+ autofocus="" />
+ <input type="password" name="password" id="password" class="text-field" placeholder="password"
+ tabindex="2" autocomplete="off" />
+
+ <input type="submit" name="login" value="Login" class="button" tabindex="3" />
+
+ </form>
+ </div>
+ <div class="disclaimer">
+ <div t:render="disclaimer"></div>
+ </div>
+</div>
+</body>
+
+</html>
diff --git a/src/pixelated/assets/normalize.min.css b/src/pixelated/assets/normalize.min.css
new file mode 100644
index 00000000..d3c7f4d5
--- /dev/null
+++ b/src/pixelated/assets/normalize.min.css
@@ -0,0 +1 @@
+/*! normalize.css v3.0.1 | MIT License | git.io/normalize */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0}input[type="number"]::-webkit-inner-spin-button,input[type="number"]::-webkit-outer-spin-button{height:auto}input[type="search"]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:bold}table{border-collapse:collapse;border-spacing:0}td,th{padding:0} \ No newline at end of file
diff --git a/src/pixelated/assets/opensans.css b/src/pixelated/assets/opensans.css
new file mode 100644
index 00000000..a42f346c
--- /dev/null
+++ b/src/pixelated/assets/opensans.css
@@ -0,0 +1,69 @@
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 300;
+ src: local("Open Sans Light"), local("OpenSans-Light"), url("/fonts/OpenSans-Light.woff") format("woff");
+}
+
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 400;
+ src: local("Open Sans"), local("OpenSans"), url("/fonts/OpenSans.woff") format("woff");
+}
+
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 600;
+ src: local("Open Sans Semibold"), local("OpenSans-Semibold"), url("/fonts/OpenSans-Semibold.woff") format("woff");
+}
+
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 700;
+ src: local("Open Sans Bold"), local("OpenSans-Bold"), url("/fonts/OpenSans-Bold.woff") format("woff");
+}
+
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 800;
+ src: local("Open Sans Extrabold"), local("OpenSans-Extrabold"), url("/fonts/OpenSans-Extrabold.woff") format("woff");
+}
+
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 300;
+ src: local("Open Sans Light Italic"), local("OpenSansLight-Italic"), url("/fonts/OpenSansLight-Italic.woff") format("woff");
+}
+
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 400;
+ src: local("Open Sans Italic"), local("OpenSans-Italic"), url("/fonts/OpenSans-Italic.woff") format("woff");
+}
+
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 600;
+ src: local("Open Sans Semibold Italic"), local("OpenSans-SemiboldItalic"), url("/fonts/OpenSans-SemiboldItalic.woff ") format("woff");
+}
+
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 700;
+ src: local("Open Sans Bold Italic"), local("OpenSans-BoldItalic"), url("/fonts/OpenSans-BoldItalic.woff") format("woff");
+}
+
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 800;
+ src: local("Open Sans Extrabold Italic"), local("OpenSans-ExtraboldItalic"), url("/fonts/OpenSans-ExtraboldItalic.woff") format("woff");
+}
diff --git a/src/pixelated/assets/pixelated-logo-orange.svg b/src/pixelated/assets/pixelated-logo-orange.svg
new file mode 100644
index 00000000..7e0ef43d
--- /dev/null
+++ b/src/pixelated/assets/pixelated-logo-orange.svg
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.0.4, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ width="509.707px" height="142.439px" viewBox="0 0 509.707 142.439" enable-background="new 0 0 509.707 142.439"
+ xml:space="preserve">
+<g>
+ <path fill="#F9A731" d="M0,35.469v71.365l62.837,35.605l62.833-35.605V35.469L62.813,0L0,35.469z M60.262,116.617L23.735,96.332
+ V52.46l36.586,20.999L60.262,116.617z M101.936,96.332l-36.148,20.285l0.067-43.123l36.081-21.034V96.332z M101.936,46.44
+ L62.951,69.553L23.733,46.44l39.218-21.131L101.936,46.44z"/>
+ <path fill="#F9A731" d="M169.505,42.332h-19.968v59.328h13.52V79.655h6.448c11.579,0,20.279-6.832,20.279-19.056
+ C189.784,48.302,181.084,42.332,169.505,42.332z M166.866,68.868h-3.809v-15.75h3.809c5.323,0,10.357,1.798,10.357,7.91
+ C177.224,67.07,172.189,68.868,166.866,68.868z"/>
+ <rect x="194.309" y="42.332" fill="#F9A731" width="13.52" height="59.328"/>
+ <polygon fill="#F9A731" points="266.516,42.332 249.689,42.332 238.759,58.514 227.827,42.332 211.721,42.332 230.417,69.73
+ 210.228,101.66 226.982,101.66 238.759,81.453 250.534,101.66 268.01,101.66 247.099,69.73 "/>
+ <polygon fill="#F9A731" points="270.128,101.66 304.069,101.66 304.069,89.795 283.647,89.795 283.647,77.857 303.207,77.857
+ 303.207,65.991 283.647,65.991 283.647,54.199 304.069,54.199 304.069,42.332 270.128,42.332 "/>
+ <path fill="#F9A731" d="M354.807,42.332l-19.156,47.463H322.33V42.332h-13.52v59.328h22.053h11.888h2.636l4.386-11.865h22.578
+ l4.391,11.865h14.524l-23.944-59.328H354.807z M354.377,77.928l6.614-17.257h0.145l6.615,17.257H354.377z"/>
+ <polygon fill="#F9A731" points="379.939,54.199 394.073,54.199 394.073,101.66 407.592,101.66 407.592,54.199 421.687,54.199
+ 421.687,42.332 379.939,42.332 "/>
+ <polygon fill="#F9A731" points="426.265,101.66 460.207,101.66 460.207,89.795 439.785,89.795 439.785,77.857 459.344,77.857
+ 459.344,65.991 439.785,65.991 439.785,54.199 460.207,54.199 460.207,42.332 426.265,42.332 "/>
+ <path fill="#F9A731" d="M479.792,42.332h-14.94v59.328h14.94c16.324,0,29.914-12.37,29.914-29.699
+ C509.707,54.701,496.044,42.332,479.792,42.332z M480.457,89.577h-2.084V54.414h2.084c10.067,0,16.9,7.695,16.9,17.619
+ C497.285,81.955,490.455,89.577,480.457,89.577z"/>
+</g>
+</svg>
diff --git a/src/pixelated/assets/pixelated.css b/src/pixelated/assets/pixelated.css
new file mode 100644
index 00000000..b3e1d16e
--- /dev/null
+++ b/src/pixelated/assets/pixelated.css
@@ -0,0 +1,128 @@
+body {
+ font-family: "Open Sans", "Microsoft YaHei", "Hiragino Sans GB", "Hiragino Sans GB W3", "微软雅黑", "Helvetica Neue", Arial, sans-serif;
+ background-color: #EAEAEA;
+ height: 100vh;
+ color: #3E3A37;
+
+ background-image: url("hive-bg.png");
+ background-repeat: repeat;
+}
+
+.content {
+ height: 100vh;
+ width: 100%;
+}
+
+.error {
+ color: #D72A25;
+ margin: 10px 0 0 0;
+}
+
+.message-panel {
+ width: 100%;
+ margin: 10px auto;
+ z-index: 10000;
+ text-align: center;
+ }
+
+.message-panel span{
+ background: #F7E8AF;
+ padding: 5px 60px;
+ border: 1px solid #f2db81;
+ color: #987b0f;
+ box-shadow: 1px 1px 3px #69560b;
+}
+
+.message-panel.message-panel-small span{
+ padding: 5px 0px;
+ display: inline-block;
+ width: 100%;
+}
+
+.login {
+ display: block;
+ width: 240px;
+ margin: auto;
+ padding: 45px 40px 35px 40px;
+ background-color: #FFF;
+ margin-top: 2%;
+ margin-bottom: 2%;
+}
+
+form#login_form {
+ padding: 10px 0;
+}
+
+.disclaimer {
+ display: block;
+ margin-top: 10%;
+ width: 50%;
+ margin: auto;
+ background-color: #2BA6CB;
+ color: #FFFFFF;
+ font-weight: 300;
+ font-size: 0.8rem;
+ padding: 1em;
+ margin-bottom: 20px;
+}
+
+.disclaimer li {
+ margin-top: 1em;
+}
+
+.logo {
+ width: 100%;
+ height: auto;
+}
+
+input {
+ display: block;
+ margin: 10px 0;
+ padding-left: 5px;
+}
+
+input.text-field {
+ width: 97%;
+}
+
+button, .button, input[type=button] {
+ cursor: pointer;
+ margin: 0 0 1.25rem;
+ border: none;
+ position: relative;
+ text-decoration: none;
+ text-align: center;
+ -webkit-appearance: none;
+ display: inline-block;
+ padding: 0.4rem 1.1rem;
+ font-size: 0.9rem;
+ background-color: #2ba6cb;
+ border-color: #2285a2;
+ color: white;
+ -webkit-border-radius: 2px;
+ -moz-border-radius: 2px;
+ -ms-border-radius: 2px;
+ -o-border-radius: 2px;
+ border-radius: 2px;
+}
+
+button:hover, button:focus, .button:hover, .button:focus, input[type=button]:hover, input[type=button]:focus {
+ background-color: #2285a2;
+ outline: none;
+ color: white;
+}
+
+ul.accounts {
+ margin-bottom: 5%;
+}
+
+ul.accounts li {
+ display: inline-block;
+ list-style: none;
+ margin-right: 35px;
+ margin-top: 0px;
+}
+
+ul.accounts li span {
+ font-weight: bold;
+}
diff --git a/src/pixelated/assets/snap.svg-min.js b/src/pixelated/assets/snap.svg-min.js
new file mode 100644
index 00000000..ca9601ab
--- /dev/null
+++ b/src/pixelated/assets/snap.svg-min.js
@@ -0,0 +1,20 @@
+// Snap.svg 0.3.0
+//
+// Copyright (c) 2013 Adobe Systems Incorporated. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// build: 2014-06-03
+!function(a){var b,c,d="0.4.2",e="hasOwnProperty",f=/[\.\/]/,g=/\s*,\s*/,h="*",i=function(a,b){return a-b},j={n:{}},k=function(){for(var a=0,b=this.length;b>a;a++)if("undefined"!=typeof this[a])return this[a]},l=function(){for(var a=this.length;--a;)if("undefined"!=typeof this[a])return this[a]},m=function(a,d){a=String(a);var e,f=c,g=Array.prototype.slice.call(arguments,2),h=m.listeners(a),j=0,n=[],o={},p=[],q=b;p.firstDefined=k,p.lastDefined=l,b=a,c=0;for(var r=0,s=h.length;s>r;r++)"zIndex"in h[r]&&(n.push(h[r].zIndex),h[r].zIndex<0&&(o[h[r].zIndex]=h[r]));for(n.sort(i);n[j]<0;)if(e=o[n[j++]],p.push(e.apply(d,g)),c)return c=f,p;for(r=0;s>r;r++)if(e=h[r],"zIndex"in e)if(e.zIndex==n[j]){if(p.push(e.apply(d,g)),c)break;do if(j++,e=o[n[j]],e&&p.push(e.apply(d,g)),c)break;while(e)}else o[e.zIndex]=e;else if(p.push(e.apply(d,g)),c)break;return c=f,b=q,p};m._events=j,m.listeners=function(a){var b,c,d,e,g,i,k,l,m=a.split(f),n=j,o=[n],p=[];for(e=0,g=m.length;g>e;e++){for(l=[],i=0,k=o.length;k>i;i++)for(n=o[i].n,c=[n[m[e]],n[h]],d=2;d--;)b=c[d],b&&(l.push(b),p=p.concat(b.f||[]));o=l}return p},m.on=function(a,b){if(a=String(a),"function"!=typeof b)return function(){};for(var c=a.split(g),d=0,e=c.length;e>d;d++)!function(a){for(var c,d=a.split(f),e=j,g=0,h=d.length;h>g;g++)e=e.n,e=e.hasOwnProperty(d[g])&&e[d[g]]||(e[d[g]]={n:{}});for(e.f=e.f||[],g=0,h=e.f.length;h>g;g++)if(e.f[g]==b){c=!0;break}!c&&e.f.push(b)}(c[d]);return function(a){+a==+a&&(b.zIndex=+a)}},m.f=function(a){var b=[].slice.call(arguments,1);return function(){m.apply(null,[a,null].concat(b).concat([].slice.call(arguments,0)))}},m.stop=function(){c=1},m.nt=function(a){return a?new RegExp("(?:\\.|\\/|^)"+a+"(?:\\.|\\/|$)").test(b):b},m.nts=function(){return b.split(f)},m.off=m.unbind=function(a,b){if(!a)return void(m._events=j={n:{}});var c=a.split(g);if(c.length>1)for(var d=0,i=c.length;i>d;d++)m.off(c[d],b);else{c=a.split(f);var k,l,n,d,i,o,p,q=[j];for(d=0,i=c.length;i>d;d++)for(o=0;o<q.length;o+=n.length-2){if(n=[o,1],k=q[o].n,c[d]!=h)k[c[d]]&&n.push(k[c[d]]);else for(l in k)k[e](l)&&n.push(k[l]);q.splice.apply(q,n)}for(d=0,i=q.length;i>d;d++)for(k=q[d];k.n;){if(b){if(k.f){for(o=0,p=k.f.length;p>o;o++)if(k.f[o]==b){k.f.splice(o,1);break}!k.f.length&&delete k.f}for(l in k.n)if(k.n[e](l)&&k.n[l].f){var r=k.n[l].f;for(o=0,p=r.length;p>o;o++)if(r[o]==b){r.splice(o,1);break}!r.length&&delete k.n[l].f}}else{delete k.f;for(l in k.n)k.n[e](l)&&k.n[l].f&&delete k.n[l].f}k=k.n}}},m.once=function(a,b){var c=function(){return m.unbind(a,c),b.apply(this,arguments)};return m.on(a,c)},m.version=d,m.toString=function(){return"You are running Eve "+d},"undefined"!=typeof module&&module.exports?module.exports=m:"function"==typeof define&&define.amd?define("eve",[],function(){return m}):a.eve=m}(this),function(a,b){"function"==typeof define&&define.amd?define(["eve"],function(c){return b(a,c)}):b(a,a.eve)}(this,function(a,b){var c=function(b){var c={},d=a.requestAnimationFrame||a.webkitRequestAnimationFrame||a.mozRequestAnimationFrame||a.oRequestAnimationFrame||a.msRequestAnimationFrame||function(a){setTimeout(a,16)},e=Array.isArray||function(a){return a instanceof Array||"[object Array]"==Object.prototype.toString.call(a)},f=0,g="M"+(+new Date).toString(36),h=function(){return g+(f++).toString(36)},i=Date.now||function(){return+new Date},j=function(a){var b=this;if(null==a)return b.s;var c=b.s-a;b.b+=b.dur*c,b.B+=b.dur*c,b.s=a},k=function(a){var b=this;return null==a?b.spd:void(b.spd=a)},l=function(a){var b=this;return null==a?b.dur:(b.s=b.s*a/b.dur,void(b.dur=a))},m=function(){var a=this;delete c[a.id],a.update(),b("mina.stop."+a.id,a)},n=function(){var a=this;a.pdif||(delete c[a.id],a.update(),a.pdif=a.get()-a.b)},o=function(){var a=this;a.pdif&&(a.b=a.get()-a.pdif,delete a.pdif,c[a.id]=a)},p=function(){var a,b=this;if(e(b.start)){a=[];for(var c=0,d=b.start.length;d>c;c++)a[c]=+b.start[c]+(b.end[c]-b.start[c])*b.easing(b.s)}else a=+b.start+(b.end-b.start)*b.easing(b.s);b.set(a)},q=function(){var a=0;for(var e in c)if(c.hasOwnProperty(e)){var f=c[e],g=f.get();a++,f.s=(g-f.b)/(f.dur/f.spd),f.s>=1&&(delete c[e],f.s=1,a--,function(a){setTimeout(function(){b("mina.finish."+a.id,a)})}(f)),f.update()}a&&d(q)},r=function(a,b,e,f,g,i,s){var t={id:h(),start:a,end:b,b:e,s:0,dur:f-e,spd:1,get:g,set:i,easing:s||r.linear,status:j,speed:k,duration:l,stop:m,pause:n,resume:o,update:p};c[t.id]=t;var u,v=0;for(u in c)if(c.hasOwnProperty(u)&&(v++,2==v))break;return 1==v&&d(q),t};return r.time=i,r.getById=function(a){return c[a]||null},r.linear=function(a){return a},r.easeout=function(a){return Math.pow(a,1.7)},r.easein=function(a){return Math.pow(a,.48)},r.easeinout=function(a){if(1==a)return 1;if(0==a)return 0;var b=.48-a/1.04,c=Math.sqrt(.1734+b*b),d=c-b,e=Math.pow(Math.abs(d),1/3)*(0>d?-1:1),f=-c-b,g=Math.pow(Math.abs(f),1/3)*(0>f?-1:1),h=e+g+.5;return 3*(1-h)*h*h+h*h*h},r.backin=function(a){if(1==a)return 1;var b=1.70158;return a*a*((b+1)*a-b)},r.backout=function(a){if(0==a)return 0;a-=1;var b=1.70158;return a*a*((b+1)*a+b)+1},r.elastic=function(a){return a==!!a?a:Math.pow(2,-10*a)*Math.sin(2*(a-.075)*Math.PI/.3)+1},r.bounce=function(a){var b,c=7.5625,d=2.75;return 1/d>a?b=c*a*a:2/d>a?(a-=1.5/d,b=c*a*a+.75):2.5/d>a?(a-=2.25/d,b=c*a*a+.9375):(a-=2.625/d,b=c*a*a+.984375),b},a.mina=r,r}("undefined"==typeof b?function(){}:b),d=function(){function d(a,b){if(a){if(a.tagName)return y(a);if(f(a,"array")&&d.set)return d.set.apply(d,a);if(a instanceof u)return a;if(null==b)return a=z.doc.querySelector(a),y(a)}return a=null==a?"100%":a,b=null==b?"100%":b,new x(a,b)}function e(a,b){if(b){if("#text"==a&&(a=z.doc.createTextNode(b.text||"")),"string"==typeof a&&(a=e(a)),"string"==typeof b)return"xlink:"==b.substring(0,6)?a.getAttributeNS(W,b.substring(6)):"xml:"==b.substring(0,4)?a.getAttributeNS(X,b.substring(4)):a.getAttribute(b);for(var c in b)if(b[A](c)){var d=B(b[c]);d?"xlink:"==c.substring(0,6)?a.setAttributeNS(W,c.substring(6),d):"xml:"==c.substring(0,4)?a.setAttributeNS(X,c.substring(4),d):a.setAttribute(c,d):a.removeAttribute(c)}}else a=z.doc.createElementNS(X,a);return a}function f(a,b){return b=B.prototype.toLowerCase.call(b),"finite"==b?isFinite(a):"array"==b&&(a instanceof Array||Array.isArray&&Array.isArray(a))?!0:"null"==b&&null===a||b==typeof a&&null!==a||"object"==b&&a===Object(a)||L.call(a).slice(8,-1).toLowerCase()==b}function h(a){if("function"==typeof a||Object(a)!==a)return a;var b=new a.constructor;for(var c in a)a[A](c)&&(b[c]=h(a[c]));return b}function i(a,b){for(var c=0,d=a.length;d>c;c++)if(a[c]===b)return a.push(a.splice(c,1)[0])}function j(a,b,c){function d(){var e=Array.prototype.slice.call(arguments,0),f=e.join("␀"),g=d.cache=d.cache||{},h=d.count=d.count||[];return g[A](f)?(i(h,f),c?c(g[f]):g[f]):(h.length>=1e3&&delete g[h.shift()],h.push(f),g[f]=a.apply(b,e),c?c(g[f]):g[f])}return d}function k(a,b,c,d,e,f){if(null==e){var g=a-c,h=b-d;return g||h?(180+180*E.atan2(-h,-g)/I+360)%360:0}return k(a,b,e,f)-k(c,d,e,f)}function l(a){return a%360*I/180}function m(a){return 180*a/I%360}function n(a){var b=[];return a=a.replace(/(?:^|\s)(\w+)\(([^)]+)\)/g,function(a,c,d){return d=d.split(/\s*,\s*|\s+/),"rotate"==c&&1==d.length&&d.push(0,0),"scale"==c&&(d.length>2?d=d.slice(0,2):2==d.length&&d.push(0,0),1==d.length&&d.push(d[0],0,0)),b.push("skewX"==c?["m",1,0,E.tan(l(d[0])),1,0,0]:"skewY"==c?["m",1,E.tan(l(d[0])),0,1,0,0]:[c.charAt(0)].concat(d)),a}),b}function o(a,b){var c=eb(a),e=new d.Matrix;if(c)for(var f=0,g=c.length;g>f;f++){var h,i,j,k,l,m=c[f],n=m.length,o=B(m[0]).toLowerCase(),p=m[0]!=o,q=p?e.invert():0;"t"==o&&2==n?e.translate(m[1],0):"t"==o&&3==n?p?(h=q.x(0,0),i=q.y(0,0),j=q.x(m[1],m[2]),k=q.y(m[1],m[2]),e.translate(j-h,k-i)):e.translate(m[1],m[2]):"r"==o?2==n?(l=l||b,e.rotate(m[1],l.x+l.width/2,l.y+l.height/2)):4==n&&(p?(j=q.x(m[2],m[3]),k=q.y(m[2],m[3]),e.rotate(m[1],j,k)):e.rotate(m[1],m[2],m[3])):"s"==o?2==n||3==n?(l=l||b,e.scale(m[1],m[n-1],l.x+l.width/2,l.y+l.height/2)):4==n?p?(j=q.x(m[2],m[3]),k=q.y(m[2],m[3]),e.scale(m[1],m[1],j,k)):e.scale(m[1],m[1],m[2],m[3]):5==n&&(p?(j=q.x(m[3],m[4]),k=q.y(m[3],m[4]),e.scale(m[1],m[2],j,k)):e.scale(m[1],m[2],m[3],m[4])):"m"==o&&7==n&&e.add(m[1],m[2],m[3],m[4],m[5],m[6])}return e}function p(a,b){if(null==b){var c=!0;if(b=a.node.getAttribute("linearGradient"==a.type||"radialGradient"==a.type?"gradientTransform":"pattern"==a.type?"patternTransform":"transform"),!b)return new d.Matrix;b=n(b)}else b=d._.rgTransform.test(b)?B(b).replace(/\.{3}|\u2026/g,a._.transform||J):n(b),f(b,"array")&&(b=d.path?d.path.toString.call(b):B(b)),a._.transform=b;var e=o(b,a.getBBox(1));return c?e:void(a.matrix=e)}function q(a){var b=a.node.ownerSVGElement&&y(a.node.ownerSVGElement)||a.node.parentNode&&y(a.node.parentNode)||d.select("svg")||d(0,0),c=b.select("defs"),e=null==c?!1:c.node;return e||(e=w("defs",b.node).node),e}function r(a){return a.node.ownerSVGElement&&y(a.node.ownerSVGElement)||d.select("svg")}function s(a,b,c){function d(a){if(null==a)return J;if(a==+a)return a;e(j,{width:a});try{return j.getBBox().width}catch(b){return 0}}function f(a){if(null==a)return J;if(a==+a)return a;e(j,{height:a});try{return j.getBBox().height}catch(b){return 0}}function g(d,e){null==b?i[d]=e(a.attr(d)||0):d==b&&(i=e(null==c?a.attr(d)||0:c))}var h=r(a).node,i={},j=h.querySelector(".svg---mgr");switch(j||(j=e("rect"),e(j,{x:-9e9,y:-9e9,width:10,height:10,"class":"svg---mgr",fill:"none"}),h.appendChild(j)),a.type){case"rect":g("rx",d),g("ry",f);case"image":g("width",d),g("height",f);case"text":g("x",d),g("y",f);break;case"circle":g("cx",d),g("cy",f),g("r",d);break;case"ellipse":g("cx",d),g("cy",f),g("rx",d),g("ry",f);break;case"line":g("x1",d),g("x2",d),g("y1",f),g("y2",f);break;case"marker":g("refX",d),g("markerWidth",d),g("refY",f),g("markerHeight",f);break;case"radialGradient":g("fx",d),g("fy",f);break;case"tspan":g("dx",d),g("dy",f);break;default:g(b,d)}return h.removeChild(j),i}function t(a){f(a,"array")||(a=Array.prototype.slice.call(arguments,0));for(var b=0,c=0,d=this.node;this[b];)delete this[b++];for(b=0;b<a.length;b++)"set"==a[b].type?a[b].forEach(function(a){d.appendChild(a.node)}):d.appendChild(a[b].node);var e=d.childNodes;for(b=0;b<e.length;b++)this[c++]=y(e[b]);return this}function u(a){if(a.snap in Y)return Y[a.snap];var b,c=this.id=V();try{b=a.ownerSVGElement}catch(d){}if(this.node=a,b&&(this.paper=new x(b)),this.type=a.tagName,this.anims={},this._={transform:[]},a.snap=c,Y[c]=this,"g"==this.type&&(this.add=t),this.type in{g:1,mask:1,pattern:1})for(var e in x.prototype)x.prototype[A](e)&&(this[e]=x.prototype[e])}function v(a){this.node=a}function w(a,b){var c=e(a);b.appendChild(c);var d=y(c);return d}function x(a,b){var c,d,f,g=x.prototype;if(a&&"svg"==a.tagName){if(a.snap in Y)return Y[a.snap];var h=a.ownerDocument;c=new u(a),d=a.getElementsByTagName("desc")[0],f=a.getElementsByTagName("defs")[0],d||(d=e("desc"),d.appendChild(h.createTextNode("Created with Snap")),c.node.appendChild(d)),f||(f=e("defs"),c.node.appendChild(f)),c.defs=f;for(var i in g)g[A](i)&&(c[i]=g[i]);c.paper=c.root=c}else c=w("svg",z.doc.body),e(c.node,{height:b,version:1.1,width:a,xmlns:X});return c}function y(a){return a?a instanceof u||a instanceof v?a:a.tagName&&"svg"==a.tagName.toLowerCase()?new x(a):a.tagName&&"object"==a.tagName.toLowerCase()&&"image/svg+xml"==a.type?new x(a.contentDocument.getElementsByTagName("svg")[0]):new u(a):a}d.version="0.3.0",d.toString=function(){return"Snap v"+this.version},d._={};var z={win:a,doc:a.document};d._.glob=z;var A="hasOwnProperty",B=String,C=parseFloat,D=parseInt,E=Math,F=E.max,G=E.min,H=E.abs,I=(E.pow,E.PI),J=(E.round,""),K=" ",L=Object.prototype.toString,M=/^\s*((#[a-f\d]{6})|(#[a-f\d]{3})|rgba?\(\s*([\d\.]+%?\s*,\s*[\d\.]+%?\s*,\s*[\d\.]+%?(?:\s*,\s*[\d\.]+%?)?)\s*\)|hsba?\(\s*([\d\.]+(?:deg|\xb0|%)?\s*,\s*[\d\.]+%?\s*,\s*[\d\.]+(?:%?\s*,\s*[\d\.]+)?%?)\s*\)|hsla?\(\s*([\d\.]+(?:deg|\xb0|%)?\s*,\s*[\d\.]+%?\s*,\s*[\d\.]+(?:%?\s*,\s*[\d\.]+)?%?)\s*\))\s*$/i,N=" \n \f\r   ᠎              \u2028\u2029",O=(d._.separator=new RegExp("[,"+N+"]+"),new RegExp("["+N+"]","g"),new RegExp("["+N+"]*,["+N+"]*")),P={hs:1,rg:1},Q=new RegExp("([a-z])["+N+",]*((-?\\d*\\.?\\d*(?:e[\\-+]?\\d+)?["+N+"]*,?["+N+"]*)+)","ig"),R=new RegExp("([rstm])["+N+",]*((-?\\d*\\.?\\d*(?:e[\\-+]?\\d+)?["+N+"]*,?["+N+"]*)+)","ig"),S=new RegExp("(-?\\d*\\.?\\d*(?:e[\\-+]?\\d+)?)["+N+"]*,?["+N+"]*","ig"),T=0,U="S"+(+new Date).toString(36),V=function(){return U+(T++).toString(36)},W="http://www.w3.org/1999/xlink",X="http://www.w3.org/2000/svg",Y={},Z=d.url=function(a){return"url('#"+a+"')"};d._.$=e,d._.id=V,d.format=function(){var a=/\{([^\}]+)\}/g,b=/(?:(?:^|\.)(.+?)(?=\[|\.|$|\()|\[('|")(.+?)\2\])(\(\))?/g,c=function(a,c,d){var e=d;return c.replace(b,function(a,b,c,d,f){b=b||d,e&&(b in e&&(e=e[b]),"function"==typeof e&&f&&(e=e()))}),e=(null==e||e==d?a:e)+""};return function(b,d){return B(b).replace(a,function(a,b){return c(a,b,d)})}}(),d._.clone=h,d._.cacher=j,d.rad=l,d.deg=m,d.angle=k,d.is=f,d.snapTo=function(a,b,c){if(c=f(c,"finite")?c:10,f(a,"array")){for(var d=a.length;d--;)if(H(a[d]-b)<=c)return a[d]}else{a=+a;var e=b%a;if(c>e)return b-e;if(e>a-c)return b-e+a}return b},d.getRGB=j(function(a){if(!a||(a=B(a)).indexOf("-")+1)return{r:-1,g:-1,b:-1,hex:"none",error:1,toString:bb};if("none"==a)return{r:-1,g:-1,b:-1,hex:"none",toString:bb};if(!(P[A](a.toLowerCase().substring(0,2))||"#"==a.charAt())&&(a=$(a)),!a)return{r:-1,g:-1,b:-1,hex:"none",error:1,toString:bb};var b,c,e,g,h,i,j=a.match(M);return j?(j[2]&&(e=D(j[2].substring(5),16),c=D(j[2].substring(3,5),16),b=D(j[2].substring(1,3),16)),j[3]&&(e=D((h=j[3].charAt(3))+h,16),c=D((h=j[3].charAt(2))+h,16),b=D((h=j[3].charAt(1))+h,16)),j[4]&&(i=j[4].split(O),b=C(i[0]),"%"==i[0].slice(-1)&&(b*=2.55),c=C(i[1]),"%"==i[1].slice(-1)&&(c*=2.55),e=C(i[2]),"%"==i[2].slice(-1)&&(e*=2.55),"rgba"==j[1].toLowerCase().slice(0,4)&&(g=C(i[3])),i[3]&&"%"==i[3].slice(-1)&&(g/=100)),j[5]?(i=j[5].split(O),b=C(i[0]),"%"==i[0].slice(-1)&&(b/=100),c=C(i[1]),"%"==i[1].slice(-1)&&(c/=100),e=C(i[2]),"%"==i[2].slice(-1)&&(e/=100),("deg"==i[0].slice(-3)||"°"==i[0].slice(-1))&&(b/=360),"hsba"==j[1].toLowerCase().slice(0,4)&&(g=C(i[3])),i[3]&&"%"==i[3].slice(-1)&&(g/=100),d.hsb2rgb(b,c,e,g)):j[6]?(i=j[6].split(O),b=C(i[0]),"%"==i[0].slice(-1)&&(b/=100),c=C(i[1]),"%"==i[1].slice(-1)&&(c/=100),e=C(i[2]),"%"==i[2].slice(-1)&&(e/=100),("deg"==i[0].slice(-3)||"°"==i[0].slice(-1))&&(b/=360),"hsla"==j[1].toLowerCase().slice(0,4)&&(g=C(i[3])),i[3]&&"%"==i[3].slice(-1)&&(g/=100),d.hsl2rgb(b,c,e,g)):(b=G(E.round(b),255),c=G(E.round(c),255),e=G(E.round(e),255),g=G(F(g,0),1),j={r:b,g:c,b:e,toString:bb},j.hex="#"+(16777216|e|c<<8|b<<16).toString(16).slice(1),j.opacity=f(g,"finite")?g:1,j)):{r:-1,g:-1,b:-1,hex:"none",error:1,toString:bb}},d),d.hsb=j(function(a,b,c){return d.hsb2rgb(a,b,c).hex}),d.hsl=j(function(a,b,c){return d.hsl2rgb(a,b,c).hex}),d.rgb=j(function(a,b,c,d){if(f(d,"finite")){var e=E.round;return"rgba("+[e(a),e(b),e(c),+d.toFixed(2)]+")"}return"#"+(16777216|c|b<<8|a<<16).toString(16).slice(1)});var $=function(a){var b=z.doc.getElementsByTagName("head")[0]||z.doc.getElementsByTagName("svg")[0],c="rgb(255, 0, 0)";return($=j(function(a){if("red"==a.toLowerCase())return c;b.style.color=c,b.style.color=a;var d=z.doc.defaultView.getComputedStyle(b,J).getPropertyValue("color");return d==c?null:d}))(a)},_=function(){return"hsb("+[this.h,this.s,this.b]+")"},ab=function(){return"hsl("+[this.h,this.s,this.l]+")"},bb=function(){return 1==this.opacity||null==this.opacity?this.hex:"rgba("+[this.r,this.g,this.b,this.opacity]+")"},cb=function(a,b,c){if(null==b&&f(a,"object")&&"r"in a&&"g"in a&&"b"in a&&(c=a.b,b=a.g,a=a.r),null==b&&f(a,string)){var e=d.getRGB(a);a=e.r,b=e.g,c=e.b}return(a>1||b>1||c>1)&&(a/=255,b/=255,c/=255),[a,b,c]},db=function(a,b,c,e){a=E.round(255*a),b=E.round(255*b),c=E.round(255*c);var g={r:a,g:b,b:c,opacity:f(e,"finite")?e:1,hex:d.rgb(a,b,c),toString:bb};return f(e,"finite")&&(g.opacity=e),g};d.color=function(a){var b;return f(a,"object")&&"h"in a&&"s"in a&&"b"in a?(b=d.hsb2rgb(a),a.r=b.r,a.g=b.g,a.b=b.b,a.opacity=1,a.hex=b.hex):f(a,"object")&&"h"in a&&"s"in a&&"l"in a?(b=d.hsl2rgb(a),a.r=b.r,a.g=b.g,a.b=b.b,a.opacity=1,a.hex=b.hex):(f(a,"string")&&(a=d.getRGB(a)),f(a,"object")&&"r"in a&&"g"in a&&"b"in a&&!("error"in a)?(b=d.rgb2hsl(a),a.h=b.h,a.s=b.s,a.l=b.l,b=d.rgb2hsb(a),a.v=b.b):(a={hex:"none"},a.r=a.g=a.b=a.h=a.s=a.v=a.l=-1,a.error=1)),a.toString=bb,a},d.hsb2rgb=function(a,b,c,d){f(a,"object")&&"h"in a&&"s"in a&&"b"in a&&(c=a.b,b=a.s,a=a.h,d=a.o),a*=360;var e,g,h,i,j;return a=a%360/60,j=c*b,i=j*(1-H(a%2-1)),e=g=h=c-j,a=~~a,e+=[j,i,0,0,i,j][a],g+=[i,j,j,i,0,0][a],h+=[0,0,i,j,j,i][a],db(e,g,h,d)},d.hsl2rgb=function(a,b,c,d){f(a,"object")&&"h"in a&&"s"in a&&"l"in a&&(c=a.l,b=a.s,a=a.h),(a>1||b>1||c>1)&&(a/=360,b/=100,c/=100),a*=360;var e,g,h,i,j;return a=a%360/60,j=2*b*(.5>c?c:1-c),i=j*(1-H(a%2-1)),e=g=h=c-j/2,a=~~a,e+=[j,i,0,0,i,j][a],g+=[i,j,j,i,0,0][a],h+=[0,0,i,j,j,i][a],db(e,g,h,d)},d.rgb2hsb=function(a,b,c){c=cb(a,b,c),a=c[0],b=c[1],c=c[2];var d,e,f,g;return f=F(a,b,c),g=f-G(a,b,c),d=0==g?null:f==a?(b-c)/g:f==b?(c-a)/g+2:(a-b)/g+4,d=(d+360)%6*60/360,e=0==g?0:g/f,{h:d,s:e,b:f,toString:_}},d.rgb2hsl=function(a,b,c){c=cb(a,b,c),a=c[0],b=c[1],c=c[2];var d,e,f,g,h,i;return g=F(a,b,c),h=G(a,b,c),i=g-h,d=0==i?null:g==a?(b-c)/i:g==b?(c-a)/i+2:(a-b)/i+4,d=(d+360)%6*60/360,f=(g+h)/2,e=0==i?0:.5>f?i/(2*f):i/(2-2*f),{h:d,s:e,l:f,toString:ab}},d.parsePathString=function(a){if(!a)return null;var b=d.path(a);if(b.arr)return d.path.clone(b.arr);var c={a:7,c:6,o:2,h:1,l:2,m:2,r:4,q:4,s:4,t:2,v:1,u:3,z:0},e=[];return f(a,"array")&&f(a[0],"array")&&(e=d.path.clone(a)),e.length||B(a).replace(Q,function(a,b,d){var f=[],g=b.toLowerCase();if(d.replace(S,function(a,b){b&&f.push(+b)}),"m"==g&&f.length>2&&(e.push([b].concat(f.splice(0,2))),g="l",b="m"==b?"l":"L"),"o"==g&&1==f.length&&e.push([b,f[0]]),"r"==g)e.push([b].concat(f));else for(;f.length>=c[g]&&(e.push([b].concat(f.splice(0,c[g]))),c[g]););}),e.toString=d.path.toString,b.arr=d.path.clone(e),e};var eb=d.parseTransformString=function(a){if(!a)return null;var b=[];return f(a,"array")&&f(a[0],"array")&&(b=d.path.clone(a)),b.length||B(a).replace(R,function(a,c,d){{var e=[];c.toLowerCase()}d.replace(S,function(a,b){b&&e.push(+b)}),b.push([c].concat(e))}),b.toString=d.path.toString,b};d._.svgTransform2string=n,d._.rgTransform=new RegExp("^[a-z]["+N+"]*-?\\.?\\d","i"),d._.transform2matrix=o,d._unit2px=s;z.doc.contains||z.doc.compareDocumentPosition?function(a,b){var c=9==a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a==d||!(!d||1!=d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)for(;b;)if(b=b.parentNode,b==a)return!0;return!1};d._.getSomeDefs=q,d._.getSomeSVG=r,d.select=function(a){return y(z.doc.querySelector(a))},d.selectAll=function(a){for(var b=z.doc.querySelectorAll(a),c=(d.set||Array)(),e=0;e<b.length;e++)c.push(y(b[e]));return c},setInterval(function(){for(var a in Y)if(Y[A](a)){var b=Y[a],c=b.node;("svg"!=b.type&&!c.ownerSVGElement||"svg"==b.type&&(!c.parentNode||"ownerSVGElement"in c.parentNode&&!c.ownerSVGElement))&&delete Y[a]}},1e4),function(a){function g(a){function b(a,b){var c=e(a.node,b);c=c&&c.match(g),c=c&&c[2],c&&"#"==c.charAt()&&(c=c.substring(1),c&&(i[c]=(i[c]||[]).concat(function(c){var d={};d[b]=Z(c),e(a.node,d)})))}function c(a){var b=e(a.node,"xlink:href");b&&"#"==b.charAt()&&(b=b.substring(1),b&&(i[b]=(i[b]||[]).concat(function(b){a.attr("xlink:href","#"+b)})))}for(var d,f=a.selectAll("*"),g=/^\s*url\(("|'|)(.*)\1\)\s*$/,h=[],i={},j=0,k=f.length;k>j;j++){d=f[j],b(d,"fill"),b(d,"stroke"),b(d,"filter"),b(d,"mask"),b(d,"clip-path"),c(d);var l=e(d.node,"id");l&&(e(d.node,{id:d.id}),h.push({old:l,id:d.id}))}for(j=0,k=h.length;k>j;j++){var m=i[h[j].old];if(m)for(var n=0,o=m.length;o>n;n++)m[n](h[j].id)}}function h(a,b,c){return function(d){var e=d.slice(a,b);return 1==e.length&&(e=e[0]),c?c(e):e}}function i(a){return function(){var b=a?"<"+this.type:"",c=this.node.attributes,d=this.node.childNodes;if(a)for(var e=0,f=c.length;f>e;e++)b+=" "+c[e].name+'="'+c[e].value.replace(/"/g,'\\"')+'"';if(d.length){for(a&&(b+=">"),e=0,f=d.length;f>e;e++)3==d[e].nodeType?b+=d[e].nodeValue:1==d[e].nodeType&&(b+=y(d[e]).toString());a&&(b+="</"+this.type+">")}else a&&(b+="/>");return b}}a.attr=function(a,c){{var d=this;d.node}if(!a)return d;if(f(a,"string")){if(!(arguments.length>1))return b("snap.util.getattr."+a,d).firstDefined();var e={};e[a]=c,a=e}for(var g in a)a[A](g)&&b("snap.util.attr."+g,d,a[g]);return d},a.getBBox=function(a){if(!d.Matrix||!d.path)return this.node.getBBox();var b=this,c=new d.Matrix;if(b.removed)return d._.box();for(;"use"==b.type;)if(a||(c=c.add(b.transform().localMatrix.translate(b.attr("x")||0,b.attr("y")||0))),b.original)b=b.original;else{var e=b.attr("xlink:href");b=b.original=b.node.ownerDocument.getElementById(e.substring(e.indexOf("#")+1))}var f=b._,g=d.path.get[b.type]||d.path.get.deflt;try{return a?(f.bboxwt=g?d.path.getBBox(b.realPath=g(b)):d._.box(b.node.getBBox()),d._.box(f.bboxwt)):(b.realPath=g(b),b.matrix=b.transform().localMatrix,f.bbox=d.path.getBBox(d.path.map(b.realPath,c.add(b.matrix))),d._.box(f.bbox))}catch(h){return d._.box()}};var j=function(){return this.string};a.transform=function(a){var b=this._;if(null==a){for(var c,f=this,g=new d.Matrix(this.node.getCTM()),h=p(this),i=[h],k=new d.Matrix,l=h.toTransformString(),m=B(h)==B(this.matrix)?B(b.transform):l;"svg"!=f.type&&(f=f.parent());)i.push(p(f));for(c=i.length;c--;)k.add(i[c]);return{string:m,globalMatrix:g,totalMatrix:k,localMatrix:h,diffMatrix:g.clone().add(h.invert()),global:g.toTransformString(),total:k.toTransformString(),local:l,toString:j}}return a instanceof d.Matrix?this.matrix=a:p(this,a),this.node&&("linearGradient"==this.type||"radialGradient"==this.type?e(this.node,{gradientTransform:this.matrix}):"pattern"==this.type?e(this.node,{patternTransform:this.matrix}):e(this.node,{transform:this.matrix})),this},a.parent=function(){return y(this.node.parentNode)},a.append=a.add=function(a){if(a){if("set"==a.type){var b=this;return a.forEach(function(a){b.add(a)}),this}a=y(a),this.node.appendChild(a.node),a.paper=this.paper}return this},a.appendTo=function(a){return a&&(a=y(a),a.append(this)),this},a.prepend=function(a){if(a){if("set"==a.type){var b,c=this;return a.forEach(function(a){b?b.after(a):c.prepend(a),b=a}),this}a=y(a);var d=a.parent();this.node.insertBefore(a.node,this.node.firstChild),this.add&&this.add(),a.paper=this.paper,this.parent()&&this.parent().add(),d&&d.add()}return this},a.prependTo=function(a){return a=y(a),a.prepend(this),this},a.before=function(a){if("set"==a.type){var b=this;return a.forEach(function(a){var c=a.parent();b.node.parentNode.insertBefore(a.node,b.node),c&&c.add()}),this.parent().add(),this}a=y(a);var c=a.parent();return this.node.parentNode.insertBefore(a.node,this.node),this.parent()&&this.parent().add(),c&&c.add(),a.paper=this.paper,this},a.after=function(a){a=y(a);var b=a.parent();return this.node.nextSibling?this.node.parentNode.insertBefore(a.node,this.node.nextSibling):this.node.parentNode.appendChild(a.node),this.parent()&&this.parent().add(),b&&b.add(),a.paper=this.paper,this},a.insertBefore=function(a){a=y(a);var b=this.parent();return a.node.parentNode.insertBefore(this.node,a.node),this.paper=a.paper,b&&b.add(),a.parent()&&a.parent().add(),this},a.insertAfter=function(a){a=y(a);var b=this.parent();return a.node.parentNode.insertBefore(this.node,a.node.nextSibling),this.paper=a.paper,b&&b.add(),a.parent()&&a.parent().add(),this},a.remove=function(){var a=this.parent();return this.node.parentNode&&this.node.parentNode.removeChild(this.node),delete this.paper,this.removed=!0,a&&a.add(),this},a.select=function(a){return y(this.node.querySelector(a))},a.selectAll=function(a){for(var b=this.node.querySelectorAll(a),c=(d.set||Array)(),e=0;e<b.length;e++)c.push(y(b[e]));return c},a.asPX=function(a,b){return null==b&&(b=this.attr(a)),+s(this,a,b)},a.use=function(){var a,b=this.node.id;return b||(b=this.id,e(this.node,{id:b})),a="linearGradient"==this.type||"radialGradient"==this.type||"pattern"==this.type?w(this.type,this.node.parentNode):w("use",this.node.parentNode),e(a.node,{"xlink:href":"#"+b}),a.original=this,a};var k=/\S+/g;a.addClass=function(a){var b,c,d,e,f=(a||"").match(k)||[],g=this.node,h=g.className.baseVal,i=h.match(k)||[];if(f.length){for(b=0;d=f[b++];)c=i.indexOf(d),~c||i.push(d);e=i.join(" "),h!=e&&(g.className.baseVal=e)}return this},a.removeClass=function(a){var b,c,d,e,f=(a||"").match(k)||[],g=this.node,h=g.className.baseVal,i=h.match(k)||[];if(i.length){for(b=0;d=f[b++];)c=i.indexOf(d),~c&&i.splice(c,1);e=i.join(" "),h!=e&&(g.className.baseVal=e)}return this},a.hasClass=function(a){var b=this.node,c=b.className.baseVal,d=c.match(k)||[];return!!~d.indexOf(a)},a.toggleClass=function(a,b){if(null!=b)return b?this.addClass(a):this.removeClass(a);var c,d,e,f,g=(a||"").match(k)||[],h=this.node,i=h.className.baseVal,j=i.match(k)||[];for(c=0;e=g[c++];)d=j.indexOf(e),~d?j.splice(d,1):j.push(e);return f=j.join(" "),i!=f&&(h.className.baseVal=f),this},a.clone=function(){var a=y(this.node.cloneNode(!0));return e(a.node,"id")&&e(a.node,{id:a.id}),g(a),a.insertAfter(this),a},a.toDefs=function(){var a=q(this);return a.appendChild(this.node),this},a.pattern=a.toPattern=function(a,b,c,d){var g=w("pattern",q(this));return null==a&&(a=this.getBBox()),f(a,"object")&&"x"in a&&(b=a.y,c=a.width,d=a.height,a=a.x),e(g.node,{x:a,y:b,width:c,height:d,patternUnits:"userSpaceOnUse",id:g.id,viewBox:[a,b,c,d].join(" ")}),g.node.appendChild(this.node),g},a.marker=function(a,b,c,d,g,h){var i=w("marker",q(this));return null==a&&(a=this.getBBox()),f(a,"object")&&"x"in a&&(b=a.y,c=a.width,d=a.height,g=a.refX||a.cx,h=a.refY||a.cy,a=a.x),e(i.node,{viewBox:[a,b,c,d].join(K),markerWidth:c,markerHeight:d,orient:"auto",refX:g||0,refY:h||0,id:i.id}),i.node.appendChild(this.node),i};var l=function(a,b,d,e){"function"!=typeof d||d.length||(e=d,d=c.linear),this.attr=a,this.dur=b,d&&(this.easing=d),e&&(this.callback=e)};d._.Animation=l,d.animation=function(a,b,c,d){return new l(a,b,c,d)},a.inAnim=function(){var a=this,b=[];for(var c in a.anims)a.anims[A](c)&&!function(a){b.push({anim:new l(a._attrs,a.dur,a.easing,a._callback),mina:a,curStatus:a.status(),status:function(b){return a.status(b)},stop:function(){a.stop()}})}(a.anims[c]);return b},d.animate=function(a,d,e,f,g,h){"function"!=typeof g||g.length||(h=g,g=c.linear);var i=c.time(),j=c(a,d,i,i+f,c.time,e,g);return h&&b.once("mina.finish."+j.id,h),j},a.stop=function(){for(var a=this.inAnim(),b=0,c=a.length;c>b;b++)a[b].stop();return this},a.animate=function(a,d,e,g){"function"!=typeof e||e.length||(g=e,e=c.linear),a instanceof l&&(g=a.callback,e=a.easing,d=e.dur,a=a.attr);var i,j,k,m,n=[],o=[],p={},q=this;for(var r in a)if(a[A](r)){q.equal?(m=q.equal(r,B(a[r])),i=m.from,j=m.to,k=m.f):(i=+q.attr(r),j=+a[r]);var s=f(i,"array")?i.length:1;p[r]=h(n.length,n.length+s,k),n=n.concat(i),o=o.concat(j)}var t=c.time(),u=c(n,o,t,t+d,c.time,function(a){var b={};for(var c in p)p[A](c)&&(b[c]=p[c](a));q.attr(b)},e);return q.anims[u.id]=u,u._attrs=a,u._callback=g,b("snap.animcreated."+q.id,u),b.once("mina.finish."+u.id,function(){delete q.anims[u.id],g&&g.call(q)}),b.once("mina.stop."+u.id,function(){delete q.anims[u.id]}),q};var m={};a.data=function(a,c){var e=m[this.id]=m[this.id]||{};if(0==arguments.length)return b("snap.data.get."+this.id,this,e,null),e;if(1==arguments.length){if(d.is(a,"object")){for(var f in a)a[A](f)&&this.data(f,a[f]);return this}return b("snap.data.get."+this.id,this,e[a],a),e[a]}return e[a]=c,b("snap.data.set."+this.id,this,c,a),this},a.removeData=function(a){return null==a?m[this.id]={}:m[this.id]&&delete m[this.id][a],this},a.outerSVG=a.toString=i(1),a.innerSVG=i()}(u.prototype),d.parse=function(a){var b=z.doc.createDocumentFragment(),c=!0,d=z.doc.createElement("div");if(a=B(a),a.match(/^\s*<\s*svg(?:\s|>)/)||(a="<svg>"+a+"</svg>",c=!1),d.innerHTML=a,a=d.getElementsByTagName("svg")[0])if(c)b=a;else for(;a.firstChild;)b.appendChild(a.firstChild);return d.innerHTML=J,new v(b)},v.prototype.select=u.prototype.select,v.prototype.selectAll=u.prototype.selectAll,d.fragment=function(){for(var a=Array.prototype.slice.call(arguments,0),b=z.doc.createDocumentFragment(),c=0,e=a.length;e>c;c++){var f=a[c];f.node&&f.node.nodeType&&b.appendChild(f.node),f.nodeType&&b.appendChild(f),"string"==typeof f&&b.appendChild(d.parse(f).node)}return new v(b)},d._.make=w,d._.wrap=y,x.prototype.el=function(a,b){var c=w(a,this.node);return b&&c.attr(b),c},b.on("snap.util.getattr",function(){var a=b.nt();a=a.substring(a.lastIndexOf(".")+1);var c=a.replace(/[A-Z]/g,function(a){return"-"+a.toLowerCase()});return fb[A](c)?this.node.ownerDocument.defaultView.getComputedStyle(this.node,null).getPropertyValue(c):e(this.node,a)});var fb={"alignment-baseline":0,"baseline-shift":0,clip:0,"clip-path":0,"clip-rule":0,color:0,"color-interpolation":0,"color-interpolation-filters":0,"color-profile":0,"color-rendering":0,cursor:0,direction:0,display:0,"dominant-baseline":0,"enable-background":0,fill:0,"fill-opacity":0,"fill-rule":0,filter:0,"flood-color":0,"flood-opacity":0,font:0,"font-family":0,"font-size":0,"font-size-adjust":0,"font-stretch":0,"font-style":0,"font-variant":0,"font-weight":0,"glyph-orientation-horizontal":0,"glyph-orientation-vertical":0,"image-rendering":0,kerning:0,"letter-spacing":0,"lighting-color":0,marker:0,"marker-end":0,"marker-mid":0,"marker-start":0,mask:0,opacity:0,overflow:0,"pointer-events":0,"shape-rendering":0,"stop-color":0,"stop-opacity":0,stroke:0,"stroke-dasharray":0,"stroke-dashoffset":0,"stroke-linecap":0,"stroke-linejoin":0,"stroke-miterlimit":0,"stroke-opacity":0,"stroke-width":0,"text-anchor":0,"text-decoration":0,"text-rendering":0,"unicode-bidi":0,visibility:0,"word-spacing":0,"writing-mode":0};b.on("snap.util.attr",function(a){var c=b.nt(),d={};c=c.substring(c.lastIndexOf(".")+1),d[c]=a;var f=c.replace(/-(\w)/gi,function(a,b){return b.toUpperCase()}),g=c.replace(/[A-Z]/g,function(a){return"-"+a.toLowerCase()});fb[A](g)?this.node.style[f]=null==a?J:a:e(this.node,d)}),function(){}(x.prototype),d.ajax=function(a,c,d,e){var g=new XMLHttpRequest,h=V();if(g){if(f(c,"function"))e=d,d=c,c=null;else if(f(c,"object")){var i=[];for(var j in c)c.hasOwnProperty(j)&&i.push(encodeURIComponent(j)+"="+encodeURIComponent(c[j]));c=i.join("&")}return g.open(c?"POST":"GET",a,!0),c&&(g.setRequestHeader("X-Requested-With","XMLHttpRequest"),g.setRequestHeader("Content-type","application/x-www-form-urlencoded")),d&&(b.once("snap.ajax."+h+".0",d),b.once("snap.ajax."+h+".200",d),b.once("snap.ajax."+h+".304",d)),g.onreadystatechange=function(){4==g.readyState&&b("snap.ajax."+h+"."+g.status,e,g)},4==g.readyState?g:(g.send(c),g)}},d.load=function(a,b,c){d.ajax(a,function(a){var e=d.parse(a.responseText);c?b.call(c,e):b(e)})};var gb=function(a){var b=a.getBoundingClientRect(),c=a.ownerDocument,d=c.body,e=c.documentElement,f=e.clientTop||d.clientTop||0,h=e.clientLeft||d.clientLeft||0,i=b.top+(g.win.pageYOffset||e.scrollTop||d.scrollTop)-f,j=b.left+(g.win.pageXOffset||e.scrollLeft||d.scrollLeft)-h;return{y:i,x:j}};return d.getElementByPoint=function(a,b){var c=this,d=(c.canvas,z.doc.elementFromPoint(a,b));if(z.win.opera&&"svg"==d.tagName){var e=gb(d),f=d.createSVGRect();f.x=a-e.x,f.y=b-e.y,f.width=f.height=1;var g=d.getIntersectionList(f,null);g.length&&(d=g[g.length-1])}return d?y(d):null},d.plugin=function(a){a(d,u,x,z,v)},z.win.Snap=d,d}();return d.plugin(function(a){function b(a,b,d,e,f,g){return null==b&&"[object SVGMatrix]"==c.call(a)?(this.a=a.a,this.b=a.b,this.c=a.c,this.d=a.d,this.e=a.e,void(this.f=a.f)):void(null!=a?(this.a=+a,this.b=+b,this.c=+d,this.d=+e,this.e=+f,this.f=+g):(this.a=1,this.b=0,this.c=0,this.d=1,this.e=0,this.f=0))}var c=Object.prototype.toString,d=String,e=Math,f="";!function(c){function g(a){return a[0]*a[0]+a[1]*a[1]
+}function h(a){var b=e.sqrt(g(a));a[0]&&(a[0]/=b),a[1]&&(a[1]/=b)}c.add=function(a,c,d,e,f,g){var h,i,j,k,l=[[],[],[]],m=[[this.a,this.c,this.e],[this.b,this.d,this.f],[0,0,1]],n=[[a,d,f],[c,e,g],[0,0,1]];for(a&&a instanceof b&&(n=[[a.a,a.c,a.e],[a.b,a.d,a.f],[0,0,1]]),h=0;3>h;h++)for(i=0;3>i;i++){for(k=0,j=0;3>j;j++)k+=m[h][j]*n[j][i];l[h][i]=k}return this.a=l[0][0],this.b=l[1][0],this.c=l[0][1],this.d=l[1][1],this.e=l[0][2],this.f=l[1][2],this},c.invert=function(){var a=this,c=a.a*a.d-a.b*a.c;return new b(a.d/c,-a.b/c,-a.c/c,a.a/c,(a.c*a.f-a.d*a.e)/c,(a.b*a.e-a.a*a.f)/c)},c.clone=function(){return new b(this.a,this.b,this.c,this.d,this.e,this.f)},c.translate=function(a,b){return this.add(1,0,0,1,a,b)},c.scale=function(a,b,c,d){return null==b&&(b=a),(c||d)&&this.add(1,0,0,1,c,d),this.add(a,0,0,b,0,0),(c||d)&&this.add(1,0,0,1,-c,-d),this},c.rotate=function(b,c,d){b=a.rad(b),c=c||0,d=d||0;var f=+e.cos(b).toFixed(9),g=+e.sin(b).toFixed(9);return this.add(f,g,-g,f,c,d),this.add(1,0,0,1,-c,-d)},c.x=function(a,b){return a*this.a+b*this.c+this.e},c.y=function(a,b){return a*this.b+b*this.d+this.f},c.get=function(a){return+this[d.fromCharCode(97+a)].toFixed(4)},c.toString=function(){return"matrix("+[this.get(0),this.get(1),this.get(2),this.get(3),this.get(4),this.get(5)].join()+")"},c.offset=function(){return[this.e.toFixed(4),this.f.toFixed(4)]},c.determinant=function(){return this.a*this.d-this.b*this.c},c.split=function(){var b={};b.dx=this.e,b.dy=this.f;var c=[[this.a,this.c],[this.b,this.d]];b.scalex=e.sqrt(g(c[0])),h(c[0]),b.shear=c[0][0]*c[1][0]+c[0][1]*c[1][1],c[1]=[c[1][0]-c[0][0]*b.shear,c[1][1]-c[0][1]*b.shear],b.scaley=e.sqrt(g(c[1])),h(c[1]),b.shear/=b.scaley,this.determinant()<0&&(b.scalex=-b.scalex);var d=-c[0][1],f=c[1][1];return 0>f?(b.rotate=a.deg(e.acos(f)),0>d&&(b.rotate=360-b.rotate)):b.rotate=a.deg(e.asin(d)),b.isSimple=!(+b.shear.toFixed(9)||b.scalex.toFixed(9)!=b.scaley.toFixed(9)&&b.rotate),b.isSuperSimple=!+b.shear.toFixed(9)&&b.scalex.toFixed(9)==b.scaley.toFixed(9)&&!b.rotate,b.noRotation=!+b.shear.toFixed(9)&&!b.rotate,b},c.toTransformString=function(a){var b=a||this.split();return+b.shear.toFixed(9)?"m"+[this.get(0),this.get(1),this.get(2),this.get(3),this.get(4),this.get(5)]:(b.scalex=+b.scalex.toFixed(4),b.scaley=+b.scaley.toFixed(4),b.rotate=+b.rotate.toFixed(4),(b.dx||b.dy?"t"+[+b.dx.toFixed(4),+b.dy.toFixed(4)]:f)+(1!=b.scalex||1!=b.scaley?"s"+[b.scalex,b.scaley,0,0]:f)+(b.rotate?"r"+[+b.rotate.toFixed(4),0,0]:f))}}(b.prototype),a.Matrix=b,a.matrix=function(a,c,d,e,f,g){return new b(a,c,d,e,f,g)}}),d.plugin(function(a,c,d,e,f){function g(d){return function(e){if(b.stop(),e instanceof f&&1==e.node.childNodes.length&&("radialGradient"==e.node.firstChild.tagName||"linearGradient"==e.node.firstChild.tagName||"pattern"==e.node.firstChild.tagName)&&(e=e.node.firstChild,n(this).appendChild(e),e=l(e)),e instanceof c)if("radialGradient"==e.type||"linearGradient"==e.type||"pattern"==e.type){e.node.id||p(e.node,{id:e.id});var g=q(e.node.id)}else g=e.attr(d);else if(g=a.color(e),g.error){var h=a(n(this).ownerSVGElement).gradient(e);h?(h.node.id||p(h.node,{id:h.id}),g=q(h.node.id)):g=e}else g=r(g);var i={};i[d]=g,p(this.node,i),this.node.style[d]=t}}function h(a){b.stop(),a==+a&&(a+="px"),this.node.style.fontSize=a}function i(a){for(var b=[],c=a.childNodes,d=0,e=c.length;e>d;d++){var f=c[d];3==f.nodeType&&b.push(f.nodeValue),"tspan"==f.tagName&&b.push(1==f.childNodes.length&&3==f.firstChild.nodeType?f.firstChild.nodeValue:i(f))}return b}function j(){return b.stop(),this.node.style.fontSize}var k=a._.make,l=a._.wrap,m=a.is,n=a._.getSomeDefs,o=/^url\(#?([^)]+)\)$/,p=a._.$,q=a.url,r=String,s=a._.separator,t="";b.on("snap.util.attr.mask",function(a){if(a instanceof c||a instanceof f){if(b.stop(),a instanceof f&&1==a.node.childNodes.length&&(a=a.node.firstChild,n(this).appendChild(a),a=l(a)),"mask"==a.type)var d=a;else d=k("mask",n(this)),d.node.appendChild(a.node);!d.node.id&&p(d.node,{id:d.id}),p(this.node,{mask:q(d.id)})}}),function(a){b.on("snap.util.attr.clip",a),b.on("snap.util.attr.clip-path",a),b.on("snap.util.attr.clipPath",a)}(function(a){if(a instanceof c||a instanceof f){if(b.stop(),"clipPath"==a.type)var d=a;else d=k("clipPath",n(this)),d.node.appendChild(a.node),!d.node.id&&p(d.node,{id:d.id});p(this.node,{"clip-path":q(d.id)})}}),b.on("snap.util.attr.fill",g("fill")),b.on("snap.util.attr.stroke",g("stroke"));var u=/^([lr])(?:\(([^)]*)\))?(.*)$/i;b.on("snap.util.grad.parse",function(a){a=r(a);var b=a.match(u);if(!b)return null;var c=b[1],d=b[2],e=b[3];return d=d.split(/\s*,\s*/).map(function(a){return+a==a?+a:a}),1==d.length&&0==d[0]&&(d=[]),e=e.split("-"),e=e.map(function(a){a=a.split(":");var b={color:a[0]};return a[1]&&(b.offset=parseFloat(a[1])),b}),{type:c,params:d,stops:e}}),b.on("snap.util.attr.d",function(c){b.stop(),m(c,"array")&&m(c[0],"array")&&(c=a.path.toString.call(c)),c=r(c),c.match(/[ruo]/i)&&(c=a.path.toAbsolute(c)),p(this.node,{d:c})})(-1),b.on("snap.util.attr.#text",function(a){b.stop(),a=r(a);for(var c=e.doc.createTextNode(a);this.node.firstChild;)this.node.removeChild(this.node.firstChild);this.node.appendChild(c)})(-1),b.on("snap.util.attr.path",function(a){b.stop(),this.attr({d:a})})(-1),b.on("snap.util.attr.class",function(a){b.stop(),this.node.className.baseVal=a})(-1),b.on("snap.util.attr.viewBox",function(a){var c;c=m(a,"object")&&"x"in a?[a.x,a.y,a.width,a.height].join(" "):m(a,"array")?a.join(" "):a,p(this.node,{viewBox:c}),b.stop()})(-1),b.on("snap.util.attr.transform",function(a){this.transform(a),b.stop()})(-1),b.on("snap.util.attr.r",function(a){"rect"==this.type&&(b.stop(),p(this.node,{rx:a,ry:a}))})(-1),b.on("snap.util.attr.textpath",function(a){if(b.stop(),"text"==this.type){var d,e,f;if(!a&&this.textPath){for(e=this.textPath;e.node.firstChild;)this.node.appendChild(e.node.firstChild);return e.remove(),void delete this.textPath}if(m(a,"string")){var g=n(this),h=l(g.parentNode).path(a);g.appendChild(h.node),d=h.id,h.attr({id:d})}else a=l(a),a instanceof c&&(d=a.attr("id"),d||(d=a.id,a.attr({id:d})));if(d)if(e=this.textPath,f=this.node,e)e.attr({"xlink:href":"#"+d});else{for(e=p("textPath",{"xlink:href":"#"+d});f.firstChild;)e.appendChild(f.firstChild);f.appendChild(e),this.textPath=l(e)}}})(-1),b.on("snap.util.attr.text",function(a){if("text"==this.type){for(var c=this.node,d=function(a){var b=p("tspan");if(m(a,"array"))for(var c=0;c<a.length;c++)b.appendChild(d(a[c]));else b.appendChild(e.doc.createTextNode(a));return b.normalize&&b.normalize(),b};c.firstChild;)c.removeChild(c.firstChild);for(var f=d(a);f.firstChild;)c.appendChild(f.firstChild)}b.stop()})(-1),b.on("snap.util.attr.fontSize",h)(-1),b.on("snap.util.attr.font-size",h)(-1),b.on("snap.util.getattr.transform",function(){return b.stop(),this.transform()})(-1),b.on("snap.util.getattr.textpath",function(){return b.stop(),this.textPath})(-1),function(){function c(c){return function(){b.stop();var d=e.doc.defaultView.getComputedStyle(this.node,null).getPropertyValue("marker-"+c);return"none"==d?d:a(e.doc.getElementById(d.match(o)[1]))}}function d(a){return function(c){b.stop();var d="marker"+a.charAt(0).toUpperCase()+a.substring(1);if(""==c||!c)return void(this.node.style[d]="none");if("marker"==c.type){var e=c.node.id;return e||p(c.node,{id:c.id}),void(this.node.style[d]=q(e))}}}b.on("snap.util.getattr.marker-end",c("end"))(-1),b.on("snap.util.getattr.markerEnd",c("end"))(-1),b.on("snap.util.getattr.marker-start",c("start"))(-1),b.on("snap.util.getattr.markerStart",c("start"))(-1),b.on("snap.util.getattr.marker-mid",c("mid"))(-1),b.on("snap.util.getattr.markerMid",c("mid"))(-1),b.on("snap.util.attr.marker-end",d("end"))(-1),b.on("snap.util.attr.markerEnd",d("end"))(-1),b.on("snap.util.attr.marker-start",d("start"))(-1),b.on("snap.util.attr.markerStart",d("start"))(-1),b.on("snap.util.attr.marker-mid",d("mid"))(-1),b.on("snap.util.attr.markerMid",d("mid"))(-1)}(),b.on("snap.util.getattr.r",function(){return"rect"==this.type&&p(this.node,"rx")==p(this.node,"ry")?(b.stop(),p(this.node,"rx")):void 0})(-1),b.on("snap.util.getattr.text",function(){if("text"==this.type||"tspan"==this.type){b.stop();var a=i(this.node);return 1==a.length?a[0]:a}})(-1),b.on("snap.util.getattr.#text",function(){return this.node.textContent})(-1),b.on("snap.util.getattr.viewBox",function(){b.stop();var c=p(this.node,"viewBox");return c?(c=c.split(s),a._.box(+c[0],+c[1],+c[2],+c[3])):void 0})(-1),b.on("snap.util.getattr.points",function(){var a=p(this.node,"points");return b.stop(),a?a.split(s):void 0})(-1),b.on("snap.util.getattr.path",function(){var a=p(this.node,"d");return b.stop(),a})(-1),b.on("snap.util.getattr.class",function(){return this.node.className.baseVal})(-1),b.on("snap.util.getattr.fontSize",j)(-1),b.on("snap.util.getattr.font-size",j)(-1)}),d.plugin(function(){function a(a){return a}function c(a){return function(b){return+b.toFixed(3)+a}}var d={"+":function(a,b){return a+b},"-":function(a,b){return a-b},"/":function(a,b){return a/b},"*":function(a,b){return a*b}},e=String,f=/[a-z]+$/i,g=/^\s*([+\-\/*])\s*=\s*([\d.eE+\-]+)\s*([^\d\s]+)?\s*$/;b.on("snap.util.attr",function(a){var c=e(a).match(g);if(c){var h=b.nt(),i=h.substring(h.lastIndexOf(".")+1),j=this.attr(i),k={};b.stop();var l=c[3]||"",m=j.match(f),n=d[c[1]];if(m&&m==l?a=n(parseFloat(j),+c[2]):(j=this.asPX(i),a=n(this.asPX(i),this.asPX(i,c[2]+l))),isNaN(j)||isNaN(a))return;k[i]=a,this.attr(k)}})(-10),b.on("snap.util.equal",function(h,i){var j=e(this.attr(h)||""),k=e(i).match(g);if(k){b.stop();var l=k[3]||"",m=j.match(f),n=d[k[1]];return m&&m==l?{from:parseFloat(j),to:n(parseFloat(j),+k[2]),f:c(m)}:(j=this.asPX(h),{from:j,to:n(j,this.asPX(h,k[2]+l)),f:a})}})(-10)}),d.plugin(function(a,c,d,e){var f=d.prototype,g=a.is;f.rect=function(a,b,c,d,e,f){var h;return null==f&&(f=e),g(a,"object")&&"[object Object]"==a?h=a:null!=a&&(h={x:a,y:b,width:c,height:d},null!=e&&(h.rx=e,h.ry=f)),this.el("rect",h)},f.circle=function(a,b,c){var d;return g(a,"object")&&"[object Object]"==a?d=a:null!=a&&(d={cx:a,cy:b,r:c}),this.el("circle",d)};var h=function(){function a(){this.parentNode.removeChild(this)}return function(b,c){var d=e.doc.createElement("img"),f=e.doc.body;d.style.cssText="position:absolute;left:-9999em;top:-9999em",d.onload=function(){c.call(d),d.onload=d.onerror=null,f.removeChild(d)},d.onerror=a,f.appendChild(d),d.src=b}}();f.image=function(b,c,d,e,f){var i=this.el("image");if(g(b,"object")&&"src"in b)i.attr(b);else if(null!=b){var j={"xlink:href":b,preserveAspectRatio:"none"};null!=c&&null!=d&&(j.x=c,j.y=d),null!=e&&null!=f?(j.width=e,j.height=f):h(b,function(){a._.$(i.node,{width:this.offsetWidth,height:this.offsetHeight})}),a._.$(i.node,j)}return i},f.ellipse=function(a,b,c,d){var e;return g(a,"object")&&"[object Object]"==a?e=a:null!=a&&(e={cx:a,cy:b,rx:c,ry:d}),this.el("ellipse",e)},f.path=function(a){var b;return g(a,"object")&&!g(a,"array")?b=a:a&&(b={d:a}),this.el("path",b)},f.group=f.g=function(a){var b=this.el("g");return 1==arguments.length&&a&&!a.type?b.attr(a):arguments.length&&b.add(Array.prototype.slice.call(arguments,0)),b},f.svg=function(a,b,c,d,e,f,h,i){var j={};return g(a,"object")&&null==b?j=a:(null!=a&&(j.x=a),null!=b&&(j.y=b),null!=c&&(j.width=c),null!=d&&(j.height=d),null!=e&&null!=f&&null!=h&&null!=i&&(j.viewBox=[e,f,h,i])),this.el("svg",j)},f.mask=function(a){var b=this.el("mask");return 1==arguments.length&&a&&!a.type?b.attr(a):arguments.length&&b.add(Array.prototype.slice.call(arguments,0)),b},f.ptrn=function(a,b,c,d,e,f,h,i){if(g(a,"object"))var j=a;else arguments.length?(j={},null!=a&&(j.x=a),null!=b&&(j.y=b),null!=c&&(j.width=c),null!=d&&(j.height=d),null!=e&&null!=f&&null!=h&&null!=i&&(j.viewBox=[e,f,h,i])):j={patternUnits:"userSpaceOnUse"};return this.el("pattern",j)},f.use=function(a){if(null!=a){{make("use",this.node)}return a instanceof c&&(a.attr("id")||a.attr({id:ID()}),a=a.attr("id")),this.el("use",{"xlink:href":a})}return c.prototype.use.call(this)},f.text=function(a,b,c){var d={};return g(a,"object")?d=a:null!=a&&(d={x:a,y:b,text:c||""}),this.el("text",d)},f.line=function(a,b,c,d){var e={};return g(a,"object")?e=a:null!=a&&(e={x1:a,x2:c,y1:b,y2:d}),this.el("line",e)},f.polyline=function(a){arguments.length>1&&(a=Array.prototype.slice.call(arguments,0));var b={};return g(a,"object")&&!g(a,"array")?b=a:null!=a&&(b={points:a}),this.el("polyline",b)},f.polygon=function(a){arguments.length>1&&(a=Array.prototype.slice.call(arguments,0));var b={};return g(a,"object")&&!g(a,"array")?b=a:null!=a&&(b={points:a}),this.el("polygon",b)},function(){function c(){return this.selectAll("stop")}function d(b,c){var d=j("stop"),e={offset:+c+"%"};return b=a.color(b),e["stop-color"]=b.hex,b.opacity<1&&(e["stop-opacity"]=b.opacity),j(d,e),this.node.appendChild(d),this}function e(){if("linearGradient"==this.type){var b=j(this.node,"x1")||0,c=j(this.node,"x2")||1,d=j(this.node,"y1")||0,e=j(this.node,"y2")||0;return a._.box(b,d,math.abs(c-b),math.abs(e-d))}var f=this.node.cx||.5,g=this.node.cy||.5,h=this.node.r||0;return a._.box(f-h,g-h,2*h,2*h)}function g(a,c){function d(a,b){for(var c=(b-l)/(a-m),d=m;a>d;d++)g[d].offset=+(+l+c*(d-m)).toFixed(2);m=a,l=b}var e,f=b("snap.util.grad.parse",null,c).firstDefined();if(!f)return null;f.params.unshift(a),e="l"==f.type.toLowerCase()?h.apply(0,f.params):i.apply(0,f.params),f.type!=f.type.toLowerCase()&&j(e.node,{gradientUnits:"userSpaceOnUse"});var g=f.stops,k=g.length,l=0,m=0;k--;for(var n=0;k>n;n++)"offset"in g[n]&&d(n,g[n].offset);for(g[k].offset=g[k].offset||100,d(k,g[k].offset),n=0;k>=n;n++){var o=g[n];e.addStop(o.color,o.offset)}return e}function h(b,f,g,h,i){var k=a._.make("linearGradient",b);return k.stops=c,k.addStop=d,k.getBBox=e,null!=f&&j(k.node,{x1:f,y1:g,x2:h,y2:i}),k}function i(b,f,g,h,i,k){var l=a._.make("radialGradient",b);return l.stops=c,l.addStop=d,l.getBBox=e,null!=f&&j(l.node,{cx:f,cy:g,r:h}),null!=i&&null!=k&&j(l.node,{fx:i,fy:k}),l}var j=a._.$;f.gradient=function(a){return g(this.defs,a)},f.gradientLinear=function(a,b,c,d){return h(this.defs,a,b,c,d)},f.gradientRadial=function(a,b,c,d,e){return i(this.defs,a,b,c,d,e)},f.toString=function(){var b,c=this.node.ownerDocument,d=c.createDocumentFragment(),e=c.createElement("div"),f=this.node.cloneNode(!0);return d.appendChild(e),e.appendChild(f),a._.$(f,{xmlns:"http://www.w3.org/2000/svg"}),b=e.innerHTML,d.removeChild(d.firstChild),b},f.clear=function(){for(var a,b=this.node.firstChild;b;)a=b.nextSibling,"defs"!=b.tagName?b.parentNode.removeChild(b):f.clear.call({node:b}),b=a}}()}),d.plugin(function(a,b){function c(a){var b=c.ps=c.ps||{};return b[a]?b[a].sleep=100:b[a]={sleep:100},setTimeout(function(){for(var c in b)b[K](c)&&c!=a&&(b[c].sleep--,!b[c].sleep&&delete b[c])}),b[a]}function d(a,b,c,d){return null==a&&(a=b=c=d=0),null==b&&(b=a.y,c=a.width,d=a.height,a=a.x),{x:a,y:b,width:c,w:c,height:d,h:d,x2:a+c,y2:b+d,cx:a+c/2,cy:b+d/2,r1:N.min(c,d)/2,r2:N.max(c,d)/2,r0:N.sqrt(c*c+d*d)/2,path:w(a,b,c,d),vb:[a,b,c,d].join(" ")}}function e(){return this.join(",").replace(L,"$1")}function f(a){var b=J(a);return b.toString=e,b}function g(a,b,c,d,e,f,g,h,j){return null==j?n(a,b,c,d,e,f,g,h):i(a,b,c,d,e,f,g,h,o(a,b,c,d,e,f,g,h,j))}function h(c,d){function e(a){return+(+a).toFixed(3)}return a._.cacher(function(a,f,h){a instanceof b&&(a=a.attr("d")),a=E(a);for(var j,k,l,m,n,o="",p={},q=0,r=0,s=a.length;s>r;r++){if(l=a[r],"M"==l[0])j=+l[1],k=+l[2];else{if(m=g(j,k,l[1],l[2],l[3],l[4],l[5],l[6]),q+m>f){if(d&&!p.start){if(n=g(j,k,l[1],l[2],l[3],l[4],l[5],l[6],f-q),o+=["C"+e(n.start.x),e(n.start.y),e(n.m.x),e(n.m.y),e(n.x),e(n.y)],h)return o;p.start=o,o=["M"+e(n.x),e(n.y)+"C"+e(n.n.x),e(n.n.y),e(n.end.x),e(n.end.y),e(l[5]),e(l[6])].join(),q+=m,j=+l[5],k=+l[6];continue}if(!c&&!d)return n=g(j,k,l[1],l[2],l[3],l[4],l[5],l[6],f-q)}q+=m,j=+l[5],k=+l[6]}o+=l.shift()+l}return p.end=o,n=c?q:d?p:i(j,k,l[0],l[1],l[2],l[3],l[4],l[5],1)},null,a._.clone)}function i(a,b,c,d,e,f,g,h,i){var j=1-i,k=R(j,3),l=R(j,2),m=i*i,n=m*i,o=k*a+3*l*i*c+3*j*i*i*e+n*g,p=k*b+3*l*i*d+3*j*i*i*f+n*h,q=a+2*i*(c-a)+m*(e-2*c+a),r=b+2*i*(d-b)+m*(f-2*d+b),s=c+2*i*(e-c)+m*(g-2*e+c),t=d+2*i*(f-d)+m*(h-2*f+d),u=j*a+i*c,v=j*b+i*d,w=j*e+i*g,x=j*f+i*h,y=90-180*N.atan2(q-s,r-t)/O;return{x:o,y:p,m:{x:q,y:r},n:{x:s,y:t},start:{x:u,y:v},end:{x:w,y:x},alpha:y}}function j(b,c,e,f,g,h,i,j){a.is(b,"array")||(b=[b,c,e,f,g,h,i,j]);var k=D.apply(null,b);return d(k.min.x,k.min.y,k.max.x-k.min.x,k.max.y-k.min.y)}function k(a,b,c){return b>=a.x&&b<=a.x+a.width&&c>=a.y&&c<=a.y+a.height}function l(a,b){return a=d(a),b=d(b),k(b,a.x,a.y)||k(b,a.x2,a.y)||k(b,a.x,a.y2)||k(b,a.x2,a.y2)||k(a,b.x,b.y)||k(a,b.x2,b.y)||k(a,b.x,b.y2)||k(a,b.x2,b.y2)||(a.x<b.x2&&a.x>b.x||b.x<a.x2&&b.x>a.x)&&(a.y<b.y2&&a.y>b.y||b.y<a.y2&&b.y>a.y)}function m(a,b,c,d,e){var f=-3*b+9*c-9*d+3*e,g=a*f+6*b-12*c+6*d;return a*g-3*b+3*c}function n(a,b,c,d,e,f,g,h,i){null==i&&(i=1),i=i>1?1:0>i?0:i;for(var j=i/2,k=12,l=[-.1252,.1252,-.3678,.3678,-.5873,.5873,-.7699,.7699,-.9041,.9041,-.9816,.9816],n=[.2491,.2491,.2335,.2335,.2032,.2032,.1601,.1601,.1069,.1069,.0472,.0472],o=0,p=0;k>p;p++){var q=j*l[p]+j,r=m(q,a,c,e,g),s=m(q,b,d,f,h),t=r*r+s*s;o+=n[p]*N.sqrt(t)}return j*o}function o(a,b,c,d,e,f,g,h,i){if(!(0>i||n(a,b,c,d,e,f,g,h)<i)){var j,k=1,l=k/2,m=k-l,o=.01;for(j=n(a,b,c,d,e,f,g,h,m);S(j-i)>o;)l/=2,m+=(i>j?1:-1)*l,j=n(a,b,c,d,e,f,g,h,m);return m}}function p(a,b,c,d,e,f,g,h){if(!(Q(a,c)<P(e,g)||P(a,c)>Q(e,g)||Q(b,d)<P(f,h)||P(b,d)>Q(f,h))){var i=(a*d-b*c)*(e-g)-(a-c)*(e*h-f*g),j=(a*d-b*c)*(f-h)-(b-d)*(e*h-f*g),k=(a-c)*(f-h)-(b-d)*(e-g);if(k){var l=i/k,m=j/k,n=+l.toFixed(2),o=+m.toFixed(2);if(!(n<+P(a,c).toFixed(2)||n>+Q(a,c).toFixed(2)||n<+P(e,g).toFixed(2)||n>+Q(e,g).toFixed(2)||o<+P(b,d).toFixed(2)||o>+Q(b,d).toFixed(2)||o<+P(f,h).toFixed(2)||o>+Q(f,h).toFixed(2)))return{x:l,y:m}}}}function q(a,b,c){var d=j(a),e=j(b);if(!l(d,e))return c?0:[];for(var f=n.apply(0,a),g=n.apply(0,b),h=~~(f/8),k=~~(g/8),m=[],o=[],q={},r=c?0:[],s=0;h+1>s;s++){var t=i.apply(0,a.concat(s/h));m.push({x:t.x,y:t.y,t:s/h})}for(s=0;k+1>s;s++)t=i.apply(0,b.concat(s/k)),o.push({x:t.x,y:t.y,t:s/k});for(s=0;h>s;s++)for(var u=0;k>u;u++){var v=m[s],w=m[s+1],x=o[u],y=o[u+1],z=S(w.x-v.x)<.001?"y":"x",A=S(y.x-x.x)<.001?"y":"x",B=p(v.x,v.y,w.x,w.y,x.x,x.y,y.x,y.y);if(B){if(q[B.x.toFixed(4)]==B.y.toFixed(4))continue;q[B.x.toFixed(4)]=B.y.toFixed(4);var C=v.t+S((B[z]-v[z])/(w[z]-v[z]))*(w.t-v.t),D=x.t+S((B[A]-x[A])/(y[A]-x[A]))*(y.t-x.t);C>=0&&1>=C&&D>=0&&1>=D&&(c?r++:r.push({x:B.x,y:B.y,t1:C,t2:D}))}}return r}function r(a,b){return t(a,b)}function s(a,b){return t(a,b,1)}function t(a,b,c){a=E(a),b=E(b);for(var d,e,f,g,h,i,j,k,l,m,n=c?0:[],o=0,p=a.length;p>o;o++){var r=a[o];if("M"==r[0])d=h=r[1],e=i=r[2];else{"C"==r[0]?(l=[d,e].concat(r.slice(1)),d=l[6],e=l[7]):(l=[d,e,d,e,h,i,h,i],d=h,e=i);for(var s=0,t=b.length;t>s;s++){var u=b[s];if("M"==u[0])f=j=u[1],g=k=u[2];else{"C"==u[0]?(m=[f,g].concat(u.slice(1)),f=m[6],g=m[7]):(m=[f,g,f,g,j,k,j,k],f=j,g=k);var v=q(l,m,c);if(c)n+=v;else{for(var w=0,x=v.length;x>w;w++)v[w].segment1=o,v[w].segment2=s,v[w].bez1=l,v[w].bez2=m;n=n.concat(v)}}}}}return n}function u(a,b,c){var d=v(a);return k(d,b,c)&&t(a,[["M",b,c],["H",d.x2+10]],1)%2==1}function v(a){var b=c(a);if(b.bbox)return J(b.bbox);if(!a)return d();a=E(a);for(var e,f=0,g=0,h=[],i=[],j=0,k=a.length;k>j;j++)if(e=a[j],"M"==e[0])f=e[1],g=e[2],h.push(f),i.push(g);else{var l=D(f,g,e[1],e[2],e[3],e[4],e[5],e[6]);h=h.concat(l.min.x,l.max.x),i=i.concat(l.min.y,l.max.y),f=e[5],g=e[6]}var m=P.apply(0,h),n=P.apply(0,i),o=Q.apply(0,h),p=Q.apply(0,i),q=d(m,n,o-m,p-n);return b.bbox=J(q),q}function w(a,b,c,d,f){if(f)return[["M",+a+ +f,b],["l",c-2*f,0],["a",f,f,0,0,1,f,f],["l",0,d-2*f],["a",f,f,0,0,1,-f,f],["l",2*f-c,0],["a",f,f,0,0,1,-f,-f],["l",0,2*f-d],["a",f,f,0,0,1,f,-f],["z"]];var g=[["M",a,b],["l",c,0],["l",0,d],["l",-c,0],["z"]];return g.toString=e,g}function x(a,b,c,d,f){if(null==f&&null==d&&(d=c),a=+a,b=+b,c=+c,d=+d,null!=f)var g=Math.PI/180,h=a+c*Math.cos(-d*g),i=a+c*Math.cos(-f*g),j=b+c*Math.sin(-d*g),k=b+c*Math.sin(-f*g),l=[["M",h,j],["A",c,c,0,+(f-d>180),0,i,k]];else l=[["M",a,b],["m",0,-d],["a",c,d,0,1,1,0,2*d],["a",c,d,0,1,1,0,-2*d],["z"]];return l.toString=e,l}function y(b){var d=c(b),g=String.prototype.toLowerCase;if(d.rel)return f(d.rel);a.is(b,"array")&&a.is(b&&b[0],"array")||(b=a.parsePathString(b));var h=[],i=0,j=0,k=0,l=0,m=0;"M"==b[0][0]&&(i=b[0][1],j=b[0][2],k=i,l=j,m++,h.push(["M",i,j]));for(var n=m,o=b.length;o>n;n++){var p=h[n]=[],q=b[n];if(q[0]!=g.call(q[0]))switch(p[0]=g.call(q[0]),p[0]){case"a":p[1]=q[1],p[2]=q[2],p[3]=q[3],p[4]=q[4],p[5]=q[5],p[6]=+(q[6]-i).toFixed(3),p[7]=+(q[7]-j).toFixed(3);break;case"v":p[1]=+(q[1]-j).toFixed(3);break;case"m":k=q[1],l=q[2];default:for(var r=1,s=q.length;s>r;r++)p[r]=+(q[r]-(r%2?i:j)).toFixed(3)}else{p=h[n]=[],"m"==q[0]&&(k=q[1]+i,l=q[2]+j);for(var t=0,u=q.length;u>t;t++)h[n][t]=q[t]}var v=h[n].length;switch(h[n][0]){case"z":i=k,j=l;break;case"h":i+=+h[n][v-1];break;case"v":j+=+h[n][v-1];break;default:i+=+h[n][v-2],j+=+h[n][v-1]}}return h.toString=e,d.rel=f(h),h}function z(b){var d=c(b);if(d.abs)return f(d.abs);if(I(b,"array")&&I(b&&b[0],"array")||(b=a.parsePathString(b)),!b||!b.length)return[["M",0,0]];var g,h=[],i=0,j=0,k=0,l=0,m=0;"M"==b[0][0]&&(i=+b[0][1],j=+b[0][2],k=i,l=j,m++,h[0]=["M",i,j]);for(var n,o,p=3==b.length&&"M"==b[0][0]&&"R"==b[1][0].toUpperCase()&&"Z"==b[2][0].toUpperCase(),q=m,r=b.length;r>q;q++){if(h.push(n=[]),o=b[q],g=o[0],g!=g.toUpperCase())switch(n[0]=g.toUpperCase(),n[0]){case"A":n[1]=o[1],n[2]=o[2],n[3]=o[3],n[4]=o[4],n[5]=o[5],n[6]=+o[6]+i,n[7]=+o[7]+j;break;case"V":n[1]=+o[1]+j;break;case"H":n[1]=+o[1]+i;break;case"R":for(var s=[i,j].concat(o.slice(1)),t=2,u=s.length;u>t;t++)s[t]=+s[t]+i,s[++t]=+s[t]+j;h.pop(),h=h.concat(G(s,p));break;case"O":h.pop(),s=x(i,j,o[1],o[2]),s.push(s[0]),h=h.concat(s);break;case"U":h.pop(),h=h.concat(x(i,j,o[1],o[2],o[3])),n=["U"].concat(h[h.length-1].slice(-2));break;case"M":k=+o[1]+i,l=+o[2]+j;default:for(t=1,u=o.length;u>t;t++)n[t]=+o[t]+(t%2?i:j)}else if("R"==g)s=[i,j].concat(o.slice(1)),h.pop(),h=h.concat(G(s,p)),n=["R"].concat(o.slice(-2));else if("O"==g)h.pop(),s=x(i,j,o[1],o[2]),s.push(s[0]),h=h.concat(s);else if("U"==g)h.pop(),h=h.concat(x(i,j,o[1],o[2],o[3])),n=["U"].concat(h[h.length-1].slice(-2));else for(var v=0,w=o.length;w>v;v++)n[v]=o[v];if(g=g.toUpperCase(),"O"!=g)switch(n[0]){case"Z":i=+k,j=+l;break;case"H":i=n[1];break;case"V":j=n[1];break;case"M":k=n[n.length-2],l=n[n.length-1];default:i=n[n.length-2],j=n[n.length-1]}}return h.toString=e,d.abs=f(h),h}function A(a,b,c,d){return[a,b,c,d,c,d]}function B(a,b,c,d,e,f){var g=1/3,h=2/3;return[g*a+h*c,g*b+h*d,g*e+h*c,g*f+h*d,e,f]}function C(b,c,d,e,f,g,h,i,j,k){var l,m=120*O/180,n=O/180*(+f||0),o=[],p=a._.cacher(function(a,b,c){var d=a*N.cos(c)-b*N.sin(c),e=a*N.sin(c)+b*N.cos(c);return{x:d,y:e}});if(k)y=k[0],z=k[1],w=k[2],x=k[3];else{l=p(b,c,-n),b=l.x,c=l.y,l=p(i,j,-n),i=l.x,j=l.y;var q=(N.cos(O/180*f),N.sin(O/180*f),(b-i)/2),r=(c-j)/2,s=q*q/(d*d)+r*r/(e*e);s>1&&(s=N.sqrt(s),d=s*d,e=s*e);var t=d*d,u=e*e,v=(g==h?-1:1)*N.sqrt(S((t*u-t*r*r-u*q*q)/(t*r*r+u*q*q))),w=v*d*r/e+(b+i)/2,x=v*-e*q/d+(c+j)/2,y=N.asin(((c-x)/e).toFixed(9)),z=N.asin(((j-x)/e).toFixed(9));y=w>b?O-y:y,z=w>i?O-z:z,0>y&&(y=2*O+y),0>z&&(z=2*O+z),h&&y>z&&(y-=2*O),!h&&z>y&&(z-=2*O)}var A=z-y;if(S(A)>m){var B=z,D=i,E=j;z=y+m*(h&&z>y?1:-1),i=w+d*N.cos(z),j=x+e*N.sin(z),o=C(i,j,d,e,f,0,h,D,E,[z,B,w,x])}A=z-y;var F=N.cos(y),G=N.sin(y),H=N.cos(z),I=N.sin(z),J=N.tan(A/4),K=4/3*d*J,L=4/3*e*J,M=[b,c],P=[b+K*G,c-L*F],Q=[i+K*I,j-L*H],R=[i,j];if(P[0]=2*M[0]-P[0],P[1]=2*M[1]-P[1],k)return[P,Q,R].concat(o);o=[P,Q,R].concat(o).join().split(",");for(var T=[],U=0,V=o.length;V>U;U++)T[U]=U%2?p(o[U-1],o[U],n).y:p(o[U],o[U+1],n).x;return T}function D(a,b,c,d,e,f,g,h){for(var i,j,k,l,m,n,o,p,q=[],r=[[],[]],s=0;2>s;++s)if(0==s?(j=6*a-12*c+6*e,i=-3*a+9*c-9*e+3*g,k=3*c-3*a):(j=6*b-12*d+6*f,i=-3*b+9*d-9*f+3*h,k=3*d-3*b),S(i)<1e-12){if(S(j)<1e-12)continue;l=-k/j,l>0&&1>l&&q.push(l)}else o=j*j-4*k*i,p=N.sqrt(o),0>o||(m=(-j+p)/(2*i),m>0&&1>m&&q.push(m),n=(-j-p)/(2*i),n>0&&1>n&&q.push(n));for(var t,u=q.length,v=u;u--;)l=q[u],t=1-l,r[0][u]=t*t*t*a+3*t*t*l*c+3*t*l*l*e+l*l*l*g,r[1][u]=t*t*t*b+3*t*t*l*d+3*t*l*l*f+l*l*l*h;return r[0][v]=a,r[1][v]=b,r[0][v+1]=g,r[1][v+1]=h,r[0].length=r[1].length=v+2,{min:{x:P.apply(0,r[0]),y:P.apply(0,r[1])},max:{x:Q.apply(0,r[0]),y:Q.apply(0,r[1])}}}function E(a,b){var d=!b&&c(a);if(!b&&d.curve)return f(d.curve);for(var e=z(a),g=b&&z(b),h={x:0,y:0,bx:0,by:0,X:0,Y:0,qx:null,qy:null},i={x:0,y:0,bx:0,by:0,X:0,Y:0,qx:null,qy:null},j=(function(a,b,c){var d,e;if(!a)return["C",b.x,b.y,b.x,b.y,b.x,b.y];switch(!(a[0]in{T:1,Q:1})&&(b.qx=b.qy=null),a[0]){case"M":b.X=a[1],b.Y=a[2];break;case"A":a=["C"].concat(C.apply(0,[b.x,b.y].concat(a.slice(1))));break;case"S":"C"==c||"S"==c?(d=2*b.x-b.bx,e=2*b.y-b.by):(d=b.x,e=b.y),a=["C",d,e].concat(a.slice(1));break;case"T":"Q"==c||"T"==c?(b.qx=2*b.x-b.qx,b.qy=2*b.y-b.qy):(b.qx=b.x,b.qy=b.y),a=["C"].concat(B(b.x,b.y,b.qx,b.qy,a[1],a[2]));break;case"Q":b.qx=a[1],b.qy=a[2],a=["C"].concat(B(b.x,b.y,a[1],a[2],a[3],a[4]));break;case"L":a=["C"].concat(A(b.x,b.y,a[1],a[2]));break;case"H":a=["C"].concat(A(b.x,b.y,a[1],b.y));break;case"V":a=["C"].concat(A(b.x,b.y,b.x,a[1]));break;case"Z":a=["C"].concat(A(b.x,b.y,b.X,b.Y))}return a}),k=function(a,b){if(a[b].length>7){a[b].shift();for(var c=a[b];c.length;)m[b]="A",g&&(n[b]="A"),a.splice(b++,0,["C"].concat(c.splice(0,6)));a.splice(b,1),r=Q(e.length,g&&g.length||0)}},l=function(a,b,c,d,f){a&&b&&"M"==a[f][0]&&"M"!=b[f][0]&&(b.splice(f,0,["M",d.x,d.y]),c.bx=0,c.by=0,c.x=a[f][1],c.y=a[f][2],r=Q(e.length,g&&g.length||0))},m=[],n=[],o="",p="",q=0,r=Q(e.length,g&&g.length||0);r>q;q++){e[q]&&(o=e[q][0]),"C"!=o&&(m[q]=o,q&&(p=m[q-1])),e[q]=j(e[q],h,p),"A"!=m[q]&&"C"==o&&(m[q]="C"),k(e,q),g&&(g[q]&&(o=g[q][0]),"C"!=o&&(n[q]=o,q&&(p=n[q-1])),g[q]=j(g[q],i,p),"A"!=n[q]&&"C"==o&&(n[q]="C"),k(g,q)),l(e,g,h,i,q),l(g,e,i,h,q);var s=e[q],t=g&&g[q],u=s.length,v=g&&t.length;h.x=s[u-2],h.y=s[u-1],h.bx=M(s[u-4])||h.x,h.by=M(s[u-3])||h.y,i.bx=g&&(M(t[v-4])||i.x),i.by=g&&(M(t[v-3])||i.y),i.x=g&&t[v-2],i.y=g&&t[v-1]}return g||(d.curve=f(e)),g?[e,g]:e}function F(a,b){if(!b)return a;var c,d,e,f,g,h,i;for(a=E(a),e=0,g=a.length;g>e;e++)for(i=a[e],f=1,h=i.length;h>f;f+=2)c=b.x(i[f],i[f+1]),d=b.y(i[f],i[f+1]),i[f]=c,i[f+1]=d;return a}function G(a,b){for(var c=[],d=0,e=a.length;e-2*!b>d;d+=2){var f=[{x:+a[d-2],y:+a[d-1]},{x:+a[d],y:+a[d+1]},{x:+a[d+2],y:+a[d+3]},{x:+a[d+4],y:+a[d+5]}];b?d?e-4==d?f[3]={x:+a[0],y:+a[1]}:e-2==d&&(f[2]={x:+a[0],y:+a[1]},f[3]={x:+a[2],y:+a[3]}):f[0]={x:+a[e-2],y:+a[e-1]}:e-4==d?f[3]=f[2]:d||(f[0]={x:+a[d],y:+a[d+1]}),c.push(["C",(-f[0].x+6*f[1].x+f[2].x)/6,(-f[0].y+6*f[1].y+f[2].y)/6,(f[1].x+6*f[2].x-f[3].x)/6,(f[1].y+6*f[2].y-f[3].y)/6,f[2].x,f[2].y])}return c}var H=b.prototype,I=a.is,J=a._.clone,K="hasOwnProperty",L=/,?([a-z]),?/gi,M=parseFloat,N=Math,O=N.PI,P=N.min,Q=N.max,R=N.pow,S=N.abs,T=h(1),U=h(),V=h(0,1),W=a._unit2px,X={path:function(a){return a.attr("path")},circle:function(a){var b=W(a);return x(b.cx,b.cy,b.r)},ellipse:function(a){var b=W(a);return x(b.cx||0,b.cy||0,b.rx,b.ry)},rect:function(a){var b=W(a);return w(b.x||0,b.y||0,b.width,b.height,b.rx,b.ry)},image:function(a){var b=W(a);return w(b.x||0,b.y||0,b.width,b.height)},line:function(a){return"M"+[a.attr("x1")||0,a.attr("y1")||0,a.attr("x2"),a.attr("y2")]},polyline:function(a){return"M"+a.attr("points")},polygon:function(a){return"M"+a.attr("points")+"z"},deflt:function(a){var b=a.node.getBBox();return w(b.x,b.y,b.width,b.height)}};a.path=c,a.path.getTotalLength=T,a.path.getPointAtLength=U,a.path.getSubpath=function(a,b,c){if(this.getTotalLength(a)-c<1e-6)return V(a,b).end;var d=V(a,c,1);return b?V(d,b).end:d},H.getTotalLength=function(){return this.node.getTotalLength?this.node.getTotalLength():void 0},H.getPointAtLength=function(a){return U(this.attr("d"),a)},H.getSubpath=function(b,c){return a.path.getSubpath(this.attr("d"),b,c)},a._.box=d,a.path.findDotsAtSegment=i,a.path.bezierBBox=j,a.path.isPointInsideBBox=k,a.path.isBBoxIntersect=l,a.path.intersection=r,a.path.intersectionNumber=s,a.path.isPointInside=u,a.path.getBBox=v,a.path.get=X,a.path.toRelative=y,a.path.toAbsolute=z,a.path.toCubic=E,a.path.map=F,a.path.toString=e,a.path.clone=f}),d.plugin(function(a){var d=Math.max,e=Math.min,f=function(a){if(this.items=[],this.bindings={},this.length=0,this.type="set",a)for(var b=0,c=a.length;c>b;b++)a[b]&&(this[this.items.length]=this.items[this.items.length]=a[b],this.length++)},g=f.prototype;g.push=function(){for(var a,b,c=0,d=arguments.length;d>c;c++)a=arguments[c],a&&(b=this.items.length,this[b]=this.items[b]=a,this.length++);return this},g.pop=function(){return this.length&&delete this[this.length--],this.items.pop()},g.forEach=function(a,b){for(var c=0,d=this.items.length;d>c;c++)if(a.call(b,this.items[c],c)===!1)return this;return this},g.animate=function(d,e,f,g){"function"!=typeof f||f.length||(g=f,f=c.linear),d instanceof a._.Animation&&(g=d.callback,f=d.easing,e=f.dur,d=d.attr);var h=arguments;if(a.is(d,"array")&&a.is(h[h.length-1],"array"))var i=!0;var j,k=function(){j?this.b=j:j=this.b},l=0,m=g&&function(){l++==this.length&&g.call(this)};return this.forEach(function(a,c){b.once("snap.animcreated."+a.id,k),i?h[c]&&a.animate.apply(a,h[c]):a.animate(d,e,f,m)})},g.remove=function(){for(;this.length;)this.pop().remove();return this},g.bind=function(a,b,c){var d={};if("function"==typeof b)this.bindings[a]=b;else{var e=c||a;this.bindings[a]=function(a){d[e]=a,b.attr(d)}}return this},g.attr=function(a){var b={};for(var c in a)this.bindings[c]?this.bindings[c](a[c]):b[c]=a[c];for(var d=0,e=this.items.length;e>d;d++)this.items[d].attr(b);return this},g.clear=function(){for(;this.length;)this.pop()},g.splice=function(a,b){a=0>a?d(this.length+a,0):a,b=d(0,e(this.length-a,b));var c,g=[],h=[],i=[];for(c=2;c<arguments.length;c++)i.push(arguments[c]);for(c=0;b>c;c++)h.push(this[a+c]);for(;c<this.length-a;c++)g.push(this[a+c]);var j=i.length;for(c=0;c<j+g.length;c++)this.items[a+c]=this[a+c]=j>c?i[c]:g[c-j];for(c=this.items.length=this.length-=b-j;this[c];)delete this[c++];return new f(h)},g.exclude=function(a){for(var b=0,c=this.length;c>b;b++)if(this[b]==a)return this.splice(b,1),!0;return!1},g.insertAfter=function(a){for(var b=this.items.length;b--;)this.items[b].insertAfter(a);return this},g.getBBox=function(){for(var a=[],b=[],c=[],f=[],g=this.items.length;g--;)if(!this.items[g].removed){var h=this.items[g].getBBox();a.push(h.x),b.push(h.y),c.push(h.x+h.width),f.push(h.y+h.height)}return a=e.apply(0,a),b=e.apply(0,b),c=d.apply(0,c),f=d.apply(0,f),{x:a,y:b,x2:c,y2:f,width:c-a,height:f-b,cx:a+(c-a)/2,cy:b+(f-b)/2}},g.clone=function(a){a=new f;for(var b=0,c=this.items.length;c>b;b++)a.push(this.items[b].clone());return a},g.toString=function(){return"Snap‘s set"},g.type="set",a.set=function(){var a=new f;return arguments.length&&a.push.apply(a,Array.prototype.slice.call(arguments,0)),a}}),d.plugin(function(a,c){function d(a){var b=a[0];switch(b.toLowerCase()){case"t":return[b,0,0];case"m":return[b,1,0,0,1,0,0];case"r":return 4==a.length?[b,0,a[2],a[3]]:[b,0];case"s":return 5==a.length?[b,1,1,a[3],a[4]]:3==a.length?[b,1,1]:[b,1]}}function e(b,c,e){c=m(c).replace(/\.{3}|\u2026/g,b),b=a.parseTransformString(b)||[],c=a.parseTransformString(c)||[];for(var f,g,h,k,l=Math.max(b.length,c.length),n=[],o=[],p=0;l>p;p++){if(h=b[p]||d(c[p]),k=c[p]||d(h),h[0]!=k[0]||"r"==h[0].toLowerCase()&&(h[2]!=k[2]||h[3]!=k[3])||"s"==h[0].toLowerCase()&&(h[3]!=k[3]||h[4]!=k[4])){b=a._.transform2matrix(b,e()),c=a._.transform2matrix(c,e()),n=[["m",b.a,b.b,b.c,b.d,b.e,b.f]],o=[["m",c.a,c.b,c.c,c.d,c.e,c.f]];break}for(n[p]=[],o[p]=[],f=0,g=Math.max(h.length,k.length);g>f;f++)f in h&&(n[p][f]=h[f]),f in k&&(o[p][f]=k[f])}return{from:j(n),to:j(o),f:i(n)}}function f(a){return a}function g(a){return function(b){return+b.toFixed(3)+a}}function h(b){return a.rgb(b[0],b[1],b[2])}function i(a){var b,c,d,e,f,g,h=0,i=[];for(b=0,c=a.length;c>b;b++){for(f="[",g=['"'+a[b][0]+'"'],d=1,e=a[b].length;e>d;d++)g[d]="val["+h++ +"]";
+f+=g+"]",i[b]=f}return Function("val","return Snap.path.toString.call(["+i+"])")}function j(a){for(var b=[],c=0,d=a.length;d>c;c++)for(var e=1,f=a[c].length;f>e;e++)b.push(a[c][e]);return b}var k={},l=/[a-z]+$/i,m=String;k.stroke=k.fill="colour",c.prototype.equal=function(a,c){return b("snap.util.equal",this,a,c).firstDefined()},b.on("snap.util.equal",function(b,c){var d,n,o=m(this.attr(b)||""),p=this;if(o==+o&&c==+c)return{from:+o,to:+c,f:f};if("colour"==k[b])return d=a.color(o),n=a.color(c),{from:[d.r,d.g,d.b,d.opacity],to:[n.r,n.g,n.b,n.opacity],f:h};if("transform"==b||"gradientTransform"==b||"patternTransform"==b)return c instanceof a.Matrix&&(c=c.toTransformString()),a._.rgTransform.test(c)||(c=a._.svgTransform2string(c)),e(o,c,function(){return p.getBBox(1)});if("d"==b||"path"==b)return d=a.path.toCubic(o,c),{from:j(d[0]),to:j(d[1]),f:i(d[0])};if("points"==b)return d=m(o).split(a._.separator),n=m(c).split(a._.separator),{from:d,to:n,f:function(a){return a}};aUnit=o.match(l);var q=m(c).match(l);return aUnit&&aUnit==q?{from:parseFloat(o),to:parseFloat(c),f:g(aUnit)}:{from:this.asPX(b),to:this.asPX(b,c),f:f}})}),d.plugin(function(a,c,d,e){for(var f=c.prototype,g="hasOwnProperty",h=("createTouch"in e.doc),i=["click","dblclick","mousedown","mousemove","mouseout","mouseover","mouseup","touchstart","touchmove","touchend","touchcancel"],j={mousedown:"touchstart",mousemove:"touchmove",mouseup:"touchend"},k=(function(a,b){var c="y"==a?"scrollTop":"scrollLeft",d=b&&b.node?b.node.ownerDocument:e.doc;return d[c in d.documentElement?"documentElement":"body"][c]}),l=function(){this.returnValue=!1},m=function(){return this.originalEvent.preventDefault()},n=function(){this.cancelBubble=!0},o=function(){return this.originalEvent.stopPropagation()},p=function(){return e.doc.addEventListener?function(a,b,c,d){var e=h&&j[b]?j[b]:b,f=function(e){var f=k("y",d),i=k("x",d);if(h&&j[g](b))for(var l=0,n=e.targetTouches&&e.targetTouches.length;n>l;l++)if(e.targetTouches[l].target==a||a.contains(e.targetTouches[l].target)){var p=e;e=e.targetTouches[l],e.originalEvent=p,e.preventDefault=m,e.stopPropagation=o;break}var q=e.clientX+i,r=e.clientY+f;return c.call(d,e,q,r)};return b!==e&&a.addEventListener(b,f,!1),a.addEventListener(e,f,!1),function(){return b!==e&&a.removeEventListener(b,f,!1),a.removeEventListener(e,f,!1),!0}}:e.doc.attachEvent?function(a,b,c,d){var e=function(a){a=a||d.node.ownerDocument.window.event;var b=k("y",d),e=k("x",d),f=a.clientX+e,g=a.clientY+b;return a.preventDefault=a.preventDefault||l,a.stopPropagation=a.stopPropagation||n,c.call(d,a,f,g)};a.attachEvent("on"+b,e);var f=function(){return a.detachEvent("on"+b,e),!0};return f}:void 0}(),q=[],r=function(a){for(var c,d=a.clientX,e=a.clientY,f=k("y"),g=k("x"),i=q.length;i--;){if(c=q[i],h){for(var j,l=a.touches&&a.touches.length;l--;)if(j=a.touches[l],j.identifier==c.el._drag.id||c.el.node.contains(j.target)){d=j.clientX,e=j.clientY,(a.originalEvent?a.originalEvent:a).preventDefault();break}}else a.preventDefault();{var m=c.el.node;m.nextSibling,m.parentNode,m.style.display}d+=g,e+=f,b("snap.drag.move."+c.el.id,c.move_scope||c.el,d-c.el._drag.x,e-c.el._drag.y,d,e,a)}},s=function(c){a.unmousemove(r).unmouseup(s);for(var d,e=q.length;e--;)d=q[e],d.el._drag={},b("snap.drag.end."+d.el.id,d.end_scope||d.start_scope||d.move_scope||d.el,c);q=[]},t=i.length;t--;)!function(b){a[b]=f[b]=function(c,d){return a.is(c,"function")&&(this.events=this.events||[],this.events.push({name:b,f:c,unbind:p(this.node||document,b,c,d||this)})),this},a["un"+b]=f["un"+b]=function(a){for(var c=this.events||[],d=c.length;d--;)if(c[d].name==b&&(c[d].f==a||!a))return c[d].unbind(),c.splice(d,1),!c.length&&delete this.events,this;return this}}(i[t]);f.hover=function(a,b,c,d){return this.mouseover(a,c).mouseout(b,d||c)},f.unhover=function(a,b){return this.unmouseover(a).unmouseout(b)};var u=[];f.drag=function(c,d,e,f,g,h){function i(i,j,k){(i.originalEvent||i).preventDefault(),this._drag.x=j,this._drag.y=k,this._drag.id=i.identifier,!q.length&&a.mousemove(r).mouseup(s),q.push({el:this,move_scope:f,start_scope:g,end_scope:h}),d&&b.on("snap.drag.start."+this.id,d),c&&b.on("snap.drag.move."+this.id,c),e&&b.on("snap.drag.end."+this.id,e),b("snap.drag.start."+this.id,g||f||this,j,k,i)}if(!arguments.length){var j;return this.drag(function(a,b){this.attr({transform:j+(j?"T":"t")+[a,b]})},function(){j=this.transform().local})}return this._drag={},u.push({el:this,start:i}),this.mousedown(i),this},f.undrag=function(){for(var c=u.length;c--;)u[c].el==this&&(this.unmousedown(u[c].start),u.splice(c,1),b.unbind("snap.drag.*."+this.id));return!u.length&&a.unmousemove(r).unmouseup(s),this}}),d.plugin(function(a,c,d){var e=(c.prototype,d.prototype),f=/^\s*url\((.+)\)/,g=String,h=a._.$;a.filter={},e.filter=function(b){var d=this;"svg"!=d.type&&(d=d.paper);var e=a.parse(g(b)),f=a._.id(),i=(d.node.offsetWidth,d.node.offsetHeight,h("filter"));return h(i,{id:f,filterUnits:"userSpaceOnUse"}),i.appendChild(e.node),d.defs.appendChild(i),new c(i)},b.on("snap.util.getattr.filter",function(){b.stop();var c=h(this.node,"filter");if(c){var d=g(c).match(f);return d&&a.select(d[1])}}),b.on("snap.util.attr.filter",function(d){if(d instanceof c&&"filter"==d.type){b.stop();var e=d.node.id;e||(h(d.node,{id:d.id}),e=d.id),h(this.node,{filter:a.url(e)})}d&&"none"!=d||(b.stop(),this.node.removeAttribute("filter"))}),a.filter.blur=function(b,c){null==b&&(b=2);var d=null==c?b:[b,c];return a.format('<feGaussianBlur stdDeviation="{def}"/>',{def:d})},a.filter.blur.toString=function(){return this()},a.filter.shadow=function(b,c,d,e,f){return"string"==typeof d&&(e=d,f=e,d=4),"string"!=typeof e&&(f=e,e="#000"),e=e||"#000",null==d&&(d=4),null==f&&(f=1),null==b&&(b=0,c=2),null==c&&(c=b),e=a.color(e),a.format('<feGaussianBlur in="SourceAlpha" stdDeviation="{blur}"/><feOffset dx="{dx}" dy="{dy}" result="offsetblur"/><feFlood flood-color="{color}"/><feComposite in2="offsetblur" operator="in"/><feComponentTransfer><feFuncA type="linear" slope="{opacity}"/></feComponentTransfer><feMerge><feMergeNode/><feMergeNode in="SourceGraphic"/></feMerge>',{color:e,dx:b,dy:c,blur:d,opacity:f})},a.filter.shadow.toString=function(){return this()},a.filter.grayscale=function(b){return null==b&&(b=1),a.format('<feColorMatrix type="matrix" values="{a} {b} {c} 0 0 {d} {e} {f} 0 0 {g} {b} {h} 0 0 0 0 0 1 0"/>',{a:.2126+.7874*(1-b),b:.7152-.7152*(1-b),c:.0722-.0722*(1-b),d:.2126-.2126*(1-b),e:.7152+.2848*(1-b),f:.0722-.0722*(1-b),g:.2126-.2126*(1-b),h:.0722+.9278*(1-b)})},a.filter.grayscale.toString=function(){return this()},a.filter.sepia=function(b){return null==b&&(b=1),a.format('<feColorMatrix type="matrix" values="{a} {b} {c} 0 0 {d} {e} {f} 0 0 {g} {h} {i} 0 0 0 0 0 1 0"/>',{a:.393+.607*(1-b),b:.769-.769*(1-b),c:.189-.189*(1-b),d:.349-.349*(1-b),e:.686+.314*(1-b),f:.168-.168*(1-b),g:.272-.272*(1-b),h:.534-.534*(1-b),i:.131+.869*(1-b)})},a.filter.sepia.toString=function(){return this()},a.filter.saturate=function(b){return null==b&&(b=1),a.format('<feColorMatrix type="saturate" values="{amount}"/>',{amount:1-b})},a.filter.saturate.toString=function(){return this()},a.filter.hueRotate=function(b){return b=b||0,a.format('<feColorMatrix type="hueRotate" values="{angle}"/>',{angle:b})},a.filter.hueRotate.toString=function(){return this()},a.filter.invert=function(b){return null==b&&(b=1),a.format('<feComponentTransfer><feFuncR type="table" tableValues="{amount} {amount2}"/><feFuncG type="table" tableValues="{amount} {amount2}"/><feFuncB type="table" tableValues="{amount} {amount2}"/></feComponentTransfer>',{amount:b,amount2:1-b})},a.filter.invert.toString=function(){return this()},a.filter.brightness=function(b){return null==b&&(b=1),a.format('<feComponentTransfer><feFuncR type="linear" slope="{amount}"/><feFuncG type="linear" slope="{amount}"/><feFuncB type="linear" slope="{amount}"/></feComponentTransfer>',{amount:b})},a.filter.brightness.toString=function(){return this()},a.filter.contrast=function(b){return null==b&&(b=1),a.format('<feComponentTransfer><feFuncR type="linear" slope="{amount}" intercept="{amount2}"/><feFuncG type="linear" slope="{amount}" intercept="{amount2}"/><feFuncB type="linear" slope="{amount}" intercept="{amount2}"/></feComponentTransfer>',{amount:b,amount2:.5-b/2})},a.filter.contrast.toString=function(){return this()}}),d}); \ No newline at end of file
diff --git a/src/pixelated/assets/welcome.mail b/src/pixelated/assets/welcome.mail
new file mode 100644
index 00000000..3f233143
--- /dev/null
+++ b/src/pixelated/assets/welcome.mail
@@ -0,0 +1,72 @@
+From: Pixelated Team <pixelated-team@pixelated-project.org>
+Date: Sat, 21 Mar 2015 19:30:09 -0300
+Subject: Welcome to Pixelated Mail
+To: Replace <will.be@the.user>
+Content-Type: multipart/alternative; boundary=000boundary000
+
+--000boundary000
+Welcome to Pixelated Mail, a modern email with encryption.
+
+Pixelated Mail is an open source project that aims to provide secure email on the browser with all the functionality we've come to expect of a modern email client.
+
+How to use it
+Pixelated Mail should provide functionality that is similar to what you've come to expect of your email software. To the left, you will find a navigation bar that provides access to all your mailboxes and tags. Clicking on them will load the corresponding messages on the middle pane - the mail list. Clicking on a message will load it on this pane, but you know it already!
+
+To compose a message look for the big blue button on the top left. You can add tags to received messages by clicking on the "+" sign under the message subject. You can also find the encryption status of messages just above the sender/recipient information.
+
+A bit more about Pixelated
+Pixelated is an open source project licensed under AGPL 3.0. It is composed of 3 main parts, the User Agent (what you are using right now), the Dispatcher (what allows you to log in with different accounts to the same instance) and the Platform (which provides the email service you will use to send and receive messages - the server behind the @ sign on your new mail address). You can learn more by visiting https://pixelated-project.org/.
+
+About this message and encryption status
+This message was not encrypted, in other words, it could have been read by others at some point during transmission, like any other email client.
+To send encrypted messages you have to have the public keys of the recipients, in Pixelated you have 2 options:
+send an email to another Pixelated account: public keys are exchanged by default.
+send an email to another email provider: you should had exchanged public keys with the recipients previously.
+
+
+Enjoy your secure messaging!
+
+--000boundary000
+Content-Type: text/html; charset=UTF-8
+Content-Transfer-Encoding: quoted-printable
+<p>
+Welcome to Pixelated Mail, a modern email with encryption.
+</p>
+<p>
+Pixelated Mail is an open source project that aims to provide secure email on the browser with all the functionality we've come to expect of a modern email client.
+</p>
+<p>
+<b>How to use it</b><br>
+Pixelated Mail should provide functionality that is similar to what you've =
+come to expect of your email software. To the left, you will find a navigat=
+ion bar that provides access to all your mailboxes and tags. Clicking on th=
+em will load the corresponding messages on the middle pane - the mail list.=
+ Clicking on a message will load it on this pane, but you know it already!
+</p>
+<p>
+To compose a message look for the big blue button on the top left. You can =
+add tags to received messages by clicking on the "+" sign under the message=
+ subject. You can also find the encryption status of messages just above th=
+e sender/recipient information.
+</p>
+<p>
+<b>A bit more about Pixelated</b><br>
+Pixelated is an open source project licensed under AGPL 3.0. It is composed=
+ of 3 main parts, the User Agent (what you are using right now), the Dispat=
+cher (what allows you to log in with different accounts to the same instanc=
+e) and the Platform (which provides the email service you will use to send =
+and receive messages - the server behind the @ sign on your new mail addres=
+s). You can learn more by visiting <a src=3D"https://pixelated-project.org/=
+">https://pixelated-project.org/</a>.
+</p>
+<p>
+<b>About this message and encryption status</b><br>
+This message was not encrypted, in other words, it could have been read by others at some point during transmission like any other email client.
+To send encrypted messages you have to have the public Keys of the recipients, in Pixelated you have 2 options:
+<p>- sending an email to another Pixelated account: public keys are exchanged by default.
+<br>- sending an email to another email provider: you should had exchanged public keys with the recipients previously.
+</p>
+<p>
+Enjoy your secure messaging!
+</p>
+--000boundary000--
diff --git a/src/pixelated/bitmask_libraries/__init__.py b/src/pixelated/bitmask_libraries/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/src/pixelated/bitmask_libraries/__init__.py
diff --git a/src/pixelated/bitmask_libraries/certs.py b/src/pixelated/bitmask_libraries/certs.py
new file mode 100644
index 00000000..f0681608
--- /dev/null
+++ b/src/pixelated/bitmask_libraries/certs.py
@@ -0,0 +1,56 @@
+#
+# 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 os
+
+
+class LeapCertificate(object):
+
+ LEAP_CERT = None
+ LEAP_FINGERPRINT = None
+
+ def __init__(self, provider):
+ self._config = provider.config
+ self._server_name = provider.server_name
+ self._provider = provider
+
+ @staticmethod
+ def set_cert_and_fingerprint(cert_file=None, cert_fingerprint=None):
+ if cert_fingerprint is None:
+ LeapCertificate.LEAP_CERT = str(cert_file) if cert_file else True
+ LeapCertificate.LEAP_FINGERPRINT = None
+ else:
+ LeapCertificate.LEAP_FINGERPRINT = cert_fingerprint
+ LeapCertificate.LEAP_CERT = False
+
+ @property
+ def provider_web_cert(self):
+ return self.LEAP_CERT
+
+ @property
+ def provider_api_cert(self):
+ return str(os.path.join(self._provider.config.leap_home, 'providers', self._server_name, 'keys', 'client', 'api.pem'))
+
+ def setup_ca_bundle(self):
+ path = os.path.join(self._provider.config.leap_home,
+ 'providers', self._server_name, 'keys', 'client')
+ if not os.path.isdir(path):
+ os.makedirs(path, 0700)
+ self._download_cert(self.provider_api_cert)
+
+ def _download_cert(self, cert_file_name):
+ cert = self._provider.fetch_valid_certificate()
+ with open(cert_file_name, 'w') as file:
+ file.write(cert)
diff --git a/src/pixelated/bitmask_libraries/config.py b/src/pixelated/bitmask_libraries/config.py
new file mode 100644
index 00000000..c521a093
--- /dev/null
+++ b/src/pixelated/bitmask_libraries/config.py
@@ -0,0 +1,46 @@
+#
+# 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 os
+from distutils.spawn import find_executable
+
+
+def discover_gpg_binary():
+ path = find_executable('gpg')
+ if path is None:
+ raise Exception('Did not find a gpg executable!')
+
+ if os.path.islink(path):
+ path = os.path.realpath(path)
+
+ return path
+
+
+SYSTEM_CA_BUNDLE = True
+
+
+class LeapConfig(object):
+
+ def __init__(self,
+ leap_home=None,
+ timeout_in_s=15,
+ start_background_jobs=False,
+ gpg_binary=discover_gpg_binary()):
+
+ self.leap_home = leap_home
+ self.timeout_in_s = timeout_in_s
+ self.start_background_jobs = start_background_jobs
+ self.gpg_binary = gpg_binary
diff --git a/src/pixelated/bitmask_libraries/nicknym.py b/src/pixelated/bitmask_libraries/nicknym.py
new file mode 100644
index 00000000..7914c567
--- /dev/null
+++ b/src/pixelated/bitmask_libraries/nicknym.py
@@ -0,0 +1,63 @@
+#
+# 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/>.
+from leap.keymanager import KeyManager, openpgp, KeyNotFound
+from .certs import LeapCertificate
+from twisted.internet import defer
+import logging
+
+logger = logging.getLogger(__name__)
+
+
+class NickNym(object):
+
+ def __init__(self, provider, config, soledad, email_address, token, uuid):
+ nicknym_url = _discover_nicknym_server(provider)
+ self._email = email_address
+ self.keymanager = KeyManager(self._email, nicknym_url,
+ soledad,
+ token=token, ca_cert_path=LeapCertificate(
+ provider).provider_api_cert, api_uri=provider.api_uri,
+ api_version=provider.api_version,
+ uid=uuid, gpgbinary=config.gpg_binary)
+
+ @defer.inlineCallbacks
+ def generate_openpgp_key(self):
+ key_present = yield self._key_exists(self._email)
+ if not key_present:
+ logger.info("Generating keys - this could take a while...")
+ yield self._gen_key()
+ yield self._send_key_to_leap()
+
+ @defer.inlineCallbacks
+ def _key_exists(self, email):
+ try:
+ yield self.fetch_key(email, private=True, fetch_remote=False)
+ defer.returnValue(True)
+ except KeyNotFound:
+ defer.returnValue(False)
+
+ def fetch_key(self, email, private=False, fetch_remote=True):
+ return self.keymanager.get_key(email, openpgp.OpenPGPKey, private=private, fetch_remote=fetch_remote)
+
+ def _gen_key(self):
+ return self.keymanager.gen_key(openpgp.OpenPGPKey)
+
+ def _send_key_to_leap(self):
+ return self.keymanager.send_key(openpgp.OpenPGPKey)
+
+
+def _discover_nicknym_server(provider):
+ return 'https://nicknym.%s:6425/' % provider.domain
diff --git a/src/pixelated/bitmask_libraries/provider.py b/src/pixelated/bitmask_libraries/provider.py
new file mode 100644
index 00000000..a42b7be9
--- /dev/null
+++ b/src/pixelated/bitmask_libraries/provider.py
@@ -0,0 +1,171 @@
+#
+# 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 json
+import os
+
+from leap.common.certs import get_digest
+import requests
+from .certs import LeapCertificate
+from pixelated.support.tls_adapter import EnforceTLSv1Adapter
+from pixelated.bitmask_libraries.soledad import SoledadDiscoverException
+
+
+class LeapProvider(object):
+
+ def __init__(self, server_name, config):
+ self.server_name = server_name
+ self.config = config
+ self.local_ca_crt = '%s/ca.crt' % self.config.leap_home
+ self.provider_json = self.fetch_provider_json()
+
+ @property
+ def api_uri(self):
+ return self.provider_json.get('api_uri')
+
+ @property
+ def ca_cert_fingerprint(self):
+ return self.provider_json.get('ca_cert_fingerprint')
+
+ @property
+ def ca_cert_uri(self):
+ return self.provider_json.get('ca_cert_uri')
+
+ @property
+ def api_version(self):
+ return self.provider_json.get('api_version')
+
+ @property
+ def domain(self):
+ return self.provider_json.get('domain')
+
+ @property
+ def services(self):
+ return self.provider_json.get('services')
+
+ def __hash__(self):
+ return hash(self.server_name)
+
+ def __eq__(self, other):
+ return self.server_name == other.server_name
+
+ def ensure_supports_mx(self):
+ if 'mx' not in self.services:
+ raise Exception
+
+ def download_certificate(self, filename=None):
+ """
+ Downloads the server certificate, validates it against the provided fingerprint and stores it to file
+ """
+ path = filename or self.local_ca_crt
+
+ directory = self._extract_directory(path)
+ if not os.path.exists(directory):
+ os.makedirs(directory)
+
+ cert = self.fetch_valid_certificate()
+ with open(path, 'w') as out:
+ out.write(cert)
+
+ def _extract_directory(self, path):
+ splited = path.split('/')
+ splited.pop(-1)
+ directory = '/'.join(splited)
+ return directory
+
+ def fetch_valid_certificate(self):
+ cert = self._fetch_certificate()
+ self.validate_certificate(cert)
+ return cert
+
+ def _fetch_certificate(self):
+ cert_url = '%s/ca.crt' % self._provider_base_url()
+ response = self._validated_get(cert_url)
+ cert_data = response.content
+ return cert_data
+
+ def validate_certificate(self, cert_data=None):
+ if cert_data is None:
+ cert_data = self._fetch_certificate()
+
+ parts = str(self.ca_cert_fingerprint).split(':')
+ method = parts[0].strip()
+ fingerprint = parts[1].strip()
+
+ digest = get_digest(cert_data, method)
+
+ if fingerprint.strip() != digest:
+ raise Exception('Certificate fingerprints don\'t match! Expected [%s] but got [%s]' % (
+ fingerprint.strip(), digest))
+
+ def smtp_info(self):
+ json_data = self.fetch_smtp_json()
+ hosts = json_data['hosts']
+ hostname = hosts.keys()[0]
+ host = hosts[hostname]
+ return host['hostname'], host['port']
+
+ def _validated_get(self, url):
+ session = requests.session()
+ try:
+ session.mount(
+ 'https://', EnforceTLSv1Adapter(assert_fingerprint=LeapCertificate.LEAP_FINGERPRINT))
+ response = session.get(url, verify=LeapCertificate(
+ self).provider_web_cert, timeout=self.config.timeout_in_s)
+ response.raise_for_status()
+ return response
+ finally:
+ session.close()
+
+ def fetch_provider_json(self):
+ url = '%s/provider.json' % self._provider_base_url()
+ response = self._validated_get(url)
+ json_data = json.loads(response.content)
+ return json_data
+
+ def fetch_soledad_json(self):
+ service_url = "%s/%s/config/soledad-service.json" % (
+ self.api_uri, self.api_version)
+ response = requests.get(service_url, verify=LeapCertificate(
+ self).provider_api_cert, timeout=self.config.timeout_in_s)
+ response.raise_for_status()
+ return json.loads(response.content)
+
+ def fetch_smtp_json(self):
+ service_url = '%s/%s/config/smtp-service.json' % (
+ self.api_uri, self.api_version)
+ response = requests.get(service_url, verify=LeapCertificate(
+ self).provider_api_cert, timeout=self.config.timeout_in_s)
+ response.raise_for_status()
+ return json.loads(response.content)
+
+ def _provider_base_url(self):
+ return 'https://%s' % self.server_name
+
+ def address_for(self, username):
+ return '%s@%s' % (username, self.domain)
+
+ def discover_soledad_server(self, user_uuid):
+ try:
+ json_data = self.fetch_soledad_json()
+
+ hosts = json_data['hosts']
+ host = hosts.keys()[0]
+ server_url = 'https://%s:%d/user-%s' % \
+ (hosts[host]['hostname'], hosts[host]['port'],
+ user_uuid)
+ return server_url
+ except Exception, e:
+ raise SoledadDiscoverException(e)
diff --git a/src/pixelated/bitmask_libraries/session.py b/src/pixelated/bitmask_libraries/session.py
new file mode 100644
index 00000000..a8e1e6f1
--- /dev/null
+++ b/src/pixelated/bitmask_libraries/session.py
@@ -0,0 +1,320 @@
+#
+# 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 errno
+import traceback
+import sys
+import os
+import requests
+import logging
+
+from twisted.internet import reactor, defer
+from pixelated.bitmask_libraries.certs import LeapCertificate
+from pixelated.adapter.mailstore import LeapMailStore
+from leap.mail.incoming.service import IncomingMail
+from leap.mail.mail import Account
+from leap.auth import SRPAuth
+from .nicknym import NickNym
+from .smtp import LeapSMTPConfig
+from .soledad import SoledadFactory
+import leap.common.certs as leap_certs
+
+from leap.common.events import (
+ register, unregister,
+ catalog as events
+)
+
+
+log = logging.getLogger(__name__)
+
+
+class LeapSession(object):
+
+ def __init__(self, provider, user_auth, mail_store, soledad, nicknym, smtp_config):
+ self.smtp_config = smtp_config
+ self.config = provider.config
+ self.provider = provider
+ self.user_auth = user_auth
+ self.mail_store = mail_store
+ self.soledad = soledad
+ self.nicknym = nicknym
+ self.fresh_account = False
+ self.incoming_mail_fetcher = None
+ self.account = None
+ self._has_been_initially_synced = False
+ self._sem_intial_sync = defer.DeferredLock()
+ self._is_closed = False
+ register(events.KEYMANAGER_FINISHED_KEY_GENERATION,
+ self._set_fresh_account, uid=self.account_email())
+
+ @defer.inlineCallbacks
+ def initial_sync(self):
+ yield self._sem_intial_sync.acquire()
+ try:
+ yield self.sync()
+ if not self._has_been_initially_synced:
+ yield self.after_first_sync()
+ self._has_been_initially_synced = True
+ finally:
+ yield self._sem_intial_sync.release()
+ defer.returnValue(self)
+
+ @defer.inlineCallbacks
+ def after_first_sync(self):
+ yield self.nicknym.generate_openpgp_key()
+ yield self._create_account(self.soledad)
+ self.incoming_mail_fetcher = yield self._create_incoming_mail_fetcher(
+ self.nicknym,
+ self.soledad,
+ self.account,
+ self.account_email())
+ reactor.callFromThread(self.incoming_mail_fetcher.startService)
+
+ def _create_account(self, soledad):
+ self.account = Account(soledad)
+ return self.account.deferred_initialization
+
+ def _set_fresh_account(self, event, email_address):
+ log.debug('Key for email %s has been generated' % email_address)
+ if email_address == self.account_email():
+ self.fresh_account = True
+
+ def account_email(self):
+ name = self.user_auth.username
+ return self.provider.address_for(name)
+
+ def close(self):
+ self._is_closed = True
+ self.stop_background_jobs()
+ unregister(events.KEYMANAGER_FINISHED_KEY_GENERATION,
+ uid=self.account_email())
+ self.soledad.close()
+ self.remove_from_cache()
+ self._close_account()
+
+ @property
+ def is_closed(self):
+ return self._is_closed
+
+ def _close_account(self):
+ if self.account:
+ self.account.end_session()
+
+ def remove_from_cache(self):
+ key = SessionCache.session_key(self.provider, self.user_auth.username)
+ SessionCache.remove_session(key)
+
+ @defer.inlineCallbacks
+ def _create_incoming_mail_fetcher(self, nicknym, soledad, account, user_mail):
+ inbox = yield account.callWhenReady(lambda _: account.get_collection_by_mailbox('INBOX'))
+ defer.returnValue(IncomingMail(nicknym.keymanager,
+ soledad,
+ inbox,
+ user_mail))
+
+ def stop_background_jobs(self):
+ if self.incoming_mail_fetcher:
+ reactor.callFromThread(self.incoming_mail_fetcher.stopService)
+ self.incoming_mail_fetcher = None
+
+ def sync(self):
+ try:
+ return self.soledad.sync()
+ except:
+ traceback.print_exc(file=sys.stderr)
+ raise
+
+
+class SmtpClientCertificate(object):
+
+ def __init__(self, provider, auth, user_path):
+ self._provider = provider
+ self._auth = auth
+ self._user_path = user_path
+
+ def cert_path(self):
+ if not self._is_cert_already_downloaded() or self._should_redownload():
+ self._download_smtp_cert()
+
+ return self._smtp_client_cert_path()
+
+ def _is_cert_already_downloaded(self):
+ return os.path.exists(self._smtp_client_cert_path())
+
+ def _should_redownload(self):
+ return leap_certs.should_redownload(self._smtp_client_cert_path())
+
+ def _download_smtp_cert(self):
+ cert_path = self._smtp_client_cert_path()
+
+ if not os.path.exists(os.path.dirname(cert_path)):
+ os.makedirs(os.path.dirname(cert_path))
+
+ SmtpCertDownloader(self._provider, self._auth).download_to(cert_path)
+
+ def _smtp_client_cert_path(self):
+ return os.path.join(
+ self._user_path,
+ "providers",
+ self._provider.domain,
+ "keys", "client", "smtp.pem")
+
+
+class SmtpCertDownloader(object):
+
+ def __init__(self, provider, auth):
+ self._provider = provider
+ self._auth = auth
+
+ def download(self):
+ cert_url = '%s/%s/smtp_cert' % (self._provider.api_uri,
+ self._provider.api_version)
+ cookies = {"_session_id": self._auth.session_id}
+ headers = {}
+ headers["Authorization"] = 'Token token="{0}"'.format(self._auth.token)
+ params = {'address': self._auth.username}
+ response = requests.post(
+ cert_url,
+ params=params,
+ data=params,
+ verify=LeapCertificate(self._provider).provider_api_cert,
+ cookies=cookies,
+ timeout=self._provider.config.timeout_in_s,
+ headers=headers)
+ response.raise_for_status()
+
+ client_cert = response.content
+
+ return client_cert
+
+ def download_to(self, target_file):
+ client_cert = self.download()
+
+ with open(target_file, 'w') as f:
+ f.write(client_cert)
+
+
+class LeapSessionFactory(object):
+
+ def __init__(self, provider):
+ self._provider = provider
+ self._config = provider.config
+
+ def create(self, username, password, auth=None):
+ key = SessionCache.session_key(self._provider, username)
+ session = SessionCache.lookup_session(key)
+ if not session:
+ session = self._create_new_session(username, password, auth)
+ SessionCache.remember_session(key, session)
+
+ return session
+
+ def _auth_leap(self, username, password):
+ srp_auth = SRPAuth(self._provider.api_uri, self._provider.local_ca_crt)
+ return srp_auth.authenticate(username, password)
+
+ def _create_new_session(self, username, password, auth=None):
+ self._create_dir(self._provider.config.leap_home)
+ self._provider.download_certificate()
+
+ auth = auth or self._auth_leap(username, password)
+ account_email = self._provider.address_for(username)
+
+ self._create_database_dir(auth.uuid)
+
+ soledad = SoledadFactory.create(auth.token,
+ auth.uuid,
+ password,
+ self._secrets_path(auth.uuid),
+ self._local_db_path(auth.uuid),
+ self._provider.discover_soledad_server(
+ auth.uuid),
+ LeapCertificate(self._provider).provider_api_cert)
+
+ mail_store = LeapMailStore(soledad)
+ nicknym = self._create_nicknym(
+ account_email, auth.token, auth.uuid, soledad)
+
+ smtp_client_cert = self._download_smtp_cert(auth)
+ smtp_host, smtp_port = self._provider.smtp_info()
+ smtp_config = LeapSMTPConfig(
+ account_email, smtp_client_cert, smtp_host, smtp_port)
+
+ return LeapSession(self._provider, auth, mail_store, soledad, nicknym, smtp_config)
+
+ def _download_smtp_cert(self, auth):
+ cert = SmtpClientCertificate(
+ self._provider, auth, self._user_path(auth.uuid))
+ return cert.cert_path()
+
+ def _create_dir(self, path):
+ try:
+ os.makedirs(path)
+ except OSError as exc:
+ if exc.errno == errno.EEXIST and os.path.isdir(path):
+ pass
+ else:
+ raise
+
+ def _create_nicknym(self, email_address, token, uuid, soledad):
+ return NickNym(self._provider, self._config, soledad, email_address, token, uuid)
+
+ def _user_path(self, user_uuid):
+ return os.path.join(self._config.leap_home, user_uuid)
+
+ def _soledad_path(self, user_uuid):
+ return os.path.join(self._config.leap_home, user_uuid, 'soledad')
+
+ def _secrets_path(self, user_uuid):
+ return os.path.join(self._soledad_path(user_uuid), 'secrets')
+
+ def _local_db_path(self, user_uuid):
+ return os.path.join(self._soledad_path(user_uuid), 'soledad.db')
+
+ def _create_database_dir(self, user_uuid):
+ try:
+ os.makedirs(self._soledad_path(user_uuid))
+ except OSError as exc:
+ if exc.errno == errno.EEXIST and os.path.isdir(self._soledad_path(user_uuid)):
+ pass
+ else:
+ raise
+
+
+class SessionCache(object):
+
+ sessions = {}
+
+ @staticmethod
+ def lookup_session(key):
+ session = SessionCache.sessions.get(key, None)
+ if session is not None and session.is_closed:
+ SessionCache.remove_session(key)
+ return None
+ else:
+ return session
+
+ @staticmethod
+ def remember_session(key, session):
+ SessionCache.sessions[key] = session
+
+ @staticmethod
+ def remove_session(key):
+ if key in SessionCache.sessions:
+ del SessionCache.sessions[key]
+
+ @staticmethod
+ def session_key(provider, username):
+ return hash((provider, username))
diff --git a/src/pixelated/bitmask_libraries/smtp.py b/src/pixelated/bitmask_libraries/smtp.py
new file mode 100644
index 00000000..643d4d4a
--- /dev/null
+++ b/src/pixelated/bitmask_libraries/smtp.py
@@ -0,0 +1,24 @@
+#
+# 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/>.
+
+
+class LeapSMTPConfig(object):
+
+ def __init__(self, account_email, cert_path, remote_smtp_host, remote_smtp_port):
+ self.account_email = account_email
+ self.cert_path = cert_path
+ self.remote_smtp_host = remote_smtp_host
+ self.remote_smtp_port = remote_smtp_port
diff --git a/src/pixelated/bitmask_libraries/soledad.py b/src/pixelated/bitmask_libraries/soledad.py
new file mode 100644
index 00000000..e6a9efce
--- /dev/null
+++ b/src/pixelated/bitmask_libraries/soledad.py
@@ -0,0 +1,48 @@
+#
+# 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/>.
+from leap.soledad.client import Soledad
+from leap.soledad.common.crypto import WrongMacError, UnknownMacMethodError
+
+
+class SoledadDiscoverException(Exception):
+
+ def __init__(self, *args, **kwargs):
+ super(SoledadDiscoverException, self).__init__(*args, **kwargs)
+
+
+class SoledadWrongPassphraseException(Exception):
+
+ def __init__(self, *args, **kwargs):
+ super(SoledadWrongPassphraseException, self).__init__(*args, **kwargs)
+
+
+class SoledadFactory(object):
+
+ @classmethod
+ def create(cls, user_token, user_uuid, encryption_passphrase, secrets, local_db, server_url, api_cert):
+ try:
+ return Soledad(user_uuid,
+ passphrase=unicode(encryption_passphrase),
+ secrets_path=secrets,
+ local_db_path=local_db,
+ server_url=server_url,
+ cert_file=api_cert,
+ shared_db=None,
+ auth_token=user_token,
+ defer_encryption=False)
+
+ except (WrongMacError, UnknownMacMethodError), e:
+ raise SoledadWrongPassphraseException(e)
diff --git a/src/pixelated/certificates/__init__.py b/src/pixelated/certificates/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/src/pixelated/certificates/__init__.py
diff --git a/src/pixelated/certificates/unstable.pixelated-project.org.ca.crt b/src/pixelated/certificates/unstable.pixelated-project.org.ca.crt
new file mode 100644
index 00000000..22ccb662
--- /dev/null
+++ b/src/pixelated/certificates/unstable.pixelated-project.org.ca.crt
@@ -0,0 +1,33 @@
+-----BEGIN CERTIFICATE-----
+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/src/pixelated/config/__init__.py b/src/pixelated/config/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/src/pixelated/config/__init__.py
diff --git a/src/pixelated/config/arguments.py b/src/pixelated/config/arguments.py
new file mode 100644
index 00000000..355a7c64
--- /dev/null
+++ b/src/pixelated/config/arguments.py
@@ -0,0 +1,98 @@
+#
+# 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 os
+import argparse
+
+
+def parse_user_agent_args():
+ parser = argparse.ArgumentParser(description='Pixelated user agent.')
+
+ parser_add_default_arguments(parser)
+
+ parser.add_argument('--host', default='127.0.0.1',
+ help='the host to run the user agent on')
+ parser.add_argument('--organization-mode', help='Runs the user agent in organization mode, the credentials will be received from the stdin',
+ default=False, action='store_true', dest='organization_mode')
+ parser.add_argument('--port', type=int, default=3333,
+ help='the port to run the user agent on')
+ parser.add_argument('-sk', '--sslkey', metavar='<server.key>', default=None,
+ help='use specified file as web server\'s SSL key (when using the user-agent together with the pixelated-dispatcher)')
+ parser.add_argument('-sc', '--sslcert', metavar='<server.crt>', default=None,
+ help='use specified file as web server\'s SSL certificate (when using the user-agent together with the pixelated-dispatcher)')
+ parser.add_argument('--multi-user', help='Run user agent in multi user mode',
+ action='store_false', default=True, dest='single_user')
+ parser.add_argument('-p', '--provider', help='specify a provider for mutli-user mode',
+ metavar='<provider host>', default=None, dest='provider')
+ parser.add_argument('--banner', help='banner file to show on login screen')
+
+ args = parser.parse_args()
+
+ return args
+
+
+def parse_maintenance_args():
+ parser = argparse.ArgumentParser(description='Pixelated maintenance')
+ parser_add_default_arguments(parser)
+ subparsers = parser.add_subparsers(help='commands', dest='command')
+ subparsers.add_parser('reset', help='reset account command')
+ mails_parser = subparsers.add_parser(
+ 'load-mails', help='load mails into account')
+ mails_parser.add_argument('file', nargs='+', help='file(s) with mail data')
+
+ markov_mails_parser = subparsers.add_parser(
+ 'markov-generate', help='generate mails using markov chains')
+ markov_mails_parser.add_argument(
+ '--seed', default=None, help='Specify a seed to always generate the same output')
+ markov_mails_parser.add_argument('-l', '--limit', metavar='count',
+ default='5', help='limit number of generated mails', dest='limit')
+ markov_mails_parser.add_argument(
+ 'file', nargs='+', help='file(s) with mail data')
+
+ subparsers.add_parser('dump-soledad', help='dump the soledad database')
+ subparsers.add_parser('sync', help='sync the soledad database')
+ subparsers.add_parser('repair', help='repair database if possible')
+ subparsers.add_parser(
+ 'integrity-check', help='run integrity check on database')
+
+ return parser.parse_args()
+
+
+def parse_register_args():
+ parser = argparse.ArgumentParser(description='Pixelated register')
+ parser.add_argument('provider', metavar='provider', action='store')
+ parser.add_argument('username', metavar='username', action='store')
+ parser.add_argument('-p', '--password', metavar='password', action='store',
+ default=None, help='used just to register account automatically by scripts')
+ parser.add_argument('-lc', '--leap-provider-cert', metavar='<leap-provider.crt>', default=None,
+ help='use specified file for LEAP provider cert authority certificate (url https://<LEAP-provider-domain>/ca.crt)')
+ parser.add_argument('-lf', '--leap-provider-cert-fingerprint', metavar='<leap provider certificate fingerprint>', default=None,
+ help='use specified fingerprint to validate connection with LEAP provider', dest='leap_provider_cert_fingerprint')
+ parser.add_argument('--leap-home', help='The folder where the user agent stores its data. Defaults to ~/.leap',
+ dest='leap_home', default=os.path.join(os.path.expanduser("~"), '.leap'))
+ return parser.parse_args()
+
+
+def parser_add_default_arguments(parser):
+ parser.add_argument('--debug', action='store_true', help='DEBUG mode.')
+ parser.add_argument('-c', '--config', dest='credentials_file', metavar='<credentials_file>',
+ default=None, help='use specified file for credentials (for test purposes only)')
+ parser.add_argument('--leap-home', help='The folder where the user agent stores its data. Defaults to ~/.leap',
+ dest='leap_home', default=os.path.join(os.path.expanduser("~"), '.leap'))
+ parser.add_argument('-lc', '--leap-provider-cert', metavar='<leap-provider.crt>', default=None,
+ help='use specified file for LEAP provider cert authority certificate (url https://<LEAP-provider-domain>/ca.crt)')
+ parser.add_argument('-lf', '--leap-provider-cert-fingerprint', metavar='<leap provider certificate fingerprint>', default=None,
+ help='use specified fingerprint to validate connection with LEAP provider', dest='leap_provider_cert_fingerprint')
diff --git a/src/pixelated/config/credentials.py b/src/pixelated/config/credentials.py
new file mode 100644
index 00000000..8bb837dd
--- /dev/null
+++ b/src/pixelated/config/credentials.py
@@ -0,0 +1,54 @@
+#
+# Copyright (c) 2015 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 getpass
+import json
+import sys
+import ConfigParser
+
+
+def read(organization_mode, credentials_file):
+ if organization_mode:
+ return read_from_dispatcher()
+ else:
+ if credentials_file:
+ return read_from_file(credentials_file)
+ return prompt_for_credentials()
+
+
+def prompt_for_credentials():
+ provider = raw_input('Which provider do you want to connect to:\n')
+ username = raw_input('What\'s your username registered on the provider:\n')
+ password = getpass.getpass('Type your password:\n')
+ return provider, username, password
+
+
+def read_from_file(credentials_file):
+ config_parser = ConfigParser.ConfigParser()
+ credentials_file_path = os.path.abspath(
+ os.path.expanduser(credentials_file))
+ config_parser.read(credentials_file_path)
+ provider, user, password = \
+ config_parser.get('pixelated', 'leap_server_name'), \
+ config_parser.get('pixelated', 'leap_username'), \
+ config_parser.get('pixelated', 'leap_password')
+ return provider, user, password
+
+
+def read_from_dispatcher():
+ config = json.loads(sys.stdin.read())
+ return config['leap_provider_hostname'], config['user'], config['password']
diff --git a/src/pixelated/config/leap.py b/src/pixelated/config/leap.py
new file mode 100644
index 00000000..dc555287
--- /dev/null
+++ b/src/pixelated/config/leap.py
@@ -0,0 +1,92 @@
+from __future__ import absolute_import
+from leap.common.events import (server as events_server)
+from leap.soledad.common.errors import InvalidAuthTokenError
+
+from pixelated.config import credentials
+from pixelated.bitmask_libraries.config import LeapConfig
+from pixelated.bitmask_libraries.certs import LeapCertificate
+from pixelated.bitmask_libraries.provider import LeapProvider
+from pixelated.bitmask_libraries.session import LeapSessionFactory
+from twisted.internet import defer
+
+import logging
+log = logging.getLogger(__name__)
+
+
+def initialize_leap_provider(provider_hostname, provider_cert, provider_fingerprint, leap_home):
+ LeapCertificate.set_cert_and_fingerprint(provider_cert,
+ provider_fingerprint)
+
+ config = LeapConfig(leap_home=leap_home, start_background_jobs=True)
+ provider = LeapProvider(provider_hostname, config)
+ provider.download_certificate()
+ LeapCertificate(provider).setup_ca_bundle()
+
+ return config, provider
+
+
+@defer.inlineCallbacks
+def initialize_leap_multi_user(provider_hostname,
+ leap_provider_cert,
+ leap_provider_cert_fingerprint,
+ credentials_file,
+ organization_mode,
+ leap_home):
+
+ config, provider = initialize_leap_provider(
+ provider_hostname, leap_provider_cert, leap_provider_cert_fingerprint, leap_home)
+
+ defer.returnValue((config, provider))
+
+
+def _create_session(provider, username, password, auth):
+ return LeapSessionFactory(provider).create(username, password, auth)
+
+
+def _force_close_session(session):
+ try:
+ session.close()
+ except Exception, e:
+ log.error(e)
+
+
+@defer.inlineCallbacks
+def authenticate_user(provider, username, password, initial_sync=True, auth=None):
+ leap_session = _create_session(provider, username, password, auth)
+ try:
+ if initial_sync:
+ yield leap_session.initial_sync()
+ except InvalidAuthTokenError:
+ _force_close_session(leap_session)
+
+ leap_session = _create_session(provider, username, password, auth)
+ if initial_sync:
+ yield leap_session.initial_sync()
+
+ defer.returnValue(leap_session)
+
+
+@defer.inlineCallbacks
+def initialize_leap_single_user(leap_provider_cert,
+ leap_provider_cert_fingerprint,
+ credentials_file,
+ organization_mode,
+ leap_home,
+ initial_sync=True):
+
+ init_monkeypatches()
+ events_server.ensure_server()
+
+ provider, username, password = credentials.read(organization_mode,
+ credentials_file)
+
+ config, provider = initialize_leap_provider(
+ provider, leap_provider_cert, leap_provider_cert_fingerprint, leap_home)
+
+ leap_session = yield authenticate_user(provider, username, password, initial_sync=initial_sync)
+
+ defer.returnValue(leap_session)
+
+
+def init_monkeypatches():
+ import pixelated.extensions.requests_urllib3
diff --git a/src/pixelated/config/logger.py b/src/pixelated/config/logger.py
new file mode 100644
index 00000000..a8000926
--- /dev/null
+++ b/src/pixelated/config/logger.py
@@ -0,0 +1,37 @@
+#
+# 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 logging
+import os
+from twisted.python import log
+
+
+def init(debug=False):
+ debug_enabled = debug or os.environ.get('DEBUG', False)
+ logging_level = logging.DEBUG if debug_enabled else logging.WARN
+ log_format = "%(asctime)s [%(name)s] %(levelname)s %(message)s"
+ date_format = '%Y-%m-%d %H:%M:%S'
+
+ logging.basicConfig(level=logging_level,
+ format=log_format,
+ datefmt=date_format,
+ filemode='a')
+
+ observer = log.PythonLoggingObserver()
+ # don't remove this line, it fix the PGP private key logged
+ logging.getLogger('gnupg').setLevel(logging.WARN)
+ logging.getLogger('pixelated').setLevel(logging.INFO)
+ observer.start()
diff --git a/src/pixelated/config/services.py b/src/pixelated/config/services.py
new file mode 100644
index 00000000..a38e7cd1
--- /dev/null
+++ b/src/pixelated/config/services.py
@@ -0,0 +1,84 @@
+import os
+import logging
+
+from pixelated.adapter.mailstore.leap_attachment_store import LeapAttachmentStore
+from pixelated.adapter.mailstore.searchable_mailstore import SearchableMailStore
+from pixelated.adapter.services.mail_service import MailService
+from pixelated.adapter.model.mail import InputMail
+from pixelated.adapter.services.mail_sender import MailSender
+from pixelated.adapter.search import SearchEngine
+from pixelated.adapter.services.draft_service import DraftService
+from pixelated.adapter.listeners.mailbox_indexer_listener import listen_all_mailboxes
+from twisted.internet import defer
+from pixelated.adapter.search.index_storage_key import SearchIndexStorageKey
+from pixelated.adapter.services.feedback_service import FeedbackService
+
+logger = logging.getLogger(__name__)
+
+
+class Services(object):
+
+ def __init__(self, leap_session):
+ self._leap_home = leap_session.config.leap_home
+ self._leap_session = leap_session
+
+ @defer.inlineCallbacks
+ def setup(self):
+ search_index_storage_key = self._setup_search_index_storage_key(
+ self._leap_session.soledad)
+ yield self._setup_search_engine(self._leap_session.user_auth.uuid, search_index_storage_key)
+
+ self._wrap_mail_store_with_indexing_mail_store(self._leap_session)
+
+ yield listen_all_mailboxes(self._leap_session.account, self.search_engine, self._leap_session.mail_store)
+
+ self.mail_service = self._setup_mail_service(self.search_engine)
+
+ self.keymanager = self._leap_session.nicknym
+ self.draft_service = self._setup_draft_service(
+ self._leap_session.mail_store)
+ self.feedback_service = self._setup_feedback_service()
+
+ yield self._index_all_mails()
+
+ def close(self):
+ self._leap_session.close()
+
+ def _wrap_mail_store_with_indexing_mail_store(self, leap_session):
+ leap_session.mail_store = SearchableMailStore(
+ leap_session.mail_store, self.search_engine)
+
+ @defer.inlineCallbacks
+ def _index_all_mails(self):
+ all_mails = yield self.mail_service.all_mails()
+ self.search_engine.index_mails(all_mails)
+
+ @defer.inlineCallbacks
+ def _setup_search_engine(self, namespace, search_index_storage_key):
+ key_unicode = yield search_index_storage_key.get_or_create_key()
+ key = str(key_unicode)
+ logger.debug('The key len is: %s' % len(key))
+ user_id = self._leap_session.user_auth.uuid
+ user_folder = os.path.join(self._leap_home, user_id)
+ search_engine = SearchEngine(key, user_home=user_folder)
+ self.search_engine = search_engine
+
+ def _setup_mail_service(self, search_engine):
+ pixelated_mail_sender = MailSender(
+ self._leap_session.smtp_config, self._leap_session.nicknym.keymanager)
+
+ return MailService(
+ pixelated_mail_sender,
+ self._leap_session.mail_store,
+ search_engine,
+ self._leap_session.account_email(),
+ LeapAttachmentStore(self._leap_session.soledad))
+
+ def _setup_draft_service(self, mail_store):
+ return DraftService(mail_store)
+
+ def _setup_search_index_storage_key(self, soledad):
+ return SearchIndexStorageKey(soledad)
+
+ def _setup_feedback_service(self):
+ return FeedbackService(self._leap_session)
diff --git a/src/pixelated/config/site.py b/src/pixelated/config/site.py
new file mode 100644
index 00000000..1fb884b0
--- /dev/null
+++ b/src/pixelated/config/site.py
@@ -0,0 +1,32 @@
+from twisted.web.server import Site, Request
+
+
+class AddSecurityHeadersRequest(Request):
+ CSP_HEADER_VALUES = "default-src 'self'; style-src 'self' 'unsafe-inline'"
+
+ def process(self):
+ self.setHeader('Content-Security-Policy', self.CSP_HEADER_VALUES)
+ self.setHeader('X-Content-Security-Policy', self.CSP_HEADER_VALUES)
+ self.setHeader('X-Webkit-CSP', self.CSP_HEADER_VALUES)
+ self.setHeader('X-Frame-Options', 'SAMEORIGIN')
+ self.setHeader('X-XSS-Protection', '1; mode=block')
+ self.setHeader('X-Content-Type-Options', 'nosniff')
+
+ if self.isSecure():
+ self.setHeader('Strict-Transport-Security',
+ 'max-age=31536000; includeSubDomains')
+
+ Request.process(self)
+
+
+class PixelatedSite(Site):
+
+ requestFactory = AddSecurityHeadersRequest
+
+ @classmethod
+ def enable_csp_requests(cls):
+ cls.requestFactory = AddSecurityHeadersRequest
+
+ @classmethod
+ def disable_csp_requests(cls):
+ cls.requestFactory = Site.requestFactory
diff --git a/src/pixelated/extensions/__init__.py b/src/pixelated/extensions/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/src/pixelated/extensions/__init__.py
diff --git a/src/pixelated/extensions/esmtp_sender_factory.py b/src/pixelated/extensions/esmtp_sender_factory.py
new file mode 100644
index 00000000..9288fc4d
--- /dev/null
+++ b/src/pixelated/extensions/esmtp_sender_factory.py
@@ -0,0 +1,28 @@
+#
+# 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/>.
+
+from twisted.mail import smtp
+
+
+def no_require_transport_security(f):
+ def wrapper(*args, **kwargs):
+ kwargs['requireTransportSecurity'] = False
+ return f(*args, **kwargs)
+ return wrapper
+
+
+smtp.ESMTPSenderFactory = no_require_transport_security(
+ smtp.ESMTPSenderFactory)
diff --git a/src/pixelated/extensions/protobuf_socket.py b/src/pixelated/extensions/protobuf_socket.py
new file mode 100644
index 00000000..548f5fd6
--- /dev/null
+++ b/src/pixelated/extensions/protobuf_socket.py
@@ -0,0 +1,37 @@
+#
+# 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/>.
+
+from __future__ import print_function
+from sys import platform as _platform
+
+import protobuf.socketrpc.server
+
+# protobuf throws a lot of 'Socket is not connected' exceptions on OSX but they are not an issue.
+# refer too https://code.google.com/p/protobuf-socket-rpc/issues/detail?id=10 and
+# or https://leap.se/code/issues/2187
+if _platform == 'darwin':
+ def try_except_decorator(func):
+ def wrapper(*args, **kwargs):
+ try:
+ func(*args, **kwargs)
+ pass
+ except:
+ pass
+
+ return wrapper
+
+ protobuf.socketrpc.server.SocketHandler.handle = try_except_decorator(
+ protobuf.socketrpc.server.SocketHandler.handle)
diff --git a/src/pixelated/extensions/requests_urllib3.py b/src/pixelated/extensions/requests_urllib3.py
new file mode 100644
index 00000000..5b0a6644
--- /dev/null
+++ b/src/pixelated/extensions/requests_urllib3.py
@@ -0,0 +1,84 @@
+#
+# 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 requests
+
+
+if requests.__version__ == '2.0.0':
+ try:
+ import requests.packages.urllib3.connectionpool
+ from socket import error as SocketError, timeout as SocketTimeout
+ from requests.packages.urllib3.packages.ssl_match_hostname import CertificateError, match_hostname
+ import socket
+ import ssl
+
+ from requests.packages.urllib3.exceptions import (
+ ClosedPoolError,
+ ConnectTimeoutError,
+ EmptyPoolError,
+ HostChangedError,
+ MaxRetryError,
+ SSLError,
+ ReadTimeoutError,
+ ProxyError,
+ )
+
+ from requests.packages.urllib3.util import (
+ assert_fingerprint,
+ get_host,
+ is_connection_dropped,
+ resolve_cert_reqs,
+ resolve_ssl_version,
+ ssl_wrap_socket,
+ Timeout,
+ )
+
+ def patched_connect(self):
+ # Add certificate verification
+ try:
+ sock = socket.create_connection(
+ address=(self.host, self.port), timeout=self.timeout)
+ except SocketTimeout:
+ raise ConnectTimeoutError(
+ self, "Connection to %s timed out. (connect timeout=%s)" % (self.host, self.timeout))
+
+ resolved_cert_reqs = resolve_cert_reqs(self.cert_reqs)
+ resolved_ssl_version = resolve_ssl_version(self.ssl_version)
+
+ if self._tunnel_host:
+ self.sock = sock
+ # Calls self._set_hostport(), so self.host is
+ # self._tunnel_host below.
+ self._tunnel()
+
+ # Wrap socket using verification with the root certs in
+ # trusted_root_certs
+ self.sock = ssl_wrap_socket(sock, self.key_file, self.cert_file,
+ cert_reqs=resolved_cert_reqs,
+ ca_certs=self.ca_certs,
+ server_hostname=self.host,
+ ssl_version=resolved_ssl_version)
+
+ if self.assert_fingerprint:
+ assert_fingerprint(self.sock.getpeercert(binary_form=True),
+ self.assert_fingerprint)
+ elif resolved_cert_reqs != ssl.CERT_NONE and self.assert_hostname is not False:
+ match_hostname(self.sock.getpeercert(),
+ self.assert_hostname or self.host)
+
+ requests.packages.urllib3.connectionpool.VerifiedHTTPSConnection.connect = patched_connect
+ except ImportError:
+ pass # The patch is specific for the debian package. Ignore it if it can't be found
diff --git a/src/pixelated/extensions/shared_db.py b/src/pixelated/extensions/shared_db.py
new file mode 100644
index 00000000..b433dd50
--- /dev/null
+++ b/src/pixelated/extensions/shared_db.py
@@ -0,0 +1,16 @@
+from leap.soledad.client.auth import TokenBasedAuth
+import base64
+from u1db import errors
+
+
+def patched_sign_request(self, method, url_query, params):
+ if 'token' in self._creds:
+ uuid, token = self._creds['token']
+ auth = '%s:%s' % (uuid, token)
+ return [('Authorization', 'Token %s' % base64.b64encode(auth))]
+ else:
+ raise errors.UnknownAuthMethod(
+ 'Wrong credentials: %s' % self._creds)
+
+
+# TokenBasedAuth._sign_request = patched_sign_request
diff --git a/src/pixelated/extensions/sqlcipher_wal.py b/src/pixelated/extensions/sqlcipher_wal.py
new file mode 100644
index 00000000..776087bf
--- /dev/null
+++ b/src/pixelated/extensions/sqlcipher_wal.py
@@ -0,0 +1,24 @@
+#
+# 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/>.
+
+from sys import platform as _platform
+
+import leap.soledad.client.sqlcipher
+
+# WAL is breaking for the debian sqlcipher package so we need to disable it
+# refer to https://leap.se/code/issues/5562
+if _platform == 'linux2':
+ leap.soledad.client.sqlcipher.SQLCipherDatabase._pragma_write_ahead_logging = lambda x, y: None
diff --git a/src/pixelated/maintenance.py b/src/pixelated/maintenance.py
new file mode 100644
index 00000000..da3d36c9
--- /dev/null
+++ b/src/pixelated/maintenance.py
@@ -0,0 +1,325 @@
+
+# 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/>.
+
+from os.path import isfile
+from mailbox import Maildir, mbox, MaildirMessage
+import random
+
+from leap.mail.adaptors.soledad import MetaMsgDocWrapper
+from twisted.internet import reactor, defer
+from twisted.internet.threads import deferToThread
+from pixelated.adapter.mailstore.maintenance import SoledadMaintenance
+from pixelated.config.leap import initialize_leap_single_user
+from pixelated.config import logger, arguments
+import logging
+
+from leap.mail.constants import MessageFlags
+from pixelated.support.mail_generator import MailGenerator
+
+REPAIR_COMMAND = 'repair'
+INTEGRITY_CHECK_COMMAND = 'integrity-check'
+
+
+log = logging.getLogger(__name__)
+
+
+def initialize():
+ args = arguments.parse_maintenance_args()
+
+ logger.init(debug=args.debug)
+
+ @defer.inlineCallbacks
+ def _run():
+ leap_session = yield initialize_leap_single_user(
+ args.leap_provider_cert,
+ args.leap_provider_cert_fingerprint,
+ args.credentials_file,
+ organization_mode=False,
+ leap_home=args.leap_home,
+ initial_sync=_do_initial_sync(args))
+
+ execute_command(args, leap_session)
+
+ reactor.callWhenRunning(_run)
+ reactor.run()
+
+
+def _do_initial_sync(args):
+ return (not _is_repair_command(args)) and (not _is_integrity_check_command(args))
+
+
+def _is_repair_command(args):
+ return args.command == REPAIR_COMMAND
+
+
+def _is_integrity_check_command(args):
+ return args.command == INTEGRITY_CHECK_COMMAND
+
+
+def execute_command(args, leap_session):
+
+ def init_soledad():
+ return leap_session
+
+ def get_soledad_handle(leap_session):
+ soledad = leap_session.soledad
+
+ return leap_session, soledad
+
+ @defer.inlineCallbacks
+ def soledad_sync(args):
+ leap_session, soledad = args
+
+ log.warn('Before sync')
+
+ yield soledad.sync()
+
+ log.warn('after sync')
+
+ defer.returnValue(args)
+
+ 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)
+
+
+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 == 'markov-generate':
+ prepareDeferred.addCallback(
+ markov_generate, args.file, int(args.limit), args.seed)
+ 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)
+ elif args.command == INTEGRITY_CHECK_COMMAND:
+ prepareDeferred.addCallback(integrity_check)
+ prepareDeferred.chainDeferred(finalizeDeferred)
+ elif args.command == REPAIR_COMMAND:
+ prepareDeferred.addCallback(repair)
+ prepareDeferred.chainDeferred(finalizeDeferred)
+ else:
+ print 'Unsupported command: %s' % args.command
+ prepareDeferred.chainDeferred(finalizeDeferred)
+
+ return finalizeDeferred
+
+
+@defer.inlineCallbacks
+def delete_all_mails(args):
+ leap_session, soledad = args
+ generation, docs = yield soledad.get_all_docs()
+
+ for doc in docs:
+ if doc.content.get('type', None) in ['head', 'cnt', 'flags']:
+ soledad.delete_doc(doc)
+
+ defer.returnValue(args)
+
+
+def is_keep_file(mail):
+ return mail['subject'] is None
+
+
+def _is_new_mail(mail):
+ return _is_maildir_msg(mail) and mail.get_subdir() == 'new'
+
+
+def _is_maildir_msg(mail):
+ return isinstance(mail, MaildirMessage)
+
+
+@defer.inlineCallbacks
+def _add_mail(store, folder_name, mail, flags, tags):
+ created_mail = yield store.add_mail(folder_name, mail.as_string())
+ leap_mail = yield store.get_mail(created_mail.mail_id)
+ leap_mail.tags |= set(tags)
+ for flag in flags:
+ leap_mail.flags.add(flag)
+
+ yield store.update_mail(leap_mail)
+
+
+@defer.inlineCallbacks
+def add_mail_folder(store, mailbox, folder_name, deferreds):
+ yield store.add_mailbox(folder_name)
+
+ for mail in mailbox:
+ if is_keep_file(mail):
+ continue
+
+ if _is_maildir_msg(mail):
+ flags = {MessageFlags.RECENT_FLAG} if _is_new_mail(mail) else set()
+
+ if 'S' in mail.get_flags():
+ flags = flags.add(MessageFlags.SEEN_FLAG)
+ if 'R' in mail.get_flags():
+ flags = flags.add(MessageFlags.ANSWERED_FLAG)
+ else:
+ flags = {MessageFlags.RECENT_FLAG}
+
+ tags = mail['X-Tags'].split() if mail['X-Tags'] else []
+
+ deferreds.append(_add_mail(store, folder_name, mail, flags, tags))
+
+
+@defer.inlineCallbacks
+def load_mails(args, mail_paths):
+ leap_session, soledad = args
+ store = leap_session.mail_store
+
+ yield _load_mails_as_is(mail_paths, store)
+
+ defer.returnValue(args)
+
+
+@defer.inlineCallbacks
+def _load_mails_as_is(mail_paths, store):
+ deferreds = []
+
+ for path in mail_paths:
+ if isfile(path):
+ mbox_mails = mbox(path, factory=None)
+ yield add_mail_folder(store, mbox_mails, 'INBOX', deferreds)
+ else:
+ maildir = Maildir(path, factory=None)
+ yield add_mail_folder(store, maildir, 'INBOX', deferreds)
+ for mail_folder_name in maildir.list_folders():
+ mail_folder = maildir.get_folder(mail_folder_name)
+ yield add_mail_folder(store, mail_folder, mail_folder_name, deferreds)
+
+ yield defer.gatherResults(deferreds, consumeErrors=True)
+
+
+@defer.inlineCallbacks
+def markov_generate(args, mail_paths, limit, seed):
+ leap_session, soledad = args
+ store = leap_session.mail_store
+
+ username = leap_session.user_auth.username
+ server_name = leap_session.provider.server_name
+
+ markov_mails = _generate_mails(
+ limit, mail_paths, seed, server_name, username)
+ deferreds = []
+ yield add_mail_folder(store, markov_mails, 'INBOX', deferreds)
+ yield defer.gatherResults(deferreds, consumeErrors=True)
+
+ defer.returnValue(args)
+
+
+def _generate_mails(limit, mail_paths, seed, server_name, username):
+ mails = []
+ for path in mail_paths:
+ mbox_mails = mbox(path, factory=None)
+ mails.extend(mbox_mails)
+ gen = MailGenerator(username, server_name, mails,
+ random=random.Random(seed))
+ markov_mails = [gen.generate_mail() for _ in range(limit)]
+ return markov_mails
+
+
+def flush_to_soledad(args, finalize):
+ leap_session, soledad = args
+
+ def after_sync(_):
+ finalize.callback((leap_session, soledad))
+
+ d = soledad.sync()
+ d.addCallback(after_sync)
+
+ return args
+
+
+@defer.inlineCallbacks
+def dump_soledad(args):
+ leap_session, soledad = args
+
+ generation, docs = yield soledad.get_all_docs()
+
+ for doc in docs:
+ print doc
+ print '\n'
+
+ defer.returnValue(args)
+
+
+@defer.inlineCallbacks
+def integrity_check(args):
+ leap_session, soledad = args
+
+ generation, docs = yield soledad.get_all_docs()
+
+ known_docs = {}
+
+ print 'Analysing %d docs\n' % len(docs)
+
+ # learn about all docs
+ for doc in docs:
+ known_docs[doc.doc_id] = doc
+
+ for doc in docs:
+ if doc.doc_id.startswith('M-'):
+ meta = MetaMsgDocWrapper(doc_id=doc.doc_id, **doc.content)
+
+ # validate header doc
+ if meta.hdoc not in known_docs:
+ print 'Error: Could not find header doc %s for meta %s' % (meta.hdoc, doc.doc_id)
+
+ if meta.fdoc not in known_docs:
+ print 'Error: Could not find flags doc %s for meta %s' % (meta.fdoc, doc.doc_id)
+
+ for cdoc in meta.cdocs:
+ if cdoc not in known_docs:
+ print 'Error: Could not find content doc %s for meta %s' % (cdoc, meta.doc_id)
+
+ defer.returnValue(args)
+
+
+@defer.inlineCallbacks
+def repair(args):
+ leap_session, soledad = args
+
+ yield SoledadMaintenance(soledad).repair()
+
+ defer.returnValue(args)
+
+
+def shutdown(args):
+ # time.sleep(30)
+ reactor.stop()
+
+
+def shutdown_on_error(error):
+ print error
+ shutdown(None)
+
+if __name__ == '__main__':
+ initialize()
diff --git a/src/pixelated/register.py b/src/pixelated/register.py
new file mode 100644
index 00000000..af748aca
--- /dev/null
+++ b/src/pixelated/register.py
@@ -0,0 +1,88 @@
+#
+# 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 re
+import getpass
+import logging
+import sys
+
+from pixelated.config import arguments
+from pixelated.config import logger as logger_config
+from pixelated.bitmask_libraries.certs import LeapCertificate
+from pixelated.bitmask_libraries.config import LeapConfig
+from pixelated.bitmask_libraries.provider import LeapProvider
+from pixelated.bitmask_libraries.session import LeapSessionFactory
+from leap.auth import SRPAuth
+from leap.common.events import server as events_server
+import pixelated.extensions.shared_db
+
+
+logger = logging.getLogger(__name__)
+
+
+def register(
+ server_name,
+ username,
+ password,
+ leap_home,
+ provider_cert,
+ provider_cert_fingerprint):
+
+ if not password:
+ password = getpass.getpass('Please enter password for %s: ' % username)
+
+ try:
+ validate_username(username)
+ validate_password(password)
+ except ValueError, e:
+ print(e.message)
+ sys.exit(1)
+
+ events_server.ensure_server()
+ LeapCertificate.set_cert_and_fingerprint(
+ provider_cert, provider_cert_fingerprint)
+ config = LeapConfig(leap_home=leap_home)
+ provider = LeapProvider(server_name, config)
+ LeapCertificate(provider).setup_ca_bundle()
+ srp_auth = SRPAuth(provider.api_uri, LeapCertificate(
+ provider).provider_api_cert)
+
+ if srp_auth.register(username, password):
+ LeapSessionFactory(provider).create(username, password)
+ else:
+ logger.error("Register failed")
+
+
+def validate_username(username):
+ accepted_characters = '^[a-z0-9\-\_\.]*$'
+ if (not re.match(accepted_characters, username)):
+ raise ValueError('Only lowercase letters, digits, . - and _ allowed.')
+
+
+def validate_password(password):
+ if len(password) < 8:
+ raise ValueError('The password must have at least 8 characters')
+
+
+def initialize():
+ logger_config.init(debug=False)
+ args = arguments.parse_register_args()
+ register(
+ args.provider,
+ args.username,
+ args.password,
+ args.leap_home,
+ args.leap_provider_cert,
+ args.leap_provider_cert_fingerprint)
diff --git a/src/pixelated/resources/__init__.py b/src/pixelated/resources/__init__.py
new file mode 100644
index 00000000..e3ba7105
--- /dev/null
+++ b/src/pixelated/resources/__init__.py
@@ -0,0 +1,118 @@
+#
+# 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 json
+import logging
+
+from twisted.web.http import UNAUTHORIZED
+from twisted.web.resource import Resource
+
+# from pixelated.resources.login_resource import LoginResource
+from pixelated.resources.session import IPixelatedSession
+
+from twisted.web.http import INTERNAL_SERVER_ERROR
+log = logging.getLogger(__name__)
+
+
+class SetEncoder(json.JSONEncoder):
+
+ def default(self, obj):
+ if isinstance(obj, set):
+ return list(obj)
+ return super(SetEncoder, self).default(obj)
+
+
+def respond_json(entity, request, status_code=200):
+ json_response = json.dumps(entity, cls=SetEncoder)
+ request.responseHeaders.addRawHeader(b"content-type", b"application/json")
+ request.code = status_code
+ return json_response
+
+
+def respond_json_deferred(entity, request, status_code=200):
+ json_response = json.dumps(entity, cls=SetEncoder)
+ request.responseHeaders.addRawHeader(b"content-type", b"application/json")
+ request.code = status_code
+ request.write(json_response)
+ request.finish()
+
+
+class GenericDeferredErrorHandler(object):
+
+ @classmethod
+ def generic_error_handling(cls, e, request):
+ log.error(e)
+ request.setResponseCode(INTERNAL_SERVER_ERROR)
+ request.write('Something went wrong!')
+ request.finish()
+
+
+class BaseResource(Resource, GenericDeferredErrorHandler):
+
+ def __init__(self, services_factory):
+ Resource.__init__(self)
+ self._services_factory = services_factory
+
+ def _get_user_id_from_request(self, request):
+ if self._services_factory.mode.is_single_user:
+ return None # it doesn't matter
+ session = self.get_session(request)
+ if session.is_logged_in():
+ return session.user_uuid
+ raise ValueError('Not logged in')
+
+ def is_logged_in(self, request):
+ session = self.get_session(request)
+ return session.is_logged_in() and self._services_factory.is_logged_in(session.user_uuid)
+
+ def get_session(self, request):
+ return IPixelatedSession(request.getSession())
+
+ def _services(self, request):
+ user_id = self._get_user_id_from_request(request)
+ return self._services_factory.services(user_id)
+
+ def _service(self, request, attribute):
+ return getattr(self._services(request), attribute)
+
+ def keymanager(self, request):
+ return self._service(request, 'keymanager')
+
+ def mail_service(self, request):
+ return self._service(request, 'mail_service')
+
+ def search_engine(self, request):
+ return self._service(request, 'search_engine')
+
+ def draft_service(self, request):
+ return self._service(request, 'draft_service')
+
+ def feedback_service(self, request):
+ return self._service(request, 'feedback_service')
+
+
+class UnAuthorizedResource(Resource):
+
+ def __init__(self):
+ Resource.__init__(self)
+
+ def render_GET(self, request):
+ request.setResponseCode(UNAUTHORIZED)
+ return "Unauthorized!"
+
+ def render_POST(self, request):
+ request.setResponseCode(UNAUTHORIZED)
+ return "Unauthorized!"
diff --git a/src/pixelated/resources/attachments_resource.py b/src/pixelated/resources/attachments_resource.py
new file mode 100644
index 00000000..146258f9
--- /dev/null
+++ b/src/pixelated/resources/attachments_resource.py
@@ -0,0 +1,113 @@
+#
+# 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 cgi
+import io
+import re
+import logging
+
+from twisted.internet import defer
+from twisted.protocols.basic import FileSender
+from twisted.python.log import msg
+from twisted.web import server
+from twisted.web.resource import Resource
+from twisted.web.server import NOT_DONE_YET
+
+from pixelated.resources import respond_json_deferred, BaseResource
+
+logger = logging.getLogger(__name__)
+
+
+class AttachmentResource(Resource):
+ isLeaf = True
+
+ def __init__(self, mail_service, attachment_id):
+ Resource.__init__(self)
+ self.attachment_id = attachment_id
+ self.mail_service = mail_service
+
+ def render_GET(self, request):
+ def error_handler(failure):
+ msg(failure, 'attachment not found')
+ request.code = 404
+ request.finish()
+
+ encoding = request.args.get('encoding', [None])[0]
+ filename = request.args.get('filename', [self.attachment_id])[0]
+ content_type = request.args.get(
+ 'content_type', ['application/octet-stream'])[0]
+ request.setHeader(b'Content-Type', content_type)
+ request.setHeader(b'Content-Disposition',
+ bytes('attachment; filename="' + filename + '"'))
+
+ d = self._send_attachment(encoding, filename, request)
+ d.addErrback(error_handler)
+
+ return server.NOT_DONE_YET
+
+ @defer.inlineCallbacks
+ def _send_attachment(self, encoding, filename, request):
+ attachment = yield self.mail_service.attachment(self.attachment_id)
+
+ bytes_io = io.BytesIO(attachment['content'])
+
+ try:
+ request.code = 200
+ yield FileSender().beginFileTransfer(bytes_io, request)
+ finally:
+ bytes_io.close()
+ request.finish()
+
+ def _extract_mimetype(self, content_type):
+ match = re.compile('([A-Za-z-]+\/[A-Za-z-]+)').search(content_type)
+ return match.group(1)
+
+
+class AttachmentsResource(BaseResource):
+ BASE_URL = 'attachment'
+
+ def __init__(self, services_factory):
+ BaseResource.__init__(self, services_factory)
+
+ def getChild(self, attachment_id, request):
+ _mail_service = self.mail_service(request)
+ return AttachmentResource(_mail_service, attachment_id)
+
+ def render_POST(self, request):
+ _mail_service = self.mail_service(request)
+ fields = cgi.FieldStorage(fp=request.content, headers=(request.getAllHeaders()),
+ environ={'REQUEST_METHOD': 'POST'})
+ _file = fields['attachment']
+ deferred = _mail_service.save_attachment(_file.value, _file.type)
+
+ def send_location(attachment_id):
+ request.headers[
+ 'Location'] = '/%s/%s' % (self.BASE_URL, attachment_id)
+ response_json = {"ident": attachment_id,
+ "content-type": _file.type,
+ "encoding": "base64", # hard coded for now -- not really used
+ "name": _file.filename,
+ "size": len(_file.value)}
+ respond_json_deferred(response_json, request, status_code=201)
+
+ def error_handler(error):
+ logger.error(error)
+ respond_json_deferred(
+ {"message": "Something went wrong. Attachment not saved."}, request, status_code=500)
+
+ deferred.addCallback(send_location)
+ deferred.addErrback(error_handler)
+
+ return NOT_DONE_YET
diff --git a/src/pixelated/resources/auth.py b/src/pixelated/resources/auth.py
new file mode 100644
index 00000000..3497f325
--- /dev/null
+++ b/src/pixelated/resources/auth.py
@@ -0,0 +1,150 @@
+#
+# Copyright (c) 2016 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 logging
+
+from leap.auth import SRPAuth
+from leap.exceptions import SRPAuthenticationError
+from twisted.cred.checkers import ANONYMOUS
+from twisted.cred.credentials import ICredentials
+from twisted.cred.error import UnauthorizedLogin
+from twisted.internet import defer, threads
+from twisted.web._auth.wrapper import UnauthorizedResource
+from twisted.web.error import UnsupportedMethod
+from zope.interface import implements, implementer, Attribute
+from twisted.cred import portal, checkers, credentials
+from twisted.web import util
+from twisted.cred import error
+from twisted.web.resource import IResource, ErrorPage
+
+from pixelated.config.leap import authenticate_user
+from pixelated.resources import IPixelatedSession
+
+
+log = logging.getLogger(__name__)
+
+
+@implementer(checkers.ICredentialsChecker)
+class LeapPasswordChecker(object):
+ credentialInterfaces = (
+ credentials.IUsernamePassword,
+ )
+
+ def __init__(self, leap_provider):
+ self._leap_provider = leap_provider
+
+ def requestAvatarId(self, credentials):
+ def _validate_credentials():
+ try:
+ srp_auth = SRPAuth(self._leap_provider.api_uri,
+ self._leap_provider.local_ca_crt)
+ return srp_auth.authenticate(credentials.username, credentials.password)
+ except SRPAuthenticationError:
+ raise UnauthorizedLogin()
+
+ def _get_leap_session(srp_auth):
+ return authenticate_user(self._leap_provider, credentials.username, credentials.password, auth=srp_auth)
+
+ d = threads.deferToThread(_validate_credentials)
+ d.addCallback(_get_leap_session)
+ return d
+
+
+class ISessionCredential(ICredentials):
+
+ request = Attribute('the current request')
+
+
+@implementer(ISessionCredential)
+class SessionCredential(object):
+
+ def __init__(self, request):
+ self.request = request
+
+
+@implementer(checkers.ICredentialsChecker)
+class SessionChecker(object):
+ credentialInterfaces = (ISessionCredential,)
+
+ def __init__(self, services_factory):
+ self._services_factory = services_factory
+
+ def requestAvatarId(self, credentials):
+ session = self.get_session(credentials.request)
+ if session.is_logged_in() and self._services_factory.is_logged_in(session.user_uuid):
+ return defer.succeed(session.user_uuid)
+ return defer.succeed(ANONYMOUS)
+
+ def get_session(self, request):
+ return IPixelatedSession(request.getSession())
+
+
+class PixelatedRealm(object):
+ implements(portal.IRealm)
+
+ def __init__(self, root_resource, anonymous_resource):
+ self._root_resource = root_resource
+ self._anonymous_resource = anonymous_resource
+
+ def requestAvatar(self, avatarId, mind, *interfaces):
+ if IResource in interfaces:
+ return IResource, avatarId, lambda: None
+ raise NotImplementedError()
+
+
+@implementer(IResource)
+class PixelatedAuthSessionWrapper(object):
+
+ isLeaf = False
+
+ def __init__(self, portal, root_resource, anonymous_resource, credentialFactories):
+ self._portal = portal
+ self._credentialFactories = credentialFactories
+ self._root_resource = root_resource
+ self._anonymous_resource = anonymous_resource
+
+ def render(self, request):
+ raise UnsupportedMethod(())
+
+ def getChildWithDefault(self, path, request):
+ request.postpath.insert(0, request.prepath.pop())
+ return self._authorizedResource(request)
+
+ def _authorizedResource(self, request):
+ creds = SessionCredential(request)
+ return util.DeferredResource(self._login(creds))
+
+ def _login(self, credentials):
+ d = self._portal.login(credentials, None, IResource)
+ d.addCallbacks(self._loginSucceeded, self._loginFailed)
+ return d
+
+ def _loginSucceeded(self, args):
+ interface, avatar, logout = args
+ if avatar == checkers.ANONYMOUS:
+ return self._anonymous_resource
+ else:
+ return self._root_resource
+
+ def _loginFailed(self, result):
+ if result.check(error.Unauthorized, error.LoginFailed):
+ return UnauthorizedResource(self._credentialFactories)
+ else:
+ log.err(
+ result,
+ "HTTPAuthSessionWrapper.getChildWithDefault encountered "
+ "unexpected error")
+ return ErrorPage(500, None, None)
diff --git a/src/pixelated/resources/contacts_resource.py b/src/pixelated/resources/contacts_resource.py
new file mode 100644
index 00000000..dc17d1ac
--- /dev/null
+++ b/src/pixelated/resources/contacts_resource.py
@@ -0,0 +1,44 @@
+#
+# 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/>.
+
+from pixelated.resources import respond_json_deferred, BaseResource
+from twisted.internet.threads import deferToThread
+from twisted.web import server
+from twisted.web.resource import Resource
+
+
+class ContactsResource(BaseResource):
+
+ isLeaf = True
+
+ def __init__(self, services_factory):
+ BaseResource.__init__(self, services_factory)
+
+ def render_GET(self, request):
+ _search_engine = self.search_engine(request)
+ query = request.args.get('q', [''])[-1]
+ d = deferToThread(lambda: _search_engine.contacts(query))
+ d.addCallback(lambda tags: respond_json_deferred(tags, request))
+
+ def handle_error(error):
+ print 'Something went wrong'
+ import traceback
+ traceback.print_exc()
+ print error
+
+ d.addErrback(handle_error)
+
+ return server.NOT_DONE_YET
diff --git a/src/pixelated/resources/features_resource.py b/src/pixelated/resources/features_resource.py
new file mode 100644
index 00000000..f2abdb9a
--- /dev/null
+++ b/src/pixelated/resources/features_resource.py
@@ -0,0 +1,52 @@
+#
+# 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/>.
+
+from pixelated.resources import respond_json
+import os
+from twisted.web.resource import Resource
+
+from pixelated.resources.logout_resource import LogoutResource
+
+
+class FeaturesResource(Resource):
+ DISABLED_FEATURES = ['draftReply']
+ isLeaf = True
+
+ def __init__(self, multi_user=False):
+ Resource.__init__(self)
+ self._multi_user = multi_user
+
+ def render_GET(self, request):
+ dispatcher_features = self._dispatcher_features()
+ disabled_features = self._disabled_features()
+ return respond_json(
+ {'disabled_features': disabled_features, 'dispatcher_features': dispatcher_features}, request)
+
+ def _dispatcher_features(self):
+ dispatcher_features = {}
+ if os.environ.get('DISPATCHER_LOGOUT_URL'):
+ dispatcher_features['logout'] = os.environ.get(
+ 'DISPATCHER_LOGOUT_URL')
+ if self._multi_user:
+ dispatcher_features['logout'] = '/%s' % LogoutResource.BASE_URL
+ return dispatcher_features
+
+ def _disabled_features(self):
+ disabled_features = [
+ default_disabled_feature for default_disabled_feature in self.DISABLED_FEATURES]
+ if os.environ.get('FEEDBACK_URL') is None:
+ disabled_features.append('feedback')
+ return disabled_features
diff --git a/src/pixelated/resources/feedback_resource.py b/src/pixelated/resources/feedback_resource.py
new file mode 100644
index 00000000..aeead401
--- /dev/null
+++ b/src/pixelated/resources/feedback_resource.py
@@ -0,0 +1,31 @@
+#
+# Copyright (c) 2015 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 json
+
+from pixelated.resources import respond_json, BaseResource
+
+
+class FeedbackResource(BaseResource):
+ isLeaf = True
+
+ def __init__(self, services_factory):
+ BaseResource.__init__(self, services_factory)
+
+ def render_POST(self, request):
+ _feedback_service = self.feedback_service(request)
+ feedback = json.loads(request.content.read()).get('feedback')
+ _feedback_service.open_ticket(feedback)
+ return respond_json({}, request)
diff --git a/src/pixelated/resources/keys_resource.py b/src/pixelated/resources/keys_resource.py
new file mode 100644
index 00000000..9075ab9e
--- /dev/null
+++ b/src/pixelated/resources/keys_resource.py
@@ -0,0 +1,30 @@
+from email.utils import parseaddr
+from pixelated.resources import respond_json_deferred, BaseResource
+from twisted.web import server
+
+
+class KeysResource(BaseResource):
+
+ isLeaf = True
+
+ def __init__(self, services_factory):
+ BaseResource.__init__(self, services_factory)
+
+ def render_GET(self, request):
+ _keymanager = self.keymanager(request)
+
+ def finish_request(key):
+ if key.private:
+ respond_json_deferred(None, request, status_code=401)
+ else:
+ respond_json_deferred(key.get_active_json(), request)
+
+ def key_not_found(_):
+ respond_json_deferred(None, request, status_code=404)
+
+ _, key_to_find = parseaddr(request.args.get('search')[0])
+ d = _keymanager.fetch_key(key_to_find)
+ d.addCallback(finish_request)
+ d.addErrback(key_not_found)
+
+ return server.NOT_DONE_YET
diff --git a/src/pixelated/resources/login_resource.py b/src/pixelated/resources/login_resource.py
new file mode 100644
index 00000000..fd09976b
--- /dev/null
+++ b/src/pixelated/resources/login_resource.py
@@ -0,0 +1,182 @@
+#
+# Copyright (c) 2016 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 logging
+import os
+from xml.sax import SAXParseException
+
+from twisted.cred import credentials
+from twisted.internet import defer
+from twisted.python.filepath import FilePath
+from twisted.web import util
+from twisted.web.http import UNAUTHORIZED, OK
+from twisted.web.resource import IResource, NoResource
+from twisted.web.server import NOT_DONE_YET
+from twisted.web.static import File
+from twisted.web.template import Element, XMLFile, renderElement, renderer
+
+from pixelated.adapter.welcome_mail import add_welcome_mail
+from pixelated.resources import BaseResource, UnAuthorizedResource, IPixelatedSession
+
+log = logging.getLogger(__name__)
+
+
+def _get_startup_folder():
+ path = os.path.dirname(os.path.abspath(__file__))
+ return os.path.join(path, '..', 'assets')
+
+
+def _get_static_folder():
+ static_folder = os.path.abspath(os.path.join(
+ os.path.abspath(__file__), "..", "..", "..", "web-ui", "app"))
+ # this is a workaround for packaging
+ if not os.path.exists(static_folder):
+ static_folder = os.path.abspath(
+ os.path.join(os.path.abspath(__file__), "..", "..", "..", "..", "web-ui", "app"))
+ if not os.path.exists(static_folder):
+ static_folder = os.path.join(
+ '/', 'usr', 'share', 'pixelated-user-agent')
+ return static_folder
+
+
+class DisclaimerElement(Element):
+ loader = XMLFile(FilePath(os.path.join(
+ _get_startup_folder(), '_login_disclaimer_banner.html')))
+
+ def __init__(self, banner):
+ super(DisclaimerElement, self).__init__()
+ self._set_loader(banner)
+ self._banner_filename = banner or "_login_disclaimer_banner.html"
+
+ def _set_loader(self, banner):
+ if banner:
+ current_path = os.path.dirname(os.path.abspath(__file__))
+ banner_file_path = os.path.join(
+ current_path, "..", "..", "..", banner)
+ self.loader = XMLFile(FilePath(banner_file_path))
+
+ def render(self, request):
+ try:
+ return super(DisclaimerElement, self).render(request)
+ except SAXParseException:
+ return ["Invalid XML template format for %s." % self._banner_filename]
+ except IOError:
+ return ["Disclaimer banner file %s could not be read or does not exit." % self._banner_filename]
+
+
+class LoginWebSite(Element):
+ loader = XMLFile(FilePath(os.path.join(
+ _get_startup_folder(), 'login.html')))
+
+ def __init__(self, error_msg=None, disclaimer_banner_file=None):
+ super(LoginWebSite, self).__init__()
+ self._error_msg = error_msg
+ self.disclaimer_banner_file = disclaimer_banner_file
+
+ @renderer
+ def error_msg(self, request, tag):
+ if self._error_msg is not None:
+ return tag(self._error_msg)
+ return tag('')
+
+ @renderer
+ def disclaimer(self, request, tag):
+ return DisclaimerElement(self.disclaimer_banner_file).render(request)
+
+
+class LoginResource(BaseResource):
+ BASE_URL = 'login'
+
+ def __init__(self, services_factory, portal=None, disclaimer_banner=None):
+ BaseResource.__init__(self, services_factory)
+ self._static_folder = _get_static_folder()
+ self._startup_folder = _get_startup_folder()
+ self._portal = portal
+ self._disclaimer_banner = disclaimer_banner
+ self.putChild('startup-assets', File(self._startup_folder))
+
+ def set_portal(self, portal):
+ self._portal = portal
+
+ def getChild(self, path, request):
+ if path == '':
+ return self
+ if path == 'login':
+ return self
+ if not self.is_logged_in(request):
+ return UnAuthorizedResource()
+ return NoResource()
+
+ def render_GET(self, request):
+ request.setResponseCode(OK)
+ return self._render_template(request)
+
+ def _render_template(self, request, error_msg=None):
+ site = LoginWebSite(error_msg=error_msg,
+ disclaimer_banner_file=self._disclaimer_banner)
+ return renderElement(request, site)
+
+ def render_POST(self, request):
+ if self.is_logged_in(request):
+ return util.redirectTo("/", request)
+
+ def render_response(leap_session):
+ request.setResponseCode(OK)
+ request.write(
+ open(os.path.join(self._startup_folder, 'Interstitial.html')).read())
+ request.finish()
+ self._setup_user_services(leap_session, request)
+
+ def render_error(error):
+ log.info('Login Error for %s' % request.args['username'][0])
+ log.info('%s' % error)
+ request.setResponseCode(UNAUTHORIZED)
+ return self._render_template(request, 'Invalid credentials')
+
+ d = self._handle_login(request)
+ d.addCallbacks(render_response, render_error)
+ d.addErrback(self.generic_error_handling, request)
+
+ return NOT_DONE_YET
+
+ @defer.inlineCallbacks
+ def _handle_login(self, request):
+ self.creds = self._get_creds_from(request)
+ iface, leap_session, logout = yield self._portal.login(self.creds, None, IResource)
+ defer.returnValue(leap_session)
+
+ def _get_creds_from(self, request):
+ username = request.args['username'][0]
+ password = request.args['password'][0]
+ return credentials.UsernamePassword(username, password)
+
+ @defer.inlineCallbacks
+ def _setup_user_services(self, leap_session, request):
+ user_id = leap_session.user_auth.uuid
+ if not self._services_factory.is_logged_in(user_id):
+ yield self._services_factory.create_services_from(leap_session)
+ self._init_http_session(request, user_id)
+
+ @defer.inlineCallbacks
+ def _initialize_services(self, leap_session):
+ yield self._services_factory.create_services_from(leap_session)
+
+ if leap_session.fresh_account:
+ yield add_welcome_mail(leap_session.mail_store)
+
+ def _init_http_session(self, request, user_id):
+ session = IPixelatedSession(request.getSession())
+ session.user_uuid = user_id
diff --git a/src/pixelated/resources/logout_resource.py b/src/pixelated/resources/logout_resource.py
new file mode 100644
index 00000000..c22815ce
--- /dev/null
+++ b/src/pixelated/resources/logout_resource.py
@@ -0,0 +1,45 @@
+#
+# 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/>.
+
+from twisted.internet import defer
+from twisted.web import util
+from twisted.web.server import NOT_DONE_YET
+
+from pixelated.resources import BaseResource
+from pixelated.resources.login_resource import LoginResource
+
+
+class LogoutResource(BaseResource):
+ BASE_URL = "logout"
+ isLeaf = True
+
+ @defer.inlineCallbacks
+ def _execute_logout(self, request):
+ session = self.get_session(request)
+ yield self._services_factory.log_out_user(session.user_uuid)
+ session.expire()
+
+ def render_POST(self, request):
+ def _redirect_to_login(_):
+ content = util.redirectTo("/%s" % LoginResource.BASE_URL, request)
+ request.write(content)
+ request.finish()
+
+ d = self._execute_logout(request)
+ d.addCallback(_redirect_to_login)
+ d.addErrback(self.generic_error_handling, request)
+
+ return NOT_DONE_YET
diff --git a/src/pixelated/resources/mail_resource.py b/src/pixelated/resources/mail_resource.py
new file mode 100644
index 00000000..15db37cb
--- /dev/null
+++ b/src/pixelated/resources/mail_resource.py
@@ -0,0 +1,79 @@
+import json
+
+from twisted.python.log import err
+from twisted.web.resource import Resource
+from twisted.web.server import NOT_DONE_YET
+
+from pixelated.resources import respond_json_deferred, BaseResource, GenericDeferredErrorHandler
+from pixelated.support import replier
+
+
+class MailTags(Resource):
+
+ isLeaf = True
+
+ def __init__(self, mail_id, mail_service):
+ Resource.__init__(self)
+ self._mail_service = mail_service
+ self._mail_id = mail_id
+
+ def render_POST(self, request):
+ new_tags = json.loads(request.content.read()).get('newtags')
+
+ d = self._mail_service.update_tags(self._mail_id, new_tags)
+ d.addCallback(lambda mail: respond_json_deferred(
+ mail.as_dict(), request))
+
+ def handle403(failure):
+ failure.trap(ValueError)
+ return respond_json_deferred(failure.getErrorMessage(), request, 403)
+ d.addErrback(handle403)
+ return NOT_DONE_YET
+
+
+class Mail(Resource, GenericDeferredErrorHandler):
+
+ def __init__(self, mail_id, mail_service):
+ Resource.__init__(self)
+ self.putChild('tags', MailTags(mail_id, mail_service))
+ self._mail_id = mail_id
+ self._mail_service = mail_service
+
+ def render_GET(self, request):
+ def populate_reply(mail):
+ mail_dict = mail.as_dict()
+ current_user = self._mail_service.account_email
+ sender = mail.headers.get('Reply-to', mail.headers.get('From'))
+ to = mail.headers.get('To', [])
+ ccs = mail.headers.get('Cc', [])
+ mail_dict['replying'] = replier.generate_recipients(
+ sender, to, ccs, current_user)
+ return mail_dict
+
+ d = self._mail_service.mail(self._mail_id)
+ d.addCallback(lambda mail: populate_reply(mail))
+ d.addCallback(
+ lambda mail_dict: respond_json_deferred(mail_dict, request))
+ d.addErrback(self.generic_error_handling, request)
+
+ return NOT_DONE_YET
+
+ def render_DELETE(self, request):
+ def response_failed(failure):
+ err(failure, 'something failed')
+ request.finish()
+
+ d = self._mail_service.delete_mail(self._mail_id)
+ d.addCallback(lambda _: respond_json_deferred(None, request))
+ d.addErrback(response_failed)
+ return NOT_DONE_YET
+
+
+class MailResource(BaseResource):
+
+ def __init__(self, services_factory):
+ BaseResource.__init__(self, services_factory)
+
+ def getChild(self, mail_id, request):
+ _mail_service = self.mail_service(request)
+ return Mail(mail_id, _mail_service)
diff --git a/src/pixelated/resources/mails_resource.py b/src/pixelated/resources/mails_resource.py
new file mode 100644
index 00000000..6ae08eb5
--- /dev/null
+++ b/src/pixelated/resources/mails_resource.py
@@ -0,0 +1,239 @@
+import time
+import json
+import logging
+from pixelated.adapter.services.mail_sender import SMTPDownException
+from pixelated.adapter.model.mail import InputMail
+from twisted.web.server import NOT_DONE_YET
+from pixelated.resources import respond_json_deferred, BaseResource
+from twisted.web.resource import Resource
+from twisted.web import server
+from twisted.internet import defer
+from twisted.python.log import err
+from leap.common import events
+
+from pixelated.support.functional import to_unicode
+
+
+log = logging.getLogger(__name__)
+
+
+class MailsUnreadResource(Resource):
+ isLeaf = True
+
+ def __init__(self, mail_service):
+ Resource.__init__(self)
+ self._mail_service = mail_service
+
+ def render_POST(self, request):
+ idents = json.load(request.content).get('idents')
+ deferreds = []
+ for ident in idents:
+ deferreds.append(self._mail_service.mark_as_unread(ident))
+
+ d = defer.gatherResults(deferreds, consumeErrors=True)
+ d.addCallback(lambda _: respond_json_deferred(None, request))
+ d.addErrback(lambda _: respond_json_deferred(
+ None, request, status_code=500))
+
+ return NOT_DONE_YET
+
+
+class MailsReadResource(Resource):
+ isLeaf = True
+
+ def __init__(self, mail_service):
+ Resource.__init__(self)
+ self._mail_service = mail_service
+
+ def render_POST(self, request):
+ idents = json.load(request.content).get('idents')
+ deferreds = []
+ for ident in idents:
+ deferreds.append(self._mail_service.mark_as_read(ident))
+
+ d = defer.gatherResults(deferreds, consumeErrors=True)
+ d.addCallback(lambda _: respond_json_deferred(None, request))
+ d.addErrback(lambda _: respond_json_deferred(
+ None, request, status_code=500))
+
+ return NOT_DONE_YET
+
+
+class MailsDeleteResource(Resource):
+ isLeaf = True
+
+ def __init__(self, mail_service):
+ Resource.__init__(self)
+ self._mail_service = mail_service
+
+ def render_POST(self, request):
+ def response_failed(failure):
+ err(failure, 'something failed')
+ request.finish()
+
+ idents = json.loads(request.content.read())['idents']
+ deferreds = []
+ for ident in idents:
+ deferreds.append(self._mail_service.delete_mail(ident))
+
+ d = defer.gatherResults(deferreds, consumeErrors=True)
+ d.addCallback(lambda _: respond_json_deferred(None, request))
+ d.addErrback(response_failed)
+ return NOT_DONE_YET
+
+
+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']
+ deferreds = []
+ for ident in idents:
+ deferreds.append(self._mail_service.recover_mail(ident))
+ d = defer.gatherResults(deferreds, consumeErrors=True)
+ d.addCallback(lambda _: respond_json_deferred(None, request))
+ d.addErrback(lambda _: respond_json_deferred(
+ None, request, status_code=500))
+ return NOT_DONE_YET
+
+
+class MailsArchiveResource(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']
+ deferreds = []
+ for ident in idents:
+ deferreds.append(self._mail_service.archive_mail(ident))
+ d = defer.gatherResults(deferreds, consumeErrors=True)
+ d.addCallback(lambda _: respond_json_deferred(
+ {'successMessage': 'Your message was archived'}, request))
+ d.addErrback(lambda _: respond_json_deferred(
+ None, request, status_code=500))
+ return NOT_DONE_YET
+
+
+class MailsResource(BaseResource):
+
+ def _register_smtp_error_handler(self):
+
+ def on_error(event, content):
+ delivery_error_mail = InputMail.delivery_error_template(
+ delivery_address=event.content)
+ self._mail_service.mailboxes.inbox.add(delivery_error_mail)
+
+ events.register(events.catalog.SMTP_SEND_MESSAGE_ERROR,
+ callback=on_error)
+
+ def __init__(self, services_factory):
+ BaseResource.__init__(self, services_factory)
+ self._register_smtp_error_handler()
+
+ def getChild(self, action, request):
+ _mail_service = self.mail_service(request)
+
+ if action == 'delete':
+ return MailsDeleteResource(_mail_service)
+ if action == 'recover':
+ return MailsRecoverResource(_mail_service)
+ if action == 'archive':
+ return MailsArchiveResource(_mail_service)
+ if action == 'read':
+ return MailsReadResource(_mail_service)
+ if action == 'unread':
+ return MailsUnreadResource(_mail_service)
+
+ def _build_mails_response(self, (mails, total)):
+ return {
+ "stats": {
+ "total": total,
+ },
+ "mails": [mail.as_dict() for mail in mails]
+ }
+
+ def render_GET(self, request):
+
+ _mail_service = self.mail_service(request)
+ query, window_size, page = request.args.get('q')[0], request.args.get('w')[
+ 0], request.args.get('p')[0]
+ unicode_query = to_unicode(query)
+ d = _mail_service.mails(unicode_query, window_size, page)
+
+ d.addCallback(self._build_mails_response)
+ d.addCallback(lambda res: respond_json_deferred(res, request))
+
+ def error_handler(error):
+ print error
+
+ d.addErrback(error_handler)
+
+ return NOT_DONE_YET
+
+ def render_POST(self, request):
+ def onError(error):
+ if isinstance(error.value, SMTPDownException):
+ respond_json_deferred(
+ {'message': str(error.value)}, request, status_code=503)
+ else:
+ err(error, 'error occurred while sending')
+ respond_json_deferred(
+ {'message': 'an error occurred while sending'}, request, status_code=422)
+
+ deferred = self._handle_post(request)
+ deferred.addErrback(onError)
+
+ return server.NOT_DONE_YET
+
+ def render_PUT(self, request):
+ def onError(error):
+ err(error, 'error saving draft')
+ respond_json_deferred("", request, status_code=422)
+
+ deferred = self._handle_put(request)
+ deferred.addErrback(onError)
+
+ return server.NOT_DONE_YET
+
+ @defer.inlineCallbacks
+ def _fetch_attachment_contents(self, content_dict, _mail_service):
+ attachments = content_dict.get(
+ 'attachments', []) if content_dict else []
+ for attachment in attachments:
+ retrieved_attachment = yield _mail_service.attachment(attachment['ident'])
+ attachment['raw'] = retrieved_attachment['content']
+ content_dict['attachments'] = attachments
+ defer.returnValue(content_dict)
+
+ @defer.inlineCallbacks
+ def _handle_post(self, request):
+ _mail_service = self.mail_service(request)
+ content_dict = json.loads(request.content.read())
+ with_attachment_content = yield self._fetch_attachment_contents(content_dict, _mail_service)
+
+ sent_mail = yield _mail_service.send_mail(with_attachment_content)
+ respond_json_deferred(sent_mail.as_dict(), request, status_code=201)
+
+ @defer.inlineCallbacks
+ def _handle_put(self, request):
+ _draft_service = self.draft_service(request)
+ _mail_service = self.mail_service(request)
+ content_dict = json.loads(request.content.read())
+ with_attachment_content = yield self._fetch_attachment_contents(content_dict, _mail_service)
+
+ _mail = InputMail.from_dict(
+ with_attachment_content, from_address=_mail_service.account_email)
+ draft_id = content_dict.get('ident')
+ pixelated_mail = yield _draft_service.process_draft(draft_id, _mail)
+
+ if not pixelated_mail:
+ respond_json_deferred("", request, status_code=422)
+ else:
+ respond_json_deferred({'ident': pixelated_mail.ident}, request)
diff --git a/src/pixelated/resources/root_resource.py b/src/pixelated/resources/root_resource.py
new file mode 100644
index 00000000..f3a41d57
--- /dev/null
+++ b/src/pixelated/resources/root_resource.py
@@ -0,0 +1,150 @@
+#
+# Copyright (c) 2016 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 hashlib
+import json
+import os
+from string import Template
+
+from pixelated.resources import BaseResource, UnAuthorizedResource
+from pixelated.resources.attachments_resource import AttachmentsResource
+from pixelated.resources.sandbox_resource import SandboxResource
+from pixelated.resources.contacts_resource import ContactsResource
+from pixelated.resources.features_resource import FeaturesResource
+from pixelated.resources.feedback_resource import FeedbackResource
+from pixelated.resources.login_resource import LoginResource
+from pixelated.resources.logout_resource import LogoutResource
+from pixelated.resources.user_settings_resource import UserSettingsResource
+from pixelated.resources.mail_resource import MailResource
+from pixelated.resources.mails_resource import MailsResource
+from pixelated.resources.tags_resource import TagsResource
+from pixelated.resources.keys_resource import KeysResource
+from twisted.web.static import File
+
+CSRF_TOKEN_LENGTH = 32
+
+MODE_STARTUP = 1
+MODE_RUNNING = 2
+
+
+class RootResource(BaseResource):
+
+ def __init__(self, services_factory, static_folder=None):
+ BaseResource.__init__(self, services_factory)
+ self._startup_assets_folder = self._get_startup_folder()
+ self._static_folder = static_folder or self._get_static_folder()
+ self._html_template = open(os.path.join(
+ self._static_folder, 'index.html')).read()
+ self._services_factory = services_factory
+ self._child_resources = ChildResourcesMap()
+ self._startup_mode()
+
+ def _startup_mode(self):
+ self.putChild('startup-assets', File(self._startup_assets_folder))
+ self._mode = MODE_STARTUP
+
+ def getChild(self, path, request):
+ if path == '':
+ return self
+ if self._is_xsrf_valid(request):
+ return self._child_resources.get(path)
+ return UnAuthorizedResource()
+
+ def _is_xsrf_valid(self, request):
+ xsrf_token = request.getCookie('XSRF-TOKEN')
+
+ ajax_request = (request.getHeader(
+ 'x-requested-with') == 'XMLHttpRequest')
+ if ajax_request:
+ xsrf_header = request.getHeader('x-xsrf-token')
+ return xsrf_header and xsrf_header == xsrf_token
+
+ get_request = (request.method == 'GET')
+ if get_request:
+ return True
+
+ csrf_input = request.args.get('csrftoken', [None])[0] or json.loads(
+ request.content.read()).get('csrftoken', [None])[0]
+ return csrf_input and csrf_input == xsrf_token
+
+ def initialize(self, portal=None, disclaimer_banner=None):
+ self._child_resources.add(
+ 'sandbox', SandboxResource(self._static_folder))
+ self._child_resources.add('assets', File(self._static_folder))
+ self._child_resources.add('keys', KeysResource(self._services_factory))
+ self._child_resources.add(
+ AttachmentsResource.BASE_URL, AttachmentsResource(self._services_factory))
+ self._child_resources.add(
+ 'contacts', ContactsResource(self._services_factory))
+ self._child_resources.add('features', FeaturesResource(portal))
+ self._child_resources.add('tags', TagsResource(self._services_factory))
+ self._child_resources.add(
+ 'mails', MailsResource(self._services_factory))
+ self._child_resources.add('mail', MailResource(self._services_factory))
+ self._child_resources.add(
+ 'feedback', FeedbackResource(self._services_factory))
+ self._child_resources.add(
+ 'user-settings', UserSettingsResource(self._services_factory))
+ self._child_resources.add(LoginResource.BASE_URL,
+ LoginResource(self._services_factory, portal, disclaimer_banner=disclaimer_banner))
+ self._child_resources.add(
+ LogoutResource.BASE_URL, LogoutResource(self._services_factory))
+
+ self._mode = MODE_RUNNING
+
+ def _get_startup_folder(self):
+ path = os.path.dirname(os.path.abspath(__file__))
+ return os.path.join(path, '..', 'assets')
+
+ def _get_static_folder(self):
+ static_folder = os.path.abspath(os.path.join(
+ os.path.abspath(__file__), "..", "..", "..", "web-ui", "app"))
+ # this is a workaround for packaging
+ if not os.path.exists(static_folder):
+ static_folder = os.path.abspath(
+ os.path.join(os.path.abspath(__file__), "..", "..", "..", "..", "web-ui", "app"))
+ if not os.path.exists(static_folder):
+ static_folder = os.path.join(
+ '/', 'usr', 'share', 'pixelated-user-agent')
+ return static_folder
+
+ def _is_starting(self):
+ return self._mode == MODE_STARTUP
+
+ def _add_csrf_cookie(self, request):
+ csrf_token = hashlib.sha256(os.urandom(CSRF_TOKEN_LENGTH)).hexdigest()
+ request.addCookie('XSRF-TOKEN', csrf_token)
+
+ def render_GET(self, request):
+ self._add_csrf_cookie(request)
+ if self._is_starting():
+ return open(os.path.join(self._startup_assets_folder, 'Interstitial.html')).read()
+ else:
+ account_email = self.mail_service(request).account_email
+ response = Template(self._html_template).safe_substitute(
+ account_email=account_email)
+ return str(response)
+
+
+class ChildResourcesMap(object):
+
+ def __init__(self):
+ self._registry = {}
+
+ def add(self, path, resource):
+ self._registry[path] = resource
+
+ def get(self, path):
+ return self._registry.get(path)
diff --git a/src/pixelated/resources/sandbox_resource.py b/src/pixelated/resources/sandbox_resource.py
new file mode 100644
index 00000000..35f99774
--- /dev/null
+++ b/src/pixelated/resources/sandbox_resource.py
@@ -0,0 +1,37 @@
+#
+# Copyright (c) 2016 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.web.static import File
+
+
+class SandboxResource(File):
+ CSP_HEADER_VALUES = "sandbox allow-popups allow-scripts;" \
+ "default-src 'self';" \
+ "style-src *;" \
+ "script-src *;" \
+ "font-src *;" \
+ "img-src *;" \
+ "object-src 'none';" \
+ "connect-src 'none';"
+
+ def render_GET(self, request):
+ request.setHeader('Content-Security-Policy', self.CSP_HEADER_VALUES)
+ request.setHeader('X-Content-Security-Policy', self.CSP_HEADER_VALUES)
+ request.setHeader('X-Webkit-CSP', self.CSP_HEADER_VALUES)
+ request.setHeader('Access-Control-Allow-Origin', '*')
+ request.setHeader('Access-Control-Allow-Methods', 'GET')
+
+ return super(SandboxResource, self).render_GET(request)
diff --git a/src/pixelated/resources/session.py b/src/pixelated/resources/session.py
new file mode 100644
index 00000000..9ade8d29
--- /dev/null
+++ b/src/pixelated/resources/session.py
@@ -0,0 +1,39 @@
+#
+# Copyright (c) 2016 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 zope.interface import Interface, Attribute, implements
+from twisted.python.components import registerAdapter
+from twisted.web.server import Session
+
+
+class IPixelatedSession(Interface):
+ user_uuid = Attribute('The uuid of the currently logged in user')
+
+
+class PixelatedSession(object):
+ implements(IPixelatedSession)
+
+ def __init__(self, session):
+ self.user_uuid = None
+
+ def is_logged_in(self):
+ return self.user_uuid is not None
+
+ def expire(self):
+ self.user_uuid = None
+
+
+registerAdapter(PixelatedSession, Session, IPixelatedSession)
diff --git a/src/pixelated/resources/tags_resource.py b/src/pixelated/resources/tags_resource.py
new file mode 100644
index 00000000..fa5982cb
--- /dev/null
+++ b/src/pixelated/resources/tags_resource.py
@@ -0,0 +1,39 @@
+#
+# 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/>.
+
+from pixelated.resources import respond_json_deferred, BaseResource
+from twisted.internet.threads import deferToThread
+from twisted.web.server import NOT_DONE_YET
+
+
+class TagsResource(BaseResource):
+
+ isLeaf = True
+
+ def __init__(self, services_factory):
+ BaseResource.__init__(self, services_factory)
+
+ def render_GET(self, request):
+ _search_engine = self.search_engine(request)
+ query = request.args.get('q', [''])[0]
+ skip_default_tags = request.args.get('skipDefaultTags', [False])[0]
+
+ d = deferToThread(lambda: _search_engine.tags(
+ query=query, skip_default_tags=skip_default_tags))
+ d.addCallback(lambda tags: respond_json_deferred(tags, request))
+ d.addErrback(self.generic_error_handling, request)
+
+ return NOT_DONE_YET
diff --git a/src/pixelated/resources/user_settings_resource.py b/src/pixelated/resources/user_settings_resource.py
new file mode 100644
index 00000000..7420a406
--- /dev/null
+++ b/src/pixelated/resources/user_settings_resource.py
@@ -0,0 +1,45 @@
+#
+# Copyright (c) 2015 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 pixelated.resources import respond_json_deferred, BaseResource
+from twisted.web import server
+
+FINGERPRINT_NOT_FOUND = 'Fingerprint not found'
+
+
+class UserSettingsResource(BaseResource):
+ isLeaf = True
+
+ def __init__(self, services_factory):
+ BaseResource.__init__(self, services_factory)
+
+ def render_GET(self, request):
+ _account_email = self.mail_service(request).account_email
+
+ def finish_request(key):
+ _fingerprint = key.fingerprint
+ respond_json_deferred(
+ {'account_email': _account_email, 'fingerprint': _fingerprint}, request)
+
+ def key_not_found(_):
+ respond_json_deferred(
+ {'account_email': _account_email, 'fingerprint': FINGERPRINT_NOT_FOUND}, request)
+
+ d = self.keymanager(request).fetch_key(_account_email)
+ d.addCallback(finish_request)
+ d.addErrback(key_not_found)
+
+ return server.NOT_DONE_YET
diff --git a/src/pixelated/support/__init__.py b/src/pixelated/support/__init__.py
new file mode 100644
index 00000000..56283918
--- /dev/null
+++ b/src/pixelated/support/__init__.py
@@ -0,0 +1,81 @@
+#
+# 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 time
+import logging
+from functools import wraps
+from twisted.internet import defer
+
+
+log = logging.getLogger(__name__)
+
+
+def _start_stopwatch():
+ return (time.time(), time.clock())
+
+
+def _stop_stopwatch(start):
+ start_time, start_clock = start
+ end_clock = time.clock()
+ end_time = time.time()
+ clock_duration = end_clock - start_clock
+ time_duration = end_time - start_time
+ if time_duration < 0.00000001: # avoid division by zero
+ time_duration = 0.00000001
+
+ estimate_percent_io = (
+ (time_duration - clock_duration) / time_duration) * 100.0
+
+ return time_duration, clock_duration, estimate_percent_io
+
+
+def log_time(f):
+
+ @wraps(f)
+ def wrapper(*args, **kwds):
+ start = _start_stopwatch()
+
+ result = f(*args, **kwds)
+
+ time_duration, clock_duration, estimate_percent_io = _stop_stopwatch(
+ start)
+ log.info('Needed %fs (%fs cpu time, %.2f%% spent outside process) to execute %s' % (
+ time_duration, clock_duration, estimate_percent_io, f))
+
+ return result
+
+ return wrapper
+
+
+def log_time_deferred(f):
+
+ def log_time(result, start):
+ time_duration, clock_duration, estimate_percent_io = _stop_stopwatch(
+ start)
+ log.info('after callback: Needed %fs (%fs cpu time, %.2f%% spent outside process) to execute %s' % (
+ time_duration, clock_duration, estimate_percent_io, f))
+ return result
+
+ @wraps(f)
+ def wrapper(*args, **kwds):
+ start = _start_stopwatch()
+ result = f(*args, **kwds)
+ if isinstance(result, defer.Deferred):
+ result.addCallback(log_time, start=start)
+ else:
+ log.warn('No Deferred returned, perhaps need to re-order annotations?')
+ return result
+
+ return wrapper
diff --git a/src/pixelated/support/date.py b/src/pixelated/support/date.py
new file mode 100644
index 00000000..0012aeea
--- /dev/null
+++ b/src/pixelated/support/date.py
@@ -0,0 +1,29 @@
+#
+# 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 datetime
+
+import dateutil.parser
+from email.utils import formatdate
+from dateutil.tz import tzlocal
+
+
+def iso_now():
+ return datetime.datetime.now(tzlocal()).isoformat()
+
+
+def mail_date_now():
+ date = dateutil.parser.parse(iso_now())
+ return formatdate(float(date.strftime('%s')))
diff --git a/src/pixelated/support/encrypted_file_storage.py b/src/pixelated/support/encrypted_file_storage.py
new file mode 100644
index 00000000..d4e06e0b
--- /dev/null
+++ b/src/pixelated/support/encrypted_file_storage.py
@@ -0,0 +1,153 @@
+#
+# 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/>.
+
+from __future__ import with_statement
+
+import hmac
+import io
+import os
+from hashlib import sha256
+
+from leap.soledad.client.crypto import decrypt_sym
+from leap.soledad.client.crypto import encrypt_sym
+from whoosh.filedb.filestore import FileStorage
+from whoosh.filedb.structfile import BufferFile, StructFile
+from whoosh.util import random_name
+
+
+class DelayedCloseBytesIO(io.BytesIO):
+
+ def __init__(self, name):
+ super(DelayedCloseBytesIO, self).__init__()
+ self._name = name
+ self.shouldClose = False
+
+ def close(self):
+
+ self.shouldClose = True
+
+ def explicit_close(self):
+ super(DelayedCloseBytesIO, self).close()
+
+
+class DelayedCloseStructFile(StructFile):
+
+ def __init__(self, fileobj, name=None, onclose=None):
+ super(DelayedCloseStructFile, self).__init__(fileobj, name, onclose)
+
+ def close(self):
+ """Closes the wrapped file.
+ """
+
+ if self.is_closed:
+ raise Exception("This file is already closed")
+ if self.onclose:
+ self.onclose(self)
+ if hasattr(self.file, "explicit_close"):
+ self.file.explicit_close()
+ self.is_closed = True
+
+
+class EncryptedFileStorage(FileStorage):
+
+ def __init__(self, path, masterkey=None):
+ FileStorage.__init__(self, path, supports_mmap=False)
+ self.masterkey = masterkey[:32]
+ self.signkey = masterkey[32:]
+ self._tmp_storage = self.temp_storage
+ self.length_cache = {}
+ self._open_files = {}
+
+ def open_file(self, name, **kwargs):
+ return self._open_encrypted_file(name)
+
+ def create_file(self, name, excl=False, mode="w+b", **kwargs):
+ f = DelayedCloseStructFile(DelayedCloseBytesIO(
+ name), name=name, onclose=self._encrypt_index_on_close(name))
+ f.is_real = False
+ self._open_files[name] = f
+ return f
+
+ def delete_file(self, name):
+ super(EncryptedFileStorage, self).delete_file(name)
+ if name in self._open_files:
+ del self._open_files[name]
+
+ def temp_storage(self, name=None):
+ name = name or "%s.tmp" % random_name()
+ path = os.path.join(self.folder, name)
+ return EncryptedFileStorage(path, self.masterkey).create()
+
+ def file_length(self, name):
+ return self.length_cache[name][0]
+
+ def gen_mac(self, iv, ciphertext):
+ verifiable_payload = ''.join((iv, ciphertext))
+ return hmac.new(self.signkey, verifiable_payload, sha256).digest()
+
+ def encrypt(self, content):
+ iv, ciphertext = encrypt_sym(content, self.masterkey)
+ mac = self.gen_mac(iv, ciphertext)
+ return ''.join((mac, iv, ciphertext))
+
+ def decrypt(self, payload):
+ payload_mac, iv, ciphertext = payload[
+ :32], payload[32:57], payload[57:]
+ generated_mac = self.gen_mac(iv, ciphertext)
+ if sha256(payload_mac).digest() != sha256(generated_mac).digest():
+ raise Exception(
+ "EncryptedFileStorage - Error opening file. Wrong MAC")
+ return decrypt_sym(ciphertext, self.masterkey, iv)
+
+ def _encrypt_index_on_close(self, name):
+ def wrapper(struct_file):
+ struct_file.seek(0)
+ content = struct_file.file.read()
+ file_hash = sha256(content).digest()
+ if name in self.length_cache and file_hash == self.length_cache[name][1]:
+ return
+ self.length_cache[name] = (len(content), file_hash)
+ encrypted_content = self.encrypt(content)
+ with open(self._fpath(name), 'w+b') as f:
+ f.write(encrypted_content)
+
+ return wrapper
+
+ def _open_encrypted_file(self, name, onclose=lambda x: None):
+ if not self.file_exists(name):
+ if name in self._open_files:
+ f = self._open_files[name]
+ if not f.is_closed:
+ state = 'closed' if f.file.shouldClose else 'open'
+ if state == 'closed':
+ self._store_file(name, f.file.getvalue())
+ f.close()
+ del self._open_files[name]
+ else:
+ raise NameError(name)
+ file_content = open(self._fpath(name), "rb").read()
+ decrypted = self.decrypt(file_content)
+ self.length_cache[name] = (len(decrypted), sha256(decrypted).digest())
+ return BufferFile(buffer(decrypted), name=name, onclose=onclose)
+
+ def _store_file(self, name, content):
+ try:
+ encrypted_content = self.encrypt(content)
+ with open(self._fpath(name), 'w+b') as f:
+ f.write(encrypted_content)
+ except Exception, e:
+ print e
+ raise
diff --git a/src/pixelated/support/error_handler.py b/src/pixelated/support/error_handler.py
new file mode 100644
index 00000000..1a0e1a11
--- /dev/null
+++ b/src/pixelated/support/error_handler.py
@@ -0,0 +1,27 @@
+#
+# 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/>.
+from requests.exceptions import SSLError
+
+
+def error_handler(excp):
+ if excp.type is SSLError:
+ print """
+ SSL Error: Please check your certificates or read our wiki for further info:
+ https://github.com/pixelated-project/pixelated-user-agent/wiki/Configuring-and-using-SSL-Certificates-for-LEAP-provider
+ Error reference: %s
+ """ % excp.getErrorMessage()
+ else:
+ raise excp
diff --git a/src/pixelated/support/functional.py b/src/pixelated/support/functional.py
new file mode 100644
index 00000000..2e293625
--- /dev/null
+++ b/src/pixelated/support/functional.py
@@ -0,0 +1,37 @@
+#
+# 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/>.
+from itertools import chain
+
+
+def flatten(_list):
+ return list(chain.from_iterable(_list))
+
+
+def unique(_list):
+ seen = set()
+ seen_add = seen.add
+ return [x for x in _list if not (x in seen or seen_add(x))]
+
+
+def compact(_list):
+ return [a for a in _list if a]
+
+
+def to_unicode(text):
+ if text and not isinstance(text, unicode):
+ encoding = 'utf-8'
+ return unicode(text, encoding=encoding)
+ return text
diff --git a/src/pixelated/support/mail_generator.py b/src/pixelated/support/mail_generator.py
new file mode 100644
index 00000000..e01274f3
--- /dev/null
+++ b/src/pixelated/support/mail_generator.py
@@ -0,0 +1,157 @@
+#
+# Copyright (c) 2015 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 email.mime.text import MIMEText
+from email.utils import formatdate
+from random import Random
+from pixelated.support.markov import MarkovGenerator
+import re
+from collections import Counter
+import time
+
+
+def filter_two_line_on_wrote(lines):
+ skip_next = False
+ if len(lines) > 0:
+ for i in xrange(len(lines) - 1):
+ if skip_next:
+ skip_next = False
+ continue
+
+ if lines[i].startswith('On') and lines[i + 1].endswith('wrote:'):
+ skip_next = True
+ else:
+ yield lines[i].strip()
+
+ yield lines[-1]
+
+
+def filter_lines(text):
+ pattern = re.compile('\s*[>-].*')
+ wrote_pattern = re.compile('\s*On.*wrote.*')
+
+ lines = text.splitlines()
+
+ lines = filter(lambda line: not pattern.match(line), lines)
+ lines = filter(lambda line: not len(line.strip()) == 0, lines)
+ lines = filter(lambda line: not wrote_pattern.match(line), lines)
+ lines = filter(lambda line: not line.endswith('writes:'), lines)
+ lines = filter(lambda line: ' ' in line.strip(), lines)
+
+ lines = filter_two_line_on_wrote(lines)
+
+ return ' '.join(lines)
+
+
+def decode_multipart_mail_text(mail):
+ for payload in mail.get_payload():
+ if payload.get_content_type() == 'text/plain':
+ return payload.get_payload(decode=True)
+ return ''
+
+
+def search_for_tags(content):
+ words = content.split()
+
+ only_alnum = filter(lambda word: word.isalnum(), words)
+ only_longer = filter(lambda word: len(word) > 5, only_alnum)
+ lower_case = map(lambda word: word.lower(), only_longer)
+
+ counter = Counter(lower_case)
+ potential_tags = counter.most_common(10)
+
+ return map(lambda tag: tag[0], potential_tags)
+
+
+def filter_too_short_texts(texts):
+ return [text for text in texts if text is not None and len(text.split()) >= 3]
+
+
+def load_all_mails(mail_list):
+ subjects = set()
+ mail_bodies = []
+
+ for mail in mail_list:
+ subjects.add(mail['Subject'])
+ if mail.is_multipart():
+ mail_bodies.append(filter_lines(decode_multipart_mail_text(mail)))
+ else:
+ if mail.get_content_type() == 'text/plain':
+ mail_bodies.append(filter_lines(mail.get_payload(decode=True)))
+ else:
+ raise Exception(mail.get_content_type())
+
+ return filter_too_short_texts(subjects), filter_too_short_texts(mail_bodies)
+
+
+class MailGenerator(object):
+
+ NAMES = ['alice', 'bob', 'eve']
+
+ def __init__(self, receiver, domain_name, sample_mail_list, random=None):
+ self._random = random if random else Random()
+ self._receiver = receiver
+ self._domain_name = domain_name
+ self._subjects, self._bodies = load_all_mails(sample_mail_list)
+
+ self._potential_tags = search_for_tags(' '.join(self._bodies))
+ self._subject_markov = MarkovGenerator(
+ self._subjects, random=self._random)
+ self._body_markov = MarkovGenerator(
+ self._bodies, random=self._random, add_paragraph_on_empty_chain=True)
+
+ def generate_mail(self):
+ body = self._body_markov.generate(150)
+ mail = MIMEText(body)
+
+ mail['Subject'] = self._subject_markov.generate(8)
+ mail['To'] = '%s@%s' % (self._receiver, self._domain_name)
+ mail['From'] = self._random_from()
+ mail['Date'] = self._random_date()
+ mail['X-Tags'] = self._random_tags()
+ mail['X-Leap-Encryption'] = self._random_encryption_state()
+ mail['X-Leap-Signature'] = self._random_signature_state()
+
+ return mail
+
+ def _random_date(self):
+ now = int(time.time())
+ ten_days = 60 * 60 * 24 * 10
+ mail_time = self._random.randint(now - ten_days, now)
+
+ return formatdate(mail_time)
+
+ def _random_encryption_state(self):
+ return self._random.choice(['true', 'decrypted'])
+
+ def _random_signature_state(self):
+ return self._random.choice(['could not verify', 'valid'])
+
+ def _random_from(self):
+ name = self._random.choice(
+ filter(lambda name: name != self._receiver, MailGenerator.NAMES))
+
+ return '%s@%s' % (name, self._domain_name)
+
+ def _random_tags(self):
+ barrier = 0.5
+ tags = set()
+ while self._random.random() > barrier:
+ tags.add(self._random.choice(self._potential_tags))
+ barrier += 0.15
+
+ return ' '.join(tags)
diff --git a/src/pixelated/support/markov.py b/src/pixelated/support/markov.py
new file mode 100644
index 00000000..8f7c0ef3
--- /dev/null
+++ b/src/pixelated/support/markov.py
@@ -0,0 +1,94 @@
+#
+# Copyright (c) 2015 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 random import Random
+
+NEW_PARAGRAPH = '\n\n'
+
+
+class MarkovGenerator(object):
+
+ def __init__(self, texts, random=None, add_paragraph_on_empty_chain=False):
+ self._markov_chain = {}
+ self._random = random if random else Random()
+ self._add_paragraph_on_empty_chain = add_paragraph_on_empty_chain
+
+ for text in filter(lambda _: _ is not None, texts):
+ self._extend_chain_with(text)
+
+ def add(self, text):
+ self._extend_chain_with(text)
+
+ @staticmethod
+ def _triplet_generator(words):
+ if len(words) < 3:
+ raise ValueError('Expected input with at least three words')
+
+ for i in xrange(len(words) - 2):
+ yield ((words[i], words[i + 1]), words[i + 2])
+
+ def _extend_chain_with(self, input_text):
+ words = input_text.split()
+ gen = self._triplet_generator(words)
+
+ for key, value in gen:
+ if key in self._markov_chain:
+ self._markov_chain[key].add(value)
+ else:
+ self._markov_chain[key] = {value}
+
+ def _generate_chain(self, length):
+ seed_pair = self._find_good_seed()
+ word, next_word = seed_pair
+ new_seed = False
+
+ for i in xrange(length):
+ yield word
+
+ if new_seed:
+ word, next_word = self._find_good_seed()
+ if self._add_paragraph_on_empty_chain:
+ yield NEW_PARAGRAPH
+ new_seed = False
+ else:
+ prev_word, word = word, next_word
+
+ try:
+ next_word = self._random_next_word(prev_word, word)
+ except KeyError:
+ new_seed = True
+
+ def _random_next_word(self, prev_word, word):
+ return self._random.choice(list(self._markov_chain[(prev_word, word)]))
+
+ def _find_good_seed(self):
+ max_tries = len(self._markov_chain.keys())
+ try_count = 0
+
+ seed_pair = self._random.choice(self._markov_chain.keys())
+ while not seed_pair[0][0].isupper() and try_count <= max_tries:
+ seed_pair = self._random.choice(self._markov_chain.keys())
+ try_count += 1
+
+ if try_count > max_tries:
+ raise ValueError('Not able find start word with captial letter')
+
+ return seed_pair
+
+ def generate(self, length):
+ if len(self._markov_chain.keys()) == 0:
+ raise ValueError('Expected at least three words input')
+ return ' '.join(self._generate_chain(length))
diff --git a/src/pixelated/support/replier.py b/src/pixelated/support/replier.py
new file mode 100644
index 00000000..83324eae
--- /dev/null
+++ b/src/pixelated/support/replier.py
@@ -0,0 +1,35 @@
+from email.utils import parseaddr
+
+
+def generate_recipients(sender, to, ccs, current_user):
+ result = {'single': None, 'all': {'to-field': [], 'cc-field': []}}
+
+ to.append(sender)
+ to = remove_duplicates(to)
+ ccs = remove_duplicates(ccs)
+
+ result['single'] = swap_recipient_if_needed(
+ sender, remove_address(to, current_user), current_user)
+ result['all'][
+ 'to-field'] = remove_address(to, current_user) if len(to) > 1 else to
+ result['all'][
+ 'cc-field'] = remove_address(ccs, current_user) if len(ccs) > 1 else ccs
+ return result
+
+
+def remove_duplicates(recipients):
+ return list(set(recipients))
+
+
+def remove_address(recipients, current_user):
+ return [recipient for recipient in recipients if not parsed_mail_matches(recipient, current_user)]
+
+
+def parsed_mail_matches(to_parse, expected):
+ return parseaddr(to_parse)[1] == expected
+
+
+def swap_recipient_if_needed(sender, recipients, current_user):
+ if len(recipients) == 1 and parsed_mail_matches(sender, current_user):
+ return recipients[0]
+ return sender
diff --git a/src/pixelated/support/tls_adapter.py b/src/pixelated/support/tls_adapter.py
new file mode 100644
index 00000000..301a2123
--- /dev/null
+++ b/src/pixelated/support/tls_adapter.py
@@ -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/>.
+
+import ssl
+from requests.adapters import HTTPAdapter
+try:
+ from urllib3.poolmanager import PoolManager
+except:
+ from requests.packages.urllib3.poolmanager import PoolManager
+
+VERIFY_HOSTNAME = None
+
+
+def latest_available_ssl_version():
+ try:
+ return ssl.PROTOCOL_TLSv1_2
+ except AttributeError:
+ return ssl.PROTOCOL_TLSv1
+
+
+class EnforceTLSv1Adapter(HTTPAdapter):
+ __slots__ = ('_assert_hostname', '_assert_fingerprint')
+
+ def __init__(self, assert_hostname=VERIFY_HOSTNAME, assert_fingerprint=None):
+ self._assert_hostname = assert_hostname
+ self._assert_fingerprint = assert_fingerprint
+ super(EnforceTLSv1Adapter, self).__init__()
+
+ def init_poolmanager(self, connections, maxsize, block=False):
+ self.poolmanager = PoolManager(num_pools=connections, maxsize=maxsize,
+ block=block,
+ assert_hostname=self._assert_hostname,
+ assert_fingerprint=self._assert_fingerprint,
+ cert_reqs=ssl.CERT_REQUIRED)