/*
 * Copyright 2006-2007 Queplix Corp.
 *
 * Licensed under the Queplix Public License, Version 1.1.1 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.queplix.com/solutions/commercial-open-source/queplix-public-license/
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 *
 */
package com.queplix.core.modules.inbox.utils;

import com.queplix.core.error.GenericSystemException;
import com.queplix.core.modules.inbox.InboxHelper;
import com.queplix.core.modules.inbox.InboxMessage;
import com.queplix.core.modules.mail.Attachment;
import com.queplix.core.modules.mail.MailAddress;
import com.queplix.core.modules.mail.MailMessage;
import com.queplix.core.utils.DateHelper;
import com.queplix.core.utils.StringHelper;
import com.queplix.core.utils.SystemHelper;
import org.apache.regexp.RE;
import org.apache.regexp.RESyntaxException;

import javax.mail.Address;
import javax.mail.Header;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Multipart;
import javax.mail.Part;
import javax.mail.Session;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMessage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.Date;
import java.util.Enumeration;
import java.util.List;
import java.util.TimeZone;

/**
 * The class realizes default message builder.
 *
 * @author Konstantin Mironov
 * @since 8 Dec 2006
 */
public class DefaultInboxMsgBuilder
        extends AbstractInboxPluggableModule implements InboxMsgBuilder {

    // ----------------------------------------------------- variables

    protected InboxProvider provider;
    protected int uniqueID = 0;

    // ----------------------------------------------------- public methods

    /*
     * No javadoc.
     * @see InboxMsgBuilder#setInboxProvider
     */

    public void setInboxProvider(InboxProvider provider) {
        this.provider = provider;
    }

    /*
     * No javadoc.
     * @see InboxMsgBuilder#build
     */
    public InboxMessage build(Message message)
            throws IOException, MessagingException {
        return build(message, false);
    }

    // ----------------------------------------------------- protected methods

    /**
     * Builds <code>InboxMessage</code> from <code>message</code>.
     *
     * @param message        Message
     * @param isAttachedMail true -- message is attachmed e-mail
     * @return InboxMessage
     * @throws IOException
     * @throws MessagingException
     */
    protected InboxMessage build(Message message, boolean isAttachedMail)
            throws IOException, MessagingException {

        //
        // Create InboxMessage.
        //

        // 1. Dates.
        Date sentDate = message.getSentDate();
        if(sentDate != null) {
            sentDate = new Date(DateHelper.toSystem(sentDate.getTime(),
                    TimeZone.getDefault()));
        }
        Date receiveDate = provider.getServerDate(message);

        // 2. 'From' address.
        MailAddress fromMa = null;
        Address[] froms = message.getFrom();
        if(froms != null) {
            InternetAddress from = (InternetAddress) froms[0];
            DEBUG(logName + "'From' - " + from.getAddress());
            fromMa = new MailAddress(from);
        }

        // 3. 'To' address.
        MailAddress[] toMas = null;
        Address[] tos = message.getRecipients(Message.RecipientType.TO);
        if(tos != null) {
            toMas = new MailAddress[tos.length];
            for(int i = 0; i < tos.length; i++) {
                InternetAddress to = (InternetAddress) tos[i];
                DEBUG(logName + "'To' [" + i + "]: " + to.getAddress());

                toMas[i] = new MailAddress(to);
            }
        }

        // 4. 'Cc' address.
        Address[] ccs = message.getRecipients(Message.RecipientType.CC);
        MailAddress[] ccMas = null;
        if(ccs != null) {
            ccMas = new MailAddress[ccs.length];
            for(int i = 0; i < ccs.length; i++) {
                InternetAddress cc = (InternetAddress) ccs[i];
                DEBUG(logName + "'Cc' [" + i + "]: " + cc.getAddress());

                ccMas[i] = new MailAddress(cc);
            }
        }

        // 5. Subject.
        String subject = message.getSubject();
        DEBUG(logName + "Subject: " + subject);

        // Do create.
        InboxMessage msg = new InboxMessage(toMas,
                fromMa,
                subject,
                null);
        msg.setSentTime(sentDate);
        msg.setReceiveTime(receiveDate);
        msg.setCc(ccMas);

        // Add message headers.
        for(Enumeration en = message.getAllHeaders(); en.hasMoreElements();) {
            Header header = (Header) en.nextElement();
            String name = header.getName();
            String value = header.getValue();
            msg.addHeader(name, value);
        }

        // Add UID (only for non-attached mails).
        if(!isAttachedMail) {
            if(provider == null) {
                throw new NullPointerException("Inbox provider not set!");
            }
            String uid = provider.getUID(message);
            if(uid != null) {
                msg.setMessageUid(uid);
            }
            DEBUG(logName + "UID: " + uid);
        }

        // Parse message body.
        parseMessage(msg, message, 0);

        // set message digest
        String messageDigest = InboxHelper.getMessageDigest(msg);
        if(messageDigest.length() > 0) {
            msg.setMessageDigest(messageDigest);
        }

        return msg;

    }

    /**
     * Parse mail part <code>messagePart</code> and build structure.
     * <p/>
     * Algorithm:
     * <p/>
     * 0. begin
     * 1. if multipart/alternative -> in cycle check content-type for
     * sub parts and set text using rules described #6
     * (if content-type = null, then treat it as text/plain)
     * 2. if multipart/+ -> in cycle call #0 for every sub part
     * 3. if message/rfc822 -> call #0 and pass
     * new InboxMessage instance, merge two instances of InboxMessage
     * 4. if Part.getFileName != null && Part.getContentID != null -> treat it as attachment
     * 5. if Part.getFileName != null -> treat it as attachment
     * 6. if text not initalized yet && if text/+ -> treat it as text
     * 7. end
     *
     * @param msg         InboxMessage
     * @param messagePart mail part
     * @param depth       current depth (for multipart parts)
     * @throws MessagingException
     * @throws IOException
     */
    protected void parseMessage(InboxMessage msg, Part messagePart, int depth)
            throws MessagingException, IOException {

        // Get content
        Object content = messagePart.getContent();

        // Get content type.
        String contentType = getContentType(messagePart);

        DEBUG(logName + "Message content type - " + contentType);
        DEBUG(logName + "Message content size - " + messagePart.getSize());
        DEBUG(logName + "Message content is part - "
                + (content instanceof Part));
        DEBUG(logName + "Message content is multi part - "
                + (content instanceof Multipart));
        DEBUG(logName + "Depth - " + depth);

        // If multipart message ->
        if(content != null && content instanceof Multipart) {
            Multipart messageMultiPart = (Multipart) content;
            int countParts = messageMultiPart.getCount();

            DEBUG(logName + "Found multipart message. Parts: " + countParts);

            // 1. Check "multipart/alternative".
            //      .. for "depth" > 0 only
            if(depth > 0 && contentType.indexOf(
                    InboxHelper.CONTENT_TYPE_ALTERNATIVE) > -1) {

                DEBUG(logName + "Found alternative part. Content type: "
                        + contentType);

                // 1.1 Looking for HTML part
                for(int i = 0; i < countParts; i++) {
                    Part partBodyPart = messageMultiPart.getBodyPart(i);
                    String partContentType = partBodyPart.getContentType();
                    if(partContentType != null &&
                            partContentType.toUpperCase().indexOf(
                                    InboxHelper.CONTENT_TYPE_HTML) > -1) {

                        // HTML found!
                        String body = getMessageBody(partBodyPart,
                                InboxHelper.CONTENT_TYPE_HTML);
                        msg.setBody(body);

                        DEBUG(logName
                                + "Found HTML alternative sub part. Content type: "
                                + partContentType);

                        return;
                    }
                }

                // 1.2 Looking for Text part
                for(int i = 0; i < countParts; i++) {
                    Part partBodyPart = messageMultiPart.getBodyPart(i);
                    String partContentType = partBodyPart.getContentType();
                    if(partContentType == null ||
                            partContentType.toUpperCase().indexOf(
                                    InboxHelper.CONTENT_TYPE_PLAIN) > -1) {

                        // Text found!
                        String body = getMessageBody(partBodyPart,
                                InboxHelper.CONTENT_TYPE_PLAIN);
                        msg.setBody(body);

                        DEBUG(logName
                                + "Found Text alternative sub part. Content type: "
                                + partContentType);

                        return;
                    }
                }

                // Text part not found. Go to #2!
            }

            // 2. For other Multiparts in cycle call #parseMessage for every sub part
            for(int i = 0; i < countParts; i++) {
                Part nextPart = messageMultiPart.getBodyPart(i);
                parseMessage(msg, nextPart, depth + 1);
            }
            return;

        } //-- end if multipart message

        //
        // Process non-multipart part.
        //

        // 3. Check "message/rfc822"
        if(contentType.equalsIgnoreCase(InboxHelper.CONTENT_TYPE_MESSAGE)) {

            DEBUG(logName + "Found 'message/rfc822' part. Content type: "
                    + contentType);

            Session session = Session.getDefaultInstance(System.getProperties(),
                    null);
            MimeMessage attachMimeMessage = new MimeMessage(session,
                    messagePart.getInputStream());

            InboxMessage attachMsg = build(attachMimeMessage, true);
            appendMessage(msg, attachMsg);
            return;
        }

        Part filePart = null;
        if(content != null && content instanceof Part) {
            filePart = (Part) content;
        } else {
            filePart = messagePart;
        }
        String fileName = filePart.getFileName();

        if(fileName != null) {

            DEBUG(logName + "Found attachment part. File name: " + fileName);

            // 4. Process attachment
            Attachment attach = getMessageAttachment(filePart);
            if(attach != null) {
                msg.addAttachments(attach);
            }

        } else {

            DEBUG(logName + "Found text part. Content type: " + filePart
                    .getContentType());

            // 5. Process message.

            String body = getMessageBody(filePart);
            if(body != null) {
                msg.setBody(body);
            }

        }
    }

    /**
     * The method gets message body from the email message
     *
     * @param messagePart Part
     * @return String
     * @throws IOException
     * @throws MessagingException
     */
    protected String getMessageBody(Part messagePart)
            throws IOException, MessagingException {

        String contentType = getContentType(messagePart);
        if(contentType.indexOf(InboxHelper.CONTENT_TYPE_HTML) > -1) {
            return getMessageBody(messagePart, InboxHelper.CONTENT_TYPE_HTML);
        } else if(contentType.indexOf(InboxHelper.CONTENT_TYPE_PLAIN) > -1) {
            return getMessageBody(messagePart, InboxHelper.CONTENT_TYPE_PLAIN);
        } else if(contentType.indexOf(InboxHelper.CONTENT_TYPE_TEXT) > -1) {
            return getMessageBody(messagePart, InboxHelper.CONTENT_TYPE_TEXT);
        } else {
            return null;
        }
    }

    /**
     * The method gets message body from the email message
     *
     * @param messagePart           Part
     * @param recognizedContentType it can be:
     *                              InboxHelper.CONTENT_TYPE_HTML or
     *                              InboxHelper.CONTENT_TYPE_PLAIN or
     *                              InboxHelper.CONTENT_TYPE_TEXT
     * @return String
     * @throws IOException
     * @throws MessagingException
     */
    protected String getMessageBody(Part messagePart,
                                    String recognizedContentType)
            throws IOException, MessagingException {

        if(recognizedContentType == null) {
            throw new NullPointerException("Got NULL recognized content type");
        }

        // serialize Part into temp buffer
        InputStream is = messagePart.getInputStream();
        ByteArrayOutputStream bosAtt = new ByteArrayOutputStream();
        byte buf[] = new byte[1024];
        int len;
        while((len = is.read(buf)) > 0) {
            bosAtt.write(buf, 0, len);
        }

        // get code page from Content Type string
        String contentType = getContentType(messagePart);
        String charsetValue = getCodePage(contentType);

        // build mail body using character set
        String mailBody = null;
        if(!StringHelper.isEmpty(charsetValue)) {
            try {
                mailBody = new String(bosAtt.toByteArray(), charsetValue);
            } catch (UnsupportedEncodingException e) {
                // Log problem with content type!
                String msg = "Cant't process the mail body for charset \"" +
                        charsetValue + "\". Content type is \"" + contentType
                        + "\"";

                INFO(logName + msg);
                publisher.INFO(msg);
            }
        }
        if(mailBody == null) {
            // build mail body without character set
            mailBody = new String(bosAtt.toByteArray());
        }

        // additional check for html body
        if(InboxHelper.CONTENT_TYPE_HTML.equals(recognizedContentType)) {

            if(!StringHelper.isHTML(mailBody)) {
                if(isHTMLContent(mailBody)) {
                    mailBody = checkUpHtml(mailBody);
                } else {
                    mailBody = StringHelper.text2html(mailBody);
                }
            }

            // Build safe HTML.
            mailBody = "<html><body>" + StringHelper.clearHtml(mailBody, true)
                    + "</body></html>";
        }

        return mailBody;
    }

    /**
     * The method builds <code>Attachment</code> using <code>filePart</code>.
     *
     * @param filePart Part
     * @return Attachment
     * @throws IOException
     * @throws MessagingException
     */
    protected Attachment getMessageAttachment(Part filePart)
            throws IOException, MessagingException {

        int size = filePart.getSize();
        if(size <= 0) {
            WARN(logName + "Bad attachment found");
            return null;
        }

        // Get file name.
        String fileName = filePart.getFileName();

        // Try to find Content-ID.
        String contentId = null;
        if(filePart instanceof MimeBodyPart) {
            contentId = ((MimeBodyPart) filePart).getContentID();
        }
        if(contentId != null) {
            fileName = InboxHelper.CONTENT_ID_CAPTION + contentId;
        }

        // Check file name.
        if(StringHelper.isEmpty(fileName)) {
            fileName = generateUniqueAttachName();
        }

        DEBUG(logName + "Attachment " +
                "\n\t\t size = " + size +
                "\n\t\t file name = " + fileName +
                "\n\t\t content-type = " + filePart.getContentType());

        // Setting up I/O streams for attachment data retrieving.
        ByteArrayOutputStream out = new ByteArrayOutputStream(size);
        InputStream in = filePart.getInputStream();

        // Go!
        // Writing each byte from the input stream to the buffer.
        int i;
        byte[] buff = new byte[1024];
        while((i = in.read(buff)) != -1) {
            out.write(buff, 0, i);
        }

        // Creating a value object for the attachment...
        out.flush();
        Attachment attach = new Attachment(fileName, out.toByteArray());
        out.close();

        // Set content type.
        attach.setFiletype(filePart.getContentType());

        return attach;
    }

    /**
     * The method appends attached message <code>attachMesage</code> to the end of
     * body of <code>mainMessage</code>. The result is always in HTML format!
     *
     * @param mainMsg   the main message
     * @param attachMsg attached message
     * @return true if the message as attachment was saved correctly
     * @throws MessagingException
     */
    protected boolean appendMessage(InboxMessage mainMsg,
                                    InboxMessage attachMsg)
            throws MessagingException {

        // set body
        String bodyMessage;
        if(mainMsg.getBodyType() == MailMessage.BODYTYPE_TEXT) {
            // .. convert to HTML
            bodyMessage = StringHelper.text2html(mainMsg.getBody());
        } else {
            bodyMessage = StringHelper.clearHtml(mainMsg.getBody(), false);
        }
        String bodyAttachMessage;
        if(attachMsg.getBodyType() == MailMessage.BODYTYPE_TEXT) {
            // .. convert to HTML
            bodyAttachMessage = StringHelper.text2html(attachMsg.getBody());
        } else {
            bodyAttachMessage = StringHelper.clearHtml(attachMsg.getBody(),
                    false);
        }
        bodyMessage += "<br><br>---------- Forwarded message ----------<br>";

        // get From addresses
        bodyMessage += "From: ";
        if(attachMsg.getFrom() != null) {
            bodyMessage += StringHelper.escape(
                    attachMsg.getFrom().toRfcString());
        }
        bodyMessage += "<br>";

        // get To addresses
        bodyMessage += "To: ";
        if(attachMsg.getTo() != null) {
            bodyMessage += StringHelper.escape(MailAddress.toRfcString(
                    attachMsg.getTo()));
        }
        bodyMessage += "<br>";

        // get CC addresses
        bodyMessage += "CC: ";
        if(attachMsg.getCc() != null) {
            bodyMessage += StringHelper.escape(MailAddress.toRfcString(
                    attachMsg.getCc()));
        }
        bodyMessage += "<br>";

        // calculate message date
        Date receiveDate = attachMsg.getReceiveTime();
        Date messageDate = (receiveDate == null) ? attachMsg.getSentTime()
                :receiveDate;

        if(messageDate != null) {
            // get date as string
            long defaultTime = DateHelper.toUser(messageDate.getTime(),
                    SystemHelper.DEFAULT_TIMEZONE);
            String dateAsString = DateHelper.formatDate(new Date(defaultTime));
            bodyMessage += "Date: " + StringHelper.escape(dateAsString)
                    + "<br>";

        } else {
            bodyMessage += "Date: <br>";
        }

        // get subject
        String subject = attachMsg.getSubject();
        if(subject == null) {
            subject = StringHelper.EMPTY_VALUE;
        }
        bodyMessage += "Subject: " + StringHelper.escape(subject) + "<br><br>";

        // add message body secondary message
        bodyMessage += bodyAttachMessage;

        // set <html> tag
        bodyMessage = checkUpHtml(bodyMessage);

        // set body into main message
        mainMsg.setBody(bodyMessage);

        // save attchments into main message
        List attachments = attachMsg.getAttachments();
        int size = (attachments == null) ? 0:attachments.size();
        for(int i = 0; i < size; i++) {
            Attachment attach = (Attachment) attachments.get(i);
            mainMsg.addAttachments(attach);
        }

        return true;
    }

    /**
     * The method gets the name of code page from Content Type string
     *
     * @param contentType formatted content type string
     * @return String or NULL
     * @see #getContentType
     */
    protected String getCodePage(String contentType) {

        // position of the first symbol charset= string
        String charsetParamName = "CHARSET=";
        int startCharset = contentType.indexOf(charsetParamName);

        // position of the last symbol charset= string
        int startCharsetLength = startCharset + charsetParamName.length();

        if(startCharsetLength + 1 >= contentType.length()) {
            // if Content Type string doesn't contain code page
            return null;
        }

        // check up the first symbol of code page - it shouldn't be comma
        String charsetValue;
        if(contentType.substring(startCharsetLength, startCharsetLength + 1)
                .equals("\"")) {
            startCharset = startCharsetLength + 1;
        } else {
            startCharset = startCharsetLength;
        }

        // get position of the last symbol of code page
        int endCharset = 0;
        for(int i = startCharset; i < contentType.length(); i++) {
            if(contentType.substring(i, i + 1).equals(" ") ||
                    contentType.substring(i, i + 1).equals("\"")) {
                break;
            } else {
                endCharset++;
            }
        }
        endCharset += startCharset;
        charsetValue = contentType.substring(startCharset, endCharset);

        return charsetValue;
    }

    /**
     * The method checks - is given string .
     * It needs for additional HTML check by <br> tag
     *
     * @param s the string to check
     * @return <b>true</b> if <br> presents
     */
    protected boolean isHTMLContent(String s) {
        if(StringHelper.isEmpty(s)) {
            return false;
        }
        try {
            RE re = new RE("<br((\\s+[^>]*)|(>))", RE.MATCH_CASEINDEPENDENT);
            return re.match(s);
        } catch (RESyntaxException rex) {
            throw new GenericSystemException(
                    "Regexp exception: " + rex.getMessage(), rex);
        }
    }

    /**
     * The method deletes \r\n symbols from HTML
     *
     * @param s the string to check
     * @return HTML string
     */
    protected String checkUpHtml(String s) {
        String result = s;
        if(!StringHelper.isEmpty(result)) {
            try {
                RE re = new RE("\\r\\n|\\r|\\n", RE.REPLACE_ALL);
                result = re.subst(result, "");
            } catch (RESyntaxException rex) {
                throw new GenericSystemException(
                        "Regexp exception: " + rex.getMessage(), rex);
            }
        }
        return result;
    }

    /**
     * Gets content type in appropriate format.
     *
     * @param part Part
     * @return String
     * @throws MessagingException
     */
    protected String getContentType(Part part)
            throws MessagingException {

        String contentType = part.getContentType();
        if(StringHelper.isEmpty(contentType)) {
            // .. if empty take "text/plain"
            contentType = InboxHelper.CONTENT_TYPE_PLAIN;
        }
        return contentType.toUpperCase();
    }

    /**
     * Returns new unique name for attachment.
     *
     * @return String
     * @see InboxHelper#generateUniqueAttachName
     */
    protected String generateUniqueAttachName() {
        return InboxHelper.generateUniqueAttachName(++uniqueID);
    }
}
