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 */
16package com.android.voicemail.impl.mail.internet;
17
18import android.text.TextUtils;
19import com.android.voicemail.impl.mail.Address;
20import com.android.voicemail.impl.mail.Body;
21import com.android.voicemail.impl.mail.BodyPart;
22import com.android.voicemail.impl.mail.Message;
23import com.android.voicemail.impl.mail.MessagingException;
24import com.android.voicemail.impl.mail.Multipart;
25import com.android.voicemail.impl.mail.Part;
26import com.android.voicemail.impl.mail.utils.LogUtils;
27import java.io.BufferedWriter;
28import java.io.IOException;
29import java.io.InputStream;
30import java.io.OutputStream;
31import java.io.OutputStreamWriter;
32import java.text.SimpleDateFormat;
33import java.util.Date;
34import java.util.Locale;
35import java.util.Stack;
36import java.util.regex.Pattern;
37import org.apache.james.mime4j.MimeException;
38import org.apache.james.mime4j.dom.field.DateTimeField;
39import org.apache.james.mime4j.field.DefaultFieldParser;
40import org.apache.james.mime4j.io.EOLConvertingInputStream;
41import org.apache.james.mime4j.parser.ContentHandler;
42import org.apache.james.mime4j.parser.MimeStreamParser;
43import org.apache.james.mime4j.stream.BodyDescriptor;
44import 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 */
53public 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
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   */
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   */
118  public MimeMessage(InputStream in) throws IOException, MessagingException, MimeException {
119    parse(in);
120  }
121
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
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   */
149  private MimeHeader getMimeHeaders() {
150    if (mHeader == null) {
151      mHeader = new MimeHeader();
152    }
153    return mHeader;
154  }
155
156  @Override
157  public Date getReceivedDate() throws MessagingException {
158    return null;
159  }
160
161  @Override
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
194  public void setSentDate(Date sentDate) throws MessagingException {
195    setHeader("Date", DATE_FORMAT.format(sentDate));
196    this.mSentDate = sentDate;
197  }
198
199  @Override
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
210  public String getDisposition() throws MessagingException {
211    return getFirstHeader(MimeHeader.HEADER_CONTENT_DISPOSITION);
212  }
213
214  @Override
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
226  public String getMimeType() throws MessagingException {
227    return MimeUtility.getHeaderParameter(getContentType(), null);
228  }
229
230  @Override
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
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
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
297  public String getSubject() throws MessagingException {
298    return MimeUtility.unfoldAndDecode(getFirstHeader("Subject"));
299  }
300
301  @Override
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
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
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
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
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
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
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
379  public void saveChanges() throws MessagingException {
380    throw new MessagingException("saveChanges not yet implemented");
381  }
382
383  @Override
384  public Body getBody() throws MessagingException {
385    return mBody;
386  }
387
388  @Override
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
403  protected String getFirstHeader(String name) throws MessagingException {
404    return getMimeHeaders().getFirstHeader(name);
405  }
406
407  @Override
408  public void addHeader(String name, String value) throws MessagingException {
409    getMimeHeaders().addHeader(name, value);
410  }
411
412  @Override
413  public void setHeader(String name, String value) throws MessagingException {
414    getMimeHeaders().setHeader(name, value);
415  }
416
417  @Override
418  public String[] getHeader(String name) throws MessagingException {
419    return getMimeHeaders().getHeader(name);
420  }
421
422  @Override
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
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
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   */
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   */
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
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
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
528    public MimeMessageBuilder() {}
529
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
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
559    public void endMessage() {
560      expect(MimeMessage.class);
561      stack.pop();
562    }
563
564    @Override
565    public void startHeader() {
566      expect(Part.class);
567    }
568
569    @Override
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
581    public void endHeader() {
582      expect(Part.class);
583    }
584
585    @Override
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
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
611    public void endMultipart() {
612      stack.pop();
613    }
614
615    @Override
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
629    public void endBodyPart() {
630      expect(BodyPart.class);
631      stack.pop();
632    }
633
634    @Override
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
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
662    public void raw(InputStream is) throws IOException {
663      throw new UnsupportedOperationException("Not supported");
664    }
665  }
666}
667