Can hide unuseful pages
[banana.git] / banana / mbox.inc.php
CommitLineData
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
10require_once dirname(__FILE__) . '/banana.inc.php';
11require_once dirname(__FILE__) . '/protocoleinterface.inc.php';
12require_once dirname(__FILE__) . '/message.inc.php';
13
14class 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 {
7d3f4749 102 $message = null;
7a5823f9 103 if (!is_numeric($id)) {
104 if (!Banana::$spool) {
7d3f4749 105 return $message;
7027794f 106 }
107 $id = Banana::$spool->ids[$id];
108 }
109 $message = $this->readMessages(array($id));
110 if (empty($message)) {
7d3f4749 111 $message = null;
112 return $message;
7027794f 113 }
7d3f4749 114 return new BananaMessage($message[$id]['message']);
7027794f 115 }
116
7a5823f9 117 /** Return the sources of the given message
118 */
119 public function getMessageSource($id)
120 {
7d3f4749 121 $message = null;
7a5823f9 122 if (!is_numeric($id)) {
123 if (!Banana::$spool) {
7d3f4749 124 return $message;
7a5823f9 125 }
126 $id = Banana::$spool->ids[$id];
7d3f4749 127 }
7a5823f9 128 $message = $this->readMessages(array($id));
7d3f4749 129 return implode("\n", $message[$id]['message']);
7a5823f9 130 }
131
132 /** Compute the number of messages of the box
133 */
7027794f 134 private function getCount()
135 {
136 $this->count = count(Banana::$spool->overview);
137 $max = @max(array_keys(Banana::$spool->overview));
138 if ($max && Banana::$spool->overview[$max]->storage['next'] == $this->filesize) {
139 $this->new_messages = 0;
140 } else {
141 $this->new_messages = $this->countMessages($this->count);
142 $this->count += $this->new_messages;
143 }
144 }
145
146 /** Return the indexes of the messages presents in the Box
147 * @return Array(number of messages, MSGNUM of the first message, MSGNUM of the last message)
148 */
149 public function getIndexes()
150 {
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 {
162 $msg_headers = array_map('strtolower', $msg_headers);
163 $messages =& $this->readMessages(range($firstid, $lastid), true);
164 $msg_headers = array_map('strtolower', $msg_headers);
165 $headers = array();
166 foreach ($msg_headers as $header) {
167 foreach ($messages as $id=>&$message) {
168 if (!isset($headers[$id])) {
169 $headers[$id] = array('beginning' => $message['beginning'], 'end' => $message['end']);
170 }
171 if ($header == 'date') {
7a5823f9 172 $headers[$id][$header] = @strtotime($message['message'][$header]);
7027794f 173 } else {
7a5823f9 174 $headers[$id][$header] = @$message['message'][$header];
7027794f 175 }
176 }
177 }
178 unset($this->messages);
179 unset($messages);
180 return $headers;
181 }
182
183 /** Add storage data in spool overview
184 */
185 public function updateSpool(array &$messages)
186 {
187 foreach ($messages as $id=>&$data) {
188 if (isset(Banana::$spool->overview[$id])) {
189 Banana::$spool->overview[$id]->storage['offset'] = $data['beginning'];
190 Banana::$spool->overview[$id]->storage['next'] = $data['end'];
191 }
192 }
193 }
194
195 /** Return the indexes of the new messages since the give date
196 * @return Array(MSGNUM of new messages)
197 */
198 public function getNewIndexes($since)
199 {
200 if (is_null($this->new_messages)) {
201 $this->getCount();
202 }
203 return range($this->count - $this->new_messages, $this->count - 1);
204 }
205
206 /** Return wether or not the protocole can be used to add new messages
207 */
208 public function canSend()
209 {
210 return true;
211 }
212
213 /** Return false because we can't cancel a mail
214 */
215 public function canCancel()
216 {
217 return false;
218 }
219
220 /** Return the list of requested headers
221 * @return Array('header1', 'header2', ...) with the key 'dest' for the destination header
222 * and 'reply' for the reply header, eg:
223 * * for a mail: Array('From', 'Subject', 'dest' => 'To', 'Cc', 'Bcc', 'reply' => 'Reply-To')
224 * * for a post: Array('From', 'Subject', 'dest' => 'Newsgroups', 'reply' => 'Followup-To')
225 */
226 public function requestedHeaders()
227 {
228 return Array('From', 'Subject', 'dest' => 'To', 'Cc', 'Bcc', 'reply' => 'Reply-To');
229 }
230
231 /** Send a message
232 * @return true if it was successfull
233 */
234 public function send(BananaMessage &$message)
235 {
236 return true;
237 }
238
239 /** Cancel a message
240 * @return true if it was successfull
241 */
242 public function cancel(BananaMessage &$message)
243 {
244 return false;
245 }
246
247 /** Return the protocole name
248 */
249 public function name()
250 {
251 return 'MBOX';
252 }
253
e9360b11 254 /** Return the spool filename
255 */
256 public function filename()
257 {
258 @list($mail, $domain) = explode('@', Banana::$group);
259 $file = "";
260 if (isset($domain)) {
261 $file = $domain . '_';
262 }
263 return $file . $mail;
264 }
265
7027794f 266#######
267# Filesystem functions
268#######
269
270 protected function getFileName($box)
271 {
272 if (is_null($box)) {
273 return null;
274 }
275 @list($mail, $domain) = explode('@', $box);
e9360b11 276 return Banana::$mbox_path . '/' . $mail;
7027794f 277 }
278
279#######
280# MBox parser
281#######
282
283 /** Go to the given message
284 */
285 private function goTo($id)
286 {
287 if ($this->current_id == $id && $this->at_beginning) {
288 return true;
289 }
290 if ($id == 0) {
291 fseek($this->file, 0);
292 $this->current_id = 0;
293 $this->at_beginning = true;
294 return true;
295 } elseif (isset(Banana::$spool->overview[$id]) || isset($this->messages[$id])) {
296 if (isset(Banana::$spool->overview[$id])) {
297 $pos = Banana::$spool->overview[$id]->storage['offset'];
298 } else {
299 $pos = $this->messages[$id]['beginning'];
300 }
301 if (fseek($this->file, $pos) == 0) {
302 $this->current_id = $id;
303 $this->at_beginning = true;
304 return true;
305 } else {
306 $this->current_id = null;
307 $this->_lasterrno = 2;
308 $this->_lasterror = _b_('Can\'t find message ') . $id;
309 return false;
310 }
311 } else {
312 $max = @max(array_keys(Banana::$spool->overview));
313 if (is_null($max)) {
314 $max = 0;
315 }
316 if ($id <= $max && $max != 0) {
317 $this->current_id = null;
318 $this->_lasterrno = 3;
319 $this->_lasterror = _b_('Invalid message index ') . $id;
320 return false;
321 }
322 if (!$this->goTo($max)) {
323 return false;
324 }
325 if (feof($this->file)) {
326 $this->current_id = null;
327 $this->_lasterrno = 4;
328 $this->_lasterror = _b_('Requested index does not exists or file has been truncated');
329 return false;
330 }
331 while ($this->readCurrentMessage(true) && $this->current_id < $id);
332 if ($this->current_id == $id) {
333 return true;
334 }
335 $this->current_id = null;
336 $this->_lasterrno = 5;
337 $this->_lasterror = _b_('Requested index does not exists or file has been truncated');
338 return false;
339 }
340 }
341
342 private function countMessages($from = 0)
343 {
344 $this->messages =& $this->readMessages(array($from), true, true);
345 return count($this->messages);
346 }
347
348 /** Read the current message (identified by current_id)
349 * @param needFrom_ BOOLEAN is true if the first line *must* be a From_ line
350 * @param alignNext BOOLEAN is true if the buffer must be aligned at the beginning of the next From_ line
351 * @return message sources (without storage data)
352 */
353 private function &readCurrentMessage($stripBody = false, $needFrom_ = true, $alignNext = true)
354 {
355 $file_cache =& $this->file_cache;
356 if ($file_cache && $file_cache != ftell($this->file)) {
357 $file_cache = null;
358 }
359 $msg = array();
360 $canFrom_ = false;
361 $inBody = false;
362 while(!feof($this->file)) {
363 // Process file cache
364 if ($file_cache) { // this is a From_ line
365 $needFrom_ = false;
366 $this->at_beginning = false;
367 $file_cache = null;
368 continue;
369 }
370
371 // Read a line
372 $line = rtrim(fgets($this->file), "\r\n");
373
374 // Process From_ line
375 if ($needFrom_ || !$msg || $canFrom_) {
376 if (substr($line, 0, 5) == 'From ') { // this is a From_ line
377 if ($needFrom_) {
378 $needFrom = false;
379 } elseif (!$msg) {
380 continue;
381 } else {
382 $this->current_id++; // we are finally in the next message
383 if ($alignNext) { // align the file pointer at the beginning of the new message
384 $this->at_beginning = true;
385 $file_cache = ftell($this->file);
386 }
387 break;
388 }
389 } elseif ($needFrom_) {
390 return $msg;
391 }
392 }
393
394 // Process non-From_ lines
395 if (substr($line, 0, 6) == '>From ') { // remove inline From_ quotation
396 $line = substr($line, 1);
397 }
398 if (!$stripBody || !$inBody) {
399 $msg[] = $line; // add the line to the message source
400 }
401 $canFrom_ = empty($line); // check if next line can be a From_ line
402 if ($canFrom_ && !$inBody && $stripBody) {
403 $inBody = true;
404 }
405 $this->at_beginning = false;
406 }
407 if (!feof($this->file) && !$canFrom_) {
408 $msg = array();
409 }
410 return $msg;
411 }
412
413 /** Read message with the given ids
414 * @param ids ARRAY of ids to look for
415 * @param strip BOOLEAN if true, only headers are retrieved
416 * @param from BOOLEAN if true, process all messages from max(ids) to the end of the mbox
417 * @return Array(Array('message' => message sources (or parsed message headers if $strip is true),
418 * 'beginning' => offset of message beginning,
419 * 'end' => offset of message end))
420 */
421 private function &readMessages(array $ids, $strip = false, $from = false)
422 {
7a5823f9 423 if ($this->messages) {
7027794f 424 return $this->messages;
425 }
426 sort($ids);
427 $messages = array();
428 while ((count($ids) || $from) && !feof($this->file)) {
429 if (count($ids)) {
430 $id = array_shift($ids);
431 } else {
432 $id++;
433 }
434 if ($id != $this->current_id || !$this->at_beginning) {
435 if (!$this->goTo($id)) {
436 continue;
437 }
438 }
439 $beginning = ftell($this->file);
440 $message =& $this->readCurrentMessage($strip, false);
441 if ($strip) {
442 $message =& BananaMimePart::parseHeaders($message);
443 }
444 $end = ftell($this->file);
445 $messages[$id] = array('message' => $message, 'beginning' => $beginning, 'end' => $end);
446 }
447 return $messages;
448 }
449}
450
451// vim:set et sw=4 sts=4 ts=4:
452?>