1/* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5const EXPORTED_SYMBOLS = [ 6 "MsgHdrToMimeMessage", 7 "MimeMessage", 8 "MimeContainer", 9 "MimeBody", 10 "MimeUnknown", 11 "MimeMessageAttachment", 12]; 13 14const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); 15 16/** 17 * The URL listener is surplus because the CallbackStreamListener ends up 18 * getting the same set of events, effectively. 19 */ 20var dumbUrlListener = { 21 OnStartRunningUrl(aUrl) {}, 22 OnStopRunningUrl(aUrl, aExitCode) {}, 23}; 24 25/** 26 * Maintain a list of all active stream listeners so that we can cancel them all 27 * during shutdown. If we don't cancel them, we risk calls into javascript 28 * from C++ after the various XPConnect contexts have already begun their 29 * teardown process. 30 */ 31var activeStreamListeners = {}; 32 33var shutdownCleanupObserver = { 34 _initialized: false, 35 ensureInitialized() { 36 if (this._initialized) { 37 return; 38 } 39 40 Services.obs.addObserver(this, "quit-application"); 41 42 this._initialized = true; 43 }, 44 45 observe(aSubject, aTopic, aData) { 46 if (aTopic == "quit-application") { 47 Services.obs.removeObserver(this, "quit-application"); 48 49 for (let uri in activeStreamListeners) { 50 let streamListener = activeStreamListeners[uri]; 51 if (streamListener._request) { 52 streamListener._request.cancel(Cr.NS_BINDING_ABORTED); 53 } 54 } 55 } 56 }, 57}; 58 59function CallbackStreamListener(aMsgHdr, aCallbackThis, aCallback) { 60 this._msgHdr = aMsgHdr; 61 let hdrURI = aMsgHdr.folder.getUriForMsg(aMsgHdr); 62 this._request = null; 63 this._stream = null; 64 if (aCallback === undefined) { 65 this._callbacksThis = [null]; 66 this._callbacks = [aCallbackThis]; 67 } else { 68 this._callbacksThis = [aCallbackThis]; 69 this._callbacks = [aCallback]; 70 } 71 activeStreamListeners[hdrURI] = this; 72} 73 74CallbackStreamListener.prototype = { 75 QueryInterface: ChromeUtils.generateQI(["nsIStreamListener"]), 76 77 // nsIRequestObserver part 78 onStartRequest(aRequest) { 79 this._request = aRequest; 80 }, 81 onStopRequest(aRequest, aStatusCode) { 82 let msgURI = this._msgHdr.folder.getUriForMsg(this._msgHdr); 83 delete activeStreamListeners[msgURI]; 84 85 aRequest.QueryInterface(Ci.nsIChannel); 86 let message = MsgHdrToMimeMessage.RESULT_RENDEVOUZ[aRequest.URI.spec]; 87 if (message === undefined) { 88 message = null; 89 } 90 91 delete MsgHdrToMimeMessage.RESULT_RENDEVOUZ[aRequest.URI.spec]; 92 93 for (let i = 0; i < this._callbacksThis.length; i++) { 94 try { 95 this._callbacks[i].call(this._callbacksThis[i], this._msgHdr, message); 96 } catch (e) { 97 // Most of the time, exceptions will silently disappear into the endless 98 // deeps of XPConnect, and never reach the surface ever again. At least 99 // warn the user if he has dump enabled. 100 dump( 101 "The MsgHdrToMimeMessage callback threw an exception: " + e + "\n" 102 ); 103 // That one will probably never make it to the original caller. 104 throw e; 105 } 106 } 107 108 this._msgHdr = null; 109 this._request = null; 110 this._stream = null; 111 this._callbacksThis = null; 112 this._callbacks = null; 113 }, 114 115 /* okay, our onDataAvailable should actually never be called. the stream 116 converter is actually eating everything except the start and stop 117 notification. */ 118 // nsIStreamListener part 119 onDataAvailable(aRequest, aInputStream, aOffset, aCount) { 120 dump("this should not be happening! arrgggggh!\n"); 121 console.error( 122 "Did you try to stream an nttp message? Doens't work. See bug 545365." 123 ); 124 if (this._stream === null) { 125 this._stream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance( 126 Ci.nsIScriptableInputStream 127 ); 128 this._stream.init(aInputStream); 129 } 130 this._stream.read(aCount); 131 }, 132}; 133 134var gMessenger = Cc["@mozilla.org/messenger;1"].createInstance(Ci.nsIMessenger); 135 136function stripEncryptedParts(aPart) { 137 if (aPart.parts && aPart.isEncrypted) { 138 aPart.parts = []; // Show an empty container. 139 } else if (aPart.parts) { 140 aPart.parts = aPart.parts.map(stripEncryptedParts); 141 } 142 return aPart; 143} 144 145/** 146 * Starts retrieval of a MimeMessage instance for the given message header. 147 * Your callback will be called with the message header you provide and the 148 * 149 * @param aMsgHdr The message header to retrieve the body for and build a MIME 150 * representation of the message. 151 * @param aCallbackThis The (optional) 'this' to use for your callback function. 152 * @param aCallback The callback function to invoke on completion of message 153 * parsing or failure. The first argument passed will be the nsIMsgDBHdr 154 * you passed to this function. The second argument will be the MimeMessage 155 * instance resulting from the processing on success, and null on failure. 156 * @param [aAllowDownload=false] Should we allow the message to be downloaded 157 * for this streaming request? The default is false, which means that we 158 * require that the message be available offline. If false is passed and 159 * the message is not available offline, we will propagate an exception 160 * thrown by the underlying code. 161 * @param [aOptions] Optional options. 162 * @param [aOptions.saneBodySize] Limit body sizes to a 'reasonable' size in 163 * order to combat corrupt offline/message stores creating pathological 164 * situations where we have erroneously multi-megabyte messages. This 165 * also likely reduces the impact of legitimately ridiculously large 166 * messages. 167 * @param [aOptions.partsOnDemand] If this is a message stored on an IMAP 168 * server, and for whatever reason, it isn't available locally, then setting 169 * this option to true will make sure that attachments aren't downloaded. 170 * This makes sure the message is available quickly. 171 * @param [aOptions.examineEncryptedParts] By default, we won't reveal the 172 * contents of multipart/encrypted parts to the consumers, unless explicitly 173 * requested. In the case of MIME/PGP messages, for instance, the message 174 * will appear as an empty multipart/encrypted container, unless this option 175 * is used. 176 */ 177function MsgHdrToMimeMessage( 178 aMsgHdr, 179 aCallbackThis, 180 aCallback, 181 aAllowDownload, 182 aOptions 183) { 184 shutdownCleanupObserver.ensureInitialized(); 185 186 let requireOffline = !aAllowDownload; 187 188 let msgURI = aMsgHdr.folder.getUriForMsg(aMsgHdr); 189 let msgService = gMessenger.messageServiceFromURI(msgURI); 190 191 MsgHdrToMimeMessage.OPTION_TUNNEL = aOptions; 192 let partsOnDemandStr = 193 aOptions && aOptions.partsOnDemand ? "&fetchCompleteMessage=false" : ""; 194 // By default, Enigmail only decrypts a message streamed via libmime if it's 195 // the one currently on display in the message reader. With this option, we're 196 // letting Enigmail know that it should decrypt the message since the client 197 // explicitly asked for it. 198 let encryptedStr = 199 aOptions && aOptions.examineEncryptedParts 200 ? "&examineEncryptedParts=true" 201 : ""; 202 203 // S/MIME, our other encryption backend, is not that smart, and always 204 // decrypts data. In order to protect sensitive data (e.g. not index it in 205 // Gloda), unless the client asked for encrypted data, we pass to the client 206 // callback a stripped-down version of the MIME structure where encrypted 207 // parts have been removed. 208 let wrapCallback = function(aCallback, aCallbackThis) { 209 if (aOptions && aOptions.examineEncryptedParts) { 210 return aCallback; 211 } 212 return (aMsgHdr, aMimeMsg) => 213 aCallback.call(aCallbackThis, aMsgHdr, stripEncryptedParts(aMimeMsg)); 214 }; 215 216 // Apparently there used to be an old syntax where the callback was the second 217 // argument... 218 let callback = aCallback ? aCallback : aCallbackThis; 219 let callbackThis = aCallback ? aCallbackThis : null; 220 221 // if we're already streaming this msg, just add the callback 222 // to the listener. 223 let listenerForURI = activeStreamListeners[msgURI]; 224 if (listenerForURI != undefined) { 225 listenerForURI._callbacks.push(wrapCallback(callback, callbackThis)); 226 listenerForURI._callbacksThis.push(callbackThis); 227 return; 228 } 229 let streamListener = new CallbackStreamListener( 230 aMsgHdr, 231 callbackThis, 232 wrapCallback(callback, callbackThis) 233 ); 234 235 try { 236 msgService.streamMessage( 237 msgURI, 238 streamListener, // consumer 239 null, // nsIMsgWindow 240 dumbUrlListener, // nsIUrlListener 241 true, // have them create the converter 242 // additional uri payload, note that "header=" is prepended automatically 243 "filter&emitter=js" + partsOnDemandStr + encryptedStr, 244 requireOffline 245 ); 246 } catch (ex) { 247 // If streamMessage throws an exception, we should make sure to clear the 248 // activeStreamListener, or any subsequent attempt at sreaming this URI 249 // will silently fail 250 if (activeStreamListeners[msgURI]) { 251 delete activeStreamListeners[msgURI]; 252 } 253 MsgHdrToMimeMessage.OPTION_TUNNEL = null; 254 throw ex; 255 } 256 257 MsgHdrToMimeMessage.OPTION_TUNNEL = null; 258} 259 260/** 261 * Let the jsmimeemitter provide us with results. The poor emitter (if I am 262 * understanding things correctly) is evaluated outside of the C.u.import 263 * world, so if we were to import him, we would not see him, but rather a new 264 * copy of him. This goes for his globals, etc. (and is why we live in this 265 * file right here). Also, it appears that the XPCOM JS wrappers aren't 266 * magically unified so that we can try and pass data as expando properties 267 * on things like the nsIUri instances either. So we have the jsmimeemitter 268 * import us and poke things into RESULT_RENDEVOUZ. We put it here on this 269 * function to try and be stealthy and avoid polluting the namespaces (or 270 * encouraging bad behaviour) of our importers. 271 * 272 * If you can come up with a prettier way to shuttle this data, please do. 273 */ 274MsgHdrToMimeMessage.RESULT_RENDEVOUZ = {}; 275/** 276 * Cram rich options here for the MimeMessageEmitter to grab from. We 277 * leverage the known control-flow to avoid needing a whole dictionary here. 278 * We set this immediately before constructing the emitter and clear it 279 * afterwards. Control flow is never yielded during the process and reentrancy 280 * cannot happen via any other means. 281 */ 282MsgHdrToMimeMessage.OPTION_TUNNEL = null; 283 284var HeaderHandlerBase = { 285 /** 286 * Look-up a header that should be present at most once. 287 * 288 * @param aHeaderName The header name to retrieve, case does not matter. 289 * @param aDefaultValue The value to return if the header was not found, null 290 * if left unspecified. 291 * @return the value of the header if present, and the default value if not 292 * (defaults to null). If the header was present multiple times, the first 293 * instance of the header is returned. Use getAll if you want all of the 294 * values for the multiply-defined header. 295 */ 296 get(aHeaderName, aDefaultValue) { 297 if (aDefaultValue === undefined) { 298 aDefaultValue = null; 299 } 300 let lowerHeader = aHeaderName.toLowerCase(); 301 if (lowerHeader in this.headers) { 302 // we require that the list cannot be empty if present 303 return this.headers[lowerHeader][0]; 304 } 305 return aDefaultValue; 306 }, 307 /** 308 * Look-up a header that can be present multiple times. Use get for headers 309 * that you only expect to be present at most once. 310 * 311 * @param aHeaderName The header name to retrieve, case does not matter. 312 * @return An array containing the values observed, which may mean a zero 313 * length array. 314 */ 315 getAll(aHeaderName) { 316 let lowerHeader = aHeaderName.toLowerCase(); 317 if (lowerHeader in this.headers) { 318 return this.headers[lowerHeader]; 319 } 320 return []; 321 }, 322 /** 323 * @param aHeaderName Header name to test for its presence. 324 * @return true if the message has (at least one value for) the given header 325 * name. 326 */ 327 has(aHeaderName) { 328 let lowerHeader = aHeaderName.toLowerCase(); 329 return lowerHeader in this.headers; 330 }, 331 _prettyHeaderString(aIndent) { 332 if (aIndent === undefined) { 333 aIndent = ""; 334 } 335 let s = ""; 336 for (let header in this.headers) { 337 let values = this.headers[header]; 338 s += "\n " + aIndent + header + ": " + values; 339 } 340 return s; 341 }, 342}; 343 344/** 345 * @ivar partName The MIME part, ex "1.2.2.1". The partName of a (top-level) 346 * message is "1", its first child is "1.1", its second child is "1.2", 347 * its first child's first child is "1.1.1", etc. 348 * @ivar headers Maps lower-cased header field names to a list of the values 349 * seen for the given header. Use get or getAll as convenience helpers. 350 * @ivar parts The list of the MIME part children of this message. Children 351 * will be either MimeMessage instances, MimeMessageAttachment instances, 352 * MimeContainer instances, or MimeUnknown instances. The latter two are 353 * the result of limitations in the Javascript representation generation 354 * at this time, combined with the need to most accurately represent the 355 * MIME structure. 356 */ 357function MimeMessage() { 358 this.partName = null; 359 this.headers = {}; 360 this.parts = []; 361 this.isEncrypted = false; 362} 363 364MimeMessage.prototype = { 365 __proto__: HeaderHandlerBase, 366 contentType: "message/rfc822", 367 368 /** 369 * @return a list of all attachments contained in this message and all its 370 * sub-messages. Only MimeMessageAttachment instances will be present in 371 * the list (no sub-messages). 372 */ 373 get allAttachments() { 374 let results = []; // messages are not attachments, don't include self 375 for (let iChild = 0; iChild < this.parts.length; iChild++) { 376 let child = this.parts[iChild]; 377 results = results.concat(child.allAttachments); 378 } 379 return results; 380 }, 381 382 /** 383 * @return a list of all attachments contained in this message, with 384 * included/forwarded messages treated as real attachments. Attachments 385 * contained in inner messages won't be shown. 386 */ 387 get allUserAttachments() { 388 if (this.url) { 389 // The jsmimeemitter camouflaged us as a MimeAttachment 390 return [this]; 391 } 392 return this.parts 393 .map(child => child.allUserAttachments) 394 .reduce((a, b) => a.concat(b), []); 395 }, 396 397 /** 398 * @return the total size of this message, that is, the size of all subparts 399 */ 400 get size() { 401 return this.parts 402 .map(child => child.size) 403 .reduce((a, b) => a + Math.max(b, 0), 0); 404 }, 405 406 /** 407 * In the case of attached messages, libmime considers them as attachments, 408 * and if the body is, say, quoted-printable encoded, then libmime will start 409 * counting bytes and notify the js mime emitter about it. The JS mime emitter 410 * being a nice guy, it will try to set a size on us. While this is the 411 * expected behavior for MimeMsgAttachments, we must make sure we can handle 412 * that (failing to write a setter results in exceptions being thrown). 413 */ 414 set size(whatever) { 415 // nop 416 }, 417 418 /** 419 * @param aMsgFolder A message folder, any message folder. Because this is 420 * a hack. 421 * @return The concatenation of all of the body parts where parts 422 * available as text/plain are pulled as-is, and parts only available 423 * as text/html are converted to plaintext form first. In other words, 424 * if we see a multipart/alternative with a text/plain, we take the 425 * text/plain. If we see a text/html without an alternative, we convert 426 * that to text. 427 */ 428 coerceBodyToPlaintext(aMsgFolder) { 429 let bodies = []; 430 for (let part of this.parts) { 431 // an undefined value for something not having the method is fine 432 let body = 433 part.coerceBodyToPlaintext && part.coerceBodyToPlaintext(aMsgFolder); 434 if (body) { 435 bodies.push(body); 436 } 437 } 438 if (bodies) { 439 return bodies.join(""); 440 } 441 return ""; 442 }, 443 444 /** 445 * Convert the message and its hierarchy into a "pretty string". The message 446 * and each MIME part get their own line. The string never ends with a 447 * newline. For a non-multi-part message, only a single line will be 448 * returned. 449 * Messages have their subject displayed, attachments have their filename and 450 * content-type (ex: image/jpeg) displayed. "Filler" classes simply have 451 * their class displayed. 452 */ 453 prettyString(aVerbose, aIndent, aDumpBody) { 454 if (aIndent === undefined) { 455 aIndent = ""; 456 } 457 let nextIndent = aIndent + " "; 458 459 let s = 460 "Message " + 461 (this.isEncrypted ? "[encrypted] " : "") + 462 "(" + 463 this.size + 464 " bytes): " + 465 "subject" in 466 this.headers 467 ? this.headers.subject 468 : ""; 469 if (aVerbose) { 470 s += this._prettyHeaderString(nextIndent); 471 } 472 473 for (let iPart = 0; iPart < this.parts.length; iPart++) { 474 let part = this.parts[iPart]; 475 s += 476 "\n" + 477 nextIndent + 478 (iPart + 1) + 479 " " + 480 part.prettyString(aVerbose, nextIndent, aDumpBody); 481 } 482 483 return s; 484 }, 485}; 486 487/** 488 * @ivar contentType The content-type of this container. 489 * @ivar parts The parts held by this container. These can be instances of any 490 * of the classes found in this file. 491 */ 492function MimeContainer(aContentType) { 493 this.partName = null; 494 this.contentType = aContentType; 495 this.headers = {}; 496 this.parts = []; 497 this.isEncrypted = false; 498} 499 500MimeContainer.prototype = { 501 __proto__: HeaderHandlerBase, 502 get allAttachments() { 503 let results = []; 504 for (let iChild = 0; iChild < this.parts.length; iChild++) { 505 let child = this.parts[iChild]; 506 results = results.concat(child.allAttachments); 507 } 508 return results; 509 }, 510 get allUserAttachments() { 511 return this.parts 512 .map(child => child.allUserAttachments) 513 .reduce((a, b) => a.concat(b), []); 514 }, 515 get size() { 516 return this.parts 517 .map(child => child.size) 518 .reduce((a, b) => a + Math.max(b, 0), 0); 519 }, 520 set size(whatever) { 521 // nop 522 }, 523 coerceBodyToPlaintext(aMsgFolder) { 524 if (this.contentType == "multipart/alternative") { 525 let htmlPart; 526 // pick the text/plain if we can find one, otherwise remember the HTML one 527 for (let part of this.parts) { 528 if (part.contentType == "text/plain") { 529 return part.body; 530 } 531 if (part.contentType == "text/html") { 532 htmlPart = part; 533 } else if (!htmlPart && part.contentType == "text/enriched") { 534 // text/enriched gets transformed into HTML, so use it if we don't 535 // already have an HTML part. 536 htmlPart = part; 537 } 538 } 539 // convert the HTML part if we have one 540 if (htmlPart) { 541 return aMsgFolder.convertMsgSnippetToPlainText(htmlPart.body); 542 } 543 } 544 // if it's not alternative, recurse/aggregate using MimeMessage logic 545 return MimeMessage.prototype.coerceBodyToPlaintext.call(this, aMsgFolder); 546 }, 547 prettyString(aVerbose, aIndent, aDumpBody) { 548 let nextIndent = aIndent + " "; 549 550 let s = 551 "Container " + 552 (this.isEncrypted ? "[encrypted] " : "") + 553 "(" + 554 this.size + 555 " bytes): " + 556 this.contentType; 557 if (aVerbose) { 558 s += this._prettyHeaderString(nextIndent); 559 } 560 561 for (let iPart = 0; iPart < this.parts.length; iPart++) { 562 let part = this.parts[iPart]; 563 s += 564 "\n" + 565 nextIndent + 566 (iPart + 1) + 567 " " + 568 part.prettyString(aVerbose, nextIndent, aDumpBody); 569 } 570 571 return s; 572 }, 573 toString() { 574 return "Container: " + this.contentType; 575 }, 576}; 577 578/** 579 * @class Represents a body portion that we understand and do not believe to be 580 * a proper attachment. This means text/plain or text/html and it has no 581 * filename. (A filename suggests an attachment.) 582 * 583 * @ivar contentType The content type of this body materal; text/plain or 584 * text/html. 585 * @ivar body The actual body content. 586 */ 587function MimeBody(aContentType) { 588 this.partName = null; 589 this.contentType = aContentType; 590 this.headers = {}; 591 this.body = ""; 592 this.isEncrypted = false; 593} 594 595MimeBody.prototype = { 596 __proto__: HeaderHandlerBase, 597 get allAttachments() { 598 return []; // we are a leaf 599 }, 600 get allUserAttachments() { 601 return []; // we are a leaf 602 }, 603 get size() { 604 return this.body.length; 605 }, 606 set size(whatever) { 607 // nop 608 }, 609 appendBody(aBuf) { 610 this.body += aBuf; 611 }, 612 coerceBodyToPlaintext(aMsgFolder) { 613 if (this.contentType == "text/plain") { 614 return this.body; 615 } 616 // text/enriched gets transformed into HTML by libmime 617 if ( 618 this.contentType == "text/html" || 619 this.contentType == "text/enriched" 620 ) { 621 return aMsgFolder.convertMsgSnippetToPlainText(this.body); 622 } 623 return ""; 624 }, 625 prettyString(aVerbose, aIndent, aDumpBody) { 626 let s = 627 "Body: " + 628 (this.isEncrypted ? "[encrypted] " : "") + 629 "" + 630 this.contentType + 631 " (" + 632 this.body.length + 633 " bytes" + 634 (aDumpBody ? ": '" + this.body + "'" : "") + 635 ")"; 636 if (aVerbose) { 637 s += this._prettyHeaderString(aIndent + " "); 638 } 639 return s; 640 }, 641 toString() { 642 return "Body: " + this.contentType + " (" + this.body.length + " bytes)"; 643 }, 644}; 645 646/** 647 * @class A MIME Leaf node that doesn't have a filename so we assume it's not 648 * intended to be an attachment proper. This is probably meant for inline 649 * display or is the result of someone amusing themselves by composing messages 650 * by hand or a bad client. This class should probably be renamed or we should 651 * introduce a better named class that we try and use in preference to this 652 * class. 653 * 654 * @ivar contentType The content type of this part. 655 */ 656function MimeUnknown(aContentType) { 657 this.partName = null; 658 this.contentType = aContentType; 659 this.headers = {}; 660 // Looks like libmime does not always interpret us as an attachment, which 661 // means we'll have to have a default size. Returning undefined would cause 662 // the recursive size computations to fail. 663 this._size = 0; 664 this.isEncrypted = false; 665 // We want to make sure MimeUnknown has a part property: S/MIME encrypted 666 // messages have a topmost MimeUnknown part, with the encrypted bit set to 1, 667 // and we need to ensure all other encrypted parts are children of this 668 // topmost part. 669 this.parts = []; 670} 671 672MimeUnknown.prototype = { 673 __proto__: HeaderHandlerBase, 674 get allAttachments() { 675 return this.parts 676 .map(child => child.allAttachments) 677 .reduce((a, b) => a.concat(b), []); 678 }, 679 get allUserAttachments() { 680 return this.parts 681 .map(child => child.allUserAttachments) 682 .reduce((a, b) => a.concat(b), []); 683 }, 684 get size() { 685 return ( 686 this._size + 687 this.parts 688 .map(child => child.size) 689 .reduce((a, b) => a + Math.max(b, 0), 0) 690 ); 691 }, 692 set size(aSize) { 693 this._size = aSize; 694 }, 695 prettyString(aVerbose, aIndent, aDumpBody) { 696 let nextIndent = aIndent + " "; 697 698 let s = 699 "Unknown: " + 700 (this.isEncrypted ? "[encrypted] " : "") + 701 "" + 702 this.contentType + 703 " (" + 704 this.size + 705 " bytes)"; 706 if (aVerbose) { 707 s += this._prettyHeaderString(aIndent + " "); 708 } 709 710 for (let iPart = 0; iPart < this.parts.length; iPart++) { 711 let part = this.parts[iPart]; 712 s += 713 "\n" + 714 nextIndent + 715 (iPart + 1) + 716 " " + 717 (part ? part.prettyString(aVerbose, nextIndent, aDumpBody) : "NULL"); 718 } 719 return s; 720 }, 721 toString() { 722 return "Unknown: " + this.contentType; 723 }, 724}; 725 726/** 727 * @class An attachment proper. We think it's an attachment because it has a 728 * filename that libmime was able to figure out. 729 * 730 * @ivar partName @see{MimeMessage.partName} 731 * @ivar name The filename of this attachment. 732 * @ivar contentType The MIME content type of this part. 733 * @ivar url The URL to stream if you want the contents of this part. 734 * @ivar isExternal Is the attachment stored someplace else than in the message? 735 * @ivar size The size of the attachment if available, -1 otherwise (size is set 736 * after initialization by jsmimeemitter.js) 737 */ 738function MimeMessageAttachment( 739 aPartName, 740 aName, 741 aContentType, 742 aUrl, 743 aIsExternal 744) { 745 this.partName = aPartName; 746 this.name = aName; 747 this.contentType = aContentType; 748 this.url = aUrl; 749 this.isExternal = aIsExternal; 750 this.headers = {}; 751 this.isEncrypted = false; 752 // parts is copied over from the part instance that preceded us 753 // headers is copied over from the part instance that preceded us 754 // isEncrypted is copied over from the part instance that preceded us 755} 756 757MimeMessageAttachment.prototype = { 758 __proto__: HeaderHandlerBase, 759 // This is a legacy property. 760 get isRealAttachment() { 761 return true; 762 }, 763 get allAttachments() { 764 return [this]; // we are a leaf, so just us. 765 }, 766 get allUserAttachments() { 767 return [this]; 768 }, 769 prettyString(aVerbose, aIndent, aDumpBody) { 770 let s = 771 "Attachment " + 772 (this.isEncrypted ? "[encrypted] " : "") + 773 "(" + 774 this.size + 775 " bytes): " + 776 this.name + 777 ", " + 778 this.contentType; 779 if (aVerbose) { 780 s += this._prettyHeaderString(aIndent + " "); 781 } 782 return s; 783 }, 784 toString() { 785 return this.prettyString(false, ""); 786 }, 787}; 788