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