Author: rdonkin Date: Mon Jul 21 13:03:38 2008 New Revision: 678541 URL: http://svn.apache.org/viewvc?rev=678541&view=rev Log: Improved encoder supports text and binary modes. More tests are needed but I think the pay will be greater if these are added as integration tests.
Added: james/mime4j/trunk/src/test/java/org/apache/james/mime4j/util/QuotedPrintableTextEncodeTest.java Modified: james/mime4j/trunk/src/main/java/org/apache/james/mime4j/util/CodecUtil.java Modified: james/mime4j/trunk/src/main/java/org/apache/james/mime4j/util/CodecUtil.java URL: http://svn.apache.org/viewvc/james/mime4j/trunk/src/main/java/org/apache/james/mime4j/util/CodecUtil.java?rev=678541&r1=678540&r2=678541&view=diff ============================================================================== --- james/mime4j/trunk/src/main/java/org/apache/james/mime4j/util/CodecUtil.java (original) +++ james/mime4j/trunk/src/main/java/org/apache/james/mime4j/util/CodecUtil.java Mon Jul 21 13:03:38 2008 @@ -35,6 +35,7 @@ private static final int DEFAULT_ENCODING_BUFFER_SIZE = 1024; + private static final byte TAB = 0x09; private static final byte SPACE = 0x20; private static final byte EQUALS = 0x3D; private static final byte CR = 0x0D; @@ -60,7 +61,7 @@ /** * Encodes the given stream using Quoted-Printable. - * This assumes that text is binary and therefore escapes + * This assumes that stream is binary and therefore escapes * all line endings. * @param in not null * @param out not null @@ -68,14 +69,18 @@ */ public static void encodeQuotedPrintableBinary(final InputStream in, final OutputStream out) throws IOException { - BinaryQuotedPrintableEncoder encoder = new BinaryQuotedPrintableEncoder(DEFAULT_ENCODING_BUFFER_SIZE); + QuotedPrintableEncoder encoder = new QuotedPrintableEncoder(DEFAULT_ENCODING_BUFFER_SIZE, true); encoder.encode(in, out); } - private static final class BinaryQuotedPrintableEncoder { + private static final class QuotedPrintableEncoder { 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 inputIndex; private int outputIndex; @@ -84,7 +89,7 @@ private OutputStream out; - public BinaryQuotedPrintableEncoder(int bufferSize) { + public QuotedPrintableEncoder(int bufferSize, boolean binary) { inBuffer = new byte[bufferSize]; outBuffer = new byte[3*bufferSize]; inputLength = 0; @@ -92,11 +97,18 @@ nextSoftBreak = QUOTED_PRINTABLE_MAX_LINE_LENGTH + 1; in = null; out = null; + this.binary = binary; + pendingSpace = false; + pendingTab = false; + pendingCR = false; } public void encode(final InputStream in, final OutputStream out) throws IOException { this.in = in; this.out = out; + pendingSpace = false; + pendingTab = false; + pendingCR = false; nextSoftBreak = QUOTED_PRINTABLE_MAX_LINE_LENGTH + 1; read(); while(inputLength > -1) { @@ -107,6 +119,7 @@ } read(); } + writePending(); flushOutput(); } @@ -115,15 +128,73 @@ inputIndex = 0; } + 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 <= SPACE) { - escape(next); - } else if (next > QUOTED_PRINTABLE_LAST_PLAIN) { - escape(next); - } else if (next == EQUALS) { - escape(next); + 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 { - plain(next); + 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); + } } } @@ -157,6 +228,10 @@ private void softBreak() throws IOException { write(EQUALS); + lineBreak(); + } + + private void lineBreak() throws IOException { write(CR); write(LF); nextSoftBreak = QUOTED_PRINTABLE_MAX_LINE_LENGTH; @@ -173,6 +248,19 @@ } /** + * Encodes the given stream using Quoted-Printable. + * This assumes that stream is text and therefore does not escape + * all line endings. + * @param in not null + * @param out not null + * @throws IOException + */ + public static void encodeQuotedPrintable(final InputStream in, final OutputStream out) throws IOException { + final QuotedPrintableEncoder encoder = new QuotedPrintableEncoder(DEFAULT_ENCODING_BUFFER_SIZE, false); + encoder.encode(in, out); + } + + /** * Encodes the given stream using Base64. * @param in not null * @param out not null Added: james/mime4j/trunk/src/test/java/org/apache/james/mime4j/util/QuotedPrintableTextEncodeTest.java URL: http://svn.apache.org/viewvc/james/mime4j/trunk/src/test/java/org/apache/james/mime4j/util/QuotedPrintableTextEncodeTest.java?rev=678541&view=auto ============================================================================== --- james/mime4j/trunk/src/test/java/org/apache/james/mime4j/util/QuotedPrintableTextEncodeTest.java (added) +++ james/mime4j/trunk/src/test/java/org/apache/james/mime4j/util/QuotedPrintableTextEncodeTest.java Mon Jul 21 13:03:38 2008 @@ -0,0 +1,181 @@ +/* + * 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.util; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.util.Arrays; + +import org.apache.commons.io.IOUtils; +import org.apache.james.mime4j.decoder.QuotedPrintableInputStream; + +import junit.framework.TestCase; + +public class QuotedPrintableTextEncodeTest extends TestCase { + + private static final Charset US_ASCII = Charset.forName("US-ASCII"); + + protected void setUp() throws Exception { + super.setUp(); + } + + protected void tearDown() throws Exception { + super.tearDown(); + } + + public void testEscapedSoftBreak() throws Exception { + byte[] content = new byte[500]; + Arrays.fill(content, (byte)0x18); + byte[] expected = new byte[1557]; + int index = 0; + for (int l=0;l<20;l++) { + for (int i=0;i<25;i++) { + expected[index++] = '='; + expected[index++] = '1'; + expected[index++] = '8'; + } + if (l<19) { + expected[index++] = '='; + expected[index++] = '\r'; + expected[index++] = '\n'; + } + } + check(content, expected); + } + + public void testPlainAsciiSoftBreak() throws Exception { + byte[] content = new byte[500]; + Arrays.fill(content, (byte)0x29); + byte[] expected = new byte[518]; + Arrays.fill(expected, (byte)0x29); + expected[75] = '='; + expected[76] = '\r'; + expected[77] = '\n'; + expected[153] = '='; + expected[154] = '\r'; + expected[155] = '\n'; + expected[231] = '='; + expected[232] = '\r'; + expected[233] = '\n'; + expected[309] = '='; + expected[310] = '\r'; + expected[311] = '\n'; + expected[387] = '='; + expected[388] = '\r'; + expected[389] = '\n'; + expected[465] = '='; + expected[466] = '\r'; + expected[467] = '\n'; + check(content, expected); + } + + public void testPlainASCII() throws Exception { + checkRoundtrip("Thisisaverysimplemessage.Thisisaverysimplemessage.Thisisaverysimplemessage." + + "Thisisaverysimplemessage.Thisisaverysimplemessage.Thisisaverysimplemessage." + + "Thisisaverysimplemessage.Thisisaverysimplemessage.Thisisaverysimplemessage." + + "Thisisaverysimplemessage.Thisisaverysimplemessage.Thisisaverysimplemessage."); + } + + public void testEncodeSpace() throws Exception { + checkRoundtrip(" A"); + } + + public void testLetterEncoding() throws Exception { + for (byte b=0;b<Byte.MAX_VALUE;b++) { + byte[] content = {b}; + // White space is only escaped when followed by CRLF + if (b != 32 && b != 9) { + checkRoundtrip(content); + } + } + } + + public void testCRLFShouldResetLineCount() throws Exception { + StringBuffer buffer = new StringBuffer(4096); + for (int i=0;i<1000;i++) { + buffer.append("Hugo\r\n"); + } + String longLine = buffer.toString(); + check(longLine, longLine); + } + + public void testDontEscapeLF() throws Exception { + check("Ready\nFor\n", "Ready\nFor\n"); + } + + public void testDontEscapeCR() throws Exception { + check("Ready\rFor\r", "Ready\rFor\r"); + } + + public void testEscapeSpaceAtLineEnd() throws Exception { + check(" \r\n", " =20\r\n"); + } + + public void testDontEscapeSpaceBeforeLineEnd() throws Exception { + check(" ", " "); + } + + public void testDontEscapeTabsBeforeLineEnd() throws Exception { + check("\t\t\t\t", "\t\t\t\t"); + } + + public void testDontWhiteSpaceBeforeLineEnd() throws Exception { + check(" \t\t \t", " \t\t \t"); + } + + private void checkRoundtrip(String content) throws Exception { + checkRoundtrip(content, US_ASCII); + } + + private void checkRoundtrip(String content, Charset charset) throws Exception { + checkRoundtrip(charset.encode(content).array()); + } + + private void checkRoundtrip(byte[] content) throws Exception { + InputStream in = new ByteArrayInputStream(content); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + CodecUtil.encodeQuotedPrintable(in, out); + // read back through decoder + in = new QuotedPrintableInputStream(new ByteArrayInputStream(out.toByteArray())); + out = new ByteArrayOutputStream(); + IOUtils.copy(in, out); + assertEquals(content, out.toByteArray()); + } + + private void check(String content, String expected) throws Exception { + check(US_ASCII.encode(content).array(), US_ASCII.encode(expected).array()); + } + + + private void check(byte[] content, byte[] expected) throws Exception { + ByteArrayInputStream in = new ByteArrayInputStream(content); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + CodecUtil.encodeQuotedPrintable(in, out); + assertEquals(expected, out.toByteArray()); + } + + private void assertEquals(byte[] expected, byte[] actual) { + assertEquals(expected.length, actual.length); + for (int i = 0; i < actual.length; i++) { + assertEquals("Mismatch@" + i, expected[i], actual[i]); + } + } +} --------------------------------------------------------------------- To unsubscribe, e-mail: [EMAIL PROTECTED] For additional commands, e-mail: [EMAIL PROTECTED]