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