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