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