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