/**************************************************************** * 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_BUFFER_SIZE = 1024 * 3; private static final byte TB = 0x09; private static final byte SP = 0x20; private static final byte EQ = 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[] outBuffer; private final boolean binary; private boolean pendingSpace; private boolean pendingTab; private boolean pendingCR; private int nextSoftBreak; private int outputIndex; private boolean closed = false; private byte[] singleByte = new byte[1]; public QuotedPrintableOutputStream(int bufsize, OutputStream out, boolean binary) { super(out); this.outBuffer = new byte[bufsize]; this.binary = binary; this.pendingSpace = false; this.pendingTab = false; this.pendingCR = false; this.outputIndex = 0; this.nextSoftBreak = QUOTED_PRINTABLE_MAX_LINE_LENGTH + 1; } public QuotedPrintableOutputStream(OutputStream out, boolean binary) { this(DEFAULT_BUFFER_SIZE, out, binary); } private void encodeChunk(byte[] buffer, int off, int len) throws IOException { for (int inputIndex = off; inputIndex < len + off; inputIndex++) { encode(buffer[inputIndex]); } } private void completeEncoding() throws IOException { writePending(); flushOutput(); } private void writePending() throws IOException { if (pendingSpace) { plain(SP); } else if (pendingTab) { plain(TB); } 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(SP); } else if (pendingTab) { escape(TB); } lineBreak(); clearPending(); } else { writePending(); plain(next); } } } else if (next == CR) { if (binary) { escape(next); } else { pendingCR = true; } } else { writePending(); if (next == SP) { if (binary) { escape(next); } else { pendingSpace = true; } } else if (next == TB) { if (binary) { escape(next); } else { pendingTab = true; } } else if (next < SP) { escape(next); } else if (next > QUOTED_PRINTABLE_LAST_PLAIN) { escape(next); } else if (next == EQ) { 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(EQ); --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(EQ); 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; } @Override public void close() throws IOException { if (closed) return; try { completeEncoding(); // do not close the wrapped stream } finally { closed = true; } } @Override public void flush() throws IOException { flushOutput(); } @Override public void write(int b) throws IOException { singleByte[0] = (byte) b; this.write(singleByte, 0, 1); } @Override public void write(byte[] b, int off, int len) throws IOException { if (closed) { throw new IOException("Stream has been closed"); } encodeChunk(b, off, len); } }