Home | History | Annotate | Download | only in internet
      1 /*
      2  * Copyright (C) 2015 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 package com.android.phone.common.mail.internet;
     17 
     18 import com.android.phone.common.mail.Address;
     19 import com.android.phone.common.mail.Body;
     20 import com.android.phone.common.mail.BodyPart;
     21 import com.android.phone.common.mail.Message;
     22 import com.android.phone.common.mail.MessagingException;
     23 import com.android.phone.common.mail.Multipart;
     24 import com.android.phone.common.mail.Part;
     25 import com.android.phone.common.mail.utils.LogUtils;
     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 supersede
     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     private boolean mComplete = true;
     71 
     72     // Shared random source for generating local message-id values
     73     private static final java.util.Random sRandom = new java.util.Random();
     74 
     75     // In MIME, en_US-like date format should be used. In other words "MMM" should be encoded to
     76     // "Jan", not the other localized format like "Ene" (meaning January in locale es).
     77     // This conversion is used when generating outgoing MIME messages. Incoming MIME date
     78     // headers are parsed by org.apache.james.mime4j.field.DateTimeField which does not have any
     79     // localization code.
     80     private static final SimpleDateFormat DATE_FORMAT =
     81         new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.US);
     82 
     83     // regex that matches content id surrounded by "<>" optionally.
     84     private static final Pattern REMOVE_OPTIONAL_BRACKETS = Pattern.compile("^<?([^>]+)>?$");
     85     // regex that matches end of line.
     86     private static final Pattern END_OF_LINE = Pattern.compile("\r?\n");
     87 
     88     public MimeMessage() {
     89         mHeader = null;
     90     }
     91 
     92     /**
     93      * Generate a local message id.  This is only used when none has been assigned, and is
     94      * installed lazily.  Any remote (typically server-assigned) message id takes precedence.
     95      * @return a long, locally-generated message-ID value
     96      */
     97     private static String generateMessageId() {
     98         final StringBuilder sb = new StringBuilder();
     99         sb.append("<");
    100         for (int i = 0; i < 24; i++) {
    101             // We'll use a 5-bit range (0..31)
    102             final int value = sRandom.nextInt() & 31;
    103             final char c = "0123456789abcdefghijklmnopqrstuv".charAt(value);
    104             sb.append(c);
    105         }
    106         sb.append(".");
    107         sb.append(Long.toString(System.currentTimeMillis()));
    108         sb.append("@email.android.com>");
    109         return sb.toString();
    110     }
    111 
    112     /**
    113      * Parse the given InputStream using Apache Mime4J to build a MimeMessage.
    114      *
    115      * @param in InputStream providing message content
    116      * @throws IOException
    117      * @throws MessagingException
    118      */
    119     public MimeMessage(InputStream in) throws IOException, MessagingException {
    120         parse(in);
    121     }
    122 
    123     private MimeStreamParser init() {
    124         // Before parsing the input stream, clear all local fields that may be superceded by
    125         // the new incoming message.
    126         getMimeHeaders().clear();
    127         mInhibitLocalMessageId = true;
    128         mFrom = null;
    129         mTo = null;
    130         mCc = null;
    131         mBcc = null;
    132         mReplyTo = null;
    133         mSentDate = null;
    134         mBody = null;
    135 
    136         final MimeStreamParser parser = new MimeStreamParser();
    137         parser.setContentHandler(new MimeMessageBuilder());
    138         return parser;
    139     }
    140 
    141     protected void parse(InputStream in) throws IOException, MessagingException {
    142         final MimeStreamParser parser = init();
    143         parser.parse(new EOLConvertingInputStream(in));
    144         mComplete = !parser.getPrematureEof();
    145     }
    146 
    147     public void parse(InputStream in, EOLConvertingInputStream.Callback callback)
    148             throws IOException, MessagingException {
    149         final MimeStreamParser parser = init();
    150         parser.parse(new EOLConvertingInputStream(in, getSize(), callback));
    151         mComplete = !parser.getPrematureEof();
    152     }
    153 
    154     /**
    155      * Return the internal mHeader value, with very lazy initialization.
    156      * The goal is to save memory by not creating the headers until needed.
    157      */
    158     private MimeHeader getMimeHeaders() {
    159         if (mHeader == null) {
    160             mHeader = new MimeHeader();
    161         }
    162         return mHeader;
    163     }
    164 
    165     @Override
    166     public Date getReceivedDate() throws MessagingException {
    167         return null;
    168     }
    169 
    170     @Override
    171     public Date getSentDate() throws MessagingException {
    172         if (mSentDate == null) {
    173             try {
    174                 DateTimeField field = (DateTimeField)Field.parse("Date: "
    175                         + MimeUtility.unfoldAndDecode(getFirstHeader("Date")));
    176                 mSentDate = field.getDate();
    177                 // TODO: We should make it more clear what exceptions can be thrown here,
    178                 // and whether they reflect a normal or error condition.
    179             } catch (Exception e) {
    180                 LogUtils.v(LogUtils.TAG, "Message missing Date header");
    181             }
    182         }
    183         if (mSentDate == null) {
    184             // If we still don't have a date, fall back to "Delivery-date"
    185             try {
    186                 DateTimeField field = (DateTimeField)Field.parse("Date: "
    187                         + MimeUtility.unfoldAndDecode(getFirstHeader("Delivery-date")));
    188                 mSentDate = field.getDate();
    189                 // TODO: We should make it more clear what exceptions can be thrown here,
    190                 // and whether they reflect a normal or error condition.
    191             } catch (Exception e) {
    192                 LogUtils.v(LogUtils.TAG, "Message also missing Delivery-Date header");
    193             }
    194         }
    195         return mSentDate;
    196     }
    197 
    198     @Override
    199     public void setSentDate(Date sentDate) throws MessagingException {
    200         setHeader("Date", DATE_FORMAT.format(sentDate));
    201         this.mSentDate = sentDate;
    202     }
    203 
    204     @Override
    205     public String getContentType() throws MessagingException {
    206         final String contentType = getFirstHeader(MimeHeader.HEADER_CONTENT_TYPE);
    207         if (contentType == null) {
    208             return "text/plain";
    209         } else {
    210             return contentType;
    211         }
    212     }
    213 
    214     @Override
    215     public String getDisposition() throws MessagingException {
    216         return getFirstHeader(MimeHeader.HEADER_CONTENT_DISPOSITION);
    217     }
    218 
    219     @Override
    220     public String getContentId() throws MessagingException {
    221         final String contentId = getFirstHeader(MimeHeader.HEADER_CONTENT_ID);
    222         if (contentId == null) {
    223             return null;
    224         } else {
    225             // remove optionally surrounding brackets.
    226             return REMOVE_OPTIONAL_BRACKETS.matcher(contentId).replaceAll("$1");
    227         }
    228     }
    229 
    230     public boolean isComplete() {
    231         return mComplete;
    232     }
    233 
    234     @Override
    235     public String getMimeType() throws MessagingException {
    236         return MimeUtility.getHeaderParameter(getContentType(), null);
    237     }
    238 
    239     @Override
    240     public int getSize() throws MessagingException {
    241         return mSize;
    242     }
    243 
    244     /**
    245      * Returns a list of the given recipient type from this message. If no addresses are
    246      * found the method returns an empty array.
    247      */
    248     @Override
    249     public Address[] getRecipients(String type) throws MessagingException {
    250         if (type == RECIPIENT_TYPE_TO) {
    251             if (mTo == null) {
    252                 mTo = Address.parse(MimeUtility.unfold(getFirstHeader("To")));
    253             }
    254             return mTo;
    255         } else if (type == RECIPIENT_TYPE_CC) {
    256             if (mCc == null) {
    257                 mCc = Address.parse(MimeUtility.unfold(getFirstHeader("CC")));
    258             }
    259             return mCc;
    260         } else if (type == RECIPIENT_TYPE_BCC) {
    261             if (mBcc == null) {
    262                 mBcc = Address.parse(MimeUtility.unfold(getFirstHeader("BCC")));
    263             }
    264             return mBcc;
    265         } else {
    266             throw new MessagingException("Unrecognized recipient type.");
    267         }
    268     }
    269 
    270     @Override
    271     public void setRecipients(String type, Address[] addresses) throws MessagingException {
    272         final int TO_LENGTH = 4;  // "To: "
    273         final int CC_LENGTH = 4;  // "Cc: "
    274         final int BCC_LENGTH = 5; // "Bcc: "
    275         if (type == RECIPIENT_TYPE_TO) {
    276             if (addresses == null || addresses.length == 0) {
    277                 removeHeader("To");
    278                 this.mTo = null;
    279             } else {
    280                 setHeader("To", MimeUtility.fold(Address.toHeader(addresses), TO_LENGTH));
    281                 this.mTo = addresses;
    282             }
    283         } else if (type == RECIPIENT_TYPE_CC) {
    284             if (addresses == null || addresses.length == 0) {
    285                 removeHeader("CC");
    286                 this.mCc = null;
    287             } else {
    288                 setHeader("CC", MimeUtility.fold(Address.toHeader(addresses), CC_LENGTH));
    289                 this.mCc = addresses;
    290             }
    291         } else if (type == RECIPIENT_TYPE_BCC) {
    292             if (addresses == null || addresses.length == 0) {
    293                 removeHeader("BCC");
    294                 this.mBcc = null;
    295             } else {
    296                 setHeader("BCC", MimeUtility.fold(Address.toHeader(addresses), BCC_LENGTH));
    297                 this.mBcc = addresses;
    298             }
    299         } else {
    300             throw new MessagingException("Unrecognized recipient type.");
    301         }
    302     }
    303 
    304     /**
    305      * Returns the unfolded, decoded value of the Subject header.
    306      */
    307     @Override
    308     public String getSubject() throws MessagingException {
    309         return MimeUtility.unfoldAndDecode(getFirstHeader("Subject"));
    310     }
    311 
    312     @Override
    313     public void setSubject(String subject) throws MessagingException {
    314         final int HEADER_NAME_LENGTH = 9;     // "Subject: "
    315         setHeader("Subject", MimeUtility.foldAndEncode2(subject, HEADER_NAME_LENGTH));
    316     }
    317 
    318     @Override
    319     public Address[] getFrom() throws MessagingException {
    320         if (mFrom == null) {
    321             String list = MimeUtility.unfold(getFirstHeader("From"));
    322             if (list == null || list.length() == 0) {
    323                 list = MimeUtility.unfold(getFirstHeader("Sender"));
    324             }
    325             mFrom = Address.parse(list);
    326         }
    327         return mFrom;
    328     }
    329 
    330     @Override
    331     public void setFrom(Address from) throws MessagingException {
    332         final int FROM_LENGTH = 6;  // "From: "
    333         if (from != null) {
    334             setHeader("From", MimeUtility.fold(from.toHeader(), FROM_LENGTH));
    335             this.mFrom = new Address[] {
    336                     from
    337                 };
    338         } else {
    339             this.mFrom = null;
    340         }
    341     }
    342 
    343     @Override
    344     public Address[] getReplyTo() throws MessagingException {
    345         if (mReplyTo == null) {
    346             mReplyTo = Address.parse(MimeUtility.unfold(getFirstHeader("Reply-to")));
    347         }
    348         return mReplyTo;
    349     }
    350 
    351     @Override
    352     public void setReplyTo(Address[] replyTo) throws MessagingException {
    353         final int REPLY_TO_LENGTH = 10;  // "Reply-to: "
    354         if (replyTo == null || replyTo.length == 0) {
    355             removeHeader("Reply-to");
    356             mReplyTo = null;
    357         } else {
    358             setHeader("Reply-to", MimeUtility.fold(Address.toHeader(replyTo), REPLY_TO_LENGTH));
    359             mReplyTo = replyTo;
    360         }
    361     }
    362 
    363     /**
    364      * Set the mime "Message-ID" header
    365      * @param messageId the new Message-ID value
    366      * @throws MessagingException
    367      */
    368     @Override
    369     public void setMessageId(String messageId) throws MessagingException {
    370         setHeader("Message-ID", messageId);
    371     }
    372 
    373     /**
    374      * Get the mime "Message-ID" header.  This value will be preloaded with a locally-generated
    375      * random ID, if the value has not previously been set.  Local generation can be inhibited/
    376      * overridden by explicitly clearing the headers, removing the message-id header, etc.
    377      * @return the Message-ID header string, or null if explicitly has been set to null
    378      */
    379     @Override
    380     public String getMessageId() throws MessagingException {
    381         String messageId = getFirstHeader("Message-ID");
    382         if (messageId == null && !mInhibitLocalMessageId) {
    383             messageId = generateMessageId();
    384             setMessageId(messageId);
    385         }
    386         return messageId;
    387     }
    388 
    389     @Override
    390     public void saveChanges() throws MessagingException {
    391         throw new MessagingException("saveChanges not yet implemented");
    392     }
    393 
    394     @Override
    395     public Body getBody() throws MessagingException {
    396         return mBody;
    397     }
    398 
    399     @Override
    400     public void setBody(Body body) throws MessagingException {
    401         this.mBody = body;
    402         if (body instanceof Multipart) {
    403             final Multipart multipart = ((Multipart)body);
    404             multipart.setParent(this);
    405             setHeader(MimeHeader.HEADER_CONTENT_TYPE, multipart.getContentType());
    406             setHeader("MIME-Version", "1.0");
    407         }
    408         else if (body instanceof TextBody) {
    409             setHeader(MimeHeader.HEADER_CONTENT_TYPE, String.format("%s;\n charset=utf-8",
    410                     getMimeType()));
    411             setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64");
    412         }
    413     }
    414 
    415     protected String getFirstHeader(String name) throws MessagingException {
    416         return getMimeHeaders().getFirstHeader(name);
    417     }
    418 
    419     @Override
    420     public void addHeader(String name, String value) throws MessagingException {
    421         getMimeHeaders().addHeader(name, value);
    422     }
    423 
    424     @Override
    425     public void setHeader(String name, String value) throws MessagingException {
    426         getMimeHeaders().setHeader(name, value);
    427     }
    428 
    429     @Override
    430     public String[] getHeader(String name) throws MessagingException {
    431         return getMimeHeaders().getHeader(name);
    432     }
    433 
    434     @Override
    435     public void removeHeader(String name) throws MessagingException {
    436         getMimeHeaders().removeHeader(name);
    437         if ("Message-ID".equalsIgnoreCase(name)) {
    438             mInhibitLocalMessageId = true;
    439         }
    440     }
    441 
    442     /**
    443      * Set extended header
    444      *
    445      * @param name Extended header name
    446      * @param value header value - flattened by removing CR-NL if any
    447      * remove header if value is null
    448      * @throws MessagingException
    449      */
    450     @Override
    451     public void setExtendedHeader(String name, String value) throws MessagingException {
    452         if (value == null) {
    453             if (mExtendedHeader != null) {
    454                 mExtendedHeader.removeHeader(name);
    455             }
    456             return;
    457         }
    458         if (mExtendedHeader == null) {
    459             mExtendedHeader = new MimeHeader();
    460         }
    461         mExtendedHeader.setHeader(name, END_OF_LINE.matcher(value).replaceAll(""));
    462     }
    463 
    464     /**
    465      * Get extended header
    466      *
    467      * @param name Extended header name
    468      * @return header value - null if header does not exist
    469      * @throws MessagingException
    470      */
    471     @Override
    472     public String getExtendedHeader(String name) throws MessagingException {
    473         if (mExtendedHeader == null) {
    474             return null;
    475         }
    476         return mExtendedHeader.getFirstHeader(name);
    477     }
    478 
    479     /**
    480      * Set entire extended headers from String
    481      *
    482      * @param headers Extended header and its value - "CR-NL-separated pairs
    483      * if null or empty, remove entire extended headers
    484      * @throws MessagingException
    485      */
    486     public void setExtendedHeaders(String headers) throws MessagingException {
    487         if (TextUtils.isEmpty(headers)) {
    488             mExtendedHeader = null;
    489         } else {
    490             mExtendedHeader = new MimeHeader();
    491             for (final String header : END_OF_LINE.split(headers)) {
    492                 final String[] tokens = header.split(":", 2);
    493                 if (tokens.length != 2) {
    494                     throw new MessagingException("Illegal extended headers: " + headers);
    495                 }
    496                 mExtendedHeader.setHeader(tokens[0].trim(), tokens[1].trim());
    497             }
    498         }
    499     }
    500 
    501     /**
    502      * Get entire extended headers as String
    503      *
    504      * @return "CR-NL-separated extended headers - null if extended header does not exist
    505      */
    506     public String getExtendedHeaders() {
    507         if (mExtendedHeader != null) {
    508             return mExtendedHeader.writeToString();
    509         }
    510         return null;
    511     }
    512 
    513     /**
    514      * Write message header and body to output stream
    515      *
    516      * @param out Output steam to write message header and body.
    517      */
    518     @Override
    519     public void writeTo(OutputStream out) throws IOException, MessagingException {
    520         final BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024);
    521         // Force creation of local message-id
    522         getMessageId();
    523         getMimeHeaders().writeTo(out);
    524         // mExtendedHeader will not be write out to external output stream,
    525         // because it is intended to internal use.
    526         writer.write("\r\n");
    527         writer.flush();
    528         if (mBody != null) {
    529             mBody.writeTo(out);
    530         }
    531     }
    532 
    533     @Override
    534     public InputStream getInputStream() throws MessagingException {
    535         return null;
    536     }
    537 
    538     class MimeMessageBuilder implements ContentHandler {
    539         private final Stack<Object> stack = new Stack<Object>();
    540 
    541         public MimeMessageBuilder() {
    542         }
    543 
    544         private void expect(Class<?> c) {
    545             if (!c.isInstance(stack.peek())) {
    546                 throw new IllegalStateException("Internal stack error: " + "Expected '"
    547                         + c.getName() + "' found '" + stack.peek().getClass().getName() + "'");
    548             }
    549         }
    550 
    551         @Override
    552         public void startMessage() {
    553             if (stack.isEmpty()) {
    554                 stack.push(MimeMessage.this);
    555             } else {
    556                 expect(Part.class);
    557                 try {
    558                     final MimeMessage m = new MimeMessage();
    559                     ((Part)stack.peek()).setBody(m);
    560                     stack.push(m);
    561                 } catch (MessagingException me) {
    562                     throw new Error(me);
    563                 }
    564             }
    565         }
    566 
    567         @Override
    568         public void endMessage() {
    569             expect(MimeMessage.class);
    570             stack.pop();
    571         }
    572 
    573         @Override
    574         public void startHeader() {
    575             expect(Part.class);
    576         }
    577 
    578         @Override
    579         public void field(String fieldData) {
    580             expect(Part.class);
    581             try {
    582                 final String[] tokens = fieldData.split(":", 2);
    583                 ((Part)stack.peek()).addHeader(tokens[0], tokens[1].trim());
    584             } catch (MessagingException me) {
    585                 throw new Error(me);
    586             }
    587         }
    588 
    589         @Override
    590         public void endHeader() {
    591             expect(Part.class);
    592         }
    593 
    594         @Override
    595         public void startMultipart(BodyDescriptor bd) {
    596             expect(Part.class);
    597 
    598             final Part e = (Part)stack.peek();
    599             try {
    600                 final MimeMultipart multiPart = new MimeMultipart(e.getContentType());
    601                 e.setBody(multiPart);
    602                 stack.push(multiPart);
    603             } catch (MessagingException me) {
    604                 throw new Error(me);
    605             }
    606         }
    607 
    608         @Override
    609         public void body(BodyDescriptor bd, InputStream in) throws IOException {
    610             expect(Part.class);
    611             final Body body = MimeUtility.decodeBody(in, bd.getTransferEncoding());
    612             try {
    613                 ((Part)stack.peek()).setBody(body);
    614             } catch (MessagingException me) {
    615                 throw new Error(me);
    616             }
    617         }
    618 
    619         @Override
    620         public void endMultipart() {
    621             stack.pop();
    622         }
    623 
    624         @Override
    625         public void startBodyPart() {
    626             expect(MimeMultipart.class);
    627 
    628             try {
    629                 final MimeBodyPart bodyPart = new MimeBodyPart();
    630                 ((MimeMultipart)stack.peek()).addBodyPart(bodyPart);
    631                 stack.push(bodyPart);
    632             } catch (MessagingException me) {
    633                 throw new Error(me);
    634             }
    635         }
    636 
    637         @Override
    638         public void endBodyPart() {
    639             expect(BodyPart.class);
    640             stack.pop();
    641         }
    642 
    643         @Override
    644         public void epilogue(InputStream is) throws IOException {
    645             expect(MimeMultipart.class);
    646             final StringBuilder sb = new StringBuilder();
    647             int b;
    648             while ((b = is.read()) != -1) {
    649                 sb.append((char)b);
    650             }
    651             // TODO: why is this commented out?
    652             // ((Multipart) stack.peek()).setEpilogue(sb.toString());
    653         }
    654 
    655         @Override
    656         public void preamble(InputStream is) throws IOException {
    657             expect(MimeMultipart.class);
    658             final StringBuilder sb = new StringBuilder();
    659             int b;
    660             while ((b = is.read()) != -1) {
    661                 sb.append((char)b);
    662             }
    663             try {
    664                 ((MimeMultipart)stack.peek()).setPreamble(sb.toString());
    665             } catch (MessagingException me) {
    666                 throw new Error(me);
    667             }
    668         }
    669 
    670         @Override
    671         public void raw(InputStream is) throws IOException {
    672             throw new UnsupportedOperationException("Not supported");
    673         }
    674     }
    675 }
    676