diff options
-rw-r--r-- | service/pixelated/adapter/model/mail.py | 31 | ||||
-rw-r--r-- | service/test/unit/adapter/test_mail.py | 19 | ||||
-rw-r--r-- | web-ui/app/js/services/model/mail.js | 176 | ||||
-rw-r--r-- | web-ui/app/js/tags/data/tags.js | 1 | ||||
-rw-r--r-- | web-ui/app/locales/en-us/translation.json | 2 | ||||
-rw-r--r-- | web-ui/app/locales/en/translation.json | 2 | ||||
-rw-r--r-- | web-ui/app/scss/_security.scss | 12 | ||||
-rw-r--r-- | web-ui/test/spec/tags/data/tags.spec.js | 8 |
8 files changed, 140 insertions, 111 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/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/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/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/_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/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'); + }); }); |