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