4545 * <p>
4646 * Note:
4747 * <p>
48- * Rules #3, #4, and #5 of the quoted-printable spec are not implemented yet because the complete quoted-printable spec
49- * does not lend itself well into the byte[] oriented codec framework. Complete the codec once the streamable codec
50- * framework is ready. The motivation behind providing the codec in a partial form is that it can already come in handy
51- * for those applications that do not require quoted-printable line formatting (rules #3, #4, #5), for instance Q codec.
48+ * Depending on the selected {@code strict} parameter, this class will implement a different set of rules of the
49+ * quoted-printable spec:
50+ * <ul>
51+ * <li>{@code strict=false}: only rules #1 and #2 are implemented
52+ * <li>{@code strict=true}: all rules #1 through #5 are implemented
53+ * </ul>
54+ * Originally, this class only supported the non-strict mode, but the codec in this partial form could already be used
55+ * for certain applications that do not require quoted-printable line formatting (rules #3, #4, #5), for instance Q codec.
56+ * The strict mode has been added in 1.10.
5257 * <p>
5358 * This class is immutable and thread-safe.
5459 *
55- * @see <a href="http://www.ietf.org/rfc/rfc1521.txt"> RFC 1521 MIME (Multipurpose Internet Mail Extensions) Part One:
60+ * @see <a href="http://www.ietf.org/rfc/rfc1521.txt">RFC 1521 MIME (Multipurpose Internet Mail Extensions) Part One:
5661 * Mechanisms for Specifying and Describing the Format of Internet Message Bodies </a>
5762 *
5863 * @since 1.3
@@ -64,6 +69,11 @@ public class QuotedPrintableCodec implements BinaryEncoder, BinaryDecoder, Strin
6469 */
6570 private final Charset charset ;
6671
72+ /**
73+ * Indicates whether soft line breaks shall be used during encoding (rule #3-5).
74+ */
75+ private final boolean strict ;
76+
6777 /**
6878 * BitSet of printable characters as defined in RFC 1521.
6979 */
@@ -74,6 +84,16 @@ public class QuotedPrintableCodec implements BinaryEncoder, BinaryDecoder, Strin
7484 private static final byte TAB = 9 ;
7585
7686 private static final byte SPACE = 32 ;
87+
88+ private static final byte CR = 13 ;
89+
90+ private static final byte LF = 10 ;
91+
92+ /**
93+ * Safe line length for quoted printable encoded text.
94+ */
95+ private static final int SAFE_LENGTH = 73 ;
96+
7797 // Static initializer for printable chars collection
7898 static {
7999 // alpha characters
@@ -91,7 +111,18 @@ public class QuotedPrintableCodec implements BinaryEncoder, BinaryDecoder, Strin
91111 * Default constructor, assumes default charset of {@link Charsets#UTF_8}
92112 */
93113 public QuotedPrintableCodec () {
94- this (Charsets .UTF_8 );
114+ this (Charsets .UTF_8 , false );
115+ }
116+
117+ /**
118+ * Constructor which allows for the selection of the strict mode.
119+ *
120+ * @param strict
121+ * if {@code true}, soft line breaks will be used
122+ * @since 1.10
123+ */
124+ public QuotedPrintableCodec (final boolean strict ) {
125+ this (Charsets .UTF_8 , strict );
95126 }
96127
97128 /**
@@ -102,7 +133,21 @@ public QuotedPrintableCodec() {
102133 * @since 1.7
103134 */
104135 public QuotedPrintableCodec (final Charset charset ) {
136+ this (charset , false );
137+ }
138+
139+ /**
140+ * Constructor which allows for the selection of a default charset and strict mode.
141+ *
142+ * @param charset
143+ * the default string charset to use.
144+ * @param strict
145+ * if {@code true}, soft line breaks will be used
146+ * @since 1.10
147+ */
148+ public QuotedPrintableCodec (final Charset charset , final boolean strict ) {
105149 this .charset = charset ;
150+ this .strict = strict ;
106151 }
107152
108153 /**
@@ -122,7 +167,7 @@ public QuotedPrintableCodec(final Charset charset) {
122167 */
123168 public QuotedPrintableCodec (final String charsetName )
124169 throws IllegalCharsetNameException , IllegalArgumentException , UnsupportedCharsetException {
125- this (Charset .forName (charsetName ));
170+ this (Charset .forName (charsetName ), false );
126171 }
127172
128173 /**
@@ -132,13 +177,65 @@ public QuotedPrintableCodec(final String charsetName)
132177 * byte to encode
133178 * @param buffer
134179 * the buffer to write to
180+ * @return The number of bytes written to the <code>buffer</code>
135181 */
136- private static final void encodeQuotedPrintable (final int b , final ByteArrayOutputStream buffer ) {
182+ private static final int encodeQuotedPrintable (final int b , final ByteArrayOutputStream buffer ) {
137183 buffer .write (ESCAPE_CHAR );
138184 final char hex1 = Character .toUpperCase (Character .forDigit ((b >> 4 ) & 0xF , 16 ));
139185 final char hex2 = Character .toUpperCase (Character .forDigit (b & 0xF , 16 ));
140186 buffer .write (hex1 );
141187 buffer .write (hex2 );
188+ return 3 ;
189+ }
190+
191+ /**
192+ * Return the byte at position <code>index</code> of the byte array and
193+ * make sure it is unsigned.
194+ *
195+ * @param index
196+ * position in the array
197+ * @param bytes
198+ * the byte array
199+ * @return the unsigned octet at position <code>index</code> from the array
200+ */
201+ private static int getUnsignedOctet (final int index , final byte [] bytes ) {
202+ int b = bytes [index ];
203+ if (b < 0 ) {
204+ b = 256 + b ;
205+ }
206+ return b ;
207+ }
208+
209+ /**
210+ * Write a byte to the buffer.
211+ *
212+ * @param b
213+ * byte to write
214+ * @param encode
215+ * indicates whether the octet shall be encoded
216+ * @param buffer
217+ * the buffer to write to
218+ * @return the number of bytes that have been written to the buffer
219+ */
220+ private static int encodeByte (final int b , final boolean encode ,
221+ final ByteArrayOutputStream buffer ) {
222+ if (encode ) {
223+ return encodeQuotedPrintable (b , buffer );
224+ } else {
225+ buffer .write (b );
226+ return 1 ;
227+ }
228+ }
229+
230+ /**
231+ * Checks whether the given byte is whitespace.
232+ *
233+ * @param b
234+ * byte to be checked
235+ * @return <code>true</code> if the byte is either a space or tab character
236+ */
237+ private static boolean isWhitespace (final int b ) {
238+ return b == SPACE || b == TAB ;
142239 }
143240
144241 /**
@@ -154,22 +251,86 @@ private static final void encodeQuotedPrintable(final int b, final ByteArrayOutp
154251 * @return array of bytes containing quoted-printable data
155252 */
156253 public static final byte [] encodeQuotedPrintable (BitSet printable , final byte [] bytes ) {
254+ return encodeQuotedPrintable (printable , bytes , false );
255+ }
256+
257+ /**
258+ * Encodes an array of bytes into an array of quoted-printable 7-bit characters. Unsafe characters are escaped.
259+ * <p>
260+ * Depending on the selection of the {@code strict} parameter, this function either implements the full ruleset
261+ * or only a subset of quoted-printable encoding specification (rule #1 and rule #2) as defined in
262+ * RFC 1521 and is suitable for encoding binary data and unformatted text.
263+ *
264+ * @param printable
265+ * bitset of characters deemed quoted-printable
266+ * @param bytes
267+ * array of bytes to be encoded
268+ * @param strict
269+ * if {@code true} the full ruleset is used, otherwise only rule #1 and rule #2
270+ * @return array of bytes containing quoted-printable data
271+ * @since 1.10
272+ */
273+ public static final byte [] encodeQuotedPrintable (BitSet printable , final byte [] bytes , boolean strict ) {
157274 if (bytes == null ) {
158275 return null ;
159276 }
160277 if (printable == null ) {
161278 printable = PRINTABLE_CHARS ;
162279 }
163280 final ByteArrayOutputStream buffer = new ByteArrayOutputStream ();
164- for (final byte c : bytes ) {
165- int b = c ;
166- if (b < 0 ) {
167- b = 256 + b ;
281+
282+ if (strict ) {
283+ int pos = 1 ;
284+ // encode up to buffer.length - 3, the last three octets will be treated
285+ // separately for simplification of note #3
286+ for (int i = 0 ; i < bytes .length - 3 ; i ++) {
287+ int b = getUnsignedOctet (i , bytes );
288+ if (pos < SAFE_LENGTH ) {
289+ // up to this length it is safe to add any byte, encoded or not
290+ pos += encodeByte (b , !printable .get (b ), buffer );
291+ } else {
292+ // rule #3: whitespace at the end of a line *must* be encoded
293+ encodeByte (b , !printable .get (b ) || isWhitespace (b ), buffer );
294+
295+ // rule #5: soft line break
296+ buffer .write (ESCAPE_CHAR );
297+ buffer .write (CR );
298+ buffer .write (LF );
299+ pos = 1 ;
300+ }
168301 }
169- if (printable .get (b )) {
170- buffer .write (b );
171- } else {
172- encodeQuotedPrintable (b , buffer );
302+
303+ // rule #3: whitespace at the end of a line *must* be encoded
304+ // if we would do a soft break line after this octet, encode whitespace
305+ int b = getUnsignedOctet (bytes .length - 3 , bytes );
306+ boolean encode = !printable .get (b ) || (isWhitespace (b ) && pos > SAFE_LENGTH - 5 );
307+ pos += encodeByte (b , encode , buffer );
308+
309+ // note #3: '=' *must not* be the ultimate or penultimate character
310+ // simplification: if < 6 bytes left, do a soft line break as we may need
311+ // exactly 6 bytes space for the last 2 bytes
312+ if (pos > SAFE_LENGTH - 2 ) {
313+ buffer .write (ESCAPE_CHAR );
314+ buffer .write (CR );
315+ buffer .write (LF );
316+ }
317+ for (int i = bytes .length - 2 ; i < bytes .length ; i ++) {
318+ b = getUnsignedOctet (i , bytes );
319+ // rule #3: trailing whitespace shall be encoded
320+ encode = !printable .get (b ) || (i > bytes .length - 2 && isWhitespace (b ));
321+ encodeByte (b , encode , buffer );
322+ }
323+ } else {
324+ for (final byte c : bytes ) {
325+ int b = c ;
326+ if (b < 0 ) {
327+ b = 256 + b ;
328+ }
329+ if (printable .get (b )) {
330+ buffer .write (b );
331+ } else {
332+ encodeQuotedPrintable (b , buffer );
333+ }
173334 }
174335 }
175336 return buffer .toByteArray ();
@@ -179,8 +340,8 @@ public static final byte[] encodeQuotedPrintable(BitSet printable, final byte[]
179340 * Decodes an array quoted-printable characters into an array of original bytes. Escaped characters are converted
180341 * back to their original representation.
181342 * <p>
182- * This function implements a subset of quoted-printable encoding specification (rule #1 and rule #2 ) as defined in
183- * RFC 1521.
343+ * This function fully implements the quoted-printable encoding specification (rule #1 through rule #5 ) as
344+ * defined in RFC 1521.
184345 *
185346 * @param bytes
186347 * array of quoted-printable characters
@@ -197,13 +358,18 @@ public static final byte[] decodeQuotedPrintable(final byte[] bytes) throws Deco
197358 final int b = bytes [i ];
198359 if (b == ESCAPE_CHAR ) {
199360 try {
200- final int u = Utils .digit16 (bytes [++i ]);
361+ // if the next octet is a CR we have found a soft line break
362+ if (bytes [++i ] == CR ) {
363+ continue ;
364+ }
365+ final int u = Utils .digit16 (bytes [i ]);
201366 final int l = Utils .digit16 (bytes [++i ]);
202367 buffer .write ((char ) ((u << 4 ) + l ));
203368 } catch (final ArrayIndexOutOfBoundsException e ) {
204369 throw new DecoderException ("Invalid quoted-printable encoding" , e );
205370 }
206- } else {
371+ } else if (b != CR && b != LF ) {
372+ // every other octet is appended except for CR & LF
207373 buffer .write (b );
208374 }
209375 }
@@ -213,7 +379,8 @@ public static final byte[] decodeQuotedPrintable(final byte[] bytes) throws Deco
213379 /**
214380 * Encodes an array of bytes into an array of quoted-printable 7-bit characters. Unsafe characters are escaped.
215381 * <p>
216- * This function implements a subset of quoted-printable encoding specification (rule #1 and rule #2) as defined in
382+ * Depending on the selection of the {@code strict} parameter, this function either implements the full ruleset
383+ * or only a subset of quoted-printable encoding specification (rule #1 and rule #2) as defined in
217384 * RFC 1521 and is suitable for encoding binary data and unformatted text.
218385 *
219386 * @param bytes
@@ -222,15 +389,15 @@ public static final byte[] decodeQuotedPrintable(final byte[] bytes) throws Deco
222389 */
223390 @ Override
224391 public byte [] encode (final byte [] bytes ) {
225- return encodeQuotedPrintable (PRINTABLE_CHARS , bytes );
392+ return encodeQuotedPrintable (PRINTABLE_CHARS , bytes , strict );
226393 }
227394
228395 /**
229396 * Decodes an array of quoted-printable characters into an array of original bytes. Escaped characters are converted
230397 * back to their original representation.
231398 * <p>
232- * This function implements a subset of quoted-printable encoding specification (rule #1 and rule #2 ) as defined in
233- * RFC 1521.
399+ * This function fully implements the quoted-printable encoding specification (rule #1 through rule #5 ) as
400+ * defined in RFC 1521.
234401 *
235402 * @param bytes
236403 * array of quoted-printable characters
@@ -246,8 +413,9 @@ public byte[] decode(final byte[] bytes) throws DecoderException {
246413 /**
247414 * Encodes a string into its quoted-printable form using the default string charset. Unsafe characters are escaped.
248415 * <p>
249- * This function implements a subset of quoted-printable encoding specification (rule #1 and rule #2) as defined in
250- * RFC 1521 and is suitable for encoding binary data.
416+ * Depending on the selection of the {@code strict} parameter, this function either implements the full ruleset
417+ * or only a subset of quoted-printable encoding specification (rule #1 and rule #2) as defined in
418+ * RFC 1521 and is suitable for encoding binary data and unformatted text.
251419 *
252420 * @param str
253421 * string to convert to quoted-printable form
@@ -392,7 +560,8 @@ public String getDefaultCharset() {
392560 /**
393561 * Encodes a string into its quoted-printable form using the specified charset. Unsafe characters are escaped.
394562 * <p>
395- * This function implements a subset of quoted-printable encoding specification (rule #1 and rule #2) as defined in
563+ * Depending on the selection of the {@code strict} parameter, this function either implements the full ruleset
564+ * or only a subset of quoted-printable encoding specification (rule #1 and rule #2) as defined in
396565 * RFC 1521 and is suitable for encoding binary data and unformatted text.
397566 *
398567 * @param str
@@ -412,7 +581,8 @@ public String encode(final String str, final Charset charset) {
412581 /**
413582 * Encodes a string into its quoted-printable form using the specified charset. Unsafe characters are escaped.
414583 * <p>
415- * This function implements a subset of quoted-printable encoding specification (rule #1 and rule #2) as defined in
584+ * Depending on the selection of the {@code strict} parameter, this function either implements the full ruleset
585+ * or only a subset of quoted-printable encoding specification (rule #1 and rule #2) as defined in
416586 * RFC 1521 and is suitable for encoding binary data and unformatted text.
417587 *
418588 * @param str
0 commit comments