package eu.faircode.email;

/*
    This file is part of FairEmail.

    FairEmail is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    FairEmail 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 FairEmail.  If not, see <http://www.gnu.org/licenses/>.

    Copyright 2018-2019 by Marcel Bokhorst (M66B)
*/

import android.content.Context;
import android.text.TextUtils;
import android.webkit.MimeTypeMap;

import com.sun.mail.util.FolderClosedIOException;

import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.List;
import java.util.Properties;

import javax.activation.DataHandler;
import javax.activation.FileDataSource;
import javax.activation.FileTypeMap;
import javax.mail.Address;
import javax.mail.BodyPart;
import javax.mail.Flags;
import javax.mail.FolderClosedException;
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.AddressException;
import javax.mail.internet.ContentType;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMultipart;
import javax.mail.internet.MimeUtility;
import javax.mail.internet.ParseException;

public class MessageHelper {
    private MimeMessage imessage;

    private final static int CONNECT_TIMEOUT = 20 * 1000; // milliseconds
    private final static int WRITE_TIMEOUT = 40 * 1000; // milliseconds
    private final static int READ_TIMEOUT = 40 * 1000; // milliseconds
    private final static int FETCH_SIZE = 1024 * 1024; // bytes, default 16K
    private final static int POOL_TIMEOUT = 45 * 1000; // milliseconds, default 45 sec

    static final int ATTACHMENT_BUFFER_SIZE = 8192; // bytes
    static final int DEFAULT_ATTACHMENT_DOWNLOAD_SIZE = 65536; // bytes

    static void setSystemProperties() {
        System.setProperty("mail.mime.decodetext.strict", "false");

        System.setProperty("mail.mime.ignoreunknownencoding", "true"); // Content-Transfer-Encoding
        System.setProperty("mail.mime.base64.ignoreerrors", "true");
        System.setProperty("mail.mime.decodefilename", "true");
        System.setProperty("mail.mime.encodefilename", "true");
        System.setProperty("mail.mime.allowutf8", "false"); // InternetAddress, MimeBodyPart, MimeUtility

        // https://docs.oracle.com/javaee/6/api/javax/mail/internet/MimeMultipart.html
        System.setProperty("mail.mime.multipart.ignoremissingboundaryparameter", "true"); // javax.mail.internet.ParseException: In parameter list
        System.setProperty("mail.mime.multipart.ignoreexistingboundaryparameter", "true");
    }

    static Properties getSessionProperties(int auth_type, String realm, boolean insecure) {
        Properties props = new Properties();

        props.put("mail.event.scope", "folder");

        String checkserveridentity = Boolean.toString(!insecure).toLowerCase();

        // https://javaee.github.io/javamail/docs/api/com/sun/mail/imap/package-summary.html#properties
        props.put("mail.imaps.ssl.checkserveridentity", checkserveridentity);
        props.put("mail.imaps.ssl.trust", "*");
        props.put("mail.imaps.starttls.enable", "false");

        if (realm != null)
            props.put("mail.imaps.auth.ntlm.domain", realm);

        // TODO: make timeouts configurable?
        props.put("mail.imaps.connectiontimeout", Integer.toString(CONNECT_TIMEOUT));
        props.put("mail.imaps.writetimeout", Integer.toString(WRITE_TIMEOUT)); // one thread overhead
        props.put("mail.imaps.timeout", Integer.toString(READ_TIMEOUT));

        props.put("mail.imaps.connectionpool.debug", "true");
        props.put("mail.imaps.connectionpooltimeout", Integer.toString(POOL_TIMEOUT));

        // https://tools.ietf.org/html/rfc4978
        // https://docs.oracle.com/javase/8/docs/api/java/util/zip/Deflater.html
        props.put("mail.imaps.compress.enable", "true");
        //props.put("mail.imaps.compress.level", "-1");
        //props.put("mail.imaps.compress.strategy", "0");

        props.put("mail.imaps.throwsearchexception", "true");
        props.put("mail.imaps.fetchsize", Integer.toString(FETCH_SIZE));
        props.put("mail.imaps.peek", "true");

        props.put("mail.imap.ssl.checkserveridentity", checkserveridentity);
        props.put("mail.imap.ssl.trust", "*");
        props.put("mail.imap.starttls.enable", "true");
        props.put("mail.imap.starttls.required", "true");

        if (realm != null)
            props.put("mail.imap.auth.ntlm.domain", realm);

        props.put("mail.imap.connectiontimeout", Integer.toString(CONNECT_TIMEOUT));
        props.put("mail.imap.writetimeout", Integer.toString(WRITE_TIMEOUT)); // one thread overhead
        props.put("mail.imap.timeout", Integer.toString(READ_TIMEOUT));

        props.put("mail.imap.connectionpool.debug", "true");
        props.put("mail.imap.connectionpooltimeout", Integer.toString(POOL_TIMEOUT));

        props.put("mail.imap.compress.enable", "true");

        props.put("mail.imap.throwsearchexception", "true");
        props.put("mail.imap.fetchsize", Integer.toString(FETCH_SIZE));
        props.put("mail.imap.peek", "true");

        // https://javaee.github.io/javamail/docs/api/com/sun/mail/smtp/package-summary.html#properties
        props.put("mail.smtps.ssl.checkserveridentity", checkserveridentity);
        props.put("mail.smtps.ssl.trust", "*");
        props.put("mail.smtps.starttls.enable", "false");
        props.put("mail.smtps.starttls.required", "false");

        props.put("mail.smtps.auth", "true");
        if (realm != null)
            props.put("mail.smtps.auth.ntlm.domain", realm);

        props.put("mail.smtps.connectiontimeout", Integer.toString(CONNECT_TIMEOUT));
        props.put("mail.smtps.writetimeout", Integer.toString(WRITE_TIMEOUT)); // one thread overhead
        props.put("mail.smtps.timeout", Integer.toString(READ_TIMEOUT));

        props.put("mail.smtp.ssl.checkserveridentity", checkserveridentity);
        props.put("mail.smtp.ssl.trust", "*");
        props.put("mail.smtp.starttls.enable", "true");
        props.put("mail.smtp.starttls.required", "true");

        props.put("mail.smtp.auth", "true");
        if (realm != null)
            props.put("mail.smtp.auth.ntlm.domain", realm);

        props.put("mail.smtp.connectiontimeout", Integer.toString(CONNECT_TIMEOUT));
        props.put("mail.smtp.writetimeout", Integer.toString(WRITE_TIMEOUT)); // one thread overhead
        props.put("mail.smtp.timeout", Integer.toString(READ_TIMEOUT));

        // MIME
        props.put("mail.mime.allowutf8", "false"); // SMTPTransport, MimeMessage
        props.put("mail.mime.address.strict", "false");

        if (false) {
            Log.i("Prefering IPv4");
            System.setProperty("java.net.preferIPv4Stack", "true");
        }

        // https://javaee.github.io/javamail/OAuth2
        Log.i("Auth type=" + auth_type);
        if (auth_type == Helper.AUTH_TYPE_GMAIL) {
            props.put("mail.imaps.auth.mechanisms", "XOAUTH2");
            props.put("mail.imap.auth.mechanisms", "XOAUTH2");
            props.put("mail.smtps.auth.mechanisms", "XOAUTH2");
            props.put("mail.smtp.auth.mechanisms", "XOAUTH2");
        }

        return props;
    }


    static MimeMessageEx from(Context context, EntityMessage message, EntityIdentity identity, Session isession)
            throws MessagingException, IOException {
        DB db = DB.getInstance(context);
        MimeMessageEx imessage = new MimeMessageEx(isession, message.msgid);

        if (message.references != null)
            imessage.addHeader("References", message.references);
        if (message.inreplyto != null)
            imessage.addHeader("In-Reply-To", message.inreplyto);

        imessage.addHeader("X-Mailer", context.getString(R.string.app_name) + " " + BuildConfig.VERSION_NAME);
        imessage.addHeader("X-FairEmail-ID", message.msgid);

        imessage.setFlag(Flags.Flag.SEEN, message.seen);
        imessage.setFlag(Flags.Flag.FLAGGED, message.flagged);
        imessage.setFlag(Flags.Flag.ANSWERED, message.answered);

        if (message.from != null && message.from.length > 0) {
            String email = ((InternetAddress) message.from[0]).getAddress();
            String name = ((InternetAddress) message.from[0]).getPersonal();
            if (email != null && identity != null && identity.sender_extra && !TextUtils.isEmpty(message.extra)) {
                int at = email.indexOf('@');
                email = message.extra + email.substring(at);
                Log.i("extra=" + email);
            }
            imessage.setFrom(new InternetAddress(email, name));
        }

        if (message.to != null && message.to.length > 0)
            imessage.setRecipients(Message.RecipientType.TO, message.to);

        if (message.cc != null && message.cc.length > 0)
            imessage.setRecipients(Message.RecipientType.CC, message.cc);

        if (message.bcc != null && message.bcc.length > 0)
            imessage.setRecipients(Message.RecipientType.BCC, message.bcc);

        if (message.subject != null)
            imessage.setSubject(message.subject);

        imessage.setSentDate(new Date());

        List<EntityAttachment> attachments = db.attachment().getAttachments(message.id);

        if (message.from != null && message.from.length > 0)
            for (EntityAttachment attachment : attachments)
                if (attachment.available && EntityAttachment.PGP_SIGNATURE.equals(attachment.encryption)) {
                    InternetAddress from = (InternetAddress) message.from[0];
                    File file = attachment.getFile(context);
                    StringBuilder sb = new StringBuilder();
                    try (BufferedReader br = new BufferedReader(new FileReader(file))) {
                        String line;
                        while ((line = br.readLine()) != null)
                            if (!line.startsWith("-----") && !line.endsWith("-----"))
                                sb.append(line);
                    }

                    imessage.addHeader("Autocrypt", "addr=" + from.getAddress() + "; keydata=" + sb.toString());
                }

        for (final EntityAttachment attachment : attachments)
            if (attachment.available && EntityAttachment.PGP_MESSAGE.equals(attachment.encryption)) {
                Multipart multipart = new MimeMultipart("encrypted; protocol=\"application/pgp-encrypted\"");

                BodyPart pgp = new MimeBodyPart();
                pgp.setContent("", "application/pgp-encrypted");
                multipart.addBodyPart(pgp);

                BodyPart bpAttachment = new MimeBodyPart();
                bpAttachment.setFileName(attachment.name);

                File file = attachment.getFile(context);
                FileDataSource dataSource = new FileDataSource(file);
                dataSource.setFileTypeMap(new FileTypeMap() {
                    @Override
                    public String getContentType(File file) {
                        return attachment.type;
                    }

                    @Override
                    public String getContentType(String filename) {
                        return attachment.type;
                    }
                });
                bpAttachment.setDataHandler(new DataHandler(dataSource));
                bpAttachment.setDisposition(Part.INLINE);

                multipart.addBodyPart(bpAttachment);

                imessage.setContent(multipart);

                return imessage;
            }

        build(context, message, attachments, identity, imessage);

        return imessage;
    }

    static void build(Context context, EntityMessage message, List<EntityAttachment> attachments, EntityIdentity identity, MimeMessage imessage) throws IOException, MessagingException {
        DB db = DB.getInstance(context);

        if (message.receipt_request != null && message.receipt_request) {
            // https://www.ietf.org/rfc/rfc3798.txt
            Multipart report = new MimeMultipart("report; report-type=disposition-notification");

            String plainContent = HtmlHelper.getText(Helper.readText(message.getFile(context)));

            BodyPart plainPart = new MimeBodyPart();
            plainPart.setContent(plainContent, "text/plain; charset=" + Charset.defaultCharset().name());
            report.addBodyPart(plainPart);

            BodyPart dnsPart = new MimeBodyPart();
            dnsPart.setContent("", "message/disposition-notification; name=\"MDNPart2.txt\"");
            dnsPart.setDisposition(Part.INLINE);
            report.addBodyPart(dnsPart);

            //BodyPart headersPart = new MimeBodyPart();
            //headersPart.setContent("", "text/rfc822-headers; name=\"MDNPart3.txt\"");
            //headersPart.setDisposition(Part.INLINE);
            //report.addBodyPart(headersPart);

            imessage.setContent(report);
            return;
        }

        StringBuilder body = new StringBuilder();
        body.append(Helper.readText(message.getFile(context)));

        if (identity != null && !TextUtils.isEmpty(identity.signature))
            body.append(identity.signature);

        File refFile = message.getRefFile(context);
        if (refFile.exists())
            body.append(Helper.readText(refFile));

        String plainContent = HtmlHelper.getText(body.toString());

        StringBuilder htmlContent = new StringBuilder();
        htmlContent.append("<!DOCTYPE html>").append("\n");
        htmlContent.append("<html>").append("\n");
        htmlContent.append("<head>").append("\n");
        htmlContent.append("<meta charset=\"utf-8\" /> ").append("\n");
        htmlContent.append("</head>").append("\n");
        htmlContent.append("<body>").append("\n");
        htmlContent.append(body.toString()).append("\n");
        htmlContent.append("</body>").append("\n");
        htmlContent.append("</html>").append("\n");

        BodyPart plainPart = new MimeBodyPart();
        plainPart.setContent(plainContent, "text/plain; charset=" + Charset.defaultCharset().name());

        BodyPart htmlPart = new MimeBodyPart();
        htmlPart.setContent(htmlContent.toString(), "text/html; charset=" + Charset.defaultCharset().name());

        Multipart alternativePart = new MimeMultipart("alternative");
        alternativePart.addBodyPart(plainPart);
        alternativePart.addBodyPart(htmlPart);

        int available = 0;
        for (EntityAttachment attachment : attachments)
            if (attachment.available)
                available++;
        Log.i("Attachments available=" + available);

        if (available == 0)
            if (identity != null && identity.plain_only)
                imessage.setContent(plainContent, "text/plain; charset=" + Charset.defaultCharset().name());
            else
                imessage.setContent(alternativePart);
        else {
            Multipart mixedPart = new MimeMultipart("mixed");

            BodyPart attachmentPart = new MimeBodyPart();
            if (identity != null && identity.plain_only)
                attachmentPart.setContent(plainContent, "text/plain; charset=" + Charset.defaultCharset().name());
            else
                attachmentPart.setContent(alternativePart);
            mixedPart.addBodyPart(attachmentPart);

            for (final EntityAttachment attachment : attachments)
                if (attachment.available) {
                    BodyPart bpAttachment = new MimeBodyPart();
                    bpAttachment.setFileName(attachment.name);

                    File file = attachment.getFile(context);
                    FileDataSource dataSource = new FileDataSource(file);
                    dataSource.setFileTypeMap(new FileTypeMap() {
                        @Override
                        public String getContentType(File file) {
                            return attachment.type;
                        }

                        @Override
                        public String getContentType(String filename) {
                            return attachment.type;
                        }
                    });
                    bpAttachment.setDataHandler(new DataHandler(dataSource));
                    if (attachment.disposition != null)
                        bpAttachment.setDisposition(attachment.disposition);
                    if (attachment.cid != null)
                        bpAttachment.setHeader("Content-ID", attachment.cid);

                    mixedPart.addBodyPart(bpAttachment);
                }

            imessage.setContent(mixedPart);
        }
    }

    MessageHelper(MimeMessage message) {
        this.imessage = message;
    }

    boolean getSeen() throws MessagingException {
        return imessage.isSet(Flags.Flag.SEEN);
    }

    boolean getAnsered() throws MessagingException {
        return imessage.isSet(Flags.Flag.ANSWERED);
    }

    boolean getFlagged() throws MessagingException {
        return imessage.isSet(Flags.Flag.FLAGGED);
    }

    String getFlags() throws MessagingException {
        if (!BuildConfig.DEBUG)
            return null;

        Flags flags = imessage.getFlags();
        flags.clearUserFlags();
        return flags.toString();
    }

    String[] getKeywords() throws MessagingException {
        return imessage.getFlags().getUserFlags();
    }

    String getMessageID() throws MessagingException {
        // Outlook outbox -> sent
        String[] xID = imessage.getHeader("X-FairEmail-ID");
        if (xID != null && xID.length > 0)
            return xID[0];

        return imessage.getHeader("Message-ID", null);
    }

    String[] getReferences() throws MessagingException {
        String refs = imessage.getHeader("References", null);
        return (refs == null ? new String[0] : refs.split("\\s+"));
    }

    String getDeliveredTo() throws MessagingException {
        String header = imessage.getHeader("Delivered-To", null);
        if (header == null)
            header = imessage.getHeader("X-Delivered-To", null);
        return header;
    }

    String getInReplyTo() throws MessagingException {
        return imessage.getHeader("In-Reply-To", null);
    }

    String getThreadId(Context context, long account, long uid) throws MessagingException {
        List<String> refs = new ArrayList<>();

        for (String ref : getReferences())
            if (!TextUtils.isEmpty(ref))
                refs.add(ref);

        String inreplyto = getInReplyTo();
        if (!TextUtils.isEmpty(inreplyto) && !refs.contains(inreplyto))
            refs.add(inreplyto);

        DB db = DB.getInstance(context);
        for (String ref : refs) {
            List<EntityMessage> messages = db.message().getMessageByMsgId(account, ref);
            if (messages.size() > 0)
                return messages.get(0).thread;
        }

        if (refs.size() > 0)
            return refs.get(0);

        String msgid = getMessageID();
        return (TextUtils.isEmpty(msgid) ? Long.toString(uid) : msgid);
    }

    boolean getReceiptRequested() throws MessagingException {
        return (imessage.getHeader("Return-Receipt-To") != null ||
                imessage.getHeader("Disposition-Notification-To") != null);
    }

    Address[] getReceiptTo() throws MessagingException {
        String to = imessage.getHeader("Disposition-Notification-To", null);
        if (to == null)
            return null;

        InternetAddress[] address = null;
        try {
            address = InternetAddress.parse(to);
        } catch (AddressException ex) {
            Log.w(ex);
        }

        if (address == null || address.length == 0)
            return null;

        fix(address[0]);

        return new Address[]{address[0]};
    }

    String getAuthentication() throws MessagingException {
        String header = imessage.getHeader("Authentication-Results", "");
        return (header == null ? null : header.replaceAll("\\r?\\n", ""));
    }

    static Boolean getAuthentication(String type, String header) {
        if (header == null)
            return null;

        // https://tools.ietf.org/html/rfc7601
        Boolean result = null;
        String[] part = header.split(";");
        for (int i = 1; i < part.length; i++) {
            String[] kv = part[i].split("=");
            if (kv.length > 1) {
                String key = kv[0].trim();
                String[] val = kv[1].trim().split(" ");
                if (val.length > 0 && type.equals(key)) {
                    if ("fail".equals(val[0]))
                        result = false;
                    else if ("pass".equals(val[0]))
                        if (result == null)
                            result = true;
                }
            }
        }

        return result;
    }

    Address[] getFrom() throws MessagingException {
        return fix(imessage.getFrom());
    }

    Address[] getTo() throws MessagingException {
        return fix(imessage.getRecipients(Message.RecipientType.TO));
    }

    Address[] getCc() throws MessagingException {
        return fix(imessage.getRecipients(Message.RecipientType.CC));
    }

    Address[] getBcc() throws MessagingException {
        return fix(imessage.getRecipients(Message.RecipientType.BCC));
    }

    Address[] getReply() throws MessagingException {
        String[] headers = imessage.getHeader("Reply-To");
        if (headers != null && headers.length > 0)
            return fix(imessage.getReplyTo());
        else
            return null;
    }

    Address[] getListPost() throws MessagingException {
        // https://www.ietf.org/rfc/rfc2369.txt
        String list = imessage.getHeader("List-Post", null);
        if (list == null || "NO".equals(list))
            return null;

        InternetAddress[] address = null;
        try {
            address = InternetAddress.parse(list);
        } catch (AddressException ex) {
            Log.w(ex);
        }

        if (address == null || address.length == 0)
            return null;

        fix(address[0]);

        return new Address[]{address[0]};
    }

    private static Address[] fix(Address[] addresses) {
        if (addresses != null)
            for (int i = 0; i < addresses.length; i++)
                fix((InternetAddress) addresses[i]);
        return addresses;
    }

    private static void fix(InternetAddress address) {
        try {
            String email = decodeMime(address.getAddress());
            String personal = decodeMime(address.getPersonal());
            try {
                InternetAddress[] a = InternetAddress.parse(email);
                if (a.length < 1)
                    throw new AddressException("empty");
                String p = a[0].getPersonal();
                address.setAddress(a[0].getAddress());
                address.setPersonal(TextUtils.isEmpty(personal) ? p : personal + (p == null ? "" : " " + p));
            } catch (AddressException ex) {
                Log.w(ex);
                address.setAddress(email);
                address.setPersonal(personal);
            }
        } catch (UnsupportedEncodingException ex) {
            Log.w(ex);
        }
    }

    String getSubject() throws MessagingException {
        String subject = imessage.getSubject();
        return decodeMime(subject);
    }

    Long getSize() throws MessagingException {
        long size = imessage.getSize();
        if (size == 0)
            throw new MessagingException("Message empty");
        return (size < 0 ? null : size);
    }

    long getReceived() throws MessagingException {
        return imessage.getReceivedDate().getTime();
    }

    Long getSent() throws MessagingException {
        Date date = imessage.getSentDate();
        return (date == null ? null : date.getTime());
    }

    String getHeaders() throws MessagingException {
        StringBuilder sb = new StringBuilder();
        Enumeration<Header> headers = imessage.getAllHeaders();
        while (headers.hasMoreElements()) {
            Header header = headers.nextElement();
            sb.append(header.getName()).append(": ").append(header.getValue()).append("\n");
        }
        return sb.toString();
    }

    static String formatAddresses(Address[] addresses) {
        return formatAddresses(addresses, true, false);
    }

    static String formatAddressesShort(Address[] addresses) {
        return formatAddresses(addresses, false, false);
    }

    static String formatAddressesCompose(Address[] addresses) {
        String result = formatAddresses(addresses, true, true);
        if (!TextUtils.isEmpty(result))
            result += ", ";
        return result;
    }

    static String formatAddresses(Address[] addresses, boolean full, boolean compose) {
        if (addresses == null || addresses.length == 0)
            return "";

        List<String> formatted = new ArrayList<>();
        for (Address address : addresses)
            if (address instanceof InternetAddress) {
                InternetAddress a = (InternetAddress) address;
                String personal = a.getPersonal();
                if (TextUtils.isEmpty(personal))
                    formatted.add(a.getAddress());
                else {
                    if (compose) {
                        boolean quote = false;
                        for (int i = 0; i < personal.length(); i++)
                            if ("()<>,;:\\\"[]@".indexOf(personal.charAt(i)) >= 0) {
                                quote = true;
                                break;
                            }
                        if (quote)
                            personal = "\"" + personal + "\"";
                    }

                    if (full)
                        formatted.add(personal + " <" + a.getAddress() + ">");
                    else
                        formatted.add(personal);
                }
            } else
                formatted.add(address.toString());
        return TextUtils.join(", ", formatted);
    }

    static String decodeMime(String text) {
        if (text == null)
            return null;

        int i = 0;
        int s = text.indexOf("=?", i);
        int e = text.indexOf("?=", i);
        while (s >= 0 && e >= 0 && i < text.length()) {
            String decode = text.substring(s, e + 2);
            try {
                String decoded = MimeUtility.decodeWord(decode);
                text = text.substring(0, s) + decoded + text.substring(e + 2);
                i += decoded.length();
            } catch (ParseException ex) {
                Log.w(ex);
                i += decode.length();
            } catch (UnsupportedEncodingException ex) {
                Log.w(ex);
                i += decode.length();
            }
            s = text.indexOf("=?", i);
            e = text.indexOf("?=", i);
        }

        return text;
    }

    static String getSortKey(Address[] addresses) {
        if (addresses == null || addresses.length == 0)
            return null;
        InternetAddress address = (InternetAddress) addresses[0];
        // Sort on name will result in inconsistent results
        // because the sender name and sender contact name can differ
        return address.getAddress();
    }

    class MessageParts {
        private Part plain = null;
        private Part html = null;
        private List<AttachmentPart> attachments = new ArrayList<>();
        private ArrayList<String> warnings = new ArrayList<>();

        String getHtml(Context context) throws MessagingException {
            if (plain == null && html == null) {
                warnings.add(context.getString(R.string.title_no_body));
                return null;
            }

            String result;
            boolean text = false;
            Part part = (html == null ? plain : html);

            try {
                Object content = part.getContent();
                if (content instanceof String)
                    result = (String) content;
                else if (content instanceof InputStream)
                    // Typically com.sun.mail.util.QPDecoderStream
                    result = readStream((InputStream) content, "UTF-8");
                else
                    result = content.toString();
            } catch (FolderClosedException ex) {
                throw ex;
            } catch (FolderClosedIOException ex) {
                throw new FolderClosedException(ex.getFolder(), "getHtml", ex);
            } catch (Throwable ex) {
                Log.w(ex);
                text = true;
                result = ex + "\n" + android.util.Log.getStackTraceString(ex);
            }

            ContentType ct = new ContentType(part.getContentType());
            String charset = ct.getParameter("charset");
            if (TextUtils.isEmpty(charset)) {
                if (BuildConfig.DEBUG)
                    warnings.add(context.getString(R.string.title_no_charset, ct.toString()));
                if (part.isMimeType("text/plain")) {
                    // The first 127 characters are the same as in US-ASCII
                    result = new String(result.getBytes(StandardCharsets.ISO_8859_1));
                }
            } else {
                if ("US-ASCII".equals(Charset.forName(charset).name()) &&
                        !"US-ASCII".equals(charset.toUpperCase()))
                    warnings.add(context.getString(R.string.title_no_charset, charset));
            }

            if (part.isMimeType("text/plain") || text)
                result = "<pre>" + result.replaceAll("\\r?\\n", "<br />") + "</pre>";

            return result;
        }

        List<AttachmentPart> getAttachmentParts() {
            return attachments;
        }

        List<EntityAttachment> getAttachments() throws MessagingException {
            List<EntityAttachment> result = new ArrayList<>();

            for (AttachmentPart apart : attachments) {
                ContentType ct;
                try {
                    ct = new ContentType(apart.part.getContentType());
                } catch (ParseException ex) {
                    Log.w(ex);
                    ct = new ContentType("application/octet-stream");
                }
                String[] cid = apart.part.getHeader("Content-ID");

                EntityAttachment attachment = new EntityAttachment();
                attachment.name = apart.filename;
                attachment.type = ct.getBaseType().toLowerCase();
                attachment.disposition = apart.disposition;
                attachment.size = (long) apart.part.getSize();
                attachment.cid = (cid == null || cid.length == 0 ? null : cid[0]);
                attachment.encryption = (apart.pgp ? EntityAttachment.PGP_MESSAGE : null);

                if ("text/calendar".equalsIgnoreCase(attachment.type) && TextUtils.isEmpty(attachment.name))
                    attachment.name = "invite.ics";

                // Try to guess a better content type
                // Sometimes PDF files are sent using the wrong type
                if ("application/octet-stream".equalsIgnoreCase(attachment.type)) {
                    String extension = Helper.getExtension(attachment.name);
                    if (extension != null) {
                        String type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension.toLowerCase());
                        if (type != null) {
                            Log.w("Guessing file=" + attachment.name + " type=" + type);
                            attachment.type = type;
                        }
                    }
                }

                if (attachment.size < 0)
                    attachment.size = null;

                result.add(attachment);
            }

            // Fix duplicate CIDs
            for (int i = 0; i < result.size(); i++) {
                String cid = result.get(i).cid;
                if (cid != null)
                    for (int j = i + 1; j < result.size(); j++) {
                        EntityAttachment a = result.get(j);
                        if (cid.equals(a.cid))
                            a.cid = null;
                    }
            }

            return result;
        }

        void downloadAttachment(Context context, int index, long id) throws MessagingException, IOException {
            Log.i("downloading attchment id=" + id + " seq=" + index);
            // Attachments of drafts might not have been uploaded yet
            if (index > attachments.size()) {
                Log.w("Attachment unavailable sequence=" + index + " size=" + attachments.size());
                return;
            }

            DB db = DB.getInstance(context);

            // Get data
            AttachmentPart apart = attachments.get(index);
            EntityAttachment attachment = db.attachment().getAttachment(id);
            if (attachment == null)
                return;
            File file = attachment.getFile(context);

            // Download attachment
            db.attachment().setProgress(id, null);
            try (InputStream is = apart.part.getInputStream()) {
                long size = 0;
                long total = apart.part.getSize();

                try (OutputStream os = new BufferedOutputStream(new FileOutputStream(file))) {
                    byte[] buffer = new byte[ATTACHMENT_BUFFER_SIZE];
                    for (int len = is.read(buffer); len != -1; len = is.read(buffer)) {
                        size += len;
                        os.write(buffer, 0, len);

                        // Update progress
                        if (total > 0)
                            db.attachment().setProgress(id, (int) (size * 100 / total));
                    }
                }

                // Store attachment data
                db.attachment().setDownloaded(id, size);

                Log.i("Downloaded attachment size=" + size);
            } catch (FolderClosedIOException ex) {
                throw new FolderClosedException(ex.getFolder(), "downloadAttachment", ex);
            } catch (Throwable ex) {
                // Reset progress on failure
                db.attachment().setError(id, Helper.formatThrowable(ex));
                throw ex;
            }
        }

        String getWarnings(String existing) {
            if (existing != null)
                warnings.add(0, existing);
            if (warnings.size() == 0)
                return null;
            else
                return TextUtils.join(", ", warnings);
        }
    }

    class AttachmentPart {
        String disposition;
        String filename;
        boolean pgp;
        Part part;
    }

    MessageParts getMessageParts() throws IOException, FolderClosedException {
        MessageParts parts = new MessageParts();

        MimeMessage cmessage = imessage;
        try {
            // Load body structure
            cmessage.getContentID();
        } catch (MessagingException ex) {
            // https://javaee.github.io/javamail/FAQ#imapserverbug
            if ("Unable to load BODYSTRUCTURE".equals(ex.getMessage())) {
                Log.w(ex);
                parts.warnings.add(Helper.formatThrowable(ex));
                try {
                    cmessage = new MimeMessage(imessage);
                } catch (MessagingException ignored) {
                }
            }
        }

        getMessageParts(cmessage, parts, false);

        return parts;
    }

    private void getMessageParts(Part part, MessageParts parts, boolean pgp) throws IOException, FolderClosedException {
        try {
            if (part.isMimeType("multipart/*")) {
                Multipart multipart = (Multipart) part.getContent();
                for (int i = 0; i < multipart.getCount(); i++)
                    try {
                        Part cpart = multipart.getBodyPart(i);
                        getMessageParts(cpart, parts, pgp);
                        ContentType ct = new ContentType(cpart.getContentType());
                        if ("application/pgp-encrypted".equals(ct.getBaseType().toLowerCase()))
                            pgp = true;
                    } catch (ParseException ex) {
                        // Nested body: try to continue
                        // ParseException: In parameter list boundary="...">, expected parameter name, got ";"
                        Log.w(ex);
                        parts.warnings.add(Helper.formatThrowable(ex));
                    }
            } else {
                // https://www.iana.org/assignments/cont-disp/cont-disp.xhtml
                String disposition;
                try {
                    disposition = part.getDisposition();
                } catch (MessagingException ex) {
                    Log.w(ex);
                    parts.warnings.add(Helper.formatThrowable(ex));
                    disposition = null;
                }

                String filename;
                try {
                    filename = part.getFileName();
                } catch (MessagingException ex) {
                    Log.w(ex);
                    parts.warnings.add(Helper.formatThrowable(ex));
                    filename = null;
                }

                //Log.i("Part" +
                //        " disposition=" + disposition +
                //        " filename=" + filename +
                //        " content type=" + part.getContentType());

                if (!Part.ATTACHMENT.equalsIgnoreCase(disposition) &&
                        ((parts.plain == null && part.isMimeType("text/plain")) ||
                                (parts.html == null && part.isMimeType("text/html")))) {
                    if (part.isMimeType("text/plain"))
                        parts.plain = part;
                    else
                        parts.html = part;
                } else {
                    AttachmentPart apart = new AttachmentPart();
                    apart.disposition = disposition;
                    apart.filename = filename;
                    apart.pgp = pgp;
                    apart.part = part;
                    parts.attachments.add(apart);
                }
            }
        } catch (FolderClosedException ex) {
            throw ex;
        } catch (MessagingException ex) {
            Log.w(ex);
            parts.warnings.add(Helper.formatThrowable(ex));
        }
    }

    private static String readStream(InputStream is, String charset) throws IOException {
        ByteArrayOutputStream os = new ByteArrayOutputStream();
        byte[] buffer = new byte[4096];
        for (int len = is.read(buffer); len != -1; len = is.read(buffer))
            os.write(buffer, 0, len);
        return new String(os.toByteArray(), charset);
    }

    static boolean equal(Address[] a1, Address[] a2) {
        if (a1 == null && a2 == null)
            return true;

        if (a1 == null || a2 == null)
            return false;

        if (a1.length != a2.length)
            return false;

        for (int i = 0; i < a1.length; i++)
            if (!a1[i].toString().equals(a2[i].toString()))
                return false;

        return true;
    }
}
