diff options
Diffstat (limited to 'mailnews/mime/src/mimemalt.cpp')
-rw-r--r-- | mailnews/mime/src/mimemalt.cpp | 580 |
1 files changed, 580 insertions, 0 deletions
diff --git a/mailnews/mime/src/mimemalt.cpp b/mailnews/mime/src/mimemalt.cpp new file mode 100644 index 000000000..3354b1f9b --- /dev/null +++ b/mailnews/mime/src/mimemalt.cpp @@ -0,0 +1,580 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + BACKGROUND + ---------- + + At the simplest level, multipart/alternative means "pick one of these and + display it." However, it's actually a lot more complicated than that. + + The alternatives are in preference order, and counterintuitively, they go + from *least* to *most* preferred rather than the reverse. Therefore, when + we're parsing, we can't just take the first one we like and throw the rest + away -- we have to parse through the whole thing, discarding the n'th part if + we are capable of displaying the n+1'th. + + Adding a wrinkle to that is the fact that we give the user the option of + demanding the plain-text alternative even though we are perfectly capable of + displaying the HTML, and it is almost always the preferred format, i.e., it + almost always comes after the plain-text alternative. + + Speaking of which, you can't assume that each of the alternatives is just a + basic text/[whatever]. There may be, for example, a text/plain followed by a + multipart/related which contains text/html and associated embedded + images. Yikes! + + You also can't assume that there will be just two parts. There can be an + arbitrary number, and the ones we are capable of displaying and the ones we + aren't could be interspersed in any order by the producer of the MIME. + + We can't just throw away the parts we're not displaying when we're processing + the MIME for display. If we were to do that, then the MIME parts that + remained wouldn't get numbered properly, and that would mean, for example, + that deleting attachments wouldn't work in some messages. Indeed, that very + problem is what prompted a rewrite of this file into its current + architecture. + + ARCHITECTURE + ------------ + + Parts are read and queued until we know whether we're going to display + them. If the first pending part is one we don't know how to display, then we + can add it to the MIME structure immediatelly, with output_p disabled. If the + first pending part is one we know how to display, then we can't add it to the + in-memory MIME structure until either (a) we encounter a later, more + preferred part we know how to display, or (b) we reach the end of the + parts. A display-capable part of the queue may be followed by one or more + display-incapable parts. We can't add them to the in-memory structure until + we figure out what to do with the first, display-capable pending part, + because otherwise the order and numbering will be wrong. All of the logic in + this paragraph is implemented in the flush_children function. + + The display_cached_part function is what actually adds a MIME part to the + in-memory MIME structure. There is one complication there which forces us to + violate abstrations... Even if we set output_p on a child before adding it to + the parent, the parse_begin function resets it. The kluge I came up with to + prevent that was to give the child a separate options object and set + output_fn to nullptr in it, because that causes parse_begin to set output_p to + false. This seemed like the least onerous way to accomplish this, although I + can't say it's a solution I'm particularly fond of. + + Another complication in display_cached_part is that if we were just a normal + multipart type, we could rely on MimeMultipart_parse_line to notify emitters + about content types, character sets, part numbers, etc. as our new children + get created. However, since we defer creation of some children, the + notification doesn't happen there, so we have to handle it + ourselves. Unfortunately, this requires a small abstraction violation in + MimeMultipart_parse_line -- we have to check there if the entity is + multipart/alternative and if so not notify emitters there because + MimeMultipartAlternative_create_child handles it. + + - Jonathan Kamens, 2010-07-23 + + When the option prefer_plaintext is on, the last text/plain part + should be preferred over any other part that can be displayed. But + if no text/plain part is found, then the algorithm should go as + normal and convert any html part found to text. To achive this I + found that the simplest way was to change the function display_part_p + into returning priority as an integer instead of boolean can/can't + display. Then I also changed the function flush_children so it selects + the last part with the highest priority. (Priority 0 means it cannot + be displayed and the part is never choosen.) + + - Terje BrĂ¥ten, 2013-02-16 +*/ + +#include "mimemalt.h" +#include "prmem.h" +#include "plstr.h" +#include "prlog.h" +#include "nsMimeTypes.h" +#include "nsMimeStringResources.h" +#include "nsIPrefBranch.h" +#include "mimemoz2.h" // for prefs + +extern "C" MimeObjectClass mimeMultipartRelatedClass; + +#define MIME_SUPERCLASS mimeMultipartClass +MimeDefClass(MimeMultipartAlternative, MimeMultipartAlternativeClass, + mimeMultipartAlternativeClass, &MIME_SUPERCLASS); + +static int MimeMultipartAlternative_initialize (MimeObject *); +static void MimeMultipartAlternative_finalize (MimeObject *); +static int MimeMultipartAlternative_parse_eof (MimeObject *, bool); +static int MimeMultipartAlternative_create_child(MimeObject *); +static int MimeMultipartAlternative_parse_child_line (MimeObject *, const char *, + int32_t, bool); +static int MimeMultipartAlternative_close_child(MimeObject *); + +static int MimeMultipartAlternative_flush_children(MimeObject *, bool, priority_t); +static priority_t MimeMultipartAlternative_display_part_p(MimeObject *self, + MimeHeaders *sub_hdrs); +static priority_t MimeMultipartAlternative_prioritize_part(char *content_type, + bool prefer_plaintext); + +static int MimeMultipartAlternative_display_cached_part(MimeObject *, + MimeHeaders *, + MimePartBufferData *, + bool); + +static int +MimeMultipartAlternativeClassInitialize(MimeMultipartAlternativeClass *clazz) +{ + MimeObjectClass *oclass = (MimeObjectClass *) clazz; + MimeMultipartClass *mclass = (MimeMultipartClass *) clazz; + PR_ASSERT(!oclass->class_initialized); + oclass->initialize = MimeMultipartAlternative_initialize; + oclass->finalize = MimeMultipartAlternative_finalize; + oclass->parse_eof = MimeMultipartAlternative_parse_eof; + mclass->create_child = MimeMultipartAlternative_create_child; + mclass->parse_child_line = MimeMultipartAlternative_parse_child_line; + mclass->close_child = MimeMultipartAlternative_close_child; + return 0; +} + + +static int +MimeMultipartAlternative_initialize (MimeObject *obj) +{ + MimeMultipartAlternative *malt = (MimeMultipartAlternative *) obj; + + NS_ASSERTION(!malt->part_buffers, "object initialized multiple times"); + NS_ASSERTION(!malt->buffered_hdrs, "object initialized multiple times"); + malt->pending_parts = 0; + malt->max_parts = 0; + malt->buffered_priority = PRIORITY_UNDISPLAYABLE; + malt->buffered_hdrs = nullptr; + malt->part_buffers = nullptr; + + return ((MimeObjectClass*)&MIME_SUPERCLASS)->initialize(obj); +} + +static void +MimeMultipartAlternative_cleanup(MimeObject *obj) +{ + MimeMultipartAlternative *malt = (MimeMultipartAlternative *) obj; + int32_t i; + + for (i = 0; i < malt->pending_parts; i++) { + MimeHeaders_free(malt->buffered_hdrs[i]); + MimePartBufferDestroy(malt->part_buffers[i]); + } + PR_FREEIF(malt->buffered_hdrs); + PR_FREEIF(malt->part_buffers); + malt->pending_parts = 0; + malt->max_parts = 0; +} + + +static void +MimeMultipartAlternative_finalize (MimeObject *obj) +{ + MimeMultipartAlternative_cleanup(obj); + ((MimeObjectClass*)&MIME_SUPERCLASS)->finalize(obj); +} + + +static int +MimeMultipartAlternative_flush_children(MimeObject *obj, + bool finished, + priority_t next_priority) +{ + /* + The cache should always have at the head the part with highest priority. + + Possible states: + + 1. Cache contains nothing: do nothing. + + 2. Finished, and the cache contains one displayable body followed + by zero or more bodies with lower priority: + + 3. Finished, and the cache contains one non-displayable body: + create it with output off. + + 4. Not finished, and the cache contains one displayable body + followed by zero or more bodies with lower priority, and the new + body we're about to create is higher or equal priority: + create all cached bodies with output off. + + 5. Not finished, and the cache contains one displayable body + followed by zero or more bodies with lower priority, and the new + body we're about to create has lower priority: do nothing. + + 6. Not finished, and the cache contains one non-displayable body: + create it with output off. + */ + MimeMultipartAlternative *malt = (MimeMultipartAlternative *) obj; + bool have_displayable, do_flush, do_display; + + /* Case 1 */ + if (! malt->pending_parts) + return 0; + + have_displayable = (malt->buffered_priority > next_priority); + + if (finished && have_displayable) { + /* Case 2 */ + do_flush = true; + do_display = true; + } + else if (finished && ! have_displayable) { + /* Case 3 */ + do_flush = true; + do_display = false; + } + else if (! finished && have_displayable) { + /* Case 5 */ + do_flush = false; + do_display = false; + } + else if (! finished && ! have_displayable) { + /* Case 4 */ + /* Case 6 */ + do_flush = true; + do_display = false; + } + else { + NS_ERROR("mimemalt.cpp: logic error in flush_children"); + return -1; + } + + if (do_flush) { + int32_t i; + for (i = 0; i < malt->pending_parts; i++) { + MimeMultipartAlternative_display_cached_part(obj, + malt->buffered_hdrs[i], + malt->part_buffers[i], + do_display && (i == 0)); + MimeHeaders_free(malt->buffered_hdrs[i]); + MimePartBufferDestroy(malt->part_buffers[i]); + } + malt->pending_parts = 0; + } + return 0; +} + +static int +MimeMultipartAlternative_parse_eof (MimeObject *obj, bool abort_p) +{ + int status = 0; + + if (obj->closed_p) return 0; + + status = ((MimeObjectClass*)&MIME_SUPERCLASS)->parse_eof(obj, abort_p); + if (status < 0) return status; + + + status = MimeMultipartAlternative_flush_children(obj, true, + PRIORITY_UNDISPLAYABLE); + if (status < 0) + return status; + + MimeMultipartAlternative_cleanup(obj); + + return status; +} + + +static int +MimeMultipartAlternative_create_child(MimeObject *obj) +{ + MimeMultipart *mult = (MimeMultipart *) obj; + MimeMultipartAlternative *malt = (MimeMultipartAlternative *) obj; + + priority_t priority = + MimeMultipartAlternative_display_part_p (obj, mult->hdrs); + + MimeMultipartAlternative_flush_children(obj, false, priority); + + mult->state = MimeMultipartPartFirstLine; + int32_t i = malt->pending_parts++; + + if (i==0) { + malt->buffered_priority = priority; + } + + if (malt->pending_parts > malt->max_parts) { + malt->max_parts = malt->pending_parts; + MimeHeaders **newBuf = (MimeHeaders **) + PR_REALLOC(malt->buffered_hdrs, + malt->max_parts * sizeof(*malt->buffered_hdrs)); + NS_ENSURE_TRUE(newBuf, MIME_OUT_OF_MEMORY); + malt->buffered_hdrs = newBuf; + + MimePartBufferData **newBuf2 = (MimePartBufferData **) + PR_REALLOC(malt->part_buffers, + malt->max_parts * sizeof(*malt->part_buffers)); + NS_ENSURE_TRUE(newBuf2, MIME_OUT_OF_MEMORY); + malt->part_buffers = newBuf2; + } + + malt->buffered_hdrs[i] = MimeHeaders_copy(mult->hdrs); + NS_ENSURE_TRUE(malt->buffered_hdrs[i], MIME_OUT_OF_MEMORY); + + malt->part_buffers[i] = MimePartBufferCreate(); + NS_ENSURE_TRUE(malt->part_buffers[i], MIME_OUT_OF_MEMORY); + + return 0; +} + + +static int +MimeMultipartAlternative_parse_child_line (MimeObject *obj, + const char *line, int32_t length, + bool first_line_p) +{ + MimeMultipartAlternative *malt = (MimeMultipartAlternative *) obj; + + NS_ASSERTION(malt->pending_parts, "should be pending parts, but there aren't"); + if (!malt->pending_parts) + return -1; + int32_t i = malt->pending_parts - 1; + + /* Push this line into the buffer for later retrieval. */ + return MimePartBufferWrite (malt->part_buffers[i], line, length); +} + + +static int +MimeMultipartAlternative_close_child(MimeObject *obj) +{ + MimeMultipartAlternative *malt = (MimeMultipartAlternative *) obj; + MimeMultipart *mult = (MimeMultipart *) obj; + + /* PR_ASSERT(malt->part_buffer); Some Mac brokenness trips this... + if (!malt->part_buffer) return -1; */ + + if (malt->pending_parts) + MimePartBufferClose(malt->part_buffers[malt->pending_parts-1]); + + /* PR_ASSERT(mult->hdrs); I expect the Mac trips this too */ + + if (mult->hdrs) { + MimeHeaders_free(mult->hdrs); + mult->hdrs = 0; + } + + return 0; +} + + +static priority_t +MimeMultipartAlternative_display_part_p(MimeObject *self, + MimeHeaders *sub_hdrs) +{ + priority_t priority = PRIORITY_UNDISPLAYABLE; + char *ct = MimeHeaders_get (sub_hdrs, HEADER_CONTENT_TYPE, true, false); + if (!ct) + return priority; + + /* RFC 1521 says: + Receiving user agents should pick and display the last format + they are capable of displaying. In the case where one of the + alternatives is itself of type "multipart" and contains unrecognized + sub-parts, the user agent may choose either to show that alternative, + an earlier alternative, or both. + */ + + // We must pass 'true' as last parameter so that text/calendar is + // only displayable when Lightning is installed. + MimeObjectClass *clazz = mime_find_class(ct, sub_hdrs, self->options, true); + if (clazz && clazz->displayable_inline_p(clazz, sub_hdrs)) { + // prefer_plaintext pref + bool prefer_plaintext = false; + nsIPrefBranch *prefBranch = GetPrefBranch(self->options); + if (prefBranch) { + prefBranch->GetBoolPref("mailnews.display.prefer_plaintext", + &prefer_plaintext); + } + prefer_plaintext = prefer_plaintext && + (self->options->format_out != nsMimeOutput::nsMimeMessageSaveAs) && + (self->options->format_out != nsMimeOutput::nsMimeMessageRaw); + + priority = MimeMultipartAlternative_prioritize_part(ct, prefer_plaintext); + } + + PR_FREEIF(ct); + return priority; +} + +/** +* RFC 1521 says we should display the last format we are capable of displaying. +* But for various reasons (mainly to improve the user experience) we choose +* to ignore that in some cases, and rather pick one that we prioritize. +*/ +static priority_t +MimeMultipartAlternative_prioritize_part(char *content_type, + bool prefer_plaintext) +{ + /* + * PRIORITY_NORMAL is the priority of text/html, multipart/..., etc. that + * we normally display. We should try to have as few exceptions from + * PRIORITY_NORMAL as possible + */ + + /* (with no / in the type) */ + if (!PL_strcasecmp(content_type, "text")) { + if (prefer_plaintext) { + /* When in plain text view, a plain text part is what we want. */ + return PRIORITY_HIGH; + } + /* We normally prefer other parts over the unspecified text type. */ + return PRIORITY_TEXT_UNKNOWN; + } + + if (!PL_strncasecmp(content_type, "text/", 5)) { + char *text_type = content_type + 5; + + if (!PL_strncasecmp(text_type, "plain", 5)) { + if (prefer_plaintext) { + /* When in plain text view, + the text/plain part is exactly what we want */ + return PRIORITY_HIGHEST; + } + /* + * Because the html and the text part may be switched, + * or we have an extra text/plain added by f.ex. a buggy virus checker, + * we prioritize text/plain lower than normal. + */ + return PRIORITY_TEXT_PLAIN; + } + + if (!PL_strncasecmp(text_type, "calendar", 8) && prefer_plaintext) { + /* + * text/calendar receives an equally high priority so an invitation + * shows even in plaintext mode. + */ + return PRIORITY_HIGHEST; + } + + /* Need to white-list all text/... types that are or could be implemented. */ + if (!PL_strncasecmp(text_type, "html", 4) || + !PL_strncasecmp(text_type, "enriched", 8) || + !PL_strncasecmp(text_type, "richtext", 8) || + !PL_strncasecmp(text_type, "calendar", 8) || + !PL_strncasecmp(text_type, "rtf", 3)) { + return PRIORITY_NORMAL; + } + + /* We prefer other parts over unknown text types. */ + return PRIORITY_TEXT_UNKNOWN; + } + + // Guard against rogue messages with incorrect MIME structure and + // don't show images when plain text is requested. + if (!PL_strncasecmp(content_type, "image", 5)) { + if (prefer_plaintext) + return PRIORITY_UNDISPLAYABLE; + else + return PRIORITY_LOW; + } + + return PRIORITY_NORMAL; +} + +static int +MimeMultipartAlternative_display_cached_part(MimeObject *obj, + MimeHeaders *hdrs, + MimePartBufferData *buffer, + bool do_display) +{ + int status; + bool old_options_no_output_p; + + char *ct = (hdrs + ? MimeHeaders_get (hdrs, HEADER_CONTENT_TYPE, true, false) + : 0); + const char *dct = (((MimeMultipartClass *) obj->clazz)->default_part_type); + MimeObject *body; + /** Don't pass in NULL as the content-type (this means that the + * auto-uudecode-hack won't ever be done for subparts of a + * multipart, but only for untyped children of message/rfc822. + */ + const char *uct = (ct && *ct) ? ct : (dct ? dct: TEXT_PLAIN); + + // We always want to display the cached part inline. + body = mime_create(uct, hdrs, obj->options, true); + PR_FREEIF(ct); + if (!body) return MIME_OUT_OF_MEMORY; + body->output_p = do_display; + + status = ((MimeContainerClass *) obj->clazz)->add_child(obj, body); + if (status < 0) + { + mime_free(body); + return status; + } + /* add_child assigns body->options from obj->options, but that's + just a pointer so if we muck with it in the child it'll modify + the parent as well, which we definitely don't want. Therefore we + need to make a copy of the old value and restore it later. */ + old_options_no_output_p = obj->options->no_output_p; + if (! do_display) + body->options->no_output_p = true; + +#ifdef MIME_DRAFTS + /* if this object is a child of a multipart/related object, the parent is + taking care of decomposing the whole part, don't need to do it at this level. + However, we still have to call decompose_file_init_fn and decompose_file_close_fn + in order to set the correct content-type. But don't call MimePartBufferRead + */ + bool multipartRelatedChild = mime_typep(obj->parent,(MimeObjectClass*)&mimeMultipartRelatedClass); + bool decomposeFile = do_display && obj->options && + obj->options->decompose_file_p && + obj->options->decompose_file_init_fn && + !mime_typep(body, (MimeObjectClass *) &mimeMultipartClass); + + if (decomposeFile) + { + status = obj->options->decompose_file_init_fn ( + obj->options->stream_closure, hdrs); + if (status < 0) return status; + } +#endif /* MIME_DRAFTS */ + + /* Now that we've added this new object to our list of children, + notify emitters and start its parser going. */ + MimeMultipart_notify_emitter(body); + + status = body->clazz->parse_begin(body); + if (status < 0) return status; + +#ifdef MIME_DRAFTS + if (decomposeFile && !multipartRelatedChild) + status = MimePartBufferRead (buffer, + obj->options->decompose_file_output_fn, + obj->options->stream_closure); + else +#endif /* MIME_DRAFTS */ + + status = MimePartBufferRead (buffer, + /* The MimeConverterOutputCallback cast is to turn the + `void' argument into `MimeObject'. */ + ((MimeConverterOutputCallback) body->clazz->parse_buffer), + body); + + if (status < 0) return status; + + /* Done parsing. */ + status = body->clazz->parse_eof(body, false); + if (status < 0) return status; + status = body->clazz->parse_end(body, false); + if (status < 0) return status; + +#ifdef MIME_DRAFTS + if (decomposeFile) + { + status = obj->options->decompose_file_close_fn ( obj->options->stream_closure ); + if (status < 0) return status; + } +#endif /* MIME_DRAFTS */ + + /* Restore options to what parent classes expects. */ + obj->options->no_output_p = old_options_no_output_p; + + return 0; +} |