001/****************************************************************
002 * Licensed to the Apache Software Foundation (ASF) under one   *
003 * or more contributor license agreements.  See the NOTICE file *
004 * distributed with this work for additional information        *
005 * regarding copyright ownership.  The ASF licenses this file   *
006 * to you under the Apache License, Version 2.0 (the            *
007 * "License"); you may not use this file except in compliance   *
008 * with the License.  You may obtain a copy of the License at   *
009 *                                                              *
010 *   http://www.apache.org/licenses/LICENSE-2.0                 *
011 *                                                              *
012 * Unless required by applicable law or agreed to in writing,   *
013 * software distributed under the License is distributed on an  *
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
015 * KIND, either express or implied.  See the License for the    *
016 * specific language governing permissions and limitations      *
017 * under the License.                                           *
018 ****************************************************************/
019
020package org.apache.james.mime4j.codec;
021
022import java.io.FilterOutputStream;
023import java.io.IOException;
024import java.io.OutputStream;
025
026/**
027 * Performs Quoted-Printable encoding on an underlying stream.
028 *
029 * Encodes every "required" char plus the dot ".". We encode the dot
030 * by default because this is a workaround for some "filter"/"antivirus"
031 * "old mua" having issues with dots at the beginning or the end of a
032 * qp encode line (maybe a bad dot-destuffing algo).
033 */
034public class QuotedPrintableOutputStream extends FilterOutputStream {
035
036    private static final int DEFAULT_BUFFER_SIZE = 1024 * 3;
037
038    private static final byte TB = 0x09;
039    private static final byte SP = 0x20;
040    private static final byte EQ = 0x3D;
041    private static final byte DOT = 0x2E;
042    private static final byte CR = 0x0D;
043    private static final byte LF = 0x0A;
044    private static final byte QUOTED_PRINTABLE_LAST_PLAIN = 0x7E;
045    private static final int QUOTED_PRINTABLE_MAX_LINE_LENGTH = 76;
046    private static final int QUOTED_PRINTABLE_OCTETS_PER_ESCAPE = 3;
047    private static final byte[] HEX_DIGITS = {
048        '0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'};
049
050    private final byte[] outBuffer;
051    private final boolean binary;
052
053    private boolean pendingSpace;
054    private boolean pendingTab;
055    private boolean pendingCR;
056    private int nextSoftBreak;
057    private int outputIndex;
058
059    private boolean closed = false;
060
061    private byte[] singleByte = new byte[1];
062
063    public QuotedPrintableOutputStream(int bufsize, OutputStream out, boolean binary) {
064        super(out);
065        this.outBuffer = new byte[bufsize];
066        this.binary = binary;
067        this.pendingSpace = false;
068        this.pendingTab = false;
069        this.pendingCR = false;
070        this.outputIndex = 0;
071        this.nextSoftBreak = QUOTED_PRINTABLE_MAX_LINE_LENGTH + 1;
072    }
073
074    public QuotedPrintableOutputStream(OutputStream out, boolean binary) {
075        this(DEFAULT_BUFFER_SIZE, out, binary);
076    }
077
078    private void encodeChunk(byte[] buffer, int off, int len) throws IOException {
079        for (int inputIndex = off; inputIndex < len + off; inputIndex++) {
080            encode(buffer[inputIndex]);
081        }
082    }
083
084    private void completeEncoding() throws IOException {
085        writePending();
086        flushOutput();
087    }
088
089    private void writePending() throws IOException {
090        if (pendingSpace) {
091            plain(SP);
092        } else if (pendingTab) {
093            plain(TB);
094        } else if (pendingCR) {
095            plain(CR);
096        }
097        clearPending();
098    }
099
100    private void clearPending() throws IOException {
101        pendingSpace  = false;
102        pendingTab = false;
103        pendingCR = false;
104    }
105
106    private void encode(byte next) throws IOException {
107        if (next == LF) {
108            if (binary) {
109                writePending();
110                escape(next);
111            } else {
112                if (pendingCR) {
113                    // Expect either space or tab pending
114                    // but not both
115                    if (pendingSpace) {
116                        escape(SP);
117                    } else if (pendingTab) {
118                        escape(TB);
119                    }
120                    lineBreak();
121                    clearPending();
122                } else {
123                    writePending();
124                    plain(next);
125                }
126            }
127        } else if (next == CR) {
128            if (binary)  {
129                escape(next);
130            } else {
131                pendingCR = true;
132            }
133        } else {
134            writePending();
135            if (next == SP) {
136                if (binary)  {
137                    escape(next);
138                } else {
139                    pendingSpace = true;
140                }
141            } else if (next == TB) {
142                if (binary)  {
143                    escape(next);
144                } else {
145                    pendingTab = true;
146                }
147            } else if (next < SP) {
148                escape(next);
149            } else if (next > QUOTED_PRINTABLE_LAST_PLAIN) {
150                escape(next);
151            } else if (next == EQ || next == DOT) {
152                escape(next);
153            } else {
154                plain(next);
155            }
156        }
157    }
158
159    private void plain(byte next) throws IOException {
160        if (--nextSoftBreak <= 1) {
161            softBreak();
162        }
163        write(next);
164    }
165
166    private void escape(byte next) throws IOException {
167        if (--nextSoftBreak <= QUOTED_PRINTABLE_OCTETS_PER_ESCAPE) {
168            softBreak();
169        }
170
171        int nextUnsigned = next & 0xff;
172
173        write(EQ);
174        --nextSoftBreak;
175        write(HEX_DIGITS[nextUnsigned >> 4]);
176        --nextSoftBreak;
177        write(HEX_DIGITS[nextUnsigned % 0x10]);
178    }
179
180    private void write(byte next) throws IOException {
181        outBuffer[outputIndex++] = next;
182        if (outputIndex >= outBuffer.length) {
183            flushOutput();
184        }
185    }
186
187    private void softBreak() throws IOException {
188        write(EQ);
189        lineBreak();
190    }
191
192    private void lineBreak() throws IOException {
193        write(CR);
194        write(LF);
195        nextSoftBreak = QUOTED_PRINTABLE_MAX_LINE_LENGTH;
196    }
197
198    void flushOutput() throws IOException {
199        if (outputIndex < outBuffer.length) {
200            out.write(outBuffer, 0, outputIndex);
201        } else {
202            out.write(outBuffer);
203        }
204        outputIndex = 0;
205    }
206
207    @Override
208    public void close() throws IOException {
209        if (closed)
210            return;
211
212        try {
213            completeEncoding();
214            // do not close the wrapped stream
215        } finally {
216            closed = true;
217        }
218    }
219
220    @Override
221    public void flush() throws IOException {
222        flushOutput();
223    }
224
225    @Override
226    public void write(int b) throws IOException {
227        singleByte[0] = (byte) b;
228        this.write(singleByte, 0, 1);
229    }
230
231    @Override
232    public void write(byte[] b, int off, int len) throws IOException {
233        if (closed) {
234            throw new IOException("Stream has been closed");
235        }
236        encodeChunk(b, off, len);
237    }
238
239}