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 
MimeMessage()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    */
generateMessageId()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    */
MimeMessage(InputStream in)120   public MimeMessage(InputStream in) throws IOException, MessagingException, MimeException {
121     parse(in);
122   }
123 
init()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 
parse(InputStream in)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    */
getMimeHeaders()151   private MimeHeader getMimeHeaders() {
152     if (header == null) {
153       header = new MimeHeader();
154     }
155     return header;
156   }
157 
158   @Override
getReceivedDate()159   public Date getReceivedDate() throws MessagingException {
160     return null;
161   }
162 
163   @Override
getSentDate()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
setSentDate(Date sentDate)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
getDuration()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
getContentType()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
getDisposition()234   public String getDisposition() throws MessagingException {
235     return getFirstHeader(MimeHeader.HEADER_CONTENT_DISPOSITION);
236   }
237 
238   @Override
getContentId()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
getMimeType()250   public String getMimeType() throws MessagingException {
251     return MimeUtility.getHeaderParameter(getContentType(), null);
252   }
253 
254   @Override
getSize()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
getRecipients(String type)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
setRecipients(String type, Address[] addresses)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
getSubject()321   public String getSubject() throws MessagingException {
322     return MimeUtility.unfoldAndDecode(getFirstHeader("Subject"));
323   }
324 
325   @Override
setSubject(String subject)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
getFrom()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
setFrom(Address from)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
getReplyTo()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
setReplyTo(Address[] replyTo)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
setMessageId(String messageId)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
getMessageId()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
saveChanges()403   public void saveChanges() throws MessagingException {
404     throw new MessagingException("saveChanges not yet implemented");
405   }
406 
407   @Override
getBody()408   public Body getBody() throws MessagingException {
409     return body;
410   }
411 
412   @Override
setBody(Body body)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 
getFirstHeader(String name)427   protected String getFirstHeader(String name) throws MessagingException {
428     return getMimeHeaders().getFirstHeader(name);
429   }
430 
431   @Override
addHeader(String name, String value)432   public void addHeader(String name, String value) throws MessagingException {
433     getMimeHeaders().addHeader(name, value);
434   }
435 
436   @Override
setHeader(String name, String value)437   public void setHeader(String name, String value) throws MessagingException {
438     getMimeHeaders().setHeader(name, value);
439   }
440 
441   @Override
getHeader(String name)442   public String[] getHeader(String name) throws MessagingException {
443     return getMimeHeaders().getHeader(name);
444   }
445 
446   @Override
removeHeader(String name)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
setExtendedHeader(String name, String value)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
getExtendedHeader(String name)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    */
setExtendedHeaders(String headers)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    */
getExtendedHeaders()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
writeTo(OutputStream out)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
getInputStream()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 
MimeMessageBuilder()552     public MimeMessageBuilder() {}
553 
expect(Class<?> c)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
startMessage()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
endMessage()583     public void endMessage() {
584       expect(MimeMessage.class);
585       stack.pop();
586     }
587 
588     @Override
startHeader()589     public void startHeader() {
590       expect(Part.class);
591     }
592 
593     @Override
field(Field rawField)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
endHeader()605     public void endHeader() {
606       expect(Part.class);
607     }
608 
609     @Override
startMultipart(BodyDescriptor bd)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
body(BodyDescriptor bd, InputStream in)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
endMultipart()635     public void endMultipart() {
636       stack.pop();
637     }
638 
639     @Override
startBodyPart()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
endBodyPart()653     public void endBodyPart() {
654       expect(BodyPart.class);
655       stack.pop();
656     }
657 
658     @Override
epilogue(InputStream is)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
preamble(InputStream is)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
raw(InputStream is)686     public void raw(InputStream is) throws IOException {
687       throw new UnsupportedOperationException("Not supported");
688     }
689   }
690 }
691