1<?php
2/**
3 * Copyright 2003-2017 Horde LLC (http://www.horde.org/)
4 *
5 * See the enclosed file COPYING for license information (LGPL). If you
6 * did not receive this file, see http://www.horde.org/licenses/lgpl21.
7 *
8 * This class is based on code by:
9 * Antony Raijekov <dev@strategma.bg>
10 * http://uruds.gateway.bg/zeos/
11 *
12 * @author   Jan Schneider <jan@horde.org>
13 * @category Horde
14 * @license  http://www.horde.org/licenses/lgpl21 LGPL 2.1
15 * @package  Compress
16 */
17
18/**
19 * This class allows dbx files (e.g. from Outlook Express) to be read.
20 *
21 * @author    Jan Schneider <jan@horde.org>
22 * @category  Horde
23 * @copyright 2003-2017 Horde LLC
24 * @license   http://www.horde.org/licenses/lgpl21 LGPL 2.1
25 * @package   Compress
26 */
27class Horde_Compress_Dbx extends Horde_Compress_Base
28{
29    /**
30     */
31    public $canDecompress = true;
32
33    /**
34     * TODO
35     *
36     * @var array
37     */
38    protected $_flagArray = array(
39        0x1 => 'MsgFlags',
40        0x2 => 'Sent',
41        0x4 => 'position',
42        0x7 => 'MessageID',
43        0x8 => 'Subject',
44        0x9 => 'From_reply',
45        0xA => 'References',
46        0xB => 'Newsgroup',
47        0xD => 'From',
48        0xE => 'Reply_To',
49        0x12 => 'Received',
50        0x13 => 'Receipt',
51        0x1A => 'Account',
52        0x1B => 'AccountID',
53        0x80 => 'Msg',
54        0x81 => 'MsgFlags',
55        0x84 => 'position',
56        0x91 => 'size',
57    );
58
59    /**
60     * TODO
61     *
62     * @var array
63     */
64    protected $_mails = array();
65
66    /**
67     * TODO
68     *
69     * @var array
70     */
71    protected $_tmp = array();
72
73    /**
74     * @return array  List of messages.
75     */
76    public function decompress($data, array $params = array())
77    {
78        $this->_mails = $this->_tmp = array();
79
80        $position = 0xC4;
81        $header_info = unpack('Lposition/LDataLength/nHeaderLength/nFlagCount', substr($data, $position, 12));
82        $position += 12;
83
84        // Go to the first table offest and process it.
85        if ($header_info['position'] > 0) {
86            $position = 0x30;
87            $buf = unpack('Lposition', substr($data, $position, 4));
88            $position = $buf['position'];
89            $this->_readIndex($data, $position);
90        }
91
92        return $this->_mails;
93    }
94
95    /**
96     * Returns a null-terminated string from the specified data.
97     *
98     * @param string $buf   TODO
99     * @param integer $pos  TODO
100     *
101     * @return string  TODO
102     */
103    protected function _readString($buf, $pos)
104    {
105        return ($len = strpos(substr($buf, $pos), chr(0)))
106            ? substr($buf, $pos, $len)
107            : '';
108    }
109
110    /**
111     * TODO
112     *
113     * @param string $data       TODO
114     * @param integer $position  TODO
115     *
116     * @return string  TODO
117     * @throws Horde_Compress_Exception
118     */
119    protected function _readMessage($data, $position)
120    {
121        $msg = '';
122        $part = 0;
123
124        if ($position > 0) {
125            $IndexItemsCount = array_pop(unpack('S', substr($data, 0xC4, 4)));
126            if ($IndexItemsCount > 0) {
127                while ($position < strlen($data)) {
128                    $part++;
129                    $s = substr($data, $position, 528);
130                    if (strlen($s) == 0) {
131                        break;
132                    }
133                    if (version_compare(PHP_VERSION, '5.5', '>=')) {
134                        $msg_item = unpack('LFilePos/LUnknown/LItemSize/LNextItem/Z512Content', $s);
135                    } else {
136                        $msg_item = unpack('LFilePos/LUnknown/LItemSize/LNextItem/a512Content', $s);
137                    }
138                    if ($msg_item['FilePos'] != $position) {
139                        throw new Horde_Compress_Exception(Horde_Compress_Translation::t("Invalid file format"));
140                    }
141                    $position += 528;
142                    $msg .= substr($msg_item['Content'], 0, $msg_item['ItemSize']);
143                    $position = $msg_item['NextItem'];
144                    if ($position == 0) {
145                        break;
146                    }
147                }
148            }
149        }
150
151        return $msg;
152    }
153
154    /**
155     * TODO
156     *
157     * @param string $data       TODO
158     * @param integer $position  TODO
159     *
160     * @return array  TODO
161     * @throws Horde_Compress_Exception
162     */
163    protected function _readMessageInfo($data, $position)
164    {
165        $message_info = array();
166        $msg_header = unpack('Lposition/LDataLength/SHeaderLength/SFlagCount', substr($data, $position, 12));
167        if ($msg_header['position'] != $position) {
168            throw new Horde_Compress_Exception(Horde_Compress_Translation::t("Invalid file format"));
169        }
170        $position += 12;
171        $message_info['HeaderPosition'] = $msg_header['position'];
172        $flags = $msg_header['FlagCount'] & 0xFF;
173        $DataSize = $msg_header['DataLength'] - $flags * 4;
174        $size = 4 * $flags;
175        $FlagsBuffer = substr($data, $position, $size);
176        $position += $size;
177        $size = $DataSize;
178        $DataBuffer = substr($data, $position, $size);
179        $position += $size;
180        $message_info = array();
181
182        /* Process flags */
183        for ($i = 0; $i < $flags; ++$i) {
184            $pos = 0;
185            $f = array_pop(unpack('L', substr($FlagsBuffer, $i * 4, 4)));
186
187            $mask = $f & 0xFF;
188            switch ($mask) {
189            case 0x1:
190                $pos = $pos + ($f >> 8);
191                $message_info['MsgFlags'] = array_pop(unpack('C', substr($DataBuffer, $pos++, 1)));
192                $message_info['MsgFlags'] += array_pop(unpack('C', substr($DataBuffer, $pos++, 1))) * 256;
193                $message_info['MsgFlags'] += array_pop(unpack('C', substr($DataBuffer, $pos, 1))) * 65536;
194                break;
195
196            case 0x2:
197            case 0x4:
198                $pos += array_pop(unpack('L', substr($FlagsBuffer, $i * 4, 4))) >> 8;
199                $message_info[$this->_flagArray[$mask]] = array_pop(unpack('L', substr($DataBuffer, $pos, 4)));
200                break;
201
202            case 0x7:
203            case 0x8:
204            case 0x9:
205            case 0xA:
206            case 0xB:
207            case 0xD:
208            case 0xE:
209            case 0x13:
210            case 0x1A:
211                $pos += array_pop(unpack('L', substr($FlagsBuffer, $i * 4, 4))) >> 8;
212                $message_info[$this->_flagArray[$mask]] = $this->_readString($DataBuffer, $pos);
213                break;
214
215            case 0x12:
216                $pos += array_pop(unpack('L', substr($FlagsBuffer, $i * 4, 4))) >> 8;
217                $message_info['Received'] = array_pop(unpack('L', substr($DataBuffer, $pos, 4)));
218                break;
219
220            case 0x1B:
221                $pos += array_pop(unpack('L', substr($FlagsBuffer, $i * 4, 4))) >> 8;
222                $message_info['AccountID'] = intval($this->_readString($DataBuffer, $pos));
223                break;
224
225            case 0x80:
226            case 0x81:
227            case 0x84:
228            case 0x91:
229                $message_info[$this->_flagArray[$mask]] = array_pop(unpack('L', substr($FlagsBuffer, $i * 4, 4))) >> 8;
230                break;
231            }
232        }
233
234        return $message_info;
235    }
236
237    /**
238     * TODO
239     *
240     * @param string $data       TODO
241     * @param integer $position  TODO
242     *
243     * @throws Horde_Compress_Exception
244     */
245    protected function _readIndex($data, $position)
246    {
247        $index_header = unpack('LFilePos/LUnknown1/LPrevIndex/LNextIndex/LCount/LUnknown', substr($data, $position, 24));
248        if ($index_header['FilePos'] != $position) {
249            throw new Horde_Compress_Exception(Horde_Compress_Translation::t("Invalid file format"));
250        }
251
252        // Push it into list of processed items.
253        $this->_tmp[$position] = true;
254        if (($index_header['NextIndex'] > 0) &&
255            empty($this->_tmp[$index_header['NextIndex']])) {
256            $this->_readIndex($data, $index_header['NextIndex']);
257        }
258        if (($index_header['PrevIndex'] > 0) &&
259            empty($this->_tmp[$index_header['PrevIndex']])) {
260            $this->_readIndex($data, $index_header['PrevIndex']);
261        }
262        $position += 24;
263        $icount = $index_header['Count'] >> 8;
264        if ($icount > 0) {
265            $buf = substr($data, $position, 12 * $icount);
266            for ($i = 0; $i < $icount; $i++) {
267                $hdr_buf = substr($buf, $i * 12, 12);
268                $IndexItem = unpack('LHeaderPos/LChildIndex/LUnknown', $hdr_buf);
269                if ($IndexItem['HeaderPos'] > 0) {
270                    $mail['info'] = $this->_readMessageInfo($data, $IndexItem['HeaderPos']);
271                    $mail['content'] = $this->_readMessage($data, $mail['info']['position']);
272                    $this->_mails[] = $mail;
273                }
274                if (($IndexItem['ChildIndex'] > 0) &&
275                    empty($this->_tmp[$IndexItem['ChildIndex']])) {
276                    $this->_readIndex($data, $IndexItem['ChildIndex']);
277                }
278            }
279        }
280    }
281
282}
283