<?php
/**
 * Originally based on code:
 *
 *  Copyright (C) 2000 Edmund Grimley Evans <edmundo@rano.org>
 *  Released under the GPL (version 2)
 *
 *  Translated from C to PHP by Thomas Bruederli <roundcube@gmail.com>
 *  Code extracted from the RoundCube Webmail (http://roundcube.net) project,
 *    SVN revision 1757
 *  The RoundCube project is released under the GPL (version 2)
 *
 * Copyright 2008-2017 Horde LLC (http://www.horde.org/)
 *
 * See the enclosed file COPYING for license information (LGPL). If you
 * did not receive this file, see http://www.horde.org/licenses/lgpl21.
 *
 * @category  Horde
 * @copyright 2000 Edmund Grimley Evans <edmundo@rano.org>
 * @copyright 2008-2017 Horde LLC
 * @license   http://www.horde.org/licenses/lgpl21 LGPL 2.1
 * @package   Imap_Client
 */

/**
 * Allows conversions between UTF-8 and UTF7-IMAP (RFC 3501 [5.1.3]).
 *
 * @author    Michael Slusarz <slusarz@horde.org>
 * @category  Horde
 * @copyright 2000 Edmund Grimley Evans <edmundo@rano.org>
 * @copyright 2008-2017 Horde LLC
 * @license   http://www.horde.org/licenses/lgpl21 LGPL 2.1
 * @package   Imap_Client
 */
class Horde_Imap_Client_Utf7imap
{
    /**
     * Lookup table for conversion.
     *
     * @var array
     */
    private static $_index64 = array(
        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, 63, -1, -1, -1,
        52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1,
        -1,  0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14,
        15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1,
        -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
        41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1
    );

    /**
     * Lookup table for conversion.
     *
     * @var array
     */
    private static $_base64 = array(
        'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N',
        'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b',
        'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p',
        'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3',
        '4', '5', '6', '7', '8', '9', '+', ','
    );

    /**
     * Is mbstring extension available?
     *
     * @var array
     */
    protected static $_mbstring = null;

    /**
     * Convert a string from UTF7-IMAP to UTF-8.
     *
     * @param string $str  The UTF7-IMAP string.
     *
     * @return string  The converted UTF-8 string.
     * @throws Horde_Imap_Client_Exception
     */
    public static function Utf7ImapToUtf8($str)
    {
        if ($str instanceof Horde_Imap_Client_Mailbox) {
            return $str->utf8;
        }

        $str = strval($str);

        /* Try mbstring, if available, which should be faster. Don't use the
         * IMAP utf7_* functions because they are known to be buggy. */
        if (is_null(self::$_mbstring)) {
            self::$_mbstring = extension_loaded('mbstring');
        }
        if (self::$_mbstring) {
            return @mb_convert_encoding($str, 'UTF-8', 'UTF7-IMAP');
        }

        $p = '';
        $ptr = &self::$_index64;

        for ($i = 0, $u7len = strlen($str); $u7len > 0; ++$i, --$u7len) {
            $u7 = $str[$i];
            if ($u7 === '&') {
                $u7 = $str[++$i];
                if (--$u7len && ($u7 === '-')) {
                    $p .= '&';
                    continue;
                }

                $ch = 0;
                $k = 10;
                for (; $u7len > 0; ++$i, --$u7len) {
                    $u7 = $str[$i];

                    if ((ord($u7) & 0x80) || ($b = $ptr[ord($u7)]) === -1) {
                        break;
                    }

                    if ($k > 0) {
                        $ch |= $b << $k;
                        $k -= 6;
                    } else {
                        $ch |= $b >> (-$k);
                        if ($ch < 0x80) {
                            /* Printable US-ASCII */
                            if ((0x20 <= $ch) && ($ch < 0x7f)) {
                                throw new Horde_Imap_Client_Exception(
                                    Horde_Imap_Client_Translation::r("Error converting UTF7-IMAP string."),
                                    Horde_Imap_Client_Exception::UTF7IMAP_CONVERSION
                                );
                            }
                            $p .= chr($ch);
                        } else if ($ch < 0x800) {
                            $p .= chr(0xc0 | ($ch >> 6)) .
                                  chr(0x80 | ($ch & 0x3f));
                        } else {
                            $p .= chr(0xe0 | ($ch >> 12)) .
                                  chr(0x80 | (($ch >> 6) & 0x3f)) .
                                  chr(0x80 | ($ch & 0x3f));
                        }

                        $ch = ($b << (16 + $k)) & 0xffff;
                        $k += 10;
                    }
                }

                /* Non-zero or too many extra bits -OR-
                 * Base64 not properly terminated -OR-
                 * Adjacent Base64 sections. */
                if (($ch || ($k < 6)) ||
                    (!$u7len || $u7 !== '-') ||
                    (($u7len > 2) &&
                     ($str[$i + 1] === '&') &&
                     ($str[$i + 2] !== '-'))) {
                    throw new Horde_Imap_Client_Exception(
                        Horde_Imap_Client_Translation::r("Error converting UTF7-IMAP string."),
                        Horde_Imap_Client_Exception::UTF7IMAP_CONVERSION
                    );
                }
            } elseif ((ord($u7) < 0x20) || (ord($u7) >= 0x7f)) {
                /* Not printable US-ASCII */
                throw new Horde_Imap_Client_Exception(
                    Horde_Imap_Client_Translation::r("Error converting UTF7-IMAP string."),
                    Horde_Imap_Client_Exception::UTF7IMAP_CONVERSION
                );
            } else {
                $p .= $u7;
            }
        }

        return $p;
    }

    /**
     * Convert a string from UTF-8 to UTF7-IMAP.
     *
     * @param string $str     The UTF-8 string.
     * @param boolean $force  Assume $str is UTF-8 (no-autodetection)? If
     *                        false, attempts to auto-detect if string is
     *                        already in UTF7-IMAP.
     *
     * @return string  The converted UTF7-IMAP string.
     * @throws Horde_Imap_Client_Exception
     */
    public static function Utf8ToUtf7Imap($str, $force = true)
    {
        if ($str instanceof Horde_Imap_Client_Mailbox) {
            return $str->utf7imap;
        }

        $str = strval($str);

        /* No need to do conversion if all chars are in US-ASCII range or if
         * no ampersand is present. But will assume that an already encoded
         * ampersand means string is in UTF7-IMAP already. */
        if (!$force &&
            !preg_match('/[\x80-\xff]|&$|&(?![,+A-Za-z0-9]*-)/', $str)) {
            return $str;
        }

        /* Try mbstring, if available, which should be faster. Don't use the
         * IMAP utf7_* functions because they are known to be buggy. */
        if (is_null(self::$_mbstring)) {
            self::$_mbstring = extension_loaded('mbstring');
        }
        if (self::$_mbstring) {
            return @mb_convert_encoding($str, 'UTF7-IMAP', 'UTF-8');
        }

        $u8len = strlen($str);
        $i = 0;
        $base64 = false;
        $p = '';
        $ptr = &self::$_base64;

        while ($u8len) {
            $u8 = $str[$i];
            $c = ord($u8);

            if ($c < 0x80) {
                $ch = $c;
                $n = 0;
            } elseif ($c < 0xc2) {
                throw new Horde_Imap_Client_Exception(
                    Horde_Imap_Client_Translation::r("Error converting UTF7-IMAP string."),
                    Horde_Imap_Client_Exception::UTF7IMAP_CONVERSION
                );
            } elseif ($c < 0xe0) {
                $ch = $c & 0x1f;
                $n = 1;
            } elseif ($c < 0xf0) {
                $ch = $c & 0x0f;
                $n = 2;
            } elseif ($c < 0xf8) {
                $ch = $c & 0x07;
                $n = 3;
            } elseif ($c < 0xfc) {
                $ch = $c & 0x03;
                $n = 4;
            } elseif ($c < 0xfe) {
                $ch = $c & 0x01;
                $n = 5;
            } else {
                throw new Horde_Imap_Client_Exception(
                    Horde_Imap_Client_Translation::r("Error converting UTF7-IMAP string."),
                    Horde_Imap_Client_Exception::UTF7IMAP_CONVERSION
                );
            }

            if ($n > --$u8len) {
                throw new Horde_Imap_Client_Exception(
                    Horde_Imap_Client_Translation::r("Error converting UTF7-IMAP string."),
                    Horde_Imap_Client_Exception::UTF7IMAP_CONVERSION
                );
            }

            ++$i;

            for ($j = 0; $j < $n; ++$j) {
                $o = ord($str[$i + $j]);
                if (($o & 0xc0) !== 0x80) {
                    throw new Horde_Imap_Client_Exception(
                        Horde_Imap_Client_Translation::r("Error converting UTF7-IMAP string."),
                        Horde_Imap_Client_Exception::UTF7IMAP_CONVERSION
                    );
                }
                $ch = ($ch << 6) | ($o & 0x3f);
            }

            if (($n > 1) && !($ch >> ($n * 5 + 1))) {
                throw new Horde_Imap_Client_Exception(
                    Horde_Imap_Client_Translation::r("Error converting UTF7-IMAP string."),
                    Horde_Imap_Client_Exception::UTF7IMAP_CONVERSION
                );
            }

            $i += $n;
            $u8len -= $n;

            if (($ch < 0x20) || ($ch >= 0x7f)) {
                if (!$base64) {
                    $p .= '&';
                    $base64 = true;
                    $b = 0;
                    $k = 10;
                }

                if ($ch & ~0xffff) {
                    $ch = 0xfffe;
                }

                $p .= $ptr[($b | $ch >> $k)];
                $k -= 6;
                for (; $k >= 0; $k -= 6) {
                    $p .= $ptr[(($ch >> $k) & 0x3f)];
                }

                $b = ($ch << (-$k)) & 0x3f;
                $k += 16;
            } else {
                if ($base64) {
                    if ($k > 10) {
                        $p .= $ptr[$b];
                    }
                    $p .= '-';
                    $base64 = false;
                }

                $p .= chr($ch);
                if (chr($ch) === '&') {
                    $p .= '-';
                }
            }
        }

        if ($base64) {
            if ($k > 10) {
                $p .= $ptr[$b];
            }
            $p .= '-';
        }

        return $p;
    }

}
