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