7027794f |
1 | <?php |
2 | /******************************************************************************** |
3 | * banana/protocoleinterface.inc.php : interface for box access |
4 | * ------------------------ |
5 | * |
6 | * This file is part of the banana distribution |
7 | * Copyright: See COPYING files that comes with this distribution |
8 | ********************************************************************************/ |
9 | |
10 | require_once dirname(__FILE__) . '/banana.inc.php'; |
11 | require_once dirname(__FILE__) . '/protocoleinterface.inc.php'; |
12 | require_once dirname(__FILE__) . '/message.inc.php'; |
13 | |
14 | class BananaMBox implements BananaProtocoleInterface |
15 | { |
7027794f |
16 | private $file = null; |
17 | private $filesize = null; |
18 | private $current_id = null; |
19 | private $at_beginning = false; |
20 | private $file_cache = null; |
21 | |
22 | private $_lasterrno = 0; |
23 | private $_lasterror = null; |
24 | |
25 | private $count = null; |
26 | private $new_messages = null; |
27 | private $messages = null; |
28 | |
29 | /** Build a protocole handler plugged on the given box |
30 | */ |
0e25d15d |
31 | public function __construct() |
7027794f |
32 | { |
0e25d15d |
33 | $filename = $this->getFileName(Banana::$group); |
7027794f |
34 | if (is_null($filename)) { |
35 | return; |
36 | } |
37 | $this->filesize = filesize($filename); |
38 | $this->file = @fopen($filename, 'r'); |
39 | if (!$this->file) { |
40 | $this->_lasterrno = 1; |
41 | $this->_lasterror = _b_('Can\'t open file'); |
42 | $this->file = null; |
43 | } |
44 | $this->current_id = 0; |
45 | $this->at_beginning = true; |
46 | } |
47 | |
48 | /** Close the file |
49 | */ |
50 | public function __destruct() |
51 | { |
52 | if ($this->file) { |
53 | fclose($this->file); |
54 | } |
55 | } |
56 | |
57 | /** Indicate if the Protocole handler has been succesfully built |
58 | */ |
59 | public function isValid() |
60 | { |
0e25d15d |
61 | return !Banana::$group || $this->file; |
7027794f |
62 | } |
63 | |
64 | /** Indicate last error n° |
65 | */ |
66 | public function lastErrNo() |
67 | { |
68 | return $this->_lasterrno;; |
69 | } |
70 | |
71 | /** Indicate last error text |
72 | */ |
73 | public function lastError() |
74 | { |
75 | return $this->_lasterror; |
76 | } |
77 | |
78 | /** Return the description of the current box |
79 | */ |
80 | public function getDescription() |
81 | { |
82 | return null; |
83 | } |
84 | |
85 | /** Return the list of the boxes |
86 | * @param mode Kind of boxes to list |
87 | * @param since date of last check (for new boxes and new messages) |
88 | * @param withstats Indicated whether msgnum and unread must be set in the result |
89 | * @return Array(boxname => array(desc => boxdescripton, msgnum => number of message, unread =>number of unread messages) |
90 | */ |
91 | public function getBoxList($mode = Banana::BOXES_ALL, $since = 0, $withstats = false) |
92 | { |
0e25d15d |
93 | return array(Banana::$group => array('desc' => '', 'msgnum' => 0, 'unread' => 0)); |
7027794f |
94 | } |
95 | |
96 | /** Return a message |
97 | * @param id Id of the emssage (can be either an Message-id or a message index) |
7027794f |
98 | * @return A BananaMessage or null if the given id can't be retreived |
99 | */ |
7a5823f9 |
100 | public function &getMessage($id) |
7027794f |
101 | { |
7a5823f9 |
102 | if (!is_numeric($id)) { |
103 | if (!Banana::$spool) { |
7027794f |
104 | return null; |
105 | } |
106 | $id = Banana::$spool->ids[$id]; |
107 | } |
108 | $message = $this->readMessages(array($id)); |
109 | if (empty($message)) { |
110 | return null; |
111 | } |
112 | $msg = new BananaMessage($message[$id]['message']); |
113 | return $msg; |
114 | } |
115 | |
7a5823f9 |
116 | /** Return the sources of the given message |
117 | */ |
118 | public function getMessageSource($id) |
119 | { |
120 | if (!is_numeric($id)) { |
121 | if (!Banana::$spool) { |
122 | return null; |
123 | } |
124 | $id = Banana::$spool->ids[$id]; |
125 | } |
126 | $message = $this->readMessages(array($id)); |
127 | return implode("\n", $message); |
128 | } |
129 | |
130 | /** Compute the number of messages of the box |
131 | */ |
7027794f |
132 | private function getCount() |
133 | { |
134 | $this->count = count(Banana::$spool->overview); |
135 | $max = @max(array_keys(Banana::$spool->overview)); |
136 | if ($max && Banana::$spool->overview[$max]->storage['next'] == $this->filesize) { |
137 | $this->new_messages = 0; |
138 | } else { |
139 | $this->new_messages = $this->countMessages($this->count); |
140 | $this->count += $this->new_messages; |
141 | } |
142 | } |
143 | |
144 | /** Return the indexes of the messages presents in the Box |
145 | * @return Array(number of messages, MSGNUM of the first message, MSGNUM of the last message) |
146 | */ |
147 | public function getIndexes() |
148 | { |
149 | if (is_null($this->count)) { |
150 | $this->getCount(); |
151 | } |
152 | return array($this->count, 0, $this->count - 1); |
153 | } |
154 | |
155 | /** Return the message headers (in BananaMessage) for messages from firstid to lastid |
156 | * @return Array(id => array(headername => headervalue)) |
157 | */ |
158 | public function &getMessageHeaders($firstid, $lastid, array $msg_headers = array()) |
159 | { |
160 | $msg_headers = array_map('strtolower', $msg_headers); |
161 | $messages =& $this->readMessages(range($firstid, $lastid), true); |
162 | $msg_headers = array_map('strtolower', $msg_headers); |
163 | $headers = array(); |
164 | foreach ($msg_headers as $header) { |
165 | foreach ($messages as $id=>&$message) { |
166 | if (!isset($headers[$id])) { |
167 | $headers[$id] = array('beginning' => $message['beginning'], 'end' => $message['end']); |
168 | } |
169 | if ($header == 'date') { |
7a5823f9 |
170 | $headers[$id][$header] = @strtotime($message['message'][$header]); |
7027794f |
171 | } else { |
7a5823f9 |
172 | $headers[$id][$header] = @$message['message'][$header]; |
7027794f |
173 | } |
174 | } |
175 | } |
176 | unset($this->messages); |
177 | unset($messages); |
178 | return $headers; |
179 | } |
180 | |
181 | /** Add storage data in spool overview |
182 | */ |
183 | public function updateSpool(array &$messages) |
184 | { |
185 | foreach ($messages as $id=>&$data) { |
186 | if (isset(Banana::$spool->overview[$id])) { |
187 | Banana::$spool->overview[$id]->storage['offset'] = $data['beginning']; |
188 | Banana::$spool->overview[$id]->storage['next'] = $data['end']; |
189 | } |
190 | } |
191 | } |
192 | |
193 | /** Return the indexes of the new messages since the give date |
194 | * @return Array(MSGNUM of new messages) |
195 | */ |
196 | public function getNewIndexes($since) |
197 | { |
198 | if (is_null($this->new_messages)) { |
199 | $this->getCount(); |
200 | } |
201 | return range($this->count - $this->new_messages, $this->count - 1); |
202 | } |
203 | |
204 | /** Return wether or not the protocole can be used to add new messages |
205 | */ |
206 | public function canSend() |
207 | { |
208 | return true; |
209 | } |
210 | |
211 | /** Return false because we can't cancel a mail |
212 | */ |
213 | public function canCancel() |
214 | { |
215 | return false; |
216 | } |
217 | |
218 | /** Return the list of requested headers |
219 | * @return Array('header1', 'header2', ...) with the key 'dest' for the destination header |
220 | * and 'reply' for the reply header, eg: |
221 | * * for a mail: Array('From', 'Subject', 'dest' => 'To', 'Cc', 'Bcc', 'reply' => 'Reply-To') |
222 | * * for a post: Array('From', 'Subject', 'dest' => 'Newsgroups', 'reply' => 'Followup-To') |
223 | */ |
224 | public function requestedHeaders() |
225 | { |
226 | return Array('From', 'Subject', 'dest' => 'To', 'Cc', 'Bcc', 'reply' => 'Reply-To'); |
227 | } |
228 | |
229 | /** Send a message |
230 | * @return true if it was successfull |
231 | */ |
232 | public function send(BananaMessage &$message) |
233 | { |
234 | return true; |
235 | } |
236 | |
237 | /** Cancel a message |
238 | * @return true if it was successfull |
239 | */ |
240 | public function cancel(BananaMessage &$message) |
241 | { |
242 | return false; |
243 | } |
244 | |
245 | /** Return the protocole name |
246 | */ |
247 | public function name() |
248 | { |
249 | return 'MBOX'; |
250 | } |
251 | |
e9360b11 |
252 | /** Return the spool filename |
253 | */ |
254 | public function filename() |
255 | { |
256 | @list($mail, $domain) = explode('@', Banana::$group); |
257 | $file = ""; |
258 | if (isset($domain)) { |
259 | $file = $domain . '_'; |
260 | } |
261 | return $file . $mail; |
262 | } |
263 | |
7027794f |
264 | ####### |
265 | # Filesystem functions |
266 | ####### |
267 | |
268 | protected function getFileName($box) |
269 | { |
270 | if (is_null($box)) { |
271 | return null; |
272 | } |
273 | @list($mail, $domain) = explode('@', $box); |
e9360b11 |
274 | return Banana::$mbox_path . '/' . $mail; |
7027794f |
275 | } |
276 | |
277 | ####### |
278 | # MBox parser |
279 | ####### |
280 | |
281 | /** Go to the given message |
282 | */ |
283 | private function goTo($id) |
284 | { |
285 | if ($this->current_id == $id && $this->at_beginning) { |
286 | return true; |
287 | } |
288 | if ($id == 0) { |
289 | fseek($this->file, 0); |
290 | $this->current_id = 0; |
291 | $this->at_beginning = true; |
292 | return true; |
293 | } elseif (isset(Banana::$spool->overview[$id]) || isset($this->messages[$id])) { |
294 | if (isset(Banana::$spool->overview[$id])) { |
295 | $pos = Banana::$spool->overview[$id]->storage['offset']; |
296 | } else { |
297 | $pos = $this->messages[$id]['beginning']; |
298 | } |
299 | if (fseek($this->file, $pos) == 0) { |
300 | $this->current_id = $id; |
301 | $this->at_beginning = true; |
302 | return true; |
303 | } else { |
304 | $this->current_id = null; |
305 | $this->_lasterrno = 2; |
306 | $this->_lasterror = _b_('Can\'t find message ') . $id; |
307 | return false; |
308 | } |
309 | } else { |
310 | $max = @max(array_keys(Banana::$spool->overview)); |
311 | if (is_null($max)) { |
312 | $max = 0; |
313 | } |
314 | if ($id <= $max && $max != 0) { |
315 | $this->current_id = null; |
316 | $this->_lasterrno = 3; |
317 | $this->_lasterror = _b_('Invalid message index ') . $id; |
318 | return false; |
319 | } |
320 | if (!$this->goTo($max)) { |
321 | return false; |
322 | } |
323 | if (feof($this->file)) { |
324 | $this->current_id = null; |
325 | $this->_lasterrno = 4; |
326 | $this->_lasterror = _b_('Requested index does not exists or file has been truncated'); |
327 | return false; |
328 | } |
329 | while ($this->readCurrentMessage(true) && $this->current_id < $id); |
330 | if ($this->current_id == $id) { |
331 | return true; |
332 | } |
333 | $this->current_id = null; |
334 | $this->_lasterrno = 5; |
335 | $this->_lasterror = _b_('Requested index does not exists or file has been truncated'); |
336 | return false; |
337 | } |
338 | } |
339 | |
340 | private function countMessages($from = 0) |
341 | { |
342 | $this->messages =& $this->readMessages(array($from), true, true); |
343 | return count($this->messages); |
344 | } |
345 | |
346 | /** Read the current message (identified by current_id) |
347 | * @param needFrom_ BOOLEAN is true if the first line *must* be a From_ line |
348 | * @param alignNext BOOLEAN is true if the buffer must be aligned at the beginning of the next From_ line |
349 | * @return message sources (without storage data) |
350 | */ |
351 | private function &readCurrentMessage($stripBody = false, $needFrom_ = true, $alignNext = true) |
352 | { |
353 | $file_cache =& $this->file_cache; |
354 | if ($file_cache && $file_cache != ftell($this->file)) { |
355 | $file_cache = null; |
356 | } |
357 | $msg = array(); |
358 | $canFrom_ = false; |
359 | $inBody = false; |
360 | while(!feof($this->file)) { |
361 | // Process file cache |
362 | if ($file_cache) { // this is a From_ line |
363 | $needFrom_ = false; |
364 | $this->at_beginning = false; |
365 | $file_cache = null; |
366 | continue; |
367 | } |
368 | |
369 | // Read a line |
370 | $line = rtrim(fgets($this->file), "\r\n"); |
371 | |
372 | // Process From_ line |
373 | if ($needFrom_ || !$msg || $canFrom_) { |
374 | if (substr($line, 0, 5) == 'From ') { // this is a From_ line |
375 | if ($needFrom_) { |
376 | $needFrom = false; |
377 | } elseif (!$msg) { |
378 | continue; |
379 | } else { |
380 | $this->current_id++; // we are finally in the next message |
381 | if ($alignNext) { // align the file pointer at the beginning of the new message |
382 | $this->at_beginning = true; |
383 | $file_cache = ftell($this->file); |
384 | } |
385 | break; |
386 | } |
387 | } elseif ($needFrom_) { |
388 | return $msg; |
389 | } |
390 | } |
391 | |
392 | // Process non-From_ lines |
393 | if (substr($line, 0, 6) == '>From ') { // remove inline From_ quotation |
394 | $line = substr($line, 1); |
395 | } |
396 | if (!$stripBody || !$inBody) { |
397 | $msg[] = $line; // add the line to the message source |
398 | } |
399 | $canFrom_ = empty($line); // check if next line can be a From_ line |
400 | if ($canFrom_ && !$inBody && $stripBody) { |
401 | $inBody = true; |
402 | } |
403 | $this->at_beginning = false; |
404 | } |
405 | if (!feof($this->file) && !$canFrom_) { |
406 | $msg = array(); |
407 | } |
408 | return $msg; |
409 | } |
410 | |
411 | /** Read message with the given ids |
412 | * @param ids ARRAY of ids to look for |
413 | * @param strip BOOLEAN if true, only headers are retrieved |
414 | * @param from BOOLEAN if true, process all messages from max(ids) to the end of the mbox |
415 | * @return Array(Array('message' => message sources (or parsed message headers if $strip is true), |
416 | * 'beginning' => offset of message beginning, |
417 | * 'end' => offset of message end)) |
418 | */ |
419 | private function &readMessages(array $ids, $strip = false, $from = false) |
420 | { |
7a5823f9 |
421 | if ($this->messages) { |
7027794f |
422 | return $this->messages; |
423 | } |
424 | sort($ids); |
425 | $messages = array(); |
426 | while ((count($ids) || $from) && !feof($this->file)) { |
427 | if (count($ids)) { |
428 | $id = array_shift($ids); |
429 | } else { |
430 | $id++; |
431 | } |
432 | if ($id != $this->current_id || !$this->at_beginning) { |
433 | if (!$this->goTo($id)) { |
434 | continue; |
435 | } |
436 | } |
437 | $beginning = ftell($this->file); |
438 | $message =& $this->readCurrentMessage($strip, false); |
439 | if ($strip) { |
440 | $message =& BananaMimePart::parseHeaders($message); |
441 | } |
442 | $end = ftell($this->file); |
443 | $messages[$id] = array('message' => $message, 'beginning' => $beginning, 'end' => $end); |
444 | } |
445 | return $messages; |
446 | } |
447 | } |
448 | |
449 | // vim:set et sw=4 sts=4 ts=4: |
450 | ?> |