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