mirror of
https://github.com/moparisthebest/k-9
synced 2024-12-24 08:38:51 -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:
parent
77407eb5b7
commit
45e3d8459e
@ -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...
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
29
src/com/fsck/k9/mail/CompositeBody.java
Normal file
29
src/com/fsck/k9/mail/CompositeBody.java
Normal 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;
|
||||
|
||||
}
|
@ -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 {
|
||||
* </p>
|
||||
*/
|
||||
public abstract Message clone();
|
||||
public abstract void setUsing7bitTransport() throws MessagingException;
|
||||
}
|
||||
|
@ -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<BodyPart> mParts = new ArrayList<BodyPart>();
|
||||
@ -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 {
|
||||
|
@ -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.
|
||||
* <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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
62
src/com/fsck/k9/mail/internet/BinaryTempFileMessageBody.java
Normal file
62
src/com/fsck/k9/mail/internet/BinaryTempFileMessageBody.java
Normal 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.
|
||||
*/
|
||||
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
* <ul>
|
||||
* <li>null: base64
|
||||
* <li>message/rfc822: 8bit
|
||||
* <li>message/*: 7bit
|
||||
* <li>multipart/signed: 7bit
|
||||
* <li>multipart/*: 8bit
|
||||
* <li>*/*: 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) {
|
||||
while (part != null) {
|
||||
if (part instanceof Message)
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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()) {
|
||||
|
Loading…
Reference in New Issue
Block a user