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