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