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