text: 'use strict';
  
  var libmime = require('libmime');
  var libqp = require('libqp');
  var libbase64 = require('libbase64');
  var punycode = require('punycode');
  var addressparser = require('addressparser');
  var stream = require('stream');
  var PassThrough = stream.PassThrough;
  var fs = require('fs');
  var fetch = require('nodemailer-fetch');
  
  module.exports = MimeNode;
  
  /**
   * Creates a new mime tree node. Assumes 'multipart/*' as the content type
   * if it is a branch, anything else counts as leaf. If rootNode is missing from
   * the options, assumes this is the root.
   *
   * @param {String} contentType Define the content type for the node. Can be left blank for attachments (derived from filename)
   * @param {Object} [options] optional options
   * @param {Object} [options.rootNode] root node for this tree
   * @param {Object} [options.parentNode] immediate parent for this node
   * @param {Object} [options.filename] filename for an attachment node
   * @param {String} [options.baseBoundary] shared part of the unique multipart boundary
   * @param {Boolean} [options.keepBcc] If true, do not exclude Bcc from the generated headers
   * @param {String} [options.textEncoding] either 'Q' (the default) or 'B'
   */
  function MimeNode(contentType, options) {
      this.nodeCounter = 0;
  
      options = options || {};
  
      /**
       * shared part of the unique multipart boundary
       */
      this.baseBoundary = options.baseBoundary || Date.now().toString() + Math.random();
  
      /**
       * If date headers is missing and current node is the root, this value is used instead
       */
      this.date = new Date();
  
      /**
       * Root node for current mime tree
       */
      this.rootNode = options.rootNode || this;
  
      /**
       * If true include Bcc in generated headers (if available)
       */
      this.keepBcc = !!options.keepBcc;
  
      /**
       * If filename is specified but contentType is not (probably an attachment)
       * detect the content type from filename extension
       */
      if (options.filename) {
          /**
           * Filename for this node. Useful with attachments
           */
          this.filename = options.filename;
          if (!contentType) {
              contentType = libmime.detectMimeType(this.filename.split('.').pop());
          }
      }
  
      /**
       * Indicates which encoding should be used for header strings: "Q" or "B"
       */
      this.textEncoding = (options.textEncoding || '').toString().trim().charAt(0).toUpperCase();
  
      /**
       * Immediate parent for this node (or undefined if not set)
       */
      this.parentNode = options.parentNode;
  
      /**
       * An array for possible child nodes
       */
      this.childNodes = [];
  
      /**
       * Used for generating unique boundaries (prepended to the shared base)
       */
      this._nodeId = ++this.rootNode.nodeCounter;
  
      /**
       * A list of header values for this node in the form of [{key:'', value:''}]
       */
      this._headers = [];
  
      /**
       * True if the content only uses ASCII printable characters
       * @type {Boolean}
       */
      this._isPlainText = false;
  
      /**
       * True if the content is plain text but has longer lines than allowed
       * @type {Boolean}
       */
      this._hasLongLines = false;
  
      /**
       * If set, use instead this value for envelopes instead of generating one
       * @type {Boolean}
       */
      this._envelope = false;
  
      /**
       * If set then use this value as the stream content instead of building it
       * @type {String|Buffer|Stream}
       */
      this._raw = false;
  
      /**
       * Additional transform streams that the message will be piped before
       * exposing by createReadStream
       * @type {Array}
       */
      this._transforms = [];
  
      /**
       * If content type is set (or derived from the filename) add it to headers
       */
      if (contentType) {
          this.setHeader('content-type', contentType);
      }
  }
  
  /////// PUBLIC METHODS
  
  /**
   * Creates and appends a child node.Arguments provided are passed to MimeNode constructor
   *
   * @param {String} [contentType] Optional content type
   * @param {Object} [options] Optional options object
   * @return {Object} Created node object
   */
  MimeNode.prototype.createChild = function (contentType, options) {
      if (!options && typeof contentType === 'object') {
          options = contentType;
          contentType = undefined;
      }
      var node = new MimeNode(contentType, options);
      this.appendChild(node);
      return node;
  };
  
  /**
   * Appends an existing node to the mime tree. Removes the node from an existing
   * tree if needed
   *
   * @param {Object} childNode node to be appended
   * @return {Object} Appended node object
   */
  MimeNode.prototype.appendChild = function (childNode) {
  
      if (childNode.rootNode !== this.rootNode) {
          childNode.rootNode = this.rootNode;
          childNode._nodeId = ++this.rootNode.nodeCounter;
      }
  
      childNode.parentNode = this;
  
      this.childNodes.push(childNode);
      return childNode;
  };
  
  /**
   * Replaces current node with another node
   *
   * @param {Object} node Replacement node
   * @return {Object} Replacement node
   */
  MimeNode.prototype.replace = function (node) {
      if (node === this) {
          return this;
      }
  
      this.parentNode.childNodes.forEach(function (childNode, i) {
          if (childNode === this) {
  
              node.rootNode = this.rootNode;
              node.parentNode = this.parentNode;
              node._nodeId = this._nodeId;
  
              this.rootNode = this;
              this.parentNode = undefined;
  
              node.parentNode.childNodes[i] = node;
          }
      }.bind(this));
  
      return node;
  };
  
  /**
   * Removes current node from the mime tree
   *
   * @return {Object} removed node
   */
  MimeNode.prototype.remove = function () {
      if (!this.parentNode) {
          return this;
      }
  
      for (var i = this.parentNode.childNodes.length - 1; i >= 0; i--) {
          if (this.parentNode.childNodes[i] === this) {
              this.parentNode.childNodes.splice(i, 1);
              this.parentNode = undefined;
              this.rootNode = this;
              return this;
          }
      }
  };
  
  /**
   * Sets a header value. If the value for selected key exists, it is overwritten.
   * You can set multiple values as well by using [{key:'', value:''}] or
   * {key: 'value'} as the first argument.
   *
   * @param {String|Array|Object} key Header key or a list of key value pairs
   * @param {String} value Header value
   * @return {Object} current node
   */
  MimeNode.prototype.setHeader = function (key, value) {
      var added = false,
          headerValue;
  
      // Allow setting multiple headers at once
      if (!value && key && typeof key === 'object') {
          // allow {key:'content-type', value: 'text/plain'}
          if (key.key && 'value' in key) {
              this.setHeader(key.key, key.value);
          }
          // allow [{key:'content-type', value: 'text/plain'}]
          else if (Array.isArray(key)) {
              key.forEach(function (i) {
                  this.setHeader(i.key, i.value);
              }.bind(this));
          }
          // allow {'content-type': 'text/plain'}
          else {
              Object.keys(key).forEach(function (i) {
                  this.setHeader(i, key[i]);
              }.bind(this));
          }
          return this;
      }
  
      key = this._normalizeHeaderKey(key);
  
      headerValue = {
          key: key,
          value: value
      };
  
      // Check if the value exists and overwrite
      for (var i = 0, len = this._headers.length; i < len; i++) {
          if (this._headers[i].key === key) {
              if (!added) {
                  // replace the first match
                  this._headers[i] = headerValue;
                  added = true;
              } else {
                  // remove following matches
                  this._headers.splice(i, 1);
                  i--;
                  len--;
              }
          }
      }
  
      // match not found, append the value
      if (!added) {
          this._headers.push(headerValue);
      }
  
      return this;
  };
  
  /**
   * Adds a header value. If the value for selected key exists, the value is appended
   * as a new field and old one is not touched.
   * You can set multiple values as well by using [{key:'', value:''}] or
   * {key: 'value'} as the first argument.
   *
   * @param {String|Array|Object} key Header key or a list of key value pairs
   * @param {String} value Header value
   * @return {Object} current node
   */
  MimeNode.prototype.addHeader = function (key, value) {
  
      // Allow setting multiple headers at once
      if (!value && key && typeof key === 'object') {
          // allow {key:'content-type', value: 'text/plain'}
          if (key.key && key.value) {
              this.addHeader(key.key, key.value);
          }
          // allow [{key:'content-type', value: 'text/plain'}]
          else if (Array.isArray(key)) {
              key.forEach(function (i) {
                  this.addHeader(i.key, i.value);
              }.bind(this));
          }
          // allow {'content-type': 'text/plain'}
          else {
              Object.keys(key).forEach(function (i) {
                  this.addHeader(i, key[i]);
              }.bind(this));
          }
          return this;
      } else if (Array.isArray(value)) {
          value.forEach(function (val) {
              this.addHeader(key, val);
          }.bind(this));
          return this;
      }
  
      this._headers.push({
          key: this._normalizeHeaderKey(key),
          value: value
      });
  
      return this;
  };
  
  /**
   * Retrieves the first mathcing value of a selected key
   *
   * @param {String} key Key to search for
   * @retun {String} Value for the key
   */
  MimeNode.prototype.getHeader = function (key) {
      key = this._normalizeHeaderKey(key);
      for (var i = 0, len = this._headers.length; i < len; i++) {
          if (this._headers[i].key === key) {
              return this._headers[i].value;
          }
      }
  };
  
  /**
   * Sets body content for current node. If the value is a string, charset is added automatically
   * to Content-Type (if it is text/*). If the value is a Buffer, you need to specify
   * the charset yourself
   *
   * @param (String|Buffer) content Body content
   * @return {Object} current node
   */
  MimeNode.prototype.setContent = function (content) {
      var _self = this;
      this.content = content;
      if (typeof this.content.pipe === 'function') {
          // pre-stream handler. might be triggered if a stream is set as content
          // and 'error' fires before anything is done with this stream
          this._contentErrorHandler = function (err) {
              _self.content.removeListener('error', _self._contentErrorHandler);
              _self.content = err;
          };
          this.content.once('error', this._contentErrorHandler);
      } else if (typeof this.content === 'string') {
          this._isPlainText = libmime.isPlainText(this.content);
          if (this._isPlainText && libmime.hasLongerLines(this.content, 76)) {
              // If there are lines longer than 76 symbols/bytes do not use 7bit
              this._hasLongLines = true;
          }
      }
      return this;
  };
  
  MimeNode.prototype.build = function (callback) {
      var stream = this.createReadStream();
      var buf = [];
      var buflen = 0;
      var returned = false;
  
      stream.on('readable', function () {
          var chunk;
  
          while ((chunk = stream.read()) !== null) {
              buf.push(chunk);
              buflen += chunk.length;
          }
      });
  
      stream.once('error', function (err) {
          if (returned) {
              return;
          }
          returned = true;
  
          return callback(err);
      });
  
      stream.once('end', function (chunk) {
          if (returned) {
              return;
          }
          returned = true;
  
          if (chunk && chunk.length) {
              buf.push(chunk);
              buflen += chunk.length;
          }
          return callback(null, Buffer.concat(buf, buflen));
      });
  };
  
  MimeNode.prototype.getTransferEncoding = function () {
      var transferEncoding = false;
      var contentType = (this.getHeader('Content-Type') || '').toString().toLowerCase().trim();
  
      if (this.content) {
          transferEncoding = (this.getHeader('Content-Transfer-Encoding') || '').toString().toLowerCase().trim();
          if (!transferEncoding || ['base64', 'quoted-printable'].indexOf(transferEncoding) < 0) {
              if (/^text\//i.test(contentType)) {
                  // If there are no special symbols, no need to modify the text
                  if (this._isPlainText && !this._hasLongLines) {
                      transferEncoding = '7bit';
                  } else if (typeof this.content === 'string' || this.content instanceof Buffer) {
                      // detect preferred encoding for string value
                      transferEncoding = this._getTextEncoding(this.content) === 'Q' ? 'quoted-printable' : 'base64';
                  } else {
                      // we can not check content for a stream, so either use preferred encoding or fallback to QP
                      transferEncoding = this.transferEncoding === 'B' ? 'base64' : 'quoted-printable';
                  }
              } else if (!/^multipart\//i.test(contentType)) {
                  transferEncoding = transferEncoding || 'base64';
              }
          }
      }
      return transferEncoding;
  };
  
  /**
   * Builds the header block for the mime node. Append \r\n\r\n before writing the content
   *
   * @returns {String} Headers
   */
  MimeNode.prototype.buildHeaders = function () {
      var _self = this;
      var transferEncoding = this.getTransferEncoding();
      var headers = [];
  
      if (transferEncoding) {
          this.setHeader('Content-Transfer-Encoding', transferEncoding);
      }
  
      if (this.filename && !this.getHeader('Content-Disposition')) {
          this.setHeader('Content-Disposition', 'attachment');
      }
  
      // Ensure mandatory header fields
      if (this.rootNode === this) {
          if (!this.getHeader('Date')) {
              this.setHeader('Date', this.date.toUTCString().replace(/GMT/, '+0000'));
          }
  
          // ensure that Message-Id is present
          this.messageId();
  
          if (!this.getHeader('MIME-Version')) {
              this.setHeader('MIME-Version', '1.0');
          }
      }
  
      this._headers.forEach(function (header) {
          var key = header.key;
          var value = header.value;
          var structured;
          var param;
          var options = {};
          var formattedHeaders = ['From', 'Sender', 'To', 'Cc', 'Bcc', 'Reply-To', 'Date', 'References'];
  
          if (value && formattedHeaders.indexOf(key) < 0 && typeof value === 'object') {
              Object.keys(value).forEach(function (key) {
                  if (key !== 'value') {
                      options[key] = value[key];
                  }
              });
              value = (value.value || '').toString();
              if (!value.trim()) {
                  return;
              }
          }
  
          if (options.prepared) {
              // header value is
              headers.push(key + ': ' + value);
              return;
          }
  
          switch (header.key) {
              case 'Content-Disposition':
                  structured = libmime.parseHeaderValue(value);
                  if (_self.filename) {
                      structured.params.filename = _self.filename;
                  }
                  value = libmime.buildHeaderValue(structured);
                  break;
              case 'Content-Type':
                  structured = libmime.parseHeaderValue(value);
  
                  _self._handleContentType(structured);
  
                  if (structured.value.match(/^text\/plain\b/) && typeof _self.content === 'string' && /[\u0080-\uFFFF]/.test(_self.content)) {
                      structured.params.charset = 'utf-8';
                  }
  
                  value = libmime.buildHeaderValue(structured);
  
                  if (_self.filename) {
                      // add support for non-compliant clients like QQ webmail
                      // we can't build the value with buildHeaderValue as the value is non standard and
                      // would be converted to parameter continuation encoding that we do not want
                      param = this._encodeWords(_self.filename);
                      if (param !== _self.filename || /[\s"=;]/.test(param)) {
                          // include value in quotes if needed
                          param = '"' + param + '"';
                      }
                      value += '; name=' + param;
                  }
                  break;
              case 'Bcc':
                  if (!_self.keepBcc) {
                      // skip BCC values
                      return;
                  }
                  break;
          }
  
          value = _self._encodeHeaderValue(key, value);
  
          // skip empty lines
          if (!(value || '').toString().trim()) {
              return;
          }
  
          headers.push(libmime.foldLines(key + ': ' + value, 76));
      }.bind(this));
  
      return headers.join('\r\n');
  };
  
  /**
   * Streams the rfc2822 message from the current node. If this is a root node,
   * mandatory header fields are set if missing (Date, Message-Id, MIME-Version)
   *
   * @return {String} Compiled message
   */
  MimeNode.prototype.createReadStream = function (options) {
      options = options || {};
  
      var outputStream = new PassThrough(options);
      var transform;
  
      this.stream(outputStream, options, function (err) {
          if (err) {
              outputStream.emit('error', err);
              return;
          }
          outputStream.end();
      });
  
      for (var i = 0, len = this._transforms.length; i < len; i++) {
          transform = typeof this._transforms[i] === 'function' ? this._transforms[i]() : this._transforms[i];
          outputStream.once('error', function (err) {
              transform.emit('error', err);
          });
          outputStream = outputStream.pipe(transform);
      }
  
      return outputStream;
  };
  
  /**
   * Appends a transform stream object to the transforms list. Final output
   * is passed through this stream before exposing
   *
   * @param {Object} transform Read-Write stream
   */
  MimeNode.prototype.transform = function (transform) {
      this._transforms.push(transform);
  };
  
  MimeNode.prototype.stream = function (outputStream, options, done) {
      var _self = this;
      var transferEncoding = this.getTransferEncoding();
      var contentStream;
      var localStream;
  
      // protect actual callback against multiple triggering
      var returned = false;
      var callback = function (err) {
          if (returned) {
              return;
          }
          returned = true;
          done(err);
      };
  
      // pushes node content
      function sendContent() {
          if (_self.content) {
  
              if (Object.prototype.toString.call(_self.content) === '[object Error]') {
                  // content is already errored
                  return callback(_self.content);
              }
  
              if (typeof _self.content.pipe === 'function') {
                  _self.content.removeListener('error', _self._contentErrorHandler);
                  _self._contentErrorHandler = function (err) {
                      return callback(err);
                  };
                  _self.content.once('error', _self._contentErrorHandler);
              }
  
              if (['quoted-printable', 'base64'].indexOf(transferEncoding) >= 0) {
                  contentStream = new(transferEncoding === 'base64' ? libbase64 : libqp).Encoder(options);
  
                  contentStream.pipe(outputStream, {
                      end: false
                  });
                  contentStream.once('end', finalize);
                  contentStream.once('error', function (err) {
                      return callback(err);
                  });
  
                  localStream = _self._getStream(_self.content);
                  localStream.pipe(contentStream);
              } else {
                  // anything that is not QP or Base54 passes as-is
                  localStream = _self._getStream(_self.content);
                  localStream.pipe(outputStream, {
                      end: false
                  });
                  localStream.once('end', finalize);
              }
  
              localStream.once('error', function (err) {
                  return callback(err);
              });
  
              return;
          } else {
              return setImmediate(finalize);
          }
      }
  
      // for multipart nodes, push child nodes
      // for content nodes end the stream
      function finalize() {
          var childId = 0;
          var processChildNode = function () {
              if (childId >= _self.childNodes.length) {
                  outputStream.write('\r\n--' + _self.boundary + '--\r\n');
                  return callback();
              }
              var child = _self.childNodes[childId++];
              outputStream.write((childId > 1 ? '\r\n' : '') + '--' + _self.boundary + '\r\n');
              child.stream(outputStream, options, function (err) {
                  if (err) {
                      return callback(err);
                  }
                  setImmediate(processChildNode);
              });
          };
  
          if (_self.multipart) {
              setImmediate(processChildNode);
          } else {
              return callback();
          }
      }
  
      if (this._raw) {
          setImmediate(function () {
              if (Object.prototype.toString.call(_self._raw) === '[object Error]') {
                  // content is already errored
                  return callback(_self._raw);
              }
  
              // remove default error handler (if set)
              if (typeof _self._raw.pipe === 'function') {
                  _self._raw.removeListener('error', _self._contentErrorHandler);
              }
  
              var raw = _self._getStream(_self._raw);
              raw.pipe(outputStream, {
                  end: false
              });
              raw.on('error', function (err) {
                  outputStream.emit('error', err);
              });
              raw.on('end', finalize);
          });
      } else {
          outputStream.write(this.buildHeaders() + '\r\n\r\n');
          setImmediate(sendContent);
      }
  };
  
  /**
   * Sets envelope to be used instead of the generated one
   *
   * @return {Object} SMTP envelope in the form of {from: 'from@example.com', to: ['to@example.com']}
   */
  MimeNode.prototype.setEnvelope = function (envelope) {
      var list;
  
      this._envelope = {
          from: false,
          to: []
      };
  
      if (envelope.from) {
          list = [];
          this._convertAddresses(this._parseAddresses(envelope.from), list);
          list = list.filter(function (address) {
              return address && address.address;
          });
          if (list.length && list[0]) {
              this._envelope.from = list[0].address;
          }
      }
      ['to', 'cc', 'bcc'].forEach(function (key) {
          if (envelope[key]) {
              this._convertAddresses(this._parseAddresses(envelope[key]), this._envelope.to);
          }
      }.bind(this));
  
      this._envelope.to = this._envelope.to.map(function (to) {
          return to.address;
      }).filter(function (address) {
          return address;
      });
  
      return this;
  };
  
  /**
   * Generates and returns an object with parsed address fields
   *
   * @return {Object} Address object
   */
  MimeNode.prototype.getAddresses = function () {
      var addresses = {};
  
      this._headers.forEach(function (header) {
          var key = header.key.toLowerCase();
          if (['from', 'sender', 'reply-to', 'to', 'cc', 'bcc'].indexOf(key) >= 0) {
              if (!Array.isArray(addresses[key])) {
                  addresses[key] = [];
              }
  
              this._convertAddresses(this._parseAddresses(header.value), addresses[key]);
          }
      }.bind(this));
  
      return addresses;
  };
  
  /**
   * Generates and returns SMTP envelope with the sender address and a list of recipients addresses
   *
   * @return {Object} SMTP envelope in the form of {from: 'from@example.com', to: ['to@example.com']}
   */
  MimeNode.prototype.getEnvelope = function () {
      if (this._envelope) {
          return this._envelope;
      }
  
      var envelope = {
          from: false,
          to: []
      };
      this._headers.forEach(function (header) {
          var list = [];
          if (header.key === 'From' || (!envelope.from && ['Reply-To', 'Sender'].indexOf(header.key) >= 0)) {
              this._convertAddresses(this._parseAddresses(header.value), list);
              if (list.length && list[0]) {
                  envelope.from = list[0].address;
              }
          } else if (['To', 'Cc', 'Bcc'].indexOf(header.key) >= 0) {
              this._convertAddresses(this._parseAddresses(header.value), envelope.to);
          }
      }.bind(this));
  
      envelope.to = envelope.to.map(function (to) {
          return to.address;
      });
  
      return envelope;
  };
  
  /**
   * Returns Message-Id value. If it does not exist, then creates one
   *
   * @return {String} Message-Id value
   */
  MimeNode.prototype.messageId = function () {
      var messageId = this.getHeader('Message-Id');
      // You really should define your own Message-Id field!
      if (!messageId) {
          messageId = '<' +
              // crux to generate random strings like this:
              // "1401391905590-58aa8c32-d32a065c-c1a2aad2"
              [0, 0, 0].reduce(function (prev) {
                  return prev + '-' + Math.floor((1 + Math.random()) * 0x100000000).
                  toString(16).
                  substring(1);
              }, Date.now()) +
              '@' +
              // try to use the domain of the FROM address or fallback localhost
              (this.getEnvelope().from || 'localhost').split('@').pop() +
              '>';
          this.setHeader('Message-Id', messageId);
      }
  
      return messageId;
  };
  
  /**
   * Sets pregenerated content that will be used as the output of this node
   *
   * @param {String|Buffer|Stream} Raw MIME contents
   */
  MimeNode.prototype.setRaw = function (raw) {
      var _self = this;
  
      this._raw = raw;
  
      if (this._raw && typeof this._raw.pipe === 'function') {
          // pre-stream handler. might be triggered if a stream is set as content
          // and 'error' fires before anything is done with this stream
          this._contentErrorHandler = function (err) {
              _self._raw.removeListener('error', _self._contentErrorHandler);
              _self._raw = err;
          };
          _self._raw.once('error', this._contentErrorHandler);
      }
  
      return this;
  };
  
  /////// PRIVATE METHODS
  
  /**
   * Detects and returns handle to a stream related with the content.
   *
   * @param {Mixed} content Node content
   * @returns {Object} Stream object
   */
  MimeNode.prototype._getStream = function (content) {
      var contentStream;
  
      if (typeof content.pipe === 'function') {
          // assume as stream
          return content;
      } else if (content && typeof content.path === 'string' && !content.href) {
          // read file
          return fs.createReadStream(content.path);
      } else if (content && typeof content.href === 'string') {
          // fetch URL
          return fetch(content.href);
      } else {
          // pass string or buffer content as a stream
          contentStream = new PassThrough();
          setImmediate(function () {
              contentStream.end(content || '');
          });
          return contentStream;
      }
  };
  
  /**
   * Parses addresses. Takes in a single address or an array or an
   * array of address arrays (eg. To: [[first group], [second group],...])
   *
   * @param {Mixed} addresses Addresses to be parsed
   * @return {Array} An array of address objects
   */
  MimeNode.prototype._parseAddresses = function (addresses) {
      return [].concat.apply([], [].concat(addresses).map(function (address) {
          if (address && address.address) {
              address = this._convertAddresses(address);
          }
          return addressparser(address);
      }.bind(this)));
  };
  
  /**
   * Normalizes a header key, uses Camel-Case form, except for uppercase MIME-
   *
   * @param {String} key Key to be normalized
   * @return {String} key in Camel-Case form
   */
  MimeNode.prototype._normalizeHeaderKey = function (key) {
      return (key || '').toString().
          // no newlines in keys
      replace(/\r?\n|\r/g, ' ').
      trim().toLowerCase().
          // use uppercase words, except MIME
      replace(/^MIME\b|^[a-z]|\-[a-z]/ig, function (c) {
          return c.toUpperCase();
      });
  };
  
  /**
   * Checks if the content type is multipart and defines boundary if needed.
   * Doesn't return anything, modifies object argument instead.
   *
   * @param {Object} structured Parsed header value for 'Content-Type' key
   */
  MimeNode.prototype._handleContentType = function (structured) {
      this.contentType = structured.value.trim().toLowerCase();
  
      this.multipart = this.contentType.split('/').reduce(function (prev, value) {
          return prev === 'multipart' ? value : false;
      });
  
      if (this.multipart) {
          this.boundary = structured.params.boundary = structured.params.boundary || this.boundary || this._generateBoundary();
      } else {
          this.boundary = false;
      }
  };
  
  /**
   * Generates a multipart boundary value
   *
   * @return {String} boundary value
   */
  MimeNode.prototype._generateBoundary = function () {
      return '----sinikael-?=_' + this._nodeId + '-' + this.rootNode.baseBoundary;
  };
  
  /**
   * Encodes a header value for use in the generated rfc2822 email.
   *
   * @param {String} key Header key
   * @param {String} value Header value
   */
  MimeNode.prototype._encodeHeaderValue = function (key, value) {
      key = this._normalizeHeaderKey(key);
  
      switch (key) {
  
          // Structured headers
          case 'From':
          case 'Sender':
          case 'To':
          case 'Cc':
          case 'Bcc':
          case 'Reply-To':
              return this._convertAddresses(this._parseAddresses(value));
  
              // values enclosed in <>
          case 'Message-Id':
          case 'In-Reply-To':
          case 'Content-Id':
              value = (value || '').toString().replace(/\r?\n|\r/g, ' ');
  
              if (value.charAt(0) !== '<') {
                  value = '<' + value;
              }
  
              if (value.charAt(value.length - 1) !== '>') {
                  value = value + '>';
              }
              return value;
  
              // space separated list of values enclosed in <>
          case 'References':
              value = [].concat.apply([], [].concat(value || '').map(function (elm) {
                  elm = (elm || '').toString().replace(/\r?\n|\r/g, ' ').trim();
                  return elm.replace(/<[^>]*>/g, function (str) {
                      return str.replace(/\s/g, '');
                  }).split(/\s+/);
              })).map(function (elm) {
                  if (elm.charAt(0) !== '<') {
                      elm = '<' + elm;
                  }
                  if (elm.charAt(elm.length - 1) !== '>') {
                      elm = elm + '>';
                  }
                  return elm;
              });
  
              return value.join(' ').trim();
  
          case 'Date':
              if (Object.prototype.toString.call(value) === '[object Date]') {
                  return value.toUTCString().replace(/GMT/, '+0000');
              }
  
              value = (value || '').toString().replace(/\r?\n|\r/g, ' ');
              return this._encodeWords(value);
  
          default:
              value = (value || '').toString().replace(/\r?\n|\r/g, ' ');
              // encodeWords only encodes if needed, otherwise the original string is returned
              return this._encodeWords(value);
      }
  };
  
  /**
   * Rebuilds address object using punycode and other adjustments
   *
   * @param {Array} addresses An array of address objects
   * @param {Array} [uniqueList] An array to be populated with addresses
   * @return {String} address string
   */
  MimeNode.prototype._convertAddresses = function (addresses, uniqueList) {
      var values = [];
  
      uniqueList = uniqueList || [];
  
      [].concat(addresses || []).forEach(function (address) {
          if (address.address) {
              address.address = address.address.replace(/@.+$/, function (domain) {
                  // usernames are not touched and are kept as is even if these include unicode
                  // domains are punycoded by default
                  // 'jõgeva.ee' will be converted to 'xn--jgeva-dua.ee'
                  // non-unicode domains are left as is
                  return '@' + punycode.toASCII(domain.substr(1));
              });
  
              if (!address.name) {
                  values.push(address.address);
              } else if (address.name) {
                  values.push(this._encodeAddressName(address.name) + ' <' + address.address + '>');
              }
  
              if (address.address) {
                  if (!uniqueList.filter(
                          function (a) {
                              return a.address === address.address;
                          }).length) {
                      uniqueList.push(address);
                  }
              }
          } else if (address.group) {
              values.push(this._encodeAddressName(address.name) + ':' + (address.group.length ? this._convertAddresses(address.group, uniqueList) : '').trim() + ';');
          }
      }.bind(this));
  
      return values.join(', ');
  };
  
  /**
   * If needed, mime encodes the name part
   *
   * @param {String} name Name part of an address
   * @returns {String} Mime word encoded string if needed
   */
  MimeNode.prototype._encodeAddressName = function (name) {
      if (!/^[\w ']*$/.test(name)) {
          if (/^[\x20-\x7e]*$/.test(name)) {
              return '"' + name.replace(/([\\"])/g, '\\$1') + '"';
          } else {
              return libmime.encodeWord(name, this._getTextEncoding(name), 52);
          }
      }
      return name;
  };
  
  /**
   * If needed, mime encodes the name part
   *
   * @param {String} name Name part of an address
   * @returns {String} Mime word encoded string if needed
   */
  MimeNode.prototype._encodeWords = function (value) {
      return libmime.encodeWords(value, this._getTextEncoding(value), 52);
  };
  
  /**
   * Detects best mime encoding for a text value
   *
   * @param {String} value Value to check for
   * @return {String} either 'Q' or 'B'
   */
  MimeNode.prototype._getTextEncoding = function (value) {
      value = (value || '').toString();
  
      var encoding = this.textEncoding;
      var latinLen;
      var nonLatinLen;
  
      if (!encoding) {
          // count latin alphabet symbols and 8-bit range symbols + control symbols
          // if there are more latin characters, then use quoted-printable
          // encoding, otherwise use base64
          nonLatinLen = (value.match(/[\x00-\x08\x0B\x0C\x0E-\x1F\u0080-\uFFFF]/g) || []).length;
          latinLen = (value.match(/[a-z]/gi) || []).length;
          // if there are more latin symbols than binary/unicode, then prefer Q, otherwise B
          encoding = nonLatinLen < latinLen ? 'Q' : 'B';
      }
      return encoding;
  };
  