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