/*
 *
 *    OPEN-XCHANGE legal information
 *
 *    All intellectual property rights in the Software are protected by
 *    international copyright laws.
 *
 *
 *    In some countries OX, OX Open-Xchange, open xchange and OXtender
 *    as well as the corresponding Logos OX Open-Xchange and OX are registered
 *    trademarks of the Open-Xchange, Inc. group of companies.
 *    The use of the Logos is not covered by the GNU General Public License.
 *    Instead, you are allowed to use these Logos according to the terms and
 *    conditions of the Creative Commons License, Version 2.5, Attribution,
 *    Non-commercial, ShareAlike, and the interpretation of the term
 *    Non-commercial applicable to the aforementioned license is published
 *    on the web site http://www.open-xchange.com/EN/legal/index.html.
 *
 *    Please make sure that third-party modules and libraries are used
 *    according to their respective licenses.
 *
 *    Any modifications to this package must retain all copyright notices
 *    of the original copyright holder(s) for the original code used.
 *
 *    After any such modifications, the original and derivative code shall remain
 *    under the copyright of the copyright holder(s) and/or original author(s)per
 *    the Attribution and Assignment Agreement that can be located at
 *    http://www.open-xchange.com/EN/developer/. The contributing author shall be
 *    given Attribution for the derivative code and a license granting use.
 *
 *     Copyright (C) 2004-2011 Open-Xchange, Inc.
 *     Mail: info@open-xchange.com
 *
 *
 *     This program is free software; you can redistribute it and/or modify it
 *     under the terms of the GNU General Public License, Version 2 as published
 *     by the Free Software Foundation.
 *
 *     This program is distributed in the hope that it will be useful, but
 *     WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 *     or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
 *     for more details.
 *
 *     You should have received a copy of the GNU General Public License along
 *     with this program; if not, write to the Free Software Foundation, Inc., 59
 *     Temple Place, Suite 330, Boston, MA 02111-1307 USA
 *
 */

package com.openexchange.mail.structure.parser;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.mail.MessagingException;
import javax.mail.Part;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MailDateFormat;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeUtility;
import org.apache.commons.codec.binary.Base64;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONValue;
import com.openexchange.mail.MailException;
import com.openexchange.mail.MailJSONField;
import com.openexchange.mail.MailListField;
import com.openexchange.mail.config.MailProperties;
import com.openexchange.mail.dataobjects.MailMessage;
import com.openexchange.mail.dataobjects.MailPart;
import com.openexchange.mail.mime.ContentDisposition;
import com.openexchange.mail.mime.ContentType;
import com.openexchange.mail.mime.HeaderName;
import com.openexchange.mail.mime.MIMEDefaultSession;
import com.openexchange.mail.mime.MIMEMailException;
import com.openexchange.mail.mime.MIMEType2ExtMap;
import com.openexchange.mail.mime.MIMETypes;
import com.openexchange.mail.mime.MessageHeaders;
import com.openexchange.mail.mime.ParameterizedHeader;
import com.openexchange.mail.mime.PlainTextAddress;
import com.openexchange.mail.mime.QuotedInternetAddress;
import com.openexchange.mail.mime.converters.MIMEMessageConverter;
import com.openexchange.mail.mime.utils.MIMEMessageUtility;
import com.openexchange.mail.structure.Base64JSONString;
import com.openexchange.mail.structure.StructureHandler;
import com.openexchange.mail.structure.StructureMailMessageParser;
import com.openexchange.mail.utils.CharsetDetector;
import com.openexchange.mail.utils.MessageUtility;
import com.openexchange.mail.uuencode.UUEncodedPart;
import com.openexchange.tools.TimeZoneUtils;
import com.openexchange.tools.regex.MatcherReplacer;
import com.openexchange.tools.stream.UnsynchronizedByteArrayInputStream;
import com.openexchange.tools.stream.UnsynchronizedByteArrayOutputStream;

/**
 * {@link MIMEStructureHandler} - The handler to generate a JSON object reflecting a message's MIME structure.
 * 
 * @author <a href="mailto:thorben.betten@open-xchange.com">Thorben Betten</a>
 */
public final class MIMEStructureHandler implements StructureHandler {

    /**
     * The logger.
     */
    private static final org.apache.commons.logging.Log LOG = org.apache.commons.logging.LogFactory.getLog(MIMEStructureHandler.class);

    private static final MailDateFormat MAIL_DATE_FORMAT;

    static {
        MAIL_DATE_FORMAT = new MailDateFormat();
        MAIL_DATE_FORMAT.setTimeZone(TimeZoneUtils.getTimeZone("GMT"));
    }

    /*-
     * #####################################################################################
     */

    private static final String KEY_ID = MailListField.ID.getKey();

    private static final String KEY_HEADERS = MailJSONField.HEADERS.getKey();

    private static final String BODY = "body";

    private static final String DATA = "data";

    private static final String CONTENT_TRANSFER_ENCODING = "content-transfer-encoding";

    private static final String CONTENT_DISPOSITION = "content-disposition";

    private static final String CONTENT_TYPE = "content-type";

    private final LinkedList<JSONObject> mailJsonObjectQueue;

    private JSONObject currentMailObject;

    private JSONValue currentBodyObject;

    private int multipartCount;

    private final long maxSize;

    // private JSONValue bodyJsonObject;

    private JSONArray userFlags;

    /**
     * Initializes a new {@link MIMEStructureHandler}.
     * 
     * @param maxSize The max. size of a mail part to let its content being inserted as base64 encoded or UTF-8 string.
     */
    public MIMEStructureHandler(final long maxSize) {
        super();
        mailJsonObjectQueue = new LinkedList<JSONObject>();
        mailJsonObjectQueue.addLast((currentMailObject = new JSONObject()));
        this.maxSize = maxSize;
    }

    /**
     * Gets the JSON representation of mail's MIME structure.
     * 
     * @return The JSON representation of mail's MIME structure
     */
    public JSONObject getJSONMailObject() {
        return mailJsonObjectQueue.getFirst();
    }

    private JSONArray getUserFlags() throws JSONException {
        if (null == userFlags) {
            userFlags = new JSONArray();
            currentMailObject.put(MailJSONField.USER.getKey(), userFlags);
        }
        return userFlags;
    }

    private void add2BodyJsonObject(final JSONObject bodyObject) throws JSONException {
        if (null == currentBodyObject) {
            /*
             * Nothing applied before
             */
            currentBodyObject = bodyObject;
            currentMailObject.put(BODY, currentBodyObject);
        } else {
            /*
             * Already applied, turn to an array (if not done before) and append given JSON object
             */
            final JSONValue prev = currentBodyObject;
            final JSONArray jsonArray;
            if (prev.isArray()) {
                jsonArray = (JSONArray) prev;
            } else {
                jsonArray = new JSONArray();
                jsonArray.put(prev);
                currentBodyObject = jsonArray;
                /*
                 * Replace
                 */
                currentMailObject.put(BODY, currentBodyObject);
            }
            jsonArray.put(bodyObject);
        }
    }

    private static final int BUFLEN = 8192;

    public boolean handleAttachment(final MailPart part, final String id) throws MailException {
        addBodyPart(part, id);
        return true;
    }

    public boolean handleColorLabel(final int colorLabel) throws MailException {
        try {
            /*-
             * TODO: Decide whether to add separate "color_label" field or add it to user flags:
             * 
             * Uncomment this for adding to user flags:
             * 
             *   getUserFlags().put(MailMessage.getColorLabelStringValue(colorLabel));
             */
            currentMailObject.put(MailJSONField.COLOR_LABEL.getKey(), colorLabel);
            return true;
        } catch (final JSONException e) {
            throw new MailException(MailException.Code.JSON_ERROR, e, e.getMessage());
        }
    }

    public boolean handleHeaders(final Iterator<Entry<String, String>> iter) throws MailException {
        generateHeadersObject(iter, currentMailObject);
        return true;
    }

    public boolean handleInlineUUEncodedAttachment(final UUEncodedPart part, final String id) throws MailException {
        final String filename = part.getFileName();
        String contentType = MIMETypes.MIME_APPL_OCTET;
        try {
            contentType = MIMEType2ExtMap.getContentType(new File(filename.toLowerCase()).getName()).toLowerCase();
        } catch (final Exception e) {
            final Throwable t =
                new Throwable(new StringBuilder("Unable to fetch content-type for '").append(filename).append("': ").append(e).toString());
            LOG.warn(t.getMessage(), t);
        }
        /*
         * Dummy headers
         */
        final StringBuilder sb = new StringBuilder(64);
        final Map<String, String> headers = new HashMap<String, String>(4);
        final String encodeFN;
        try {
            encodeFN = MimeUtility.encodeText(filename, "UTF-8", "Q");
        } catch (final UnsupportedEncodingException e) {
            throw new MailException(MailException.Code.ENCODING_ERROR, e, e.getMessage());
        }
        headers.put(CONTENT_TYPE, sb.append(contentType).append("; name=").append(encodeFN).toString());
        sb.setLength(0);
        headers.put(CONTENT_DISPOSITION, new StringBuilder(Part.ATTACHMENT).append("; filename=").append(encodeFN).toString());
        headers.put(CONTENT_TRANSFER_ENCODING, "base64");
        /*
         * Add body part
         */
        addBodyPart(part.getFileSize(), new InputStreamProvider() {

            public InputStream getInputStream() throws IOException {
                return part.getInputStream();
            }
        }, new ContentType(contentType), id, headers.entrySet().iterator());
        return true;
    }

    public boolean handleInlineUUEncodedPlainText(final String decodedTextContent, final ContentType contentType, final int size, final String fileName, final String id) throws MailException {
        /*
         * Dummy headers
         */
        final Map<String, String> headers = new HashMap<String, String>(4);
        headers.put(CONTENT_TYPE, "text/plain; charset=UTF-8");
        headers.put(CONTENT_DISPOSITION, Part.INLINE);
        /*
         * Add body part
         */
        addBodyPart(size, new InputStreamProvider() {

            public InputStream getInputStream() throws IOException {
                return new UnsynchronizedByteArrayInputStream(decodedTextContent.getBytes("UTF-8"));
            }
        }, contentType, id, headers.entrySet().iterator());
        return true;
    }

    public boolean handleMultipartStart(final ContentType contentType, final int bodyPartCount, final String id) throws MailException {
        try {
            // Increment
            if (++multipartCount > 1) { // Enqueue nested multipart
                // Create a new mail object
                final JSONObject newMailObject = new JSONObject();
                // Apply new mail object to current mail object's body element
                add2BodyJsonObject(newMailObject);
                // Assign new mail object to current mail object
                currentMailObject = newMailObject;
                mailJsonObjectQueue.addLast(currentMailObject);
                currentBodyObject = null;
                // Add multipart's headers
                final Map<String, String> headers = new HashMap<String, String>(1);
                headers.put(CONTENT_TYPE, contentType.toString());
                generateHeadersObject(headers.entrySet().iterator(), currentMailObject);
            } else {
                /*
                 * Ensure proper content type
                 */
                final JSONObject headers = currentMailObject.getJSONObject(KEY_HEADERS);
                if (null == headers) {
                    // Add multipart's headers
                    final Map<String, String> headersMap = new HashMap<String, String>(1);
                    headersMap.put(CONTENT_TYPE, contentType.toString());
                    generateHeadersObject(headersMap.entrySet().iterator(), currentMailObject);
                } else {
                    // Set content type in existing headers
                    headers.put(CONTENT_TYPE, generateParameterizedHeader(
                        contentType,
                        contentType.getBaseType().toLowerCase(Locale.ENGLISH)));
                }
            }
            return true;
        } catch (final JSONException e) {
            throw new MailException(MailException.Code.JSON_ERROR, e, e.getMessage());
        }
    }

    public boolean handleMultipartEnd() throws MailException {
        // Decrement
        if (--multipartCount > 0) { // Dequeue nested multipart
            // Dequeue
            mailJsonObjectQueue.removeLast();
            currentMailObject = mailJsonObjectQueue.getLast();
            currentBodyObject = (JSONValue) currentMailObject.opt(BODY);
        }
        return true;
    }

    public boolean handleNestedMessage(final MailPart mailPart, final String id) throws MailException {
        try {
            final Object content = mailPart.getContent();
            final MailMessage nestedMail;
            if (content instanceof MailMessage) {
                nestedMail = (MailMessage) content;
            } else if (content instanceof InputStream) {
                try {
                    nestedMail =
                        MIMEMessageConverter.convertMessage(new MimeMessage(MIMEDefaultSession.getDefaultSession(), (InputStream) content));
                } catch (final MessagingException e) {
                    throw MIMEMailException.handleMessagingException(e);
                }
            } else {
                final StringBuilder sb = new StringBuilder(128);
                sb.append("Ignoring nested message.").append(
                    "Cannot handle part's content which should be a RFC822 message according to its content type: ");
                sb.append((null == content ? "null" : content.getClass().getSimpleName()));
                LOG.error(sb.toString());
                return true;
            }
            /*
             * Inner parser
             */
            final MIMEStructureHandler inner = new MIMEStructureHandler(maxSize);
            new StructureMailMessageParser().parseMailMessage(nestedMail, inner, id);
            /*
             * Apply to this
             */
            final JSONObject bodyObject = new JSONObject();
            if (multipartCount > 0) {
                // Put headers
                generateHeadersObject(mailPart.getHeadersIterator(), bodyObject);
                // Put body
                final JSONObject jsonMailObject = inner.getJSONMailObject();
                jsonMailObject.put(KEY_ID, mailPart.containsSequenceId() ? mailPart.getSequenceId() : id);
                bodyObject.put(BODY, jsonMailObject);
            }
            add2BodyJsonObject(bodyObject);
            return true;
        } catch (final JSONException e) {
            throw new MailException(MailException.Code.JSON_ERROR, e, e.getMessage());
        }
    }

    public boolean handleReceivedDate(final Date receivedDate) throws MailException {
        try {
            if (receivedDate == null) {
                currentMailObject.put(MailJSONField.RECEIVED_DATE.getKey(), JSONObject.NULL);
            } else {
                final JSONObject dateObject = new JSONObject();
                dateObject.put("utc", receivedDate.getTime());
                synchronized (MAIL_DATE_FORMAT) {
                    dateObject.put("date", MAIL_DATE_FORMAT.format(receivedDate));
                }
                currentMailObject.put(MailJSONField.RECEIVED_DATE.getKey(), dateObject);
            }
            return true;
        } catch (final JSONException e) {
            throw new MailException(MailException.Code.JSON_ERROR, e, e.getMessage());
        }
    }

    public boolean handleSystemFlags(final int flags) throws MailException {
        try {
            final String key = MailJSONField.FLAGS.getKey();
            if (currentMailObject.hasAndNotNull(key)) {
                final int prevFlags = currentMailObject.getInt(key);
                currentMailObject.put(key, prevFlags | flags);
            } else {
                currentMailObject.put(key, flags);
            }
            return true;
        } catch (final JSONException e) {
            throw new MailException(MailException.Code.JSON_ERROR, e, e.getMessage());
        }
    }

    public boolean handleUserFlags(final String[] userFlags) throws MailException {
        try {
            final JSONArray userFlagsArr = getUserFlags();
            for (final String userFlag : userFlags) {
                userFlagsArr.put(userFlag);
            }
            return true;
        } catch (final JSONException e) {
            throw new MailException(MailException.Code.JSON_ERROR, e, e.getMessage());
        }
    }

    private void addBodyPart(final MailPart part, final String id) throws MailException {
        try {
            final JSONObject bodyObject = new JSONObject();
            if (multipartCount > 0) {
                // Put headers
                final JSONObject headersObject = generateHeadersObject(part.getHeadersIterator(), bodyObject);
                // Put body
                final JSONObject body = new JSONObject();
                fillBodyPart(body, part, headersObject, id);
                bodyObject.put(BODY, body);
            } else {
                // Put direct
                fillBodyPart(bodyObject, part, currentMailObject.getJSONObject(KEY_HEADERS), id);
            }
            add2BodyJsonObject(bodyObject);
        } catch (final JSONException e) {
            throw new MailException(MailException.Code.JSON_ERROR, e, e.getMessage());
        }
    }

    private void addBodyPart(final long size, final InputStreamProvider isp, final ContentType contentType, final String id, final Iterator<Entry<String, String>> iter) throws MailException {
        try {
            final JSONObject bodyObject = new JSONObject();
            if (multipartCount > 0) {
                // Put headers
                final JSONObject headersObject = generateHeadersObject(iter, bodyObject);
                // Put body
                final JSONObject body = new JSONObject();
                fillBodyPart(body, size, isp, contentType, headersObject, id);
                bodyObject.put(BODY, body);
            } else {
                // Put direct
                fillBodyPart(bodyObject, size, isp, contentType, currentMailObject.getJSONObject(KEY_HEADERS), id);
            }
            add2BodyJsonObject(bodyObject);
        } catch (final JSONException e) {
            throw new MailException(MailException.Code.JSON_ERROR, e, e.getMessage());
        }
    }

    private static final String PRIMARY_TEXT = "text/";

    private static final String TEXT_HTML = "text/htm";

    private static final Pattern PAT_META_CT = Pattern.compile("<meta[^>]*?http-equiv=\"?content-type\"?[^>]*?>", Pattern.CASE_INSENSITIVE);

    private void fillBodyPart(final JSONObject bodyObject, final MailPart part, final JSONObject headerObject, final String id) throws MailException {
        try {
            bodyObject.put(KEY_ID, id);
            final long size = part.getSize();
            if (maxSize > 0 && size > maxSize) {
                bodyObject.put(DATA, JSONObject.NULL);
            } else {
                final ContentType contentType = part.getContentType();
                if (contentType.startsWith(PRIMARY_TEXT)) {
                    // Set UTF-8 text
                    if (contentType.startsWith(TEXT_HTML)) {
                        final String html = readContent(part, contentType);
                        final Matcher m = PAT_META_CT.matcher(html);
                        final MatcherReplacer mr = new MatcherReplacer(m, html);
                        final StringBuilder replaceBuffer = new StringBuilder(html.length());
                        if (m.find()) {
                            replaceBuffer.append("<meta content=\"").append(contentType.getBaseType().toLowerCase(Locale.ENGLISH));
                            replaceBuffer.append("; charset=UTF-8\" http-equiv=\"Content-Type\" />");
                            final String replacement = replaceBuffer.toString();
                            replaceBuffer.setLength(0);
                            mr.appendLiteralReplacement(replaceBuffer, replacement);
                        }
                        mr.appendTail(replaceBuffer);
                        bodyObject.put(DATA, replaceBuffer.toString());
                    } else {
                        bodyObject.put(DATA, readContent(part, contentType));
                    }
                    // Set header according to UTF-8 content without transfer-encoding
                    headerObject.remove(CONTENT_TRANSFER_ENCODING);
                    contentType.setCharsetParameter("UTF-8");
                    headerObject.put(CONTENT_TYPE, generateParameterizedHeader(contentType, contentType.getBaseType().toLowerCase(
                        Locale.ENGLISH)));
                } else {
                    fillBase64JSONString(part.getInputStream(), bodyObject, true);
                    // Set Transfer-Encoding to base64
                    headerObject.put(CONTENT_TRANSFER_ENCODING, "base64");
                }
            }
        } catch (final JSONException e) {
            throw new MailException(MailException.Code.JSON_ERROR, e, e.getMessage());
        } catch (final IOException e) {
            throw new MailException(MailException.Code.IO_ERROR, e, e.getMessage());
        }
    }

    private void fillBodyPart(final JSONObject bodyObject, final long size, final InputStreamProvider isp, final ContentType contentType, final JSONObject headerObject, final String id) throws MailException {
        try {
            bodyObject.put(KEY_ID, id);
            if (maxSize > 0 && size > maxSize) {
                bodyObject.put(DATA, JSONObject.NULL);
            } else {
                if (contentType.startsWith(PRIMARY_TEXT)) {
                    // Set UTF-8 text
                    bodyObject.put(DATA, readContent(isp, contentType));
                    // Set header according to utf-8 content without transfer-encoding
                    headerObject.remove(CONTENT_TRANSFER_ENCODING);
                    contentType.setCharsetParameter("UTF-8");
                    headerObject.put(CONTENT_TYPE, generateParameterizedHeader(contentType, contentType.getBaseType().toLowerCase(
                        Locale.ENGLISH)));
                } else {
                    fillBase64JSONString(isp.getInputStream(), bodyObject, true);
                    // Set Transfer-Encoding to base64
                    headerObject.put(CONTENT_TRANSFER_ENCODING, "base64");
                }
            }
        } catch (final JSONException e) {
            throw new MailException(MailException.Code.JSON_ERROR, e, e.getMessage());
        } catch (final IOException e) {
            throw new MailException(MailException.Code.IO_ERROR, e, e.getMessage());
        }
    }

    private static void fillBase64JSONString(final InputStream inputStream, final JSONObject bodyObject, final boolean streaming) throws MailException {
        try {
            if (streaming) {
                bodyObject.put(DATA, new Base64JSONString(inputStream));
            } else {
                final byte[] bytes;
                {
                    try {
                        final byte[] buf = new byte[BUFLEN];
                        final ByteArrayOutputStream out = new UnsynchronizedByteArrayOutputStream(BUFLEN << 2);
                        int read;
                        while ((read = inputStream.read(buf, 0, BUFLEN)) >= 0) {
                            out.write(buf, 0, read);
                        }
                        bytes = out.toByteArray();
                    } catch (final IOException e) {
                        throw new MailException(MailException.Code.IO_ERROR, e, e.getMessage());
                    } finally {
                        if (null != inputStream) {
                            try {
                                inputStream.close();
                            } catch (final IOException e) {
                                LOG.error(e.getMessage(), e);
                            }
                        }
                    }
                }
                // Add own JSONString implementation to support streaming
                bodyObject.put(DATA, new String(Base64.encodeBase64(bytes, false), "US-ASCII"));
            }
        } catch (final UnsupportedEncodingException e) {
            throw new MailException(MailException.Code.ENCODING_ERROR, e, "US-ASCII");
        } catch (final JSONException e) {
            throw new MailException(MailException.Code.JSON_ERROR, e, e.getMessage());
        }
    }

    private static final HeaderName HN_CONTENT_TYPE = HeaderName.valueOf(CONTENT_TYPE);

    private static final HeaderName HN_DATE = HeaderName.valueOf("date");

    private static final Set<HeaderName> PARAMETERIZED_HEADERS =
        new HashSet<HeaderName>(Arrays.asList(HN_CONTENT_TYPE, HeaderName.valueOf(CONTENT_DISPOSITION)));

    private static final Set<HeaderName> ADDRESS_HEADERS =
        new HashSet<HeaderName>(Arrays.asList(
            HeaderName.valueOf("From"),
            HeaderName.valueOf("To"),
            HeaderName.valueOf("Cc"),
            HeaderName.valueOf("Bcc"),
            HeaderName.valueOf("Reply-To"),
            HeaderName.valueOf("Sender"),
            HeaderName.valueOf("Errors-To"),
            HeaderName.valueOf("Resent-Bcc"),
            HeaderName.valueOf("Resent-Cc"),
            HeaderName.valueOf("Resent-From"),
            HeaderName.valueOf("Resent-To"),
            HeaderName.valueOf("Resent-Sender"),
            HeaderName.valueOf("Disposition-Notification-To")));

    private JSONObject generateHeadersObject(final Iterator<Entry<String, String>> iter, final JSONObject parent) throws MailException {
        try {
            final JSONObject hdrObject = new JSONObject();
            while (iter.hasNext()) {
                final Entry<String, String> entry = iter.next();
                final String name = entry.getKey().toLowerCase(Locale.ENGLISH);
                final HeaderName headerName = HeaderName.valueOf(name);
                if (ADDRESS_HEADERS.contains(headerName)) {
                    final InternetAddress[] internetAddresses = getAddressHeader(entry.getValue());
                    final JSONArray ja;
                    if (hdrObject.has(name)) {
                        ja = hdrObject.getJSONArray(name);
                    } else {
                        ja = new JSONArray();
                        hdrObject.put(name, ja);
                    }
                    for (final InternetAddress internetAddress : internetAddresses) {
                        final JSONObject addressJsonObject = new JSONObject();
                        final String personal = internetAddress.getPersonal();
                        if (null != personal) {
                            addressJsonObject.put("personal", personal);
                        }
                        addressJsonObject.put("address", QuotedInternetAddress.toIDN(internetAddress.getAddress()));
                        ja.put(addressJsonObject);
                    }
                } else if (PARAMETERIZED_HEADERS.contains(headerName)) {
                    final JSONObject parameterJsonObject = generateParameterizedHeader(entry.getValue(), headerName);
                    if (hdrObject.has(name)) {
                        final Object previous = hdrObject.get(name);
                        final JSONArray ja;
                        if (previous instanceof JSONArray) {
                            ja = (JSONArray) previous;
                        } else {
                            ja = new JSONArray();
                            hdrObject.put(name, ja);
                            ja.put(previous);
                        }
                        ja.put(parameterJsonObject);
                    } else {
                        hdrObject.put(name, parameterJsonObject);
                    }
                } else if (HN_DATE.equals(headerName)) {
                    hdrObject.put(name, generateDateObject(entry.getValue()));
                } else {
                    if (hdrObject.has(name)) {
                        final Object previous = hdrObject.get(name);
                        final JSONArray ja;
                        if (previous instanceof JSONArray) {
                            ja = (JSONArray) previous;
                        } else {
                            ja = new JSONArray();
                            hdrObject.put(name, ja);
                            ja.put(previous);
                        }
                        ja.put(MIMEMessageUtility.decodeMultiEncodedHeader(entry.getValue()));
                    } else {
                        hdrObject.put(name, MIMEMessageUtility.decodeMultiEncodedHeader(entry.getValue()));
                    }
                }
            }
            parent.put(KEY_HEADERS, hdrObject.length() > 0 ? hdrObject : JSONObject.NULL);
            return hdrObject;
        } catch (final JSONException e) {
            throw new MailException(MailException.Code.JSON_ERROR, e, e.getMessage());
        }
    }

    private JSONObject generateParameterizedHeader(final String value, final HeaderName headerName) throws MailException, JSONException {
        if (HN_CONTENT_TYPE.equals(headerName)) {
            final ContentType ct = new ContentType(value);
            return generateParameterizedHeader(ct, ct.getBaseType().toLowerCase(Locale.ENGLISH));
        }
        final ContentDisposition cd = new ContentDisposition(value);
        return generateParameterizedHeader(cd, cd.getDisposition().toLowerCase(Locale.ENGLISH));
    }

    private JSONObject generateParameterizedHeader(final ParameterizedHeader parameterizedHeader, final String type) throws JSONException {
        final JSONObject parameterJsonObject = new JSONObject();
        parameterJsonObject.put("type", type);
        final JSONObject paramListJsonObject = new JSONObject();
        for (final Iterator<String> pi = parameterizedHeader.getParameterNames(); pi.hasNext();) {
            final String paramName = pi.next();
            if ("read-date".equalsIgnoreCase(paramName)) {
                paramListJsonObject.put(
                    paramName.toLowerCase(Locale.ENGLISH),
                    generateDateObject(parameterizedHeader.getParameter(paramName)));
            } else {
                paramListJsonObject.put(paramName.toLowerCase(Locale.ENGLISH), parameterizedHeader.getParameter(paramName));
            }
        }
        if (paramListJsonObject.length() > 0) {
            parameterJsonObject.put("params", paramListJsonObject);
        }
        return parameterJsonObject;
    }

    private Object generateDateObject(final String date) throws JSONException {
        if (null == date) {
            return JSONObject.NULL;
        }
        final JSONObject dateObject = new JSONObject();
        synchronized (MAIL_DATE_FORMAT) {
            try {
                final Date parsedDate = MAIL_DATE_FORMAT.parse(date);
                if (null != parsedDate) {
                    dateObject.put("utc", parsedDate.getTime());
                }
            } catch (final ParseException pex) {
                LOG.warn("Date string could not be parsed: " + date);
            }
        }
        dateObject.put("date", date);
        return dateObject;
    }

    /**
     * Gets the address headers denoted by specified header name in a safe manner.
     * <p>
     * If strict parsing of address headers yields a {@link AddressException}, then a plain-text version is generated to display broken
     * address header as it is.
     * 
     * @param name The address header name
     * @param message The message providing the address header
     * @return The parsed address headers as an array of {@link InternetAddress} instances
     */
    private static InternetAddress[] getAddressHeader(final String addresses) {
        if (null == addresses || 0 == addresses.length()) {
            return new InternetAddress[0];
        }
        try {
            return QuotedInternetAddress.parseHeader(addresses, true);
        } catch (final AddressException e) {
            return getAddressHeaderNonStrict(addresses);
        }
    }

    private static InternetAddress[] getAddressHeaderNonStrict(final String addressStrings) {
        try {
            final InternetAddress[] addresses = QuotedInternetAddress.parseHeader(addressStrings, false);
            final List<InternetAddress> addressList = new ArrayList<InternetAddress>(addresses.length);
            for (final InternetAddress internetAddress : addresses) {
                try {
                    addressList.add(new QuotedInternetAddress(internetAddress.toString()));
                } catch (final AddressException e) {
                    addressList.add(internetAddress);
                }
            }
            return addressList.toArray(new InternetAddress[addressList.size()]);
        } catch (final AddressException e) {
            if (LOG.isDebugEnabled()) {
                LOG.debug(
                    new StringBuilder(128).append("Internet addresses could not be properly parsed: \"").append(e.getMessage()).append(
                        "\". Using plain addresses' string representation instead.").toString(),
                    e);
            }
            return getAddressesOnParseError(addressStrings);
        }
    }

    private static InternetAddress[] getAddressesOnParseError(final String addr) {
        return new InternetAddress[] { new PlainTextAddress(addr) };
    }

    private static String readContent(final MailPart mailPart, final ContentType contentType) throws MailException, IOException {
        final String charset = getCharset(mailPart, contentType);
        try {
            return MessageUtility.readMailPart(mailPart, charset);
        } catch (final java.io.CharConversionException e) {
            // Obviously charset was wrong or bogus implementation of character conversion
            final String fallback = "US-ASCII";
            if (LOG.isWarnEnabled()) {
                LOG.warn(new StringBuilder("Character conversion exception while reading content with charset \"").append(charset).append(
                    "\". Using fallback charset \"").append(fallback).append("\" instead."), e);
            }
            return MessageUtility.readMailPart(mailPart, fallback);
        }
    }

    private static String getCharset(final MailPart mailPart, final ContentType contentType) throws MailException {
        final String charset;
        if (mailPart.containsHeader(MessageHeaders.HDR_CONTENT_TYPE)) {
            String cs = contentType.getCharsetParameter();
            if (!CharsetDetector.isValid(cs)) {
                StringBuilder sb = null;
                if (null != cs) {
                    sb = new StringBuilder(64).append("Illegal or unsupported encoding: \"").append(cs).append("\".");
                }
                if (contentType.startsWith(PRIMARY_TEXT)) {
                    cs = CharsetDetector.detectCharset(mailPart.getInputStream());
                    if (LOG.isWarnEnabled() && null != sb) {
                        sb.append(" Using auto-detected encoding: \"").append(cs).append('"');
                        LOG.warn(sb.toString());
                    }
                } else {
                    cs = MailProperties.getInstance().getDefaultMimeCharset();
                    if (LOG.isWarnEnabled() && null != sb) {
                        sb.append(" Using fallback encoding: \"").append(cs).append('"');
                        LOG.warn(sb.toString());
                    }
                }
            }
            charset = cs;
        } else {
            if (contentType.startsWith(PRIMARY_TEXT)) {
                charset = CharsetDetector.detectCharset(mailPart.getInputStream());
            } else {
                charset = MailProperties.getInstance().getDefaultMimeCharset();
            }
        }
        return charset;
    }

    private static String readContent(final InputStreamProvider isp, final ContentType contentType) throws IOException {
        final String charset = getCharset(isp, contentType);
        try {
            return MessageUtility.readStream(isp.getInputStream(), charset);
        } catch (final java.io.CharConversionException e) {
            // Obviously charset was wrong or bogus implementation of character conversion
            final String fallback = "US-ASCII";
            if (LOG.isWarnEnabled()) {
                LOG.warn(new StringBuilder("Character conversion exception while reading content with charset \"").append(charset).append(
                    "\". Using fallback charset \"").append(fallback).append("\" instead."), e);
            }
            return MessageUtility.readStream(isp.getInputStream(), fallback);
        }
    }

    private static String getCharset(final InputStreamProvider isp, final ContentType contentType) throws IOException {
        final String charset;
        if (contentType.startsWith(PRIMARY_TEXT)) {
            final String cs = contentType.getCharsetParameter();
            if (!CharsetDetector.isValid(cs)) {
                charset = CharsetDetector.detectCharset(isp.getInputStream());
            } else {
                charset = cs;
            }
        } else {
            charset = MailProperties.getInstance().getDefaultMimeCharset();
        }
        return charset;
    }

    private static interface InputStreamProvider {

        InputStream getInputStream() throws IOException;
    }

}
