summaryrefslogtreecommitdiffstats
path: root/src/com/android/emailcommon/internet
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/emailcommon/internet')
-rw-r--r--src/com/android/emailcommon/internet/BinaryTempFileBody.java89
-rw-r--r--src/com/android/emailcommon/internet/MimeBodyPart.java193
-rw-r--r--src/com/android/emailcommon/internet/MimeHeader.java153
-rw-r--r--src/com/android/emailcommon/internet/MimeMessage.java626
-rw-r--r--src/com/android/emailcommon/internet/MimeMultipart.java111
-rw-r--r--src/com/android/emailcommon/internet/MimeUtility.java452
-rw-r--r--src/com/android/emailcommon/internet/Rfc822Output.java357
-rw-r--r--src/com/android/emailcommon/internet/TextBody.java62
8 files changed, 2043 insertions, 0 deletions
diff --git a/src/com/android/emailcommon/internet/BinaryTempFileBody.java b/src/com/android/emailcommon/internet/BinaryTempFileBody.java
new file mode 100644
index 000000000..f0821edd4
--- /dev/null
+++ b/src/com/android/emailcommon/internet/BinaryTempFileBody.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.internet;
+
+import com.android.emailcommon.TempDirectory;
+import com.android.emailcommon.mail.Body;
+import com.android.emailcommon.mail.MessagingException;
+
+import org.apache.commons.io.IOUtils;
+
+import android.util.Base64;
+import android.util.Base64OutputStream;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * A Body that is backed by a temp file. The Body exposes a getOutputStream method that allows
+ * the user to write to the temp file. After the write the body is available via getInputStream
+ * and writeTo one time. After writeTo is called, or the InputStream returned from
+ * getInputStream is closed the file is deleted and the Body should be considered disposed of.
+ */
+public class BinaryTempFileBody implements Body {
+ private File mFile;
+
+ /**
+ * An alternate way to put data into a BinaryTempFileBody is to simply supply an already-
+ * created file. Note that this file will be deleted after it is read.
+ * @param filePath The file containing the data to be stored on disk temporarily
+ */
+ public void setFile(String filePath) {
+ mFile = new File(filePath);
+ }
+
+ public OutputStream getOutputStream() throws IOException {
+ mFile = File.createTempFile("body", null, TempDirectory.getTempDirectory());
+ mFile.deleteOnExit();
+ return new FileOutputStream(mFile);
+ }
+
+ public InputStream getInputStream() throws MessagingException {
+ try {
+ return new BinaryTempFileBodyInputStream(new FileInputStream(mFile));
+ }
+ catch (IOException ioe) {
+ throw new MessagingException("Unable to open body", ioe);
+ }
+ }
+
+ public void writeTo(OutputStream out) throws IOException, MessagingException {
+ InputStream in = getInputStream();
+ Base64OutputStream base64Out = new Base64OutputStream(
+ out, Base64.CRLF | Base64.NO_CLOSE);
+ IOUtils.copy(in, base64Out);
+ base64Out.close();
+ mFile.delete();
+ }
+
+ class BinaryTempFileBodyInputStream extends FilterInputStream {
+ public BinaryTempFileBodyInputStream(InputStream in) {
+ super(in);
+ }
+
+ @Override
+ public void close() throws IOException {
+ super.close();
+ mFile.delete();
+ }
+ }
+}
diff --git a/src/com/android/emailcommon/internet/MimeBodyPart.java b/src/com/android/emailcommon/internet/MimeBodyPart.java
new file mode 100644
index 000000000..01efd554f
--- /dev/null
+++ b/src/com/android/emailcommon/internet/MimeBodyPart.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.internet;
+
+import com.android.emailcommon.mail.Body;
+import com.android.emailcommon.mail.BodyPart;
+import com.android.emailcommon.mail.MessagingException;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.util.regex.Pattern;
+
+/**
+ * TODO this is a close approximation of Message, need to update along with
+ * Message.
+ */
+public class MimeBodyPart extends BodyPart {
+ protected MimeHeader mHeader = new MimeHeader();
+ protected MimeHeader mExtendedHeader;
+ protected Body mBody;
+ protected int mSize;
+
+ // regex that matches content id surrounded by "<>" optionally.
+ private static final Pattern REMOVE_OPTIONAL_BRACKETS = Pattern.compile("^<?([^>]+)>?$");
+ // regex that matches end of line.
+ private static final Pattern END_OF_LINE = Pattern.compile("\r?\n");
+
+ public MimeBodyPart() throws MessagingException {
+ this(null);
+ }
+
+ public MimeBodyPart(Body body) throws MessagingException {
+ this(body, null);
+ }
+
+ public MimeBodyPart(Body body, String mimeType) throws MessagingException {
+ if (mimeType != null) {
+ setHeader(MimeHeader.HEADER_CONTENT_TYPE, mimeType);
+ }
+ setBody(body);
+ }
+
+ protected String getFirstHeader(String name) throws MessagingException {
+ return mHeader.getFirstHeader(name);
+ }
+
+ public void addHeader(String name, String value) throws MessagingException {
+ mHeader.addHeader(name, value);
+ }
+
+ public void setHeader(String name, String value) throws MessagingException {
+ mHeader.setHeader(name, value);
+ }
+
+ public String[] getHeader(String name) throws MessagingException {
+ return mHeader.getHeader(name);
+ }
+
+ public void removeHeader(String name) throws MessagingException {
+ mHeader.removeHeader(name);
+ }
+
+ public Body getBody() throws MessagingException {
+ return mBody;
+ }
+
+ public void setBody(Body body) throws MessagingException {
+ this.mBody = body;
+ if (body instanceof com.android.emailcommon.mail.Multipart) {
+ com.android.emailcommon.mail.Multipart multipart =
+ ((com.android.emailcommon.mail.Multipart)body);
+ multipart.setParent(this);
+ setHeader(MimeHeader.HEADER_CONTENT_TYPE, multipart.getContentType());
+ }
+ else if (body instanceof TextBody) {
+ String contentType = String.format("%s;\n charset=utf-8", getMimeType());
+ String name = MimeUtility.getHeaderParameter(getContentType(), "name");
+ if (name != null) {
+ contentType += String.format(";\n name=\"%s\"", name);
+ }
+ setHeader(MimeHeader.HEADER_CONTENT_TYPE, contentType);
+ setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64");
+ }
+ }
+
+ public String getContentType() throws MessagingException {
+ String contentType = getFirstHeader(MimeHeader.HEADER_CONTENT_TYPE);
+ if (contentType == null) {
+ return "text/plain";
+ } else {
+ return contentType;
+ }
+ }
+
+ public String getDisposition() throws MessagingException {
+ String contentDisposition = getFirstHeader(MimeHeader.HEADER_CONTENT_DISPOSITION);
+ if (contentDisposition == null) {
+ return null;
+ } else {
+ return contentDisposition;
+ }
+ }
+
+ public String getContentId() throws MessagingException {
+ String contentId = getFirstHeader(MimeHeader.HEADER_CONTENT_ID);
+ if (contentId == null) {
+ return null;
+ } else {
+ // remove optionally surrounding brackets.
+ return REMOVE_OPTIONAL_BRACKETS.matcher(contentId).replaceAll("$1");
+ }
+ }
+
+ public String getMimeType() throws MessagingException {
+ return MimeUtility.getHeaderParameter(getContentType(), null);
+ }
+
+ public boolean isMimeType(String mimeType) throws MessagingException {
+ return getMimeType().equals(mimeType);
+ }
+
+ public void setSize(int size) {
+ this.mSize = size;
+ }
+
+ public int getSize() throws MessagingException {
+ return mSize;
+ }
+
+ /**
+ * Set extended header
+ *
+ * @param name Extended header name
+ * @param value header value - flattened by removing CR-NL if any
+ * remove header if value is null
+ * @throws MessagingException
+ */
+ public void setExtendedHeader(String name, String value) throws MessagingException {
+ if (value == null) {
+ if (mExtendedHeader != null) {
+ mExtendedHeader.removeHeader(name);
+ }
+ return;
+ }
+ if (mExtendedHeader == null) {
+ mExtendedHeader = new MimeHeader();
+ }
+ mExtendedHeader.setHeader(name, END_OF_LINE.matcher(value).replaceAll(""));
+ }
+
+ /**
+ * Get extended header
+ *
+ * @param name Extended header name
+ * @return header value - null if header does not exist
+ * @throws MessagingException
+ */
+ public String getExtendedHeader(String name) throws MessagingException {
+ if (mExtendedHeader == null) {
+ return null;
+ }
+ return mExtendedHeader.getFirstHeader(name);
+ }
+
+ /**
+ * Write the MimeMessage out in MIME format.
+ */
+ public void writeTo(OutputStream out) throws IOException, MessagingException {
+ BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024);
+ mHeader.writeTo(out);
+ writer.write("\r\n");
+ writer.flush();
+ if (mBody != null) {
+ mBody.writeTo(out);
+ }
+ }
+}
diff --git a/src/com/android/emailcommon/internet/MimeHeader.java b/src/com/android/emailcommon/internet/MimeHeader.java
new file mode 100644
index 000000000..b0ad7772e
--- /dev/null
+++ b/src/com/android/emailcommon/internet/MimeHeader.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.internet;
+
+import com.android.emailcommon.mail.MessagingException;
+import com.android.emailcommon.utility.Utility;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.util.ArrayList;
+
+public class MimeHeader {
+ /**
+ * Application specific header that contains Store specific information about an attachment.
+ * In IMAP this contains the IMAP BODYSTRUCTURE part id so that the ImapStore can later
+ * retrieve the attachment at will from the server.
+ * The info is recorded from this header on LocalStore.appendMessages and is put back
+ * into the MIME data by LocalStore.fetch.
+ */
+ public static final String HEADER_ANDROID_ATTACHMENT_STORE_DATA = "X-Android-Attachment-StoreData";
+ /**
+ * Application specific header that is used to tag body parts for quoted/forwarded messages.
+ */
+ public static final String HEADER_ANDROID_BODY_QUOTED_PART = "X-Android-Body-Quoted-Part";
+
+ public static final String HEADER_CONTENT_TYPE = "Content-Type";
+ public static final String HEADER_CONTENT_TRANSFER_ENCODING = "Content-Transfer-Encoding";
+ public static final String HEADER_CONTENT_DISPOSITION = "Content-Disposition";
+ public static final String HEADER_CONTENT_ID = "Content-ID";
+
+ /**
+ * Fields that should be omitted when writing the header using writeTo()
+ */
+ private static final String[] WRITE_OMIT_FIELDS = {
+// HEADER_ANDROID_ATTACHMENT_DOWNLOADED,
+// HEADER_ANDROID_ATTACHMENT_ID,
+ HEADER_ANDROID_ATTACHMENT_STORE_DATA
+ };
+
+ protected final ArrayList<Field> mFields = new ArrayList<Field>();
+
+ public void clear() {
+ mFields.clear();
+ }
+
+ public String getFirstHeader(String name) throws MessagingException {
+ String[] header = getHeader(name);
+ if (header == null) {
+ return null;
+ }
+ return header[0];
+ }
+
+ public void addHeader(String name, String value) throws MessagingException {
+ mFields.add(new Field(name, value));
+ }
+
+ public void setHeader(String name, String value) throws MessagingException {
+ if (name == null || value == null) {
+ return;
+ }
+ removeHeader(name);
+ addHeader(name, value);
+ }
+
+ public String[] getHeader(String name) throws MessagingException {
+ ArrayList<String> values = new ArrayList<String>();
+ for (Field field : mFields) {
+ if (field.name.equalsIgnoreCase(name)) {
+ values.add(field.value);
+ }
+ }
+ if (values.size() == 0) {
+ return null;
+ }
+ return values.toArray(new String[] {});
+ }
+
+ public void removeHeader(String name) throws MessagingException {
+ ArrayList<Field> removeFields = new ArrayList<Field>();
+ for (Field field : mFields) {
+ if (field.name.equalsIgnoreCase(name)) {
+ removeFields.add(field);
+ }
+ }
+ mFields.removeAll(removeFields);
+ }
+
+ /**
+ * Write header into String
+ *
+ * @return CR-NL separated header string except the headers in writeOmitFields
+ * null if header is empty
+ */
+ public String writeToString() {
+ if (mFields.size() == 0) {
+ return null;
+ }
+ StringBuilder builder = new StringBuilder();
+ for (Field field : mFields) {
+ if (!Utility.arrayContains(WRITE_OMIT_FIELDS, field.name)) {
+ builder.append(field.name + ": " + field.value + "\r\n");
+ }
+ }
+ return builder.toString();
+ }
+
+ public void writeTo(OutputStream out) throws IOException, MessagingException {
+ BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024);
+ for (Field field : mFields) {
+ if (!Utility.arrayContains(WRITE_OMIT_FIELDS, field.name)) {
+ writer.write(field.name + ": " + field.value + "\r\n");
+ }
+ }
+ writer.flush();
+ }
+
+ private static class Field {
+ final String name;
+ final String value;
+
+ public Field(String name, String value) {
+ this.name = name;
+ this.value = value;
+ }
+
+ @Override
+ public String toString() {
+ return name + "=" + value;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return (mFields == null) ? null : mFields.toString();
+ }
+}
diff --git a/src/com/android/emailcommon/internet/MimeMessage.java b/src/com/android/emailcommon/internet/MimeMessage.java
new file mode 100644
index 000000000..412092da4
--- /dev/null
+++ b/src/com/android/emailcommon/internet/MimeMessage.java
@@ -0,0 +1,626 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.internet;
+
+import com.android.emailcommon.mail.Address;
+import com.android.emailcommon.mail.Body;
+import com.android.emailcommon.mail.BodyPart;
+import com.android.emailcommon.mail.Message;
+import com.android.emailcommon.mail.MessagingException;
+import com.android.emailcommon.mail.Multipart;
+import com.android.emailcommon.mail.Part;
+
+import org.apache.james.mime4j.BodyDescriptor;
+import org.apache.james.mime4j.ContentHandler;
+import org.apache.james.mime4j.EOLConvertingInputStream;
+import org.apache.james.mime4j.MimeStreamParser;
+import org.apache.james.mime4j.field.DateTimeField;
+import org.apache.james.mime4j.field.Field;
+
+import android.text.TextUtils;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+import java.util.Stack;
+import java.util.regex.Pattern;
+
+/**
+ * An implementation of Message that stores all of its metadata in RFC 822 and
+ * RFC 2045 style headers.
+ *
+ * NOTE: Automatic generation of a local message-id is becoming unwieldy and should be removed.
+ * It would be better to simply do it explicitly on local creation of new outgoing messages.
+ */
+public class MimeMessage extends Message {
+ private MimeHeader mHeader;
+ private MimeHeader mExtendedHeader;
+
+ // NOTE: The fields here are transcribed out of headers, and values stored here will supercede
+ // the values found in the headers. Use caution to prevent any out-of-phase errors. In
+ // particular, any adds/changes/deletes here must be echoed by changes in the parse() function.
+ private Address[] mFrom;
+ private Address[] mTo;
+ private Address[] mCc;
+ private Address[] mBcc;
+ private Address[] mReplyTo;
+ private Date mSentDate;
+ private Body mBody;
+ protected int mSize;
+ private boolean mInhibitLocalMessageId = false;
+
+ // Shared random source for generating local message-id values
+ private static final java.util.Random sRandom = new java.util.Random();
+
+ // In MIME, en_US-like date format should be used. In other words "MMM" should be encoded to
+ // "Jan", not the other localized format like "Ene" (meaning January in locale es).
+ // This conversion is used when generating outgoing MIME messages. Incoming MIME date
+ // headers are parsed by org.apache.james.mime4j.field.DateTimeField which does not have any
+ // localization code.
+ private static final SimpleDateFormat DATE_FORMAT =
+ new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.US);
+
+ // regex that matches content id surrounded by "<>" optionally.
+ private static final Pattern REMOVE_OPTIONAL_BRACKETS = Pattern.compile("^<?([^>]+)>?$");
+ // regex that matches end of line.
+ private static final Pattern END_OF_LINE = Pattern.compile("\r?\n");
+
+ public MimeMessage() {
+ mHeader = null;
+ }
+
+ /**
+ * Generate a local message id. This is only used when none has been assigned, and is
+ * installed lazily. Any remote (typically server-assigned) message id takes precedence.
+ * @return a long, locally-generated message-ID value
+ */
+ private String generateMessageId() {
+ StringBuffer sb = new StringBuffer();
+ sb.append("<");
+ for (int i = 0; i < 24; i++) {
+ // We'll use a 5-bit range (0..31)
+ int value = sRandom.nextInt() & 31;
+ char c = "0123456789abcdefghijklmnopqrstuv".charAt(value);
+ sb.append(c);
+ }
+ sb.append(".");
+ sb.append(Long.toString(System.currentTimeMillis()));
+ sb.append("@email.android.com>");
+ return sb.toString();
+ }
+
+ /**
+ * Parse the given InputStream using Apache Mime4J to build a MimeMessage.
+ *
+ * @param in
+ * @throws IOException
+ * @throws MessagingException
+ */
+ public MimeMessage(InputStream in) throws IOException, MessagingException {
+ parse(in);
+ }
+
+ protected void parse(InputStream in) throws IOException, MessagingException {
+ // Before parsing the input stream, clear all local fields that may be superceded by
+ // the new incoming message.
+ getMimeHeaders().clear();
+ mInhibitLocalMessageId = true;
+ mFrom = null;
+ mTo = null;
+ mCc = null;
+ mBcc = null;
+ mReplyTo = null;
+ mSentDate = null;
+ mBody = null;
+
+ MimeStreamParser parser = new MimeStreamParser();
+ parser.setContentHandler(new MimeMessageBuilder());
+ parser.parse(new EOLConvertingInputStream(in));
+ }
+
+ /**
+ * Return the internal mHeader value, with very lazy initialization.
+ * The goal is to save memory by not creating the headers until needed.
+ */
+ private MimeHeader getMimeHeaders() {
+ if (mHeader == null) {
+ mHeader = new MimeHeader();
+ }
+ return mHeader;
+ }
+
+ @Override
+ public Date getReceivedDate() throws MessagingException {
+ return null;
+ }
+
+ @Override
+ public Date getSentDate() throws MessagingException {
+ if (mSentDate == null) {
+ try {
+ DateTimeField field = (DateTimeField)Field.parse("Date: "
+ + MimeUtility.unfoldAndDecode(getFirstHeader("Date")));
+ mSentDate = field.getDate();
+ } catch (Exception e) {
+
+ }
+ }
+ return mSentDate;
+ }
+
+ @Override
+ public void setSentDate(Date sentDate) throws MessagingException {
+ setHeader("Date", DATE_FORMAT.format(sentDate));
+ this.mSentDate = sentDate;
+ }
+
+ @Override
+ public String getContentType() throws MessagingException {
+ String contentType = getFirstHeader(MimeHeader.HEADER_CONTENT_TYPE);
+ if (contentType == null) {
+ return "text/plain";
+ } else {
+ return contentType;
+ }
+ }
+
+ public String getDisposition() throws MessagingException {
+ String contentDisposition = getFirstHeader(MimeHeader.HEADER_CONTENT_DISPOSITION);
+ if (contentDisposition == null) {
+ return null;
+ } else {
+ return contentDisposition;
+ }
+ }
+
+ public String getContentId() throws MessagingException {
+ String contentId = getFirstHeader(MimeHeader.HEADER_CONTENT_ID);
+ if (contentId == null) {
+ return null;
+ } else {
+ // remove optionally surrounding brackets.
+ return REMOVE_OPTIONAL_BRACKETS.matcher(contentId).replaceAll("$1");
+ }
+ }
+
+ public String getMimeType() throws MessagingException {
+ return MimeUtility.getHeaderParameter(getContentType(), null);
+ }
+
+ public int getSize() throws MessagingException {
+ return mSize;
+ }
+
+ /**
+ * Returns a list of the given recipient type from this message. If no addresses are
+ * found the method returns an empty array.
+ */
+ @Override
+ public Address[] getRecipients(RecipientType type) throws MessagingException {
+ if (type == RecipientType.TO) {
+ if (mTo == null) {
+ mTo = Address.parse(MimeUtility.unfold(getFirstHeader("To")));
+ }
+ return mTo;
+ } else if (type == RecipientType.CC) {
+ if (mCc == null) {
+ mCc = Address.parse(MimeUtility.unfold(getFirstHeader("CC")));
+ }
+ return mCc;
+ } else if (type == RecipientType.BCC) {
+ if (mBcc == null) {
+ mBcc = Address.parse(MimeUtility.unfold(getFirstHeader("BCC")));
+ }
+ return mBcc;
+ } else {
+ throw new MessagingException("Unrecognized recipient type.");
+ }
+ }
+
+ @Override
+ public void setRecipients(RecipientType type, Address[] addresses) throws MessagingException {
+ final int TO_LENGTH = 4; // "To: "
+ final int CC_LENGTH = 4; // "Cc: "
+ final int BCC_LENGTH = 5; // "Bcc: "
+ if (type == RecipientType.TO) {
+ if (addresses == null || addresses.length == 0) {
+ removeHeader("To");
+ this.mTo = null;
+ } else {
+ setHeader("To", MimeUtility.fold(Address.toHeader(addresses), TO_LENGTH));
+ this.mTo = addresses;
+ }
+ } else if (type == RecipientType.CC) {
+ if (addresses == null || addresses.length == 0) {
+ removeHeader("CC");
+ this.mCc = null;
+ } else {
+ setHeader("CC", MimeUtility.fold(Address.toHeader(addresses), CC_LENGTH));
+ this.mCc = addresses;
+ }
+ } else if (type == RecipientType.BCC) {
+ if (addresses == null || addresses.length == 0) {
+ removeHeader("BCC");
+ this.mBcc = null;
+ } else {
+ setHeader("BCC", MimeUtility.fold(Address.toHeader(addresses), BCC_LENGTH));
+ this.mBcc = addresses;
+ }
+ } else {
+ throw new MessagingException("Unrecognized recipient type.");
+ }
+ }
+
+ /**
+ * Returns the unfolded, decoded value of the Subject header.
+ */
+ @Override
+ public String getSubject() throws MessagingException {
+ return MimeUtility.unfoldAndDecode(getFirstHeader("Subject"));
+ }
+
+ @Override
+ public void setSubject(String subject) throws MessagingException {
+ final int HEADER_NAME_LENGTH = 9; // "Subject: "
+ setHeader("Subject", MimeUtility.foldAndEncode2(subject, HEADER_NAME_LENGTH));
+ }
+
+ @Override
+ public Address[] getFrom() throws MessagingException {
+ if (mFrom == null) {
+ String list = MimeUtility.unfold(getFirstHeader("From"));
+ if (list == null || list.length() == 0) {
+ list = MimeUtility.unfold(getFirstHeader("Sender"));
+ }
+ mFrom = Address.parse(list);
+ }
+ return mFrom;
+ }
+
+ @Override
+ public void setFrom(Address from) throws MessagingException {
+ final int FROM_LENGTH = 6; // "From: "
+ if (from != null) {
+ setHeader("From", MimeUtility.fold(from.toHeader(), FROM_LENGTH));
+ this.mFrom = new Address[] {
+ from
+ };
+ } else {
+ this.mFrom = null;
+ }
+ }
+
+ @Override
+ public Address[] getReplyTo() throws MessagingException {
+ if (mReplyTo == null) {
+ mReplyTo = Address.parse(MimeUtility.unfold(getFirstHeader("Reply-to")));
+ }
+ return mReplyTo;
+ }
+
+ @Override
+ public void setReplyTo(Address[] replyTo) throws MessagingException {
+ final int REPLY_TO_LENGTH = 10; // "Reply-to: "
+ if (replyTo == null || replyTo.length == 0) {
+ removeHeader("Reply-to");
+ mReplyTo = null;
+ } else {
+ setHeader("Reply-to", MimeUtility.fold(Address.toHeader(replyTo), REPLY_TO_LENGTH));
+ mReplyTo = replyTo;
+ }
+ }
+
+ /**
+ * Set the mime "Message-ID" header
+ * @param messageId the new Message-ID value
+ * @throws MessagingException
+ */
+ @Override
+ public void setMessageId(String messageId) throws MessagingException {
+ setHeader("Message-ID", messageId);
+ }
+
+ /**
+ * Get the mime "Message-ID" header. This value will be preloaded with a locally-generated
+ * random ID, if the value has not previously been set. Local generation can be inhibited/
+ * overridden by explicitly clearing the headers, removing the message-id header, etc.
+ * @return the Message-ID header string, or null if explicitly has been set to null
+ */
+ @Override
+ public String getMessageId() throws MessagingException {
+ String messageId = getFirstHeader("Message-ID");
+ if (messageId == null && !mInhibitLocalMessageId) {
+ messageId = generateMessageId();
+ setMessageId(messageId);
+ }
+ return messageId;
+ }
+
+ @Override
+ public void saveChanges() throws MessagingException {
+ throw new MessagingException("saveChanges not yet implemented");
+ }
+
+ @Override
+ public Body getBody() throws MessagingException {
+ return mBody;
+ }
+
+ @Override
+ public void setBody(Body body) throws MessagingException {
+ this.mBody = body;
+ if (body instanceof Multipart) {
+ Multipart multipart = ((Multipart)body);
+ multipart.setParent(this);
+ setHeader(MimeHeader.HEADER_CONTENT_TYPE, multipart.getContentType());
+ setHeader("MIME-Version", "1.0");
+ }
+ else if (body instanceof TextBody) {
+ setHeader(MimeHeader.HEADER_CONTENT_TYPE, String.format("%s;\n charset=utf-8",
+ getMimeType()));
+ setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64");
+ }
+ }
+
+ protected String getFirstHeader(String name) throws MessagingException {
+ return getMimeHeaders().getFirstHeader(name);
+ }
+
+ @Override
+ public void addHeader(String name, String value) throws MessagingException {
+ getMimeHeaders().addHeader(name, value);
+ }
+
+ @Override
+ public void setHeader(String name, String value) throws MessagingException {
+ getMimeHeaders().setHeader(name, value);
+ }
+
+ @Override
+ public String[] getHeader(String name) throws MessagingException {
+ return getMimeHeaders().getHeader(name);
+ }
+
+ @Override
+ public void removeHeader(String name) throws MessagingException {
+ getMimeHeaders().removeHeader(name);
+ if ("Message-ID".equalsIgnoreCase(name)) {
+ mInhibitLocalMessageId = true;
+ }
+ }
+
+ /**
+ * Set extended header
+ *
+ * @param name Extended header name
+ * @param value header value - flattened by removing CR-NL if any
+ * remove header if value is null
+ * @throws MessagingException
+ */
+ public void setExtendedHeader(String name, String value) throws MessagingException {
+ if (value == null) {
+ if (mExtendedHeader != null) {
+ mExtendedHeader.removeHeader(name);
+ }
+ return;
+ }
+ if (mExtendedHeader == null) {
+ mExtendedHeader = new MimeHeader();
+ }
+ mExtendedHeader.setHeader(name, END_OF_LINE.matcher(value).replaceAll(""));
+ }
+
+ /**
+ * Get extended header
+ *
+ * @param name Extended header name
+ * @return header value - null if header does not exist
+ * @throws MessagingException
+ */
+ public String getExtendedHeader(String name) throws MessagingException {
+ if (mExtendedHeader == null) {
+ return null;
+ }
+ return mExtendedHeader.getFirstHeader(name);
+ }
+
+ /**
+ * Set entire extended headers from String
+ *
+ * @param headers Extended header and its value - "CR-NL-separated pairs
+ * if null or empty, remove entire extended headers
+ * @throws MessagingException
+ */
+ public void setExtendedHeaders(String headers) throws MessagingException {
+ if (TextUtils.isEmpty(headers)) {
+ mExtendedHeader = null;
+ } else {
+ mExtendedHeader = new MimeHeader();
+ for (String header : END_OF_LINE.split(headers)) {
+ String[] tokens = header.split(":", 2);
+ if (tokens.length != 2) {
+ throw new MessagingException("Illegal extended headers: " + headers);
+ }
+ mExtendedHeader.setHeader(tokens[0].trim(), tokens[1].trim());
+ }
+ }
+ }
+
+ /**
+ * Get entire extended headers as String
+ *
+ * @return "CR-NL-separated extended headers - null if extended header does not exist
+ */
+ public String getExtendedHeaders() {
+ if (mExtendedHeader != null) {
+ return mExtendedHeader.writeToString();
+ }
+ return null;
+ }
+
+ /**
+ * Write message header and body to output stream
+ *
+ * @param out Output steam to write message header and body.
+ */
+ public void writeTo(OutputStream out) throws IOException, MessagingException {
+ BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024);
+ // Force creation of local message-id
+ getMessageId();
+ getMimeHeaders().writeTo(out);
+ // mExtendedHeader will not be write out to external output stream,
+ // because it is intended to internal use.
+ writer.write("\r\n");
+ writer.flush();
+ if (mBody != null) {
+ mBody.writeTo(out);
+ }
+ }
+
+ public InputStream getInputStream() throws MessagingException {
+ return null;
+ }
+
+ class MimeMessageBuilder implements ContentHandler {
+ private Stack<Object> stack = new Stack<Object>();
+
+ public MimeMessageBuilder() {
+ }
+
+ private void expect(Class c) {
+ if (!c.isInstance(stack.peek())) {
+ throw new IllegalStateException("Internal stack error: " + "Expected '"
+ + c.getName() + "' found '" + stack.peek().getClass().getName() + "'");
+ }
+ }
+
+ public void startMessage() {
+ if (stack.isEmpty()) {
+ stack.push(MimeMessage.this);
+ } else {
+ expect(Part.class);
+ try {
+ MimeMessage m = new MimeMessage();
+ ((Part)stack.peek()).setBody(m);
+ stack.push(m);
+ } catch (MessagingException me) {
+ throw new Error(me);
+ }
+ }
+ }
+
+ public void endMessage() {
+ expect(MimeMessage.class);
+ stack.pop();
+ }
+
+ public void startHeader() {
+ expect(Part.class);
+ }
+
+ public void field(String fieldData) {
+ expect(Part.class);
+ try {
+ String[] tokens = fieldData.split(":", 2);
+ ((Part)stack.peek()).addHeader(tokens[0], tokens[1].trim());
+ } catch (MessagingException me) {
+ throw new Error(me);
+ }
+ }
+
+ public void endHeader() {
+ expect(Part.class);
+ }
+
+ public void startMultipart(BodyDescriptor bd) {
+ expect(Part.class);
+
+ Part e = (Part)stack.peek();
+ try {
+ MimeMultipart multiPart = new MimeMultipart(e.getContentType());
+ e.setBody(multiPart);
+ stack.push(multiPart);
+ } catch (MessagingException me) {
+ throw new Error(me);
+ }
+ }
+
+ public void body(BodyDescriptor bd, InputStream in) throws IOException {
+ expect(Part.class);
+ Body body = MimeUtility.decodeBody(in, bd.getTransferEncoding());
+ try {
+ ((Part)stack.peek()).setBody(body);
+ } catch (MessagingException me) {
+ throw new Error(me);
+ }
+ }
+
+ public void endMultipart() {
+ stack.pop();
+ }
+
+ public void startBodyPart() {
+ expect(MimeMultipart.class);
+
+ try {
+ MimeBodyPart bodyPart = new MimeBodyPart();
+ ((MimeMultipart)stack.peek()).addBodyPart(bodyPart);
+ stack.push(bodyPart);
+ } catch (MessagingException me) {
+ throw new Error(me);
+ }
+ }
+
+ public void endBodyPart() {
+ expect(BodyPart.class);
+ stack.pop();
+ }
+
+ public void epilogue(InputStream is) throws IOException {
+ expect(MimeMultipart.class);
+ StringBuffer sb = new StringBuffer();
+ int b;
+ while ((b = is.read()) != -1) {
+ sb.append((char)b);
+ }
+ // ((Multipart) stack.peek()).setEpilogue(sb.toString());
+ }
+
+ public void preamble(InputStream is) throws IOException {
+ expect(MimeMultipart.class);
+ StringBuffer sb = new StringBuffer();
+ int b;
+ while ((b = is.read()) != -1) {
+ sb.append((char)b);
+ }
+ try {
+ ((MimeMultipart)stack.peek()).setPreamble(sb.toString());
+ } catch (MessagingException me) {
+ throw new Error(me);
+ }
+ }
+
+ public void raw(InputStream is) throws IOException {
+ throw new UnsupportedOperationException("Not supported");
+ }
+ }
+}
diff --git a/src/com/android/emailcommon/internet/MimeMultipart.java b/src/com/android/emailcommon/internet/MimeMultipart.java
new file mode 100644
index 000000000..e6977ee4f
--- /dev/null
+++ b/src/com/android/emailcommon/internet/MimeMultipart.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.internet;
+
+import com.android.emailcommon.mail.BodyPart;
+import com.android.emailcommon.mail.MessagingException;
+import com.android.emailcommon.mail.Multipart;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+
+public class MimeMultipart extends Multipart {
+ protected String mPreamble;
+
+ protected String mContentType;
+
+ protected String mBoundary;
+
+ protected String mSubType;
+
+ public MimeMultipart() throws MessagingException {
+ mBoundary = generateBoundary();
+ setSubType("mixed");
+ }
+
+ public MimeMultipart(String contentType) throws MessagingException {
+ this.mContentType = contentType;
+ try {
+ mSubType = MimeUtility.getHeaderParameter(contentType, null).split("/")[1];
+ mBoundary = MimeUtility.getHeaderParameter(contentType, "boundary");
+ if (mBoundary == null) {
+ throw new MessagingException("MultiPart does not contain boundary: " + contentType);
+ }
+ } catch (Exception e) {
+ throw new MessagingException(
+ "Invalid MultiPart Content-Type; must contain subtype and boundary. ("
+ + contentType + ")", e);
+ }
+ }
+
+ public String generateBoundary() {
+ StringBuffer sb = new StringBuffer();
+ sb.append("----");
+ for (int i = 0; i < 30; i++) {
+ sb.append(Integer.toString((int)(Math.random() * 35), 36));
+ }
+ return sb.toString().toUpperCase();
+ }
+
+ public String getPreamble() throws MessagingException {
+ return mPreamble;
+ }
+
+ public void setPreamble(String preamble) throws MessagingException {
+ this.mPreamble = preamble;
+ }
+
+ @Override
+ public String getContentType() throws MessagingException {
+ return mContentType;
+ }
+
+ public void setSubType(String subType) throws MessagingException {
+ this.mSubType = subType;
+ mContentType = String.format("multipart/%s; boundary=\"%s\"", subType, mBoundary);
+ }
+
+ public void writeTo(OutputStream out) throws IOException, MessagingException {
+ BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024);
+
+ if (mPreamble != null) {
+ writer.write(mPreamble + "\r\n");
+ }
+
+ for (int i = 0, count = mParts.size(); i < count; i++) {
+ BodyPart bodyPart = mParts.get(i);
+ writer.write("--" + mBoundary + "\r\n");
+ writer.flush();
+ bodyPart.writeTo(out);
+ writer.write("\r\n");
+ }
+
+ writer.write("--" + mBoundary + "--\r\n");
+ writer.flush();
+ }
+
+ public InputStream getInputStream() throws MessagingException {
+ return null;
+ }
+
+ public String getSubTypeForTest() {
+ return mSubType;
+ }
+}
diff --git a/src/com/android/emailcommon/internet/MimeUtility.java b/src/com/android/emailcommon/internet/MimeUtility.java
new file mode 100644
index 000000000..69330d76b
--- /dev/null
+++ b/src/com/android/emailcommon/internet/MimeUtility.java
@@ -0,0 +1,452 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.internet;
+
+import com.android.emailcommon.Logging;
+import com.android.emailcommon.mail.Body;
+import com.android.emailcommon.mail.BodyPart;
+import com.android.emailcommon.mail.Message;
+import com.android.emailcommon.mail.MessagingException;
+import com.android.emailcommon.mail.Multipart;
+import com.android.emailcommon.mail.Part;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.james.mime4j.codec.EncoderUtil;
+import org.apache.james.mime4j.decoder.DecoderUtil;
+import org.apache.james.mime4j.decoder.QuotedPrintableInputStream;
+import org.apache.james.mime4j.util.CharsetUtil;
+
+import android.util.Base64;
+import android.util.Base64DataException;
+import android.util.Base64InputStream;
+import android.util.Log;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class MimeUtility {
+
+ public static final String MIME_TYPE_RFC822 = "message/rfc822";
+ private final static Pattern PATTERN_CR_OR_LF = Pattern.compile("\r|\n");
+
+ /**
+ * Replace sequences of CRLF+WSP with WSP. Tries to preserve original string
+ * object whenever possible.
+ */
+ public static String unfold(String s) {
+ if (s == null) {
+ return null;
+ }
+ Matcher patternMatcher = PATTERN_CR_OR_LF.matcher(s);
+ if (patternMatcher.find()) {
+ patternMatcher.reset();
+ s = patternMatcher.replaceAll("");
+ }
+ return s;
+ }
+
+ public static String decode(String s) {
+ if (s == null) {
+ return null;
+ }
+ return DecoderUtil.decodeEncodedWords(s);
+ }
+
+ public static String unfoldAndDecode(String s) {
+ return decode(unfold(s));
+ }
+
+ // TODO implement proper foldAndEncode
+ // NOTE: When this really works, we *must* remove all calls to foldAndEncode2() to prevent
+ // duplication of encoding.
+ public static String foldAndEncode(String s) {
+ return s;
+ }
+
+ /**
+ * INTERIM version of foldAndEncode that will be used only by Subject: headers.
+ * This is safer than implementing foldAndEncode() (see above) and risking unknown damage
+ * to other headers.
+ *
+ * TODO: Copy this code to foldAndEncode(), get rid of this function, confirm all working OK.
+ *
+ * @param s original string to encode and fold
+ * @param usedCharacters number of characters already used up by header name
+
+ * @return the String ready to be transmitted
+ */
+ public static String foldAndEncode2(String s, int usedCharacters) {
+ // james.mime4j.codec.EncoderUtil.java
+ // encode: encodeIfNecessary(text, usage, numUsedInHeaderName)
+ // Usage.TEXT_TOKENlooks like the right thing for subjects
+ // use WORD_ENTITY for address/names
+
+ String encoded = EncoderUtil.encodeIfNecessary(s, EncoderUtil.Usage.TEXT_TOKEN,
+ usedCharacters);
+
+ return fold(encoded, usedCharacters);
+ }
+
+ /**
+ * INTERIM: From newer version of org.apache.james (but we don't want to import
+ * the entire MimeUtil class).
+ *
+ * Splits the specified string into a multiple-line representation with
+ * lines no longer than 76 characters (because the line might contain
+ * encoded words; see <a href='http://www.faqs.org/rfcs/rfc2047.html'>RFC
+ * 2047</a> section 2). If the string contains non-whitespace sequences
+ * longer than 76 characters a line break is inserted at the whitespace
+ * character following the sequence resulting in a line longer than 76
+ * characters.
+ *
+ * @param s
+ * string to split.
+ * @param usedCharacters
+ * number of characters already used up. Usually the number of
+ * characters for header field name plus colon and one space.
+ * @return a multiple-line representation of the given string.
+ */
+ public static String fold(String s, int usedCharacters) {
+ final int maxCharacters = 76;
+
+ final int length = s.length();
+ if (usedCharacters + length <= maxCharacters)
+ return s;
+
+ StringBuilder sb = new StringBuilder();
+
+ int lastLineBreak = -usedCharacters;
+ int wspIdx = indexOfWsp(s, 0);
+ while (true) {
+ if (wspIdx == length) {
+ sb.append(s.substring(Math.max(0, lastLineBreak)));
+ return sb.toString();
+ }
+
+ int nextWspIdx = indexOfWsp(s, wspIdx + 1);
+
+ if (nextWspIdx - lastLineBreak > maxCharacters) {
+ sb.append(s.substring(Math.max(0, lastLineBreak), wspIdx));
+ sb.append("\r\n");
+ lastLineBreak = wspIdx;
+ }
+
+ wspIdx = nextWspIdx;
+ }
+ }
+
+ /**
+ * INTERIM: From newer version of org.apache.james (but we don't want to import
+ * the entire MimeUtil class).
+ *
+ * Search for whitespace.
+ */
+ private static int indexOfWsp(String s, int fromIndex) {
+ final int len = s.length();
+ for (int index = fromIndex; index < len; index++) {
+ char c = s.charAt(index);
+ if (c == ' ' || c == '\t')
+ return index;
+ }
+ return len;
+ }
+
+ /**
+ * Returns the named parameter of a header field. If name is null the first
+ * parameter is returned, or if there are no additional parameters in the
+ * field the entire field is returned. Otherwise the named parameter is
+ * searched for in a case insensitive fashion and returned. If the parameter
+ * cannot be found the method returns null.
+ *
+ * TODO: quite inefficient with the inner trimming & splitting.
+ * TODO: Also has a latent bug: uses "startsWith" to match the name, which can false-positive.
+ * TODO: The doc says that for a null name you get the first param, but you get the header.
+ * Should probably just fix the doc, but if other code assumes that behavior, fix the code.
+ * TODO: Need to decode %-escaped strings, as in: filename="ab%22d".
+ * ('+' -> ' ' conversion too? check RFC)
+ *
+ * @param header
+ * @param name
+ * @return the entire header (if name=null), the found parameter, or null
+ */
+ public static String getHeaderParameter(String header, String name) {
+ if (header == null) {
+ return null;
+ }
+ String[] parts = unfold(header).split(";");
+ if (name == null) {
+ return parts[0].trim();
+ }
+ String lowerCaseName = name.toLowerCase();
+ for (String part : parts) {
+ if (part.trim().toLowerCase().startsWith(lowerCaseName)) {
+ String[] parameterParts = part.split("=", 2);
+ if (parameterParts.length < 2) {
+ return null;
+ }
+ String parameter = parameterParts[1].trim();
+ if (parameter.startsWith("\"") && parameter.endsWith("\"")) {
+ return parameter.substring(1, parameter.length() - 1);
+ } else {
+ return parameter;
+ }
+ }
+ }
+ return null;
+ }
+
+ public static Part findFirstPartByMimeType(Part part, String mimeType)
+ throws MessagingException {
+ if (part.getBody() instanceof Multipart) {
+ Multipart multipart = (Multipart)part.getBody();
+ for (int i = 0, count = multipart.getCount(); i < count; i++) {
+ BodyPart bodyPart = multipart.getBodyPart(i);
+ Part ret = findFirstPartByMimeType(bodyPart, mimeType);
+ if (ret != null) {
+ return ret;
+ }
+ }
+ }
+ else if (part.getMimeType().equalsIgnoreCase(mimeType)) {
+ return part;
+ }
+ return null;
+ }
+
+ public static Part findPartByContentId(Part part, String contentId) throws Exception {
+ if (part.getBody() instanceof Multipart) {
+ Multipart multipart = (Multipart)part.getBody();
+ for (int i = 0, count = multipart.getCount(); i < count; i++) {
+ BodyPart bodyPart = multipart.getBodyPart(i);
+ Part ret = findPartByContentId(bodyPart, contentId);
+ if (ret != null) {
+ return ret;
+ }
+ }
+ }
+ String cid = part.getContentId();
+ if (contentId.equals(cid)) {
+ return part;
+ }
+ return null;
+ }
+
+ /**
+ * Reads the Part's body and returns a String based on any charset conversion that needed
+ * to be done.
+ * @param part The part containing a body
+ * @return a String containing the converted text in the body, or null if there was no text
+ * or an error during conversion.
+ */
+ public static String getTextFromPart(Part part) {
+ try {
+ if (part != null && part.getBody() != null) {
+ InputStream in = part.getBody().getInputStream();
+ String mimeType = part.getMimeType();
+ if (mimeType != null && MimeUtility.mimeTypeMatches(mimeType, "text/*")) {
+ /*
+ * Now we read the part into a buffer for further processing. Because
+ * the stream is now wrapped we'll remove any transfer encoding at this point.
+ */
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ IOUtils.copy(in, out);
+ in.close();
+ in = null; // we want all of our memory back, and close might not release
+
+ /*
+ * We've got a text part, so let's see if it needs to be processed further.
+ */
+ String charset = getHeaderParameter(part.getContentType(), "charset");
+ if (charset != null) {
+ /*
+ * See if there is conversion from the MIME charset to the Java one.
+ */
+ charset = CharsetUtil.toJavaCharset(charset);
+ }
+ /*
+ * No encoding, so use us-ascii, which is the standard.
+ */
+ if (charset == null) {
+ charset = "ASCII";
+ }
+ /*
+ * Convert and return as new String
+ */
+ String result = out.toString(charset);
+ out.close();
+ return result;
+ }
+ }
+
+ }
+ catch (OutOfMemoryError oom) {
+ /*
+ * If we are not able to process the body there's nothing we can do about it. Return
+ * null and let the upper layers handle the missing content.
+ */
+ Log.e(Logging.LOG_TAG, "Unable to getTextFromPart " + oom.toString());
+ }
+ catch (Exception e) {
+ /*
+ * If we are not able to process the body there's nothing we can do about it. Return
+ * null and let the upper layers handle the missing content.
+ */
+ Log.e(Logging.LOG_TAG, "Unable to getTextFromPart " + e.toString());
+ }
+ return null;
+ }
+
+ /**
+ * Returns true if the given mimeType matches the matchAgainst specification. The comparison
+ * ignores case and the matchAgainst string may include "*" for a wildcard (e.g. "image/*").
+ *
+ * @param mimeType A MIME type to check.
+ * @param matchAgainst A MIME type to check against. May include wildcards.
+ * @return true if the mimeType matches
+ */
+ public static boolean mimeTypeMatches(String mimeType, String matchAgainst) {
+ Pattern p = Pattern.compile(matchAgainst.replaceAll("\\*", "\\.\\*"),
+ Pattern.CASE_INSENSITIVE);
+ return p.matcher(mimeType).matches();
+ }
+
+ /**
+ * Returns true if the given mimeType matches any of the matchAgainst specifications. The
+ * comparison ignores case and the matchAgainst strings may include "*" for a wildcard
+ * (e.g. "image/*").
+ *
+ * @param mimeType A MIME type to check.
+ * @param matchAgainst An array of MIME types to check against. May include wildcards.
+ * @return true if the mimeType matches any of the matchAgainst strings
+ */
+ public static boolean mimeTypeMatches(String mimeType, String[] matchAgainst) {
+ for (String matchType : matchAgainst) {
+ if (mimeTypeMatches(mimeType, matchType)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Given an input stream and a transfer encoding, return a wrapped input stream for that
+ * encoding (or the original if none is required)
+ * @param in the input stream
+ * @param contentTransferEncoding the content transfer encoding
+ * @return a properly wrapped stream
+ */
+ public static InputStream getInputStreamForContentTransferEncoding(InputStream in,
+ String contentTransferEncoding) {
+ if (contentTransferEncoding != null) {
+ contentTransferEncoding =
+ MimeUtility.getHeaderParameter(contentTransferEncoding, null);
+ if ("quoted-printable".equalsIgnoreCase(contentTransferEncoding)) {
+ in = new QuotedPrintableInputStream(in);
+ }
+ else if ("base64".equalsIgnoreCase(contentTransferEncoding)) {
+ in = new Base64InputStream(in, Base64.DEFAULT);
+ }
+ }
+ return in;
+ }
+
+ /**
+ * Removes any content transfer encoding from the stream and returns a Body.
+ */
+ public static Body decodeBody(InputStream in, String contentTransferEncoding)
+ throws IOException {
+ /*
+ * We'll remove any transfer encoding by wrapping the stream.
+ */
+ in = getInputStreamForContentTransferEncoding(in, contentTransferEncoding);
+ BinaryTempFileBody tempBody = new BinaryTempFileBody();
+ OutputStream out = tempBody.getOutputStream();
+ try {
+ IOUtils.copy(in, out);
+ } catch (Base64DataException bde) {
+ // STOPSHIP Need to fix this somehow
+ //String warning = "\n\n" + Email.getMessageDecodeErrorString();
+ //out.write(warning.getBytes());
+ } finally {
+ out.close();
+ }
+ return tempBody;
+ }
+
+ /**
+ * Recursively scan a Part (usually a Message) and sort out which of its children will be
+ * "viewable" and which will be attachments.
+ *
+ * @param part The part to be broken down
+ * @param viewables This arraylist will be populated with all parts that appear to be
+ * the "message" (e.g. text/plain & text/html)
+ * @param attachments This arraylist will be populated with all parts that appear to be
+ * attachments (including inlines)
+ * @throws MessagingException
+ */
+ public static void collectParts(Part part, ArrayList<Part> viewables,
+ ArrayList<Part> attachments) throws MessagingException {
+ String disposition = part.getDisposition();
+ String dispositionType = null;
+ String dispositionFilename = null;
+ if (disposition != null) {
+ dispositionType = MimeUtility.getHeaderParameter(disposition, null);
+ dispositionFilename = MimeUtility.getHeaderParameter(disposition, "filename");
+ }
+ boolean attachmentDisposition = "attachment".equalsIgnoreCase(dispositionType);
+ boolean inlineDisposition = "inline".equalsIgnoreCase(dispositionType);
+
+ // A guess that this part is intended to be an attachment
+ boolean attachment = attachmentDisposition
+ || (dispositionFilename != null && !inlineDisposition);
+
+ // A guess that this part is intended to be an inline.
+ boolean inline = inlineDisposition && (dispositionFilename != null);
+
+ // One or the other
+ boolean attachmentOrInline = attachment || inline;
+
+ if (part.getBody() instanceof Multipart) {
+ // If the part is Multipart but not alternative it's either mixed or
+ // something we don't know about, which means we treat it as mixed
+ // per the spec. We just process its pieces recursively.
+ Multipart mp = (Multipart)part.getBody();
+ for (int i = 0; i < mp.getCount(); i++) {
+ collectParts(mp.getBodyPart(i), viewables, attachments);
+ }
+ } else if (part.getBody() instanceof Message) {
+ // If the part is an embedded message we just continue to process
+ // it, pulling any viewables or attachments into the running list.
+ Message message = (Message)part.getBody();
+ collectParts(message, viewables, attachments);
+ } else if ((!attachmentOrInline) && ("text/html".equalsIgnoreCase(part.getMimeType()))) {
+ // If the part is HTML and we got this far, it's a viewable part of a mixed
+ viewables.add(part);
+ } else if ((!attachmentOrInline) && ("text/plain".equalsIgnoreCase(part.getMimeType()))) {
+ // If the part is text and we got this far, it's a viewable part of a mixed
+ viewables.add(part);
+ } else if (attachmentOrInline) {
+ // Finally, if it's an attachment or an inline we will include it as an attachment.
+ attachments.add(part);
+ }
+ }
+}
diff --git a/src/com/android/emailcommon/internet/Rfc822Output.java b/src/com/android/emailcommon/internet/Rfc822Output.java
new file mode 100644
index 000000000..7382d30b2
--- /dev/null
+++ b/src/com/android/emailcommon/internet/Rfc822Output.java
@@ -0,0 +1,357 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.internet;
+
+import com.android.emailcommon.mail.Address;
+import com.android.emailcommon.mail.MessagingException;
+import com.android.emailcommon.provider.EmailContent.Attachment;
+import com.android.emailcommon.provider.EmailContent.Body;
+import com.android.emailcommon.provider.EmailContent.Message;
+
+import org.apache.commons.io.IOUtils;
+
+import android.content.ContentUris;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.util.Base64;
+import android.util.Base64OutputStream;
+
+import java.io.BufferedOutputStream;
+import java.io.ByteArrayInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Utility class to output RFC 822 messages from provider email messages
+ */
+public class Rfc822Output {
+
+ private static final Pattern PATTERN_START_OF_LINE = Pattern.compile("(?m)^");
+ private static final Pattern PATTERN_ENDLINE_CRLF = Pattern.compile("\r\n");
+
+ // In MIME, en_US-like date format should be used. In other words "MMM" should be encoded to
+ // "Jan", not the other localized format like "Ene" (meaning January in locale es).
+ private static final SimpleDateFormat DATE_FORMAT =
+ new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.US);
+
+ private static final String WHERE_NOT_SMART_FORWARD = "(" + Attachment.FLAGS + "&" +
+ Attachment.FLAG_SMART_FORWARD + ")=0";
+
+ /*package*/ static String buildBodyText(Context context, Message message,
+ boolean useSmartReply) {
+ Body body = Body.restoreBodyWithMessageId(context, message.mId);
+ if (body == null) {
+ return null;
+ }
+
+ String text = body.mTextContent;
+ int flags = message.mFlags;
+ boolean isReply = (flags & Message.FLAG_TYPE_REPLY) != 0;
+ boolean isForward = (flags & Message.FLAG_TYPE_FORWARD) != 0;
+ // For all forwards/replies, we add the intro text
+ if (isReply || isForward) {
+ String intro = body.mIntroText == null ? "" : body.mIntroText;
+ text += intro;
+ }
+ if (useSmartReply) {
+ // useSmartReply is set to true for use by SmartReply/SmartForward in EAS.
+ // SmartForward doesn't put a break between the original and new text, so we add an LF
+ if (isForward) {
+ text += "\n";
+ }
+ return text;
+ }
+
+ String quotedText = body.mTextReply;
+ if (quotedText != null) {
+ // fix CR-LF line endings to LF-only needed by EditText.
+ Matcher matcher = PATTERN_ENDLINE_CRLF.matcher(quotedText);
+ quotedText = matcher.replaceAll("\n");
+ }
+ if (isReply) {
+ if (quotedText != null) {
+ Matcher matcher = PATTERN_START_OF_LINE.matcher(quotedText);
+ text += matcher.replaceAll(">");
+ }
+ } else if (isForward) {
+ if (quotedText != null) {
+ text += quotedText;
+ }
+ }
+ return text;
+ }
+
+ /**
+ * Write the entire message to an output stream. This method provides buffering, so it is
+ * not necessary to pass in a buffered output stream here.
+ *
+ * @param context system context for accessing the provider
+ * @param messageId the message to write out
+ * @param out the output stream to write the message to
+ * @param useSmartReply whether or not quoted text is appended to a reply/forward
+ *
+ * TODO alternative parts (e.g. text+html) are not supported here.
+ */
+ public static void writeTo(Context context, long messageId, OutputStream out,
+ boolean useSmartReply, boolean sendBcc) throws IOException, MessagingException {
+ Message message = Message.restoreMessageWithId(context, messageId);
+ if (message == null) {
+ // throw something?
+ return;
+ }
+
+ OutputStream stream = new BufferedOutputStream(out, 1024);
+ Writer writer = new OutputStreamWriter(stream);
+
+ // Write the fixed headers. Ordering is arbitrary (the legacy code iterated through a
+ // hashmap here).
+
+ String date = DATE_FORMAT.format(new Date(message.mTimeStamp));
+ writeHeader(writer, "Date", date);
+
+ writeEncodedHeader(writer, "Subject", message.mSubject);
+
+ writeHeader(writer, "Message-ID", message.mMessageId);
+
+ writeAddressHeader(writer, "From", message.mFrom);
+ writeAddressHeader(writer, "To", message.mTo);
+ writeAddressHeader(writer, "Cc", message.mCc);
+ // Address fields. Note that we skip bcc unless the sendBcc argument is true
+ // SMTP should NOT send bcc headers, but EAS must send it!
+ if (sendBcc) {
+ writeAddressHeader(writer, "Bcc", message.mBcc);
+ }
+ writeAddressHeader(writer, "Reply-To", message.mReplyTo);
+ writeHeader(writer, "MIME-Version", "1.0");
+
+ // Analyze message and determine if we have multiparts
+ String text = buildBodyText(context, message, useSmartReply);
+
+ Uri uri = ContentUris.withAppendedId(Attachment.MESSAGE_ID_URI, messageId);
+ Cursor attachmentsCursor = context.getContentResolver().query(uri,
+ Attachment.CONTENT_PROJECTION, WHERE_NOT_SMART_FORWARD, null, null);
+
+ try {
+ int attachmentCount = attachmentsCursor.getCount();
+ boolean multipart = attachmentCount > 0;
+ String multipartBoundary = null;
+ String multipartType = "mixed";
+
+ // Simplified case for no multipart - just emit text and be done.
+ if (!multipart) {
+ if (text != null) {
+ writeTextWithHeaders(writer, stream, text);
+ } else {
+ writer.write("\r\n"); // a truly empty message
+ }
+ } else {
+ // continue with multipart headers, then into multipart body
+ multipartBoundary = "--_com.android.email_" + System.nanoTime();
+
+ // Move to the first attachment; this must succeed because multipart is true
+ attachmentsCursor.moveToFirst();
+ if (attachmentCount == 1) {
+ // If we've got one attachment and it's an ics "attachment", we want to send
+ // this as multipart/alternative instead of multipart/mixed
+ int flags = attachmentsCursor.getInt(Attachment.CONTENT_FLAGS_COLUMN);
+ if ((flags & Attachment.FLAG_ICS_ALTERNATIVE_PART) != 0) {
+ multipartType = "alternative";
+ }
+ }
+
+ writeHeader(writer, "Content-Type",
+ "multipart/" + multipartType + "; boundary=\"" + multipartBoundary + "\"");
+ // Finish headers and prepare for body section(s)
+ writer.write("\r\n");
+
+ // first multipart element is the body
+ if (text != null) {
+ writeBoundary(writer, multipartBoundary, false);
+ writeTextWithHeaders(writer, stream, text);
+ }
+
+ // Write out the attachments until we run out
+ do {
+ writeBoundary(writer, multipartBoundary, false);
+ Attachment attachment =
+ Attachment.getContent(attachmentsCursor, Attachment.class);
+ writeOneAttachment(context, writer, stream, attachment);
+ writer.write("\r\n");
+ } while (attachmentsCursor.moveToNext());
+
+ // end of multipart section
+ writeBoundary(writer, multipartBoundary, true);
+ }
+ } finally {
+ attachmentsCursor.close();
+ }
+
+ writer.flush();
+ out.flush();
+ }
+
+ /**
+ * Write a single attachment and its payload
+ */
+ private static void writeOneAttachment(Context context, Writer writer, OutputStream out,
+ Attachment attachment) throws IOException, MessagingException {
+ writeHeader(writer, "Content-Type",
+ attachment.mMimeType + ";\n name=\"" + attachment.mFileName + "\"");
+ writeHeader(writer, "Content-Transfer-Encoding", "base64");
+ // Most attachments (real files) will send Content-Disposition. The suppression option
+ // is used when sending calendar invites.
+ if ((attachment.mFlags & Attachment.FLAG_ICS_ALTERNATIVE_PART) == 0) {
+ writeHeader(writer, "Content-Disposition",
+ "attachment;"
+ + "\n filename=\"" + attachment.mFileName + "\";"
+ + "\n size=" + Long.toString(attachment.mSize));
+ }
+ writeHeader(writer, "Content-ID", attachment.mContentId);
+ writer.append("\r\n");
+
+ // Set up input stream and write it out via base64
+ InputStream inStream = null;
+ try {
+ // Use content, if provided; otherwise, use the contentUri
+ if (attachment.mContentBytes != null) {
+ inStream = new ByteArrayInputStream(attachment.mContentBytes);
+ } else {
+ // try to open the file
+ Uri fileUri = Uri.parse(attachment.mContentUri);
+ inStream = context.getContentResolver().openInputStream(fileUri);
+ }
+ // switch to output stream for base64 text output
+ writer.flush();
+ Base64OutputStream base64Out = new Base64OutputStream(
+ out, Base64.CRLF | Base64.NO_CLOSE);
+ // copy base64 data and close up
+ IOUtils.copy(inStream, base64Out);
+ base64Out.close();
+
+ // The old Base64OutputStream wrote an extra CRLF after
+ // the output. It's not required by the base-64 spec; not
+ // sure if it's required by RFC 822 or not.
+ out.write('\r');
+ out.write('\n');
+ out.flush();
+ }
+ catch (FileNotFoundException fnfe) {
+ // Ignore this - empty file is OK
+ }
+ catch (IOException ioe) {
+ throw new MessagingException("Invalid attachment.", ioe);
+ }
+ }
+
+ /**
+ * Write a single header with no wrapping or encoding
+ *
+ * @param writer the output writer
+ * @param name the header name
+ * @param value the header value
+ */
+ private static void writeHeader(Writer writer, String name, String value) throws IOException {
+ if (value != null && value.length() > 0) {
+ writer.append(name);
+ writer.append(": ");
+ writer.append(value);
+ writer.append("\r\n");
+ }
+ }
+
+ /**
+ * Write a single header using appropriate folding & encoding
+ *
+ * @param writer the output writer
+ * @param name the header name
+ * @param value the header value
+ */
+ private static void writeEncodedHeader(Writer writer, String name, String value)
+ throws IOException {
+ if (value != null && value.length() > 0) {
+ writer.append(name);
+ writer.append(": ");
+ writer.append(MimeUtility.foldAndEncode2(value, name.length() + 2));
+ writer.append("\r\n");
+ }
+ }
+
+ /**
+ * Unpack, encode, and fold address(es) into a header
+ *
+ * @param writer the output writer
+ * @param name the header name
+ * @param value the header value (a packed list of addresses)
+ */
+ private static void writeAddressHeader(Writer writer, String name, String value)
+ throws IOException {
+ if (value != null && value.length() > 0) {
+ writer.append(name);
+ writer.append(": ");
+ writer.append(MimeUtility.fold(Address.packedToHeader(value), name.length() + 2));
+ writer.append("\r\n");
+ }
+ }
+
+ /**
+ * Write a multipart boundary
+ *
+ * @param writer the output writer
+ * @param boundary the boundary string
+ * @param end false if inner boundary, true if final boundary
+ */
+ private static void writeBoundary(Writer writer, String boundary, boolean end)
+ throws IOException {
+ writer.append("--");
+ writer.append(boundary);
+ if (end) {
+ writer.append("--");
+ }
+ writer.append("\r\n");
+ }
+
+ /**
+ * Write text (either as main body or inside a multipart), preceded by appropriate headers.
+ *
+ * Note this always uses base64, even when not required. Slightly less efficient for
+ * US-ASCII text, but handles all formats even when non-ascii chars are involved. A small
+ * optimization might be to prescan the string for safety and send raw if possible.
+ *
+ * @param writer the output writer
+ * @param out the output stream inside the writer (used for byte[] access)
+ * @param text The original text of the message
+ */
+ private static void writeTextWithHeaders(Writer writer, OutputStream out, String text)
+ throws IOException {
+ writeHeader(writer, "Content-Type", "text/plain; charset=utf-8");
+ writeHeader(writer, "Content-Transfer-Encoding", "base64");
+ writer.write("\r\n");
+ byte[] bytes = text.getBytes("UTF-8");
+ writer.flush();
+ out.write(Base64.encode(bytes, Base64.CRLF));
+ }
+}
diff --git a/src/com/android/emailcommon/internet/TextBody.java b/src/com/android/emailcommon/internet/TextBody.java
new file mode 100644
index 000000000..09c265c8e
--- /dev/null
+++ b/src/com/android/emailcommon/internet/TextBody.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.emailcommon.internet;
+
+import com.android.emailcommon.mail.Body;
+import com.android.emailcommon.mail.MessagingException;
+
+import android.util.Base64;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+
+public class TextBody implements Body {
+ String mBody;
+
+ public TextBody(String body) {
+ this.mBody = body;
+ }
+
+ public void writeTo(OutputStream out) throws IOException, MessagingException {
+ byte[] bytes = mBody.getBytes("UTF-8");
+ out.write(Base64.encode(bytes, Base64.CRLF));
+ }
+
+ /**
+ * Get the text of the body in it's unencoded format.
+ * @return
+ */
+ public String getText() {
+ return mBody;
+ }
+
+ /**
+ * Returns an InputStream that reads this body's text in UTF-8 format.
+ */
+ public InputStream getInputStream() throws MessagingException {
+ try {
+ byte[] b = mBody.getBytes("UTF-8");
+ return new ByteArrayInputStream(b);
+ }
+ catch (UnsupportedEncodingException usee) {
+ return null;
+ }
+ }
+}