    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 *      http://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="http://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
    040 * printable characters in the ASCII character set. It encodes the data in such a way that the resulting octets are
    041 * unlikely to be modified by mail transport. If the data being encoded are mostly ASCII text, the encoded form of the
    042 * data remains largely recognizable by humans. A body which is entirely ASCII may also be encoded in Quoted-Printable
    043 * to ensure the integrity of the data should the message pass through a character- translating, and/or line-wrapping
    044 * gateway.
    045 * </p>
    046 * <p>
    047 * Note:
    048 * </p>
    049 * <p>
    050 * Depending on the selected {@code strict} parameter, this class will implement a different set of rules of the
    051 * quoted-printable spec:
    052 * </p>
    053 * <ul>
    054 *   <li>{@code strict=false}: only rules #1 and #2 are implemented</li>
    055 *   <li>{@code strict=true}: all rules #1 through #5 are implemented</li>
    056 * </ul>
    057 * <p>
    058 * Originally, this class only supported the non-strict mode, but the codec in this partial form could already be used
    059 * for certain applications that do not require quoted-printable line formatting (rules #3, #4, #5), for instance
    060 * Q codec. The strict mode has been added in 1.10.
    061 * </p>
    062 * <p>
    063 * This class is immutable and thread-safe.
    064 * </p>
    065 *
    066 * @see <a href="http://www.ietf.org/rfc/rfc1521.txt">RFC 1521 MIME (Multipurpose Internet Mail Extensions) Part One:
    067 *          Mechanisms for Specifying and Describing the Format of Internet Message Bodies </a>
    068 *
    069 * @since 1.3
    070 */
    071public class QuotedPrintableCodec implements BinaryEncoder, BinaryDecoder, StringEncoder, StringDecoder {
    072    /**
    073     * The default Charset used for string decoding and encoding.
    074     */
    075    private final Charset charset;
    076
    077    /**
    078     * Indicates whether soft line breaks shall be used during encoding (rule #3-5).
    079     */
    080    private final boolean strict;
    081
    082    /**
    083     * BitSet of printable characters as defined in RFC 1521.
    084     */
    085    private static final BitSet PRINTABLE_CHARS = new BitSet(256);
    086
    087    private static final byte ESCAPE_CHAR = '=';
    088
    089    private static final byte TAB = 9;
    090
    091    private static final byte SPACE = 32;
    092
    093    private static final byte CR = 13;
    094
    095    private static final byte LF = 10;
    096
    097    /**
    098     * Safe line length for quoted printable encoded text.
    099     */
    100    private static final int SAFE_LENGTH = 73;
    101
    102    // Static initializer for printable chars collection
    103    static {
    104        // alpha characters
    105        for (int i = 33; i <= 60; i++) {
    106            PRINTABLE_CHARS.set(i);
    107        }
    108        for (int i = 62; i <= 126; i++) {
    109            PRINTABLE_CHARS.set(i);
    110        }
    111        PRINTABLE_CHARS.set(TAB);
    112        PRINTABLE_CHARS.set(SPACE);
    113    }
    114
    115    /**
    116     * Default constructor, assumes default Charset of {@link StandardCharsets#UTF_8}
    117     */
    118    public QuotedPrintableCodec() {
    119        this(StandardCharsets.UTF_8, false);
    120    }
    121
    122    /**
    123     * Constructor which allows for the selection of the strict mode.
    124     *
    125     * @param strict
    126     *            if {@code true}, soft line breaks will be used
    127     * @since 1.10
    128     */
    129    public QuotedPrintableCodec(final boolean strict) {
    130        this(StandardCharsets.UTF_8, strict);
    131    }
    132
    133    /**
    134     * Constructor which allows for the selection of a default Charset.
    135     *
    136     * @param charset
    137     *            the default string Charset to use.
    138     * @since 1.7
    139     */
    140    public QuotedPrintableCodec(final Charset charset) {
    141        this(charset, false);
    142    }
    143
    144    /**
    145     * Constructor which allows for the selection of a default Charset and strict mode.
    146     *
    147     * @param charset
    148     *            the default string Charset to use.
    149     * @param strict
    150     *            if {@code true}, soft line breaks will be used
    151     * @since 1.10
    152     */
    153    public QuotedPrintableCodec(final Charset charset, final boolean strict) {
    154        this.charset = charset;
    155        this.strict = strict;
    156    }
    157
    158    /**
    159     * Constructor which allows for the selection of a default Charset.
    160     *
    161     * @param charsetName
    162     *            the default string Charset to use.
    163     * @throws UnsupportedCharsetException
    164     *             If no support for the named Charset is available
    165     *             in this instance of the Java virtual machine
    166     * @throws IllegalArgumentException
    167     *             If the given charsetName is null
    168     * @throws IllegalCharsetNameException
    169     *             If the given Charset name is illegal
    170     *
    171     * @since 1.7 throws UnsupportedCharsetException if the named Charset is unavailable
    172     */
    173    public QuotedPrintableCodec(final String charsetName)
    174            throws IllegalCharsetNameException, IllegalArgumentException, UnsupportedCharsetException {
    175        this(Charset.forName(charsetName), false);
    176    }
    177
    178    /**
    179     * Encodes byte into its quoted-printable representation.
    180     *
    181     * @param b
    182     *            byte to encode
    183     * @param buffer
    184     *            the buffer to write to
    185     * @return The number of bytes written to the {@code buffer}
    186     */
    187    private static final int encodeQuotedPrintable(final int b, final ByteArrayOutputStream buffer) {
    188        buffer.write(ESCAPE_CHAR);
    189        final char hex1 = Utils.hexDigit(b >> 4);
    190        final char hex2 = Utils.hexDigit(b);
    191        buffer.write(hex1);
    192        buffer.write(hex2);
    193        return 3;
    194    }
    195
    196    /**
    197     * Return the byte at position {@code index} of the byte array and
    198     * make sure it is unsigned.
    199     *
    200     * @param index
    201     *            position in the array
    202     * @param bytes
    203     *            the byte array
    204     * @return the unsigned octet at position {@code index} from the array
    205     */
    206    private static int getUnsignedOctet(final int index, final byte[] bytes) {
    207        int b = bytes[index];
    208        if (b < 0) {
    209            b = 256 + b;
    210        }
    211        return b;
    212    }
    213
    214    /**
    215     * Write a byte to the buffer.
    216     *
    217     * @param b
    218     *            byte to write
    219     * @param encode
    220     *            indicates whether the octet shall be encoded
    221     * @param buffer
    222     *            the buffer to write to
    223     * @return the number of bytes that have been written to the buffer
    224     */
    225    private static int encodeByte(final int b, final boolean encode,
    226                                  final ByteArrayOutputStream buffer) {
    227        if (encode) {
    228            return encodeQuotedPrintable(b, buffer);
    229        }
    230        buffer.write(b);
    231        return 1;
    232    }
    233
    234    /**
    235     * Checks whether the given byte is whitespace.
    236     *
    237     * @param b
    238     *            byte to be checked
    239     * @return {@code true} if the byte is either a space or tab character
    240     */
    241    private static boolean isWhitespace(final int b) {
    242        return b == SPACE || b == TAB;
    243    }
    244
    245    /**
    246     * Encodes an array of bytes into an array of quoted-printable 7-bit characters. Unsafe characters are escaped.
    247     * <p>
    248     * This function implements a subset of quoted-printable encoding specification (rule #1 and rule #2) as defined in
    249     * RFC 1521 and is suitable for encoding binary data and unformatted text.
    250     * </p>
    251     *
    252     * @param printable
    253     *            bitset of characters deemed quoted-printable
    254     * @param bytes
    255     *            array of bytes to be encoded
    256     * @return array of bytes containing quoted-printable data
    257     */
    258    public static final byte[] encodeQuotedPrintable(final BitSet printable, final byte[] bytes) {
    259        return encodeQuotedPrintable(printable, bytes, false);
    260    }
    261
    262    /**
    263     * Encodes an array of bytes into an array of quoted-printable 7-bit characters. Unsafe characters are escaped.
    264     * <p>
    265     * Depending on the selection of the {@code strict} parameter, this function either implements the full ruleset
    266     * or only a subset of quoted-printable encoding specification (rule #1 and rule #2) as defined in
    267     * RFC 1521 and is suitable for encoding binary data and unformatted text.
    268     * </p>
    269     *
    270     * @param printable
    271     *            bitset of characters deemed quoted-printable
    272     * @param bytes
    273     *            array of bytes to be encoded
    274     * @param strict
    275     *            if {@code true} the full ruleset is used, otherwise only rule #1 and rule #2
    276     * @return array of bytes containing quoted-printable data
    277     * @since 1.10
    278     */
    279    public static final byte[] encodeQuotedPrintable(BitSet printable, final byte[] bytes, final boolean strict) {
    280        if (bytes == null) {
    281            return null;
    282        }
    283        if (printable == null) {
    284            printable = PRINTABLE_CHARS;
    285        }
    286        final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
    287        final int bytesLength = bytes.length;
    288
    289        if (strict) {
    290            int pos = 1;
    291            // encode up to buffer.length - 3, the last three octets will be treated
    292            // separately for simplification of note #3
    293            for (int i = 0; i < bytesLength - 3; i++) {
    294                final int b = getUnsignedOctet(i, bytes);
    295                if (pos < SAFE_LENGTH) {
    296                    // up to this length it is safe to add any byte, encoded or not
    297                    pos += encodeByte(b, !printable.get(b), buffer);
    298                } else {
    299                    // rule #3: whitespace at the end of a line *must* be encoded
    300                    encodeByte(b, !printable.get(b) || isWhitespace(b), buffer);
    301
    302                    // rule #5: soft line break
    303                    buffer.write(ESCAPE_CHAR);
    304                    buffer.write(CR);
    305                    buffer.write(LF);
    306                    pos = 1;
    307                }
    308            }
    309
    310            // rule #3: whitespace at the end of a line *must* be encoded
    311            // if we would do a soft break line after this octet, encode whitespace
    312            int b = getUnsignedOctet(bytesLength - 3, bytes);
    313            boolean encode = !printable.get(b) || (isWhitespace(b) && pos > SAFE_LENGTH - 5);
    314            pos += encodeByte(b, encode, buffer);
    315
    316            // note #3: '=' *must not* be the ultimate or penultimate character
    317            // simplification: if < 6 bytes left, do a soft line break as we may need
    318            //                 exactly 6 bytes space for the last 2 bytes
    319            if (pos > SAFE_LENGTH - 2) {
    320                buffer.write(ESCAPE_CHAR);
    321                buffer.write(CR);
    322                buffer.write(LF);
    323            }
    324            for (int i = bytesLength - 2; i < bytesLength; i++) {
    325                b = getUnsignedOctet(i, bytes);
    326                // rule #3: trailing whitespace shall be encoded
    327                encode = !printable.get(b) || (i > bytesLength - 2 && isWhitespace(b));
    328                encodeByte(b, encode, buffer);
    329            }
    330        } else {
    331            for (final byte c : bytes) {
    332                int b = c;
    333                if (b < 0) {
    334                    b = 256 + b;
    335                }
    336                if (printable.get(b)) {
    337                    buffer.write(b);
    338                } else {
    339                    encodeQuotedPrintable(b, buffer);
    340                }
    341            }
    342        }
    343        return buffer.toByteArray();
    344    }
    345
    346    /**
    347     * Decodes an array quoted-printable characters into an array of original bytes. Escaped characters are converted
    348     * back to their original representation.
    349     * <p>
    350     * This function fully implements the quoted-printable encoding specification (rule #1 through rule #5) as
    351     * defined in RFC 1521.
    352     * </p>
    353     *
    354     * @param bytes
    355     *            array of quoted-printable characters
    356     * @return array of original bytes
    357     * @throws DecoderException
    358     *             Thrown if quoted-printable decoding is unsuccessful
    359     */
    360    public static final byte[] decodeQuotedPrintable(final byte[] bytes) throws DecoderException {
    361        if (bytes == null) {
    362            return null;
    363        }
    364        final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
    365        for (int i = 0; i < bytes.length; i++) {
    366            final int b = bytes[i];
    367            if (b == ESCAPE_CHAR) {
    368                try {
    369                    // if the next octet is a CR we have found a soft line break
    370                    if (bytes[++i] == CR) {
    371                        continue;
    372                    }
    373                    final int u = Utils.digit16(bytes[i]);
    374                    final int l = Utils.digit16(bytes[++i]);
    375                    buffer.write((char) ((u << 4) + l));
    376                } catch (final ArrayIndexOutOfBoundsException e) {
    377                    throw new DecoderException("Invalid quoted-printable encoding", e);
    378                }
    379            } else if (b != CR && b != LF) {
    380                // every other octet is appended except for CR & LF
    381                buffer.write(b);
    382            }
    383        }
    384        return buffer.toByteArray();
    385    }
    386
    387    /**
    388     * Encodes an array of bytes into an array of quoted-printable 7-bit characters. Unsafe characters are escaped.
    389     * <p>
    390     * Depending on the selection of the {@code strict} parameter, this function either implements the full ruleset
    391     * or only a subset of quoted-printable encoding specification (rule #1 and rule #2) as defined in
    392     * RFC 1521 and is suitable for encoding binary data and unformatted text.
    393     * </p>
    394     *
    395     * @param bytes
    396     *            array of bytes to be encoded
    397     * @return array of bytes containing quoted-printable data
    398     */
    399    @Override
    400    public byte[] encode(final byte[] bytes) {
    401        return encodeQuotedPrintable(PRINTABLE_CHARS, bytes, strict);
    402    }
    403
    404    /**
    405     * Decodes an array of quoted-printable characters into an array of original bytes. Escaped characters are converted
    406     * back to their original representation.
    407     * <p>
    408     * This function fully implements the quoted-printable encoding specification (rule #1 through rule #5) as
    409     * defined in RFC 1521.
    410     * </p>
    411     *
    412     * @param bytes
    413     *            array of quoted-printable characters
    414     * @return array of original bytes
    415     * @throws DecoderException
    416     *             Thrown if quoted-printable decoding is unsuccessful
    417     */
    418    @Override
    419    public byte[] decode(final byte[] bytes) throws DecoderException {
    420        return decodeQuotedPrintable(bytes);
    421    }
    422
    423    /**
    424     * Encodes a string into its quoted-printable form using the default string Charset. Unsafe characters are escaped.
    425     * <p>
    426     * Depending on the selection of the {@code strict} parameter, this function either implements the full ruleset
    427     * or only a subset of quoted-printable encoding specification (rule #1 and rule #2) as defined in
    428     * RFC 1521 and is suitable for encoding binary data and unformatted text.
    429     * </p>
    430     *
    431     * @param sourceStr
    432     *            string to convert to quoted-printable form
    433     * @return quoted-printable string
    434     * @throws EncoderException
    435     *             Thrown if quoted-printable encoding is unsuccessful
    436     *
    437     * @see #getCharset()
    438     */
    439    @Override
    440    public String encode(final String sourceStr) throws EncoderException {
    441        return this.encode(sourceStr, getCharset());
    442    }
    443
    444    /**
    445     * Decodes a quoted-printable string into its original form using the specified string Charset. Escaped characters
    446     * are converted back to their original representation.
    447     *
    448     * @param sourceStr
    449     *            quoted-printable string to convert into its original form
    450     * @param sourceCharset
    451     *            the original string Charset
    452     * @return original string
    453     * @throws DecoderException
    454     *             Thrown if quoted-printable decoding is unsuccessful
    455     * @since 1.7
    456     */
    457    public String decode(final String sourceStr, final Charset sourceCharset) throws DecoderException {
    458        if (sourceStr == null) {
    459            return null;
    460        }
    461        return new String(this.decode(StringUtils.getBytesUsAscii(sourceStr)), sourceCharset);
    462    }
    463
    464    /**
    465     * Decodes a quoted-printable string into its original form using the specified string Charset. Escaped characters
    466     * are converted back to their original representation.
    467     *
    468     * @param sourceStr
    469     *            quoted-printable string to convert into its original form
    470     * @param sourceCharset
    471     *            the original string Charset
    472     * @return original string
    473     * @throws DecoderException
    474     *             Thrown if quoted-printable decoding is unsuccessful
    475     * @throws UnsupportedEncodingException
    476     *             Thrown if Charset is not supported
    477     */
    478    public String decode(final String sourceStr, final String sourceCharset)
    479            throws DecoderException, UnsupportedEncodingException {
    480        if (sourceStr == null) {
    481            return null;
    482        }
    483        return new String(decode(StringUtils.getBytesUsAscii(sourceStr)), sourceCharset);
    484    }
    485
    486    /**
    487     * Decodes a quoted-printable string into its original form using the default string Charset. Escaped characters are
    488     * converted back to their original representation.
    489     *
    490     * @param sourceStr
    491     *            quoted-printable string to convert into its original form
    492     * @return original string
    493     * @throws DecoderException
    494     *             Thrown if quoted-printable decoding is unsuccessful. Thrown if Charset is not supported.
    495     * @see #getCharset()
    496     */
    497    @Override
    498    public String decode(final String sourceStr) throws DecoderException {
    499        return this.decode(sourceStr, this.getCharset());
    500    }
    501
    502    /**
    503     * Encodes an object into its quoted-printable safe form. Unsafe characters are escaped.
    504     *
    505     * @param obj
    506     *            string to convert to a quoted-printable form
    507     * @return quoted-printable object
    508     * @throws EncoderException
    509     *             Thrown if quoted-printable encoding is not applicable to objects of this type or if encoding is
    510     *             unsuccessful
    511     */
    512    @Override
    513    public Object encode(final Object obj) throws EncoderException {
    514        if (obj == null) {
    515            return null;
    516        }
    517        if (obj instanceof byte[]) {
    518            return encode((byte[]) obj);
    519        }
    520        if (obj instanceof String) {
    521            return encode((String) obj);
    522        }
    523        throw new EncoderException("Objects of type " +
    524              obj.getClass().getName() +
    525              " cannot be quoted-printable encoded");
    526    }
    527
    528    /**
    529     * Decodes a quoted-printable object into its original form. Escaped characters are converted back to their original
    530     * representation.
    531     *
    532     * @param obj
    533     *            quoted-printable object to convert into its original form
    534     * @return original object
    535     * @throws DecoderException
    536     *             Thrown if the argument is not a {@code String} or {@code byte[]}. Thrown if a failure
    537     *             condition is encountered during the decode process.
    538     */
    539    @Override
    540    public Object decode(final Object obj) throws DecoderException {
    541        if (obj == null) {
    542            return null;
    543        }
    544        if (obj instanceof byte[]) {
    545            return decode((byte[]) obj);
    546        }
    547        if (obj instanceof String) {
    548            return decode((String) obj);
    549        }
    550        throw new DecoderException("Objects of type " +
    551              obj.getClass().getName() +
    552              " cannot be quoted-printable decoded");
    553    }
    554
    555    /**
    556     * Gets the default Charset name used for string decoding and encoding.
    557     *
    558     * @return the default Charset name
    559     * @since 1.7
    560     */
    561    public Charset getCharset() {
    562        return this.charset;
    563    }
    564
    565    /**
    566     * Gets the default Charset name used for string decoding and encoding.
    567     *
    568     * @return the default Charset name
    569     */
    570    public String getDefaultCharset() {
    571        return this.charset.name();
    572    }
    573
    574    /**
    575     * Encodes a string into its quoted-printable form using the specified Charset. Unsafe characters are escaped.
    576     * <p>
    577     * Depending on the selection of the {@code strict} parameter, this function either implements the full ruleset
    578     * or only a subset of quoted-printable encoding specification (rule #1 and rule #2) as defined in
    579     * RFC 1521 and is suitable for encoding binary data and unformatted text.
    580     * </p>
    581     *
    582     * @param sourceStr
    583     *            string to convert to quoted-printable form
    584     * @param sourceCharset
    585     *            the Charset for sourceStr
    586     * @return quoted-printable string
    587     * @since 1.7
    588     */
    589    public String encode(final String sourceStr, final Charset sourceCharset) {
    590        if (sourceStr == null) {
    591            return null;
    592        }
    593        return StringUtils.newStringUsAscii(this.encode(sourceStr.getBytes(sourceCharset)));
    594    }
    595
    596    /**
    597     * Encodes a string into its quoted-printable form using the specified Charset. Unsafe characters are escaped.
    598     * <p>
    599     * Depending on the selection of the {@code strict} parameter, this function either implements the full ruleset
    600     * or only a subset of quoted-printable encoding specification (rule #1 and rule #2) as defined in
    601     * RFC 1521 and is suitable for encoding binary data and unformatted text.
    602     * </p>
    603     *
    604     * @param sourceStr
    605     *            string to convert to quoted-printable form
    606     * @param sourceCharset
    607     *            the Charset for sourceStr
    608     * @return quoted-printable string
    609     * @throws UnsupportedEncodingException
    610     *             Thrown if the Charset is not supported
    611     */
    612    public String encode(final String sourceStr, final String sourceCharset) throws UnsupportedEncodingException {
    613        if (sourceStr == null) {
    614            return null;
    615        }
    616        return StringUtils.newStringUsAscii(encode(sourceStr.getBytes(sourceCharset)));
    617    }
    618}