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