diff --git a/src/com/fsck/k9/mail/internet/MimeBodyPart.java b/src/com/fsck/k9/mail/internet/MimeBodyPart.java index dfb39e366..4307b6efb 100644 --- a/src/com/fsck/k9/mail/internet/MimeBodyPart.java +++ b/src/com/fsck/k9/mail/internet/MimeBodyPart.java @@ -87,11 +87,7 @@ public class MimeBodyPart extends BodyPart contentType += String.format(";\n name=\"%s\"", name); } setHeader(MimeHeader.HEADER_CONTENT_TYPE, contentType); - //TODO: Use quoted-printable - //using org.apache.commons.codec.net.QuotedPrintableCodec - //when it will implement all rules (missing #3, $4 & #5) of the RFC - //http://www.ietf.org/rfc/rfc1521.txt - setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64"); + setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "quoted-printable"); } } diff --git a/src/com/fsck/k9/mail/internet/MimeMessage.java b/src/com/fsck/k9/mail/internet/MimeMessage.java index e4a3b6d41..207a720f5 100644 --- a/src/com/fsck/k9/mail/internet/MimeMessage.java +++ b/src/com/fsck/k9/mail/internet/MimeMessage.java @@ -379,7 +379,7 @@ public class MimeMessage extends Message { setHeader(MimeHeader.HEADER_CONTENT_TYPE, String.format("%s;\n charset=utf-8", getMimeType())); - setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64"); + setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "quoted-printable"); } } diff --git a/src/com/fsck/k9/mail/internet/TextBody.java b/src/com/fsck/k9/mail/internet/TextBody.java index ded7a78ee..9bcac1ab5 100644 --- a/src/com/fsck/k9/mail/internet/TextBody.java +++ b/src/com/fsck/k9/mail/internet/TextBody.java @@ -1,12 +1,13 @@ package com.fsck.k9.mail.internet; -import com.fsck.k9.codec.binary.Base64; import com.fsck.k9.mail.Body; import com.fsck.k9.mail.MessagingException; import java.io.*; +import org.apache.james.mime4j.codec.QuotedPrintableOutputStream; + public class TextBody implements Body { private String mBody; @@ -28,7 +29,9 @@ public class TextBody implements Body } else { - out.write(Base64.encodeBase64Chunked(bytes)); + QuotedPrintableOutputStream qp = new QuotedPrintableOutputStream(out, false); + qp.write(bytes); + qp.flush(); } } } diff --git a/src/org/apache/james/mime4j/codec/QuotedPrintableEncoder.java b/src/org/apache/james/mime4j/codec/QuotedPrintableEncoder.java new file mode 100644 index 000000000..63d5e19b8 --- /dev/null +++ b/src/org/apache/james/mime4j/codec/QuotedPrintableEncoder.java @@ -0,0 +1,269 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.mime4j.codec; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +final class QuotedPrintableEncoder +{ + private static final byte TAB = 0x09; + private static final byte SPACE = 0x20; + private static final byte EQUALS = 0x3D; + private static final byte CR = 0x0D; + private static final byte LF = 0x0A; + private static final byte QUOTED_PRINTABLE_LAST_PLAIN = 0x7E; + private static final int QUOTED_PRINTABLE_MAX_LINE_LENGTH = 76; + private static final int QUOTED_PRINTABLE_OCTETS_PER_ESCAPE = 3; + private static final byte[] HEX_DIGITS = { '0', '1', '2', '3', '4', '5', + '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; + + private final byte[] inBuffer; + private final byte[] outBuffer; + private final boolean binary; + + private boolean pendingSpace; + private boolean pendingTab; + private boolean pendingCR; + private int nextSoftBreak; + private int outputIndex; + private OutputStream out; + + public QuotedPrintableEncoder(int bufferSize, boolean binary) + { + inBuffer = new byte[bufferSize]; + outBuffer = new byte[3 * bufferSize]; + outputIndex = 0; + nextSoftBreak = QUOTED_PRINTABLE_MAX_LINE_LENGTH + 1; + out = null; + this.binary = binary; + pendingSpace = false; + pendingTab = false; + pendingCR = false; + } + + void initEncoding(final OutputStream out) + { + this.out = out; + pendingSpace = false; + pendingTab = false; + pendingCR = false; + nextSoftBreak = QUOTED_PRINTABLE_MAX_LINE_LENGTH + 1; + } + + void encodeChunk(byte[] buffer, int off, int len) throws IOException + { + for (int inputIndex = off; inputIndex < len + off; inputIndex++) + { + encode(buffer[inputIndex]); + } + } + + void completeEncoding() throws IOException + { + writePending(); + flushOutput(); + } + + public void encode(final InputStream in, final OutputStream out) + throws IOException + { + initEncoding(out); + int inputLength; + while ((inputLength = in.read(inBuffer)) > -1) + { + encodeChunk(inBuffer, 0, inputLength); + } + completeEncoding(); + } + + private void writePending() throws IOException + { + if (pendingSpace) + { + plain(SPACE); + } + else if (pendingTab) + { + plain(TAB); + } + else if (pendingCR) + { + plain(CR); + } + clearPending(); + } + + private void clearPending() throws IOException + { + pendingSpace = false; + pendingTab = false; + pendingCR = false; + } + + private void encode(byte next) throws IOException + { + if (next == LF) + { + if (binary) + { + writePending(); + escape(next); + } + else + { + if (pendingCR) + { + // Expect either space or tab pending + // but not both + if (pendingSpace) + { + escape(SPACE); + } + else if (pendingTab) + { + escape(TAB); + } + lineBreak(); + clearPending(); + } + else + { + writePending(); + plain(next); + } + } + } + else if (next == CR) + { + if (binary) + { + escape(next); + } + else + { + pendingCR = true; + } + } + else + { + writePending(); + if (next == SPACE) + { + if (binary) + { + escape(next); + } + else + { + pendingSpace = true; + } + } + else if (next == TAB) + { + if (binary) + { + escape(next); + } + else + { + pendingTab = true; + } + } + else if (next < SPACE) + { + escape(next); + } + else if (next > QUOTED_PRINTABLE_LAST_PLAIN) + { + escape(next); + } + else if (next == EQUALS) + { + escape(next); + } + else + { + plain(next); + } + } + } + + private void plain(byte next) throws IOException + { + if (--nextSoftBreak <= 1) + { + softBreak(); + } + write(next); + } + + private void escape(byte next) throws IOException + { + if (--nextSoftBreak <= QUOTED_PRINTABLE_OCTETS_PER_ESCAPE) + { + softBreak(); + } + + int nextUnsigned = next & 0xff; + + write(EQUALS); + --nextSoftBreak; + write(HEX_DIGITS[nextUnsigned >> 4]); + --nextSoftBreak; + write(HEX_DIGITS[nextUnsigned % 0x10]); + } + + private void write(byte next) throws IOException + { + outBuffer[outputIndex++] = next; + if (outputIndex >= outBuffer.length) + { + flushOutput(); + } + } + + private void softBreak() throws IOException + { + write(EQUALS); + lineBreak(); + } + + private void lineBreak() throws IOException + { + write(CR); + write(LF); + nextSoftBreak = QUOTED_PRINTABLE_MAX_LINE_LENGTH; + } + + void flushOutput() throws IOException + { + if (outputIndex < outBuffer.length) + { + out.write(outBuffer, 0, outputIndex); + } + else + { + out.write(outBuffer); + } + outputIndex = 0; + } +} diff --git a/src/org/apache/james/mime4j/codec/QuotedPrintableOutputStream.java b/src/org/apache/james/mime4j/codec/QuotedPrintableOutputStream.java new file mode 100644 index 000000000..44397a19c --- /dev/null +++ b/src/org/apache/james/mime4j/codec/QuotedPrintableOutputStream.java @@ -0,0 +1,86 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.mime4j.codec; + +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +/** + * Performs Quoted-Printable encoding on an underlying stream. + */ +public class QuotedPrintableOutputStream extends FilterOutputStream +{ + + private static final int DEFAULT_ENCODING_BUFFER_SIZE = 1024; + + private QuotedPrintableEncoder encoder; + private boolean closed = false; + + public QuotedPrintableOutputStream(OutputStream out, boolean binary) + { + super(out); + encoder = new QuotedPrintableEncoder(DEFAULT_ENCODING_BUFFER_SIZE, binary); + encoder.initEncoding(out); + } + + @Override + public void close() throws IOException + { + if (closed) + { + return; + } + + try + { + encoder.completeEncoding(); + // do not close the wrapped stream + } + finally + { + closed = true; + } + } + + @Override + public void flush() throws IOException + { + encoder.flushOutput(); + } + + @Override + public void write(int b) throws IOException + { + this.write(new byte[] { (byte) b }, 0, 1); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException + { + if (closed) + { + throw new IOException("QuotedPrintableOutputStream has been closed"); + } + + encoder.encodeChunk(b, off, len); + } + +}