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 
     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 
     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      */
     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      */
    118     public MimeMessage(InputStream in) throws IOException, MessagingException {
    119         parse(in);
    120     }
    121 
    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      */
    144     private MimeHeader getMimeHeaders() {
    145         if (mHeader == null) {
    146             mHeader = new MimeHeader();
    147         }
    148         return mHeader;
    149     }
    150 
    151     @Override
    152     public Date getReceivedDate() throws MessagingException {
    153         return null;
    154     }
    155 
    156     @Override
    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
    171     public void setSentDate(Date sentDate) throws MessagingException {
    172         setHeader("Date", DATE_FORMAT.format(sentDate));
    173         this.mSentDate = sentDate;
    174     }
    175 
    176     @Override
    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 
    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 
    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 
    205     public String getMimeType() throws MessagingException {
    206         return MimeUtility.getHeaderParameter(getContentType(), null);
    207     }
    208 
    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
    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
    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
    277     public String getSubject() throws MessagingException {
    278         return MimeUtility.unfoldAndDecode(getFirstHeader("Subject"));
    279     }
    280 
    281     @Override
    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
    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
    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
    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
    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
    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
    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
    359     public void saveChanges() throws MessagingException {
    360         throw new MessagingException("saveChanges not yet implemented");
    361     }
    362 
    363     @Override
    364     public Body getBody() throws MessagingException {
    365         return mBody;
    366     }
    367 
    368     @Override
    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 
    384     protected String getFirstHeader(String name) throws MessagingException {
    385         return getMimeHeaders().getFirstHeader(name);
    386     }
    387 
    388     @Override
    389     public void addHeader(String name, String value) throws MessagingException {
    390         getMimeHeaders().addHeader(name, value);
    391     }
    392 
    393     @Override
    394     public void setHeader(String name, String value) throws MessagingException {
    395         getMimeHeaders().setHeader(name, value);
    396     }
    397 
    398     @Override
    399     public String[] getHeader(String name) throws MessagingException {
    400         return getMimeHeaders().getHeader(name);
    401     }
    402 
    403     @Override
    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      */
    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      */
    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      */
    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      */
    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      */
    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 
    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 
    506         public MimeMessageBuilder() {
    507         }
    508 
    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 
    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 
    531         public void endMessage() {
    532             expect(MimeMessage.class);
    533             stack.pop();
    534         }
    535 
    536         public void startHeader() {
    537             expect(Part.class);
    538         }
    539 
    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 
    550         public void endHeader() {
    551             expect(Part.class);
    552         }
    553 
    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 
    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 
    577         public void endMultipart() {
    578             stack.pop();
    579         }
    580 
    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 
    593         public void endBodyPart() {
    594             expect(BodyPart.class);
    595             stack.pop();
    596         }
    597 
    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 
    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 
    622         public void raw(InputStream is) throws IOException {
    623             throw new UnsupportedOperationException("Not supported");
    624         }
    625     }
    626 }
    627