001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * https://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017 018package org.apache.commons.codec.net; 019 020import java.io.ByteArrayOutputStream; 021import java.io.UnsupportedEncodingException; 022import java.nio.charset.Charset; 023import java.nio.charset.IllegalCharsetNameException; 024import java.nio.charset.StandardCharsets; 025import java.nio.charset.UnsupportedCharsetException; 026import java.util.BitSet; 027 028import org.apache.commons.codec.BinaryDecoder; 029import org.apache.commons.codec.BinaryEncoder; 030import org.apache.commons.codec.DecoderException; 031import org.apache.commons.codec.EncoderException; 032import org.apache.commons.codec.StringDecoder; 033import org.apache.commons.codec.StringEncoder; 034import org.apache.commons.codec.binary.StringUtils; 035 036/** 037 * Codec for the Quoted-Printable section of <a href="https://www.ietf.org/rfc/rfc1521.txt">RFC 1521</a>. 038 * <p> 039 * The Quoted-Printable encoding is intended to represent data that largely consists of octets that correspond to printable characters in the ASCII character 040 * set. It encodes the data in such a way that the resulting octets are unlikely to be modified by mail transport. If the data being encoded are mostly ASCII 041 * text, the encoded form of the data remains largely recognizable by humans. A body which is entirely ASCII may also be encoded in Quoted-Printable to ensure 042 * the integrity of the data should the message pass through a character- translating, and/or line-wrapping gateway. 043 * </p> 044 * <p> 045 * Note: 046 * </p> 047 * <p> 048 * Depending on the selected {@code strict} parameter, encoding implements a different set of rules of the quoted-printable spec: 049 * </p> 050 * <ul> 051 * <li>{@code strict=false}: only rules #1 and #2 are implemented</li> 052 * <li>{@code strict=true}: all rules #1 through #5 are implemented</li> 053 * </ul> 054 * <p> 055 * Originally, this class only supported the non-strict mode, but the codec in this partial form could already be used for certain applications that do not 056 * require quoted-printable line formatting (rules #3, #4, #5), for instance Q codec. The strict mode has been added in 1.10. 057 * Decoding is independent of this parameter; see {@link #decodeQuotedPrintable(byte[])} for its behavior. 058 * </p> 059 * <p> 060 * This class is immutable and thread-safe. 061 * </p> 062 * 063 * @see <a href="https://www.ietf.org/rfc/rfc1521.txt">RFC 1521 MIME (Multipurpose Internet Mail Extensions) Part One: Mechanisms for Specifying and Describing 064 * the Format of Internet Message Bodies </a> 065 * 066 * @since 1.3 067 */ 068public class QuotedPrintableCodec implements BinaryEncoder, BinaryDecoder, StringEncoder, StringDecoder { 069 070 /** 071 * BitSet of printable characters as defined in RFC 1521. 072 */ 073 private static final BitSet PRINTABLE_CHARS = new BitSet(256); 074 private static final byte ESCAPE_CHAR = '='; 075 private static final byte TAB = 9; 076 private static final byte CR = 13; 077 private static final byte LF = 10; 078 079 /** 080 * Minimum length required for the byte arrays used by encodeQuotedPrintable method. 081 */ 082 private static final int MIN_BYTES = 3; 083 084 /** 085 * Safe line length for quoted printable encoded text. 086 */ 087 private static final int SAFE_LENGTH = 73; 088 089 // Static initializer for printable chars collection 090 static { 091 // alpha characters 092 for (int i = 33; i <= 60; i++) { 093 PRINTABLE_CHARS.set(i); 094 } 095 for (int i = 62; i <= 126; i++) { 096 PRINTABLE_CHARS.set(i); 097 } 098 PRINTABLE_CHARS.set(TAB); 099 PRINTABLE_CHARS.set(Utils.SPACE); 100 } 101 102 /** 103 * Decodes quoted-printable bytes. 104 * 105 * <p> 106 * Converts hexadecimal escapes to their original bytes, removes soft line breaks ({@code =CRLF}), and preserves hard CRLF line breaks. 107 * </p> 108 * 109 * <p> 110 * As a lenient extension for malformed input, unpaired CR and LF bytes are also preserved. An equals sign followed by CR without LF is rejected. 111 * This method does not perform full MIME validation: for example, it neither removes trailing whitespace nor handles transport padding after an 112 * equals sign. The {@code strict} constructor parameter affects encoding only. 113 * </p> 114 * 115 * <p> 116 * Since 1.23.0, unescaped CR and LF bytes are preserved and {@code =CR} without a following LF is rejected. Earlier versions discarded unescaped 117 * CR and LF bytes and accepted {@code =CR} as a soft line break. 118 * </p> 119 * 120 * @param bytes array of quoted-printable characters. 121 * @return array of original bytes, or {@code null} if the input is {@code null}. 122 * @throws DecoderException Thrown if an escape is incomplete or invalid, including a soft line break without the full CRLF pair. 123 */ 124 public static final byte[] decodeQuotedPrintable(final byte[] bytes) throws DecoderException { 125 if (bytes == null) { 126 return null; 127 } 128 final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); 129 for (int i = 0; i < bytes.length; i++) { 130 final int b = bytes[i]; 131 if (b == ESCAPE_CHAR) { 132 try { 133 // rule #5: a soft line break is the escape character followed by a CRLF sequence; 134 // it is removed entirely from the decoded output 135 if (bytes[++i] == CR) { 136 if (++i >= bytes.length || bytes[i] != LF) { 137 throw new DecoderException("Invalid quoted-printable encoding: soft line break must be =CRLF"); 138 } 139 continue; 140 } 141 final int u = Utils.digit16(bytes[i]); 142 final int l = Utils.digit16(bytes[++i]); 143 buffer.write((char) ((u << 4) + l)); 144 } catch (final ArrayIndexOutOfBoundsException e) { 145 throw new DecoderException("Invalid quoted-printable encoding", e); 146 } 147 } else { 148 // Preserve hard line breaks and, leniently, unpaired CR and LF bytes. 149 buffer.write(b); 150 } 151 } 152 return buffer.toByteArray(); 153 } 154 155 /** 156 * Encodes a byte in the buffer. 157 * 158 * @param b byte to write. 159 * @param encode indicates whether the octet shall be encoded. 160 * @param buffer The buffer to write to. 161 * @return The number of bytes that have been written to the buffer. 162 */ 163 private static int encodeByte(final int b, final boolean encode, final ByteArrayOutputStream buffer) { 164 if (encode) { 165 return encodeQuotedPrintable(b, buffer); 166 } 167 buffer.write(b); 168 return 1; 169 } 170 171 /** 172 * Encodes an array of bytes into an array of quoted-printable 7-bit characters. Unsafe characters are escaped. 173 * <p> 174 * This function implements a subset of quoted-printable encoding specification (rule #1 and rule #2) as defined in RFC 1521 and is suitable for encoding 175 * binary data and unformatted text. 176 * </p> 177 * 178 * @param printable bitset of characters deemed quoted-printable. 179 * @param bytes array of bytes to be encoded. 180 * @return array of bytes containing quoted-printable data. 181 */ 182 public static final byte[] encodeQuotedPrintable(final BitSet printable, final byte[] bytes) { 183 return encodeQuotedPrintable(printable, bytes, false); 184 } 185 186 /** 187 * Encodes an array of bytes into an array of quoted-printable 7-bit characters. Unsafe characters are escaped. 188 * <p> 189 * Depending on the selection of the {@code strict} parameter, this function either implements the full ruleset or only a subset of quoted-printable 190 * encoding specification (rule #1 and rule #2) as defined in RFC 1521 and is suitable for encoding binary data and unformatted text. 191 * </p> 192 * 193 * @param printable bitset of characters deemed quoted-printable. 194 * @param bytes array of bytes to be encoded. 195 * @param strict if {@code true} the full ruleset is used, otherwise only rule #1 and rule #2. 196 * @return array of bytes containing quoted-printable data. 197 * @since 1.10 198 */ 199 public static final byte[] encodeQuotedPrintable(BitSet printable, final byte[] bytes, final boolean strict) { 200 if (bytes == null) { 201 return null; 202 } 203 if (printable == null) { 204 printable = PRINTABLE_CHARS; 205 } 206 final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); 207 final int bytesLength = bytes.length; 208 if (strict) { 209 if (bytesLength < MIN_BYTES) { 210 return null; 211 } 212 int pos = 1; 213 // encode up to buffer.length - 3, the last three octets will be treated 214 // separately for simplification of note #3 215 for (int i = 0; i < bytesLength - 3; i++) { 216 final int b = getUnsignedOctet(i, bytes); 217 if (pos < SAFE_LENGTH) { 218 // up to this length it is safe to add any byte, encoded or not 219 pos += encodeByte(b, !printable.get(b), buffer); 220 } else { 221 // rule #3: whitespace at the end of a line *must* be encoded 222 encodeByte(b, !printable.get(b) || isWhitespace(b), buffer); 223 // rule #5: soft line break 224 buffer.write(ESCAPE_CHAR); 225 buffer.write(CR); 226 buffer.write(LF); 227 pos = 1; 228 } 229 } 230 // rule #3: whitespace at the end of a line *must* be encoded 231 // if we would do a soft break line after this octet, encode whitespace 232 int b = getUnsignedOctet(bytesLength - 3, bytes); 233 boolean encode = !printable.get(b) || isWhitespace(b) && pos > SAFE_LENGTH - 5; 234 pos += encodeByte(b, encode, buffer); 235 // note #3: '=' *must not* be the ultimate or penultimate character 236 // simplification: if < 6 bytes left, do a soft line break as we may need 237 // exactly 6 bytes space for the last 2 bytes 238 if (pos > SAFE_LENGTH - 2) { 239 buffer.write(ESCAPE_CHAR); 240 buffer.write(CR); 241 buffer.write(LF); 242 } 243 for (int i = bytesLength - 2; i < bytesLength; i++) { 244 b = getUnsignedOctet(i, bytes); 245 // rule #3: trailing whitespace shall be encoded 246 encode = !printable.get(b) || i > bytesLength - 2 && isWhitespace(b); 247 encodeByte(b, encode, buffer); 248 } 249 } else { 250 for (final byte c : bytes) { 251 int b = c; 252 if (b < 0) { 253 b = 256 + b; 254 } 255 if (printable.get(b)) { 256 buffer.write(b); 257 } else { 258 encodeQuotedPrintable(b, buffer); 259 } 260 } 261 } 262 return buffer.toByteArray(); 263 } 264 265 /** 266 * Encodes byte into its quoted-printable representation. 267 * 268 * @param b byte to encode. 269 * @param buffer The buffer to write to. 270 * @return The number of bytes written to the {@code buffer}. 271 */ 272 private static int encodeQuotedPrintable(final int b, final ByteArrayOutputStream buffer) { 273 buffer.write(ESCAPE_CHAR); 274 final char hex1 = Utils.hexChar(b >> 4); 275 final char hex2 = Utils.hexChar(b); 276 buffer.write(hex1); 277 buffer.write(hex2); 278 return 3; 279 } 280 281 /** 282 * Gets the byte at position {@code index} of the byte array and makes sure it is unsigned. 283 * 284 * @param index position in the array. 285 * @param bytes The byte array. 286 * @return The unsigned octet at position {@code index} from the array. 287 */ 288 private static int getUnsignedOctet(final int index, final byte[] bytes) { 289 int b = bytes[index]; 290 if (b < 0) { 291 b = 256 + b; 292 } 293 return b; 294 } 295 296 /** 297 * Tests whether the given byte is whitespace. 298 * 299 * @param b byte to be checked. 300 * @return {@code true} if the byte is either a space or tab character. 301 */ 302 private static boolean isWhitespace(final int b) { 303 return b == Utils.SPACE || b == TAB; 304 } 305 306 /** 307 * The default Charset used for string decoding and encoding. 308 */ 309 private final Charset charset; 310 311 /** 312 * Indicates whether soft line breaks shall be used during encoding (rule #3-5). 313 */ 314 private final boolean strict; 315 316 /** 317 * Constructs a new instance, assumes default Charset of {@link StandardCharsets#UTF_8} 318 */ 319 public QuotedPrintableCodec() { 320 this(StandardCharsets.UTF_8, false); 321 } 322 323 /** 324 * Constructs a new instance for the selection of the strict mode. 325 * 326 * @param strict if {@code true}, soft line breaks will be used. 327 * @since 1.10 328 */ 329 public QuotedPrintableCodec(final boolean strict) { 330 this(StandardCharsets.UTF_8, strict); 331 } 332 333 /** 334 * Constructs a new instance for the selection of a default Charset. 335 * 336 * @param charset The default string Charset to use. 337 * @since 1.7 338 */ 339 public QuotedPrintableCodec(final Charset charset) { 340 this(charset, false); 341 } 342 343 /** 344 * Constructs a new instance for the selection of a default Charset and strict mode. 345 * 346 * @param charset The default string Charset to use. 347 * @param strict if {@code true}, soft line breaks will be used. 348 * @since 1.10 349 */ 350 public QuotedPrintableCodec(final Charset charset, final boolean strict) { 351 this.charset = charset; 352 this.strict = strict; 353 } 354 355 /** 356 * Constructs a new instance for the selection of a default Charset. 357 * 358 * @param charsetName The default string Charset to use. 359 * @throws UnsupportedCharsetException Thrown if no support for the named Charset is available in this instance of the Java virtual machine. 360 * @throws IllegalArgumentException Thrown if the given charsetName is null. 361 * @throws IllegalCharsetNameException Thrown if the given Charset name is illegal. 362 * 363 * @since 1.7 throws UnsupportedCharsetException if the named Charset is unavailable 364 */ 365 public QuotedPrintableCodec(final String charsetName) throws IllegalCharsetNameException, IllegalArgumentException, UnsupportedCharsetException { 366 this(Charset.forName(charsetName), false); 367 } 368 369 /** 370 * Decodes an array of quoted-printable characters into an array of original bytes. Escaped characters are converted back to their original representation. 371 * <p> 372 * This function fully implements the quoted-printable encoding specification (rule #1 through rule #5) as defined in RFC 1521. 373 * </p> 374 * 375 * @param bytes array of quoted-printable characters. 376 * @return array of original bytes. 377 * @throws DecoderException Thrown if quoted-printable decoding is unsuccessful. 378 */ 379 @Override 380 public byte[] decode(final byte[] bytes) throws DecoderException { 381 return decodeQuotedPrintable(bytes); 382 } 383 384 /** 385 * Decodes a quoted-printable object into its original form. Escaped characters are converted back to their original representation. 386 * 387 * @param obj quoted-printable object to convert into its original form. 388 * @return original object. 389 * @throws DecoderException Thrown if the argument is not a {@code String} or {@code byte[]}. Thrown if a failure condition is encountered during the decode 390 * process. 391 */ 392 @Override 393 public Object decode(final Object obj) throws DecoderException { 394 if (obj == null) { 395 return null; 396 } 397 if (obj instanceof byte[]) { 398 return decode((byte[]) obj); 399 } 400 if (obj instanceof String) { 401 return decode((String) obj); 402 } 403 throw new DecoderException("Objects of type " + obj.getClass().getName() + " cannot be quoted-printable decoded"); 404 } 405 406 /** 407 * Decodes a quoted-printable string into its original form using the default string Charset. Escaped characters are converted back to their original 408 * representation. 409 * 410 * @param sourceStr quoted-printable string to convert into its original form. 411 * @return original string. 412 * @throws DecoderException Thrown if quoted-printable decoding is unsuccessful. Thrown if Charset is not supported. 413 * @see #getCharset() 414 */ 415 @Override 416 public String decode(final String sourceStr) throws DecoderException { 417 return this.decode(sourceStr, getCharset()); 418 } 419 420 /** 421 * Decodes a quoted-printable string into its original form using the specified string Charset. Escaped characters are converted back to their original 422 * representation. 423 * 424 * @param sourceStr quoted-printable string to convert into its original form. 425 * @param sourceCharset The original string Charset. 426 * @return original string. 427 * @throws DecoderException Thrown if quoted-printable decoding is unsuccessful. 428 * @since 1.7 429 */ 430 public String decode(final String sourceStr, final Charset sourceCharset) throws DecoderException { 431 if (sourceStr == null) { 432 return null; 433 } 434 return new String(this.decode(StringUtils.getBytesUsAscii(sourceStr)), sourceCharset); 435 } 436 437 /** 438 * Decodes a quoted-printable string into its original form using the specified string Charset. Escaped characters are converted back to their original 439 * representation. 440 * 441 * @param sourceStr quoted-printable string to convert into its original form. 442 * @param sourceCharset The original string Charset. 443 * @return original string. 444 * @throws DecoderException Thrown if quoted-printable decoding is unsuccessful. 445 * @throws UnsupportedEncodingException Thrown if Charset is not supported. 446 */ 447 public String decode(final String sourceStr, final String sourceCharset) throws DecoderException, UnsupportedEncodingException { 448 if (sourceStr == null) { 449 return null; 450 } 451 return new String(decode(StringUtils.getBytesUsAscii(sourceStr)), sourceCharset); 452 } 453 454 /** 455 * Encodes an array of bytes into an array of quoted-printable 7-bit characters. Unsafe characters are escaped. 456 * <p> 457 * Depending on the selection of the {@code strict} parameter, this function either implements the full ruleset or only a subset of quoted-printable 458 * encoding specification (rule #1 and rule #2) as defined in RFC 1521 and is suitable for encoding binary data and unformatted text. 459 * </p> 460 * 461 * @param bytes array of bytes to be encoded. 462 * @return array of bytes containing quoted-printable data. 463 */ 464 @Override 465 public byte[] encode(final byte[] bytes) { 466 return encodeQuotedPrintable(PRINTABLE_CHARS, bytes, strict); 467 } 468 469 /** 470 * Encodes an object into its quoted-printable safe form. Unsafe characters are escaped. 471 * 472 * @param obj string to convert to a quoted-printable form. 473 * @return quoted-printable object. 474 * @throws EncoderException Thrown if quoted-printable encoding is not applicable to objects of this type or if encoding is unsuccessful. 475 */ 476 @Override 477 public Object encode(final Object obj) throws EncoderException { 478 if (obj == null) { 479 return null; 480 } 481 if (obj instanceof byte[]) { 482 return encode((byte[]) obj); 483 } 484 if (obj instanceof String) { 485 return encode((String) obj); 486 } 487 throw new EncoderException("Objects of type " + obj.getClass().getName() + " cannot be quoted-printable encoded"); 488 } 489 490 /** 491 * Encodes a string into its quoted-printable form using the default string Charset. Unsafe characters are escaped. 492 * <p> 493 * Depending on the selection of the {@code strict} parameter, this function either implements the full ruleset or only a subset of quoted-printable 494 * encoding specification (rule #1 and rule #2) as defined in RFC 1521 and is suitable for encoding binary data and unformatted text. 495 * </p> 496 * 497 * @param sourceStr string to convert to quoted-printable form. 498 * @return quoted-printable string. 499 * @throws EncoderException Thrown if quoted-printable encoding is unsuccessful. 500 * 501 * @see #getCharset() 502 */ 503 @Override 504 public String encode(final String sourceStr) throws EncoderException { 505 return encode(sourceStr, getCharset()); 506 } 507 508 /** 509 * Encodes a string into its quoted-printable form using the specified Charset. Unsafe characters are escaped. 510 * <p> 511 * Depending on the selection of the {@code strict} parameter, this function either implements the full ruleset or only a subset of quoted-printable 512 * encoding specification (rule #1 and rule #2) as defined in RFC 1521 and is suitable for encoding binary data and unformatted text. 513 * </p> 514 * 515 * @param sourceStr string to convert to quoted-printable form. 516 * @param sourceCharset The Charset for sourceStr. 517 * @return quoted-printable string. 518 * @since 1.7 519 */ 520 public String encode(final String sourceStr, final Charset sourceCharset) { 521 if (sourceStr == null) { 522 return null; 523 } 524 return StringUtils.newStringUsAscii(this.encode(sourceStr.getBytes(sourceCharset))); 525 } 526 527 /** 528 * Encodes a string into its quoted-printable form using the specified Charset. Unsafe characters are escaped. 529 * <p> 530 * Depending on the selection of the {@code strict} parameter, this function either implements the full ruleset or only a subset of quoted-printable 531 * encoding specification (rule #1 and rule #2) as defined in RFC 1521 and is suitable for encoding binary data and unformatted text. 532 * </p> 533 * 534 * @param sourceStr string to convert to quoted-printable form. 535 * @param sourceCharset The Charset for sourceStr. 536 * @return quoted-printable string. 537 * @throws UnsupportedEncodingException Thrown if the Charset is not supported. 538 */ 539 public String encode(final String sourceStr, final String sourceCharset) throws UnsupportedEncodingException { 540 if (sourceStr == null) { 541 return null; 542 } 543 return StringUtils.newStringUsAscii(encode(sourceStr.getBytes(sourceCharset))); 544 } 545 546 /** 547 * Gets the default Charset name used for string decoding and encoding. 548 * 549 * @return The default Charset name. 550 * @since 1.7 551 */ 552 public Charset getCharset() { 553 return this.charset; 554 } 555 556 /** 557 * Gets the default Charset name used for string decoding and encoding. 558 * 559 * @return The default Charset name. 560 */ 561 public String getDefaultCharset() { 562 return this.charset.name(); 563 } 564}