Home | History | Annotate | Download | only in map
      1 /*
      2 * Copyright (C) 2013 Samsung System LSI
      3 * Licensed under the Apache License, Version 2.0 (the "License");
      4 * you may not use this file except in compliance with the License.
      5 * You may obtain a copy of the License at
      6 *
      7 *      http://www.apache.org/licenses/LICENSE-2.0
      8 *
      9 * Unless required by applicable law or agreed to in writing, software
     10 * distributed under the License is distributed on an "AS IS" BASIS,
     11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     12 * See the License for the specific language governing permissions and
     13 * limitations under the License.
     14 */
     15 package com.android.bluetooth.map;
     16 
     17 import android.text.util.Rfc822Token;
     18 import android.text.util.Rfc822Tokenizer;
     19 import android.util.Base64;
     20 import android.util.Log;
     21 
     22 import java.io.UnsupportedEncodingException;
     23 import java.nio.charset.Charset;
     24 import java.nio.charset.IllegalCharsetNameException;
     25 import java.text.SimpleDateFormat;
     26 import java.util.ArrayList;
     27 import java.util.Arrays;
     28 import java.util.Date;
     29 import java.util.Locale;
     30 import java.util.UUID;
     31 
     32 public class BluetoothMapbMessageMime extends BluetoothMapbMessage {
     33 
     34     public static class MimePart {
     35         public long mId = INVALID_VALUE;   /* The _id from the content provider, can be used to
     36                                             * sort the parts if needed */
     37         public String mContentType = null; /* The mime type, e.g. text/plain */
     38         public String mContentId = null;
     39         public String mContentLocation = null;
     40         public String mContentDisposition = null;
     41         public String mPartName = null;    /* e.g. text_1.txt*/
     42         public String mCharsetName = null; /* This seems to be a number e.g. 106 for UTF-8
     43                                               CharacterSets holds a method for the mapping. */
     44         public String mFileName = null;     /* Do not seem to be used */
     45         public byte[] mData = null;        /* The raw un-encoded data e.g. the raw
     46                                             * jpeg data or the text.getBytes("utf-8") */
     47 
     48 
     49         String getDataAsString() {
     50             String result = null;
     51             String charset = mCharsetName;
     52             // Figure out if we support the charset, else fall back to UTF-8, as this is what
     53             // the MAP specification suggest to use, and is compatible with US-ASCII.
     54             if (charset == null) {
     55                 charset = "UTF-8";
     56             } else {
     57                 charset = charset.toUpperCase();
     58                 try {
     59                     if (!Charset.isSupported(charset)) {
     60                         charset = "UTF-8";
     61                     }
     62                 } catch (IllegalCharsetNameException e) {
     63                     Log.w(TAG, "Received unknown charset: " + charset + " - using UTF-8.");
     64                     charset = "UTF-8";
     65                 }
     66             }
     67             try {
     68                 result = new String(mData, charset);
     69             } catch (UnsupportedEncodingException e) {
     70                 /* This cannot happen unless Charset.isSupported() is out of sync with String */
     71                 try {
     72                     result = new String(mData, "UTF-8");
     73                 } catch (UnsupportedEncodingException e2) {
     74                     Log.e(TAG, "getDataAsString: " + e);
     75                 }
     76             }
     77             return result;
     78         }
     79 
     80         public void encode(StringBuilder sb, String boundaryTag, boolean last)
     81                 throws UnsupportedEncodingException {
     82             sb.append("--").append(boundaryTag).append("\r\n");
     83             if (mContentType != null) {
     84                 sb.append("Content-Type: ").append(mContentType);
     85             }
     86             if (mCharsetName != null) {
     87                 sb.append("; ").append("charset=\"").append(mCharsetName).append("\"");
     88             }
     89             sb.append("\r\n");
     90             if (mContentLocation != null) {
     91                 sb.append("Content-Location: ").append(mContentLocation).append("\r\n");
     92             }
     93             if (mContentId != null) {
     94                 sb.append("Content-ID: ").append(mContentId).append("\r\n");
     95             }
     96             if (mContentDisposition != null) {
     97                 sb.append("Content-Disposition: ").append(mContentDisposition).append("\r\n");
     98             }
     99             if (mData != null) {
    100                 /* TODO: If errata 4176 is adopted in the current form (it is not in either 1.1
    101                 or 1.2),
    102                 the below use of UTF-8 is not allowed, Base64 should be used for text. */
    103 
    104                 if (mContentType != null && (mContentType.toUpperCase().contains("TEXT")
    105                         || mContentType.toUpperCase().contains("SMIL"))) {
    106                     String text = new String(mData, "UTF-8");
    107                     if (text.getBytes().length == text.getBytes("UTF-8").length) {
    108                         /* Add the header split empty line */
    109                         sb.append("Content-Transfer-Encoding: 8BIT\r\n\r\n");
    110                     } else {
    111                         /* Add the header split empty line */
    112                         sb.append("Content-Transfer-Encoding: Quoted-Printable\r\n\r\n");
    113                         text = BluetoothMapUtils.encodeQuotedPrintable(mData);
    114                     }
    115                     sb.append(text).append("\r\n");
    116                 } else {
    117                     /* Add the header split empty line */
    118                     sb.append("Content-Transfer-Encoding: Base64\r\n\r\n");
    119                     sb.append(Base64.encodeToString(mData, Base64.DEFAULT)).append("\r\n");
    120                 }
    121             }
    122             if (last) {
    123                 sb.append("--").append(boundaryTag).append("--").append("\r\n");
    124             }
    125         }
    126 
    127         public void encodePlainText(StringBuilder sb) throws UnsupportedEncodingException {
    128             if (mContentType != null && mContentType.toUpperCase().contains("TEXT")) {
    129                 String text = new String(mData, "UTF-8");
    130                 if (text.getBytes().length != text.getBytes("UTF-8").length) {
    131                     text = BluetoothMapUtils.encodeQuotedPrintable(mData);
    132                 }
    133                 sb.append(text).append("\r\n");
    134             } else if (mContentType != null && mContentType.toUpperCase().contains("/SMIL")) {
    135                 /* Skip the smil.xml, as no-one knows what it is. */
    136             } else {
    137                 /* Not a text part, just print the filename or part name if they exist. */
    138                 if (mPartName != null) {
    139                     sb.append("<").append(mPartName).append(">\r\n");
    140                 } else {
    141                     sb.append("<").append("attachment").append(">\r\n");
    142                 }
    143             }
    144         }
    145     }
    146 
    147     private long mDate = INVALID_VALUE;
    148     private String mSubject = null;
    149     private ArrayList<Rfc822Token> mFrom = null;   // Shall not be empty
    150     private ArrayList<Rfc822Token> mSender = null;   // Shall not be empty
    151     private ArrayList<Rfc822Token> mTo = null;     // Shall not be empty
    152     private ArrayList<Rfc822Token> mCc = null;     // Can be empty
    153     private ArrayList<Rfc822Token> mBcc = null;    // Can be empty
    154     private ArrayList<Rfc822Token> mReplyTo = null; // Can be empty
    155     private String mMessageId = null;
    156     private ArrayList<MimePart> mParts = null;
    157     private String mContentType = null;
    158     private String mBoundary = null;
    159     private boolean mTextonly = false;
    160     private boolean mIncludeAttachments;
    161     private boolean mHasHeaders = false;
    162     private String mMyEncoding = null;
    163 
    164     private String getBoundary() {
    165         // Include "=_" as these cannot occur in quoted printable text
    166         if (mBoundary == null) {
    167             mBoundary = "--=_" + UUID.randomUUID();
    168         }
    169         return mBoundary;
    170     }
    171 
    172     /**
    173      * @return the parts
    174      */
    175     public ArrayList<MimePart> getMimeParts() {
    176         return mParts;
    177     }
    178 
    179     public String getMessageAsText() {
    180         StringBuilder sb = new StringBuilder();
    181         if (mSubject != null && !mSubject.isEmpty()) {
    182             sb.append("<Sub:").append(mSubject).append("> ");
    183         }
    184         if (mParts != null) {
    185             for (MimePart part : mParts) {
    186                 if (part.mContentType.toUpperCase().contains("TEXT")) {
    187                     sb.append(new String(part.mData));
    188                 }
    189             }
    190         }
    191         return sb.toString();
    192     }
    193 
    194     public MimePart addMimePart() {
    195         if (mParts == null) {
    196             mParts = new ArrayList<BluetoothMapbMessageMime.MimePart>();
    197         }
    198         MimePart newPart = new MimePart();
    199         mParts.add(newPart);
    200         return newPart;
    201     }
    202 
    203     public String getDateString() {
    204         SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.US);
    205         Date dateObj = new Date(mDate);
    206         return format.format(dateObj); // Format according to RFC 2822 page 14
    207     }
    208 
    209     public long getDate() {
    210         return mDate;
    211     }
    212 
    213     public void setDate(long date) {
    214         this.mDate = date;
    215     }
    216 
    217     public String getSubject() {
    218         return mSubject;
    219     }
    220 
    221     public void setSubject(String subject) {
    222         this.mSubject = subject;
    223     }
    224 
    225     public ArrayList<Rfc822Token> getFrom() {
    226         return mFrom;
    227     }
    228 
    229     public void setFrom(ArrayList<Rfc822Token> from) {
    230         this.mFrom = from;
    231     }
    232 
    233     public void addFrom(String name, String address) {
    234         if (this.mFrom == null) {
    235             this.mFrom = new ArrayList<Rfc822Token>(1);
    236         }
    237         this.mFrom.add(new Rfc822Token(name, address, null));
    238     }
    239 
    240     public ArrayList<Rfc822Token> getSender() {
    241         return mSender;
    242     }
    243 
    244     public void setSender(ArrayList<Rfc822Token> sender) {
    245         this.mSender = sender;
    246     }
    247 
    248     public void addSender(String name, String address) {
    249         if (this.mSender == null) {
    250             this.mSender = new ArrayList<Rfc822Token>(1);
    251         }
    252         this.mSender.add(new Rfc822Token(name, address, null));
    253     }
    254 
    255     public ArrayList<Rfc822Token> getTo() {
    256         return mTo;
    257     }
    258 
    259     public void setTo(ArrayList<Rfc822Token> to) {
    260         this.mTo = to;
    261     }
    262 
    263     public void addTo(String name, String address) {
    264         if (this.mTo == null) {
    265             this.mTo = new ArrayList<Rfc822Token>(1);
    266         }
    267         this.mTo.add(new Rfc822Token(name, address, null));
    268     }
    269 
    270     public ArrayList<Rfc822Token> getCc() {
    271         return mCc;
    272     }
    273 
    274     public void setCc(ArrayList<Rfc822Token> cc) {
    275         this.mCc = cc;
    276     }
    277 
    278     public void addCc(String name, String address) {
    279         if (this.mCc == null) {
    280             this.mCc = new ArrayList<Rfc822Token>(1);
    281         }
    282         this.mCc.add(new Rfc822Token(name, address, null));
    283     }
    284 
    285     public ArrayList<Rfc822Token> getBcc() {
    286         return mBcc;
    287     }
    288 
    289     public void setBcc(ArrayList<Rfc822Token> bcc) {
    290         this.mBcc = bcc;
    291     }
    292 
    293     public void addBcc(String name, String address) {
    294         if (this.mBcc == null) {
    295             this.mBcc = new ArrayList<Rfc822Token>(1);
    296         }
    297         this.mBcc.add(new Rfc822Token(name, address, null));
    298     }
    299 
    300     public ArrayList<Rfc822Token> getReplyTo() {
    301         return mReplyTo;
    302     }
    303 
    304     public void setReplyTo(ArrayList<Rfc822Token> replyTo) {
    305         this.mReplyTo = replyTo;
    306     }
    307 
    308     public void addReplyTo(String name, String address) {
    309         if (this.mReplyTo == null) {
    310             this.mReplyTo = new ArrayList<Rfc822Token>(1);
    311         }
    312         this.mReplyTo.add(new Rfc822Token(name, address, null));
    313     }
    314 
    315     public void setMessageId(String messageId) {
    316         this.mMessageId = messageId;
    317     }
    318 
    319     public String getMessageId() {
    320         return mMessageId;
    321     }
    322 
    323     public void setContentType(String contentType) {
    324         this.mContentType = contentType;
    325     }
    326 
    327     public String getContentType() {
    328         return mContentType;
    329     }
    330 
    331     public void setTextOnly(boolean textOnly) {
    332         this.mTextonly = textOnly;
    333     }
    334 
    335     public boolean getTextOnly() {
    336         return mTextonly;
    337     }
    338 
    339     public void setIncludeAttachments(boolean includeAttachments) {
    340         this.mIncludeAttachments = includeAttachments;
    341     }
    342 
    343     public boolean getIncludeAttachments() {
    344         return mIncludeAttachments;
    345     }
    346 
    347     public void updateCharset() {
    348         if (mParts != null) {
    349             mCharset = null;
    350             for (MimePart part : mParts) {
    351                 if (part.mContentType != null && part.mContentType.toUpperCase().contains("TEXT")) {
    352                     mCharset = "UTF-8";
    353                     if (V) {
    354                         Log.v(TAG, "Charset set to UTF-8");
    355                     }
    356                     break;
    357                 }
    358             }
    359         }
    360     }
    361 
    362     public int getSize() {
    363         int messageSize = 0;
    364         if (mParts != null) {
    365             for (MimePart part : mParts) {
    366                 messageSize += part.mData.length;
    367             }
    368         }
    369         return messageSize;
    370     }
    371 
    372     /**
    373      * Encode an address header, and perform folding if needed.
    374      * @param sb The stringBuilder to write to
    375      * @param headerName The RFC 2822 header name
    376      * @param addresses the reformatted address substrings to encode.
    377      */
    378     public void encodeHeaderAddresses(StringBuilder sb, String headerName,
    379             ArrayList<Rfc822Token> addresses) {
    380         /* TODO: Do we need to encode the addresses if they contain illegal characters?
    381          * This depends of the outcome of errata 4176. The current spec. states to use UTF-8
    382          * where possible, but the RFCs states to use US-ASCII for the headers - hence encoding
    383          * would be needed to support non US-ASCII characters. But the MAP spec states not to
    384          * use any encoding... */
    385         int partLength, lineLength = 0;
    386         lineLength += headerName.getBytes().length;
    387         sb.append(headerName);
    388         for (Rfc822Token address : addresses) {
    389             partLength = address.toString().getBytes().length + 1;
    390             // Add folding if needed
    391             if (lineLength + partLength >= 998 /* max line length in RFC2822 */) {
    392                 sb.append("\r\n "); // Append a FWS (folding whitespace)
    393                 lineLength = 0;
    394             }
    395             sb.append(address.toString()).append(";");
    396             lineLength += partLength;
    397         }
    398         sb.append("\r\n");
    399     }
    400 
    401     public void encodeHeaders(StringBuilder sb) throws UnsupportedEncodingException {
    402         /* TODO: From RFC-4356 - about the RFC-(2)822 headers:
    403          *    "Current Internet Message format requires that only 7-bit US-ASCII
    404          *     characters be present in headers.  Non-7-bit characters in an address
    405          *     domain must be encoded with [IDN].  If there are any non-7-bit
    406          *     characters in the local part of an address, the message MUST be
    407          *     rejected.  Non-7-bit characters elsewhere in a header MUST be encoded
    408          *     according to [Hdr-Enc]."
    409          *    We need to add the address encoding in encodeHeaderAddresses, but it is not
    410          *    straight forward, as it is unclear how to do this.  */
    411         if (mDate != INVALID_VALUE) {
    412             sb.append("Date: ").append(getDateString()).append("\r\n");
    413         }
    414         /* According to RFC-2822 headers must use US-ASCII, where the MAP specification states
    415          * UTF-8 should be used for the entire <bmessage-body-content>. We let the MAP specification
    416          * take precedence above the RFC-2822.
    417          */
    418         /* If we are to use US-ASCII anyway, here is the code for it for base64.
    419           if (subject != null){
    420             // Use base64 encoding for the subject, as it may contain non US-ASCII characters or
    421             // other illegal (RFC822 header), and android do not seem to have encoders/decoders
    422             // for quoted-printables
    423             sb.append("Subject:").append("=?utf-8?B?");
    424             sb.append(Base64.encodeToString(subject.getBytes("utf-8"), Base64.DEFAULT));
    425             sb.append("?=\r\n");
    426         }*/
    427         if (mSubject != null) {
    428             sb.append("Subject: ").append(mSubject).append("\r\n");
    429         }
    430         if (mFrom == null) {
    431             sb.append("From: \r\n");
    432         }
    433         if (mFrom != null) {
    434             encodeHeaderAddresses(sb, "From: ", mFrom); // This includes folding if needed.
    435         }
    436         if (mSender != null) {
    437             encodeHeaderAddresses(sb, "Sender: ", mSender); // This includes folding if needed.
    438         }
    439         /* For MMS one recipient(to, cc or bcc) must exists, if none: 'To:  undisclosed-
    440          * recipients:;' could be used.
    441          */
    442         if (mTo == null && mCc == null && mBcc == null) {
    443             sb.append("To:  undisclosed-recipients:;\r\n");
    444         }
    445         if (mTo != null) {
    446             encodeHeaderAddresses(sb, "To: ", mTo); // This includes folding if needed.
    447         }
    448         if (mCc != null) {
    449             encodeHeaderAddresses(sb, "Cc: ", mCc); // This includes folding if needed.
    450         }
    451         if (mBcc != null) {
    452             encodeHeaderAddresses(sb, "Bcc: ", mBcc); // This includes folding if needed.
    453         }
    454         if (mReplyTo != null) {
    455             encodeHeaderAddresses(sb, "Reply-To: ", mReplyTo); // This includes folding if needed.
    456         }
    457         if (mIncludeAttachments) {
    458             if (mMessageId != null) {
    459                 sb.append("Message-Id: ").append(mMessageId).append("\r\n");
    460             }
    461             if (mContentType != null) {
    462                 sb.append("Content-Type: ")
    463                         .append(mContentType)
    464                         .append("; boundary=")
    465                         .append(getBoundary())
    466                         .append("\r\n");
    467             }
    468         }
    469         // If no headers exists, we still need two CRLF, hence keep it out of the if above.
    470         sb.append("\r\n");
    471     }
    472 
    473     /* Notes on MMS
    474      * ------------
    475      * According to rfc4356 all headers of a MMS converted to an E-mail must use
    476      * 7-bit encoding. According the the MAP specification only 8-bit encoding is
    477      * allowed - hence the bMessage-body should contain no SMTP headers. (Which makes
    478      * sense, since the info is already present in the bMessage properties.)
    479      * The result is that no information from RFC4356 is needed, since it does not
    480      * describe any mapping between MMS content and E-mail content.
    481      * Suggestion:
    482      * Clearly state in the MAP specification that
    483      * only the actual message content should be included in the <bmessage-body-content>.
    484      * Correct the Example to not include the E-mail headers, and in stead show how to
    485      * include a picture or another binary attachment.
    486      *
    487      * If the headers should be included, clearly state which, as the example clearly shows
    488      * that some of the headers should be excluded.
    489      * Additionally it is not clear how to handle attachments. There is a parameter in the
    490      * get message to include attachments, but since only 8-bit encoding is allowed,
    491      * (hence neither base64 nor binary) there is no mechanism to embed the attachment in
    492      * the <bmessage-body-content>.
    493      *
    494      * UPDATE: Errata 4176 allows the needed encoding typed inside the <bmessage-body-content>
    495      * including Base64 and Quoted Printables - hence it is possible to encode non-us-ascii
    496      * messages - e.g. pictures and utf-8 strings with non-us-ascii content.
    497      * It have not yet been adopted, but since the comments clearly suggest that it is allowed
    498      * to use encoding schemes for non-text parts, it is still not clear what to do about non
    499      * US-ASCII text in the headers.
    500      * */
    501 
    502     /**
    503      * Encode the bMessage as a Mime message(MMS/IM)
    504      * @return
    505      * @throws UnsupportedEncodingException
    506      */
    507     public byte[] encodeMime() throws UnsupportedEncodingException {
    508         ArrayList<byte[]> bodyFragments = new ArrayList<byte[]>();
    509         StringBuilder sb = new StringBuilder();
    510         int count = 0;
    511         String mimeBody;
    512 
    513         mEncoding = "8BIT"; // The encoding used
    514 
    515         encodeHeaders(sb);
    516         if (mParts != null) {
    517             if (!getIncludeAttachments()) {
    518                 for (MimePart part : mParts) {
    519                     /* We call encode on all parts, to include a tag,
    520                      * where an attachment is missing. */
    521                     part.encodePlainText(sb);
    522                 }
    523             } else {
    524                 for (MimePart part : mParts) {
    525                     count++;
    526                     part.encode(sb, getBoundary(), (count == mParts.size()));
    527                 }
    528             }
    529         }
    530 
    531         mimeBody = sb.toString();
    532 
    533         if (mimeBody != null) {
    534             // Replace any occurrences of END:MSG with \END:MSG
    535             String tmpBody = mimeBody.replaceAll("END:MSG", "/END\\:MSG");
    536             bodyFragments.add(tmpBody.getBytes("UTF-8"));
    537         } else {
    538             bodyFragments.add(new byte[0]);
    539         }
    540 
    541         return encodeGeneric(bodyFragments);
    542     }
    543 
    544 
    545     /**
    546      * Try to parse the hdrPart string as e-mail headers.
    547      * @param hdrPart The string to parse.
    548      * @return Null if the entire string were e-mail headers. The part of the string in which
    549      * no headers were found.
    550      */
    551     private String parseMimeHeaders(String hdrPart) {
    552         String[] headers = hdrPart.split("\r\n");
    553         if (D) {
    554             Log.d(TAG, "Header count=" + headers.length);
    555         }
    556         String header;
    557         mHasHeaders = false;
    558 
    559         for (int i = 0, c = headers.length; i < c; i++) {
    560             header = headers[i];
    561             if (D) {
    562                 Log.d(TAG, "Header[" + i + "]: " + header);
    563             }
    564             /* We need to figure out if any headers are present, in cases where devices do
    565              * not follow the e-mail RFCs.
    566              * Skip empty lines, and then parse headers until a non-header line is found,
    567              * at which point we treat the remaining as plain text.
    568              */
    569             if (header.trim().isEmpty()) {
    570                 continue;
    571             }
    572             String[] headerParts = header.split(":", 2);
    573             if (headerParts.length != 2) {
    574                 // We treat the remaining content as plain text.
    575                 StringBuilder remaining = new StringBuilder();
    576                 for (; i < c; i++) {
    577                     remaining.append(headers[i]);
    578                 }
    579 
    580                 return remaining.toString();
    581             }
    582 
    583             String headerType = headerParts[0].toUpperCase();
    584             String headerValue = headerParts[1].trim();
    585 
    586             // Address headers
    587             /* If this is empty, the MSE needs to fill it in before sending the message.
    588              * This happens when sending the MMS.
    589              */
    590             if (headerType.contains("FROM")) {
    591                 headerValue = BluetoothMapUtils.stripEncoding(headerValue);
    592                 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(headerValue);
    593                 mFrom = new ArrayList<Rfc822Token>(Arrays.asList(tokens));
    594             } else if (headerType.contains("TO")) {
    595                 headerValue = BluetoothMapUtils.stripEncoding(headerValue);
    596                 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(headerValue);
    597                 mTo = new ArrayList<Rfc822Token>(Arrays.asList(tokens));
    598             } else if (headerType.contains("CC")) {
    599                 headerValue = BluetoothMapUtils.stripEncoding(headerValue);
    600                 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(headerValue);
    601                 mCc = new ArrayList<Rfc822Token>(Arrays.asList(tokens));
    602             } else if (headerType.contains("BCC")) {
    603                 headerValue = BluetoothMapUtils.stripEncoding(headerValue);
    604                 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(headerValue);
    605                 mBcc = new ArrayList<Rfc822Token>(Arrays.asList(tokens));
    606             } else if (headerType.contains("REPLY-TO")) {
    607                 headerValue = BluetoothMapUtils.stripEncoding(headerValue);
    608                 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(headerValue);
    609                 mReplyTo = new ArrayList<Rfc822Token>(Arrays.asList(tokens));
    610             } else if (headerType.contains("SUBJECT")) { // Other headers
    611                 mSubject = BluetoothMapUtils.stripEncoding(headerValue);
    612             } else if (headerType.contains("MESSAGE-ID")) {
    613                 mMessageId = headerValue;
    614             } else if (headerType.contains("DATE")) {
    615                 /* The date is not needed, as the time stamp will be set in the DB
    616                  * when the message is send. */
    617             } else if (headerType.contains("MIME-VERSION")) {
    618                 /* The mime version is not needed */
    619             } else if (headerType.contains("CONTENT-TYPE")) {
    620                 String[] contentTypeParts = headerValue.split(";");
    621                 mContentType = contentTypeParts[0];
    622                 // Extract the boundary if it exists
    623                 for (int j = 1, n = contentTypeParts.length; j < n; j++) {
    624                     if (contentTypeParts[j].contains("boundary")) {
    625                         mBoundary = contentTypeParts[j].split("boundary[\\s]*=", 2)[1].trim();
    626                         // removing quotes from boundary string
    627                         if ((mBoundary.charAt(0) == '\"') && (
    628                                 mBoundary.charAt(mBoundary.length() - 1) == '\"')) {
    629                             mBoundary = mBoundary.substring(1, mBoundary.length() - 1);
    630                         }
    631                         if (D) {
    632                             Log.d(TAG, "Boundary tag=" + mBoundary);
    633                         }
    634                     } else if (contentTypeParts[j].contains("charset")) {
    635                         mCharset = contentTypeParts[j].split("charset[\\s]*=", 2)[1].trim();
    636                     }
    637                 }
    638             } else if (headerType.contains("CONTENT-TRANSFER-ENCODING")) {
    639                 mMyEncoding = headerValue;
    640             } else {
    641                 if (D) {
    642                     Log.w(TAG, "Skipping unknown header: " + headerType + " (" + header + ")");
    643                 }
    644             }
    645         }
    646         return null;
    647     }
    648 
    649     private void parseMimePart(String partStr) {
    650         String[] parts = partStr.split("\r\n\r\n", 2); // Split the header from the body
    651         MimePart newPart = addMimePart();
    652         String partEncoding = mMyEncoding; /* Use the overall encoding as default */
    653         String body;
    654 
    655         String[] headers = parts[0].split("\r\n");
    656         if (D) {
    657             Log.d(TAG, "parseMimePart: headers count=" + headers.length);
    658         }
    659 
    660         if (parts.length != 2) {
    661             body = partStr;
    662         } else {
    663             for (String header : headers) {
    664                 // Skip empty lines(the \r\n after the boundary tag) and endBoundary tags
    665                 if ((header.length() == 0) || (header.trim().isEmpty()) || header.trim()
    666                         .equals("--")) {
    667                     continue;
    668                 }
    669 
    670                 String[] headerParts = header.split(":", 2);
    671                 if (headerParts.length != 2) {
    672                     if (D) {
    673                         Log.w(TAG, "part-Header not formatted correctly: ");
    674                     }
    675                     continue;
    676                 }
    677                 if (D) {
    678                     Log.d(TAG, "parseMimePart: header=" + header);
    679                 }
    680                 String headerType = headerParts[0].toUpperCase();
    681                 String headerValue = headerParts[1].trim();
    682                 if (headerType.contains("CONTENT-TYPE")) {
    683                     String[] contentTypeParts = headerValue.split(";");
    684                     newPart.mContentType = contentTypeParts[0];
    685                     // Extract the boundary if it exists
    686                     for (int j = 1, n = contentTypeParts.length; j < n; j++) {
    687                         String value = contentTypeParts[j].toLowerCase();
    688                         if (value.contains("charset")) {
    689                             newPart.mCharsetName = value.split("charset[\\s]*=", 2)[1].trim();
    690                         }
    691                     }
    692                 } else if (headerType.contains("CONTENT-LOCATION")) {
    693                     // This is used if the smil refers to a file name in its src
    694                     newPart.mContentLocation = headerValue;
    695                     newPart.mPartName = headerValue;
    696                 } else if (headerType.contains("CONTENT-TRANSFER-ENCODING")) {
    697                     partEncoding = headerValue;
    698                 } else if (headerType.contains("CONTENT-ID")) {
    699                     // This is used if the smil refers to a cid:<xxx> in it's src
    700                     newPart.mContentId = headerValue;
    701                 } else if (headerType.contains("CONTENT-DISPOSITION")) {
    702                     // This is used if the smil refers to a cid:<xxx> in it's src
    703                     newPart.mContentDisposition = headerValue;
    704                 } else {
    705                     if (D) {
    706                         Log.w(TAG, "Skipping unknown part-header: " + headerType + " (" + header
    707                                 + ")");
    708                     }
    709                 }
    710             }
    711             body = parts[1];
    712             if (body.length() > 2) {
    713                 if (body.charAt(body.length() - 2) == '\r'
    714                         && body.charAt(body.length() - 2) == '\n') {
    715                     body = body.substring(0, body.length() - 2);
    716                 }
    717             }
    718         }
    719         // Now for the body
    720         newPart.mData = decodeBody(body, partEncoding, newPart.mCharsetName);
    721     }
    722 
    723     private void parseMimeBody(String body) {
    724         MimePart newPart = addMimePart();
    725         newPart.mCharsetName = mCharset;
    726         newPart.mData = decodeBody(body, mMyEncoding, mCharset);
    727     }
    728 
    729     private byte[] decodeBody(String body, String encoding, String charset) {
    730         if (encoding != null && encoding.toUpperCase().contains("BASE64")) {
    731             return Base64.decode(body, Base64.DEFAULT);
    732         } else if (encoding != null && encoding.toUpperCase().contains("QUOTED-PRINTABLE")) {
    733             return BluetoothMapUtils.quotedPrintableToUtf8(body, charset);
    734         } else {
    735             // TODO: handle other encoding types? - here we simply store the string data as bytes
    736             try {
    737 
    738                 return body.getBytes("UTF-8");
    739             } catch (UnsupportedEncodingException e) {
    740                 // This will never happen, as UTF-8 is mandatory on Android platforms
    741             }
    742         }
    743         return null;
    744     }
    745 
    746     private void parseMime(String message) {
    747         /* Overall strategy for decoding:
    748          * 1) split on first empty line to extract the header
    749          * 2) unfold and parse headers
    750          * 3) split on boundary to split into parts (or use the remaining as a part,
    751          *    if part is not found)
    752          * 4) parse each part
    753          * */
    754         String[] messageParts;
    755         String[] mimeParts;
    756         String remaining = null;
    757         String messageBody = null;
    758         message = message.replaceAll("\\r\\n[ \\\t]+", ""); // Unfold
    759         messageParts = message.split("\r\n\r\n", 2); // Split the header from the body
    760         if (messageParts.length != 2) {
    761             // Handle entire message as plain text
    762             messageBody = message;
    763         } else {
    764             remaining = parseMimeHeaders(messageParts[0]);
    765             // If we have some text not being a header, add it to the message body.
    766             if (remaining != null) {
    767                 messageBody = remaining + messageParts[1];
    768                 if (D) {
    769                     Log.d(TAG, "parseMime remaining=" + remaining);
    770                 }
    771             } else {
    772                 messageBody = messageParts[1];
    773             }
    774         }
    775 
    776         if (mBoundary == null) {
    777             // If the boundary is not set, handle as non-multi-part
    778             parseMimeBody(messageBody);
    779             setTextOnly(true);
    780             if (mContentType == null) {
    781                 mContentType = "text/plain";
    782             }
    783             mParts.get(0).mContentType = mContentType;
    784         } else {
    785             mimeParts = messageBody.split("--" + mBoundary);
    786             if (D) {
    787                 Log.d(TAG, "mimePart count=" + mimeParts.length);
    788             }
    789             // Part 0 is the message to clients not capable of decoding MIME
    790             for (int i = 1; i < mimeParts.length - 1; i++) {
    791                 String part = mimeParts[i];
    792                 if (part != null && (part.length() > 0)) {
    793                     parseMimePart(part);
    794                 }
    795             }
    796         }
    797     }
    798 
    799     /* Notes on SMIL decoding (from http://tools.ietf.org/html/rfc2557):
    800      * src="filename.jpg" refers to a part with Content-Location: filename.jpg
    801      * src="cid:1234 (at) hest.net" refers to a part with Content-ID:<1234 (at) hest.net>*/
    802     @Override
    803     public void parseMsgPart(String msgPart) {
    804         parseMime(msgPart);
    805 
    806     }
    807 
    808     @Override
    809     public void parseMsgInit() {
    810         // Not used for e-mail
    811 
    812     }
    813 
    814     @Override
    815     public byte[] encode() throws UnsupportedEncodingException {
    816         return encodeMime();
    817     }
    818 
    819 }
    820