summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--service/pixelated/adapter/model/mail.py31
-rw-r--r--service/pixelated/adapter/soledad/soledad_facade_mixin.py6
-rw-r--r--service/pixelated/assets/welcome.mail74
-rw-r--r--service/pixelated/config/app_factory.py4
-rw-r--r--service/pixelated/config/welcome_mail.py48
-rw-r--r--service/test/integration/test_welcome_mail.py35
-rw-r--r--service/test/unit/adapter/test_mail.py19
-rw-r--r--service/test/unit/config/test_welcome_mail.py32
-rw-r--r--web-ui/app/js/helpers/view_helper.js13
-rw-r--r--web-ui/app/js/mail_list/ui/mail_item_factory.js6
-rw-r--r--web-ui/app/js/mixins/with_mail_edit_base.js4
-rw-r--r--web-ui/app/js/services/model/mail.js176
-rw-r--r--web-ui/app/js/tags/data/tags.js1
-rw-r--r--web-ui/app/js/tags/ui/tag_list.js1
-rw-r--r--web-ui/app/js/views/i18n.js8
-rw-r--r--web-ui/app/js/views/templates.js3
-rw-r--r--web-ui/app/locales/en-us/translation.json2
-rw-r--r--web-ui/app/locales/en/translation.json2
-rw-r--r--web-ui/app/scss/_mixins.scss10
-rw-r--r--web-ui/app/scss/_security.scss12
-rw-r--r--web-ui/app/templates/mails/full_view.hbs12
-rw-r--r--web-ui/app/templates/mails/single.hbs1
-rw-r--r--web-ui/app/templates/mails/trash.hbs26
-rw-r--r--web-ui/test/spec/helpers/view_helper.spec.js8
-rw-r--r--web-ui/test/spec/tags/data/tags.spec.js8
-rw-r--r--web-ui/test/spec/tags/ui/tag_list.spec.js7
26 files changed, 412 insertions, 137 deletions
diff --git a/service/pixelated/adapter/model/mail.py b/service/pixelated/adapter/model/mail.py
index 618b980a..8e7b745c 100644
--- a/service/pixelated/adapter/model/mail.py
+++ b/service/pixelated/adapter/model/mail.py
@@ -239,22 +239,33 @@ class PixelatedMail(Mail):
content_type = self._parse_charset_header(part['headers'].get('Content-Type'))
try:
- decoding_map = {
- 'quoted-printable': lambda content, content_type: unicode(content.decode('quopri'), content_type),
- 'base64': lambda content, content_type: content.decode('base64').decode('utf-8'),
- '7bit': lambda content, content_type: content.encode(content_type),
- '8bit': lambda content, content_type: content.encode(content_type)
- }
- if encoding:
- return decoding_map[encoding](part['content'], content_type)
- else:
- return part['content']
+ decoding_func = self._decoding_function_for_encoding(encoding)
+ return self._decode_content_with_fallback(part['content'], decoding_func, content_type)
except Exception:
logger.error('Failed to decode mail part with:')
logger.error('Content-Transfer-Encoding: %s' % encoding)
logger.error('Content-Type: %s' % part['headers'].get('Content-Type'))
raise
+ def _decoding_function_for_encoding(self, encoding):
+ decoding_map = {
+ 'quoted-printable': lambda content, content_type: content.decode('quopri').decode(content_type),
+ 'base64': lambda content, content_type: content.decode('base64').decode('utf-8'),
+ '7bit': lambda content, content_type: content.encode(content_type),
+ '8bit': lambda content, content_type: content.encode(content_type)
+ }
+ if encoding in decoding_map:
+ return decoding_map[encoding]
+ else:
+ return decoding_map['8bit']
+
+ def _decode_content_with_fallback(self, content, decode_func, content_type):
+ try:
+ return decode_func(content, content_type)
+ # return content.encode(content_type)
+ except ValueError:
+ return content.encode('ascii', 'ignore')
+
@property
def alternatives(self):
return self.parts.get('alternatives')
diff --git a/service/pixelated/adapter/soledad/soledad_facade_mixin.py b/service/pixelated/adapter/soledad/soledad_facade_mixin.py
index 280fc81e..761ef1e2 100644
--- a/service/pixelated/adapter/soledad/soledad_facade_mixin.py
+++ b/service/pixelated/adapter/soledad/soledad_facade_mixin.py
@@ -59,8 +59,10 @@ class SoledadDbFacadeMixin(object):
def get_mbox(self, mbox):
return self.soledad.get_from_index('by-type-and-mbox', 'mbox', mbox) if mbox else []
- def get_lastuid(self, mbox_doc):
- return mbox_doc.content['lastuid']
+ def get_lastuid(self, mbox):
+ if isinstance(mbox, str):
+ mbox = self.get_mbox(mbox)[0]
+ return mbox.content['lastuid']
def get_search_index_masterkey(self):
return self.soledad.get_from_index('by-type', 'index_key')
diff --git a/service/pixelated/assets/welcome.mail b/service/pixelated/assets/welcome.mail
new file mode 100644
index 00000000..e85694f1
--- /dev/null
+++ b/service/pixelated/assets/welcome.mail
@@ -0,0 +1,74 @@
+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
+Content-Type: text/plain; charset=UTF-8
+
+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/service/pixelated/config/app_factory.py b/service/pixelated/config/app_factory.py
index f20b1229..5dcf60cb 100644
--- a/service/pixelated/config/app_factory.py
+++ b/service/pixelated/config/app_factory.py
@@ -39,6 +39,7 @@ from leap.common.events import (
events_pb2 as proto
)
from twisted.web.server import Site
+from .welcome_mail import check_welcome_mail_wrapper
CREATE_KEYS_IF_KEYS_DONT_EXISTS_CALLBACK = 12345
@@ -117,6 +118,9 @@ def init_app(app, leap_home, leap_session):
search_engine=search_engine,
mail_service=mail_service))
+ register(signal=proto.SOLEDAD_DONE_DATA_SYNC,
+ callback=check_welcome_mail_wrapper(pixelated_mailboxes.inbox()))
+
register(signal=proto.SOLEDAD_DONE_DATA_SYNC, uid=CREATE_KEYS_IF_KEYS_DONT_EXISTS_CALLBACK,
callback=look_for_user_key_and_create_if_cant_find(leap_session))
diff --git a/service/pixelated/config/welcome_mail.py b/service/pixelated/config/welcome_mail.py
new file mode 100644
index 00000000..236c4331
--- /dev/null
+++ b/service/pixelated/config/welcome_mail.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/>.
+import os
+from pixelated.adapter.model.mail import InputMail
+from pixelated.support.date import iso_now
+from email import message_from_file
+from email.MIMEMultipart import MIMEMultipart
+
+
+def check_welcome_mail(mailbox):
+ if mailbox.fresh:
+ welcome_mail = build_welcome_mail()
+ mailbox.add(welcome_mail)
+
+
+def build_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)
+ welcome_mail = InputMail()
+ welcome_mail.headers['To'] = InputMail.FROM_EMAIL_ADDRESS
+ welcome_mail.headers['Subject'] = mail_template['Subject']
+ welcome_mail.headers['Date'] = iso_now()
+ welcome_mail._mime = MIMEMultipart()
+ for payload in mail_template.get_payload():
+ welcome_mail._mime.attach(payload)
+ if payload.get_content_type() == 'text/plain':
+ welcome_mail.body = payload.as_string()
+ return welcome_mail
+
+
+def check_welcome_mail_wrapper(mailbox):
+ def wrapper(*args, **kwargs):
+ check_welcome_mail(mailbox)
+ return wrapper
diff --git a/service/test/integration/test_welcome_mail.py b/service/test/integration/test_welcome_mail.py
new file mode 100644
index 00000000..ed37f50e
--- /dev/null
+++ b/service/test/integration/test_welcome_mail.py
@@ -0,0 +1,35 @@
+#
+# 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 test.support.integration import SoledadTestBase
+from pixelated.config.welcome_mail import check_welcome_mail
+
+
+class TestWelcomeMail(SoledadTestBase):
+
+ def test_that_a_fresh_INBOX_will_receive_a_welcome_mail_only_once(self):
+ inbox = self.mailboxes.inbox()
+ check_welcome_mail(inbox) # adds a mail
+ check_welcome_mail(inbox) # should not repeat
+
+ inbox_mails = self.get_mails_by_tag('inbox')
+ self.assertEquals(1, len(inbox_mails))
+
+ self.delete_mail(inbox_mails[0].ident)
+ check_welcome_mail(inbox) # it is empty, but not fresh anymore
+
+ inbox_mails = self.get_mails_by_tag('inbox')
+ self.assertEquals(0, len(inbox_mails))
diff --git a/service/test/unit/adapter/test_mail.py b/service/test/unit/adapter/test_mail.py
index d77816cd..1a9280ff 100644
--- a/service/test/unit/adapter/test_mail.py
+++ b/service/test/unit/adapter/test_mail.py
@@ -232,6 +232,25 @@ class TestPixelatedMail(unittest.TestCase):
self.assertEquals(u'H\xe4llo', mail.text_plain_body)
self.assertEquals(u'<p>H\xe4llo</p>', mail.html_body)
+ def test_broken_content_type_defaults_to_usascii(self):
+ plain_headers = {'Content-Type': 'I lie to you', 'Content-Transfer-Encoding': 'quoted-printable'}
+ html_headers = {'Content-Type': 'text/html;\ncharset=utf-8', 'Content-Transfer-Encoding': 'quoted-printable'}
+ parts = {'alternatives': [{'content': 'H=E4llo', 'headers': plain_headers}, {'content': '<p>H=C3=A4llo</p>', 'headers': html_headers}]}
+
+ mail = PixelatedMail.from_soledad(None, None, self._create_bdoc(raw='some raw body'), parts=parts, soledad_querier=None)
+
+ self.assertEquals(u'H=E4llo', mail.text_plain_body)
+
+ def test_broken_encoding_defaults_to_8bit(self):
+ plain_headers = {'Content-Type': 'text/plain;\ncharset=iso-8859-1', 'Content-Transfer-Encoding': 'I lie to you!'}
+ html_headers = {'Content-Type': 'text/html;\ncharset=utf-8', 'Content-Transfer-Encoding': 'quoted-printable'}
+ parts = {'alternatives': [{'content': 'H=E4llo', 'headers': plain_headers}, {'content': '<p>H=C3=A4llo</p>', 'headers': html_headers}]}
+
+ mail = PixelatedMail.from_soledad(None, None, self._create_bdoc(raw='some raw body'), parts=parts, soledad_querier=None)
+
+ self.assertEquals(u'H=E4llo', mail.text_plain_body)
+ self.assertEquals(u'<p>H\xe4llo</p>', mail.html_body)
+
def test_clean_line_breaks_on_address_headers(self):
many_recipients = 'One <one@mail.com>,\nTwo <two@mail.com>, Normal <normal@mail.com>,\nalone@mail.com'
headers = {'Cc': many_recipients,
diff --git a/service/test/unit/config/test_welcome_mail.py b/service/test/unit/config/test_welcome_mail.py
new file mode 100644
index 00000000..3971c73f
--- /dev/null
+++ b/service/test/unit/config/test_welcome_mail.py
@@ -0,0 +1,32 @@
+#
+# Copyright (c) 2014 ThoughtWorks, Inc.
+#
+# Pixelated is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Pixelated is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+import unittest
+
+from pixelated.config.welcome_mail import build_welcome_mail
+from pixelated.adapter.model.mail import InputMail
+
+
+class WelcomeMailTest(unittest.TestCase):
+
+ def test_build_plain_welcome_mail(self):
+ user_address = InputMail.FROM_EMAIL_ADDRESS = 'welcomed@user'
+ mail = build_welcome_mail()
+ self.assertEquals(user_address, mail.to)
+ self.assertEquals('Welcome to Pixelated Mail', mail.headers['Subject'])
+ self.assertIn('How to use it', mail.body)
+ self.assertIn('text/plain', mail._mime.as_string())
+ self.assertIn('text/html', mail._mime.as_string())
+ self.assertTrue(mail.headers['Date'])
diff --git a/web-ui/app/js/helpers/view_helper.js b/web-ui/app/js/helpers/view_helper.js
index 6755b891..a682ae5e 100644
--- a/web-ui/app/js/helpers/view_helper.js
+++ b/web-ui/app/js/helpers/view_helper.js
@@ -22,7 +22,7 @@ define(
'quoted-printable/quoted-printable',
'utf8/utf8'
],
- function(contentType, htmlWhitelister, i18n_lib, quotedPrintable, utf8) {
+ function(contentType, htmlWhitelister, i18n, quotedPrintable, utf8) {
'use strict';
function formatStatusClasses(ss) {
@@ -109,12 +109,15 @@ define(
}, 1);
}
- function quoteMail(mail) {
- return '\n\n' + mail.textPlainBody.replace(/^/mg, '> ');
+ function prependFrom(mail) {
+ return i18n(
+ 'On __date__, <__from__> wrote:\n',
+ {'date': new Date(mail.header.date).toString(), 'from': mail.header.from}
+ );
}
- function i18n(text) {
- return i18n_lib.get(text);
+ function quoteMail(mail) {
+ return '\n\n' + prependFrom(mail) + mail.textPlainBody.replace(/^/mg, '> ');
}
return {
diff --git a/web-ui/app/js/mail_list/ui/mail_item_factory.js b/web-ui/app/js/mail_list/ui/mail_item_factory.js
index 87a883b0..3c815401 100644
--- a/web-ui/app/js/mail_list/ui/mail_item_factory.js
+++ b/web-ui/app/js/mail_list/ui/mail_item_factory.js
@@ -26,12 +26,14 @@ define(
var MAIL_ITEM_TYPE = {
'drafts': DraftItem,
- 'sent': SentItem
+ 'sent': SentItem,
+ 'trash': GenericMailItem
};
var TEMPLATE_TYPE = {
'drafts': 'draft',
- 'sent': 'sent'
+ 'sent': 'sent',
+ 'trash': 'trash'
};
var createAndAttach = function (nodeToAttachTo, mail, currentMailIdent, currentTag, isChecked) {
diff --git a/web-ui/app/js/mixins/with_mail_edit_base.js b/web-ui/app/js/mixins/with_mail_edit_base.js
index 9942e747..5efb8967 100644
--- a/web-ui/app/js/mixins/with_mail_edit_base.js
+++ b/web-ui/app/js/mixins/with_mail_edit_base.js
@@ -134,7 +134,7 @@ define(
} else {
this.trigger(
events.ui.userAlerts.displayMessage,
- {message: i18n.get('One or more of the recipients are not valid emails')}
+ {message: i18n('One or more of the recipients are not valid emails')}
);
this.trigger(events.mail.send_failed);
}
@@ -176,7 +176,7 @@ define(
this.draftSaved = function(event, data) {
this.attr.ident = data.ident;
if(!this.attr.silent) {
- this.trigger(document, events.ui.userAlerts.displayMessage, { message: i18n.get('Saved as draft.') });
+ this.trigger(document, events.ui.userAlerts.displayMessage, { message: i18n('Saved as draft.') });
}
delete this.attr.silent;
};
diff --git a/web-ui/app/js/services/model/mail.js b/web-ui/app/js/services/model/mail.js
index 34cba610..c41bcff9 100644
--- a/web-ui/app/js/services/model/mail.js
+++ b/web-ui/app/js/services/model/mail.js
@@ -16,121 +16,111 @@
*/
'use strict';
-define(['helpers/contenttype'],
- function (contentType) {
-
- var asMail = (function () {
-
- function isSentMail() {
- return _.has(this, 'mailbox') && this.mailbox.toUpperCase() === 'SENT';
- }
-
- function isDraftMail() {
- return _.has(this, 'mailbox') && this.mailbox.toUpperCase() === 'DRAFTS';
- }
-
- function isInTrash() {
- return _.has(this, 'mailbox') && this.mailbox.toUpperCase() === 'TRASH';
- }
-
- function setDraftReplyFor(ident) {
- this.draft_reply_for = ident;
- }
-
- function replyToAddress() {
- return {
- to: [this.replying.single],
- cc: []
- };
- }
-
- function replyToAllAddress() {
- return {
- to: this.replying.all['to-field'],
- cc: this.replying.all['cc-field']
- };
- }
+define(['helpers/contenttype'], function (contentType) {
+ function isSentMail() {
+ return _.has(this, 'mailbox') && this.mailbox.toUpperCase() === 'SENT';
+ }
+
+ function isDraftMail() {
+ return _.has(this, 'mailbox') && this.mailbox.toUpperCase() === 'DRAFTS';
+ }
+
+ function isInTrash() {
+ return _.has(this, 'mailbox') && this.mailbox.toUpperCase() === 'TRASH';
+ }
+
+ function setDraftReplyFor(ident) {
+ this.draft_reply_for = ident;
+ }
+
+ function replyToAddress() {
+ return {
+ to: [this.replying.single],
+ cc: []
+ };
+ }
- function getHeadersFromMailPart (rawBody) {
- var lines, headerLines, endOfHeaders, headers;
+ function replyToAllAddress() {
+ return {
+ to: this.replying.all['to-field'],
+ cc: this.replying.all['cc-field']
+ };
+ }
- lines = rawBody.split('\n');
- endOfHeaders = _.indexOf(lines, '');
- headerLines = lines.slice(0, endOfHeaders);
+ function getHeadersFromMailPart (rawBody) {
+ var lines, headerLines, endOfHeaders, headers;
- headers = _.map(headerLines, function (headerLine) {
- return _.map(headerLine.split(':'), function(elem){return elem.trim();});
- });
+ lines = rawBody.split('\n');
+ endOfHeaders = _.indexOf(lines, '');
+ headerLines = lines.slice(0, endOfHeaders);
- return _.object(headers);
- }
+ headers = _.map(headerLines, function (headerLine) {
+ return _.map(headerLine.split(':'), function(elem){return elem.trim();});
+ });
- function getBodyFromMailPart (rawBody) {
- var lines, endOfHeaders;
+ return _.object(headers);
+ }
- lines = rawBody.split('\n');
- endOfHeaders = _.indexOf(lines, '');
+ function getBodyFromMailPart (rawBody) {
+ var lines, endOfHeaders;
- return lines.slice(endOfHeaders + 1).join('\n');
- }
+ lines = rawBody.split('\n');
+ endOfHeaders = _.indexOf(lines, '');
- function parseWithHeaders(rawBody) {
- return {headers: getHeadersFromMailPart(rawBody), body: getBodyFromMailPart(rawBody)};
- }
+ return lines.slice(endOfHeaders + 1).join('\n');
+ }
- function getMailMultiParts () {
- var mediaType = this.getMailMediaType();
- var boundary = '--' + mediaType.params.boundary + '\n';
- var finalBoundary = '--' + mediaType.params.boundary + '--';
+ function parseWithHeaders(rawBody) {
+ return {headers: getHeadersFromMailPart(rawBody), body: getBodyFromMailPart(rawBody)};
+ }
- var bodyParts = this.body.split(finalBoundary)[0].split(boundary);
+ function getMailMultiParts () {
+ var mediaType = this.getMailMediaType();
+ var boundary = '--' + mediaType.params.boundary + '\n';
+ var finalBoundary = '--' + mediaType.params.boundary + '--';
- bodyParts = _.reject(bodyParts, function(bodyPart) { return _.isEmpty(bodyPart.trim()); });
+ var bodyParts = this.body.split(finalBoundary)[0].split(boundary);
- return _.map(bodyParts, parseWithHeaders);
- }
+ bodyParts = _.reject(bodyParts, function(bodyPart) { return _.isEmpty(bodyPart.trim()); });
- function getMailMediaType () {
- return new contentType.MediaType(this.header.content_type);
- }
+ return _.map(bodyParts, parseWithHeaders);
+ }
- function isMailMultipartAlternative () {
- return this.getMailMediaType().type === 'multipart/alternative';
- }
+ function getMailMediaType () {
+ return new contentType.MediaType(this.header.content_type);
+ }
- function availableBodyPartsContentType () {
- var bodyParts = this.getMailMultiParts();
+ function isMailMultipartAlternative () {
+ return this.getMailMediaType().type === 'multipart/alternative';
+ }
- return _.pluck(_.pluck(bodyParts, 'headers'), 'Content-Type');
- }
+ function availableBodyPartsContentType () {
+ var bodyParts = this.getMailMultiParts();
- function getMailPartByContentType (contentType) {
- var bodyParts = this.getMailMultiParts();
+ return _.pluck(_.pluck(bodyParts, 'headers'), 'Content-Type');
+ }
- return _.findWhere(bodyParts, {headers: {'Content-Type': contentType}});
- }
+ function getMailPartByContentType (contentType) {
+ var bodyParts = this.getMailMultiParts();
- return function () {
- this.isSentMail = isSentMail;
- this.isDraftMail = isDraftMail;
- this.isInTrash = isInTrash;
- this.setDraftReplyFor = setDraftReplyFor;
- this.replyToAddress = replyToAddress;
- this.replyToAllAddress = replyToAllAddress;
- this.getMailMediaType = getMailMediaType;
- this.isMailMultipartAlternative = isMailMultipartAlternative;
- this.getMailMultiParts = getMailMultiParts;
- this.availableBodyPartsContentType = availableBodyPartsContentType;
- this.getMailPartByContentType = getMailPartByContentType;
- return this;
- };
- }());
+ return _.findWhere(bodyParts, {headers: {'Content-Type': contentType}});
+ }
return {
create: function (mail) {
- if (mail) {
- asMail.apply(mail);
- }
+ if (!mail) { return; }
+
+ mail.isSentMail = isSentMail;
+ mail.isDraftMail = isDraftMail;
+ mail.isInTrash = isInTrash;
+ mail.setDraftReplyFor = setDraftReplyFor;
+ mail.replyToAddress = replyToAddress;
+ mail.replyToAllAddress = replyToAllAddress;
+ mail.getMailMediaType = getMailMediaType;
+ mail.isMailMultipartAlternative = isMailMultipartAlternative;
+ mail.getMailMultiParts = getMailMultiParts;
+ mail.availableBodyPartsContentType = availableBodyPartsContentType;
+ mail.getMailPartByContentType = getMailPartByContentType;
return mail;
}
};
diff --git a/web-ui/app/js/tags/data/tags.js b/web-ui/app/js/tags/data/tags.js
index 401b41f7..348c1832 100644
--- a/web-ui/app/js/tags/data/tags.js
+++ b/web-ui/app/js/tags/data/tags.js
@@ -57,6 +57,7 @@ define(['flight/lib/component', 'page/events', 'helpers/monitored_ajax', 'mixins
this.after('initialize', function () {
this.on(document, events.tags.want, this.fetchTags);
+ this.on(document, events.mail.sent, this.fetchTags);
});
}
diff --git a/web-ui/app/js/tags/ui/tag_list.js b/web-ui/app/js/tags/ui/tag_list.js
index a2172c6d..d6f6f76c 100644
--- a/web-ui/app/js/tags/ui/tag_list.js
+++ b/web-ui/app/js/tags/ui/tag_list.js
@@ -98,6 +98,7 @@ define(
this.after('initialize', function() {
this.on(document, events.tags.received, this.displayTags);
this.on(document, events.ui.tag.select, this.updateCurrentTag);
+ this.on(document, events.ui.tag.select, events.ui.mails.uncheckAll);
this.renderTagListTemplate();
});
}
diff --git a/web-ui/app/js/views/i18n.js b/web-ui/app/js/views/i18n.js
index b09490f5..568e7635 100644
--- a/web-ui/app/js/views/i18n.js
+++ b/web-ui/app/js/views/i18n.js
@@ -17,15 +17,11 @@
define(['i18next'], function(i18n) {
'use strict';
- var self = function(str) {
- return i18n.t(str);
- };
-
- self.get = self;
+ var self = i18n.t;
self.init = function(path) {
i18n.init({detectLngQS: 'lang', fallbackLng: 'en', lowerCaseLng: true, getAsync: false, resGetPath: path + 'locales/__lng__/__ns__.json'});
- Handlebars.registerHelper('t', self.get.bind(self));
+ Handlebars.registerHelper('t', self.bind(self));
};
return self;
diff --git a/web-ui/app/js/views/templates.js b/web-ui/app/js/views/templates.js
index 470c6e51..64e269ca 100644
--- a/web-ui/app/js/views/templates.js
+++ b/web-ui/app/js/views/templates.js
@@ -41,7 +41,8 @@ define(['hbs/templates'], function (templates) {
fullView: window.Pixelated['app/templates/mails/full_view.hbs'],
mailActions: window.Pixelated['app/templates/mails/mail_actions.hbs'],
draft: window.Pixelated['app/templates/mails/draft.hbs'],
- sent: window.Pixelated['app/templates/mails/sent.hbs']
+ sent: window.Pixelated['app/templates/mails/sent.hbs'],
+ trash: window.Pixelated['app/templates/mails/trash.hbs']
},
mailActions: {
actionsBox: window.Pixelated['app/templates/mail_actions/actions_box.hbs'],
diff --git a/web-ui/app/locales/en-us/translation.json b/web-ui/app/locales/en-us/translation.json
index 2a474c80..b6fd6f0a 100644
--- a/web-ui/app/locales/en-us/translation.json
+++ b/web-ui/app/locales/en-us/translation.json
@@ -46,7 +46,7 @@
"encrypted encryption-error": "Unable to decrypt",
"encrypted encryption-valid": "Encrypted",
"not-encrypted": "Not Encrypted",
- "signed": "Certified sender.",
+ "signed": "Certified sender",
"signed signature-revoked": "Sender could not be securely identified.",
"signed signature-expired": "Sender could not be securely identified.",
"signed signature-not-trusted": "Sender and/or message cannot be trusted.",
diff --git a/web-ui/app/locales/en/translation.json b/web-ui/app/locales/en/translation.json
index 2a474c80..b6fd6f0a 100644
--- a/web-ui/app/locales/en/translation.json
+++ b/web-ui/app/locales/en/translation.json
@@ -46,7 +46,7 @@
"encrypted encryption-error": "Unable to decrypt",
"encrypted encryption-valid": "Encrypted",
"not-encrypted": "Not Encrypted",
- "signed": "Certified sender.",
+ "signed": "Certified sender",
"signed signature-revoked": "Sender could not be securely identified.",
"signed signature-expired": "Sender could not be securely identified.",
"signed signature-not-trusted": "Sender and/or message cannot be trusted.",
diff --git a/web-ui/app/scss/_mixins.scss b/web-ui/app/scss/_mixins.scss
index 14a1679f..2ce4999a 100644
--- a/web-ui/app/scss/_mixins.scss
+++ b/web-ui/app/scss/_mixins.scss
@@ -81,6 +81,12 @@
}
@mixin tags {
+ i.tags-label {
+ vertical-align: bottom;
+ font-size: medium;
+ color: #c2c2c2;
+ }
+
ul.tags {
li {
font-size: 0.6rem;
@@ -105,7 +111,8 @@
background: transparent;
border: 1px solid #DDD;
line-height: 0;
- padding: 3px 2px 2px 2px;
+ padding: 1px 2px;
+ margin-left: -5px;
@include border-radius(2px);
&:hover {
opacity: 1;
@@ -114,6 +121,7 @@
i {
&:before {
vertical-align: middle;
+ font-size: smaller;
}
}
}
diff --git a/web-ui/app/scss/_security.scss b/web-ui/app/scss/_security.scss
index be306d86..0c2baa52 100644
--- a/web-ui/app/scss/_security.scss
+++ b/web-ui/app/scss/_security.scss
@@ -12,35 +12,35 @@
}
&.encrypted {
&:before {
- content: "\f023 \f00c";
+ content: "\f023";
}
&.encryption-error {
background: $attention;
&:before {
- content: "\f023 \f12a";
+ content: "\f023 \f057";
}
}
}
&.signed {
&:before {
- content: "\f007 \f00c";
+ content: "\f00c";
}
&.signature-not-trusted {
background: $error;
&:before {
- content: "\f007 \f05e";
+ content: "\f05e";
}
}
}
&[class^=not-], &.signature-expired, &.signature-revoked {
background: $attention;
&:before {
- content: "\f007 \f12a"
+ content: "\f05e"
}
}
&.not-encrypted {
&:before {
- content: "\f023 \f12a";
+ content: "\f13e ";
}
}
}
diff --git a/web-ui/app/templates/mails/full_view.hbs b/web-ui/app/templates/mails/full_view.hbs
index c72b3af9..55d116d2 100644
--- a/web-ui/app/templates/mails/full_view.hbs
+++ b/web-ui/app/templates/mails/full_view.hbs
@@ -10,7 +10,7 @@
<div style="display:inline-block;padding-top: 5px;width:95%;flex-shrink:1" >
- <div class="column large-10 no-padding security-status">
+ <div class="column large-12 no-padding security-status">
{{#if signatureStatus}}
<span class="{{signatureStatus}}">
{{t signatureStatus }}
@@ -22,10 +22,7 @@
</span>
{{/if}}
</div>
- <div class="column large-2 no-padding text-right">
- <span class="received-date">{{ header.formattedDate }}</span>
- </div>
- <div class="recipients column large-12 no-padding">
+ <div class="recipients column large-10 no-padding">
<span class="from">
{{#if header.from }}
{{ header.from }}
@@ -36,13 +33,16 @@
<i class="fa fa-long-arrow-right"></i>
{{{formatRecipients header}}}
</div>
-
+ <div class="recipients column large-2 text-right">
+ <span class="received-date">{{ header.formattedDate }}</span>
+ </div>
<div>
<h3 class="subjectArea column large-10 no-padding">
<span class="subject">{{ header.subject }}</span>
<div class="tagsArea">
<ul class="tags">
+ <i class="tags-label fa fa-tags"></i>
{{#each tags }}
<li class="tag" data-tag="{{this}}">{{ this }}</li>
{{/each }}
diff --git a/web-ui/app/templates/mails/single.hbs b/web-ui/app/templates/mails/single.hbs
index a74c9606..90023713 100644
--- a/web-ui/app/templates/mails/single.hbs
+++ b/web-ui/app/templates/mails/single.hbs
@@ -12,7 +12,6 @@
</span>
<div class="from">{{#if header.from }}{{ header.from }}{{else}}{{t "you"}}{{/if}}</div>
<div class="subject-and-tags">
- <i class="fa fa-trash-o"></i>
{{ header.subject }}
</div>
<div class="subject-and-tags">
diff --git a/web-ui/app/templates/mails/trash.hbs b/web-ui/app/templates/mails/trash.hbs
new file mode 100644
index 00000000..a74c9606
--- /dev/null
+++ b/web-ui/app/templates/mails/trash.hbs
@@ -0,0 +1,26 @@
+<span>
+ <input type="checkbox" {{#if isChecked }}checked="true"{{/if}} />
+</span>
+<span>
+ <a href="/#/{{ tag }}/mail/{{ ident }}">
+ <span class="received-date">{{ header.formattedDate }}
+ {{#if attachments}}
+ <div class="attachment-indicator">
+ <i class="fa fa-paperclip"></i>
+ </div>
+ {{/if}}
+ </span>
+ <div class="from">{{#if header.from }}{{ header.from }}{{else}}{{t "you"}}{{/if}}</div>
+ <div class="subject-and-tags">
+ <i class="fa fa-trash-o"></i>
+ {{ header.subject }}
+ </div>
+ <div class="subject-and-tags">
+ <ul class="tags">
+ {{#each tagsForListView }}
+ <li class="tag" data-tag="{{this}}">{{ this }}</li>
+ {{/each }}
+ </ul>
+ </div>
+ </a>
+</span>
diff --git a/web-ui/test/spec/helpers/view_helper.spec.js b/web-ui/test/spec/helpers/view_helper.spec.js
index 7b5b960b..888c6cda 100644
--- a/web-ui/test/spec/helpers/view_helper.spec.js
+++ b/web-ui/test/spec/helpers/view_helper.spec.js
@@ -15,6 +15,14 @@ define(['helpers/view_helper'], function (viewHelper) {
expect(quotedMail).toContain('> First Line\n> Second Line');
});
+
+ it('should add the mail sender information', function() {
+ testData.rawMail.mail.textPlainBody = 'First Line\nSecond Line';
+
+ var quotedMail = viewHelper.quoteMail(testData.rawMail.mail);
+
+ expect(quotedMail).toContain('On Wed Jun 04 2014 17:41:13 GMT+0000 (UTC), <laurel@hamill.info> wrote');
+ });
});
describe('getFormmattedDate', function() {
diff --git a/web-ui/test/spec/tags/data/tags.spec.js b/web-ui/test/spec/tags/data/tags.spec.js
index 469ab0ce..53978fb8 100644
--- a/web-ui/test/spec/tags/data/tags.spec.js
+++ b/web-ui/test/spec/tags/data/tags.spec.js
@@ -47,4 +47,12 @@ describeComponent('tags/data/tags', function () {
tags.push(this.component.all);
expect(eventSpy.mostRecentCall.data).toEqual(jasmine.objectContaining({tags: tags}));
});
+
+ it('should reload taglist on mail sent', function(){
+ spyOn($, 'ajax').and.returnValue($.Deferred());
+
+ $(document).trigger(Pixelated.events.mail.sent);
+
+ expect($.ajax.calls.mostRecent().args[0]).toEqual('/tags');
+ });
});
diff --git a/web-ui/test/spec/tags/ui/tag_list.spec.js b/web-ui/test/spec/tags/ui/tag_list.spec.js
index f92f72af..e84c68aa 100644
--- a/web-ui/test/spec/tags/ui/tag_list.spec.js
+++ b/web-ui/test/spec/tags/ui/tag_list.spec.js
@@ -62,6 +62,13 @@ describeComponent('tags/ui/tag_list', function () {
expect(this.component.attr.currentTag).toEqual('amazing');
});
+ it('should uncheck all emails when a new tag is selected', function () {
+ var uncheckAllEvent = spyOnEvent(document, Pixelated.events.ui.mails.uncheckAll);
+ $(document).trigger(Pixelated.events.ui.tag.select, { tag: 'amazing'});
+
+ expect(uncheckAllEvent).toHaveBeenTriggeredOn(document);
+ });
+
it('resets the tag lists when loading tags', function () {
var tagList = [tag('tag1', 1, false), tag('tag2', 2, true), tag('tag3', 3, true)];
$(document).trigger(Pixelated.events.tags.received, {tags: tagList});