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