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