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