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}