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 
getDataAsString()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 
encode(StringBuilder sb, String boundaryTag, boolean last)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 
encodePlainText(StringBuilder sb)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 
getBoundary()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      */
getMimeParts()175     public ArrayList<MimePart> getMimeParts() {
176         return mParts;
177     }
178 
getMessageAsText()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 
addMimePart()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 
getDateString()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 
getDate()209     public long getDate() {
210         return mDate;
211     }
212 
setDate(long date)213     public void setDate(long date) {
214         this.mDate = date;
215     }
216 
getSubject()217     public String getSubject() {
218         return mSubject;
219     }
220 
setSubject(String subject)221     public void setSubject(String subject) {
222         this.mSubject = subject;
223     }
224 
getFrom()225     public ArrayList<Rfc822Token> getFrom() {
226         return mFrom;
227     }
228 
setFrom(ArrayList<Rfc822Token> from)229     public void setFrom(ArrayList<Rfc822Token> from) {
230         this.mFrom = from;
231     }
232 
addFrom(String name, String address)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 
getSender()240     public ArrayList<Rfc822Token> getSender() {
241         return mSender;
242     }
243 
setSender(ArrayList<Rfc822Token> sender)244     public void setSender(ArrayList<Rfc822Token> sender) {
245         this.mSender = sender;
246     }
247 
addSender(String name, String address)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 
getTo()255     public ArrayList<Rfc822Token> getTo() {
256         return mTo;
257     }
258 
setTo(ArrayList<Rfc822Token> to)259     public void setTo(ArrayList<Rfc822Token> to) {
260         this.mTo = to;
261     }
262 
addTo(String name, String address)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 
getCc()270     public ArrayList<Rfc822Token> getCc() {
271         return mCc;
272     }
273 
setCc(ArrayList<Rfc822Token> cc)274     public void setCc(ArrayList<Rfc822Token> cc) {
275         this.mCc = cc;
276     }
277 
addCc(String name, String address)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 
getBcc()285     public ArrayList<Rfc822Token> getBcc() {
286         return mBcc;
287     }
288 
setBcc(ArrayList<Rfc822Token> bcc)289     public void setBcc(ArrayList<Rfc822Token> bcc) {
290         this.mBcc = bcc;
291     }
292 
addBcc(String name, String address)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 
getReplyTo()300     public ArrayList<Rfc822Token> getReplyTo() {
301         return mReplyTo;
302     }
303 
setReplyTo(ArrayList<Rfc822Token> replyTo)304     public void setReplyTo(ArrayList<Rfc822Token> replyTo) {
305         this.mReplyTo = replyTo;
306     }
307 
addReplyTo(String name, String address)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 
setMessageId(String messageId)315     public void setMessageId(String messageId) {
316         this.mMessageId = messageId;
317     }
318 
getMessageId()319     public String getMessageId() {
320         return mMessageId;
321     }
322 
setContentType(String contentType)323     public void setContentType(String contentType) {
324         this.mContentType = contentType;
325     }
326 
getContentType()327     public String getContentType() {
328         return mContentType;
329     }
330 
setTextOnly(boolean textOnly)331     public void setTextOnly(boolean textOnly) {
332         this.mTextonly = textOnly;
333     }
334 
getTextOnly()335     public boolean getTextOnly() {
336         return mTextonly;
337     }
338 
setIncludeAttachments(boolean includeAttachments)339     public void setIncludeAttachments(boolean includeAttachments) {
340         this.mIncludeAttachments = includeAttachments;
341     }
342 
getIncludeAttachments()343     public boolean getIncludeAttachments() {
344         return mIncludeAttachments;
345     }
346 
updateCharset()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 
getSize()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      */
encodeHeaderAddresses(StringBuilder sb, String headerName, ArrayList<Rfc822Token> addresses)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 
encodeHeaders(StringBuilder sb)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      */
encodeMime()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      */
parseMimeHeaders(String hdrPart)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 
parseMimePart(String partStr)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 
parseMimeBody(String body)723     private void parseMimeBody(String body) {
724         MimePart newPart = addMimePart();
725         newPart.mCharsetName = mCharset;
726         newPart.mData = decodeBody(body, mMyEncoding, mCharset);
727     }
728 
decodeBody(String body, String encoding, String charset)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 
parseMime(String message)746     private void parseMime(String message) {
747         // Check for null String, otherwise NPE will cause BT to crash
748         if (message == null) {
749             Log.e(TAG, "parseMime called with a NULL message, terminating early");
750             return;
751         }
752 
753         /* Overall strategy for decoding:
754          * 1) split on first empty line to extract the header
755          * 2) unfold and parse headers
756          * 3) split on boundary to split into parts (or use the remaining as a part,
757          *    if part is not found)
758          * 4) parse each part
759          * */
760         String[] messageParts;
761         String[] mimeParts;
762         String remaining = null;
763         String messageBody = null;
764 
765         message = message.replaceAll("\\r\\n[ \\\t]+", ""); // Unfold
766         messageParts = message.split("\r\n\r\n", 2); // Split the header from the body
767         if (messageParts.length != 2) {
768             // Handle entire message as plain text
769             messageBody = message;
770         } else {
771             remaining = parseMimeHeaders(messageParts[0]);
772             // If we have some text not being a header, add it to the message body.
773             if (remaining != null) {
774                 messageBody = remaining + messageParts[1];
775                 if (D) {
776                     Log.d(TAG, "parseMime remaining=" + remaining);
777                 }
778             } else {
779                 messageBody = messageParts[1];
780             }
781         }
782 
783         if (mBoundary == null) {
784             // If the boundary is not set, handle as non-multi-part
785             parseMimeBody(messageBody);
786             setTextOnly(true);
787             if (mContentType == null) {
788                 mContentType = "text/plain";
789             }
790             mParts.get(0).mContentType = mContentType;
791         } else {
792             mimeParts = messageBody.split("--" + mBoundary);
793             if (D) {
794                 Log.d(TAG, "mimePart count=" + mimeParts.length);
795             }
796             // Part 0 is the message to clients not capable of decoding MIME
797             for (int i = 1; i < mimeParts.length - 1; i++) {
798                 String part = mimeParts[i];
799                 if (part != null && (part.length() > 0)) {
800                     parseMimePart(part);
801                 }
802             }
803         }
804     }
805 
806     /* Notes on SMIL decoding (from http://tools.ietf.org/html/rfc2557):
807      * src="filename.jpg" refers to a part with Content-Location: filename.jpg
808      * src="cid:1234@hest.net" refers to a part with Content-ID:<1234@hest.net>*/
809     @Override
parseMsgPart(String msgPart)810     public void parseMsgPart(String msgPart) {
811         parseMime(msgPart);
812 
813     }
814 
815     @Override
parseMsgInit()816     public void parseMsgInit() {
817         // Not used for e-mail
818 
819     }
820 
821     @Override
encode()822     public byte[] encode() throws UnsupportedEncodingException {
823         return encodeMime();
824     }
825 
826 }
827