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    
020    package org.apache.james.mime4j.codec;
021    
022    import java.io.FilterOutputStream;
023    import java.io.IOException;
024    import 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     */
034    public 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    }