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 }