/*
* Copyright (C) 2013 Samsung System LSI
* Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0
*
* 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.android.bluetooth.map;

import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.nio.charset.IllegalCharsetNameException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.Locale;
import java.util.UUID;

import android.text.util.Rfc822Token;
import android.text.util.Rfc822Tokenizer;
import android.util.Base64;
import android.util.Log;

public class BluetoothMapbMessageMms extends BluetoothMapbMessage {

    public static class MimePart {
        public long mId = INVALID_VALUE;   /* The _id from the content provider, can be used to sort the parts if needed */
        public String mContentType = null;  /* The mime type, e.g. text/plain */
        public String mContentId = null;
        public String mContentLocation = null;
        public String mContentDisposition = null;
        public String mPartName = null;     /* e.g. text_1.txt*/
        public String mCharsetName = null;  /* This seems to be a number e.g. 106 for UTF-8 CharacterSets
                                                holds a method for the mapping. */
        public String mFileName = null;     /* Do not seem to be used */
        public byte[] mData = null;        /* The raw un-encoded data e.g. the raw jpeg data or the text.getBytes("utf-8") */


        String getDataAsString() {
            String result = null;
            String charset = mCharsetName;
            // Figure out if we support the charset, else fall back to UTF-8, as this is what
            // the MAP specification suggest to use, and is compatible with US-ASCII.
            if(charset == null){
                charset = "UTF-8";
            } else {
                charset = charset.toUpperCase();
                try {
                    if(Charset.isSupported(charset) == false) {
                        charset = "UTF-8";
                    }
                } catch (IllegalCharsetNameException e) {
                    Log.w(TAG, "Received unknown charset: " + charset + " - using UTF-8.");
                    charset = "UTF-8";
                }
            }
            try{
                result = new String(mData, charset);
            } catch (UnsupportedEncodingException e) {
                /* This cannot happen unless Charset.isSupported() is out of sync with String */
                try{
                    result = new String(mData, "UTF-8");
                } catch (UnsupportedEncodingException e2) {/* This cannot happen */}
            }
            return result;
        }

        public void encode(StringBuilder sb, String boundaryTag, boolean last) throws UnsupportedEncodingException {
            sb.append("--").append(boundaryTag).append("\r\n");
            if(mContentType != null)
                sb.append("Content-Type: ").append(mContentType);
            if(mCharsetName != null)
                sb.append("; ").append("charset=\"").append(mCharsetName).append("\"");
            sb.append("\r\n");
            if(mContentLocation != null)
                sb.append("Content-Location: ").append(mContentLocation).append("\r\n");
            if(mContentId != null)
                sb.append("Content-ID: ").append(mContentId).append("\r\n");
            if(mContentDisposition != null)
                sb.append("Content-Disposition: ").append(mContentDisposition).append("\r\n");
            if(mData != null) {
                /* TODO: If errata 4176 is adopted in the current form (it is not in either 1.1 or 1.2),
                the below use of UTF-8 is not allowed, Base64 should be used for text. */

                if(mContentType != null &&
                        (mContentType.toUpperCase().contains("TEXT") ||
                         mContentType.toUpperCase().contains("SMIL") )) {
                    sb.append("Content-Transfer-Encoding: 8BIT\r\n\r\n"); // Add the header split empty line
                    sb.append(new String(mData,"UTF-8")).append("\r\n");
                }
                else {
                    sb.append("Content-Transfer-Encoding: Base64\r\n\r\n"); // Add the header split empty line
                    sb.append(Base64.encodeToString(mData, Base64.DEFAULT)).append("\r\n");
                }
            }
            if(last) {
                sb.append("--").append(boundaryTag).append("--").append("\r\n");
            }
        }

        public void encodePlainText(StringBuilder sb) throws UnsupportedEncodingException {
            if(mContentType != null && mContentType.toUpperCase().contains("TEXT")) {
                sb.append(new String(mData,"UTF-8")).append("\r\n");
            } else if(mContentType != null && mContentType.toUpperCase().contains("/SMIL")) {
                /* Skip the smil.xml, as no-one knows what it is. */
            } else {
                /* Not a text part, just print the filename or part name if they exist. */
                if(mPartName != null)
                    sb.append("<").append(mPartName).append(">\r\n");
                else
                    sb.append("<").append("attachment").append(">\r\n");
            }
        }
    }

    private long date = INVALID_VALUE;
    private String subject = null;
    private ArrayList<Rfc822Token> from = null;   // Shall not be empty
    private ArrayList<Rfc822Token> sender = null;   // Shall not be empty
    private ArrayList<Rfc822Token> to = null;     // Shall not be empty
    private ArrayList<Rfc822Token> cc = null;     // Can be empty
    private ArrayList<Rfc822Token> bcc = null;    // Can be empty
    private ArrayList<Rfc822Token> replyTo = null;// Can be empty
    private String messageId = null;
    private ArrayList<MimePart> parts = null;
    private String contentType = null;
    private String boundary = null;
    private boolean textOnly = false;
    private boolean includeAttachments;
    private boolean hasHeaders = false;
    private String encoding = null;

    private String getBoundary() {
        if(boundary == null)
            // Include "=_" as these cannot occur in quoted printable text
            boundary = "--=_" + UUID.randomUUID();
        return boundary;
    }

    /**
     * @return the parts
     */
    public ArrayList<MimePart> getMimeParts() {
        return parts;
    }

    public String getMessageAsText() {
        StringBuilder sb = new StringBuilder();
        if(subject != null && !subject.isEmpty()) {
            sb.append("<Sub:").append(subject).append("> ");
        }
        if(parts != null) {
            for(MimePart part : parts) {
                if(part.mContentType.toUpperCase().contains("TEXT")) {
                    sb.append(new String(part.mData));
                }
            }
        }
        return sb.toString();
    }
    public MimePart addMimePart() {
        if(parts == null)
            parts = new ArrayList<BluetoothMapbMessageMms.MimePart>();
        MimePart newPart = new MimePart();
        parts.add(newPart);
        return newPart;
    }
    public String getDateString() {
        SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.US);
        Date dateObj = new Date(date);
        return format.format(dateObj); // Format according to RFC 2822 page 14
    }
    public long getDate() {
        return date;
    }
    public void setDate(long date) {
        this.date = date;
    }
    public String getSubject() {
        return subject;
    }
    public void setSubject(String subject) {
        this.subject = subject;
    }
    public ArrayList<Rfc822Token> getFrom() {
        return from;
    }
    public void setFrom(ArrayList<Rfc822Token> from) {
        this.from = from;
    }
    public void addFrom(String name, String address) {
        if(this.from == null)
            this.from = new ArrayList<Rfc822Token>(1);
        this.from.add(new Rfc822Token(name, address, null));
    }
    public ArrayList<Rfc822Token> getSender() {
        return sender;
    }
    public void setSender(ArrayList<Rfc822Token> sender) {
        this.sender = sender;
    }
    public void addSender(String name, String address) {
        if(this.sender == null)
            this.sender = new ArrayList<Rfc822Token>(1);
        this.sender.add(new Rfc822Token(name,address,null));
    }
    public ArrayList<Rfc822Token> getTo() {
        return to;
    }
    public void setTo(ArrayList<Rfc822Token> to) {
        this.to = to;
    }
    public void addTo(String name, String address) {
        if(this.to == null)
            this.to = new ArrayList<Rfc822Token>(1);
        this.to.add(new Rfc822Token(name, address, null));
    }
    public ArrayList<Rfc822Token> getCc() {
        return cc;
    }
    public void setCc(ArrayList<Rfc822Token> cc) {
        this.cc = cc;
    }
    public void addCc(String name, String address) {
        if(this.cc == null)
            this.cc = new ArrayList<Rfc822Token>(1);
        this.cc.add(new Rfc822Token(name, address, null));
    }
    public ArrayList<Rfc822Token> getBcc() {
        return bcc;
    }
    public void setBcc(ArrayList<Rfc822Token> bcc) {
        this.bcc = bcc;
    }
    public void addBcc(String name, String address) {
        if(this.bcc == null)
            this.bcc = new ArrayList<Rfc822Token>(1);
        this.bcc.add(new Rfc822Token(name, address, null));
    }
    public ArrayList<Rfc822Token> getReplyTo() {
        return replyTo;
    }
    public void setReplyTo(ArrayList<Rfc822Token> replyTo) {
        this.replyTo = replyTo;
    }
    public void addReplyTo(String name, String address) {
        if(this.replyTo == null)
            this.replyTo = new ArrayList<Rfc822Token>(1);
        this.replyTo.add(new Rfc822Token(name, address, null));
    }
    public void setMessageId(String messageId) {
        this.messageId = messageId;
    }
    public String getMessageId() {
        return messageId;
    }
    public void setContentType(String contentType) {
        this.contentType = contentType;
    }
    public String getContentType() {
        return contentType;
    }
    public void setTextOnly(boolean textOnly) {
        this.textOnly = textOnly;
    }
    public boolean getTextOnly() {
        return textOnly;
    }
    public void setIncludeAttachments(boolean includeAttachments) {
        this.includeAttachments = includeAttachments;
    }
    public boolean getIncludeAttachments() {
        return includeAttachments;
    }
    public void updateCharset() {
        if(parts != null) {
            mCharset = null;
            for(MimePart part : parts) {
                if(part.mContentType != null &&
                   part.mContentType.toUpperCase().contains("TEXT")) {
                    mCharset = "UTF-8";
                    if(V) Log.v(TAG,"Charset set to UTF-8");
                    break;
                }
            }
        }
    }
    public int getSize() {
        int message_size = 0;
        if(parts != null) {
            for(MimePart part : parts) {
                message_size += part.mData.length;
            }
        }
        return message_size;
    }

    /**
     * Encode an address header, and perform folding if needed.
     * @param sb The stringBuilder to write to
     * @param headerName The RFC 2822 header name
     * @param addresses the reformatted address substrings to encode.
     */
    public void encodeHeaderAddresses(StringBuilder sb, String headerName,
            ArrayList<Rfc822Token> addresses) {
        /* TODO: Do we need to encode the addresses if they contain illegal characters?
         * This depends of the outcome of errata 4176. The current spec. states to use UTF-8
         * where possible, but the RFCs states to use US-ASCII for the headers - hence encoding
         * would be needed to support non US-ASCII characters. But the MAP spec states not to
         * use any encoding... */
        int partLength, lineLength = 0;
        lineLength += headerName.getBytes().length;
        sb.append(headerName);
        for(Rfc822Token address : addresses) {
            partLength = address.toString().getBytes().length+1;
            // Add folding if needed
            if(lineLength + partLength >= 998) // max line length in RFC2822
            {
                sb.append("\r\n "); // Append a FWS (folding whitespace)
                lineLength = 0;
            }
            sb.append(address.toString()).append(";");
            lineLength += partLength;
        }
        sb.append("\r\n");
    }

    public void encodeHeaders(StringBuilder sb) throws UnsupportedEncodingException
    {
        /* TODO: From RFC-4356 - about the RFC-(2)822 headers:
         *    "Current Internet Message format requires that only 7-bit US-ASCII
         *     characters be present in headers.  Non-7-bit characters in an address
         *     domain must be encoded with [IDN].  If there are any non-7-bit
         *     characters in the local part of an address, the message MUST be
         *     rejected.  Non-7-bit characters elsewhere in a header MUST be encoded
         *     according to [Hdr-Enc]."
         *    We need to add the address encoding in encodeHeaderAddresses, but it is not
         *    straight forward, as it is unclear how to do this.  */
        if (date != INVALID_VALUE)
            sb.append("Date: ").append(getDateString()).append("\r\n");
        /* According to RFC-2822 headers must use US-ASCII, where the MAP specification states
         * UTF-8 should be used for the entire <bmessage-body-content>. We let the MAP specification
         * take precedence above the RFC-2822.
         */
        /* If we are to use US-ASCII anyway, here is the code for it for base64.
          if (subject != null){
            // Use base64 encoding for the subject, as it may contain non US-ASCII characters or other
            // illegal (RFC822 header), and android do not seem to have encoders/decoders for quoted-printables
            sb.append("Subject:").append("=?utf-8?B?");
            sb.append(Base64.encodeToString(subject.getBytes("utf-8"), Base64.DEFAULT));
            sb.append("?=\r\n");
        }*/
        if (subject != null)
            sb.append("Subject: ").append(subject).append("\r\n");
        if(from == null)
            sb.append("From: \r\n");
        if(from != null)
            encodeHeaderAddresses(sb, "From: ", from); // This includes folding if needed.
        if(sender != null)
            encodeHeaderAddresses(sb, "Sender: ", sender); // This includes folding if needed.
        /* For MMS one recipient(to, cc or bcc) must exists, if none: 'To:  undisclosed-
         * recipients:;' could be used.
         */
        if(to == null && cc == null && bcc == null)
            sb.append("To:  undisclosed-recipients:;\r\n");
        if(to != null)
            encodeHeaderAddresses(sb, "To: ", to); // This includes folding if needed.
        if(cc != null)
            encodeHeaderAddresses(sb, "Cc: ", cc); // This includes folding if needed.
        if(bcc != null)
            encodeHeaderAddresses(sb, "Bcc: ", bcc); // This includes folding if needed.
        if(replyTo != null)
            encodeHeaderAddresses(sb, "Reply-To: ", replyTo); // This includes folding if needed.
        if(includeAttachments == true)
        {
            if(messageId != null)
                sb.append("Message-Id: ").append(messageId).append("\r\n");
            if(contentType != null)
                sb.append("Content-Type: ").append(contentType).append("; boundary=").append(getBoundary()).append("\r\n");
        }
        sb.append("\r\n"); // If no headers exists, we still need two CRLF, hence keep it out of the if above.
    }

    /* Notes on MMS
     * ------------
     * According to rfc4356 all headers of a MMS converted to an E-mail must use
     * 7-bit encoding. According the the MAP specification only 8-bit encoding is
     * allowed - hence the bMessage-body should contain no SMTP headers. (Which makes
     * sense, since the info is already present in the bMessage properties.)
     * The result is that no information from RFC4356 is needed, since it does not
     * describe any mapping between MMS content and E-mail content.
     * Suggestion:
     * Clearly state in the MAP specification that
     * only the actual message content should be included in the <bmessage-body-content>.
     * Correct the Example to not include the E-mail headers, and in stead show how to
     * include a picture or another binary attachment.
     *
     * If the headers should be included, clearly state which, as the example clearly shows
     * that some of the headers should be excluded.
     * Additionally it is not clear how to handle attachments. There is a parameter in the
     * get message to include attachments, but since only 8-bit encoding is allowed,
     * (hence neither base64 nor binary) there is no mechanism to embed the attachment in
     * the <bmessage-body-content>.
     *
     * UPDATE: Errata 4176 allows the needed encoding typed inside the <bmessage-body-content>
     * including Base64 and Quoted Printables - hence it is possible to encode non-us-ascii
     * messages - e.g. pictures and utf-8 strings with non-us-ascii content.
     * It have not yet been adopted, but since the comments clearly suggest that it is allowed
     * to use encoding schemes for non-text parts, it is still not clear what to do about non
     * US-ASCII text in the headers.
     * */

    /**
     * Encode the bMessage as a MMS
     * @return
     * @throws UnsupportedEncodingException
     */
    public byte[] encodeMms() throws UnsupportedEncodingException
    {
        ArrayList<byte[]> bodyFragments = new ArrayList<byte[]>();
        StringBuilder sb = new StringBuilder();
        int count = 0;
        String mmsBody;

        encoding = "8BIT"; // The encoding used

        encodeHeaders(sb);
        if(parts != null) {
            if(getIncludeAttachments() == false) {
                for(MimePart part : parts) {
                    part.encodePlainText(sb); /* We call encode on all parts, to include a tag, where an attachment is missing. */
                }
            } else {
                for(MimePart part : parts) {
                    count++;
                    part.encode(sb, getBoundary(), (count == parts.size()));
                }
            }
        }

        mmsBody = sb.toString();

        if(mmsBody != null) {
            String tmpBody = mmsBody.replaceAll("END:MSG", "/END\\:MSG"); // Replace any occurrences of END:MSG with \END:MSG
            bodyFragments.add(tmpBody.getBytes("UTF-8"));
        } else {
            bodyFragments.add(new byte[0]);
        }

        return encodeGeneric(bodyFragments);
    }


    /**
     * Try to parse the hdrPart string as e-mail headers.
     * @param hdrPart The string to parse.
     * @return Null if the entire string were e-mail headers. The part of the string in which
     * no headers were found.
     */
    private String parseMmsHeaders(String hdrPart) {
        String[] headers = hdrPart.split("\r\n");
        if(D) Log.d(TAG,"Header count=" + headers.length);
        String header;
        hasHeaders = false;

        for(int i = 0, c = headers.length; i < c; i++) {
            header = headers[i];
            if(D) Log.d(TAG,"Header[" + i + "]: " + header);
            /* We need to figure out if any headers are present, in cases where devices do not follow the e-mail RFCs.
             * Skip empty lines, and then parse headers until a non-header line is found, at which point we treat the
             * remaining as plain text.
             */
            if(header.trim() == "")
                continue;
            String[] headerParts = header.split(":",2);
            if(headerParts.length != 2) {
                // We treat the remaining content as plain text.
                StringBuilder remaining = new StringBuilder();
                for(; i < c; i++)
                    remaining.append(headers[i]);

                return remaining.toString();
            }

            String headerType = headerParts[0].toUpperCase();
            String headerValue = headerParts[1].trim();

            // Address headers
            /* If this is empty, the MSE needs to fill it in before sending the message.
             * This happens when sending the MMS.
             */
            if(headerType.contains("FROM")) {
                Rfc822Token tokens[] = Rfc822Tokenizer.tokenize(headerValue);
                from = new ArrayList<Rfc822Token>(Arrays.asList(tokens));
            } else if(headerType.contains("TO")) {
                Rfc822Token tokens[] = Rfc822Tokenizer.tokenize(headerValue);
                to = new ArrayList<Rfc822Token>(Arrays.asList(tokens));
            } else if(headerType.contains("CC")) {
                Rfc822Token tokens[] = Rfc822Tokenizer.tokenize(headerValue);
                cc = new ArrayList<Rfc822Token>(Arrays.asList(tokens));
            } else if(headerType.contains("BCC")) {
                Rfc822Token tokens[] = Rfc822Tokenizer.tokenize(headerValue);
                bcc = new ArrayList<Rfc822Token>(Arrays.asList(tokens));
            } else if(headerType.contains("REPLY-TO")) {
                Rfc822Token tokens[] = Rfc822Tokenizer.tokenize(headerValue);
                replyTo = new ArrayList<Rfc822Token>(Arrays.asList(tokens));
            } else if(headerType.contains("SUBJECT")) { // Other headers
                subject = headerValue;
            } else if(headerType.contains("MESSAGE-ID")) {
                messageId = headerValue;
            } else if(headerType.contains("DATE")) {
                /* The date is not needed, as the time stamp will be set in the DB
                 * when the message is send. */
            } else if(headerType.contains("MIME-VERSION")) {
                /* The mime version is not needed */
            } else if(headerType.contains("CONTENT-TYPE")) {
                String[] contentTypeParts = headerValue.split(";");
                contentType = contentTypeParts[0];
                // Extract the boundary if it exists
                for(int j=1, n=contentTypeParts.length; j<n; j++)
                {
                    if(contentTypeParts[j].contains("boundary")) {
                        boundary = contentTypeParts[j].split("boundary[\\s]*=", 2)[1].trim();
                        // removing quotes from boundary string
                        if ((boundary.charAt(0) == '\"') && (boundary.charAt(boundary.length()-1) == '\"'))
                            boundary = boundary.substring(1, boundary.length()-1);
                        if(D) Log.d(TAG,"Boundary tag=" + boundary);
                    } else if(contentTypeParts[j].contains("charset")) {
                        mCharset = contentTypeParts[j].split("charset[\\s]*=", 2)[1].trim();
                    }
                }
            } else if(headerType.contains("CONTENT-TRANSFER-ENCODING")) {
                encoding = headerValue;
            } else {
                if(D) Log.w(TAG,"Skipping unknown header: " + headerType + " (" + header + ")");
            }
        }
        return null;
    }

    private void parseMmsMimePart(String partStr) {
        String[] parts = partStr.split("\r\n\r\n", 2); // Split the header from the body
        MimePart newPart = addMimePart();
        String partEncoding = encoding; /* Use the overall encoding as default */
        String body;

        String[] headers = parts[0].split("\r\n");
        if(D) Log.d(TAG, "parseMmsMimePart: headers count=" + headers.length);

        if(parts.length != 2) {
            body = partStr;
        } else {
            for(String header : headers) {
                // Skip empty lines(the \r\n after the boundary tag) and endBoundary tags
                if((header.length() == 0) || (header.trim().isEmpty()) || header.trim().equals("--"))
                    continue;

                String[] headerParts = header.split(":",2);
                if(headerParts.length != 2) {
                    if(D) Log.w(TAG, "part-Header not formatted correctly: ");
                    continue;
                }
                if(D) Log.d(TAG, "parseMmsMimePart: header=" + header);
                String headerType = headerParts[0].toUpperCase();
                String headerValue = headerParts[1].trim();
                if(headerType.contains("CONTENT-TYPE")) {
                    String[] contentTypeParts = headerValue.split(";");
                    newPart.mContentType = contentTypeParts[0];
                    // Extract the boundary if it exists
                    for(int j=1, n=contentTypeParts.length; j<n; j++)
                    {
                        String value = contentTypeParts[j].toLowerCase();
                        if(value.contains("charset")) {
                            newPart.mCharsetName = value.split("charset[\\s]*=", 2)[1].trim();
                        }
                    }
                }
                else if(headerType.contains("CONTENT-LOCATION")) {
                    // This is used if the smil refers to a file name in its src
                    newPart.mContentLocation = headerValue;
                    newPart.mPartName = headerValue;
                }
                else if(headerType.contains("CONTENT-TRANSFER-ENCODING")) {
                    partEncoding = headerValue;
                }
                else if(headerType.contains("CONTENT-ID")) {
                    // This is used if the smil refers to a cid:<xxx> in it's src
                    newPart.mContentId = headerValue;
                }
                else if(headerType.contains("CONTENT-DISPOSITION")) {
                    // This is used if the smil refers to a cid:<xxx> in it's src
                    newPart.mContentDisposition = headerValue;
                }
                else {
                    if(D) Log.w(TAG,"Skipping unknown part-header: " + headerType + " (" + header + ")");
                }
            }
            body = parts[1];
            if(body.length() > 2) {
                if(body.charAt(body.length()-2) == '\r'
                        && body.charAt(body.length()-2) == '\n') {
                    body = body.substring(0, body.length()-2);
                }
            }
        }
        // Now for the body
        newPart.mData = decodeBody(body, partEncoding, newPart.mCharsetName);
    }

    private void parseMmsMimeBody(String body) {
        MimePart newPart = addMimePart();
        newPart.mCharsetName = mCharset;
        newPart.mData = decodeBody(body, encoding, mCharset);
    }

    private byte[] decodeBody(String body, String encoding, String charset) {
        if(encoding != null && encoding.toUpperCase().contains("BASE64")) {
            return Base64.decode(body, Base64.DEFAULT);
        } else if(encoding != null && encoding.toUpperCase().contains("QUOTED-PRINTABLE")) {
            return quotedPrintableToUtf8(body, charset);
        }else{
            // TODO: handle other encoding types? - here we simply store the string data as bytes
            try {

                return body.getBytes("UTF-8");
            } catch (UnsupportedEncodingException e) {
                // This will never happen, as UTF-8 is mandatory on Android platforms
            }
        }
        return null;
    }

    private void parseMms(String message) {
        /* Overall strategy for decoding:
         * 1) split on first empty line to extract the header
         * 2) unfold and parse headers
         * 3) split on boundary to split into parts (or use the remaining as a part,
         *    if part is not found)
         * 4) parse each part
         * */
        String[] messageParts;
        String[] mimeParts;
        String remaining = null;
        String messageBody = null;
        message = message.replaceAll("\\r\\n[ \\\t]+", ""); // Unfold
        messageParts = message.split("\r\n\r\n", 2); // Split the header from the body
        if(messageParts.length != 2) {
            // Handle entire message as plain text
            messageBody = message;
        }
        else
        {
            remaining = parseMmsHeaders(messageParts[0]);
            // If we have some text not being a header, add it to the message body.
            if(remaining != null) {
                messageBody = remaining + messageParts[1];
                if(D) Log.d(TAG, "parseMms remaining=" + remaining );
            } else {
                messageBody = messageParts[1];
            }
        }

        if(boundary == null)
        {
            // If the boundary is not set, handle as non-multi-part
            parseMmsMimeBody(messageBody);
            setTextOnly(true);
            if(contentType == null)
                contentType = "text/plain";
            parts.get(0).mContentType = contentType;
        }
        else
        {
            mimeParts = messageBody.split("--" + boundary);
            if(D) Log.d(TAG, "mimePart count=" + mimeParts.length);
            // Part 0 is the message to clients not capable of decoding MIME
            for(int i = 1; i < mimeParts.length - 1; i++) {
                String part = mimeParts[i];
                if (part != null && (part.length() > 0))
                    parseMmsMimePart(part);
            }
        }
    }

    /**
     * Convert a quoted-printable encoded string to a UTF-8 string:
     *  - Remove any soft line breaks: "=<CRLF>"
     *  - Convert all "=xx" to the corresponding byte
     * @param text quoted-printable encoded UTF-8 text
     * @return decoded UTF-8 string
     */
    public static byte[] quotedPrintableToUtf8(String text, String charset) {
        byte[] output = new byte[text.length()]; // We allocate for the worst case memory need
        byte[] input = null;
        try {
            input = text.getBytes("US-ASCII");
        } catch (UnsupportedEncodingException e) {
            /* This cannot happen as "US-ASCII" is supported for all Java implementations */ }

        if(input == null){
            return "".getBytes();
        }

        int in, out, stopCnt = input.length-2; // Leave room for peaking the next two bytes

        /* Algorithm:
         *  - Search for token, copying all non token chars
         * */
        for(in=0, out=0; in < stopCnt; in++){
            byte b0 = input[in];
            if(b0 == '=') {
                byte b1 = input[++in];
                byte b2 = input[++in];
                if(b1 == '\r' && b2 == '\n') {
                    continue; // soft line break, remove all tree;
                }
                if(((b1 >= '0' && b1 <= '9') || (b1 >= 'A' && b1 <= 'F') || (b1 >= 'a' && b1 <= 'f')) &&
                   ((b2 >= '0' && b2 <= '9') || (b2 >= 'A' && b2 <= 'F') || (b2 >= 'a' && b2 <= 'f'))) {
                    if(V)Log.v(TAG, "Found hex number: " + String.format("%c%c", b1, b2));
                    if(b1 <= '9')       b1 = (byte) (b1 - '0');
                    else if (b1 <= 'F') b1 = (byte) (b1 - 'A' + 10);
                    else if (b1 <= 'f') b1 = (byte) (b1 - 'a' + 10);

                    if(b2 <= '9')       b2 = (byte) (b2 - '0');
                    else if (b2 <= 'F') b2 = (byte) (b2 - 'A' + 10);
                    else if (b2 <= 'f') b2 = (byte) (b2 - 'a' + 10);

                    if(V)Log.v(TAG, "Resulting nibble values: " + String.format("b1=%x b2=%x", b1, b2));

                    output[out++] = (byte)(b1<<4 | b2); // valid hex char, append
                    if(V)Log.v(TAG, "Resulting value: "  + String.format("0x%2x", output[out-1]));
                    continue;
                }
                Log.w(TAG, "Received wrongly quoted printable encoded text. Continuing at best effort...");
                /* If we get a '=' without either a hex value or CRLF following, just add it and
                 * rewind the in counter. */
                output[out++] = b0;
                in -= 2;
                continue;
            } else {
                output[out++] = b0;
                continue;
            }
        }

        // Just add any remaining characters. If they contain any encoding, it is invalid,
        // and best effort would be just to display the characters.
        while (in < input.length) {
            output[out++] = input[in++];
        }

        String result = null;
        // Figure out if we support the charset, else fall back to UTF-8, as this is what
        // the MAP specification suggest to use, and is compatible with US-ASCII.
        if(charset == null){
            charset = "UTF-8";
        } else {
            charset = charset.toUpperCase();
            try {
                if(Charset.isSupported(charset) == false) {
                    charset = "UTF-8";
                }
            } catch (IllegalCharsetNameException e) {
                Log.w(TAG, "Received unknown charset: " + charset + " - using UTF-8.");
                charset = "UTF-8";
            }
        }
        try{
            result = new String(output, 0, out, charset);
        } catch (UnsupportedEncodingException e) {
            /* This cannot happen unless Charset.isSupported() is out of sync with String */
            try{
                result = new String(output, 0, out, "UTF-8");
            } catch (UnsupportedEncodingException e2) {/* This cannot happen */}
        }
        return result.getBytes(); /* return the result as "UTF-8" bytes */
    }

    /* Notes on SMIL decoding (from http://tools.ietf.org/html/rfc2557):
     * src="filename.jpg" refers to a part with Content-Location: filename.jpg
     * src="cid:1234@hest.net" refers to a part with Content-ID:<1234@hest.net>*/
    @Override
    public void parseMsgPart(String msgPart) {
        parseMms(msgPart);

    }

    @Override
    public void parseMsgInit() {
        // Not used for e-mail

    }

    @Override
    public byte[] encode() throws UnsupportedEncodingException {
        return encodeMms();
    }

}
