1<?php
2/**
3 * Copyright 2008-2017 Horde LLC (http://www.horde.org/)
4 *
5 * getBaseSubject() code adapted from imap-base-subject.c (Dovecot 1.2)
6 *   Original code released under the LGPL-2.1
7 *   Copyright (c) 2002-2008 Timo Sirainen <tss@iki.fi>
8 *
9 * See the enclosed file LICENSE for license information (LGPL). If you
10 * did not receive this file, see http://www.horde.org/licenses/lgpl21.
11 *
12 * @category  Horde
13 * @copyright 2002-2008 Timo Sirainen
14 * @copyright 2008-2017 Horde LLC
15 * @license   http://www.horde.org/licenses/lgpl21 LGPL 2.1
16 * @package   Imap_Client
17 */
18
19/**
20 * Determines the "base subject" of a string (RFC 5256 [2.1]).
21 *
22 * @author    Timo Sirainen <tss@iki.fi>
23 * @author    Michael Slusarz <slusarz@horde.org>
24 * @category  Horde
25 * @copyright 2002-2008 Timo Sirainen
26 * @copyright 2011-2017 Horde LLC
27 * @license   http://www.horde.org/licenses/lgpl21 LGPL 2.1
28 * @package   Imap_Client
29 */
30class Horde_Imap_Client_Data_BaseSubject
31{
32    /**
33     * The base subject.
34     *
35     * @var string
36     */
37    protected $_subject;
38
39    /**
40     * Constructor.
41     *
42     * @param string $str  The subject string.
43     * @param array $opts  Additional options:
44     *   - keepblob: (boolean) Don't remove any "blob" information (i.e. text
45     *               leading text between square brackets) from string.
46     *
47     * @return string  The cleaned up subject string.
48     */
49    public function __construct($str, array $opts = array())
50    {
51        // Rule 1a: MIME decode.
52        $str = Horde_Mime::decode($str);
53
54        // Rule 1b: Remove superfluous whitespace.
55        $str = preg_replace("/[\t\r\n ]+/", ' ', $str);
56
57        do {
58            /* (2) Remove all trailing text of the subject that matches the
59             * the subj-trailer ABNF, repeat until no more matches are
60             * possible. */
61            $str = preg_replace("/(?:\s*\(fwd\)\s*)+$/i", '', $str);
62
63            do {
64                /* (3) Remove all prefix text of the subject that matches the
65                 * subj-leader ABNF. */
66                $found = $this->_removeSubjLeader($str, !empty($opts['keepblob']));
67
68                /* (4) If there is prefix text of the subject that matches
69                 * the subj-blob ABNF, and removing that prefix leaves a
70                 * non-empty subj-base, then remove the prefix text. */
71                $found = (empty($opts['keepblob']) && $this->_removeBlobWhenNonempty($str)) || $found;
72
73                /* (5) Repeat (3) and (4) until no matches remain. */
74            } while ($found);
75
76            /* (6) If the resulting text begins with the subj-fwd-hdr ABNF and
77             * ends with the subj-fwd-trl ABNF, remove the subj-fwd-hdr and
78             * subj-fwd-trl and repeat from step (2). */
79        } while ($this->_removeSubjFwdHdr($str));
80
81        $this->_subject = strval($str);
82    }
83
84    /**
85     * Return the "base subject" defined in RFC 5256 [2.1].
86     *
87     * @return string  The base subject.
88     */
89    public function __toString()
90    {
91        return $this->_subject;
92    }
93
94    /**
95     * Remove all prefix text of the subject that matches the subj-leader
96     * ABNF.
97     *
98     * @param string &$str       The subject string.
99     * @param boolean $keepblob  Remove blob information?
100     *
101     * @return boolean  True if string was altered.
102     */
103    protected function _removeSubjLeader(&$str, $keepblob = false)
104    {
105        $ret = false;
106
107        if (!strlen($str)) {
108            return $ret;
109        }
110
111        if ($len = strspn($str, " \t")) {
112            $str = substr($str, $len);
113            $ret = true;
114        }
115
116        $i = 0;
117
118        if (!$keepblob) {
119            while (isset($str[$i]) && ($str[$i] === '[')) {
120                if (($i = $this->_removeBlob($str, $i)) === false) {
121                    return $ret;
122                }
123            }
124        }
125
126        if (stripos($str, 're', $i) === 0) {
127            $i += 2;
128        } elseif (stripos($str, 'fw', $i) === 0) {
129            $i += (stripos($str, 'fwd', $i) === 0) ? 3 : 2;
130        } else {
131            return $ret;
132        }
133
134        $i += strspn($str, " \t", $i);
135
136        if (!$keepblob) {
137            while (isset($str[$i]) && ($str[$i] === '[')) {
138                if (($i = $this->_removeBlob($str, $i)) === false) {
139                    return $ret;
140                }
141            }
142        }
143
144        if (!isset($str[$i]) || ($str[$i] !== ':')) {
145            return $ret;
146        }
147
148        $str = substr($str, ++$i);
149
150        return true;
151    }
152
153    /**
154     * Remove "[...]" text.
155     *
156     * @param string $str  The subject string.
157     * @param integer $i   Current position.
158     *
159     * @return boolean|integer  False if blob was not found, otherwise the
160     *                          string position of the first non-blob char.
161     */
162    protected function _removeBlob($str, $i)
163    {
164        if ($str[$i] !== '[') {
165            return false;
166        }
167
168        ++$i;
169
170        for ($cnt = strlen($str); $i < $cnt; ++$i) {
171            if ($str[$i] === ']') {
172                break;
173            }
174
175            if ($str[$i] === '[') {
176                return false;
177            }
178        }
179
180        if ($i === ($cnt - 1)) {
181            return false;
182        }
183
184        ++$i;
185
186        if ($str[$i] === ' ') {
187            ++$i;
188        }
189
190        return $i;
191    }
192
193    /**
194     * Remove "[...]" text if it doesn't result in the subject becoming
195     * empty.
196     *
197     * @param string &$str  The subject string.
198     *
199     * @return boolean  True if string was altered.
200     */
201    protected function _removeBlobWhenNonempty(&$str)
202    {
203        if ($str &&
204            ($str[0] === '[') &&
205            (($i = $this->_removeBlob($str, 0)) !== false) &&
206            ($i !== strlen($str))) {
207            $str = substr($str, $i);
208            return true;
209        }
210
211        return false;
212    }
213
214    /**
215     * Remove a "[fwd: ... ]" string.
216     *
217     * @param string &$str  The subject string.
218     *
219     * @return boolean  True if string was altered.
220     */
221    protected function _removeSubjFwdHdr(&$str)
222    {
223        if ((stripos($str, '[fwd:') !== 0) || (substr($str, -1) !== ']')) {
224            return false;
225        }
226
227        $str = substr($str, 5, -1);
228        return true;
229    }
230
231}
232