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