1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.voicemail.impl.mail.internet; 17 18 import android.support.annotation.Nullable; 19 import android.text.TextUtils; 20 import com.android.voicemail.impl.VvmLog; 21 import com.android.voicemail.impl.mail.Address; 22 import com.android.voicemail.impl.mail.Body; 23 import com.android.voicemail.impl.mail.BodyPart; 24 import com.android.voicemail.impl.mail.Message; 25 import com.android.voicemail.impl.mail.MessagingException; 26 import com.android.voicemail.impl.mail.Multipart; 27 import com.android.voicemail.impl.mail.Part; 28 import com.android.voicemail.impl.mail.utils.LogUtils; 29 import java.io.BufferedWriter; 30 import java.io.IOException; 31 import java.io.InputStream; 32 import java.io.OutputStream; 33 import java.io.OutputStreamWriter; 34 import java.text.SimpleDateFormat; 35 import java.util.Date; 36 import java.util.Locale; 37 import java.util.Stack; 38 import java.util.regex.Pattern; 39 import org.apache.james.mime4j.MimeException; 40 import org.apache.james.mime4j.dom.field.DateTimeField; 41 import org.apache.james.mime4j.field.DefaultFieldParser; 42 import org.apache.james.mime4j.io.EOLConvertingInputStream; 43 import org.apache.james.mime4j.parser.ContentHandler; 44 import org.apache.james.mime4j.parser.MimeStreamParser; 45 import org.apache.james.mime4j.stream.BodyDescriptor; 46 import org.apache.james.mime4j.stream.Field; 47 48 /** 49 * An implementation of Message that stores all of its metadata in RFC 822 and RFC 2045 style 50 * headers. 51 * 52 * <p>NOTE: Automatic generation of a local message-id is becoming unwieldy and should be removed. 53 * It would be better to simply do it explicitly on local creation of new outgoing messages. 54 */ 55 public class MimeMessage extends Message { 56 private MimeHeader header; 57 private MimeHeader extendedHeader; 58 59 // NOTE: The fields here are transcribed out of headers, and values stored here will supersede 60 // the values found in the headers. Use caution to prevent any out-of-phase errors. In 61 // particular, any adds/changes/deletes here must be echoed by changes in the parse() function. 62 private Address[] from; 63 private Address[] to; 64 private Address[] cc; 65 private Address[] bcc; 66 private Address[] replyTo; 67 private Date sentDate; 68 private Body body; 69 protected int size; 70 private boolean inhibitLocalMessageId = false; 71 72 // Shared random source for generating local message-id values 73 private static final java.util.Random random = new java.util.Random(); 74 75 // In MIME, en_US-like date format should be used. In other words "MMM" should be encoded to 76 // "Jan", not the other localized format like "Ene" (meaning January in locale es). 77 // This conversion is used when generating outgoing MIME messages. Incoming MIME date 78 // headers are parsed by org.apache.james.mime4j.field.DateTimeField which does not have any 79 // localization code. 80 private static final SimpleDateFormat DATE_FORMAT = 81 new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.US); 82 83 // regex that matches content id surrounded by "<>" optionally. 84 private static final Pattern REMOVE_OPTIONAL_BRACKETS = Pattern.compile("^<?([^>]+)>?$"); 85 // regex that matches end of line. 86 private static final Pattern END_OF_LINE = Pattern.compile("\r?\n"); 87 MimeMessage()88 public MimeMessage() { 89 header = null; 90 } 91 92 /** 93 * Generate a local message id. This is only used when none has been assigned, and is installed 94 * lazily. Any remote (typically server-assigned) message id takes precedence. 95 * 96 * @return a long, locally-generated message-ID value 97 */ generateMessageId()98 private static String generateMessageId() { 99 final StringBuilder sb = new StringBuilder(); 100 sb.append("<"); 101 for (int i = 0; i < 24; i++) { 102 // We'll use a 5-bit range (0..31) 103 final int value = random.nextInt() & 31; 104 final char c = "0123456789abcdefghijklmnopqrstuv".charAt(value); 105 sb.append(c); 106 } 107 sb.append("."); 108 sb.append(Long.toString(System.currentTimeMillis())); 109 sb.append("@email.android.com>"); 110 return sb.toString(); 111 } 112 113 /** 114 * Parse the given InputStream using Apache Mime4J to build a MimeMessage. 115 * 116 * @param in InputStream providing message content 117 * @throws IOException 118 * @throws MessagingException 119 */ MimeMessage(InputStream in)120 public MimeMessage(InputStream in) throws IOException, MessagingException, MimeException { 121 parse(in); 122 } 123 init()124 private MimeStreamParser init() { 125 // Before parsing the input stream, clear all local fields that may be superceded by 126 // the new incoming message. 127 getMimeHeaders().clear(); 128 inhibitLocalMessageId = true; 129 from = null; 130 to = null; 131 cc = null; 132 bcc = null; 133 replyTo = null; 134 sentDate = null; 135 body = null; 136 137 final MimeStreamParser parser = new MimeStreamParser(); 138 parser.setContentHandler(new MimeMessageBuilder()); 139 return parser; 140 } 141 parse(InputStream in)142 public void parse(InputStream in) throws IOException, MessagingException, MimeException { 143 final MimeStreamParser parser = init(); 144 parser.parse(new EOLConvertingInputStream(in)); 145 } 146 147 /** 148 * Return the internal mHeader value, with very lazy initialization. The goal is to save memory by 149 * not creating the headers until needed. 150 */ getMimeHeaders()151 private MimeHeader getMimeHeaders() { 152 if (header == null) { 153 header = new MimeHeader(); 154 } 155 return header; 156 } 157 158 @Override getReceivedDate()159 public Date getReceivedDate() throws MessagingException { 160 return null; 161 } 162 163 @Override getSentDate()164 public Date getSentDate() throws MessagingException { 165 if (sentDate == null) { 166 try { 167 DateTimeField field = 168 (DateTimeField) 169 DefaultFieldParser.parse( 170 "Date: " + MimeUtility.unfoldAndDecode(getFirstHeader("Date"))); 171 sentDate = field.getDate(); 172 // TODO: We should make it more clear what exceptions can be thrown here, 173 // and whether they reflect a normal or error condition. 174 } catch (Exception e) { 175 LogUtils.v(LogUtils.TAG, "Message missing Date header"); 176 } 177 } 178 if (sentDate == null) { 179 // If we still don't have a date, fall back to "Delivery-date" 180 try { 181 DateTimeField field = 182 (DateTimeField) 183 DefaultFieldParser.parse( 184 "Date: " + MimeUtility.unfoldAndDecode(getFirstHeader("Delivery-date"))); 185 sentDate = field.getDate(); 186 // TODO: We should make it more clear what exceptions can be thrown here, 187 // and whether they reflect a normal or error condition. 188 } catch (Exception e) { 189 LogUtils.v(LogUtils.TAG, "Message also missing Delivery-Date header"); 190 } 191 } 192 return sentDate; 193 } 194 195 @Override setSentDate(Date sentDate)196 public void setSentDate(Date sentDate) throws MessagingException { 197 setHeader("Date", DATE_FORMAT.format(sentDate)); 198 this.sentDate = sentDate; 199 } 200 201 @Override 202 @Nullable getDuration()203 public Long getDuration() { 204 String durationHeader = null; 205 try { 206 durationHeader = getFirstHeader(MimeHeader.HEADER_CONTENT_DURATION); 207 } catch (MessagingException e) { 208 VvmLog.e("MimeMessage.getDuration", "cannot retrieve header: ", e); 209 return null; 210 } 211 if (durationHeader == null) { 212 VvmLog.w("MimeMessage.getDuration", "message missing Content-Duration header"); 213 return null; 214 } 215 try { 216 return Long.valueOf(durationHeader); 217 } catch (NumberFormatException e) { 218 VvmLog.w("MimeMessage.getDuration", "cannot parse duration " + durationHeader); 219 return null; 220 } 221 } 222 223 @Override getContentType()224 public String getContentType() throws MessagingException { 225 final String contentType = getFirstHeader(MimeHeader.HEADER_CONTENT_TYPE); 226 if (contentType == null) { 227 return "text/plain"; 228 } else { 229 return contentType; 230 } 231 } 232 233 @Override getDisposition()234 public String getDisposition() throws MessagingException { 235 return getFirstHeader(MimeHeader.HEADER_CONTENT_DISPOSITION); 236 } 237 238 @Override getContentId()239 public String getContentId() throws MessagingException { 240 final String contentId = getFirstHeader(MimeHeader.HEADER_CONTENT_ID); 241 if (contentId == null) { 242 return null; 243 } else { 244 // remove optionally surrounding brackets. 245 return REMOVE_OPTIONAL_BRACKETS.matcher(contentId).replaceAll("$1"); 246 } 247 } 248 249 @Override getMimeType()250 public String getMimeType() throws MessagingException { 251 return MimeUtility.getHeaderParameter(getContentType(), null); 252 } 253 254 @Override getSize()255 public int getSize() throws MessagingException { 256 return size; 257 } 258 259 /** 260 * Returns a list of the given recipient type from this message. If no addresses are found the 261 * method returns an empty array. 262 */ 263 @Override getRecipients(String type)264 public Address[] getRecipients(String type) throws MessagingException { 265 if (type == RECIPIENT_TYPE_TO) { 266 if (to == null) { 267 to = Address.parse(MimeUtility.unfold(getFirstHeader("To"))); 268 } 269 return to; 270 } else if (type == RECIPIENT_TYPE_CC) { 271 if (cc == null) { 272 cc = Address.parse(MimeUtility.unfold(getFirstHeader("CC"))); 273 } 274 return cc; 275 } else if (type == RECIPIENT_TYPE_BCC) { 276 if (bcc == null) { 277 bcc = Address.parse(MimeUtility.unfold(getFirstHeader("BCC"))); 278 } 279 return bcc; 280 } else { 281 throw new MessagingException("Unrecognized recipient type."); 282 } 283 } 284 285 @Override setRecipients(String type, Address[] addresses)286 public void setRecipients(String type, Address[] addresses) throws MessagingException { 287 final int toLength = 4; // "To: " 288 final int ccLength = 4; // "Cc: " 289 final int bccLength = 5; // "Bcc: " 290 if (type == RECIPIENT_TYPE_TO) { 291 if (addresses == null || addresses.length == 0) { 292 removeHeader("To"); 293 this.to = null; 294 } else { 295 setHeader("To", MimeUtility.fold(Address.toHeader(addresses), toLength)); 296 this.to = addresses; 297 } 298 } else if (type == RECIPIENT_TYPE_CC) { 299 if (addresses == null || addresses.length == 0) { 300 removeHeader("CC"); 301 this.cc = null; 302 } else { 303 setHeader("CC", MimeUtility.fold(Address.toHeader(addresses), ccLength)); 304 this.cc = addresses; 305 } 306 } else if (type == RECIPIENT_TYPE_BCC) { 307 if (addresses == null || addresses.length == 0) { 308 removeHeader("BCC"); 309 this.bcc = null; 310 } else { 311 setHeader("BCC", MimeUtility.fold(Address.toHeader(addresses), bccLength)); 312 this.bcc = addresses; 313 } 314 } else { 315 throw new MessagingException("Unrecognized recipient type."); 316 } 317 } 318 319 /** Returns the unfolded, decoded value of the Subject header. */ 320 @Override getSubject()321 public String getSubject() throws MessagingException { 322 return MimeUtility.unfoldAndDecode(getFirstHeader("Subject")); 323 } 324 325 @Override setSubject(String subject)326 public void setSubject(String subject) throws MessagingException { 327 final int headerNameLength = 9; // "Subject: " 328 setHeader("Subject", MimeUtility.foldAndEncode2(subject, headerNameLength)); 329 } 330 331 @Override getFrom()332 public Address[] getFrom() throws MessagingException { 333 if (from == null) { 334 String list = MimeUtility.unfold(getFirstHeader("From")); 335 if (list == null || list.length() == 0) { 336 list = MimeUtility.unfold(getFirstHeader("Sender")); 337 } 338 from = Address.parse(list); 339 } 340 return from; 341 } 342 343 @Override setFrom(Address from)344 public void setFrom(Address from) throws MessagingException { 345 final int fromLength = 6; // "From: " 346 if (from != null) { 347 setHeader("From", MimeUtility.fold(from.toHeader(), fromLength)); 348 this.from = new Address[] {from}; 349 } else { 350 this.from = null; 351 } 352 } 353 354 @Override getReplyTo()355 public Address[] getReplyTo() throws MessagingException { 356 if (replyTo == null) { 357 replyTo = Address.parse(MimeUtility.unfold(getFirstHeader("Reply-to"))); 358 } 359 return replyTo; 360 } 361 362 @Override setReplyTo(Address[] replyTo)363 public void setReplyTo(Address[] replyTo) throws MessagingException { 364 final int replyToLength = 10; // "Reply-to: " 365 if (replyTo == null || replyTo.length == 0) { 366 removeHeader("Reply-to"); 367 this.replyTo = null; 368 } else { 369 setHeader("Reply-to", MimeUtility.fold(Address.toHeader(replyTo), replyToLength)); 370 this.replyTo = replyTo; 371 } 372 } 373 374 /** 375 * Set the mime "Message-ID" header 376 * 377 * @param messageId the new Message-ID value 378 * @throws MessagingException 379 */ 380 @Override setMessageId(String messageId)381 public void setMessageId(String messageId) throws MessagingException { 382 setHeader("Message-ID", messageId); 383 } 384 385 /** 386 * Get the mime "Message-ID" header. This value will be preloaded with a locally-generated random 387 * ID, if the value has not previously been set. Local generation can be inhibited/ overridden by 388 * explicitly clearing the headers, removing the message-id header, etc. 389 * 390 * @return the Message-ID header string, or null if explicitly has been set to null 391 */ 392 @Override getMessageId()393 public String getMessageId() throws MessagingException { 394 String messageId = getFirstHeader("Message-ID"); 395 if (messageId == null && !inhibitLocalMessageId) { 396 messageId = generateMessageId(); 397 setMessageId(messageId); 398 } 399 return messageId; 400 } 401 402 @Override saveChanges()403 public void saveChanges() throws MessagingException { 404 throw new MessagingException("saveChanges not yet implemented"); 405 } 406 407 @Override getBody()408 public Body getBody() throws MessagingException { 409 return body; 410 } 411 412 @Override setBody(Body body)413 public void setBody(Body body) throws MessagingException { 414 this.body = body; 415 if (body instanceof Multipart) { 416 final Multipart multipart = ((Multipart) body); 417 multipart.setParent(this); 418 setHeader(MimeHeader.HEADER_CONTENT_TYPE, multipart.getContentType()); 419 setHeader("MIME-Version", "1.0"); 420 } else if (body instanceof TextBody) { 421 setHeader( 422 MimeHeader.HEADER_CONTENT_TYPE, String.format("%s;\n charset=utf-8", getMimeType())); 423 setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64"); 424 } 425 } 426 getFirstHeader(String name)427 protected String getFirstHeader(String name) throws MessagingException { 428 return getMimeHeaders().getFirstHeader(name); 429 } 430 431 @Override addHeader(String name, String value)432 public void addHeader(String name, String value) throws MessagingException { 433 getMimeHeaders().addHeader(name, value); 434 } 435 436 @Override setHeader(String name, String value)437 public void setHeader(String name, String value) throws MessagingException { 438 getMimeHeaders().setHeader(name, value); 439 } 440 441 @Override getHeader(String name)442 public String[] getHeader(String name) throws MessagingException { 443 return getMimeHeaders().getHeader(name); 444 } 445 446 @Override removeHeader(String name)447 public void removeHeader(String name) throws MessagingException { 448 getMimeHeaders().removeHeader(name); 449 if ("Message-ID".equalsIgnoreCase(name)) { 450 inhibitLocalMessageId = true; 451 } 452 } 453 454 /** 455 * Set extended header 456 * 457 * @param name Extended header name 458 * @param value header value - flattened by removing CR-NL if any remove header if value is null 459 * @throws MessagingException 460 */ 461 @Override setExtendedHeader(String name, String value)462 public void setExtendedHeader(String name, String value) throws MessagingException { 463 if (value == null) { 464 if (extendedHeader != null) { 465 extendedHeader.removeHeader(name); 466 } 467 return; 468 } 469 if (extendedHeader == null) { 470 extendedHeader = new MimeHeader(); 471 } 472 extendedHeader.setHeader(name, END_OF_LINE.matcher(value).replaceAll("")); 473 } 474 475 /** 476 * Get extended header 477 * 478 * @param name Extended header name 479 * @return header value - null if header does not exist 480 * @throws MessagingException 481 */ 482 @Override getExtendedHeader(String name)483 public String getExtendedHeader(String name) throws MessagingException { 484 if (extendedHeader == null) { 485 return null; 486 } 487 return extendedHeader.getFirstHeader(name); 488 } 489 490 /** 491 * Set entire extended headers from String 492 * 493 * @param headers Extended header and its value - "CR-NL-separated pairs if null or empty, remove 494 * entire extended headers 495 * @throws MessagingException 496 */ setExtendedHeaders(String headers)497 public void setExtendedHeaders(String headers) throws MessagingException { 498 if (TextUtils.isEmpty(headers)) { 499 extendedHeader = null; 500 } else { 501 extendedHeader = new MimeHeader(); 502 for (final String header : END_OF_LINE.split(headers)) { 503 final String[] tokens = header.split(":", 2); 504 if (tokens.length != 2) { 505 throw new MessagingException("Illegal extended headers: " + headers); 506 } 507 extendedHeader.setHeader(tokens[0].trim(), tokens[1].trim()); 508 } 509 } 510 } 511 512 /** 513 * Get entire extended headers as String 514 * 515 * @return "CR-NL-separated extended headers - null if extended header does not exist 516 */ getExtendedHeaders()517 public String getExtendedHeaders() { 518 if (extendedHeader != null) { 519 return extendedHeader.writeToString(); 520 } 521 return null; 522 } 523 524 /** 525 * Write message header and body to output stream 526 * 527 * @param out Output steam to write message header and body. 528 */ 529 @Override writeTo(OutputStream out)530 public void writeTo(OutputStream out) throws IOException, MessagingException { 531 final BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024); 532 // Force creation of local message-id 533 getMessageId(); 534 getMimeHeaders().writeTo(out); 535 // mExtendedHeader will not be write out to external output stream, 536 // because it is intended to internal use. 537 writer.write("\r\n"); 538 writer.flush(); 539 if (body != null) { 540 body.writeTo(out); 541 } 542 } 543 544 @Override getInputStream()545 public InputStream getInputStream() throws MessagingException { 546 return null; 547 } 548 549 class MimeMessageBuilder implements ContentHandler { 550 private final Stack<Object> stack = new Stack<Object>(); 551 MimeMessageBuilder()552 public MimeMessageBuilder() {} 553 expect(Class<?> c)554 private void expect(Class<?> c) { 555 if (!c.isInstance(stack.peek())) { 556 throw new IllegalStateException( 557 "Internal stack error: " 558 + "Expected '" 559 + c.getName() 560 + "' found '" 561 + stack.peek().getClass().getName() 562 + "'"); 563 } 564 } 565 566 @Override startMessage()567 public void startMessage() { 568 if (stack.isEmpty()) { 569 stack.push(MimeMessage.this); 570 } else { 571 expect(Part.class); 572 try { 573 final MimeMessage m = new MimeMessage(); 574 ((Part) stack.peek()).setBody(m); 575 stack.push(m); 576 } catch (MessagingException me) { 577 throw new Error(me); 578 } 579 } 580 } 581 582 @Override endMessage()583 public void endMessage() { 584 expect(MimeMessage.class); 585 stack.pop(); 586 } 587 588 @Override startHeader()589 public void startHeader() { 590 expect(Part.class); 591 } 592 593 @Override field(Field rawField)594 public void field(Field rawField) { 595 expect(Part.class); 596 try { 597 final String[] tokens = rawField.getRaw().toString().split(":", 2); 598 ((Part) stack.peek()).addHeader(tokens[0], tokens[1].trim()); 599 } catch (MessagingException me) { 600 throw new Error(me); 601 } 602 } 603 604 @Override endHeader()605 public void endHeader() { 606 expect(Part.class); 607 } 608 609 @Override startMultipart(BodyDescriptor bd)610 public void startMultipart(BodyDescriptor bd) { 611 expect(Part.class); 612 613 final Part e = (Part) stack.peek(); 614 try { 615 final MimeMultipart multiPart = new MimeMultipart(e.getContentType()); 616 e.setBody(multiPart); 617 stack.push(multiPart); 618 } catch (MessagingException me) { 619 throw new Error(me); 620 } 621 } 622 623 @Override body(BodyDescriptor bd, InputStream in)624 public void body(BodyDescriptor bd, InputStream in) throws IOException { 625 expect(Part.class); 626 final Body body = MimeUtility.decodeBody(in, bd.getTransferEncoding()); 627 try { 628 ((Part) stack.peek()).setBody(body); 629 } catch (MessagingException me) { 630 throw new Error(me); 631 } 632 } 633 634 @Override endMultipart()635 public void endMultipart() { 636 stack.pop(); 637 } 638 639 @Override startBodyPart()640 public void startBodyPart() { 641 expect(MimeMultipart.class); 642 643 try { 644 final MimeBodyPart bodyPart = new MimeBodyPart(); 645 ((MimeMultipart) stack.peek()).addBodyPart(bodyPart); 646 stack.push(bodyPart); 647 } catch (MessagingException me) { 648 throw new Error(me); 649 } 650 } 651 652 @Override endBodyPart()653 public void endBodyPart() { 654 expect(BodyPart.class); 655 stack.pop(); 656 } 657 658 @Override epilogue(InputStream is)659 public void epilogue(InputStream is) throws IOException { 660 expect(MimeMultipart.class); 661 final StringBuilder sb = new StringBuilder(); 662 int b; 663 while ((b = is.read()) != -1) { 664 sb.append((char) b); 665 } 666 // TODO: why is this commented out? 667 // ((Multipart) stack.peek()).setEpilogue(sb.toString()); 668 } 669 670 @Override preamble(InputStream is)671 public void preamble(InputStream is) throws IOException { 672 expect(MimeMultipart.class); 673 final StringBuilder sb = new StringBuilder(); 674 int b; 675 while ((b = is.read()) != -1) { 676 sb.append((char) b); 677 } 678 try { 679 ((MimeMultipart) stack.peek()).setPreamble(sb.toString()); 680 } catch (MessagingException me) { 681 throw new Error(me); 682 } 683 } 684 685 @Override raw(InputStream is)686 public void raw(InputStream is) throws IOException { 687 throw new UnsupportedOperationException("Not supported"); 688 } 689 } 690 } 691