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