1<?php
2/**
3 * Copyright 1999-2017 Horde LLC (http://www.horde.org/)
4 *
5 * See the enclosed file LICENSE for license information (LGPL). If you
6 * did not receive this file, see http://www.horde.org/licenses/lgpl21.
7 *
8 * @category  Horde
9 * @copyright 1999-2017 Horde LLC
10 * @license   http://www.horde.org/licenses/lgpl21 LGPL 2.1
11 * @package   Mime
12 */
13
14/**
15 * Provide methods for dealing with MIME encoding (RFC 2045-2049);
16 *
17 * @author    Chuck Hagenbuch <chuck@horde.org>
18 * @author    Michael Slusarz <slusarz@horde.org>
19 * @category  Horde
20 * @copyright 1999-2017 Horde LLC
21 * @license   http://www.horde.org/licenses/lgpl21 LGPL 2.1
22 * @package   Mime
23 */
24class Horde_Mime
25{
26    /**
27     * The RFC defined EOL string.
28     *
29     * @var string
30     */
31    const EOL = "\r\n";
32
33    /**
34     * Use windows-1252 charset when decoding ISO-8859-1 data?
35     * HTML 5 requires this behavior, so it is the default.
36     *
37     * @var boolean
38     */
39    public static $decodeWindows1252 = true;
40
41    /**
42     * Determines if a string contains 8-bit (non US-ASCII) characters.
43     *
44     * @param string $string   The string to check.
45     * @param string $charset  The charset of the string. Defaults to
46     *                         US-ASCII. (@deprecated)
47     *
48     * @return boolean  True if string contains non 7-bit characters.
49     */
50    public static function is8bit($string, $charset = null)
51    {
52        $string = strval($string);
53        for ($i = 0, $len = strlen($string); $i < $len; ++$i) {
54            if (ord($string[$i]) > 127) {
55                return true;
56            }
57        }
58
59        return false;
60    }
61
62    /**
63     * MIME encodes a string (RFC 2047).
64     *
65     * @param string $text     The text to encode (UTF-8).
66     * @param string $charset  The character set to encode to.
67     *
68     * @return string  The MIME encoded string (US-ASCII).
69     */
70    public static function encode($text, $charset = 'UTF-8')
71    {
72        $charset = Horde_String::lower($charset);
73        $text = Horde_String::convertCharset($text, 'UTF-8', $charset);
74
75        $encoded = $is_encoded = false;
76        $lwsp = $word = null;
77        $out = '';
78
79        /* 0 = word unencoded
80         * 1 = word encoded
81         * 2 = spaces */
82        $parts = array();
83
84        /* Tokenize string. */
85        for ($i = 0, $len = strlen($text); $i < $len; ++$i) {
86            switch ($text[$i]) {
87            case "\t":
88            case "\r":
89            case "\n":
90                if (!is_null($word)) {
91                    $parts[] = array(intval($encoded), $word, $i - $word);
92                    $word = null;
93                } elseif (!is_null($lwsp)) {
94                    $parts[] = array(2, $lwsp, $i - $lwsp);
95                    $lwsp = null;
96                }
97
98                $parts[] = array(0, $i, 1);
99                break;
100
101            case ' ':
102                if (!is_null($word)) {
103                    $parts[] = array(intval($encoded), $word, $i - $word);
104                    $word = null;
105                }
106                if (is_null($lwsp)) {
107                    $lwsp = $i;
108                }
109                break;
110
111            default:
112                if (is_null($word)) {
113                    $encoded = false;
114                    $word = $i;
115                    if (!is_null($lwsp)) {
116                        $parts[] = array(2, $lwsp, $i - $lwsp);
117                        $lwsp = null;
118                    }
119
120                    /* Check for MIME encoding delimiter. Encode it if
121                     * found. */
122                    if (($text[$i] === '=') &&
123                        (($i + 1) < $len) &&
124                        ($text[$i +1] === '?')) {
125                        ++$i;
126                        $encoded = $is_encoded = true;
127                    }
128                }
129
130                /* Check for 8-bit characters or control characters. */
131                if (!$encoded) {
132                    $c = ord($text[$i]);
133                    if ($encoded = (($c & 0x80) || ($c < 32))) {
134                        $is_encoded = true;
135                    }
136                }
137                break;
138            }
139        }
140
141        if (!$is_encoded) {
142            return $text;
143        }
144
145        if (is_null($lwsp)) {
146            $parts[] = array(intval($encoded), $word, $len);
147        } else {
148            $parts[] = array(2, $lwsp, $len);
149        }
150
151        /* Combine parts into MIME encoded string. */
152        for ($i = 0, $cnt = count($parts); $i < $cnt; ++$i) {
153            $val = $parts[$i];
154
155            switch ($val[0]) {
156            case 0:
157            case 2:
158                $out .= substr($text, $val[1], $val[2]);
159                break;
160
161            case 1:
162                $j = $i;
163                for ($k = $i + 1; $k < $cnt; ++$k) {
164                    switch ($parts[$k][0]) {
165                    case 0:
166                        break 2;
167
168                    case 1:
169                        $i = $k;
170                        break;
171                    }
172                }
173
174                $encode = '';
175                for (; $j <= $i; ++$j) {
176                    $encode .= substr($text, $parts[$j][1], $parts[$j][2]);
177                }
178
179                $delim = '=?' . $charset . '?b?';
180                $e_parts = explode(
181                    self::EOL,
182                    rtrim(
183                        chunk_split(
184                            base64_encode($encode),
185                            /* strlen($delim) + 2 = space taken by MIME
186                             * delimiter */
187                            intval((75 - strlen($delim) + 2) / 4) * 4
188                        )
189                    )
190                );
191
192                $tmp = array();
193                foreach ($e_parts as $val) {
194                    $tmp[] = $delim . $val . '?=';
195                }
196
197                $out .= implode(' ', $tmp);
198                break;
199            }
200        }
201
202        return rtrim($out);
203    }
204
205    /**
206     * Decodes a MIME encoded (RFC 2047) string.
207     *
208     * @param string $string  The MIME encoded text.
209     *
210     * @return string  The decoded text.
211     */
212    public static function decode($string)
213    {
214        $old_pos = 0;
215        $out = '';
216
217        while (($pos = strpos($string, '=?', $old_pos)) !== false) {
218            /* Save any preceding text, if it is not LWSP between two
219             * encoded words. */
220            $pre = substr($string, $old_pos, $pos - $old_pos);
221            if (!$old_pos ||
222                (strspn($pre, " \t\n\r") != strlen($pre))) {
223                $out .= $pre;
224            }
225
226            /* Search for first delimiting question mark (charset). */
227            if (($d1 = strpos($string, '?', $pos + 2)) === false) {
228                break;
229            }
230
231            $orig_charset = substr($string, $pos + 2, $d1 - $pos - 2);
232            if (self::$decodeWindows1252 &&
233                (Horde_String::lower($orig_charset) == 'iso-8859-1')) {
234                $orig_charset = 'windows-1252';
235            }
236
237            /* Search for second delimiting question mark (encoding). */
238            if (($d2 = strpos($string, '?', $d1 + 1)) === false) {
239                break;
240            }
241
242            $encoding = substr($string, $d1 + 1, $d2 - $d1 - 1);
243
244            /* Search for end of encoded data. */
245            if (($end = strpos($string, '?=', $d2 + 1)) === false) {
246                break;
247            }
248
249            $encoded_text = substr($string, $d2 + 1, $end - $d2 - 1);
250
251            switch ($encoding) {
252            case 'Q':
253            case 'q':
254                $out .= Horde_String::convertCharset(
255                    quoted_printable_decode(
256                        str_replace('_', ' ', $encoded_text)
257                    ),
258                    $orig_charset,
259                    'UTF-8'
260                );
261            break;
262
263            case 'B':
264            case 'b':
265                $out .= Horde_String::convertCharset(
266                    base64_decode($encoded_text),
267                    $orig_charset,
268                    'UTF-8'
269                );
270            break;
271
272            default:
273                // Ignore unknown encoding.
274                break;
275            }
276
277            $old_pos = $end + 2;
278        }
279
280        return $out . substr($string, $old_pos);
281    }
282
283    /* Deprecated methods. */
284
285    /**
286     * @deprecated  Use Horde_Mime_Headers_MessageId::create() instead.
287     */
288    public static function generateMessageId()
289    {
290        return Horde_Mime_Headers_MessageId::create()->value;
291    }
292
293    /**
294     * @deprecated  Use Horde_Mime_Uudecode instead.
295     */
296    public static function uudecode($input)
297    {
298        $uudecode = new Horde_Mime_Uudecode($input);
299        return iterator_to_array($uudecode);
300    }
301
302    /**
303     * @deprecated
304     */
305    public static $brokenRFC2231 = false;
306
307    /**
308     * @deprecated
309     */
310    const MIME_PARAM_QUOTED = '/[\x01-\x20\x22\x28\x29\x2c\x2f\x3a-\x40\x5b-\x5d]/';
311
312    /**
313     * @deprecated  Use Horde_Mime_Headers_ContentParam#encode() instead.
314     */
315    public static function encodeParam($name, $val, array $opts = array())
316    {
317        $cp = new Horde_Mime_Headers_ContentParam(
318            'UNUSED',
319            array($name => $val)
320        );
321
322        return $cp->encode(array_merge(array(
323            'broken_rfc2231' => self::$brokenRFC2231
324        ), $opts));
325    }
326
327    /**
328     * @deprecated  Use Horde_Mime_Headers_ELement_ContentParam instead.
329     */
330    public static function decodeParam($type, $data)
331    {
332        $cp = new Horde_Mime_Headers_ContentParam(
333            'UNUSED',
334            $data
335        );
336
337        if (strlen($cp->value)) {
338            $val = $cp->value;
339        } else {
340            $val = (Horde_String::lower($type) == 'content-type')
341                ? 'text/plain'
342                : 'attachment';
343        }
344
345        return array(
346            'params' => $cp->params,
347            'val' => $val
348        );
349    }
350
351    /**
352     * @deprecated  Use Horde_Mime_Id instead.
353     */
354    public static function mimeIdArithmetic($id, $action, $options = array())
355    {
356        $id_ob = new Horde_Mime_Id($id);
357
358        switch ($action) {
359        case 'down':
360            $action = $id_ob::ID_DOWN;
361            break;
362
363        case 'next':
364            $action = $id_ob::ID_NEXT;
365            break;
366
367        case 'prev':
368            $action = $id_ob::ID_PREV;
369            break;
370
371        case 'up':
372            $action = $id_ob::ID_UP;
373            break;
374        }
375
376        return $id_ob->idArithmetic($action, $options);
377    }
378
379    /**
380     * @deprecated  Use Horde_Mime_Id instead.
381     */
382    public static function isChild($base, $id)
383    {
384        $id_ob = new Horde_Mime_Id($base);
385        return $id_ob->isChild($id);
386    }
387
388    /**
389     * @deprecated  Use Horde_Mime_QuotedPrintable instead.
390     */
391    public static function quotedPrintableEncode($text, $eol = self::EOL,
392                                                 $wrap = 76)
393    {
394        return Horde_Mime_QuotedPrintable::encode($text, $eol, $wrap);
395    }
396
397}
398