/* 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/. */ #include "mimemult.h" #include #include "mimeeobj.h" #include "mimehdrs.h" #include "mimemalt.h" #include "mimemoz2.h" #include "mimemrel.h" #include "mimemsig.h" #include "msgCore.h" #include "nsMailHeaders.h" #include "nsMimeStringResources.h" #include "nsMimeTypes.h" #include "plstr.h" #include "prio.h" #include "prlog.h" #include "prmem.h" #ifdef XP_MACOSX # include "mimemapl.h" #endif #define MIME_SUPERCLASS mimeContainerClass MimeDefClass(MimeMultipart, MimeMultipartClass, mimeMultipartClass, &MIME_SUPERCLASS); static int MimeMultipart_initialize(MimeObject*); static void MimeMultipart_finalize(MimeObject*); static int MimeMultipart_parse_line(const char* line, int32_t length, MimeObject*); static int MimeMultipart_parse_eof(MimeObject* object, bool abort_p); static MimeMultipartBoundaryType MimeMultipart_check_boundary(MimeObject*, const char*, int32_t); static int MimeMultipart_create_child(MimeObject*); static bool MimeMultipart_output_child_p(MimeObject*, MimeObject*); static int MimeMultipart_parse_child_line(MimeObject*, const char*, int32_t, bool); static int MimeMultipart_close_child(MimeObject*); /** * Decomposing true attachments from the MIME stream into a standalone file. * * We avoid it for: * 1. Display containers (Alternative/Related/Signed) where parts are * components of the message body rather than distinct attachments. * 2. Nested multiparts, which handle their own recursive decomposition. * 3. vCards, which are historically processed as metadata. */ static bool MimeMultipart_should_decompose_child(MimeObject* obj, MimeObject* child) { // clang-format off if (mime_typep(obj, (MimeObjectClass*)&mimeMultipartAlternativeClass) || mime_typep(obj, (MimeObjectClass*)&mimeMultipartRelatedClass) || mime_typep(obj, (MimeObjectClass*)&mimeMultipartSignedClass)) { return false; } if (mime_typep(child, (MimeObjectClass*)&mimeMultipartClass)) { return false; } if ((mime_typep(child, (MimeObjectClass*)&mimeExternalObjectClass) || mime_typep(child, (MimeObjectClass*)&mimeSuppressedCryptoClass)) && (!strcmp(child->content_type, "text/vcard") || !strcmp(child->content_type, "text/x-vcard"))) { return false; } // clang-format on return true; } extern "C" MimeMultipartSignedClass mimeMultipartSignedClass; extern "C" MimeObjectClass mimeInlineTextVCardClass; extern "C" MimeExternalObjectClass mimeExternalObjectClass; extern "C" MimeSuppressedCryptoClass mimeSuppressedCryptoClass; #if defined(DEBUG) && defined(XP_UNIX) static int MimeMultipart_debug_print(MimeObject*, PRFileDesc*, int32_t); #endif static int MimeMultipartClassInitialize(MimeObjectClass* oclass) { MimeMultipartClass* mclass = (MimeMultipartClass*)oclass; PR_ASSERT(!oclass->class_initialized); oclass->initialize = MimeMultipart_initialize; oclass->finalize = MimeMultipart_finalize; oclass->parse_line = MimeMultipart_parse_line; oclass->parse_eof = MimeMultipart_parse_eof; mclass->check_boundary = MimeMultipart_check_boundary; mclass->create_child = MimeMultipart_create_child; mclass->output_child_p = MimeMultipart_output_child_p; mclass->parse_child_line = MimeMultipart_parse_child_line; mclass->close_child = MimeMultipart_close_child; #if defined(DEBUG) && defined(XP_UNIX) oclass->debug_print = MimeMultipart_debug_print; #endif return 0; } static int MimeMultipart_initialize(MimeObject* object) { MimeMultipart* mult = (MimeMultipart*)object; char* ct; /* This is an abstract class; it shouldn't be directly instantiated. */ PR_ASSERT(object->clazz != (MimeObjectClass*)&mimeMultipartClass); ct = MimeHeaders_get(object->headers, HEADER_CONTENT_TYPE, false, false); mult->boundary = (ct ? MimeHeaders_get_parameter(ct, HEADER_PARM_BOUNDARY, NULL, NULL) : 0); PR_FREEIF(ct); mult->state = MimeMultipartPreamble; return ((MimeObjectClass*)&MIME_SUPERCLASS)->initialize(object); } static void MimeMultipart_finalize(MimeObject* object) { MimeMultipart* mult = (MimeMultipart*)object; object->clazz->parse_eof(object, false); PR_FREEIF(mult->boundary); if (mult->hdrs) MimeHeaders_free(mult->hdrs); mult->hdrs = 0; ((MimeObjectClass*)&MIME_SUPERCLASS)->finalize(object); } int MimeWriteAString(MimeObject* obj, const nsACString& string) { const nsCString& flatString = PromiseFlatCString(string); return MimeObject_write(obj, flatString.get(), flatString.Length(), true); } static int MimeMultipart_parse_line(const char* line, int32_t length, MimeObject* obj) { MimeMultipart* mult = (MimeMultipart*)obj; MimeContainer* container = (MimeContainer*)obj; int status = 0; MimeMultipartBoundaryType boundary; NS_ASSERTION(line && *line, "empty line in multipart parse_line"); if (!line || !*line) return -1; NS_ASSERTION(!obj->closed_p, "obj shouldn't already be closed"); if (obj->closed_p) return -1; /* If we're supposed to write this object, but aren't supposed to convert it to HTML, simply pass it through unaltered. */ if (obj->output_p && obj->options && !obj->options->write_html_p && obj->options->output_fn && obj->options->format_out != nsMimeOutput::nsMimeMessageAttach) return MimeObject_write(obj, line, length, true); if (mult->state == MimeMultipartEpilogue) /* already done */ boundary = MimeMultipartBoundaryTypeNone; else boundary = ((MimeMultipartClass*)obj->clazz)->check_boundary(obj, line, length); if (boundary == MimeMultipartBoundaryTypeTerminator || boundary == MimeMultipartBoundaryTypeSeparator) { /* Match! Close the currently-open part, move on to the next state, and discard this line. */ bool endOfPart = (mult->state != MimeMultipartPreamble); if (endOfPart) status = ((MimeMultipartClass*)obj->clazz)->close_child(obj); if (status < 0) return status; if (boundary == MimeMultipartBoundaryTypeTerminator) mult->state = MimeMultipartEpilogue; else { mult->state = MimeMultipartHeaders; /* Reset the header parser for this upcoming part. */ NS_ASSERTION(!mult->hdrs, "mult->hdrs should be null here"); if (mult->hdrs) MimeHeaders_free(mult->hdrs); mult->hdrs = MimeHeaders_new(); if (!mult->hdrs) return MIME_OUT_OF_MEMORY; if (obj->options && obj->options->state && obj->options->state->partsToStrip.Length() > 0) { nsAutoCString newPart; newPart.Adopt(mime_part_address(obj)); newPart.Append('.'); newPart.AppendInt(container->nchildren + 1); obj->options->state->strippingPart = false; // check if this is a sub-part of a part we're stripping. for (uint32_t partIndex = 0; partIndex < obj->options->state->partsToStrip.Length(); partIndex++) { nsCString& curPartToStrip = obj->options->state->partsToStrip[partIndex]; if (newPart.Find(curPartToStrip) == 0 && (newPart.Length() == curPartToStrip.Length() || newPart.CharAt(curPartToStrip.Length()) == '.')) { obj->options->state->strippingPart = true; if (partIndex < obj->options->state->detachToFiles.Length()) obj->options->state->detachedFilePath = obj->options->state->detachToFiles[partIndex]; break; } } } } // if stripping out attachments, write the boundary line. Otherwise, return // to ignore it. if (obj->options && obj->options->format_out == nsMimeOutput::nsMimeMessageAttach) { // Because MimeMultipart_parse_child_line strips out the // the CRLF of the last line before the end of a part, we need to add that // back in here. if (endOfPart) MimeWriteAString(obj, nsLiteralCString(MSG_LINEBREAK)); status = MimeObject_write(obj, line, length, true); } return 0; } /* Otherwise, this isn't a boundary string. So do whatever it is we should do with this line (parse it as a header, feed it to the child part, ignore it, etc.) */ switch (mult->state) { case MimeMultipartPreamble: case MimeMultipartEpilogue: /* Ignore this line. */ break; case MimeMultipartHeaders: /* Parse this line as a header for the sub-part. */ { status = MimeHeaders_parse_line(line, length, mult->hdrs); bool stripping = false; if (status < 0) return status; // If this line is blank, we're now done parsing headers, and should // now examine the content-type to create this "body" part. // if (*line == '\r' || *line == '\n') { if (obj->options && obj->options->state && obj->options->state->strippingPart) { stripping = true; bool detachingPart = obj->options->state->detachedFilePath.Length() > 0; nsAutoCString fileName; fileName.Adopt(MimeHeaders_get_name(mult->hdrs, obj->options)); // clang-format off if (detachingPart) { char *contentType = MimeHeaders_get(mult->hdrs, "Content-Type", false, false); if (contentType) { MimeWriteAString(obj, "Content-Type: "_ns); MimeWriteAString(obj, nsDependentCString(contentType)); PR_Free(contentType); } MimeWriteAString(obj, nsLiteralCString(MSG_LINEBREAK)); MimeWriteAString(obj, "Content-Disposition: attachment; filename=\""_ns); MimeWriteAString(obj, fileName); MimeWriteAString(obj, "\""_ns MSG_LINEBREAK); MimeWriteAString(obj, "X-Mozilla-External-Attachment-URL: "_ns); MimeWriteAString(obj, obj->options->state->detachedFilePath); MimeWriteAString(obj, nsLiteralCString(MSG_LINEBREAK)); MimeWriteAString(obj, "X-Mozilla-Altered: AttachmentDetached; date=\""_ns); } else { nsAutoCString header("Content-Type: text/x-moz-deleted; name=\"Deleted: "); header.Append(fileName); MimeWriteAString(obj, header); MimeWriteAString(obj, "\""_ns MSG_LINEBREAK "Content-Transfer-Encoding: 8bit"_ns MSG_LINEBREAK); MimeWriteAString(obj, "Content-Disposition: inline; filename=\"Deleted: "_ns); MimeWriteAString(obj, fileName); MimeWriteAString(obj, "\""_ns MSG_LINEBREAK "X-Mozilla-Altered: AttachmentDeleted; date=\""_ns); } nsCString result; char timeBuffer[128]; PRExplodedTime now; PR_ExplodeTime(PR_Now(), PR_LocalTimeParameters, &now); PR_FormatTimeUSEnglish(timeBuffer, sizeof(timeBuffer), "%a %b %d %H:%M:%S %Y", &now); MimeWriteAString(obj, nsDependentCString(timeBuffer)); MimeWriteAString(obj, "\""_ns MSG_LINEBREAK); MimeWriteAString(obj, MSG_LINEBREAK "You deleted an attachment from this message. The original "_ns "MIME headers for the attachment were:"_ns MSG_LINEBREAK); MimeHeaders_write_raw_headers(mult->hdrs, obj->options, false); // clang-format on } int32_t old_nchildren = container->nchildren; status = ((MimeMultipartClass*)obj->clazz)->create_child(obj); if (status < 0) return status; NS_ASSERTION(mult->state != MimeMultipartHeaders, "mult->state shouldn't be MimeMultipartHeaders"); if (!stripping && container->nchildren > old_nchildren && obj->options && !mime_typep(obj, (MimeObjectClass*)&mimeMultipartAlternativeClass)) { // Notify emitter about content type and part path. MimeObject* kid = container->children[container->nchildren - 1]; MimeMultipart_notify_emitter(kid); } } break; } case MimeMultipartPartFirstLine: /* Hand this line off to the sub-part. */ status = (((MimeMultipartClass*)obj->clazz) ->parse_child_line(obj, line, length, true)); if (status < 0) return status; mult->state = MimeMultipartPartLine; break; case MimeMultipartPartLine: /* Hand this line off to the sub-part. */ status = (((MimeMultipartClass*)obj->clazz) ->parse_child_line(obj, line, length, false)); if (status < 0) return status; break; default: NS_ERROR("unexpected state in parse line"); return -1; } if (obj->options && obj->options->format_out == nsMimeOutput::nsMimeMessageAttach && (!(obj->options->state && obj->options->state->strippingPart) && mult->state != MimeMultipartPartLine)) return MimeObject_write(obj, line, length, false); return 0; } void MimeMultipart_notify_emitter(MimeObject* obj) { char* ct = nullptr; NS_ASSERTION(obj->options, "MimeMultipart_notify_emitter called with null options"); if (!obj->options) return; ct = MimeHeaders_get(obj->headers, HEADER_CONTENT_TYPE, false, false); if (obj->options->notify_nested_bodies) { mimeEmitterAddHeaderField(obj->options, HEADER_CONTENT_TYPE, ct ? ct : TEXT_PLAIN); char* part_path = mime_part_address(obj); if (part_path) { mimeEmitterAddHeaderField(obj->options, "x-jsemitter-part-path", part_path); PR_Free(part_path); } } // Examine the headers and see if there is a special charset // (i.e. non US-ASCII) for this message. If so, we need to // tell the emitter that this is the case for use in any // possible reply or forward operation. if (ct && (obj->options->notify_nested_bodies || MimeObjectIsMessageBody(obj))) { char* cset = MimeHeaders_get_parameter(ct, "charset", NULL, NULL); if (cset) { mimeEmitterUpdateCharacterSet(obj->options, cset); if (!obj->options->override_charset) // Also set this charset to msgWindow SetMailCharacterSetToMsgWindow(obj, cset); PR_Free(cset); } } PR_FREEIF(ct); } static MimeMultipartBoundaryType MimeMultipart_check_boundary(MimeObject* obj, const char* line, int32_t length) { MimeMultipart* mult = (MimeMultipart*)obj; int32_t blen; bool term_p; if (!mult->boundary || line[0] != '-' || line[1] != '-') return MimeMultipartBoundaryTypeNone; /* This is a candidate line to be a boundary. Check it out... */ blen = strlen(mult->boundary); term_p = false; /* strip trailing whitespace (including the newline.) */ while (length > 2 && IS_SPACE(line[length - 1])) length--; /* Could this be a terminating boundary? */ if (length == blen + 4 && line[length - 1] == '-' && line[length - 2] == '-') { term_p = true; } // looks like we have a separator but first, we need to check it's not for one // of the part's children. MimeContainer* cont = (MimeContainer*)obj; if (cont->nchildren > 0) { MimeObject* kid = cont->children[cont->nchildren - 1]; if (kid) if (mime_typep(kid, (MimeObjectClass*)&mimeMultipartClass)) { // Don't ask the kid to check the boundary if it has already detected a // Terminator MimeMultipart* mult = (MimeMultipart*)kid; if (mult->state != MimeMultipartEpilogue) if (MimeMultipart_check_boundary(kid, line, length) != MimeMultipartBoundaryTypeNone) return MimeMultipartBoundaryTypeNone; } } if (term_p) length -= 2; if (blen == length - 2 && !strncmp(line + 2, mult->boundary, length - 2)) return (term_p ? MimeMultipartBoundaryTypeTerminator : MimeMultipartBoundaryTypeSeparator); else return MimeMultipartBoundaryTypeNone; } static int MimeMultipart_create_child(MimeObject* obj) { MimeMultipart* mult = (MimeMultipart*)obj; int status; char* ct = (mult->hdrs ? MimeHeaders_get(mult->hdrs, HEADER_CONTENT_TYPE, true, false) : 0); const char* dct = (((MimeMultipartClass*)obj->clazz)->default_part_type); MimeObject* body = NULL; mult->state = MimeMultipartPartFirstLine; if (obj->options) obj->options->is_child = true; /* 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. */ body = mime_create(((ct && *ct) ? ct : (dct ? dct : TEXT_PLAIN)), mult->hdrs, obj->options, false, mime_child_part_depth(obj), obj); PR_FREEIF(ct); if (!body) return MIME_OUT_OF_MEMORY; status = ((MimeContainerClass*)obj->clazz)->add_child(obj, body); if (status < 0) { mime_free(body); return status; } if (obj->options && obj->options->decompose_file_p && obj->options->is_multipart_msg && obj->options->decompose_file_init_fn) { if (MimeMultipart_should_decompose_child(obj, body)) { status = obj->options->decompose_file_init_fn( obj->options->stream_closure, mult->hdrs); if (status < 0) return status; } } /* Now that we've added this new object to our list of children, start its parser going (if we want to display it.) */ body->output_p = (((MimeMultipartClass*)obj->clazz)->output_child_p(obj, body)); if (body->output_p) { status = body->clazz->parse_begin(body); #ifdef XP_MACOSX /* if we are saving an apple double attachment, we need to set correctly the * content type of the channel */ if (mime_typep(obj, (MimeObjectClass*)&mimeMultipartAppleDoubleClass)) { mime_stream_data* msd = body->options->stream_closure.AsMimeStreamData(); if (msd) { if (!body->options->write_html_p && body->content_type && !PL_strcasecmp(body->content_type, APPLICATION_APPLEFILE)) { if (msd->channel) msd->channel->SetContentType( nsLiteralCString(APPLICATION_APPLEFILE)); } } } #endif if (status < 0) return status; } return 0; } static bool MimeMultipart_output_child_p(MimeObject* obj, MimeObject* child) { /* We don't output a child if we're stripping it. */ if (obj->options && obj->options->state && obj->options->state->strippingPart) return false; /* if we are saving an apple double attachment, ignore the appledouble wrapper * part */ return (obj->options && obj->options->write_html_p) || PL_strcasecmp(child->content_type, MULTIPART_APPLEDOUBLE); } static int MimeMultipart_close_child(MimeObject* object) { MimeMultipart* mult = (MimeMultipart*)object; MimeContainer* cont = (MimeContainer*)object; if (!mult->hdrs) return 0; MimeHeaders_free(mult->hdrs); mult->hdrs = 0; NS_ASSERTION(cont->nchildren > 0, "badly formed mime message"); if (cont->nchildren > 0) { MimeObject* kid = cont->children[cont->nchildren - 1]; // If we have a child and it has not already been closed, process it. // The kid would be already be closed if we encounter a multipart section // that did not have a fully delineated header block. No header block means // no creation of a new child, but the termination case still happens and // we still end up here. Obviously, we don't want to close the child a // second time and the best thing we can do is nothing. if (kid && !kid->closed_p) { int status; status = kid->clazz->parse_eof(kid, false); if (status < 0) return status; status = kid->clazz->parse_end(kid, false); if (status < 0) return status; if (object->options && object->options->decompose_file_p && object->options->is_multipart_msg && object->options->decompose_file_close_fn) { if (MimeMultipart_should_decompose_child(object, kid)) { status = object->options->decompose_file_close_fn( object->options->stream_closure); if (status < 0) return status; } } } } return 0; } static int MimeMultipart_parse_child_line(MimeObject* obj, const char* line, int32_t length, bool first_line_p) { MimeContainer* cont = (MimeContainer*)obj; int status; MimeObject* kid; PR_ASSERT(cont->nchildren > 0); if (cont->nchildren <= 0) return -1; kid = cont->children[cont->nchildren - 1]; PR_ASSERT(kid); if (!kid) return -1; if (obj->options && obj->options->decompose_file_p && obj->options->is_multipart_msg && obj->options->decompose_file_output_fn) { if (MimeMultipart_should_decompose_child(obj, kid)) { return obj->options->decompose_file_output_fn( line, length, obj->options->stream_closure); } } /* The newline issues here are tricky, since both the newlines before and after the boundary string are to be considered part of the boundary: this is so that a part can be specified such that it does not end in a trailing newline. To implement this, we send a newline *before* each line instead of after, except for the first line, which is not preceded by a newline. */ /* Remove the trailing newline... */ if (length > 0 && line[length - 1] == '\n') length--; if (length > 0 && line[length - 1] == '\r') length--; if (!first_line_p) { /* Push out a preceding newline... */ char nl[] = MSG_LINEBREAK; status = kid->clazz->parse_buffer( nl, MSG_LINEBREAK_LEN, MimeClosure(MimeClosure::isMimeObject, kid)); if (status < 0) return status; } /* Now push out the line sans trailing newline. */ return kid->clazz->parse_buffer(line, length, MimeClosure(MimeClosure::isMimeObject, kid)); } static int MimeMultipart_parse_eof(MimeObject* obj, bool abort_p) { MimeMultipart* mult = (MimeMultipart*)obj; MimeContainer* cont = (MimeContainer*)obj; if (obj->closed_p) return 0; /* Push out the last trailing line if there's one in the buffer. If this happens, this object does not end in a trailing newline (and the parse_line method will be called with a string with no trailing newline, which isn't the usual case.) */ if (!abort_p && obj->ibuffer_fp > 0) { /* There is leftover data without a terminating newline. */ int status = obj->clazz->parse_line(obj->ibuffer, obj->ibuffer_fp, obj); obj->ibuffer_fp = 0; if (status < 0) { obj->closed_p = true; return status; } } /* Now call parse_eof for our active child, if there is one. */ if (cont->nchildren > 0 && (mult->state == MimeMultipartPartLine || mult->state == MimeMultipartPartFirstLine)) { MimeObject* kid = cont->children[cont->nchildren - 1]; NS_ASSERTION(kid, "not expecting null kid"); if (kid) { int status = kid->clazz->parse_eof(kid, abort_p); if (status < 0) return status; status = kid->clazz->parse_end(kid, abort_p); if (status < 0) return status; if (obj->options && obj->options->decompose_file_p && obj->options->is_multipart_msg && obj->options->decompose_file_close_fn) { // For malformed messages missing a closing boundary, we must explicitly // flush the last child's decomposition stream at EOF to ensure // attachment integrity. if (MimeMultipart_should_decompose_child(obj, kid)) { status = obj->options->decompose_file_close_fn( obj->options->stream_closure); if (status < 0) return status; } } } } return ((MimeObjectClass*)&MIME_SUPERCLASS)->parse_eof(obj, abort_p); } #if defined(DEBUG) && defined(XP_UNIX) static int MimeMultipart_debug_print(MimeObject* obj, PRFileDesc* stream, int32_t depth) { /* MimeMultipart *mult = (MimeMultipart *) obj; */ MimeContainer* cont = (MimeContainer*)obj; char* addr = mime_part_address(obj); int i; for (i = 0; i < depth; i++) PR_Write(stream, " ", 2); /** fprintf(stream, "<%s %s (%d kid%s) boundary=%s 0x%08X>\n", obj->clazz->class_name, addr ? addr : "???", cont->nchildren, (cont->nchildren == 1 ? "" : "s"), (mult->boundary ? mult->boundary : "(none)"), (uint32_t) mult); **/ PR_FREEIF(addr); /* if (cont->nchildren > 0) fprintf(stream, "\n"); */ for (i = 0; i < cont->nchildren; i++) { MimeObject* kid = cont->children[i]; int status = kid->clazz->debug_print(kid, stream, depth + 1); if (status < 0) return status; } /* if (cont->nchildren > 0) fprintf(stream, "\n"); */ return 0; } #endif #undef MIME_SUPERCLASS