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