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.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