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