1
0
mirror of https://github.com/moparisthebest/k-9 synced 2024-11-10 11:35:11 -05:00

Recursively convert attachments of type message/rfc822 to 7bit if necessary.

The preceding commit resulted in attachments of type message/rfc822 being
sent with 8bit encoding even when the SMTP server did not support
8BITMIME.  This commit assures that messages will be converted to 7bit
when necessary.

A new interface CompositeBody was created that extends Body, and classes
Message and Multipart were changed from implementing Body to
CompositeBody.  Additional classes BinaryTempFileMessageBody and
LocalAttachmentMessageBody were created (by extending BinaryTempFileBody
and LocalAttachmentBody, respectively), and they too implement
CompositeBody.

A CompositeBody is a Body containing a composite-type that can contain
subparts that may require recursive processing when converting from 8bit
to 7bit.  The Part to which a CompositeBody belongs is only permitted to
use 8bit or 7bit encoding for the CompositeBody.

Previously, a Message was created so that it was 7bit clean by default
(even though that meant base64 encoding all attachments, including
messages).  Then, if the SMTP server supported 8BITMIME,
Message.setEncoding("8bit") was called so that bodies of type TextBody
would been transmitted using 8bit encoding rather than quoted-printable.

Now, messages are created with 8bit encoding by default.  Then, if the
SMTP server does not support 8BITMIME, Message.setUsing7bitTransport is
called to recursively convert the message and its subparts to 7bit.  The
method setUsing7bitTransport was added to the interfaces Part and
CompositeBody.

setEncoding no longer iterates over parts in Multipart.  That task belongs
to setUsing7bitTransport, which may in turn call setEncoding on the parts.

MimeUtility.getEncodingforType was created as a helper function for
choosing a default encoding that should be used for a given MIME type when
an attachment is added to a message (either while composing or when
retrieving from LocalStore).

setEncoding was implemented in MimeBodyPart to assure that the encoding
set in the Part's headers was the same as set for the Part's Body.  (The
method already existed in MimeMessage, which has similarities with
MimeBodyPart.)

MimeMessage.parse(InputStream in, boolean recurse) was implemented so that
the parser could be told to recursively process nested messages read from
the InputStream, thus giving access to all subparts at any level that may
need to be converted from 8bit to 7bit.
This commit is contained in:
Joe Steele 2013-09-02 23:49:28 -04:00
parent 77407eb5b7
commit 45e3d8459e
17 changed files with 431 additions and 68 deletions

View File

@ -79,6 +79,7 @@ import com.fsck.k9.mail.internet.MimeMultipart;
import com.fsck.k9.mail.internet.MimeUtility; import com.fsck.k9.mail.internet.MimeUtility;
import com.fsck.k9.mail.internet.TextBody; import com.fsck.k9.mail.internet.TextBody;
import com.fsck.k9.mail.store.LocalStore.LocalAttachmentBody; import com.fsck.k9.mail.store.LocalStore.LocalAttachmentBody;
import com.fsck.k9.mail.store.LocalStore.LocalAttachmentMessageBody;
import com.fsck.k9.view.MessageWebView; import com.fsck.k9.view.MessageWebView;
import org.apache.james.mime4j.codec.EncoderUtil; import org.apache.james.mime4j.codec.EncoderUtil;
import org.apache.james.mime4j.util.MimeUtil; import org.apache.james.mime4j.util.MimeUtil;
@ -1471,14 +1472,17 @@ public class MessageCompose extends K9Activity implements OnClickListener {
* @throws MessagingException * @throws MessagingException
*/ */
private void addAttachmentsToMessage(final MimeMultipart mp) throws MessagingException { private void addAttachmentsToMessage(final MimeMultipart mp) throws MessagingException {
LocalAttachmentBody body;
for (int i = 0, count = mAttachments.getChildCount(); i < count; i++) { for (int i = 0, count = mAttachments.getChildCount(); i < count; i++) {
Attachment attachment = (Attachment) mAttachments.getChildAt(i).getTag(); Attachment attachment = (Attachment) mAttachments.getChildAt(i).getTag();
String contentType = attachment.contentType; String contentType = attachment.contentType;
String encoding = (MimeUtil.isMessage(contentType)? "8bit" : "base64"); if (MimeUtil.isMessage(contentType)) {
body = new LocalAttachmentMessageBody(attachment.uri,
LocalAttachmentBody body = new LocalAttachmentBody(attachment.uri, getApplication()); getApplication());
} else {
body = new LocalAttachmentBody(attachment.uri, getApplication());
}
MimeBodyPart bp = new MimeBodyPart(body); MimeBodyPart bp = new MimeBodyPart(body);
body.setEncoding(encoding);
/* /*
* Correctly encode the filename here. Otherwise the whole * Correctly encode the filename here. Otherwise the whole
@ -1490,7 +1494,7 @@ public class MessageCompose extends K9Activity implements OnClickListener {
EncoderUtil.encodeIfNecessary(attachment.name, EncoderUtil.encodeIfNecessary(attachment.name,
EncoderUtil.Usage.WORD_ENTITY, 7))); EncoderUtil.Usage.WORD_ENTITY, 7)));
bp.addHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, encoding); bp.setEncoding(MimeUtility.getEncodingforType(contentType));
/* /*
* TODO: Oh the joys of MIME... * TODO: Oh the joys of MIME...

View File

@ -9,6 +9,6 @@ import com.fsck.k9.mail.store.UnavailableStorageException;
public interface Body { public interface Body {
public InputStream getInputStream() throws MessagingException; 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; public void writeTo(OutputStream out) throws IOException, MessagingException;
} }

View File

@ -11,4 +11,8 @@ public abstract class BodyPart implements Part {
public void setParent(Multipart parent) { public void setParent(Multipart parent) {
mParent = parent; mParent = parent;
} }
public abstract void setEncoding(String encoding) throws MessagingException;
public abstract void setUsing7bitTransport() throws MessagingException;
} }

View File

@ -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.
* <p>
* 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;
}

View File

@ -15,7 +15,7 @@ import com.fsck.k9.mail.filter.EOLConvertingOutputStream;
import com.fsck.k9.mail.store.UnavailableStorageException; 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 static final Flag[] EMPTY_FLAG_ARRAY = new Flag[0];
private MessageReference mReference = null; private MessageReference mReference = null;
@ -240,7 +240,7 @@ public abstract class Message implements Part, Body {
public void destroy() throws MessagingException {} 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; public abstract void setCharset(String charset) throws MessagingException;
@ -298,4 +298,5 @@ public abstract class Message implements Part, Body {
* </p> * </p>
*/ */
public abstract Message clone(); public abstract Message clone();
public abstract void setUsing7bitTransport() throws MessagingException;
} }

View File

@ -3,11 +3,12 @@ package com.fsck.k9.mail;
import java.util.ArrayList; 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.MimeUtility;
import com.fsck.k9.mail.internet.TextBody; import com.fsck.k9.mail.internet.TextBody;
public abstract class Multipart implements Body { public abstract class Multipart implements CompositeBody {
protected Part mParent; protected Part mParent;
protected ArrayList<BodyPart> mParts = new ArrayList<BodyPart>(); protected ArrayList<BodyPart> mParts = new ArrayList<BodyPart>();
@ -54,19 +55,14 @@ public abstract class Multipart implements Body {
this.mParent = parent; this.mParent = parent;
} }
public void setEncoding(String encoding) { public void setEncoding(String encoding) throws MessagingException {
for (BodyPart part : mParts) { if (!MimeUtil.ENC_7BIT.equalsIgnoreCase(encoding)
try { && !MimeUtil.ENC_8BIT.equalsIgnoreCase(encoding)) {
Body body = part.getBody(); throw new MessagingException(
if (body instanceof TextBody) { "Incompatible content-transfer-encoding applied to a CompositeBody");
part.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, encoding);
((TextBody)body).setEncoding(encoding);
}
} catch (MessagingException e) {
// Ignore
}
} }
/* Nothing else to do. Each subpart has its own separate encoding */
} }
public void setCharset(String charset) throws MessagingException { public void setCharset(String charset) throws MessagingException {

View File

@ -30,4 +30,17 @@ public interface Part {
public void setBody(Body body) throws MessagingException; public void setBody(Body body) throws MessagingException;
public void writeTo(OutputStream out) throws IOException, MessagingException; public void writeTo(OutputStream out) throws IOException, MessagingException;
/**
* Called just prior to transmission, once the type of transport is known to
* be 7bit.
* <p>
* 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;
} }

View File

@ -20,13 +20,13 @@ public class BinaryTempFileBody implements Body {
private File mFile; private File mFile;
private String mEncoding = null; String mEncoding = null;
public static void setTempDirectory(File tempDirectory) { public static void setTempDirectory(File tempDirectory) {
mTempDirectory = tempDirectory; mTempDirectory = tempDirectory;
} }
public void setEncoding(String encoding) { public void setEncoding(String encoding) throws MessagingException {
mEncoding = encoding; mEncoding = encoding;
} }

View File

@ -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.
*/
}
}

View File

@ -3,12 +3,17 @@ package com.fsck.k9.mail.internet;
import com.fsck.k9.mail.Body; import com.fsck.k9.mail.Body;
import com.fsck.k9.mail.BodyPart; import com.fsck.k9.mail.BodyPart;
import com.fsck.k9.mail.CompositeBody;
import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.Multipart;
import java.io.BufferedWriter; import java.io.BufferedWriter;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import java.io.OutputStreamWriter; 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 * 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); mHeader.addHeader(name, value);
} }
public void setHeader(String name, String value) throws MessagingException { public void setHeader(String name, String value) {
mHeader.setHeader(name, value); mHeader.setHeader(name, value);
} }
@ -60,10 +65,16 @@ public class MimeBodyPart extends BodyPart {
public void setBody(Body body) throws MessagingException { public void setBody(Body body) throws MessagingException {
this.mBody = body; this.mBody = body;
if (body instanceof com.fsck.k9.mail.Multipart) { if (body instanceof Multipart) {
com.fsck.k9.mail.Multipart multipart = ((com.fsck.k9.mail.Multipart)body); Multipart multipart = ((Multipart)body);
multipart.setParent(this); 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) { } else if (body instanceof TextBody) {
String contentType = String.format("%s;\n charset=utf-8", getMimeType()); String contentType = String.format("%s;\n charset=utf-8", getMimeType());
String name = MimeUtility.getHeaderParameter(getContentType(), "name"); String name = MimeUtility.getHeaderParameter(getContentType(), "name");
@ -71,10 +82,18 @@ public class MimeBodyPart extends BodyPart {
contentType += String.format(";\n name=\"%s\"", name); contentType += String.format(";\n name=\"%s\"", name);
} }
setHeader(MimeHeader.HEADER_CONTENT_TYPE, contentType); 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 { public String getContentType() throws MessagingException {
String contentType = getFirstHeader(MimeHeader.HEADER_CONTENT_TYPE); String contentType = getFirstHeader(MimeHeader.HEADER_CONTENT_TYPE);
return (contentType == null) ? "text/plain" : contentType; return (contentType == null) ? "text/plain" : contentType;
@ -122,4 +141,46 @@ public class MimeBodyPart extends BodyPart {
mBody.writeTo(out); 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);
}
}
} }

View File

@ -22,10 +22,12 @@ import org.apache.james.mime4j.parser.MimeStreamParser;
import org.apache.james.mime4j.stream.BodyDescriptor; import org.apache.james.mime4j.stream.BodyDescriptor;
import org.apache.james.mime4j.stream.Field; import org.apache.james.mime4j.stream.Field;
import org.apache.james.mime4j.stream.MimeConfig; 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.Address;
import com.fsck.k9.mail.Body; import com.fsck.k9.mail.Body;
import com.fsck.k9.mail.BodyPart; import com.fsck.k9.mail.BodyPart;
import com.fsck.k9.mail.CompositeBody;
import com.fsck.k9.mail.Message; import com.fsck.k9.mail.Message;
import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.Multipart; import com.fsck.k9.mail.Multipart;
@ -69,7 +71,23 @@ public class MimeMessage extends Message {
parse(in); parse(in);
} }
/**
* 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 { protected void parse(InputStream in) throws IOException, MessagingException {
parse(in, false);
}
protected void parse(InputStream in, boolean recurse) throws IOException, MessagingException {
mHeader.clear(); mHeader.clear();
mFrom = null; mFrom = null;
mTo = null; mTo = null;
@ -92,6 +110,9 @@ public class MimeMessage extends Message {
parserConfig.setMaxHeaderCount(-1); // Disable the check for header count. parserConfig.setMaxHeaderCount(-1); // Disable the check for header count.
MimeStreamParser parser = new MimeStreamParser(parserConfig); MimeStreamParser parser = new MimeStreamParser(parserConfig);
parser.setContentHandler(new MimeMessageBuilder()); parser.setContentHandler(new MimeMessageBuilder());
if (recurse) {
parser.setRecurse();
}
try { try {
parser.parse(new EOLConvertingInputStream(in)); parser.parse(new EOLConvertingInputStream(in));
} catch (MimeException me) { } catch (MimeException me) {
@ -355,11 +376,17 @@ public class MimeMessage extends Message {
if (body instanceof Multipart) { if (body instanceof Multipart) {
Multipart multipart = ((Multipart)body); Multipart multipart = ((Multipart)body);
multipart.setParent(this); 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) { } else if (body instanceof TextBody) {
setHeader(MimeHeader.HEADER_CONTENT_TYPE, String.format("%s;\n charset=utf-8", setHeader(MimeHeader.HEADER_CONTENT_TYPE, String.format("%s;\n charset=utf-8",
getMimeType())); getMimeType()));
setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "quoted-printable"); setEncoding(MimeUtil.ENC_8BIT);
} }
} }
@ -408,13 +435,11 @@ public class MimeMessage extends Message {
} }
@Override @Override
public void setEncoding(String encoding) throws UnavailableStorageException { public void setEncoding(String encoding) throws MessagingException {
if (mBody instanceof Multipart) { if (mBody != null) {
((Multipart)mBody).setEncoding(encoding); mBody.setEncoding(encoding);
} else if (mBody instanceof TextBody) {
setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, encoding);
((TextBody)mBody).setEncoding(encoding);
} }
setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, encoding);
} }
@Override @Override
@ -487,8 +512,9 @@ public class MimeMessage extends Message {
public void body(BodyDescriptor bd, InputStream in) throws IOException { public void body(BodyDescriptor bd, InputStream in) throws IOException {
expect(Part.class); expect(Part.class);
Body body = MimeUtility.decodeBody(in, bd.getTransferEncoding());
try { try {
Body body = MimeUtility.decodeBody(in,
bd.getTransferEncoding(), bd.getMimeType());
((Part)stack.peek()).setBody(body); ((Part)stack.peek()).setBody(body);
} catch (MessagingException me) { } catch (MessagingException me) {
throw new Error(me); throw new Error(me);
@ -597,4 +623,47 @@ public class MimeMessage extends Message {
public boolean hasAttachments() { public boolean hasAttachments() {
return false; 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);
}
}
} }

View File

@ -99,4 +99,11 @@ public class MimeMultipart extends Multipart {
public InputStream getInputStream() throws MessagingException { public InputStream getInputStream() throws MessagingException {
return null; return null;
} }
@Override
public void setUsing7bitTransport() throws MessagingException {
for (BodyPart part : mParts) {
part.setUsing7bitTransport();
}
}
} }

View File

@ -13,6 +13,7 @@ import com.fsck.k9.mail.internet.BinaryTempFileBody.BinaryTempFileBodyInputStrea
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.apache.james.mime4j.codec.Base64InputStream; import org.apache.james.mime4j.codec.Base64InputStream;
import org.apache.james.mime4j.codec.QuotedPrintableInputStream; import org.apache.james.mime4j.codec.QuotedPrintableInputStream;
import org.apache.james.mime4j.util.MimeUtil;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
@ -1155,23 +1156,30 @@ public class MimeUtility {
/** /**
* Removes any content transfer encoding from the stream and returns a Body. * Removes any content transfer encoding from the stream and returns a Body.
* @throws MessagingException
*/ */
public static Body decodeBody(InputStream in, String contentTransferEncoding) public static Body decodeBody(InputStream in,
throws IOException { String contentTransferEncoding, String contentType)
throws IOException, MessagingException {
/* /*
* We'll remove any transfer encoding by wrapping the stream. * We'll remove any transfer encoding by wrapping the stream.
*/ */
if (contentTransferEncoding != null) { if (contentTransferEncoding != null) {
contentTransferEncoding = contentTransferEncoding =
MimeUtility.getHeaderParameter(contentTransferEncoding, null); MimeUtility.getHeaderParameter(contentTransferEncoding, null);
if ("quoted-printable".equalsIgnoreCase(contentTransferEncoding)) { if (MimeUtil.ENC_QUOTED_PRINTABLE.equalsIgnoreCase(contentTransferEncoding)) {
in = new QuotedPrintableInputStream(in); in = new QuotedPrintableInputStream(in);
} else if ("base64".equalsIgnoreCase(contentTransferEncoding)) { } else if (MimeUtil.ENC_BASE64.equalsIgnoreCase(contentTransferEncoding)) {
in = new Base64InputStream(in); in = new Base64InputStream(in);
} }
} }
BinaryTempFileBody tempBody = new BinaryTempFileBody(); BinaryTempFileBody tempBody;
if (MimeUtil.isMessage(contentType)) {
tempBody = new BinaryTempFileMessageBody();
} else {
tempBody = new BinaryTempFileBody();
}
tempBody.setEncoding(contentTransferEncoding); tempBody.setEncoding(contentTransferEncoding);
OutputStream out = tempBody.getOutputStream(); OutputStream out = tempBody.getOutputStream();
try { try {
@ -2161,6 +2169,38 @@ public class MimeUtility {
return canonicalizeMimeType(mimeType); 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.
* <ul>
* <li>null: base64
* <li>message/rfc822: 8bit
* <li>message/*: 7bit
* <li>multipart/signed: 7bit
* <li>multipart/*: 8bit
* <li>*&#47;*: base64
* </ul>
*
* @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) { private static Message getMessageFromPart(Part part) {
while (part != null) { while (part != null) {
if (part instanceof Message) if (part instanceof Message)

View File

@ -7,6 +7,7 @@ import com.fsck.k9.mail.MessagingException;
import java.io.*; import java.io.*;
import org.apache.james.mime4j.codec.QuotedPrintableOutputStream; import org.apache.james.mime4j.codec.QuotedPrintableOutputStream;
import org.apache.james.mime4j.util.MimeUtil;
public class TextBody implements Body { public class TextBody implements Body {
@ -31,7 +32,7 @@ public class TextBody implements Body {
public void writeTo(OutputStream out) throws IOException, MessagingException { public void writeTo(OutputStream out) throws IOException, MessagingException {
if (mBody != null) { if (mBody != null) {
byte[] bytes = mBody.getBytes(mCharset); byte[] bytes = mBody.getBytes(mCharset);
if ("8bit".equals(mEncoding)) { if (MimeUtil.ENC_8BIT.equalsIgnoreCase(mEncoding)) {
out.write(bytes); out.write(bytes);
} else { } else {
QuotedPrintableOutputStream qp = new QuotedPrintableOutputStream(out, false); QuotedPrintableOutputStream qp = new QuotedPrintableOutputStream(out, false);

View File

@ -1653,9 +1653,12 @@ public class ImapStore extends Store {
String bodyString = (String)literal; String bodyString = (String)literal;
InputStream bodyStream = new ByteArrayInputStream(bodyString.getBytes()); InputStream bodyStream = new ByteArrayInputStream(bodyString.getBytes());
String contentTransferEncoding = part.getHeader( String contentTransferEncoding = part
MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING)[0]; .getHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING)[0];
part.setBody(MimeUtility.decodeBody(bodyStream, contentTransferEncoding)); String contentType = part
.getHeader(MimeHeader.HEADER_CONTENT_TYPE)[0];
part.setBody(MimeUtility.decodeBody(bodyStream,
contentTransferEncoding, contentType));
} else { } else {
// This shouldn't happen // This shouldn't happen
throw new MessagingException("Got FETCH response with bogus parameters"); throw new MessagingException("Got FETCH response with bogus parameters");
@ -3547,10 +3550,13 @@ public class ImapStore extends Store {
ImapResponseParser.equalsIgnoreCase(response.get(1), "FETCH")) { ImapResponseParser.equalsIgnoreCase(response.get(1), "FETCH")) {
//TODO: check for correct UID //TODO: check for correct UID
String contentTransferEncoding = mPart.getHeader( String contentTransferEncoding = mPart
MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING)[0]; .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; return null;
} }

View File

@ -25,6 +25,7 @@ import java.util.UUID;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.apache.james.mime4j.codec.QuotedPrintableOutputStream;
import org.apache.james.mime4j.util.MimeUtil; import org.apache.james.mime4j.util.MimeUtil;
import android.app.Application; 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.Address;
import com.fsck.k9.mail.Body; import com.fsck.k9.mail.Body;
import com.fsck.k9.mail.BodyPart; import com.fsck.k9.mail.BodyPart;
import com.fsck.k9.mail.CompositeBody;
import com.fsck.k9.mail.FetchProfile; import com.fsck.k9.mail.FetchProfile;
import com.fsck.k9.mail.Flag; import com.fsck.k9.mail.Flag;
import com.fsck.k9.mail.Folder; import com.fsck.k9.mail.Folder;
@ -1937,7 +1939,7 @@ public class LocalStore extends Store implements Serializable {
String contentUri = cursor.getString(5); String contentUri = cursor.getString(5);
String contentId = cursor.getString(6); String contentId = cursor.getString(6);
String contentDisposition = cursor.getString(7); String contentDisposition = cursor.getString(7);
String encoding = (MimeUtil.isMessage(type)? "8bit" : "base64"); String encoding = MimeUtility.getEncodingforType(type);
Body body = null; Body body = null;
if (contentDisposition == null) { if (contentDisposition == null) {
@ -1945,12 +1947,19 @@ public class LocalStore extends Store implements Serializable {
} }
if (contentUri != null) { if (contentUri != null) {
body = new LocalAttachmentBody(Uri.parse(contentUri), mApplication); if (MimeUtil.isMessage(type)) {
((LocalAttachmentBody) body).setEncoding(encoding); body = new LocalAttachmentMessageBody(
Uri.parse(contentUri),
mApplication);
} else {
body = new LocalAttachmentBody(
Uri.parse(contentUri),
mApplication);
}
} }
MimeBodyPart bp = new LocalAttachmentBodyPart(body, id); MimeBodyPart bp = new LocalAttachmentBodyPart(body, id);
bp.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, encoding); bp.setEncoding(encoding);
if (name != null) { if (name != null) {
bp.setHeader(MimeHeader.HEADER_CONTENT_TYPE, bp.setHeader(MimeHeader.HEADER_CONTENT_TYPE,
String.format("%s;\n name=\"%s\"", String.format("%s;\n name=\"%s\"",
@ -2855,7 +2864,13 @@ public class LocalStore extends Store implements Serializable {
contentUri = AttachmentProvider.getAttachmentUri( contentUri = AttachmentProvider.getAttachmentUri(
mAccount, mAccount,
attachmentId); 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(); ContentValues cv = new ContentValues();
cv.put("content_uri", contentUri != null ? contentUri.toString() : null); cv.put("content_uri", contentUri != null ? contentUri.toString() : null);
db.update("attachments", cv, "id = ?", new String[] 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 static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
private Application mApplication; private Application mApplication;
private Uri mUri; private Uri mUri;
private String mEncoding; protected String mEncoding;
public LocalAttachmentBody(Uri uri, Application application) { public LocalAttachmentBody(Uri uri, Application application) {
mApplication = application; mApplication = application;
@ -4013,21 +4028,20 @@ public class LocalStore extends Store implements Serializable {
@Override @Override
public void writeTo(OutputStream out) throws IOException, MessagingException { public void writeTo(OutputStream out) throws IOException, MessagingException {
boolean closeStream = false;
InputStream in = getInputStream(); InputStream in = getInputStream();
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)) { if (MimeUtil.isBase64Encoding(mEncoding)) {
out = new Base64OutputStream(out); out = (OutputStream) new Base64OutputStream(out);
closeStream = true;
} else if (MimeUtil.isQuotedPrintableEncoded(mEncoding)){
out = new QuotedPrintableOutputStream(out, false);
closeStream = true;
} }
try {
try { try {
IOUtils.copy(in, out); IOUtils.copy(in, out);
} finally { } finally {
if (MimeUtil.isBase64Encoding(mEncoding)) { if (closeStream) {
out.close(); out.close();
} }
} }
@ -4040,7 +4054,61 @@ public class LocalStore extends Store implements Serializable {
return mUri; 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; mEncoding = encoding;
} }
} }

View File

@ -477,7 +477,9 @@ public class SmtpTransport extends Transport {
close(); close();
open(); 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 // 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 // the size of messages, count the message's size before sending it
if (mLargestAcceptableMessage > 0 && ((LocalMessage)message).hasAttachments()) { if (mLargestAcceptableMessage > 0 && ((LocalMessage)message).hasAttachments()) {