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 mParts = new ArrayList(); @@ -54,19 +55,14 @@ public abstract class Multipart implements Body { this.mParent = parent; } - public void setEncoding(String encoding) { - for (BodyPart part : mParts) { - try { - Body body = part.getBody(); - if (body instanceof TextBody) { - part.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, encoding); - ((TextBody)body).setEncoding(encoding); - } - } catch (MessagingException e) { - // Ignore - } + 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"); } + /* Nothing else to do. Each subpart has its own separate encoding */ } public void setCharset(String charset) throws MessagingException { diff --git a/src/com/fsck/k9/mail/Part.java b/src/com/fsck/k9/mail/Part.java index 9dad93a04..731861b76 100644 --- a/src/com/fsck/k9/mail/Part.java +++ b/src/com/fsck/k9/mail/Part.java @@ -30,4 +30,17 @@ public interface Part { public void setBody(Body body) throws MessagingException; public void writeTo(OutputStream out) throws IOException, MessagingException; + + /** + * Called just prior to transmission, once the type of transport is known to + * be 7bit. + *

+ * 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. + *

+ * + * @param type + * A String representing a MIME content-type + * @return A String representing a MIME content-transfer-encoding + */ + public static String getEncodingforType(String type) { + if (type == null) { + return (MimeUtil.ENC_BASE64); + } else if (MimeUtil.isMessage(type)) { + return (MimeUtil.ENC_8BIT); + } else if ("multipart/signed".equalsIgnoreCase(type) || type.toLowerCase(Locale.US).startsWith("message/")) { + return (MimeUtil.ENC_7BIT); + } else if (type.toLowerCase(Locale.US).startsWith("multipart/")) { + return (MimeUtil.ENC_8BIT); + } else { + return (MimeUtil.ENC_BASE64); + } + } + private static Message getMessageFromPart(Part part) { while (part != null) { if (part instanceof Message) diff --git a/src/com/fsck/k9/mail/internet/TextBody.java b/src/com/fsck/k9/mail/internet/TextBody.java index 4e9dae3fe..c107937f8 100644 --- a/src/com/fsck/k9/mail/internet/TextBody.java +++ b/src/com/fsck/k9/mail/internet/TextBody.java @@ -7,6 +7,7 @@ import com.fsck.k9.mail.MessagingException; import java.io.*; import org.apache.james.mime4j.codec.QuotedPrintableOutputStream; +import org.apache.james.mime4j.util.MimeUtil; public class TextBody implements Body { @@ -31,7 +32,7 @@ public class TextBody implements Body { public void writeTo(OutputStream out) throws IOException, MessagingException { if (mBody != null) { byte[] bytes = mBody.getBytes(mCharset); - if ("8bit".equals(mEncoding)) { + if (MimeUtil.ENC_8BIT.equalsIgnoreCase(mEncoding)) { out.write(bytes); } else { QuotedPrintableOutputStream qp = new QuotedPrintableOutputStream(out, false); diff --git a/src/com/fsck/k9/mail/store/ImapStore.java b/src/com/fsck/k9/mail/store/ImapStore.java index e95ab2294..e2bbfa0f1 100644 --- a/src/com/fsck/k9/mail/store/ImapStore.java +++ b/src/com/fsck/k9/mail/store/ImapStore.java @@ -1653,9 +1653,12 @@ public class ImapStore extends Store { String bodyString = (String)literal; InputStream bodyStream = new ByteArrayInputStream(bodyString.getBytes()); - String contentTransferEncoding = part.getHeader( - MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING)[0]; - part.setBody(MimeUtility.decodeBody(bodyStream, contentTransferEncoding)); + String contentTransferEncoding = part + .getHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING)[0]; + String contentType = part + .getHeader(MimeHeader.HEADER_CONTENT_TYPE)[0]; + part.setBody(MimeUtility.decodeBody(bodyStream, + contentTransferEncoding, contentType)); } else { // This shouldn't happen throw new MessagingException("Got FETCH response with bogus parameters"); @@ -3547,10 +3550,13 @@ public class ImapStore extends Store { ImapResponseParser.equalsIgnoreCase(response.get(1), "FETCH")) { //TODO: check for correct UID - String contentTransferEncoding = mPart.getHeader( - MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING)[0]; + String contentTransferEncoding = mPart + .getHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING)[0]; + String contentType = mPart + .getHeader(MimeHeader.HEADER_CONTENT_TYPE)[0]; - return MimeUtility.decodeBody(literal, contentTransferEncoding); + return MimeUtility.decodeBody(literal, contentTransferEncoding, + contentType); } return null; } diff --git a/src/com/fsck/k9/mail/store/LocalStore.java b/src/com/fsck/k9/mail/store/LocalStore.java index b75c37375..42c0309d7 100644 --- a/src/com/fsck/k9/mail/store/LocalStore.java +++ b/src/com/fsck/k9/mail/store/LocalStore.java @@ -25,6 +25,7 @@ import java.util.UUID; import java.util.regex.Pattern; import org.apache.commons.io.IOUtils; +import org.apache.james.mime4j.codec.QuotedPrintableOutputStream; import org.apache.james.mime4j.util.MimeUtil; import android.app.Application; @@ -52,6 +53,7 @@ import com.fsck.k9.helper.Utility; 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.FetchProfile; import com.fsck.k9.mail.Flag; import com.fsck.k9.mail.Folder; @@ -1937,7 +1939,7 @@ public class LocalStore extends Store implements Serializable { String contentUri = cursor.getString(5); String contentId = cursor.getString(6); String contentDisposition = cursor.getString(7); - String encoding = (MimeUtil.isMessage(type)? "8bit" : "base64"); + String encoding = MimeUtility.getEncodingforType(type); Body body = null; if (contentDisposition == null) { @@ -1945,12 +1947,19 @@ public class LocalStore extends Store implements Serializable { } if (contentUri != null) { - body = new LocalAttachmentBody(Uri.parse(contentUri), mApplication); - ((LocalAttachmentBody) body).setEncoding(encoding); + if (MimeUtil.isMessage(type)) { + body = new LocalAttachmentMessageBody( + Uri.parse(contentUri), + mApplication); + } else { + body = new LocalAttachmentBody( + Uri.parse(contentUri), + mApplication); + } } MimeBodyPart bp = new LocalAttachmentBodyPart(body, id); - bp.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, encoding); + bp.setEncoding(encoding); if (name != null) { bp.setHeader(MimeHeader.HEADER_CONTENT_TYPE, String.format("%s;\n name=\"%s\"", @@ -2855,7 +2864,13 @@ public class LocalStore extends Store implements Serializable { contentUri = AttachmentProvider.getAttachmentUri( mAccount, attachmentId); - attachment.setBody(new LocalAttachmentBody(contentUri, mApplication)); + if (MimeUtil.isMessage(attachment.getMimeType())) { + attachment.setBody(new LocalAttachmentMessageBody( + contentUri, mApplication)); + } else { + attachment.setBody(new LocalAttachmentBody( + contentUri, mApplication)); + } ContentValues cv = new ContentValues(); cv.put("content_uri", contentUri != null ? contentUri.toString() : null); db.update("attachments", cv, "id = ?", new String[] @@ -3991,7 +4006,7 @@ public class LocalStore extends Store implements Serializable { private static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; private Application mApplication; private Uri mUri; - private String mEncoding; + protected String mEncoding; public LocalAttachmentBody(Uri uri, Application application) { mApplication = application; @@ -4013,21 +4028,20 @@ public class LocalStore extends Store implements Serializable { @Override public void writeTo(OutputStream out) throws IOException, MessagingException { + boolean closeStream = false; InputStream in = getInputStream(); + if (MimeUtil.isBase64Encoding(mEncoding)) { + out = (OutputStream) new Base64OutputStream(out); + closeStream = true; + } else if (MimeUtil.isQuotedPrintableEncoded(mEncoding)){ + out = new QuotedPrintableOutputStream(out, false); + closeStream = true; + } try { - - // TODO: attachments of type rfc822 are sent with 8bit encoding - // without regard to the SMTP server's support of 8BITMIME, whereas - // strict protocol compliance requires that they be converted to - // 7bit in the (unlikely) event that 8BITMIME is not supported. - - if (MimeUtil.isBase64Encoding(mEncoding)) { - out = new Base64OutputStream(out); - } try { IOUtils.copy(in, out); } finally { - if (MimeUtil.isBase64Encoding(mEncoding)) { + if (closeStream) { out.close(); } } @@ -4040,7 +4054,61 @@ public class LocalStore extends Store implements Serializable { return mUri; } - public void setEncoding(String encoding) { + public void setEncoding(String encoding) throws MessagingException { + mEncoding = encoding; + } + } + + /** + * A {@link LocalAttachmentBody} extension containing a message/rfc822 type body + * + */ + public static class LocalAttachmentMessageBody extends LocalAttachmentBody implements CompositeBody { + + public LocalAttachmentMessageBody(Uri uri, Application application) { + super(uri, application); + } + + @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. + */ + + } + + @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; } } diff --git a/src/com/fsck/k9/mail/transport/SmtpTransport.java b/src/com/fsck/k9/mail/transport/SmtpTransport.java index d0bff5ce5..a958c42d6 100644 --- a/src/com/fsck/k9/mail/transport/SmtpTransport.java +++ b/src/com/fsck/k9/mail/transport/SmtpTransport.java @@ -477,7 +477,9 @@ public class SmtpTransport extends Transport { close(); open(); - message.setEncoding(m8bitEncodingAllowed ? "8bit" : null); + if (!m8bitEncodingAllowed) { + message.setUsing7bitTransport(); + } // If the message has attachments and our server has told us about a limit on // the size of messages, count the message's size before sending it if (mLargestAcceptableMessage > 0 && ((LocalMessage)message).hasAttachments()) {