diff --git a/src/com/fsck/k9/activity/MessageCompose.java b/src/com/fsck/k9/activity/MessageCompose.java index 8a79039a7..9823e4a37 100644 --- a/src/com/fsck/k9/activity/MessageCompose.java +++ b/src/com/fsck/k9/activity/MessageCompose.java @@ -79,6 +79,7 @@ import com.fsck.k9.mail.internet.MimeMultipart; import com.fsck.k9.mail.internet.MimeUtility; import com.fsck.k9.mail.internet.TextBody; import com.fsck.k9.mail.store.LocalStore.LocalAttachmentBody; +import com.fsck.k9.mail.store.LocalStore.LocalAttachmentMessageBody; import com.fsck.k9.view.MessageWebView; import org.apache.james.mime4j.codec.EncoderUtil; import org.apache.james.mime4j.util.MimeUtil; @@ -1471,14 +1472,17 @@ public class MessageCompose extends K9Activity implements OnClickListener { * @throws MessagingException */ private void addAttachmentsToMessage(final MimeMultipart mp) throws MessagingException { + LocalAttachmentBody body; for (int i = 0, count = mAttachments.getChildCount(); i < count; i++) { Attachment attachment = (Attachment) mAttachments.getChildAt(i).getTag(); String contentType = attachment.contentType; - String encoding = (MimeUtil.isMessage(contentType)? "8bit" : "base64"); - - LocalAttachmentBody body = new LocalAttachmentBody(attachment.uri, getApplication()); + if (MimeUtil.isMessage(contentType)) { + body = new LocalAttachmentMessageBody(attachment.uri, + getApplication()); + } else { + body = new LocalAttachmentBody(attachment.uri, getApplication()); + } MimeBodyPart bp = new MimeBodyPart(body); - body.setEncoding(encoding); /* * Correctly encode the filename here. Otherwise the whole @@ -1490,7 +1494,7 @@ public class MessageCompose extends K9Activity implements OnClickListener { EncoderUtil.encodeIfNecessary(attachment.name, EncoderUtil.Usage.WORD_ENTITY, 7))); - bp.addHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, encoding); + bp.setEncoding(MimeUtility.getEncodingforType(contentType)); /* * TODO: Oh the joys of MIME... diff --git a/src/com/fsck/k9/mail/Body.java b/src/com/fsck/k9/mail/Body.java index bb990b3f2..7b18a75a7 100644 --- a/src/com/fsck/k9/mail/Body.java +++ b/src/com/fsck/k9/mail/Body.java @@ -9,6 +9,6 @@ import com.fsck.k9.mail.store.UnavailableStorageException; public interface Body { public InputStream getInputStream() throws MessagingException; - public void setEncoding(String encoding) throws UnavailableStorageException; + public void setEncoding(String encoding) throws UnavailableStorageException, MessagingException; public void writeTo(OutputStream out) throws IOException, MessagingException; } diff --git a/src/com/fsck/k9/mail/BodyPart.java b/src/com/fsck/k9/mail/BodyPart.java index e59aaf344..163fe7ba6 100644 --- a/src/com/fsck/k9/mail/BodyPart.java +++ b/src/com/fsck/k9/mail/BodyPart.java @@ -11,4 +11,8 @@ public abstract class BodyPart implements Part { public void setParent(Multipart parent) { mParent = parent; } + + public abstract void setEncoding(String encoding) throws MessagingException; + + public abstract void setUsing7bitTransport() throws MessagingException; } diff --git a/src/com/fsck/k9/mail/CompositeBody.java b/src/com/fsck/k9/mail/CompositeBody.java new file mode 100644 index 000000000..88a1996e1 --- /dev/null +++ b/src/com/fsck/k9/mail/CompositeBody.java @@ -0,0 +1,29 @@ +package com.fsck.k9.mail; + + +/** + * A CompositeBody is a {@link Body} extension that can contain subparts that + * may require recursing through or iterating over when converting the + * CompositeBody from 8bit to 7bit encoding. The {@link Part} to which a + * CompositeBody belongs is only permitted to use 8bit or 7bit content transfer + * encoding for the CompositeBody. + * + */ +public interface CompositeBody extends Body { + + /** + * Called just prior to transmission, once the type of transport is known to + * be 7bit. + *
+ * All subparts that are 8bit and of type {@link CompositeBody} will be + * converted to 7bit and recursed. All supbparts that are 8bit but not + * of type CompositeBody will be converted to quoted-printable. Bodies with + * encodings other than 8bit remain unchanged. + * + * @throws MessagingException + * + */ + + public abstract void setUsing7bitTransport() throws MessagingException; + +} diff --git a/src/com/fsck/k9/mail/Message.java b/src/com/fsck/k9/mail/Message.java index 6f0a819bc..21ff61f8a 100644 --- a/src/com/fsck/k9/mail/Message.java +++ b/src/com/fsck/k9/mail/Message.java @@ -15,7 +15,7 @@ import com.fsck.k9.mail.filter.EOLConvertingOutputStream; import com.fsck.k9.mail.store.UnavailableStorageException; -public abstract class Message implements Part, Body { +public abstract class Message implements Part, CompositeBody { private static final Flag[] EMPTY_FLAG_ARRAY = new Flag[0]; private MessageReference mReference = null; @@ -240,7 +240,7 @@ public abstract class Message implements Part, Body { public void destroy() throws MessagingException {} - public abstract void setEncoding(String encoding) throws UnavailableStorageException; + public abstract void setEncoding(String encoding) throws UnavailableStorageException, MessagingException; public abstract void setCharset(String charset) throws MessagingException; @@ -298,4 +298,5 @@ public abstract class Message implements Part, Body { *
*/ public abstract Message clone(); + public abstract void setUsing7bitTransport() throws MessagingException; } diff --git a/src/com/fsck/k9/mail/Multipart.java b/src/com/fsck/k9/mail/Multipart.java index 895d76d12..cab1fa280 100644 --- a/src/com/fsck/k9/mail/Multipart.java +++ b/src/com/fsck/k9/mail/Multipart.java @@ -3,11 +3,12 @@ package com.fsck.k9.mail; import java.util.ArrayList; -import com.fsck.k9.mail.internet.MimeHeader; +import org.apache.james.mime4j.util.MimeUtil; + import com.fsck.k9.mail.internet.MimeUtility; import com.fsck.k9.mail.internet.TextBody; -public abstract class Multipart implements Body { +public abstract class Multipart implements CompositeBody { protected Part mParent; protected ArrayList+ * All bodies that are 8bit will be converted to 7bit and recursed if of + * type {@link CompositeBody}, or will be converted to quoted-printable in all other + * cases. Bodies with encodings other than 8bit remain unchanged. + * + * @throws MessagingException + * + */ + public abstract void setUsing7bitTransport() throws MessagingException; } diff --git a/src/com/fsck/k9/mail/internet/BinaryTempFileBody.java b/src/com/fsck/k9/mail/internet/BinaryTempFileBody.java index 244998a70..ccdeba468 100644 --- a/src/com/fsck/k9/mail/internet/BinaryTempFileBody.java +++ b/src/com/fsck/k9/mail/internet/BinaryTempFileBody.java @@ -20,13 +20,13 @@ public class BinaryTempFileBody implements Body { private File mFile; - private String mEncoding = null; + String mEncoding = null; public static void setTempDirectory(File tempDirectory) { mTempDirectory = tempDirectory; } - public void setEncoding(String encoding) { + public void setEncoding(String encoding) throws MessagingException { mEncoding = encoding; } diff --git a/src/com/fsck/k9/mail/internet/BinaryTempFileMessageBody.java b/src/com/fsck/k9/mail/internet/BinaryTempFileMessageBody.java new file mode 100644 index 000000000..c1503b342 --- /dev/null +++ b/src/com/fsck/k9/mail/internet/BinaryTempFileMessageBody.java @@ -0,0 +1,62 @@ +package com.fsck.k9.mail.internet; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.apache.commons.io.IOUtils; +import org.apache.james.mime4j.util.MimeUtil; + +import com.fsck.k9.mail.CompositeBody; +import com.fsck.k9.mail.MessagingException; + +/** + * A {@link BinaryTempFileBody} extension containing a body of type + * message/rfc822. This relates to a BinaryTempFileBody the same way that a + * {@link LocalAttachmentMessageBody} relates to a {@link LocalAttachmentBody}. + * + */ +public class BinaryTempFileMessageBody extends BinaryTempFileBody implements CompositeBody { + + @Override + public void setEncoding(String encoding) throws MessagingException { + if (!MimeUtil.ENC_7BIT.equalsIgnoreCase(encoding) + && !MimeUtil.ENC_8BIT.equalsIgnoreCase(encoding)) { + throw new MessagingException( + "Incompatible content-transfer-encoding applied to a CompositeBody"); + } + mEncoding = encoding; + } + + @Override + public void writeTo(OutputStream out) throws IOException, MessagingException { + InputStream in = getInputStream(); + try { + if (MimeUtil.ENC_7BIT.equalsIgnoreCase(mEncoding)) { + /* + * If we knew the message was already 7bit clean, then it + * could be sent along without processing. But since we + * don't know, we recursively parse it. + */ + MimeMessage message = new MimeMessage(in, true); + message.setUsing7bitTransport(); + message.writeTo(out); + } else { + IOUtils.copy(in, out); + } + } finally { + in.close(); + } + } + + @Override + public void setUsing7bitTransport() throws MessagingException { + /* + * There's nothing to recurse into here, so there's nothing to do. + * The enclosing BodyPart already called setEncoding(MimeUtil.ENC_7BIT). Once + * writeTo() is called, the file with the rfc822 body will be opened + * for reading and will then be recursed. + */ + + } +} \ No newline at end of file diff --git a/src/com/fsck/k9/mail/internet/MimeBodyPart.java b/src/com/fsck/k9/mail/internet/MimeBodyPart.java index 70801cc74..44d1b2540 100644 --- a/src/com/fsck/k9/mail/internet/MimeBodyPart.java +++ b/src/com/fsck/k9/mail/internet/MimeBodyPart.java @@ -3,12 +3,17 @@ package com.fsck.k9.mail.internet; import com.fsck.k9.mail.Body; import com.fsck.k9.mail.BodyPart; +import com.fsck.k9.mail.CompositeBody; import com.fsck.k9.mail.MessagingException; +import com.fsck.k9.mail.Multipart; import java.io.BufferedWriter; import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; +import java.util.Locale; + +import org.apache.james.mime4j.util.MimeUtil; /** * TODO this is a close approximation of Message, need to update along with @@ -42,7 +47,7 @@ public class MimeBodyPart extends BodyPart { mHeader.addHeader(name, value); } - public void setHeader(String name, String value) throws MessagingException { + public void setHeader(String name, String value) { mHeader.setHeader(name, value); } @@ -60,10 +65,16 @@ public class MimeBodyPart extends BodyPart { public void setBody(Body body) throws MessagingException { this.mBody = body; - if (body instanceof com.fsck.k9.mail.Multipart) { - com.fsck.k9.mail.Multipart multipart = ((com.fsck.k9.mail.Multipart)body); + if (body instanceof Multipart) { + Multipart multipart = ((Multipart)body); multipart.setParent(this); - setHeader(MimeHeader.HEADER_CONTENT_TYPE, multipart.getContentType()); + String type = multipart.getContentType(); + setHeader(MimeHeader.HEADER_CONTENT_TYPE, type); + if ("multipart/signed".equalsIgnoreCase(type)) { + setEncoding(MimeUtil.ENC_7BIT); + } else { + setEncoding(MimeUtil.ENC_8BIT); + } } else if (body instanceof TextBody) { String contentType = String.format("%s;\n charset=utf-8", getMimeType()); String name = MimeUtility.getHeaderParameter(getContentType(), "name"); @@ -71,10 +82,18 @@ public class MimeBodyPart extends BodyPart { contentType += String.format(";\n name=\"%s\"", name); } setHeader(MimeHeader.HEADER_CONTENT_TYPE, contentType); - setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "quoted-printable"); + setEncoding(MimeUtil.ENC_8BIT); } } + @Override + public void setEncoding(String encoding) throws MessagingException { + if (mBody != null) { + mBody.setEncoding(encoding); + } + setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, encoding); + } + public String getContentType() throws MessagingException { String contentType = getFirstHeader(MimeHeader.HEADER_CONTENT_TYPE); return (contentType == null) ? "text/plain" : contentType; @@ -122,4 +141,46 @@ public class MimeBodyPart extends BodyPart { mBody.writeTo(out); } } + + @Override + public void setUsing7bitTransport() throws MessagingException { + String type = getFirstHeader(MimeHeader.HEADER_CONTENT_TYPE); + /* + * We don't trust that a multipart/* will properly have an 8bit encoding + * header if any of its subparts are 8bit, so we automatically recurse + * (as long as its not multipart/signed). + */ + if (mBody instanceof CompositeBody + && !"multipart/signed".equalsIgnoreCase(type)) { + setEncoding(MimeUtil.ENC_7BIT); + // recurse + ((CompositeBody) mBody).setUsing7bitTransport(); + } else if (!MimeUtil.ENC_8BIT + .equalsIgnoreCase(getFirstHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING))) { + return; + } else if (type != null + && (type.equalsIgnoreCase("multipart/signed") || type + .toLowerCase(Locale.US).startsWith("message/"))) { + /* + * This shouldn't happen. In any case, it would be wrong to convert + * them to some other encoding for 7bit transport. + * + * RFC 1847 says multipart/signed must be 7bit. It also says their + * bodies must be treated as opaque, so we must not change the + * encoding. + * + * We've dealt with (CompositeBody) type message/rfc822 above. Here + * we must deal with all other message/* types. RFC 2045 says + * message/* can only be 7bit or 8bit. RFC 2046 says unknown + * message/* types must be treated as application/octet-stream, + * which means we can't recurse into them. It also says that + * existing subtypes message/partial and message/external must only + * be 7bit, and that future subtypes "should be" 7bit. + */ + throw new MessagingException( + "Unable to convert 8bit body part to 7bit"); + } else { + setEncoding(MimeUtil.ENC_QUOTED_PRINTABLE); + } + } } diff --git a/src/com/fsck/k9/mail/internet/MimeMessage.java b/src/com/fsck/k9/mail/internet/MimeMessage.java index 2777420de..e2b53414f 100644 --- a/src/com/fsck/k9/mail/internet/MimeMessage.java +++ b/src/com/fsck/k9/mail/internet/MimeMessage.java @@ -22,10 +22,12 @@ import org.apache.james.mime4j.parser.MimeStreamParser; import org.apache.james.mime4j.stream.BodyDescriptor; import org.apache.james.mime4j.stream.Field; import org.apache.james.mime4j.stream.MimeConfig; +import org.apache.james.mime4j.util.MimeUtil; import com.fsck.k9.mail.Address; import com.fsck.k9.mail.Body; import com.fsck.k9.mail.BodyPart; +import com.fsck.k9.mail.CompositeBody; import com.fsck.k9.mail.Message; import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.Multipart; @@ -69,7 +71,23 @@ public class MimeMessage extends Message { parse(in); } - protected void parse(InputStream in) throws IOException, MessagingException { + /** + * Parse the given InputStream using Apache Mime4J to build a MimeMessage. + * + * @param in + * @param recurse A boolean indicating to recurse through all nested MimeMessage subparts. + * @throws IOException + * @throws MessagingException + */ + public MimeMessage(InputStream in, boolean recurse) throws IOException, MessagingException { + parse(in, true); + } + + protected void parse(InputStream in) throws IOException, MessagingException { + parse(in, false); + } + + protected void parse(InputStream in, boolean recurse) throws IOException, MessagingException { mHeader.clear(); mFrom = null; mTo = null; @@ -92,6 +110,9 @@ public class MimeMessage extends Message { parserConfig.setMaxHeaderCount(-1); // Disable the check for header count. MimeStreamParser parser = new MimeStreamParser(parserConfig); parser.setContentHandler(new MimeMessageBuilder()); + if (recurse) { + parser.setRecurse(); + } try { parser.parse(new EOLConvertingInputStream(in)); } catch (MimeException me) { @@ -355,11 +376,17 @@ public class MimeMessage extends Message { if (body instanceof Multipart) { Multipart multipart = ((Multipart)body); multipart.setParent(this); - setHeader(MimeHeader.HEADER_CONTENT_TYPE, multipart.getContentType()); + String type = multipart.getContentType(); + setHeader(MimeHeader.HEADER_CONTENT_TYPE, type); + if ("multipart/signed".equalsIgnoreCase(type)) { + setEncoding(MimeUtil.ENC_7BIT); + } else { + setEncoding(MimeUtil.ENC_8BIT); + } } else if (body instanceof TextBody) { setHeader(MimeHeader.HEADER_CONTENT_TYPE, String.format("%s;\n charset=utf-8", getMimeType())); - setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "quoted-printable"); + setEncoding(MimeUtil.ENC_8BIT); } } @@ -408,13 +435,11 @@ public class MimeMessage extends Message { } @Override - public void setEncoding(String encoding) throws UnavailableStorageException { - if (mBody instanceof Multipart) { - ((Multipart)mBody).setEncoding(encoding); - } else if (mBody instanceof TextBody) { - setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, encoding); - ((TextBody)mBody).setEncoding(encoding); + public void setEncoding(String encoding) throws MessagingException { + if (mBody != null) { + mBody.setEncoding(encoding); } + setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, encoding); } @Override @@ -487,8 +512,9 @@ public class MimeMessage extends Message { public void body(BodyDescriptor bd, InputStream in) throws IOException { expect(Part.class); - Body body = MimeUtility.decodeBody(in, bd.getTransferEncoding()); try { + Body body = MimeUtility.decodeBody(in, + bd.getTransferEncoding(), bd.getMimeType()); ((Part)stack.peek()).setBody(body); } catch (MessagingException me) { throw new Error(me); @@ -597,4 +623,47 @@ public class MimeMessage extends Message { public boolean hasAttachments() { return false; } + + + @Override + public void setUsing7bitTransport() throws MessagingException { + String type = getFirstHeader(MimeHeader.HEADER_CONTENT_TYPE); + /* + * We don't trust that a multipart/* will properly have an 8bit encoding + * header if any of its subparts are 8bit, so we automatically recurse + * (as long as its not multipart/signed). + */ + if (mBody instanceof CompositeBody + && !"multipart/signed".equalsIgnoreCase(type)) { + setEncoding(MimeUtil.ENC_7BIT); + // recurse + ((CompositeBody) mBody).setUsing7bitTransport(); + } else if (!MimeUtil.ENC_8BIT + .equalsIgnoreCase(getFirstHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING))) { + return; + } else if (type != null + && (type.equalsIgnoreCase("multipart/signed") || type + .toLowerCase(Locale.US).startsWith("message/"))) { + /* + * This shouldn't happen. In any case, it would be wrong to convert + * them to some other encoding for 7bit transport. + * + * RFC 1847 says multipart/signed must be 7bit. It also says their + * bodies must be treated as opaque, so we must not change the + * encoding. + * + * We've dealt with (CompositeBody) type message/rfc822 above. Here + * we must deal with all other message/* types. RFC 2045 says + * message/* can only be 7bit or 8bit. RFC 2046 says unknown + * message/* types must be treated as application/octet-stream, + * which means we can't recurse into them. It also says that + * existing subtypes message/partial and message/external must only + * be 7bit, and that future subtypes "should be" 7bit. + */ + throw new MessagingException( + "Unable to convert 8bit body part to 7bit"); + } else { + setEncoding(MimeUtil.ENC_QUOTED_PRINTABLE); + } + } } diff --git a/src/com/fsck/k9/mail/internet/MimeMultipart.java b/src/com/fsck/k9/mail/internet/MimeMultipart.java index dec82461b..e00d4da3e 100644 --- a/src/com/fsck/k9/mail/internet/MimeMultipart.java +++ b/src/com/fsck/k9/mail/internet/MimeMultipart.java @@ -99,4 +99,11 @@ public class MimeMultipart extends Multipart { public InputStream getInputStream() throws MessagingException { return null; } + + @Override + public void setUsing7bitTransport() throws MessagingException { + for (BodyPart part : mParts) { + part.setUsing7bitTransport(); + } + } } diff --git a/src/com/fsck/k9/mail/internet/MimeUtility.java b/src/com/fsck/k9/mail/internet/MimeUtility.java index 8605da2ea..7d8544288 100644 --- a/src/com/fsck/k9/mail/internet/MimeUtility.java +++ b/src/com/fsck/k9/mail/internet/MimeUtility.java @@ -13,6 +13,7 @@ import com.fsck.k9.mail.internet.BinaryTempFileBody.BinaryTempFileBodyInputStrea import org.apache.commons.io.IOUtils; import org.apache.james.mime4j.codec.Base64InputStream; import org.apache.james.mime4j.codec.QuotedPrintableInputStream; +import org.apache.james.mime4j.util.MimeUtil; import java.io.IOException; import java.io.InputStream; @@ -1155,23 +1156,30 @@ public class MimeUtility { /** * Removes any content transfer encoding from the stream and returns a Body. + * @throws MessagingException */ - public static Body decodeBody(InputStream in, String contentTransferEncoding) - throws IOException { + public static Body decodeBody(InputStream in, + String contentTransferEncoding, String contentType) + throws IOException, MessagingException { /* * We'll remove any transfer encoding by wrapping the stream. */ if (contentTransferEncoding != null) { contentTransferEncoding = MimeUtility.getHeaderParameter(contentTransferEncoding, null); - if ("quoted-printable".equalsIgnoreCase(contentTransferEncoding)) { + if (MimeUtil.ENC_QUOTED_PRINTABLE.equalsIgnoreCase(contentTransferEncoding)) { in = new QuotedPrintableInputStream(in); - } else if ("base64".equalsIgnoreCase(contentTransferEncoding)) { + } else if (MimeUtil.ENC_BASE64.equalsIgnoreCase(contentTransferEncoding)) { in = new Base64InputStream(in); } } - BinaryTempFileBody tempBody = new BinaryTempFileBody(); + BinaryTempFileBody tempBody; + if (MimeUtil.isMessage(contentType)) { + tempBody = new BinaryTempFileMessageBody(); + } else { + tempBody = new BinaryTempFileBody(); + } tempBody.setEncoding(contentTransferEncoding); OutputStream out = tempBody.getOutputStream(); try { @@ -2161,6 +2169,38 @@ public class MimeUtility { return canonicalizeMimeType(mimeType); } + + /** + * Get a default content-transfer-encoding for use with a given content-type + * when adding an unencoded attachment. It's possible that 8bit encodings + * may later be converted to 7bit for 7bit transport. + *