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