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 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 */ 98 private static String generateMessageId() { 99 StringBuffer sb = new StringBuffer(); 100 sb.append("<"); 101 for (int i = 0; i < 24; i++) { 102 // We'll use a 5-bit range (0..31) 103 int value = sRandom.nextInt() & 31; 104 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 117 * @throws IOException 118 * @throws MessagingException 119 */ 120 public MimeMessage(InputStream in) throws IOException, MessagingException { 121 parse(in); 122 } 123 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 MimeStreamParser parser = new MimeStreamParser(); 138 parser.setContentHandler(new MimeMessageBuilder()); 139 return parser; 140 } 141 142 protected void parse(InputStream in) throws IOException, MessagingException { 143 MimeStreamParser parser = init(); 144 parser.parse(new EOLConvertingInputStream(in)); 145 mComplete = !parser.getPrematureEof(); 146 } 147 148 public void parse(InputStream in, EOLConvertingInputStream.Callback callback) 149 throws IOException, MessagingException { 150 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 */ 159 private MimeHeader getMimeHeaders() { 160 if (mHeader == null) { 161 mHeader = new MimeHeader(); 162 } 163 return mHeader; 164 } 165 166 @Override 167 public Date getReceivedDate() throws MessagingException { 168 return null; 169 } 170 171 @Override 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 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 194 } 195 } 196 return mSentDate; 197 } 198 199 @Override 200 public void setSentDate(Date sentDate) throws MessagingException { 201 setHeader("Date", DATE_FORMAT.format(sentDate)); 202 this.mSentDate = sentDate; 203 } 204 205 @Override 206 public String getContentType() throws MessagingException { 207 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 216 public String getDisposition() throws MessagingException { 217 String contentDisposition = getFirstHeader(MimeHeader.HEADER_CONTENT_DISPOSITION); 218 if (contentDisposition == null) { 219 return null; 220 } else { 221 return contentDisposition; 222 } 223 } 224 225 @Override 226 public String getContentId() throws MessagingException { 227 String contentId = getFirstHeader(MimeHeader.HEADER_CONTENT_ID); 228 if (contentId == null) { 229 return null; 230 } else { 231 // remove optionally surrounding brackets. 232 return REMOVE_OPTIONAL_BRACKETS.matcher(contentId).replaceAll("$1"); 233 } 234 } 235 236 public boolean isComplete() { 237 return mComplete; 238 } 239 240 @Override 241 public String getMimeType() throws MessagingException { 242 return MimeUtility.getHeaderParameter(getContentType(), null); 243 } 244 245 @Override 246 public int getSize() throws MessagingException { 247 return mSize; 248 } 249 250 /** 251 * Returns a list of the given recipient type from this message. If no addresses are 252 * found the method returns an empty array. 253 */ 254 @Override 255 public Address[] getRecipients(RecipientType type) throws MessagingException { 256 if (type == RecipientType.TO) { 257 if (mTo == null) { 258 mTo = Address.parse(MimeUtility.unfold(getFirstHeader("To"))); 259 } 260 return mTo; 261 } else if (type == RecipientType.CC) { 262 if (mCc == null) { 263 mCc = Address.parse(MimeUtility.unfold(getFirstHeader("CC"))); 264 } 265 return mCc; 266 } else if (type == RecipientType.BCC) { 267 if (mBcc == null) { 268 mBcc = Address.parse(MimeUtility.unfold(getFirstHeader("BCC"))); 269 } 270 return mBcc; 271 } else { 272 throw new MessagingException("Unrecognized recipient type."); 273 } 274 } 275 276 @Override 277 public void setRecipients(RecipientType type, Address[] addresses) throws MessagingException { 278 final int TO_LENGTH = 4; // "To: " 279 final int CC_LENGTH = 4; // "Cc: " 280 final int BCC_LENGTH = 5; // "Bcc: " 281 if (type == RecipientType.TO) { 282 if (addresses == null || addresses.length == 0) { 283 removeHeader("To"); 284 this.mTo = null; 285 } else { 286 setHeader("To", MimeUtility.fold(Address.toHeader(addresses), TO_LENGTH)); 287 this.mTo = addresses; 288 } 289 } else if (type == RecipientType.CC) { 290 if (addresses == null || addresses.length == 0) { 291 removeHeader("CC"); 292 this.mCc = null; 293 } else { 294 setHeader("CC", MimeUtility.fold(Address.toHeader(addresses), CC_LENGTH)); 295 this.mCc = addresses; 296 } 297 } else if (type == RecipientType.BCC) { 298 if (addresses == null || addresses.length == 0) { 299 removeHeader("BCC"); 300 this.mBcc = null; 301 } else { 302 setHeader("BCC", MimeUtility.fold(Address.toHeader(addresses), BCC_LENGTH)); 303 this.mBcc = addresses; 304 } 305 } else { 306 throw new MessagingException("Unrecognized recipient type."); 307 } 308 } 309 310 /** 311 * Returns the unfolded, decoded value of the Subject header. 312 */ 313 @Override 314 public String getSubject() throws MessagingException { 315 return MimeUtility.unfoldAndDecode(getFirstHeader("Subject")); 316 } 317 318 @Override 319 public void setSubject(String subject) throws MessagingException { 320 final int HEADER_NAME_LENGTH = 9; // "Subject: " 321 setHeader("Subject", MimeUtility.foldAndEncode2(subject, HEADER_NAME_LENGTH)); 322 } 323 324 @Override 325 public Address[] getFrom() throws MessagingException { 326 if (mFrom == null) { 327 String list = MimeUtility.unfold(getFirstHeader("From")); 328 if (list == null || list.length() == 0) { 329 list = MimeUtility.unfold(getFirstHeader("Sender")); 330 } 331 mFrom = Address.parse(list); 332 } 333 return mFrom; 334 } 335 336 @Override 337 public void setFrom(Address from) throws MessagingException { 338 final int FROM_LENGTH = 6; // "From: " 339 if (from != null) { 340 setHeader("From", MimeUtility.fold(from.toHeader(), FROM_LENGTH)); 341 this.mFrom = new Address[] { 342 from 343 }; 344 } else { 345 this.mFrom = null; 346 } 347 } 348 349 @Override 350 public Address[] getReplyTo() throws MessagingException { 351 if (mReplyTo == null) { 352 mReplyTo = Address.parse(MimeUtility.unfold(getFirstHeader("Reply-to"))); 353 } 354 return mReplyTo; 355 } 356 357 @Override 358 public void setReplyTo(Address[] replyTo) throws MessagingException { 359 final int REPLY_TO_LENGTH = 10; // "Reply-to: " 360 if (replyTo == null || replyTo.length == 0) { 361 removeHeader("Reply-to"); 362 mReplyTo = null; 363 } else { 364 setHeader("Reply-to", MimeUtility.fold(Address.toHeader(replyTo), REPLY_TO_LENGTH)); 365 mReplyTo = replyTo; 366 } 367 } 368 369 /** 370 * Set the mime "Message-ID" header 371 * @param messageId the new Message-ID value 372 * @throws MessagingException 373 */ 374 @Override 375 public void setMessageId(String messageId) throws MessagingException { 376 setHeader("Message-ID", messageId); 377 } 378 379 /** 380 * Get the mime "Message-ID" header. This value will be preloaded with a locally-generated 381 * random ID, if the value has not previously been set. Local generation can be inhibited/ 382 * overridden by explicitly clearing the headers, removing the message-id header, etc. 383 * @return the Message-ID header string, or null if explicitly has been set to null 384 */ 385 @Override 386 public String getMessageId() throws MessagingException { 387 String messageId = getFirstHeader("Message-ID"); 388 if (messageId == null && !mInhibitLocalMessageId) { 389 messageId = generateMessageId(); 390 setMessageId(messageId); 391 } 392 return messageId; 393 } 394 395 @Override 396 public void saveChanges() throws MessagingException { 397 throw new MessagingException("saveChanges not yet implemented"); 398 } 399 400 @Override 401 public Body getBody() throws MessagingException { 402 return mBody; 403 } 404 405 @Override 406 public void setBody(Body body) throws MessagingException { 407 this.mBody = body; 408 if (body instanceof Multipart) { 409 Multipart multipart = ((Multipart)body); 410 multipart.setParent(this); 411 setHeader(MimeHeader.HEADER_CONTENT_TYPE, multipart.getContentType()); 412 setHeader("MIME-Version", "1.0"); 413 } 414 else if (body instanceof TextBody) { 415 setHeader(MimeHeader.HEADER_CONTENT_TYPE, String.format("%s;\n charset=utf-8", 416 getMimeType())); 417 setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64"); 418 } 419 } 420 421 protected String getFirstHeader(String name) throws MessagingException { 422 return getMimeHeaders().getFirstHeader(name); 423 } 424 425 @Override 426 public void addHeader(String name, String value) throws MessagingException { 427 getMimeHeaders().addHeader(name, value); 428 } 429 430 @Override 431 public void setHeader(String name, String value) throws MessagingException { 432 getMimeHeaders().setHeader(name, value); 433 } 434 435 @Override 436 public String[] getHeader(String name) throws MessagingException { 437 return getMimeHeaders().getHeader(name); 438 } 439 440 @Override 441 public void removeHeader(String name) throws MessagingException { 442 getMimeHeaders().removeHeader(name); 443 if ("Message-ID".equalsIgnoreCase(name)) { 444 mInhibitLocalMessageId = true; 445 } 446 } 447 448 /** 449 * Set extended header 450 * 451 * @param name Extended header name 452 * @param value header value - flattened by removing CR-NL if any 453 * remove header if value is null 454 * @throws MessagingException 455 */ 456 @Override 457 public void setExtendedHeader(String name, String value) throws MessagingException { 458 if (value == null) { 459 if (mExtendedHeader != null) { 460 mExtendedHeader.removeHeader(name); 461 } 462 return; 463 } 464 if (mExtendedHeader == null) { 465 mExtendedHeader = new MimeHeader(); 466 } 467 mExtendedHeader.setHeader(name, END_OF_LINE.matcher(value).replaceAll("")); 468 } 469 470 /** 471 * Get extended header 472 * 473 * @param name Extended header name 474 * @return header value - null if header does not exist 475 * @throws MessagingException 476 */ 477 @Override 478 public String getExtendedHeader(String name) throws MessagingException { 479 if (mExtendedHeader == null) { 480 return null; 481 } 482 return mExtendedHeader.getFirstHeader(name); 483 } 484 485 /** 486 * Set entire extended headers from String 487 * 488 * @param headers Extended header and its value - "CR-NL-separated pairs 489 * if null or empty, remove entire extended headers 490 * @throws MessagingException 491 */ 492 public void setExtendedHeaders(String headers) throws MessagingException { 493 if (TextUtils.isEmpty(headers)) { 494 mExtendedHeader = null; 495 } else { 496 mExtendedHeader = new MimeHeader(); 497 for (String header : END_OF_LINE.split(headers)) { 498 String[] tokens = header.split(":", 2); 499 if (tokens.length != 2) { 500 throw new MessagingException("Illegal extended headers: " + headers); 501 } 502 mExtendedHeader.setHeader(tokens[0].trim(), tokens[1].trim()); 503 } 504 } 505 } 506 507 /** 508 * Get entire extended headers as String 509 * 510 * @return "CR-NL-separated extended headers - null if extended header does not exist 511 */ 512 public String getExtendedHeaders() { 513 if (mExtendedHeader != null) { 514 return mExtendedHeader.writeToString(); 515 } 516 return null; 517 } 518 519 /** 520 * Write message header and body to output stream 521 * 522 * @param out Output steam to write message header and body. 523 */ 524 @Override 525 public void writeTo(OutputStream out) throws IOException, MessagingException { 526 BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024); 527 // Force creation of local message-id 528 getMessageId(); 529 getMimeHeaders().writeTo(out); 530 // mExtendedHeader will not be write out to external output stream, 531 // because it is intended to internal use. 532 writer.write("\r\n"); 533 writer.flush(); 534 if (mBody != null) { 535 mBody.writeTo(out); 536 } 537 } 538 539 @Override 540 public InputStream getInputStream() throws MessagingException { 541 return null; 542 } 543 544 class MimeMessageBuilder implements ContentHandler { 545 private final Stack<Object> stack = new Stack<Object>(); 546 547 public MimeMessageBuilder() { 548 } 549 550 private void expect(Class<?> c) { 551 if (!c.isInstance(stack.peek())) { 552 throw new IllegalStateException("Internal stack error: " + "Expected '" 553 + c.getName() + "' found '" + stack.peek().getClass().getName() + "'"); 554 } 555 } 556 557 @Override 558 public void startMessage() { 559 if (stack.isEmpty()) { 560 stack.push(MimeMessage.this); 561 } else { 562 expect(Part.class); 563 try { 564 MimeMessage m = new MimeMessage(); 565 ((Part)stack.peek()).setBody(m); 566 stack.push(m); 567 } catch (MessagingException me) { 568 throw new Error(me); 569 } 570 } 571 } 572 573 @Override 574 public void endMessage() { 575 expect(MimeMessage.class); 576 stack.pop(); 577 } 578 579 @Override 580 public void startHeader() { 581 expect(Part.class); 582 } 583 584 @Override 585 public void field(String fieldData) { 586 expect(Part.class); 587 try { 588 String[] tokens = fieldData.split(":", 2); 589 ((Part)stack.peek()).addHeader(tokens[0], tokens[1].trim()); 590 } catch (MessagingException me) { 591 throw new Error(me); 592 } 593 } 594 595 @Override 596 public void endHeader() { 597 expect(Part.class); 598 } 599 600 @Override 601 public void startMultipart(BodyDescriptor bd) { 602 expect(Part.class); 603 604 Part e = (Part)stack.peek(); 605 try { 606 MimeMultipart multiPart = new MimeMultipart(e.getContentType()); 607 e.setBody(multiPart); 608 stack.push(multiPart); 609 } catch (MessagingException me) { 610 throw new Error(me); 611 } 612 } 613 614 @Override 615 public void body(BodyDescriptor bd, InputStream in) throws IOException { 616 expect(Part.class); 617 Body body = MimeUtility.decodeBody(in, bd.getTransferEncoding()); 618 try { 619 ((Part)stack.peek()).setBody(body); 620 } catch (MessagingException me) { 621 throw new Error(me); 622 } 623 } 624 625 @Override 626 public void endMultipart() { 627 stack.pop(); 628 } 629 630 @Override 631 public void startBodyPart() { 632 expect(MimeMultipart.class); 633 634 try { 635 MimeBodyPart bodyPart = new MimeBodyPart(); 636 ((MimeMultipart)stack.peek()).addBodyPart(bodyPart); 637 stack.push(bodyPart); 638 } catch (MessagingException me) { 639 throw new Error(me); 640 } 641 } 642 643 @Override 644 public void endBodyPart() { 645 expect(BodyPart.class); 646 stack.pop(); 647 } 648 649 @Override 650 public void epilogue(InputStream is) throws IOException { 651 expect(MimeMultipart.class); 652 StringBuffer sb = new StringBuffer(); 653 int b; 654 while ((b = is.read()) != -1) { 655 sb.append((char)b); 656 } 657 // ((Multipart) stack.peek()).setEpilogue(sb.toString()); 658 } 659 660 @Override 661 public void preamble(InputStream is) throws IOException { 662 expect(MimeMultipart.class); 663 StringBuffer sb = new StringBuffer(); 664 int b; 665 while ((b = is.read()) != -1) { 666 sb.append((char)b); 667 } 668 try { 669 ((MimeMultipart)stack.peek()).setPreamble(sb.toString()); 670 } catch (MessagingException me) { 671 throw new Error(me); 672 } 673 } 674 675 @Override 676 public void raw(InputStream is) throws IOException { 677 throw new UnsupportedOperationException("Not supported"); 678 } 679 } 680 } 681