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