• 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         StringBuffer sb = new StringBuffer();
100         sb.append("<");
101         for (int i = 0; i < 24; i++) {
102             // We'll use a 5-bit range (0..31)
103             int value = sRandom.nextInt() & 31;
104             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
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         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         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         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 
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 
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         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         String contentDisposition = getFirstHeader(MimeHeader.HEADER_CONTENT_DISPOSITION);
218         if (contentDisposition == null) {
219             return null;
220         } else {
221             return contentDisposition;
222         }
223     }
224 
225     @Override
getContentId()226     public String getContentId() throws MessagingException {
227         String contentId = getFirstHeader(MimeHeader.HEADER_CONTENT_ID);
228         if (contentId == null) {
229             return null;
230         } else {
231             // remove optionally surrounding brackets.
232             return REMOVE_OPTIONAL_BRACKETS.matcher(contentId).replaceAll("$1");
233         }
234     }
235 
isComplete()236     public boolean isComplete() {
237         return mComplete;
238     }
239 
240     @Override
getMimeType()241     public String getMimeType() throws MessagingException {
242         return MimeUtility.getHeaderParameter(getContentType(), null);
243     }
244 
245     @Override
getSize()246     public int getSize() throws MessagingException {
247         return mSize;
248     }
249 
250     /**
251      * Returns a list of the given recipient type from this message. If no addresses are
252      * found the method returns an empty array.
253      */
254     @Override
getRecipients(RecipientType type)255     public Address[] getRecipients(RecipientType type) throws MessagingException {
256         if (type == RecipientType.TO) {
257             if (mTo == null) {
258                 mTo = Address.parse(MimeUtility.unfold(getFirstHeader("To")));
259             }
260             return mTo;
261         } else if (type == RecipientType.CC) {
262             if (mCc == null) {
263                 mCc = Address.parse(MimeUtility.unfold(getFirstHeader("CC")));
264             }
265             return mCc;
266         } else if (type == RecipientType.BCC) {
267             if (mBcc == null) {
268                 mBcc = Address.parse(MimeUtility.unfold(getFirstHeader("BCC")));
269             }
270             return mBcc;
271         } else {
272             throw new MessagingException("Unrecognized recipient type.");
273         }
274     }
275 
276     @Override
setRecipients(RecipientType type, Address[] addresses)277     public void setRecipients(RecipientType type, Address[] addresses) throws MessagingException {
278         final int TO_LENGTH = 4;  // "To: "
279         final int CC_LENGTH = 4;  // "Cc: "
280         final int BCC_LENGTH = 5; // "Bcc: "
281         if (type == RecipientType.TO) {
282             if (addresses == null || addresses.length == 0) {
283                 removeHeader("To");
284                 this.mTo = null;
285             } else {
286                 setHeader("To", MimeUtility.fold(Address.toHeader(addresses), TO_LENGTH));
287                 this.mTo = addresses;
288             }
289         } else if (type == RecipientType.CC) {
290             if (addresses == null || addresses.length == 0) {
291                 removeHeader("CC");
292                 this.mCc = null;
293             } else {
294                 setHeader("CC", MimeUtility.fold(Address.toHeader(addresses), CC_LENGTH));
295                 this.mCc = addresses;
296             }
297         } else if (type == RecipientType.BCC) {
298             if (addresses == null || addresses.length == 0) {
299                 removeHeader("BCC");
300                 this.mBcc = null;
301             } else {
302                 setHeader("BCC", MimeUtility.fold(Address.toHeader(addresses), BCC_LENGTH));
303                 this.mBcc = addresses;
304             }
305         } else {
306             throw new MessagingException("Unrecognized recipient type.");
307         }
308     }
309 
310     /**
311      * Returns the unfolded, decoded value of the Subject header.
312      */
313     @Override
getSubject()314     public String getSubject() throws MessagingException {
315         return MimeUtility.unfoldAndDecode(getFirstHeader("Subject"));
316     }
317 
318     @Override
setSubject(String subject)319     public void setSubject(String subject) throws MessagingException {
320         final int HEADER_NAME_LENGTH = 9;     // "Subject: "
321         setHeader("Subject", MimeUtility.foldAndEncode2(subject, HEADER_NAME_LENGTH));
322     }
323 
324     @Override
getFrom()325     public Address[] getFrom() throws MessagingException {
326         if (mFrom == null) {
327             String list = MimeUtility.unfold(getFirstHeader("From"));
328             if (list == null || list.length() == 0) {
329                 list = MimeUtility.unfold(getFirstHeader("Sender"));
330             }
331             mFrom = Address.parse(list);
332         }
333         return mFrom;
334     }
335 
336     @Override
setFrom(Address from)337     public void setFrom(Address from) throws MessagingException {
338         final int FROM_LENGTH = 6;  // "From: "
339         if (from != null) {
340             setHeader("From", MimeUtility.fold(from.toHeader(), FROM_LENGTH));
341             this.mFrom = new Address[] {
342                     from
343                 };
344         } else {
345             this.mFrom = null;
346         }
347     }
348 
349     @Override
getReplyTo()350     public Address[] getReplyTo() throws MessagingException {
351         if (mReplyTo == null) {
352             mReplyTo = Address.parse(MimeUtility.unfold(getFirstHeader("Reply-to")));
353         }
354         return mReplyTo;
355     }
356 
357     @Override
setReplyTo(Address[] replyTo)358     public void setReplyTo(Address[] replyTo) throws MessagingException {
359         final int REPLY_TO_LENGTH = 10;  // "Reply-to: "
360         if (replyTo == null || replyTo.length == 0) {
361             removeHeader("Reply-to");
362             mReplyTo = null;
363         } else {
364             setHeader("Reply-to", MimeUtility.fold(Address.toHeader(replyTo), REPLY_TO_LENGTH));
365             mReplyTo = replyTo;
366         }
367     }
368 
369     /**
370      * Set the mime "Message-ID" header
371      * @param messageId the new Message-ID value
372      * @throws MessagingException
373      */
374     @Override
setMessageId(String messageId)375     public void setMessageId(String messageId) throws MessagingException {
376         setHeader("Message-ID", messageId);
377     }
378 
379     /**
380      * Get the mime "Message-ID" header.  This value will be preloaded with a locally-generated
381      * random ID, if the value has not previously been set.  Local generation can be inhibited/
382      * overridden by explicitly clearing the headers, removing the message-id header, etc.
383      * @return the Message-ID header string, or null if explicitly has been set to null
384      */
385     @Override
getMessageId()386     public String getMessageId() throws MessagingException {
387         String messageId = getFirstHeader("Message-ID");
388         if (messageId == null && !mInhibitLocalMessageId) {
389             messageId = generateMessageId();
390             setMessageId(messageId);
391         }
392         return messageId;
393     }
394 
395     @Override
saveChanges()396     public void saveChanges() throws MessagingException {
397         throw new MessagingException("saveChanges not yet implemented");
398     }
399 
400     @Override
getBody()401     public Body getBody() throws MessagingException {
402         return mBody;
403     }
404 
405     @Override
setBody(Body body)406     public void setBody(Body body) throws MessagingException {
407         this.mBody = body;
408         if (body instanceof Multipart) {
409             Multipart multipart = ((Multipart)body);
410             multipart.setParent(this);
411             setHeader(MimeHeader.HEADER_CONTENT_TYPE, multipart.getContentType());
412             setHeader("MIME-Version", "1.0");
413         }
414         else if (body instanceof TextBody) {
415             setHeader(MimeHeader.HEADER_CONTENT_TYPE, String.format("%s;\n charset=utf-8",
416                     getMimeType()));
417             setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64");
418         }
419     }
420 
getFirstHeader(String name)421     protected String getFirstHeader(String name) throws MessagingException {
422         return getMimeHeaders().getFirstHeader(name);
423     }
424 
425     @Override
addHeader(String name, String value)426     public void addHeader(String name, String value) throws MessagingException {
427         getMimeHeaders().addHeader(name, value);
428     }
429 
430     @Override
setHeader(String name, String value)431     public void setHeader(String name, String value) throws MessagingException {
432         getMimeHeaders().setHeader(name, value);
433     }
434 
435     @Override
getHeader(String name)436     public String[] getHeader(String name) throws MessagingException {
437         return getMimeHeaders().getHeader(name);
438     }
439 
440     @Override
removeHeader(String name)441     public void removeHeader(String name) throws MessagingException {
442         getMimeHeaders().removeHeader(name);
443         if ("Message-ID".equalsIgnoreCase(name)) {
444             mInhibitLocalMessageId = true;
445         }
446     }
447 
448     /**
449      * Set extended header
450      *
451      * @param name Extended header name
452      * @param value header value - flattened by removing CR-NL if any
453      * remove header if value is null
454      * @throws MessagingException
455      */
456     @Override
setExtendedHeader(String name, String value)457     public void setExtendedHeader(String name, String value) throws MessagingException {
458         if (value == null) {
459             if (mExtendedHeader != null) {
460                 mExtendedHeader.removeHeader(name);
461             }
462             return;
463         }
464         if (mExtendedHeader == null) {
465             mExtendedHeader = new MimeHeader();
466         }
467         mExtendedHeader.setHeader(name, END_OF_LINE.matcher(value).replaceAll(""));
468     }
469 
470     /**
471      * Get extended header
472      *
473      * @param name Extended header name
474      * @return header value - null if header does not exist
475      * @throws MessagingException
476      */
477     @Override
getExtendedHeader(String name)478     public String getExtendedHeader(String name) throws MessagingException {
479         if (mExtendedHeader == null) {
480             return null;
481         }
482         return mExtendedHeader.getFirstHeader(name);
483     }
484 
485     /**
486      * Set entire extended headers from String
487      *
488      * @param headers Extended header and its value - "CR-NL-separated pairs
489      * if null or empty, remove entire extended headers
490      * @throws MessagingException
491      */
setExtendedHeaders(String headers)492     public void setExtendedHeaders(String headers) throws MessagingException {
493         if (TextUtils.isEmpty(headers)) {
494             mExtendedHeader = null;
495         } else {
496             mExtendedHeader = new MimeHeader();
497             for (String header : END_OF_LINE.split(headers)) {
498                 String[] tokens = header.split(":", 2);
499                 if (tokens.length != 2) {
500                     throw new MessagingException("Illegal extended headers: " + headers);
501                 }
502                 mExtendedHeader.setHeader(tokens[0].trim(), tokens[1].trim());
503             }
504         }
505     }
506 
507     /**
508      * Get entire extended headers as String
509      *
510      * @return "CR-NL-separated extended headers - null if extended header does not exist
511      */
getExtendedHeaders()512     public String getExtendedHeaders() {
513         if (mExtendedHeader != null) {
514             return mExtendedHeader.writeToString();
515         }
516         return null;
517     }
518 
519     /**
520      * Write message header and body to output stream
521      *
522      * @param out Output steam to write message header and body.
523      */
524     @Override
writeTo(OutputStream out)525     public void writeTo(OutputStream out) throws IOException, MessagingException {
526         BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024);
527         // Force creation of local message-id
528         getMessageId();
529         getMimeHeaders().writeTo(out);
530         // mExtendedHeader will not be write out to external output stream,
531         // because it is intended to internal use.
532         writer.write("\r\n");
533         writer.flush();
534         if (mBody != null) {
535             mBody.writeTo(out);
536         }
537     }
538 
539     @Override
getInputStream()540     public InputStream getInputStream() throws MessagingException {
541         return null;
542     }
543 
544     class MimeMessageBuilder implements ContentHandler {
545         private final Stack<Object> stack = new Stack<Object>();
546 
MimeMessageBuilder()547         public MimeMessageBuilder() {
548         }
549 
expect(Class<?> c)550         private void expect(Class<?> c) {
551             if (!c.isInstance(stack.peek())) {
552                 throw new IllegalStateException("Internal stack error: " + "Expected '"
553                         + c.getName() + "' found '" + stack.peek().getClass().getName() + "'");
554             }
555         }
556 
557         @Override
startMessage()558         public void startMessage() {
559             if (stack.isEmpty()) {
560                 stack.push(MimeMessage.this);
561             } else {
562                 expect(Part.class);
563                 try {
564                     MimeMessage m = new MimeMessage();
565                     ((Part)stack.peek()).setBody(m);
566                     stack.push(m);
567                 } catch (MessagingException me) {
568                     throw new Error(me);
569                 }
570             }
571         }
572 
573         @Override
endMessage()574         public void endMessage() {
575             expect(MimeMessage.class);
576             stack.pop();
577         }
578 
579         @Override
startHeader()580         public void startHeader() {
581             expect(Part.class);
582         }
583 
584         @Override
field(String fieldData)585         public void field(String fieldData) {
586             expect(Part.class);
587             try {
588                 String[] tokens = fieldData.split(":", 2);
589                 ((Part)stack.peek()).addHeader(tokens[0], tokens[1].trim());
590             } catch (MessagingException me) {
591                 throw new Error(me);
592             }
593         }
594 
595         @Override
endHeader()596         public void endHeader() {
597             expect(Part.class);
598         }
599 
600         @Override
startMultipart(BodyDescriptor bd)601         public void startMultipart(BodyDescriptor bd) {
602             expect(Part.class);
603 
604             Part e = (Part)stack.peek();
605             try {
606                 MimeMultipart multiPart = new MimeMultipart(e.getContentType());
607                 e.setBody(multiPart);
608                 stack.push(multiPart);
609             } catch (MessagingException me) {
610                 throw new Error(me);
611             }
612         }
613 
614         @Override
body(BodyDescriptor bd, InputStream in)615         public void body(BodyDescriptor bd, InputStream in) throws IOException {
616             expect(Part.class);
617             Body body = MimeUtility.decodeBody(in, bd.getTransferEncoding());
618             try {
619                 ((Part)stack.peek()).setBody(body);
620             } catch (MessagingException me) {
621                 throw new Error(me);
622             }
623         }
624 
625         @Override
endMultipart()626         public void endMultipart() {
627             stack.pop();
628         }
629 
630         @Override
startBodyPart()631         public void startBodyPart() {
632             expect(MimeMultipart.class);
633 
634             try {
635                 MimeBodyPart bodyPart = new MimeBodyPart();
636                 ((MimeMultipart)stack.peek()).addBodyPart(bodyPart);
637                 stack.push(bodyPart);
638             } catch (MessagingException me) {
639                 throw new Error(me);
640             }
641         }
642 
643         @Override
endBodyPart()644         public void endBodyPart() {
645             expect(BodyPart.class);
646             stack.pop();
647         }
648 
649         @Override
epilogue(InputStream is)650         public void epilogue(InputStream is) throws IOException {
651             expect(MimeMultipart.class);
652             StringBuffer sb = new StringBuffer();
653             int b;
654             while ((b = is.read()) != -1) {
655                 sb.append((char)b);
656             }
657             // ((Multipart) stack.peek()).setEpilogue(sb.toString());
658         }
659 
660         @Override
preamble(InputStream is)661         public void preamble(InputStream is) throws IOException {
662             expect(MimeMultipart.class);
663             StringBuffer sb = new StringBuffer();
664             int b;
665             while ((b = is.read()) != -1) {
666                 sb.append((char)b);
667             }
668             try {
669                 ((MimeMultipart)stack.peek()).setPreamble(sb.toString());
670             } catch (MessagingException me) {
671                 throw new Error(me);
672             }
673         }
674 
675         @Override
raw(InputStream is)676         public void raw(InputStream is) throws IOException {
677             throw new UnsupportedOperationException("Not supported");
678         }
679     }
680 }
681