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