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