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