Prevents error on newsgroup without any message.
[banana.git] / banana / mbox.inc.php
index 93ffff5..3483cec 100644 (file)
@@ -13,61 +13,30 @@ require_once dirname(__FILE__) . '/message.inc.php';
 
 class BananaMBox implements BananaProtocoleInterface
 {
-    private $file         = null;
-    private $filesize     = null;
-    private $current_id   = null;
-    private $at_beginning = false;
-    private $file_cache   = null;
+    private $debug      = false;
+    private $bt         = array();
 
     private $_lasterrno = 0;
     private $_lasterror = null;
 
-    private $count        = null;
-    private $new_messages = null;
-    private $messages     = null;
-
-    /** Build a protocole handler plugged on the given box
-     */
     public function __construct()
     {
-        $filename = $this->getFileName(Banana::$group);
-        if (is_null($filename)) {
-            return;
-        }
-        $this->filesize = filesize($filename);
-        $this->file = @fopen($filename, 'r');
-        if (!$this->file) {
-            $this->_lasterrno = 1;
-            $this->_lasterror = _b_('Can\'t open file');
-            $this->file = null;
-        }
-        $this->current_id   = 0;
-        $this->at_beginning = true;
-    }
-
-    /** Close the file
-     */
-    public function __destruct()
-    {
-        if ($this->file) {
-            fclose($this->file);
-        }
+        $this->debug = Banana::$debug_mbox;
     }
 
-    /** Indicate if the Protocole handler has been succesfully built
-     */
     public function isValid()
     {
-        return !Banana::$group || $this->file;
+        return true;
+        //!Banana::$group || $this->file;
     }
-    
+
     /** Indicate last error n°
      */
     public function lastErrNo()
     {
         return $this->_lasterrno;;
     }
-    
+
     /** Indicate last error text
      */
     public function lastError()
@@ -93,53 +62,63 @@ class BananaMBox implements BananaProtocoleInterface
         return array(Banana::$group => array('desc' => '', 'msgnum' => 0, 'unread' => 0));
     }
 
-    /** Return a message
-     * @param id Id of the emssage (can be either an Message-id or a message index)
-     * @return A BananaMessage or null if the given id can't be retreived
-     */
-    public function &getMessage($id)
+    private function &getRawMessage($id)
     {
         $message = null;
         if (!is_numeric($id)) {
             if (!Banana::$spool) {
                 return $message;
             }
-            $id = Banana::$spool->ids[$id];
+            $id = Banana::$spool->ids[$id]->id;
         }
-        $messages = $this->readMessages(array($id));
-        if (!empty($messages)) {
-            $message = new BananaMessage($messages[$id]['message']);
+        $options = array ('-m ' . $id);
+        $this->getMBoxPosition($options, $id);
+        return $this->callHelper('-b', $options);
+    }
+
+    /** Return a message
+     * @param id Id of the emssage (can be either an Message-id or a message index)
+     * @return A BananaMessage or null if the given id can't be retreived
+     */
+    public function &getMessage($id)
+    {
+        $messages =& $this->getRawMessage($id);
+        if ($messages) {
+            $messages = new BananaMessage($messages);
+        } else {
+            $messages = null;
         }
-        return $message;    
+        return $messages;
     }
 
     /** Return the sources of the given message
      */
     public function getMessageSource($id)
     {
-        $message = null;
-        if (!is_numeric($id)) {
-            if (!Banana::$spool) { 
-                return $message;
-            }   
-            $id = Banana::$spool->ids[$id];
-        } 
-        $message = $this->readMessages(array($id));
-        return implode("\n", $message[$id]['message']);
-    }   
+        $message =& $this->getRawMessage($id);
+        if ($message) {
+            $message = implode("\n", $message);
+        }
+        return $message;
+    }
 
     /** Compute the number of messages of the box
      */
     private function getCount()
     {
-        $this->count = count(Banana::$spool->overview);
-        $max = @max(array_keys(Banana::$spool->overview));
-        if ($max && Banana::$spool->overview[$max]->storage['next'] == $this->filesize) {
-            $this->new_messages = 0;
-        } else {
-            $this->new_messages = $this->countMessages($this->count);
-            $this->count += $this->new_messages;
-        }    
+        $options = array();
+        if (@filesize($this->getFileName()) == @Banana::$spool->storage['size']) {
+            if (!empty(Banana::$spool->overview)) {
+                return max(array_keys(Banana::$spool->overview)) + 1;
+            }
+            return 1;
+        }
+        $this->getMBoxPosition($options);
+        $val =& $this->callHelper('-c', $options);
+        if (!$val) {
+            return 0;
+        }
+        return intval(trim($val[0]));
     }
 
     /** Return the indexes of the messages presents in the Box
@@ -147,10 +126,8 @@ class BananaMBox implements BananaProtocoleInterface
      */
     public function getIndexes()
     {
-        if (is_null($this->count)) {
-            $this->getCount();
-        }
-        return array($this->count, 0, $this->count - 1);
+        $count = $this->getCount();
+        return array($count, 0, $count - 1);
     }
 
     /** Return the message headers (in BananaMessage) for messages from firstid to lastid
@@ -158,24 +135,42 @@ class BananaMBox implements BananaProtocoleInterface
      */
     public function &getMessageHeaders($firstid, $lastid, array $msg_headers = array())
     {
-        $msg_headers = array_map('strtolower', $msg_headers);
-        $messages =& $this->readMessages(range($firstid, $lastid), true);
-        $msg_headers = array_map('strtolower', $msg_headers);
-        $headers  = array();
-        foreach ($msg_headers as $header) {
-            foreach ($messages as $id=>&$message) {
-                if (!isset($headers[$id])) {
-                    $headers[$id] = array('beginning' => $message['beginning'], 'end' => $message['end']);
-                }
-                if ($header == 'date') {
-                    $headers[$id][$header] = @strtotime($message['message'][$header]);
-                } else {
-                    $headers[$id][$header] = @$message['message'][$header];
+        $headers = null;
+        $options = array();
+        $options[] = "-m $firstid:$lastid";
+        $this->getMboxPosition($options, $firstid);
+        $lines =& $this->callHelper('-d', $options, $msg_headers);
+        if (!$lines) {
+            return $headers;
+        }
+        $headers = array();
+        $in_message = false;
+        $get_pos    = true;
+        $hname      = null;
+        foreach ($lines as $key=>&$line) {
+            if (!$in_message) {
+                if (!empty($line)) {
+                    $id = intval($line);
+                    $in_message = true;
+                    $get_pos    = true;
                 }
+            } elseif ($get_pos) {
+                $headers[$id] = array('beginning' => intval($line));
+                $get_pos = false;
+            } elseif (empty($line) && empty($hname)) {
+                $in_message = false;
+            } elseif (empty($hname)) {
+                $hname = $line;
+            } elseif ($hname == 'date') {
+                $headers[$id][$hname] = @strtotime($line);
+                $hname = null;
+            } else {
+                BananaMimePart::decodeHeader($line, $hname);
+                $headers[$id][$hname] = $line;
+                $hname = null;
             }
+            unset($lines[$key]);
         }
-        unset($this->messages);
-        unset($messages);
         return $headers;
     }
 
@@ -186,9 +181,9 @@ class BananaMBox implements BananaProtocoleInterface
         foreach ($messages as $id=>&$data) {
             if (isset(Banana::$spool->overview[$id])) {
                 Banana::$spool->overview[$id]->storage['offset'] = $data['beginning'];
-                Banana::$spool->overview[$id]->storage['next']   = $data['end'];
             }
         }
+        Banana::$spool->storage['size'] = @filesize($this->getFileName());
     }
 
     /** Return the indexes of the new messages since the give date
@@ -196,8 +191,12 @@ class BananaMBox implements BananaProtocoleInterface
      */
     public function getNewIndexes($since)
     {
+        $this->open();
+        if (is_null($this->file)) {
+            return array();
+        }
         if (is_null($this->new_messages)) {
-            $this->getCount(); 
+            $this->getCount();
         }
         return range($this->count - $this->new_messages, $this->count - 1);
     }
@@ -230,15 +229,27 @@ class BananaMBox implements BananaProtocoleInterface
     /** Send a message
      * @return true if it was successfull
      */
-    public function send(BananaMessage &$message)
+    public function send(BananaMessage $message)
     {
-        return true;
+        $headers = $message->getHeaders();
+        $to      = $headers['To'];
+        $subject = $headers['Subject'];
+        unset($headers['To']);
+        unset($headers['Subject']);
+        $hdrs    = '';
+        foreach ($headers as $key=>$value) {
+            if (!empty($value)) {
+                $hdrs .= "$key: $value\r\n";
+            }
+        }
+        $body = $message->get(false);
+        return mail($to, $subject, $body, $hdrs);
     }
 
     /** Cancel a message
      * @return true if it was successfull
      */
-    public function cancel(BananaMessage &$message)
+    public function cancel(BananaMessage $message)
     {
         return false;
     }
@@ -262,16 +273,27 @@ class BananaMBox implements BananaProtocoleInterface
         return $file . $mail;
     }
 
+    /** Return the execution backtrace
+     */
+    public function backtrace()
+    {
+        if ($this->debug) {
+            return $this->bt;
+        } else {
+            return null;
+        }
+    }
+
 #######
 # Filesystem functions
 #######
 
-    protected function getFileName($box)
+    protected function getFileName()
     {
-        if (is_null($box)) {
+        if (is_null(Banana::$group)) {
             return null;
         }
-        @list($mail, $domain) = explode('@', $box);
+        @list($mail, $domain) = explode('@', Banana::$group);
         return Banana::$mbox_path . '/' . $mail;
     }
 
@@ -279,171 +301,43 @@ class BananaMBox implements BananaProtocoleInterface
 # MBox parser
 #######
 
-    /** Go to the given message
+    /** Add the '-p' optioin for callHelper
      */
-    private function goTo($id)
+    private function getMBoxPosition(array &$options, $id = null)
     {
-        if ($this->current_id == $id && $this->at_beginning) {
-            return true;
-        }
-        if ($id == 0) {
-            fseek($this->file, 0);
-            $this->current_id   = 0;
-            $this->at_beginning = true;
-            return true;
-        } elseif (isset(Banana::$spool->overview[$id]) || isset($this->messages[$id])) {
-            if (isset(Banana::$spool->overview[$id])) {
-                $pos = Banana::$spool->overview[$id]->storage['offset'];
-            } else {
-                $pos = $this->messages[$id]['beginning'];
-            }
-            if (fseek($this->file, $pos) == 0) {
-                $this->current_id   = $id;
-                $this->at_beginning = true;
-                return true;
+        if (Banana::$spool && Banana::$spool->overview) {
+            if (!is_null($id) && isset(Banana::$spool->overview[$id])) {
+                $key = $id;
             } else {
-                $this->current_id = null;
-                $this->_lasterrno = 2;
-                $this->_lasterror = _b_('Can\'t find message ') . $id;
-                return false;
-            }
-        } else {
-            $max = @max(array_keys(Banana::$spool->overview));
-            if (is_null($max)) {
-                $max = 0;
-            }
-            if ($id <= $max && $max != 0) {
-                $this->current_id = null;
-                $this->_lasterrno = 3;
-                $this->_lasterror = _b_('Invalid message index ') . $id;
-                return false;
-            }
-            if (!$this->goTo($max)) {
-                return false;
-            }
-            if (feof($this->file)) {
-                $this->current_id = null;
-                $this->_lasterrno = 4;
-                $this->_lasterror = _b_('Requested index does not exists or file has been truncated');
-                return false;
-            }
-            while ($this->readCurrentMessage(true) && $this->current_id < $id);
-            if ($this->current_id == $id) {
-                return true;
-            }
-            $this->current_id = null;
-            $this->_lasterrno = 5;
-            $this->_lasterror = _b_('Requested index does not exists or file has been truncated');
-            return false;
-        }
-    }
-
-    private function countMessages($from = 0)
-    {
-        $this->messages =& $this->readMessages(array($from), true, true);
-        return count($this->messages);
-    }
-
-    /** Read the current message (identified by current_id)
-     * @param needFrom_ BOOLEAN is true if the first line *must* be a From_ line
-     * @param alignNext BOOLEAN is true if the buffer must be aligned at the beginning of the next From_ line
-     * @return message sources (without storage data)
-     */
-    private function &readCurrentMessage($stripBody = false, $needFrom_ = true, $alignNext = true)
-    {
-        $file_cache =& $this->file_cache;
-        if ($file_cache && $file_cache != ftell($this->file)) {
-            $file_cache = null;
-        }
-        $msg        = array();
-        $canFrom_   = false;
-        $inBody     = false;
-        while(!feof($this->file)) {
-            // Process file cache
-            if ($file_cache) { // this is a From_ line
-                $needFrom_ = false;
-                $this->at_beginning = false;
-                $file_cache   = null;
-                continue;
-            }
-
-            // Read a line
-            $line    = rtrim(fgets($this->file), "\r\n");
-            
-            // Process From_ line
-            if ($needFrom_ || !$msg || $canFrom_) {
-                if (substr($line, 0, 5) == 'From ') { // this is a From_ line
-                    if ($needFrom_) {
-                        $needFrom = false;
-                    } elseif (!$msg) {
-                        continue;
-                    } else {
-                        $this->current_id++; // we are finally in the next message
-                        if ($alignNext) {  // align the file pointer at the beginning of the new message
-                            $this->at_beginning = true;
-                            $file_cache = ftell($this->file);
-                        }
-                        break;
-                    }
-                } elseif ($needFrom_) {
-                    return $msg;
+                $key = max(array_keys(Banana::$spool->overview));
+                if (!is_null($id) && $key >= $id) {
+                    return;
                 }
             }
-
-            // Process non-From_ lines
-            if (substr($line, 0, 6) == '>From ') { // remove inline From_ quotation
-                $line = substr($line, 1);
-            }
-            if (!$stripBody || !$inBody) {
-                $msg[] = $line; // add the line to the message source
-            }
-            $canFrom_ = empty($line); // check if next line can be a From_ line
-            if ($canFrom_ && !$inBody && $stripBody) {
-                $inBody = true;
+            if (isset(Banana::$spool->overview[$key]->storage['offset'])) {
+                $options[] = '-p ' . $key . ':' . Banana::$spool->overview[$key]->storage['offset'];
             }
-            $this->at_beginning = false;
-        }
-        if (!feof($this->file) && !$canFrom_) {
-            $msg = array();
         }
-        return $msg;
     }
 
-    /** Read message with the given ids
-     * @param ids ARRAY of ids to look for
-     * @param strip BOOLEAN if true, only headers are retrieved
-     * @param from BOOLEAN if true, process all messages from max(ids) to the end of the mbox
-     * @return Array(Array('message' => message sources (or parsed message headers if $strip is true),
-     *                     'beginning' => offset of message beginning,
-     *                     'end' => offset of message end))
-     */
-    private function &readMessages(array $ids, $strip = false, $from = false)
+    private function &callHelper($action, array $options = array(), array $headers = array())
     {
-        if ($this->messages) {
-            return $this->messages;
+        $action .= ' -f ' . $this->getFileName();
+        $cmd = Banana::$mbox_helper . " $action " . implode(' ', $options) . ' ' . implode(' ', $headers);
+        if ($this->debug) {
+            $start = microtime(true);
         }
-        sort($ids);
-        $messages = array();
-        while ((count($ids) || $from) && !feof($this->file)) {
-            if (count($ids)) {
-                $id = array_shift($ids);
-            } else {
-                $id++;
-            }
-            if ($id != $this->current_id || !$this->at_beginning) {
-                if (!$this->goTo($id)) {
-                    continue;
-                }
-            }
-            $beginning = ftell($this->file);
-            $message   =& $this->readCurrentMessage($strip, false);
-            if ($strip) {
-                $message =& BananaMimePart::parseHeaders($message);
-            }
-            $end       = ftell($this->file);
-            $messages[$id] = array('message' => $message, 'beginning' => $beginning, 'end' => $end);
+        exec($cmd, $out, $return);
+        if ($this->debug) {
+            $this->bt[] = array('action' => $cmd, 'time' => (microtime(true) - $start),
+                                'code' => $return, 'response' => count($out), 'error' => $return ? "Helper failed" : null);
         }
-        return $messages;
+        if ($return != 0) {
+            $this->_lasterrorno = 1;
+            $this->_lasterrorcode = "Helper failed";
+            $out = null;
+        }
+        return $out;
     }
 }