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