Rework all sources :
authorx2003bruneau <x2003bruneau@9869982d-c50d-0410-be91-f2a2ec7c7c7b>
Tue, 9 Jan 2007 13:55:03 +0000 (13:55 +0000)
committerFlorent Bruneau <florent.bruneau@polytechnique.org>
Fri, 4 Jan 2008 23:35:12 +0000 (00:35 +0100)
* add protocole abstraction and a mbox parser
* better message parser
* use Smarty for html generation
* remove php4 compatibility

git-svn-id: svn+ssh://murphy/home/svn/banana/trunk@147 9869982d-c50d-0410-be91-f2a2ec7c7c7b

21 files changed:
banana/NetNNTP.inc.php [deleted file]
banana/banana.inc.php.in
banana/groups.inc.php [deleted file]
banana/mbox.inc.php [new file with mode: 0644]
banana/message.func.inc.php [new file with mode: 0644]
banana/message.inc.php [new file with mode: 0644]
banana/mimepart.inc.php [new file with mode: 0644]
banana/misc.inc.php [deleted file]
banana/nntp.inc.php [new file with mode: 0644]
banana/nntpcore.inc.php [new file with mode: 0644]
banana/page.inc.php [new file with mode: 0644]
banana/post.inc.php [deleted file]
banana/protocoleinterface.inc.php [new file with mode: 0644]
banana/spool.inc.php
banana/templates/banana-base.tpl [new file with mode: 0644]
banana/templates/banana-boxlist.inc.tpl [new file with mode: 0644]
banana/templates/banana-message.inc.tpl [new file with mode: 0644]
banana/templates/banana-newmessage.inc.tpl [new file with mode: 0644]
banana/templates/banana-thread.inc.tpl [new file with mode: 0644]
banana/text.func.inc.php [moved from banana/utf8.php with 85% similarity]
css/style.css

diff --git a/banana/NetNNTP.inc.php b/banana/NetNNTP.inc.php
deleted file mode 100644 (file)
index 3934cd5..0000000
+++ /dev/null
@@ -1,512 +0,0 @@
-<?php
-/********************************************************************************
-* include/NetNNTP.inc.php : NNTP subroutines
-* -------------------------
-*
-* This file is part of the banana distribution
-* Copyright: See COPYING files that comes with this distribution
-********************************************************************************/
-
-/** Class NNTP
- *  implements some basic functions for NNTP protocol
- */
-class nntp
-{
-    /** socket filehandle */
-    var $ns;
-    /** posting allowed */
-    var $posting;
-    /** last NNTP error code */
-    var $lasterrorcode;
-    /** last NNTP error text */
-    var $lasterrortext;
-    /** test validity */
-    var $valid = true;
-
-    /** constructor
-     * @param $_host STRING NNTP host
-     * @param $_timeout INTEGER socket timeout
-     * @param $_reader BOOLEAN sends a "MODE READER" at connection if true
-     */
-
-    function nntp($_url, $_timeout=120, $_reader=true)
-    {
-        $url['port'] = 119;
-        $url         = parse_url($_url);
-        $this->ns    = fsockopen($url['host'], $url['port'], $errno, $errstr, $_timeout);
-        if (!$this->ns) {
-            $this->valid = false;
-            return null;
-        }
-
-        $result        = $this->gline(); 
-        $this->posting = (substr($result, 0, 3)=="200");
-        if ($_reader && ($result{0}=="2")) {
-            $this->pline("MODE READER\r\n");
-            $result        = $this->gline();
-            $this->posting = ($result{0}=="200");
-        }
-        if ($result{0}=="2" && isset($url['user']) && $url['user'] != 'anonymous') {
-            return $this->authinfo($url['user'], $url['pass']);
-        }
-        return ($result{0}=="2");
-    }
-
-# Socket functions
-
-    /** get a line from server
-     * @return STRING 
-     */
-
-    function gline()
-    {
-        return rtrim(fgets($this->ns, 1200));
-    }
-
-    /** puts a line on server
-     * @param STRING $_line line to put
-     */
-
-    function pline($_line)
-    {
-        return fputs($this->ns, $_line, strlen($_line));
-    }
-
-# strict NNTP Functions [RFC 977]
-# see http://www.faqs.org/rfcs/rfc977.html
-
-    /** authentification
-     * @param $_user STRING login
-     * @param $_pass INTEGER password
-     * @return BOOLEAN true if authentication was successful
-     */
-
-    function authinfo($_user, $_pass)
-    {
-        $user = preg_replace("/(\r|\n)/", "", $_user);
-        $pass = preg_replace("/(\r|\n)/", "", $_pass);
-        $this->pline("AUTHINFO USER $user\r\n");
-        $this->gline();
-        $this->pline("AUTHINFO PASS $pass\r\n");
-        $result=$this->gline();
-        if ($result{0}!="2") {
-            $this->lasterrorcode = substr($result, 0, 3);
-            $this->lasterrortext = substr($result, 4);
-            return false;
-        }
-        return true;
-    }
-
-    /** retrieves an article
-     * MSGID is a numeric ID a shown in article's headers. MSGNUM is a
-     * server-dependent ID (see X-Ref on many servers) and retriving 
-     * an article by this way will change the current article pointer.
-     * If an error occur, false is returned. 
-     * @param $_msgid STRING MSGID or MSGNUM of article
-     * @return ARRAY lines of the article
-     * @see body
-     * @see head
-     */
-
-    function article($_msgid="")
-    {
-        $msgid = preg_replace("/(\r|\n)/", "", $_msgid);
-        $this->pline("ARTICLE $msgid\r\n");
-        $result = $this->gline();
-        if ($result{0} != '2') {
-            $this->lasterrorcode = substr($result, 0, 3);
-            $this->lasterrortext = substr($result, 4);
-            return false;
-        }
-        $result = $this->gline();
-        while ($result != ".") {
-            $array[] = $result;
-            $result  = $this->gline();
-        }
-        return $array;
-    }
-
-    /** post a message
-     * if an error occur, false is returned
-     * @param $_message STRING message to post
-     * @return STRING MSGID of article 
-     */
-
-    function post($_message)
-    {
-        if (is_array($_message)) {
-            $message=join("\n", $_message);
-        } else {
-            $message=$_message;
-        }
-        $this->pline("POST \r\n");
-        $result=$this->gline();
-        if ($result{0} != '3') {
-            $this->lasterrorcode = substr($result, 0, 3);
-            $this->lasterrortext = substr($result, 4);
-            return false;
-        }
-        $this->pline($message."\r\n.\r\n");
-        $result = $this->gline();
-        if ($result{0} != '2') {
-            $this->lasterrorcode = substr($result, 0, 3);
-            $this->lasterrortext = substr($result, 4);
-            return false;
-        }
-        if ($result{0} == '2') {
-            if (preg_match("/(<[^@>]+@[^@>]+>)/", $result, $regs)) {
-                return $regs[0];
-            } else {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    /** fetches the body of an article
-     * params are the same as article
-     * @param $_msgid STRING MSGID or MSGNUM of article
-     * @return ARRAY lines of the article
-     * @see article
-     * @see head
-     */
-
-    function body($_msgid="")
-    {
-        $msgid = preg_replace("/(\r|\n)/", "", $_msgid);
-        $this->pline("BODY $msgid\r\n");
-        $result = $this->gline();
-        if ($result{0} != '2') {
-            $this->lasterrorcode = substr($result, 0, 3);
-            $this->lasterrortext = substr($result, 4);
-            return false;
-        }
-        $array = Array();
-        while (($result = $this->gline()) != ".") {
-            $array[] = $result;
-        }
-        return $array;
-    }
-
-    /** fetches the headers of an article
-     * params are the same as article
-     * @param $_msgid STRING MSGID or MSGNUM of article
-     * @return ARRAY lines of the article
-     * @see article
-     * @see body
-     */
-
-    function head($_msgid="")
-    {
-        $msgid = preg_replace("/(\r|\n)/", "", $_msgid);
-        $this->pline("HEAD $msgid\r\n");
-        $result = $this->gline();
-        if ($result{0}!="2") {
-            $this->lasterrorcode = substr($result, 0, 3);
-            $this->lasterrortext = substr($result, 4);
-            return false;
-        }
-        $result = $this->gline();
-        while ($result != ".") {
-            $array[] = $result;
-            $result  = $this->gline();
-        }
-        return $array;
-    }
-
-    /** set current group
-     * @param $_group STRING 
-     * @return ARRAY array : nb of articles in group, MSGNUM of first article, MSGNUM of last article, and group name
-     */
-
-    function group($_group)
-    {
-        $group = preg_replace("/(\r|\n)/", "", $_group);
-        $this->pline("GROUP $group\r\n");
-        $line = $this->gline();
-        if ($line{0}!="2") {
-            $this->lasterrorcode = substr($line, 0, 3);
-            $this->lasterrortext = substr($line, 4);
-            return false;
-        }
-        if (preg_match("/^2\d{2} (\d+) (\d+) (\d+) ([^ ]+)/", $line, $regs)) {
-            return array($regs[1], $regs[2], $regs[3], $regs[4]);
-        }
-        return false;
-    }
-
-    /** set the article pointer to the previous article in current group
-     * @return STRING MSGID of article
-     * @see next
-     */
-
-    function last()
-    {
-        $this->pline("LAST \r\n");
-        $line = $this->gline();
-        if ($line{0}!="2") {
-            $this->lasterrorcode = substr($result, 0, 3);
-            $this->lasterrortext = substr($result, 4);
-            return false;
-        }
-        if (preg_match("/^2\d{2} \d+ <([^>]+)>/", $line, $regs)) {
-            return "<{$regs[1]}>";
-        }
-        return false;
-    }
-
-    /** set the article pointer to the next article in current group
-     * @return STRING MSGID of article
-     * @see last
-     */
-
-    function next()
-    {
-        $this->pline("NEXT \r\n");
-        $line = $this->gline();
-        if ($line{0}!="2") {
-            $this->lasterrorcode = substr($result, 0, 3);
-            $this->lasterrortext = substr($result, 4);
-            return false;
-        }
-        if (preg_match("/^2\d{2} \d+ <([^>]+)>/", $line, $regs)) {
-            return "<{$regs[1]}>";
-        }
-        return false;
-    }
-
-    /** set the current article pointer
-     * @param $_msgid STRING MSGID or MSGNUM of article
-     * @return BOOLEAN true if authentication was successful, error code otherwise
-     * @see article
-     * @see body
-     */
-
-    function nntpstat($_msgid)
-    {
-        $msgid = preg_replace("/(\r|\n)/", "", $_msgid);
-        $this->pline("STAT $msgid\r\n");
-        $line  = $this->gline();
-        if ($line{0}!="2") {
-            $this->lasterrorcode = substr($result, 0, 3);
-            $this->lasterrortext = substr($result, 4);
-            return false;
-        }
-        if (preg_match("/^2\d{2} \d+ <([^>]+)>/", $line, $regs)) {
-            return "<{$regs[1]}>";
-        }
-        return false;
-    }
-
-    /** returns true if posting is allowed
-     * @return BOOLEAN true if posting is allowed lines 
-     */
-
-    function postok()
-    {
-        return ($this->posting);
-    }
-
-    /** retreive the group list
-     * @return ARRAY group name => (MSGNUM of first article, MSGNUM of last article, NNTP flags)
-     * @see newgroups, liste
-     */
-    function _grouplist()
-    {
-        global $banana;
-
-        if (substr($this->gline(), 0, 1)!="2") {
-            return false;
-        }
-        $result = $this->gline();
-        $array  = Array();
-        while ($result != ".") {
-            preg_match("/([^ ]+) (\d+) (\d+) (.)/", $result, $regs);
-            if (!isset($banana->grp_pattern) || preg_match('@'.$banana->grp_pattern.'@', $regs[1])) {
-                $array[$regs[1]] = array(intval($regs[2]), intval($regs[3]), intval($regs[4]));
-            }
-            $result = $this->gline();
-        }
-        return $array;                                                                                                                                        
-    }
-
-    /** gets information about all active newsgroups
-     * @return ARRAY group name => (MSGNUM of first article, MSGNUM of last article, NNTP flags)
-     * @see newgroups
-     */
-
-    function liste()
-    {
-        $this->pline("LIST\r\n");
-        return $this->_grouplist();
-    }
-
-    /** get information about recent newsgroups 
-     * same as list, but information are limited to newgroups created after $_since
-     * @param $_since INTEGER unix timestamp
-     * @param $_distributions STRING distributions 
-     * @return ARRAY same format as liste
-     * @see liste
-     */
-
-    function newgroups($_since, $_distributions="")
-    {
-#assume $_since is a unix timestamp
-        $distributions = preg_replace("/(\r|\n)/", "", $_distributions);
-        $this->pline("NEWGROUPS ".gmdate("ymd His", $_since)
-                ." GMT $distributions\r\n");
-        return $this->_grouplist();
-    }
-
-    /** gets a list of new articles
-     * @param $_since INTEGER unix timestamp
-     * @parma $_groups STRING pattern of intersting groups 
-     * @return ARRAY MSGID of new articles
-     */
-
-    function newnews($_since, $_groups="*", $_distributions="")
-    {
-        $distributions = preg_replace("/(\r|\n)/", "", $_distributions);
-        $groups = preg_replace("/(\r|\n)/", "", $_groups);
-        $array  = array();
-#assume $since is a unix timestamp
-        $this->pline("NEWNEWS $_groups ".gmdate("ymd His", $_since)." GMT $distributions\r\n");
-        if (substr($this->gline(), 0, 1)!="2") {
-            return false;
-        }
-        while (($result = $this->gline()) != ".") {
-            $array[] = $result;
-        }
-        return $array;
-    }
-
-    /** Tell the remote server that I am not a user client, but probably another news server
-     * @return BOOLEAN true if sucessful
-     */
-
-    function slave()
-    {
-        $this->pline("SLAVE \r\n");
-        return (substr($this->gline(), 0, 1)=="2");
-    }
-
-    /** implements IHAVE method
-     * @param $_msgid STRING MSGID of article
-     * @param $_message STRING article
-     * @return BOOLEAN 
-     */
-
-    function ihave($_msgid, $_message=false)
-    {
-        $msgid = preg_replace("/(\r|\n)/", "", $_msgid);
-        if (is_array($message)) {
-            $message = join("\n", $_message);
-        } else {
-            $message = $_message;
-        }
-        $this->pline("IHAVE $msgid \r\n");
-        $result = $this->gline();
-        if ($message && ($result{0}=="3")) {
-            $this->pline("$message\r\n.\r\n");
-            $result = $this->gline();
-        }
-        return ($result{0}=="2");
-    }
-
-    /** closes connection to server
-     */
-
-    function quit()
-    {
-        $this->pline("QUIT\r\n");
-        $this->gline();
-        fclose($this->ns);
-    }
-
-# NNTP Extensions [RFC 2980]
-
-    /** Returns the date on the remote server
-     * @return INTEGER timestamp 
-     */
-
-    function date()
-    {
-        $this->pline("DATE \r\n");
-        $result = $this->gline();
-        if (preg_match("/^111 (\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})$/", $result, $r)) {
-            return gmmktime($r[4], $r[5], $r[6], $r[2], $r[3], $r[1]);
-        }
-        return false;
-    }
-
-    /** returns group descriptions
-     * @param $_pattern STRING pattern of intersting groups
-     * @return ARRAY group name => description
-     */
-
-    function xgtitle($_pattern="*")
-    {
-        $pattern = preg_replace("/[\r\n]/", "", $_pattern);
-        $this->pline("XGTITLE $pattern \r\n");
-        if (substr($this->gline(), 0, 1)!="2") return false;
-        $result = $this->gline();
-        while ($result != ".") {
-            preg_match("/([^ \t]+)[ \t]+(.+)$/", $result, $regs);
-            $array[$regs[1]] = $regs[2];
-            $result          = $this->gline();
-        }
-        return $array;
-    }
-
-    /** obtain the header field $hdr for all the messages specified
-     * @param $_hdr STRING name of the header (eg: 'From')
-     * @param $_range STRING range of articles 
-     * @return ARRAY MSGNUM => header value
-     */
-
-    function xhdr($_hdr, $_range="")
-    {
-        $hdr    = preg_replace("/(\r|\n)/", "", $_hdr);
-        $range  = preg_replace("/(\r|\n)/", "", $_range);
-        $this->pline("XHDR $hdr $range \r\n");
-        if (substr($this->gline(), 0, 1)!="2") {
-            return false;
-        }
-            
-        $array  = array();
-        while (($result = $this->gline()) != '.') {
-            if (preg_match("/([^ \t]+) (.*)$/", $result, $regs)) {
-                $array[$regs[1]] = $regs[2];
-            }
-        }
-        return $array;
-    }
-
-    /** obtain the header field $_hdr matching $_pat for all the messages specified
-     * @param $_hdr STRING name of the header (eg: 'From')
-     * @param $_range STRING range of articles 
-     * @param $_pat STRING pattern
-     * @return ARRAY MSGNUM => header value
-     */
-
-    function xpat($_hdr, $_range, $_pat)
-    {
-        $hdr   = preg_replace("/(\r|\n)/", "", $_hdr);
-        $range = preg_replace("/(\r|\n)/", "", $_range);
-        $pat   = preg_replace("/(\r|\n)/", "", $_pat);
-        $this->pline("XPAT $hdr $range $pat\r\n");
-        if (substr($this->gline(), 0, 1)!="2") {
-            return false;
-        }
-        $result = $this->gline();
-        while ($result != ".") {
-            preg_match("/([^ \t]+) (.*)$/", $result, $regs);
-            $array[$regs[1]] = $regs[2];
-            $result          = $this->gline();
-        }
-        return $array;
-    }
-}
-
-// vim:set et sw=4 sts=4 ts=4 
-?>
index 86df250..fadd3a7 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /********************************************************************************
-* install.d/config.inc.php : configuration file
+* banana/banana.inc.php : banana main file
 * --------------------------
 *
 * This file is part of the banana distribution
@@ -9,41 +9,43 @@
 
 class Banana
 {
-    var $maxspool    = 3000;
+    static public $maxspool    = 3000;
 
-    var $hdecode     = array('from','name','organization','subject');
-    var $parse_hdr   = array('content-disposition', 'content-transfer-encoding',
-                             'content-type', 'content-id', 'date', 'followup-to',
-                             'from', 'message-id', 'newsgroups', 'organization',
-                             'references', 'subject', 'x-face');
-    var $show_hdr    = array('from', 'newsgroups', 'followup', 'date',
-                             'organization', 'references', 'x-face');
+    static public $parse_hdr   = array('content-disposition', 'content-transfer-encoding',
+                                       'content-type', 'content-id', 'date', 'followup-to',
+                                       'from', 'message-id', 'newsgroups', 'organization',
+                                       'references', 'subject', 'x-face', 'in-reply-to',
+                                       'to', 'cc', 'reply-to');
+    static public $show_hdr    = array('from', 'newsgroups', 'followup-to', 'to', 'cc', 'reply-to',
+                                       'organization', 'date', 'references', 'in-reply-to');
 
     /** Favorites MIMEtypes to use, by order for reading multipart messages
      */
-    var $body_mime   = array('text/plain', 'text/html', 'text/richtext');
+    static public $body_mime   = array('text/html', 'text/plain', 'text/enriched', 'text', 'message');
+
     /** Indicate wether posting attachment is allowed
      */
-    var $can_attach  = true;
+    static public $can_attach  = true;
     /** Maximum allowed file size for attachment
      */
-    var $maxfilesize = 100000;
+    static public $maxfilesize = 100000;
     /** Indicate wether x-face should be skinned as specials data or not
      */
-    var $formatxface = true;
+    static public $formatxface = true;
 
     /** Regexp for selecting newsgroups to show (if empty, match all newsgroups)
      * ex : '^xorg\..*' for xorg.*
      */
-    var $grp_pattern;
+    static public $grp_pattern = null;
 
-    var $tbefore     = 5;
-    var $tafter      = 5;
-    var $tmax        = 50;
+    static public $tbefore     = 5;
+    static public $tafter      = 5;
+    static public $tmax        = 50;
 
-    var $wrap        = 74;
+    static public $wrap        = 78;
+    
     /** Match an url
-     * Should be included in a regexp delimited using ! (eg: "!$url_regexp!i")
+     * Should be included in a regexp delimited using /, !, , or @ (eg: "/$url_regexp/i")
      * If it matches, return 3 main parts :
      *  \\1 and \\3 are delimiters
      *  \\2 is the url
@@ -53,526 +55,426 @@ class Banana
      *   $matches[2] = "http://www.polytechnique.org"
      *   $matches[3] = "]"
      */
-    var $url_regexp  = '(["\[])?((?:https?|ftp|news)://(?:&amp;|\.*,*[a-z@0-9~%$£µ&i#\-+=_/\?])*)(["\]])?';
+    static public $url_regexp  = '(["\[])?((?:[a-z]+:\/\/|www\.)(?:[\.\,\;\!]*[a-z\@0-9~%$£µ&i#\-+=_\/\?]+)+)(["\]])?';
 
-    
-    /** Boundary for multipart messages
-     */
-    var $boundary    = 'bananaBoundary42';
     /** Global headers to use for messages
      */
-    var $custom      = "Mime-Version: 1.0\nUser-Agent: Banana @VERSION@\n";
-    /** Global headers to use from multipart messages
-     */
-    var $custom_mp   = "Content-Type: multipart/mixed; boundary=\"bananaBoundary42\"\nContent-Transfer-Encoding: 7bit\n";
-    /** Body type when using plain text
-     */
-    var $custom_plain= "Content-Type: text/plain; charset=utf-8\nContent-Transfert-Encoding: 8bit\n"; 
+    static public $custom_hdr  = array('Mime-Version' => '1.0', 'User-Agent' => 'Banana @VERSION@');
 
     /** News serveur to use
      */
-    var $host        = 'news://localhost:119/';
+    static public $host        = 'news://localhost:119/';
 
     /** User profile
      */
-    var $profile     = Array( 'name' => 'Anonymous <anonymouse@example.com>', 'sig'  => '', 'org'  => '',
-            'customhdr' =>'', 'display' => 0, 'lastnews' => 0, 'locale'  => 'fr_FR', 'subscribe' => array());
-    
-    var $state       = Array('group' => null, 'artid' => null, 'action' => null);
-    var $nntp;
-    var $groups;
-    var $newgroups;
-    var $post;
-    var $spool;
-
-    var $get;
-
-    function Banana()
-    {
-        $this->_require('NetNNTP');
-        setlocale(LC_ALL,  $this->profile['locale']);
-        $this->nntp = new nntp($this->host);
-        if (!$this->nntp || !$this->nntp->valid) {
-            $this->nntp = null;
-        }
-    }
+    static public $profile     = Array( 'From' => 'Anonymous <anonymouse@example.com>', 'sig'  => '',
+                                        'Organization'  => '', 'custom_hdr' => array(), 'display' => 0,
+                                        'lastnews' => 0, 'locale'  => 'fr_FR', 'subscribe' => array());
+
+    static public $protocole   = null;
+    static public $spool       = null;
+    static public $message     = null;
+    static public $page        = null;
+
+    static public $group       = null;
+    static public $artid       = null;
+    static public $action      = null;
+    static public $part        = null;
+    static public $first       = null;
+
+    static public $debug_nntp   = false;
+    static public $debug_smarty = false;
+
+
+    // Actions
+    const ACTION_BOX_NEEDED = 1; // mask
+    const ACTION_BOX_LIST   = 2;
+    const ACTION_BOX_SUBS   = 4;
+    const ACTION_MSG_LIST   = 3;
+    const ACTION_MSG_READ   = 5;
+    const ACTION_MSG_NEW    = 9;
+    const ACTION_MSG_CANCEL = 17;
+
+    // Box list view
+    const BOXES_ALL = 0;
+    const BOXES_SUB = 1;
+    const BOXES_NEW = 2;
+
+    // Spool view mode
+    const SPOOL_ALL    = 0;
+    const SPOOL_UNREAD = 1;
+
+    /** Class parameters storage
+     */
+    public $params;
 
-    /** Run Banana
-     * @param STRING class Name of the class to use
-     * @param ARRAY  myget If defined is used instead of get
+    /** Build the instance of Banana
+     * This constructor only call \ref loadParams, connect to the server, and build the Smarty page
+     * @param protocole Protocole to use
      */
-    function run($class = 'Banana', $myget = null)
+    public function __construct($params = null, $protocole = 'NNTP', $pageclass = 'BananaPage')
     {
-        global $banana;
-
-        Banana::_require('misc');
-        $banana = new $class();
-
-        if (is_null($myget)) {
-            $banana->get = $_GET;
+        Banana::load('text.func');
+        if (is_null($params)) {
+            $this->params = $_GET;
         } else {
-            $banana->get = $myget;
+            $this->params = $params;
         }
+        $this->loadParams();
 
-        if (!$banana->nntp) {
-            $banana->state['page'] = 'error';
-            return makeTable('<p class="error">'._b_('Impossible de contacter le serveur').'</p>');
-        }
+        // connect to protocole handler
+        Banana::load($protocole);
+        $classname = 'Banana' . $protocole;
+        Banana::$protocole = new $classname(Banana::$group);
 
-        $group  = empty($banana->get['group']) ? null : $banana->get['group'];
-        if (!is_null($group)
-                &&  isset($banana->grp_pattern) && !preg_match('/' . $banana->grp_pattern . '/', $group)) {
-            $banana->state['page'] = 'error';
-            return makeTable('<p class="error">'
-                    . $group . _b_(' : ce newsgroup n\'existe pas ou vous n\'avez pas l\'autorisation d\'y accéder')
-                    . '</p>');
+        // build the page
+        if ($pageclass == 'BananaPage') {
+            Banana::load('page');
         }
-        $artid  = empty($banana->get['artid']) ? null : strtolower($banana->get['artid']);
-        $partid = !isset($banana->get['part']) ? -1 : $banana->get['part'];
-        $action = !isset($banana->get['action']) ? null : $banana->get['action'];
-        $banana->state = Array ('group' => $group, 'artid' => $artid, 'action' => $action);
-
-        if (is_null($group)) {
-            if (isset($banana->get['subscribe'])) {
-                $banana->state['page'] = 'subscribe';
-                return makeTable($banana->action_listSubs());
-            } elseif (isset($_POST['validsubs'])) {
-                $banana->action_saveSubs();
-            }
-            $banana->state['page'] = 'forums';
-            return makeTable($banana->action_listGroups());
-
-        } elseif (is_null($artid)) {
-            if (isset($_POST['action']) && $_POST['action'] == 'new') {
-                return makeTable($banana->action_doFup($group, isset($_POST['artid']) ? intval($_POST['artid']) : -1));
-            } elseif ($action == 'new') {
-                $banana->state['page'] = 'action';
-                return makeTable($banana->action_newFup($group));
-            } else {
-                $banana->state['page'] = 'group';
-                return makeTable($banana->action_showThread($group, isset($banana->get['first']) ? intval($banana->get['first']) : 1));
-            }
+        Banana::$page = new $pageclass;
+    }
 
-        } else {
-            if (isset($_POST['action']) && $_POST['action']=='cancel') {
-                $res = $banana->action_cancelArticle($group, $artid);
+    /** Fill state vars (Banana::$group, Banana::$artid, Banana::$action, Banana;:$part, Banana::$first)
+     */
+    protected function loadParams()
+    {
+        Banana::$group = isset($this->params['group']) ? $this->params['group'] : null;
+        Banana::$artid = isset($this->params['artid']) ? $this->params['artid'] : null;
+        Banana::$first = isset($this->params['first']) ? $this->params['first'] : null;
+        Banana::$part  = isset($this->params['part']) ? $this->params['part'] : 'text';
+
+        // Look for the action to execute
+        if (is_null(Banana::$group)) {
+            if (isset($this->params['subscribe'])) {
+                Banana::$action = Banana::ACTION_BOX_SUBS;
             } else {
-                $res = '';
+                Banana::$action = Banana::ACTION_BOX_LIST;
             }
-
-            if (!is_null($action)) {
-                $banana->state['page'] = 'action';
-                switch ($action) {
-                    case 'cancel':
-                        $res .= $banana->action_showArticle($group, $artid, $partid);
-                        if ($banana->post->checkcancel()) {
-                            $form = '<p class="error">'._b_('Voulez-vous vraiment annuler ce message ?').'</p>'
-                                  . '<form action="' 
-                                  . htmlentities(makeLink(Array('group' => $group,
-                                                                'artid' => $artid)))
-                                  . '" method="post"><p>'
-                                  . '<input type="hidden" name="action" value="cancel" />'
-                                  . '<input type="submit" value="Annuler !" />'
-                                  . '</p></form>';
-                            return makeTable($form . $res);
-                        }
-                        return makeTable("" . $res);
-
-                    case 'new':
-                        return makeTable($banana->action_newFup($group, $artid));
-                }
-            }
-
-            if (isset($banana->get['pj'])) {
-                $view = false;
-                if ($action == 'view') {
-                    $view = true;
-                }
-                $att = $banana->action_getAttachment($group, $artid, $banana->get['pj'], $view);
-                return makeTable($res . $att);
+            return;
+        }
+        $action = isset($this->params['action']) ? $this->params['action'] : null; 
+        if (is_null(Banana::$artid)) {
+            if ($action == 'new') {
+                Banana::$action = Banana::ACTION_MSG_NEW;
+            } else {
+                Banana::$action = Banana::ACTION_MSG_LIST;
             }
-            
-            $banana->state['page'] = 'message';
-            return makeTable($banana->action_showArticle($group, $artid, $partid));
+            return;
+        }
+        switch ($action) {
+          case 'new':
+            Banana::$action = Banana::ACTION_MSG_NEW;
+            return;
+          case 'cancel':
+            Banana::$action = Banana::ACTION_MSG_CANCEL;
+            return;
+          default:
+            Banana::$action = Banana::ACTION_MSG_READ;
         }
     }
 
-    /**************************************************************************/
-    /* actions                                                                */
-    /**************************************************************************/
-
-    function action_saveSubs()
+    /** Register an action to show on banana page
+     * @param action_code HTML code of the action
+     * @param pages ARRAY pages where to show the action (null == every pages)
+     * @return true if success
+     */
+    public function registerAction($action_code, array $pages = null)
     {
-        return;
+        return Banana::$page->registerAction($action_code, $pages);
     }
 
-    function action_listGroups()
+    /** Register a new page
+     * @param name Name of the page
+     * @param text Text for the tab of the page
+     * @param template Template path for the page if null, the page is not handled by banana
+     * @return true if success
+     */
+    public function registerPage($name, $text, $template = null)
     {
-        $this->_newGroup();
-        
-        $res  = $this->groups->to_html();
-        if (count($this->newgroups->overview)) {
-            $res .= '<p>'._b_('Les forums suivants ont été créés depuis ton dernier passage :').'</p>';
-            $res .= $this->newgroups->to_html();
-        }
-
-        $this->nntp->quit();
-        return $res;
+        return Banana::$page->registerPage($name, $text, $template);
     }
 
-    function action_listSubs()
+    /** Run Banana
+     * This function need user profile to be initialised
+     */
+    public function run()
     {
-        $this->_require('groups');
-        $this->groups = new BananaGroups(BANANA_GROUP_ALL);
+        // Configure locales
+        setlocale(LC_ALL,  Banana::$profile['locale']);
         
-        $res  = $this->groups->to_html(true);
+        // Check if the state is valid
+        if (Banana::$protocole->lastErrNo()) {
+            return Banana::$page->kill(_b_('Une erreur a été rencontrée lors de la connexion au serveur') . '<br />'
+                                      . Banana::$protocole->lastError());
+        }
+        if (!Banana::$protocole->isValid()) {
+            return Banana::$page->kill(_b_('Connexion non-valide'));
+        }
+        if (Banana::$action & Banana::ACTION_BOX_NEEDED) {
+            if(isset(Banana::$grp_pattern) && !preg_match('/' . Banana::$grp_pattern . '/', $group)) {
+                Banana::$page->setPage('group');        
+                return Banana::$page->kill(_b_("Ce newsgroup n'existe pas ou vous n'avez pas l'autorisation d'y accéder"));
+            }
+        }
 
-        $this->nntp->quit();
-        return $res;
+        // Dispatch to the action handlers
+        switch (Banana::$action) {
+          case Banana::ACTION_BOX_SUBS:
+            $error = $this->action_subscribe();
+            break;
+          case Banana::ACTION_BOX_LIST:
+            $error = $this->action_listBoxes();
+            break;
+          case Banana::ACTION_MSG_LIST:
+            $error = $this->action_showThread(Banana::$group, Banana::$first);
+            break;
+          case Banana::ACTION_MSG_READ:
+            $error = $this->action_showMessage(Banana::$group, Banana::$artid, Banana::$part);
+            break;
+          case Banana::ACTION_MSG_NEW:
+            $error = $this->action_newMessage(Banana::$group, Banana::$artid);
+            break;
+          case Banana::ACTION_MSG_CANCEL:
+            $error = $this->action_cancelMessage(Banana::$group, Banana::$artid);
+            break;
+          default:
+            $error = _b_("L'action demandée n'est pas supportée par Banana");
+        }
+
+        // Generate the page
+        if (is_string($error)) {
+            return Banana::$page->kill($error);
+        }
+        return Banana::$page->run();
     }
 
-    function action_showThread($group, $first)
+    /**************************************************************************/
+    /* actions                                                                */
+    /**************************************************************************/
+    protected function action_saveSubs($groups)
     {
-        if (!$this->_newSpool($group, $this->profile['display'], $this->profile['lastnews'])) {
-            return '<p class="error">'._b_('Impossible charger la liste des messages de ') . $group . '</p>';
-        }
-
-        if ($first > count($this->spool->overview)) {
-            $first = count($this->spool->overview);
-        }
-
-        $first = $first - ($first % $this->tmax) + 1;
-        
-        $pages = displayPages($first);
-        $res  = $pages . $this->spool->to_html($first, $first+$this->tmax) . $pages;
-
-        $this->nntp->quit();
-        
-        return $res;
+        Banana::$profile['subscribe'] = $groups;
+        return true;
     }
 
-    function action_showArticle($group, $id, $part)
+    protected function action_subscribe()
     {
-        if (!$this->_newSpool($group, $this->profile['display'], $this->profile['lastnews'])) {
-            return '<p class="error">'._b_('Impossible charger la liste des messages de ') . $group . '</p>';
+        Banana::$page->setPage('subscribe');
+        if (isset($_POST['validsubs'])) {
+            $this->action_saveSubs(array_keys($_POST['subscribe']));
+            Banana::$page->redirect();
         }
-
-        if (!$this->_newPost($id)) {
-            if ($this->nntp->lasterrorcode == "423") {
-                $this->spool->delid($id);
-            }
-            $this->nntp->quit();
-            return '<p class="error">'._b_('Impossible d\'accéder au message.   Le message a peut-être été annulé').'</p>';
-        }
-
-        $res = $this->post->to_html($part);
-
-        $this->nntp->quit();
-        
-        return $res;
+        $groups = Banana::$protocole->getBoxList(Banana::BOXES_ALL);
+        Banana::$page->assign('groups', $groups);
+        return true;
     }
 
-    function action_getAttachment($group, $id, $pjid, $action)
+    protected function action_listBoxes()
     {
-        if (!$this->_newSpool($group, $this->profile['display'], $this->profile['lastnews'])) {
-            return '<p class="error">'._b_('Impossible charger la liste des messages').'</p>';
-        }
-
-        if (!$this->_newPost($id)) {
-            if ($this->nntp->lasterrorcode == "423") {
-                $this->spool->delid($id);
-            }
-            $this->nntp->quit();
-            return '<p class="error">'._b_('Impossible d\'accéder au message.   Le message a peut-être été annulé').'</p>';
-        }
-
-        $this->nntp->quit();
-        if ($this->post->get_attachment($pjid, $action)) {
-            return "";
-        } else {
-            return '<p calss="error">'._b_('Impossible d\'accéder à la pièce jointe.').'</p>';
-        }
+        Banana::$page->setPage('forums');
+        $groups    = Banana::$protocole->getBoxList(Banana::BOXES_SUB, Banana::$profile['lastnews'], true);
+        $newgroups = Banana::$protocole->getBoxList(Banana::BOXES_NEW, Banana::$profile['lastnews'], true);
+        Banana::$page->assign('groups', $groups);
+        Banana::$page->assign('newgroups', $newgroups);
+        return true;
     }
 
-    function action_cancelArticle($group, $id)
+    protected function action_showThread($group, $first)
     {
-        if (!$this->_newSpool($group, $this->profile['display'], $this->profile['lastnews'])) {
-            return '<p class="error">'._b_('Impossible charger la liste des messages').'</p>';
-        }
-
-        if (!$this->_newPost($id)) {
-            return '<p class="error">'._b_('Impossible de trouver le message à annuler').'</p>';
-        }
-        $mid  = array_search($id, $this->spool->ids);
-
-        if (!$this->post->checkcancel()) {
-            return '<p class="error">'._b_('Vous n\'avez pas les permissions pour annuler ce message').'</p>'; 
-        }
-        $msg = 'From: '.$this->profile['name']."\n"
-             . "Newsgroups: $group\n"
-             . "Subject: cmsg $mid\n"
-             . $this->custom
-             . "Control: cancel $mid\n"
-             . "\n"
-             . "Message canceled with Banana";
-        if ($this->nntp->post($msg)) {
-            $ndx = $this->spool->getndx($artid) - 1;
-            if ($ndx > 50) {
-                $ndx = 0;
-            }
-            $this->spool->delid($id);
-            $this->nntp->quit();
-            redirectInBanana(Array('group' => $group,
-                                   'first' => $ndx));
-        } else {
-            return '<p class="error">'._b_('Impossible d\'annuler le message').'</p>';
+        Banana::$page->setPage('thread');
+        if (!$this->loadSpool($group)) {
+            return _b_('Impossible charger la liste des messages de ') . $group;
         }
+        $groups = Banana::$protocole->getBoxList(Banana::BOXES_SUB, Banana::$profile['lastnews'], true);
+        Banana::$page->assign('msgbypage', Banana::$tmax);
+        Banana::$page->assign('groups', $groups);
+        return true;
     }
 
-    function action_newFup($group, $id = -1)
+    protected function action_showMessage($group, $artid, $partid = 'text')
     {
-        $subject = $body = '';
-        $target  = $group;
-        
-        if (@$_POST['action'] == 'new') {
-            $subject  = $_POST['subject'];
-            $body     = $_POST['body'];
-            $target   = $_POST['newsgroups'];
-            $followup = $_POST['followup'];
-            $this->state['page']   = 'action';
-            $this->state['group']  = $group;
-            $this->state['action'] = 'new';
-            if ($id != -1) {
-                $this->state['artid'] = $id;
+        Banana::$page->setPage('message');
+        if ($partid == 'text') {
+            $this->loadSpool($group);
+        }
+        $msg =& $this->loadMessage($group, $artid);
+        if (is_null($msg)) {
+            $this->loadSpool($group);
+            $this->removeMessage($group, $artid);
+            return _b_('Le message demandé n\'existe pas. Il est possible qu\'il ait été annulé');
+        }
+        if ($partid == 'xface') {
+            $msg->getXFace();
+            exit;
+        } elseif ($partid != 'text') {
+            $part = $msg->getPartById($partid);
+            if (!is_null($part)) {
+                $part->send(true);
             }
-        } elseif ($id > 0) {
-            $this->nntp->group($group);
-            if ($this->_newPost($id)) {
-                $subject  = 'Re: ' . preg_replace("/^re\s*:\s*/i", '', $this->post->headers['subject']);
-                $body     = to_entities(utf8_encode($this->post->name." "._b_("a écrit"))." :\n"
-                                        . wrap($this->post->get_body(), "> "))
-                          . ($this->profile['sig'] ? "\n\n-- \n". $this->profile['sig'] : '');
-                $target   = isset($this->post->headers['followup-to']) ?
-                                $this->post->headers['followup-to'] : $this->post->headers['newsgroups'];
-                $followup = null;
+            $part = $msg->getFile($partid);
+            if (!is_null($part)) {
+                $part->send();
             }
-        } else {
-            $targe   = $group;
-            $subject = $followup = null;
-            $body    = $this->profile['sig'] ? "\n\n-- \n". $this->profile['sig'] : '';
-        }
-
-        $this->nntp->quit();
-
-        $html  = '<form enctype="multipart/form-data" action="'
-               . htmlentities(makeLink(Array('group' => $group)))
-               . '" method="post" accept-charset="utf-8">'
-               . '<table class="bicol" cellpadding="0" cellspacing="0">'
-               . '<tr><th colspan="2">' . _b_('En-têtes') . '</th></tr>'
-               . '<tr><td>' . _b_('Nom') . '</td>'
-               . '<td>' . htmlentities($this->profile['name']) . '</td></tr>'
-               . '<tr><td>' . _b_('Sujet') . '</td>'
-               . '<td><input type="text" name="subject" value="' . htmlentities($subject) . '" size="60" /></td></tr>'
-               . '<tr><td>' . _b_('Forums') . '</td>'
-               . '<td><input type="text" name="newsgroups" value="' . htmlentities($target) . '" size="60" /></td></tr>'
-               . '<tr><td>' . _b_('Suivi à') . '</td>'
-               . '<td><input type="text" name="followup" value="' . htmlentities($followup). '" size="60" /></td></tr>'
-               . '<tr><td>' . _b_('Organisation') . '</td>'
-               . '<td>' . $this->profile['org'] . '</td></tr>'
-               . '<tr><th colspan="2">' . _b_('Corps') . '</th></tr>'
-               . '<tr><td colspan="2"><textarea name="body" cols="74" rows="16">'
-               .  $body . '</textarea></td></tr>';
-        if ($this->can_attach) {
-            $html .= '<tr><th colspan="2">' . _b_('Pièce jointe') . '</th></tr>'
-                  . '<tr><td colspan="2">'
-                  . '<input type="hidden" name="MAX_FILE_SIZE" value="' . $this->maxfilesize . '" />'
-                  . '<input type="file" name="newpj" size="40"/></td></tr>';
-        }
-        $html .= '<tr><th colspan="2">';
-        if ($id != -1) {
-            $html .= '<input type="hidden" name="artid" value="' . $id . '" />';
+            exit;
         }
-        $html .= '<input type="hidden" name="action" value="new" />'
-              . '<input type="submit" value="' . _b_('Envoyer le message') . '" /></th></tr>'
-              . '</table></form>';
-
-        return $html;
+        $groups    = Banana::$protocole->getBoxList(Banana::BOXES_SUB, Banana::$profile['lastnews'], true);
+        Banana::$page->assign('groups', $groups);
+        Banana::$page->assign_by_ref('message', $msg);
+        Banana::$page->assign('headers', Banana::$show_hdr);
+        return true;
     }
 
-    function action_doFup($group, $artid = -1)
+    protected function action_newMessage($group, $artid)
     {
-        if ( ! (is_utf8($_POST['subject']) && is_utf8($_POST['body']))) {
-            foreach(Array('subject', 'body') as $key) {
-                $_POST[$key] = utf8_encode($_POST[$key]);
+        Banana::$page->setPage('new');
+        if (!Banana::$protocole->canSend()) {
+            return _b_('Vous n\'avez pas le droit de poster');
+        }
+        $hdrs    = Banana::$protocole->requestedHeaders();
+        $headers = array();
+        foreach ($hdrs as $header) {
+            $headers[$header] = array('name' => BananaMessage::translateHeaderName($header));
+            if (isset(Banana::$profile[$header])) {
+                $headers[$header]['fixed'] = Banana::$profile[$header];
             }
         }
-       
-        $forums = preg_split('/\s*(,|;)\s*/', $_POST['newsgroups']);
-        $fup    = $_POST['followup'];
-        if (sizeof($forums) > 1) {
-            if (empty($fup)) {
-                $fup = $forums[0];
+        if (isset($_POST['sendmessage'])) {
+            $hdr_values = array();
+            foreach ($hdrs as $header) {
+                $hdr_values[$header] = isset($headers[$header]['fixed']) ? $headers[$header]['fixed'] : @$_POST[$header];
             }
-        }
-        $to     = implode(',', $forums);
-        
-        if (!$this->_newSpool($group, $this->profile['display'], $this->profile['lastnews'])) {
-            return '<p class="error">'._b_('Impossible charger la liste des messages').'</p>';
-        }
-        
-        $body = preg_replace("/\n\.[ \t\r]*\n/m", "\n..\n", $_POST['body']);
-        $msg  = 'From: ' . $this->profile['name'] . "\n"
-              . "Newsgroups: ". $to . "\n"
-              . "Subject: " . headerEncode($_POST['subject'], 128) . "\n"
-              . (empty($this->profile['org']) ? '' : "Organization: {$this->profile['org']}\n")
-              . (empty($fup) ? '' : 'Followup-To: ' . $fup . "\n");
-
-        if ($artid != -1) {
-            $this->_require('post');
-            $post = new BananaPost($artid);
-            if (!$post || !$post->valid) {
-                return '<p class="error">'._b_('Impossible charger le message d\'origine').'</p>';
+            if ($artid) {
+                $old =& $this->loadMessage($group, $artid);
+                $hdr_values['References'] = $old->getHeaderValue('references') . $old->getHeaderValue('message-id');
             }
-            $refs = ( isset($post->headers['references']) ? $post->headers['references']." " : "" );
-            $msg .= "References: $refs{$post->headers['message-id']}\n";
-        }
-
-        $body_headers  = $this->custom_plain;
-        $body          = wrap($body, "");
-
-        // include attachment in the body
-        $uploaded = $this->_upload('newpj');
-        switch ($uploaded['error']) {
-            case UPLOAD_ERR_OK:
-                $this->custom = $this->custom_mp.$this->custom;
-                $body         = $this->_make_part($body_headers, $body);
-                $file_head    = 'Content-Type: '.$uploaded['type'].'; name="'.$uploaded['name']."\"\n"
-                              . 'Content-Transfer-Encoding: '.$uploaded['encoding']."\n"
-                              . 'Content-Disposition: attachment; filename="'.$uploaded['name']."\"\n";
-                $body        .= $this->_make_part($file_head, $uploaded['data']);
-                $body        .= "\n--".$this->boundary.'--';
-                break;
-
-            case UPLOAD_ERR_INI_SIZE: case UPLOAD_ERR_FORM_SIZE:
-                return '<p class="error">'._b_('Fichier trop gros pour être envoyé : ')
-                        .$uploaded['name'].'</p>'.$this->action_showThread($group, $artid);
-
-            case UPLOAD_ERR_PARTIAL:
-                return '<p class="error">'._b_('Erreur lors de l\'upload de ')
-                        .$uploaded['name'].'</p>'.$this->action_showThread($group, $artid);
-
-            case UPLOAD_ERR_NO_FILE:
-                return '<p class="error">'._b_('Le fichier spécifié n\'existe pas : ')
-                        .$uploaded['name'].'</p>'.$this->action_showThread($group, $artid);
-
-            case UPLOAD_ERR_NO_TMP_DIR:
-                return '<p class="error">'._b_('Une erreur est survenue sur le serveur lors de l\'upload de ')
-                        .$uploaded['name'].'</p>'.$this->action_showThread($group, $artid);
-
-            default:
-                $this->custom = $body_headers.$this->custom;
-        }
-
-        // finalise and post the message
-        $msg .= $this->custom.$this->profile['customhdr']."\n".$body;
-
-        if ($this->nntp->post($msg)) {
-            $dir = Array('group' => $group);
-            if ($artid != -1) {
-                $dir['artid'] = $artid;
+            $msg = null;
+            if (empty($hdr_values['Subject'])) {
+                Banana::$page->trig(_b_('Le message doit avoir un sujet'));
+            } elseif (Banana::$can_attach && isset($_FILES['attachment'])) {
+                $uploaded = $_FILES['attachment'];
+                if (!is_uploaded_file($uploaded['tmp_name'])) {
+                    Banana::$page->trig(_b_('Une erreur est survenue lors du téléchargement du fichier'));
+                } else {
+                    $msg = BananaMessage::newMessage($hdr_values, $_POST['body'], $uploaded);
+                }
+            } else {
+                $msg = BananaMessage::newMessage($hdr_values, $_POST['body']);
+            }
+            if (!is_null($msg)) {
+                if (Banana::$protocole->send($msg)) {
+                    Banana::$page->redirect(array('group' => $group, 'artid' => $artid));
+                }
+                Banana::$page->trig(_b_('Une erreur est survenue lors de l\'envoi du message :') . '<br />'
+                                   . Banana::$protocole->lastError());
             }
-            redirectInBanana($dir);
         } else {
-            return '<p class="error">' . _b_('Impossible de poster le message. Le serveur a retourné l\'erreur :') . '</p>'
-                   . '<pre class="error">' . utf8_encode($this->nntp->lasterrortext) .'</pre>'
-                   . $this->action_newFup($group, $artid);
-        }
-    }
-
-    /**************************************************************************/
-    /* Private functions                                                      */
-    /**************************************************************************/
-
-    function _newSpool($group, $disp=0, $since='') {
-        $this->_require('spool');
-        if (!$this->spool || $this->spool->group != $group) {
-            $this->spool = new BananaSpool($group, $disp, $since);
-            if (!$this->spool || !$this->spool->valid) {
-                $this->spool = null;
-                return false;
+            if (!is_null($artid)) {
+                $msg    =& $this->loadMessage($group, $artid);
+                $body    = $msg->getSender() . _b_(' a écrit :') . "\n" . $msg->quote();
+                $subject = $msg->getHeaderValue('subject');
+                $headers['Subject']['user'] = 'Re: ' . preg_replace("/^re\s*:\s*/i", '', $subject);
+                $target  = $msg->getHeaderValue($hdrs['reply']);
+                if (empty($target)) {
+                    $target = $group;
+                }
+                $headers[$hdrs['dest']]['user'] =& $target;
+            } else {
+                $body    = '';
+                $headers[$hdrs['dest']]['user'] = $group;
             }
+            if (Banana::$profile['sig']) {
+                $body .=  "\n\n-- \n" . Banana::$profile['sig'];
+            }
+            Banana::$page->assign('body', $body);
         }
-        if (count($this->profile['subscribe']) > 0) {
-            $this->_newGroup(false);
-        }
+
+        Banana::$page->assign('maxfilesize', Banana::$maxfilesize);
+        Banana::$page->assign('can_attach', Banana::$can_attach);
+        Banana::$page->assign('headers', $headers);
         return true;
     }
 
-    function _newPost($id)
+    protected function action_cancelMessage($group, $artid)
     {
-        $this->_require('post');
-        $this->post = new BananaPost($id);
-        if (!$this->post || !$this->post->valid) {
-            $this->post = null;
-            return false;
+        Banana::$page->setPage('cancel');
+        $msg =& $this->loadMessage($group, $artid);
+        if (!$msg->canCancel()) {
+            return _b_('Vous n\'avez pas les droits suffisants pour supprimer ce message');
+        }
+        if (isset($_POST['cancel'])) {
+            $this->loadSpool($group);
+            $ndx = Banana::$spool->getNdX($id) - 1;
+            if (!Banana::$protocole->cancel($msg)) {
+                return _b_('Une erreur s\'est produite lors de l\'annulation du message :') . '<br />'
+                       . Banana::$protocole->lastError();
+            }
+            if ($ndx < 50) {
+                 $ndx = 0;
+            }
+            $this->removeMessage($group, $artid);
+            Banana::$page->redirect(Array('group' => $group, 'first' => $ndx));
         }
+        Banana::$page->assign_by_ref('message', $msg);
         return true;
     }
 
-    function _newGroup($showNew = true)
+    /**************************************************************************/
+    /* Private functions                                                      */
+    /**************************************************************************/
+
+    private function loadSpool($group)
     {
-        $this->_require('groups');
-        $this->groups = new BananaGroups(BANANA_GROUP_SUB);
-        if ($showNew && $this->groups->type == BANANA_GROUP_SUB) {
-            $this->newgroups = new BananaGroups(BANANA_GROUP_NEW);
+        Banana::load('spool');
+        if (!Banana::$spool || Banana::$spool->group != $group) {
+            if ($group == @$_SESSION['banana_group'] && isset($_SESSION['banana_spool'])) {
+                Banana::$spool = unserialize($_SESSION['banana_spool']);
+            }    
+            BananaSpool::getSpool($group, Banana::$profile['lastnews']);
+            $_SESSION['banana_group'] = $group;
+            $_SESSION['banana_spool'] = serialize(Banana::$spool);
+            Banana::$spool->setMode(Banana::$profile['display'] ? Banana::SPOOL_UNREAD : Banana::SPOOL_ALL);
         }
+        return true;
     }
 
-    function _require($file)
+    private function &loadMessage($group, $artid)
     {
-        require_once (dirname(__FILE__).'/'.$file.'.inc.php');
+        Banana::load('message');
+        if ($group == @$_SESSION['banana_group'] && $artid == @$_SESSION['banana_artid']
+            && isset($_SESSION['banana_message'])) {
+            $message = unserialize($_SESSION['banana_message']);
+            Banana::$show_hdr = $_SESSION['banana_showhdr'];
+        }  else {
+            $message = Banana::$protocole->getMessage($artid);
+            $_SESSION['banana_group'] = $group;
+            $_SESSION['banana_artid'] = $artid;
+            $_SESSION['banana_message'] = serialize($message);
+            $_SESSION['banana_showhdr'] = Banana::$show_hdr;
+        }
+        Banana::$message =& $message;
+        return $message;
     }
 
-    function _upload($file)
+    private function removeMessage($group, $artid)
     {
-        if ($_FILES[$file]['name'] == "") {
-            return Array( 'error' => -1 );
-        }
-
-        // upload
-        $_FILES[$file]['tmp_name'];
-
-        // test if upload is ok
-        $file    = $_FILES[$file];
-        if ($file['size'] == 0 || $file['error'] != 0) {
-            if ($file['error'] == 0) {
-                $file['error'] = -1;
+        Banana::$spool->delId($artid);
+        if ($group == $_SESSION['banana_group']) {
+            $_SESSION['banana_spool'] = serialize(Banana::$spool);
+            if ($artid == $_SESSION['banana_artid']) {
+                unset($_SESSION['banana_message']);
+                unset($_SESSION['banana_showhdr']);
+                unset($_SESSION['banana_artid']);
             }
-            return $file;
-        }
-
-        // adding custum data
-        $mime    = rtrim(shell_exec('file -bi '.$file['tmp_name'])); //Because mime_content_type don't work :(
-        $encod   = 'base64';
-        if (preg_match("@([^ ]+/[^ ]+); (.*)@", $mime, $format)) {
-            $mime  = $format[1];
-            $encod = $format[2];
-        }
-        $data = fread(fopen($file['tmp_name'], 'r'), $file['size']);
-        if ($encod == 'base64') {
-            $data = chunk_split(base64_encode($data));
         }
-        $file['name']     = basename($file['name']);
-        $file['type']     = $mime;
-        $file['encoding'] = $encod;
-        $file['data']     = $data;
-
-        return $file;
+        return true;
     }
 
-    function _make_part($headers, $body)
+    static private function load($file)
     {
-        return "\n--".$this->boundary."\n".$headers."\n".$body;
+        $file = strtolower($file) . '.inc.php';
+        if (!@include_once dirname(__FILE__) . "/$file") {
+            require_once $file;
+        }
     }
 }
 
diff --git a/banana/groups.inc.php b/banana/groups.inc.php
deleted file mode 100644 (file)
index e7cd952..0000000
+++ /dev/null
@@ -1,137 +0,0 @@
-<?php
-/********************************************************************************
-* include/groups.inc.php : class for group lists
-* ------------------------
-*
-* This file is part of the banana distribution
-* Copyright: See COPYING files that comes with this distribution
-********************************************************************************/
-
-/** class for group lists
- */
-
-define ( 'BANANA_GROUP_ALL', 0 );
-define ( 'BANANA_GROUP_SUB', 1 );
-define ( 'BANANA_GROUP_NEW', 2 );
-class BananaGroups {
-    /** group list */
-    var $overview = Array();
-    /** last update */
-    var $date;
-
-    var $type;
-
-    /** constructor
-     */
-
-    function BananaGroups($_type = BANANA_GROUP_SUB) {
-        global $banana;
-
-        $this->type = $_type;
-        $this->load();
-        
-        if (empty($this->overview) && $_type == BANANA_GROUP_SUB) {
-            $this->type = BANANA_GROUP_ALL;
-            $this->load();
-        }
-    }
-
-    /** Load overviews
-     */
-    function load()
-    {
-        global $banana;
-
-        $desc = $banana->nntp->xgtitle();
-        if ($this->type == BANANA_GROUP_NEW) {
-            $list = $banana->nntp->newgroups($banana->profile['lastnews']);
-        } else {
-            $list = $banana->nntp->liste();
-            if ($this->type == BANANA_GROUP_SUB) {
-                $mylist = Array();
-                foreach ($banana->profile['subscribe'] as $g) {
-                    if (isset($list[$g])) {
-                        $mylist[$g] = $list[$g];
-                    }
-                }
-                $list = $mylist;
-            }
-        }
-
-        foreach ($list as $g=>$l) {
-            $this->overview[$g][0] = isset($desc[$g]) ? $desc[$g] : '-';
-            $this->overview[$g][1] = $l[0];
-        }
-        ksort($this->overview);
-    }
-
-    /** updates overview 
-     * @param date INTEGER date of last update
-     */
-    function update($_date) {
-        global $banana;
-        $serverdate = $banana->nntp->date();
-        if (!$serverdate) $serverdate=time();
-        $newlist = $banana->nntp->newgroups($_date);
-        if (!$newlist) return false;
-        $this->date = $serverdate;
-        foreach (array_keys($newlist) as $g) {
-            $groupstat = $banana->nntp->group($g);
-            $groupdesc = $banana->nntp->xgtitle($g);
-            $this->overview[$g][0]=($groupdesc?$groupdesc:"-");
-            $this->overview[$g][1]=$groupstat[0];
-        }
-        return true;
-    }
-
-    function to_html($show_form = false)
-    {
-        global $banana;
-        if (empty($this->overview)) {
-            return;
-        }
-
-        $html  = '<table class="bicol banana_group" cellspacing="0" cellpadding="2">'."\n";
-        $html .= '<tr><th>'._b_('Total').'</th><th>';
-        if ($show_form) {
-            $html .= _b_('Abo.').'</th><th>';
-        } elseif ($this->type == BANANA_GROUP_SUB) {
-            $html .= _b_('Nouveaux').'</th><th>';
-        }
-        $html .= _b_('Nom').'</th><th>'._b_('Description').'</th></tr>'."\n";
-
-        $b = true;
-        foreach ($this->overview as $g => $d) {
-            $b     = !$b;
-            $ginfo = $banana->nntp->group($g);
-            $new   = count($banana->nntp->newnews($banana->profile['lastnews'],$g));
-
-            $html .= '<tr class="'.($b ? 'pair' : 'impair').'">'."\n";
-            $html .= "<td class='all'>{$ginfo[0]}</td>";
-            if ($show_form) {
-                $html .= '<td class="new"><input type="checkbox" name="subscribe[]" value="'.$g.'"';
-                if (in_array($g, $banana->profile['subscribe'])) {
-                    $html .= ' checked="checked"';
-                }
-                $html .= ' /></td>';
-            } elseif ($this->type == BANANA_GROUP_SUB) {
-                $html .= '<td class="new">'.($new ? $new : '-').'</td>';
-            }
-            $html .= '<td class="grp">' . makeHREF(Array('group' => $g), $g) . '</td><td class="dsc">' . $d[0] . '</td></tr>';
-        }
-
-        $html .= '</table>';
-
-        if ($show_form) {
-            return '<form method="post" action="' . htmlentities(makeLink(Array())) . '">'
-                . '<div class="center"><input type="submit" value="Valider" name="validsubs" /></div>'
-                . $html . '<div class="center"><input type="submit" value="Valider" name="validsubs" /></div></form>';
-        }
-        
-        return $html;
-    }
-}
-
-// vim:set et sw=4 sts=4 ts=4
-?>
diff --git a/banana/mbox.inc.php b/banana/mbox.inc.php
new file mode 100644 (file)
index 0000000..c36bb5c
--- /dev/null
@@ -0,0 +1,431 @@
+<?php
+/********************************************************************************
+* banana/protocoleinterface.inc.php : interface for box access
+* ------------------------
+*
+* This file is part of the banana distribution
+* Copyright: See COPYING files that comes with this distribution
+********************************************************************************/
+
+require_once dirname(__FILE__) . '/banana.inc.php';
+require_once dirname(__FILE__) . '/protocoleinterface.inc.php';
+require_once dirname(__FILE__) . '/message.inc.php';
+
+class BananaMBox implements BananaProtocoleInterface
+{
+    private $boxname;
+
+    private $file         = null;
+    private $filesize     = null;
+    private $current_id   = null;
+    private $at_beginning = false;
+    private $file_cache   = null;
+
+    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($box = null)
+    {
+        $this->boxname = $box;
+        $filename = $this->getFileName($box);
+        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);
+        }
+    }
+
+    /** Indicate if the Protocole handler has been succesfully built
+     */
+    public function isValid()
+    {
+        return is_null($this->boxname) || !is_null($this->file);
+    }
+    
+    /** Indicate last error n°
+     */
+    public function lastErrNo()
+    {
+        return $this->_lasterrno;;
+    }
+    
+    /** Indicate last error text
+     */
+    public function lastError()
+    {
+        return $this->_lasterror;
+    }
+
+    /** Return the description of the current box
+     */
+    public function getDescription()
+    {
+        return null;
+    }
+
+    /** Return the list of the boxes
+     * @param mode Kind of boxes to list
+     * @param since date of last check (for new boxes and new messages)
+     * @param withstats Indicated whether msgnum and unread must be set in the result
+     * @return Array(boxname => array(desc => boxdescripton, msgnum => number of message, unread =>number of unread messages)
+     */
+    public function getBoxList($mode = Banana::BOXES_ALL, $since = 0, $withstats = false)
+    {
+        return array($this->boxname => 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)
+     * @param msg_headers Headers to process
+     * @param is_msgid If is set, $id is en Message-Id
+     * @return A BananaMessage or null if the given id can't be retreived
+     */
+    public function getMessage($id, array $msg_headers = array(), $is_msgid = false)
+    {
+        if ($is_msgid || !is_numeric($id)) {
+            if (is_null(Banana::$spool)) {
+                return null;
+            }
+            $id = Banana::$spool->ids[$id];
+        }
+        $message = $this->readMessages(array($id));
+        if (empty($message)) {
+            return null;
+        }
+        $msg = new BananaMessage($message[$id]['message']);
+        return $msg;
+    }
+
+    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;
+        }    
+    }
+
+    /** Return the indexes of the messages presents in the Box
+     * @return Array(number of messages, MSGNUM of the first message, MSGNUM of the last message)
+     */
+    public function getIndexes()
+    {
+        if (is_null($this->count)) {
+            $this->getCount();
+        }
+        return array($this->count, 0, $this->count - 1);
+    }
+
+    /** Return the message headers (in BananaMessage) for messages from firstid to lastid
+     * @return Array(id => array(headername => headervalue))
+     */
+    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];
+                }
+            }
+        }
+        unset($this->messages);
+        unset($messages);
+        return $headers;
+    }
+
+    /** Add storage data in spool overview
+     */
+    public function updateSpool(array &$messages)
+    {
+        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'];
+            }
+        }
+    }
+
+    /** Return the indexes of the new messages since the give date
+     * @return Array(MSGNUM of new messages)
+     */
+    public function getNewIndexes($since)
+    {
+        if (is_null($this->new_messages)) {
+            $this->getCount(); 
+        }
+        return range($this->count - $this->new_messages, $this->count - 1);
+    }
+
+    /** Return wether or not the protocole can be used to add new messages
+     */
+    public function canSend()
+    {
+        return true;
+    }
+
+    /** Return false because we can't cancel a mail
+     */
+    public function canCancel()
+    {
+        return false;
+    }
+
+    /** Return the list of requested headers
+     * @return Array('header1', 'header2', ...) with the key 'dest' for the destination header
+     * and 'reply' for the reply header, eg:
+     * * for a mail: Array('From', 'Subject', 'dest' => 'To', 'Cc', 'Bcc', 'reply' => 'Reply-To')
+     * * for a post: Array('From', 'Subject', 'dest' => 'Newsgroups', 'reply' => 'Followup-To')
+     */
+    public function requestedHeaders()
+    {
+        return Array('From', 'Subject', 'dest' => 'To', 'Cc', 'Bcc', 'reply' => 'Reply-To');
+    }
+
+    /** Send a message
+     * @return true if it was successfull
+     */
+    public function send(BananaMessage &$message)
+    {
+        return true;
+    }
+
+    /** Cancel a message
+     * @return true if it was successfull
+     */
+    public function cancel(BananaMessage &$message)
+    {
+        return false;
+    }
+
+    /** Return the protocole name
+     */
+    public function name()
+    {
+        return 'MBOX';
+    }
+
+#######
+# Filesystem functions
+#######
+
+    protected function getFileName($box)
+    {
+        if (is_null($box)) {
+            return null;
+        }
+        @list($mail, $domain) = explode('@', $box);
+        if ($mail == 'staff') {
+            return '/home/x2003bruneau/staff.polytechnique.org_innovation.mbox';
+        } else {
+            return '/var/mail/' . $mail;
+        }
+    }
+
+#######
+# MBox parser
+#######
+
+    /** Go to the given message
+     */
+    private function goTo($id)
+    {
+        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;
+            } 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;
+                }
+            }
+
+            // 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;
+            }
+            $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)
+    {
+        if (!is_null($this->messages)) {
+            return $this->messages;
+        }
+        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);
+        }
+        return $messages;
+    }
+}
+
+// vim:set et sw=4 sts=4 ts=4:
+?>
diff --git a/banana/message.func.inc.php b/banana/message.func.inc.php
new file mode 100644 (file)
index 0000000..04ad7cb
--- /dev/null
@@ -0,0 +1,415 @@
+<?php
+/********************************************************************************
+ * * banana/message.func.inc.php : function to display messages
+ * * ------------------------
+ * *
+ * * This file is part of the banana distribution
+ * * Copyright: See COPYING files that comes with this distribution
+ * ********************************************************************************/
+
+require_once dirname(__FILE__) . '/mimepart.inc.php';
+require_once dirname(__FILE__) . '/banana.inc.php';
+
+// {{{ Plain Text Functions
+
+function banana_isFlowed($line)
+{
+    return ctype_space(substr($line, -1)) && $line != '-- ';
+}
+
+function banana_removeQuotes($line, &$quote_level, $strict = true)
+{
+    $quote_level = 0;
+    if (empty($line)) {
+        return '';
+    }
+    while ($line{0} == '>') {
+        $line = substr($line, 1);
+        if (!$strict && ctype_space($line{0})) {
+            $line = substr($line, 1);
+        }
+        $quote_level++;
+    }
+    if (ctype_space($line{0})) {
+        $line = substr($line, 1);
+    }
+    return $line;
+}
+
+function banana_quote($line, $level, $mark = '>')
+{
+    $lines = explode("\n", $line);
+    foreach ($lines as &$line) {
+        if ($level > 0 && substr($line, 0, strlen($mark)) != $mark) {
+            $line = ' ' . $line;
+        }
+        for ($i = 0 ; $i < $level ; $i++) {
+            $line = $mark . $line;
+        }
+    }
+    return implode("\n", $lines);
+}
+
+function banana_unflowed($text)
+{
+    $lines = explode("\n", $text);
+    $text = '';
+    while (!is_null($line = array_shift($lines))) {
+        $level = 0;
+        $line = banana_removeQuotes($line, $level);
+        while (banana_isFlowed($line)) {
+            $lvl = 0;
+            if (is_null($nl = array_shift($lines))) {
+                break;
+            }
+            $nl = banana_removeQuotes($nl, $lvl);
+            $line .= $nl;
+        }
+        $text .= banana_quote($line, $level) . "\n";
+    }
+    return $text;
+}
+
+function banana_wordwrap($text, $quote_level)
+{
+    if ($quote_level > 0) {
+        $length = Banana::$wrap - $quote_level - 1;
+        return banana_quote(wordwrap($text, $length), $quote_level);
+    
+    }
+    return wordwrap($text, Banana::$wrap);
+}
+
+function banana_catchFormats($text)
+{
+    $formatting = Array('/' => 'em', // match / first in order not to match closing markups </...> <> </>
+                        '_' => 'u',
+                        '*' => 'strong');
+    $url = Banana::$url_regexp;
+    preg_match_all("/$url/i", $text, $urls);
+    $text = str_replace($urls[0], "&&&urls&&&", $text);
+    foreach ($formatting as $limit=>$mark) {
+        $limit = preg_quote($limit, '/');
+        $text = preg_replace("/$limit\\b(.*?)\\b$limit/s",
+                             "<$mark>\\1</$mark>", $text);
+    }
+    return preg_replace('/&&&urls&&&/e', 'array_shift($urls[0])', $text);
+}
+
+// {{{ URL Catcher tools
+
+function banana__cutlink($link)
+{
+    $link = banana_html_entity_decode($link, ENT_QUOTES);
+    if (strlen($link) > Banana::$wrap) {
+        $link = substr($link, 0, Banana::$wrap - 3) . "...";
+    }
+    return banana_htmlentities($link, ENT_QUOTES);
+}
+
+function banana__cleanURL($url)
+{
+    $url = str_replace('@', '%40', $url);
+    if (strpos($url, '://') === false) {
+        $url = 'http://' . $url;
+    }
+    return '<a href="'.$url.'" title="'.$url.'">' . banana__cutlink($url) . '</a>';
+}
+
+function banana__catchMailLink($email)
+{
+    $mid = '<' . $email . '>';
+    if (isset(Banana::$spool->ids[$mid])) {
+        return Banana::$page->makeLink(Array('group' => Banana::$group,
+                                             'artid' => Banana::$spool->ids[$mid],
+                                             'text'  => $email));
+    } elseif (strpos($email, '$') !== false) {
+        return $email;
+    }
+    return '<a href="mailto:' . $email . '">' . $email . '</a>';
+}
+
+// }}}
+
+function banana_catchURLs($text)
+{
+    $url  = Banana::$url_regexp;
+
+    $res  = preg_replace("/&(lt|gt|quot);/", " &\\1; ", $text);
+    $res  = preg_replace("/$url/ie", "'\\1'.banana__cleanurl('\\2').'\\3'", $res);
+    $res  = preg_replace('/(["\[])?(?:mailto:|news:)?([a-z0-9.\-+_\$]+@([\-.+_]?[a-z0-9])+)(["\]])?/ie',
+                         "'\\1' . banana__catchMailLink('\\2') . '\\4'",
+                          $res);
+    $res  = preg_replace("/ &(lt|gt|quot); /", "&\\1;", $res);
+    return $res;
+}
+
+// {{{ Quotes catcher functions
+
+function banana__replaceQuotes($text, $regexp)
+{
+    return stripslashes(preg_replace("@(^|<pre>|\n)$regexp@i", '\1', $text));
+}
+
+// }}}
+
+function banana_catchQuotes($res, $strict = true)
+{
+    if ($strict) {
+        $regexp = "&gt;";
+    } else {
+        $regexp = "&gt; *";
+    }
+    while (preg_match("/(^|<pre>|\n)$regexp/i", $res)) {
+        $res  = preg_replace("/(^|<pre>|\n)(($regexp.*(?:\n|$))+)/ie",
+            "'\\1</pre><blockquote><pre>'"
+            ." . banana__replaceQuotes('\\2', '$regexp')"
+            ." . '</pre></blockquote><pre>'",
+            $res);
+    }
+    return $res;
+}
+
+function banana_catchSignature($res)
+{
+    $res = preg_replace("@<pre>-- ?\n@", "<pre>\n-- \n", $res);
+    $parts = preg_split("/\n-- ?\n/", $res);
+    $sign  = '</pre><hr style="width: 100%; margin: 1em 0em; " /><pre>';
+    return join($sign, $parts);
+}
+
+function banana_plainTextToHtml($text, $strict = true)
+{
+    $text = banana_htmlentities($text);
+    $text = banana_catchFormats($text);
+    $text = banana_catchURLs($text);
+    $text = banana_catchQuotes($text, $strict);
+    $text = banana_catchSignature($text);
+    return banana_cleanHtml('<pre>' . $text . '</pre>');
+}
+
+function banana_wrap($text, $base_level = 0, $strict = true)
+{
+    $lines  = explode("\n", $text);
+    $text   = '';
+    $buffer = array();
+    $level  = 0;
+    while (!is_null($line = array_shift($lines))) {
+        $lvl = 0;
+        $line = banana_removeQuotes($line, $lvl, $strict);
+        if($lvl != $level && !empty($buffer)) {
+            $text  .= banana_wordwrap(implode("\n", $buffer), $level + $base_level) . "\n";
+            $level  = $lvl;
+            $buffer = array();
+        }
+        $buffer[] = $line;
+    }
+    if (!empty($buffer)) {
+        $text .= banana_wordwrap(implode("\n", $buffer), $level + $base_level);
+    }
+    return $text;
+}
+
+function banana_formatPlainText(BananaMimePart &$part, $base_level = 0)
+{
+    $text = $part->getText();
+    if ($part->isFlowed()) {
+        $text = banana_unflowed($text);
+    }
+    $text = banana_wrap($text, $base_level, $part->isFlowed());
+    return banana_plainTextToHtml($text, $part->isFlowed());
+}
+
+function banana_quotePlainText(BananaMimePart &$part)
+{
+    $text = $part->getText();
+    if ($part->isFlowed()) {
+        $text = banana_unflowed($text);
+    }
+    return banana_wrap($text, 1);
+}
+
+// }}}
+// {{{ HTML Functions
+
+function banana_htmlentities($text, $quote = ENT_COMPAT)
+{
+    return htmlentities($text, $quote, 'UTF-8');
+}
+
+function banana_html_entity_decode($text, $quote = ENT_COMPAT)
+{
+    return html_entity_decode($text, $quote, 'UTF-8');
+}
+
+function banana_removeEvilAttributes($tagSource)
+{
+    $stripAttrib = 'javascript:|onclick|ondblclick|onmousedown|onmouseup|onmouseover|'.
+                   'onmousemove|onmouseout|onkeypress|onkeydown|onkeyup';
+    return stripslashes(preg_replace("/$stripAttrib/i", '', $tagSource));
+}   
+    
+/**
+ * @return string
+ * @param string
+ * @desc Strip forbidden tags and delegate tag-source check to removeEvilAttributes()
+ */
+function banana_cleanHtml($source)
+{
+    $allowedTags = '<h1><b><i><a><ul><li><pre><hr><blockquote><img><br><font><div>'
+                 . '<p><small><big><sup><sub><code><em><strong><table><tr><td><th>';
+    $source = strip_tags($source, $allowedTags);
+    $source = preg_replace('/<(.*?)>/ie', "'<'.banana_removeEvilAttributes('\\1').'>'", $source);
+        
+    if (function_exists('tidy_repair_string')) {
+        $tidy_on = Array(
+            'drop-empty-paras', 'drop-proprietary-attributes',
+            'hide-comments', 'logical-emphasis', 'output-xhtml',
+            'replace-color', 'show-body-only'
+        );
+        $tidy_off = Array('join-classes', 'clean'); // 'clean' may be a good idea, but it is too aggressive
+
+        foreach($tidy_on as $opt) {
+            tidy_setopt($opt, true);
+        }
+        foreach($tidy_off as $opt) {
+            tidy_setopt($opt, false);
+        }
+        tidy_setopt('alt-text', '[ inserted by TIDY ]');
+        tidy_setopt('wrap', '120');
+        tidy_set_encoding('utf8');
+        return tidy_repair_string($source);
+    }
+    return $source;
+}
+
+function banana_catchHtmlSignature($res)
+{
+    $res = preg_replace("@(</p>)\n?-- ?\n?(<p[^>]*>|<br[^>]*>)@", "\\1<br/>-- \\2", $res);
+    $res = preg_replace("@<br[^>]*>\n?-- ?\n?(<p[^>]*>)@", "<br/>-- <br/>\\2", $res);
+    $res = preg_replace("@(<pre[^>]*>)\n?-- ?\n@", "<br/>-- <br/>\\1", $res);
+    $parts = preg_split("@(:?<p[^>]*>\n?-- ?\n?</p>|<br[^>]*>\n?-- ?\n?<br[^>]*>)@", $res);
+    $sign  = '<hr style="width: 100%; margin: 1em 0em; " />';
+    return join($sign, $parts);
+}
+
+// {{{ Link to part catcher tools
+
+function banana__linkAttachment($cid)
+{
+    return banana_htmlentities(
+        Banana::$page->makeUrl(Array('group' => Banana::$group,
+                                     'artid' => Banana::$artid,
+                                     'part'  => $cid)));
+}
+
+// }}}
+
+function banana_hideExternalImages($text)
+{
+    return preg_replace("/<img[^>]*?src=['\"](?!cid).*?>/i",
+                        Banana::$page->makeImg(array('img' => 'invalid')),
+                        $text);
+}
+
+function banana_catchPartLinks($text)
+{
+    return preg_replace('/cid:([^\'" ]+)/e', "banana__linkAttachment('\\1')", $text);
+}
+
+// {{{ HTML to Plain Text tools
+
+function banana__convertFormats($res)
+{
+    $table = array('em|i'     => '/',
+                   'strong|b' => '*',
+                   'u'        => '_');
+    foreach ($table as $tags=>$format) {
+        $res = preg_replace("!</?($tags)( .*?)?>!is", $format, $res);
+    }
+    return $res;
+}
+
+function banana__convertQuotes($res)
+{
+    return preg_replace('!<blockquote.*?>([^<]*)</blockquote>!ies',
+                        "\"\n\" . banana_quote(banana__convertQuotes('\\1' . \"\n\"), 1, '&gt;')",
+                        $res);
+}
+
+// }}}
+
+function banana_htmlToPlainText($res)
+{
+    $res = str_replace("\n", '', $res);
+    $res = banana__convertFormats($res);
+    $res = trim(strip_tags($res, '<div><br><p><blockquote>'));
+    $res = preg_replace("@</?(br|p|div).*?>@si", "\n", $res);
+    $res = banana__convertQuotes($res);
+    return banana_html_entity_decode($res);    
+}
+
+function banana_formatHtml(BananaMimePart &$part)
+{
+    $text = $part->getText();
+    $text = banana_catchHtmlSignature($text);
+    $text = banana_hideExternalImages($text);
+    $text = banana_catchPartLinks($text);
+    return banana_cleanHtml($text);
+}
+
+function banana_quoteHtml(BananaMimePart &$part)
+{
+    $text = $part->getText();
+    $text = banana_htmlToPlainText($text);
+    return banana_wrap($text, 1);
+}
+
+// }}}
+// {{{ Richtext Functions
+
+/** Convert richtext to html
+ */
+function banana_richtextToHtml($source)
+{
+    $tags = Array('bold'        => 'b',
+                  'italic'      => 'i',
+                  'smaller'     => 'small',
+                  'bigger'      => 'big',
+                  'underline'   => 'u',
+                  'subscript'   => 'sub',
+                  'superscript' => 'sup',
+                  'excerpt'     => 'blockquote',
+                  'paragraph'   => 'p',
+                  'nl'          => 'br'
+    );
+
+    // clean unsupported tags
+    $protectedTags = '<signature><lt><comment><'.join('><', array_keys($tags)).'>';
+    $source = strip_tags($source, $protectedTags);
+
+    // convert richtext tags to html
+    foreach (array_keys($tags) as $tag) {
+        $source = preg_replace('@(</?)'.$tag.'([^>]*>)@i', '\1'.$tags[$tag].'\2', $source);
+    }
+
+    // some special cases
+    $source = preg_replace('@<signature>@i', '<br>-- <br>', $source);
+    $source = preg_replace('@</signature>@i', '', $source);
+    $source = preg_replace('@<lt>@i', '&lt;', $source);
+    $source = preg_replace('@<comment[^>]*>((?:[^<]|<(?!/comment>))*)</comment>@i', '<!-- \1 -->', $source);
+    return banana_cleanHtml($source);
+}
+
+function banana_formatRichText(BananaMimePart &$part)
+{
+    $text = $part->getText();
+    $text = banana_richtextToHtml($text);
+    $text = banana_catchHtmlSignature($text);
+    return banana_cleanHtml($text);
+}
+
+// }}}
+
+// vim:set et sw=4 sts=4 ts=4:
+?>
diff --git a/banana/message.inc.php b/banana/message.inc.php
new file mode 100644 (file)
index 0000000..c83d17b
--- /dev/null
@@ -0,0 +1,285 @@
+<?php
+/********************************************************************************
+* banana/message.inc.php : class for messages
+* ------------------------
+*
+* This file is part of the banana distribution
+* Copyright: See COPYING files that comes with this distribution
+********************************************************************************/
+
+require_once dirname(__FILE__) . '/mimepart.inc.php';
+require_once dirname(__FILE__) . '/message.func.inc.php';
+require_once dirname(__FILE__) . '/banana.inc.php';
+
+final class BananaMessage extends BananaMimePart
+{
+    private $msg_headers = array();
+
+    public function __construct($data = null)
+    {
+        parent::__construct($data);
+        if (!is_null($data)) {
+            if (isset($this->headers['in-reply-to']) && isset($this->headers['references'])) {
+                unset($this->headers['in-reply-to']);
+            }
+            Banana::$show_hdr = array_intersect(Banana::$show_hdr, array_keys($this->headers));
+            Banana::$message =& $this;
+        }
+    }
+
+    public function hasHeader($hdr)
+    {
+        return isset($this->headers[$hdr]);
+    }
+
+    static public function newMessage(array $headers, $body, array $file = null)
+    {
+        $msg = new BananaMessage();
+        $msg->msg_headers = $headers;
+        $msg->makeTextPart($body, 'text/plain', '8bits', 'UTF-8', 'fixed');
+        if (!is_null($file)) {
+            $msg->addAttachment($file);
+        }
+        return $msg;
+    }
+
+    static public function translateHeaderName($hdr)
+    {
+        switch (strtolower($hdr)) {
+          case 'from':          return _b_('De');
+          case 'subject':       return _b_('Sujet');
+          case 'newsgroups':    return _b_('Forums');
+          case 'followup-to':   return _b_('Suivi à');
+          case 'to':            return _b_('À');
+          case 'cc':            return _b_('Copie à');
+          case 'bcc':           return _b_('Copie cachée à');
+          case 'reply-to':      return _b_('Répondre à');
+          case 'date':          return _b_('Date');
+          case 'organization':  return _b_('Organisation');
+          case 'in-reply-to':
+          case 'references':    return _b_('Références');
+          case 'x-face':        return _b_('Image');
+        }
+        return $hdr;
+    }
+
+    public function translateHeaderValue($hdr)
+    {
+        if (!isset($this->headers[$hdr])) {
+            return null;
+        }
+        $text = $this->headers[$hdr];
+
+        if (function_exists('hook_formatDisplayHeader')
+             && $res = hook_formatDisplayHeader($hdr, $text)) {
+            return $res;
+        }
+        switch ($hdr) {
+          case "date":
+            return BananaMessage::formatDate($text);
+
+          case "followup-to": case "newsgroups":
+            $groups = preg_split("/[\t ]*,[\t ]*/", $text);
+            $res    = '';
+            foreach ($groups as $g) {
+                $res .= Banana::$page->makeLink(Array('group' => $g, 'text' => $g)) . ', ';
+            }
+            return substr($res,0, -2);
+
+          case "from":
+            return BananaMessage::formatFrom($text);
+
+          case "references": case "in-reply-to":
+            $rsl     = "";
+            $parents = preg_grep('/^\d+$/', $this->getTranslatedReferences());
+            $p       = array_pop($parents);
+
+            $parents = array();
+            while (!is_null($p)) {
+                array_unshift($parents, $p);
+                $p         = Banana::$spool->overview[$p]->parent;  
+            }
+            $ndx = 1;
+            foreach ($parents as $p) {
+                $rsl .= Banana::$page->makeLink(Array('group' => Banana::$spool->group,
+                                                      'artid' => $p, 'text' => $ndx++)) . ' ';
+            }
+            return $rsl;
+
+          case "subject":
+            $link = null;
+            $text = stripslashes($text);
+            if (function_exists('hook_getSubject')) {
+                $link = hook_getSubject($text);
+            }
+            return banana_catchFormats($text) . $link;
+
+          default:
+            return $text;
+        }
+    }
+
+    public function getSender()
+    {
+        $from = $this->headers['from'];
+        $name = trim(preg_replace('/<[^ ]*>/', '', $from));
+        if (empty($name)) {
+            return $from;
+        }
+        return $name;
+    }
+
+    public function getHeaderValue($hdr)
+    {
+        $hdr = strtolower($hdr);
+        if (!isset($this->headers[$hdr])) {
+            return null;
+        }
+        if ($hdr == 'date') {
+            return strtotime($this->headers['date']);
+        } else {
+            return $this->headers[$hdr];
+        }
+    }
+
+    public function getHeaders()
+    {
+        $this->msg_headers = array_merge($this->msg_headers, Banana::$custom_hdr, Banana::$profile['custom_hdr']);
+        $headers = array_map(array($this, 'encodeHeader'), $this->msg_headers);
+        return array_merge($headers, parent::getHeaders());
+    }
+
+    static public function formatFrom($text)
+    {
+#     From: mark@cbosgd.ATT.COM
+#     From: <mark@cbosgd.ATT.COM>
+#     From: mark@cbosgd.ATT.COM (Mark Horton)
+#     From: Mark Horton <mark@cbosgd.ATT.COM>
+        $mailto = '<a href="mailto:';
+    
+        $result = banana_htmlentities($text);
+        if (preg_match("/^([^ ]+@[^ ]+)$/", $text, $regs)) {
+            $result = $mailto . $regs[1] . '">' . banana_htmlentities($regs[1]) . '</a>';
+        }
+        if (preg_match("/^<(.+@.+)>$/", $text, $regs)) {
+            $result = $mailto . $regs[1] . '">' . banana_htmlentities($regs[1]) . '</a>';
+        }
+        if (preg_match("/^([^ ]+@[^ ]+) \((.*)\)$/", $text, $regs)) {
+            $result = $mailto . $regs[1] . '">' . banana_htmlentities($regs[2]) . '</a>';
+        }   
+        if (preg_match("/^\"?([^<>\"]+)\"? +<(.+@.+)>$/", $text, $regs)) {
+            $nom = preg_replace("/^'(.*)'$/", '\1', $regs[1]);
+            $nom = stripslashes($nom);
+            $result = $mailto . $regs[2] . '">' . banana_htmlentities($nom) . '</a>';
+        }
+        return preg_replace("/\\\(\(|\))/","\\1",$result);
+    }
+
+    static public function formatDate($text)
+    {
+        return utf8_encode(strftime("%A %d %B %Y, %H:%M (fuseau serveur)", strtotime($text)));
+    }
+
+    public function translateHeaders()
+    {
+        $result = array();
+        foreach (array_keys($this->headers) as $name) {
+            $value = $this->translateHeaderValue($name);
+            if (!is_null($value)) {
+                $result[$this->translateHeaderName($name)] = $value;
+            }
+        }
+        return $result;
+    }
+
+    public function getReferences()
+    {
+        $text = $this->headers['references'];
+        $text = str_replace("><","> <", $text);
+        return preg_split('/\s/', $text);
+    }
+
+    public function getTranslatedReferences()
+    {
+        return BananaMessage::formatReferences($this->headers);
+    }
+
+    static public function formatReferences(array &$refs)
+    {
+        if (isset($refs['references'])) {
+            $text = str_replace('><', '> <', $refs['references']);
+            return preg_split('/\s/', strtr($text, Banana::$spool->ids));
+        } elseif (isset($refs['in-reply-to'])) {
+            return array(Banana::$spool->ids[$refs['in-reply-to']]);
+        } else {
+            return array();
+        }
+    }
+
+    public function hasXFace()
+    {
+        return Banana::$formatxface && isset($this->headers['x-face']);
+    }
+
+    public function getXFace()
+    {
+        header('Content-Type: image/gif');
+        $xface = $this->headers['x-face'];
+        passthru('echo ' . escapeshellarg($xface)
+                . '| uncompface -X '
+                . '| convert -transparent white xbm:- gif:-');
+        exit;
+    }
+
+    public function getFormattedBody($type = null)
+    {
+        $types = Banana::$body_mime;
+        if (!is_null($type)) {
+            array_unshift($types, $type);
+        }
+        foreach ($types as $type) {
+            @list($type, $subtype) = explode('/', $type);
+            $parts = $this->getParts($type, $subtype);
+            if (empty($parts)) {
+                continue;
+            }
+            foreach ($parts as &$part) {
+                list($type, $subtype) = $part->getType();
+                switch ($subtype) {
+                  case 'html': return banana_formatHtml($part);
+                  case 'enriched': case 'richtext': return banana_formatRichText($part);
+                  default: return banana_formatPlainText($part);
+                }
+            }
+        }
+        return null;
+    }
+
+    public function quote()
+    {
+        $part = $this->toPlainText();
+        if (is_null($part)) {
+            return banana_quoteHtml($this->toHtml());
+        }
+        return banana_quotePlainText($part);
+    }
+
+    public function canCancel()
+    {
+        if (!Banana::$protocole->canCancel()) {
+            return false;
+        }
+        if (function_exists('hook_checkcancel')) {
+            return hook_checkcancel($this->headers);
+        }
+        return Banana::$profile['name'] == $this->headers['from'];
+    }
+
+    public function canSend()
+    {
+        return Banana::$protocole->canSend();
+    }
+}
+
+// vim:set et sw=4 sts=4 ts=4:
+?>
diff --git a/banana/mimepart.inc.php b/banana/mimepart.inc.php
new file mode 100644 (file)
index 0000000..c486add
--- /dev/null
@@ -0,0 +1,514 @@
+<?php
+/********************************************************************************
+* banana/mimepart.inc.php : class for MIME parts
+* ------------------------
+*
+* This file is part of the banana distribution
+* Copyright: See COPYING files that comes with this distribution
+********************************************************************************/
+
+class BananaMimePart
+{
+    public $headers      = null; /* Should be protected */
+
+    private $id           = null;
+    private $content_type = null;
+    private $charset      = null;
+    private $encoding     = null;
+    private $disposition  = null;
+    private $boundary     = null;
+    private $filename     = null;
+    private $format       = null;
+
+    private $body         = null;
+    private $multipart    = null;
+
+    protected function __construct($data = null)
+    {
+        if (!is_null($data)) {
+            $this->fromRaw($data);
+        }   
+    }
+
+    protected function makeTextPart($body, $content_type, $encoding, $charset = null, $format = 'fixed')
+    {
+        $this->body         = $body;
+        $this->charset      = $charset;
+        $this->encoding     = $encoding;
+        $this->content_type = $content_type;
+        $this->format       = strtolower($format);
+        $this->parse();
+    }
+
+    protected function makeDataPart($body, $content_type, $encoding, $filename, $disposition, $id = null)
+    {
+        $this->body         = $body;
+        $this->content_type = $content_type;
+        $this->encoding     = $encoding;
+        $this->filename     = $filename;
+        $this->disposition  = $disposition;
+        $this->id           = $id;
+        if (is_null($content_type) || $content_type == 'application/octet-stream') {
+            $this->decodeContent();
+            $this->content_type = BananaMimePart::getMimeType($body, false);
+        }   
+    }
+
+    protected function makeFilePart($file, $content_type =null, $disposition = 'attachment')
+    {
+        $body = file_get_contents($file['tmp_name']);
+        if ($body === false || strlen($body) != $file['size']) {
+            return false;
+        }
+        if (is_null($content_type) || $content_type == 'application/octet-stream') {
+            $content_type = BananaMimePart::getMimeType($file['tmp_name']);
+        }
+        if (substr($content_type, 0, 5) == 'text/') {
+            $encoding = '8bit';
+        } else {
+            $encoding = 'base64';
+            $body     = chunk_split(base64_encode($body));
+        }
+        $this->filename     = $file['name'];
+        $this->content_type = $content_type;
+        $this->disposition  = $disposition;
+        $this->body         = $body;
+        $this->encoding     = $encoding;
+        return true;
+    }
+
+    protected function makeMultiPart($body, $content_type, $encoding, $boundary)
+    {
+        $this->body         = $body;
+        $this->content_type = $content_type;
+        $this->encoding     = $encoding;
+        $this->boundary     = $boundary;
+        $this->parse();
+    }
+
+    protected function convertToMultiPart()
+    {
+        if (!$this->isType('multipart', 'mixed')) {
+            $newpart = $this;
+            $this->content_type = 'multipart/mixed';
+            $this->encoding     = '8bit';
+            $this->multipart    = array($newpart);
+            $this->headers      = null;
+            $this->charset      = null;
+            $this->disposition  = null;
+            $this->filename     = null;
+            $this->boundary     = null;
+            $this->body         = null;
+            $this->format       = null;
+            $this->id           = null;
+        } 
+    }
+
+    public function addAttachment(array $file, $content_type = null, $disposition = 'attachment')
+    {
+        $newpart = new BananaMimePart;
+        if (!is_uploaded_file($file['tmp_name'])) {
+            return false;
+        }
+        if ($newpart->makeFilePart($file, $content_type, $disposition)) {
+            $this->convertToMultiPart();
+            $this->multipart[] = $newpart;
+            return true;
+        }
+        return false;
+    }
+
+    protected function getHeader($title, $filter = null)
+    {
+        if (!isset($this->headers[$title])) {
+            return null;
+        }
+        $header =& $this->headers[$title];
+        if (is_null($filter)) {
+            return trim($header);
+        } elseif (preg_match($filter, $header, $matches)) {
+            return trim($matches[1]);
+        }
+        return null;
+    } 
+
+    protected function fromRaw($data)
+    {
+        if (is_array($data)) {
+            if (array_key_exists('From', $data)) {
+                $this->headers = array_map(array($this, 'decodeHeader'), array_change_key_case($data));
+                return;
+            } else {
+                $lines = $data;
+            }
+        } else {
+            $lines   = explode("\n", $data);
+        }
+        $headers = BananaMimePart::parseHeaders($lines);
+        $this->headers =& $headers;
+        if (empty($headers) || empty($lines)) {
+            return;
+        }
+        $content       = join("\n", $lines);
+        $test          = trim($content);
+        if (empty($test)) {
+            return;
+        }
+
+        $content_type = strtolower($this->getHeader('content-type', '/^\s*([^ ;]+?)(;|$)/'));
+        if (empty($content_type)) {
+            $encoding     = '8bit';
+            $charset      = 'CP1252';
+            $content_type = 'text/plain';
+            $format       = strtolower($this->getHeader('x-rfc2646', '/format="?([^"]+?)"?\s*(;|$)/i'));
+        } else {
+            $encoding     = strtolower($this->getHeader('content-transfer-encoding'));
+            $disposition  = $this->getHeader('content-disposition', '/(inline|attachment)/i');
+            $boundary     = $this->getHeader('content-type', '/boundary="?([^"]+?)"?\s*(;|$)/i');
+            $charset      = strtolower($this->getHeader('content-type', '/charset="?([^"]+?)"?\s*(;|$)/i'));
+            $filename     = $this->getHeader('content-disposition', '/filename="?([^"]+?)"?\s*(;|$)/i');
+            $format       = strtolower($this->getHeader('content-type', '/format="?([^"]+?)"?\s*(;|$)/i'));
+            $id           = $this->getHeader('content-id', '/<(.*?)>/');
+            if (empty($filename)) {
+                $filename = $this->getHeader('content-type', '/name="?([^"]+)"?/');
+            }
+        } 
+        list($type, $subtype) = explode('/', $content_type);
+        switch ($type) {
+          case 'text': case 'message':
+            $this->makeTextPart($content, $content_type, $encoding, $charset, $format);
+            break;
+          case 'multipart':
+            $this->makeMultiPart($content, $content_type, $encoding, $boundary);
+            break;
+          default:
+            $this->makeDataPart($content, $content_type, $encoding, $filename, $disposition, $id);
+        }
+    }
+
+    private function parse()
+    {
+        if ($this->isType('multipart')) {
+            $this->splitMultipart();
+        } else {
+            $parts = $this->findUUEncoded();
+            if (count($parts)) {
+                $this->convertToMultiPart();
+                $this->multipart    = array_merge(array($textpart), $parts);
+            }
+        }
+    }
+
+    private function splitMultipart()
+    {
+        $this->decodeContent();
+        if (is_null($this->multipart)) {
+            $this->multipart = array();
+        }
+        $boundary =& $this->boundary;
+        $parts = preg_split("/\n--" . preg_quote($boundary, '/') . "(--|\n)/", $this->body, -1, PREG_SPLIT_NO_EMPTY);
+        foreach ($parts as &$part) {
+            $newpart = new BananaMimePart($part);
+            if (!is_null($newpart->content_type)) {
+                $this->multipart[] = $newpart;
+            }
+        }
+        $this->body = null;
+    }
+
+    public static function getMimeType($data, $is_filename = true)
+    {
+        if ($is_filename) {
+            $type = mime_content_type($arg);
+        } else {
+            $arg = escapeshellarg($data);
+            $type = preg_replace('/;.*/', '', trim(shell_exec("echo $arg | file -bi -")));
+        }
+        return empty($type) ? 'application/octet-stream' : $type;
+    }
+
+    private function findUUEncoded()
+    {
+        $this->decodeContent();
+        $parts = array(); 
+        if (preg_match_all("/\n(begin \d+ ([^\r\n]+)\r?(?:\n(?!end)[^\n]*)*\nend)/",
+            $this->body, $matches, PREG_SET_ORDER)) {
+            foreach ($matches as &$match) {
+                $data = convert_uudecode($match[1]);
+                $mime = BananaMimePart::getMimeType($data, false);
+                if ($mime != 'application/x-empty') {
+                    $this->body = trim(str_replace($match[0], '', $this->body));
+                    $newpart = new BananaMimePart;
+                    $newpart->makeDataPart($data, $mime, '8bit', $match[2], 'attachment');
+                    $parts[] = $newpart;
+                }
+            }   
+        } 
+        return $parts;
+    }
+
+    static private function _decodeHeader($charset, $c, $str)
+    {
+        $s = ($c == 'Q' || $c == 'q') ? quoted_printable_decode($str) : base64_decode($str);
+        $s = @iconv($charset, 'UTF-8', $s);
+        return str_replace('_', ' ', $s);
+    }
+
+    static public function decodeHeader(&$val, $key)
+    {
+        if (preg_match('/[\x80-\xff]/', $val)) {
+            if (!is_utf8($val)) {
+                $val = utf8_encode($val);
+            }
+        } else {
+            $val = preg_replace('/(=\?.*?\?[bq]\?.*?\?=) (=\?.*?\?[bq]\?.*?\?=)/i', '\1\2', $val);
+            $val = preg_replace('/=\?(.*?)\?([bq])\?(.*?)\?=/ie', 'BananaMimePart::_decodeHeader("\1", "\2", "\3")', $val);
+        }    
+    }
+
+    static public function &parseHeaders(array &$lines)
+    {
+        $headers = array();
+        while (count($lines)) {
+            $line = array_shift($lines);
+            if (preg_match('/^[\t\r ]+/', $line) && isset($hdr)) {
+                $headers[$hdr] .= ' ' . trim($line);
+            } elseif (!empty($line)) {
+                if (preg_match("/:[ \t\r]*/", $line)) {
+                    list($hdr, $val) = split(":[ \t\r]*", $line, 2);
+                    $hdr = strtolower($hdr);
+                    if (in_array($hdr, Banana::$parse_hdr)) {  
+                        $headers[$hdr] = $val;
+                    } else {
+                        unset($hdr);
+                    }
+                }
+            } else {
+                break;
+            }
+        }
+        array_walk($headers, array('BananaMimePart', 'decodeHeader'));
+        return $headers;
+    }
+
+    static public function encodeHeader($value, $trim = 0)
+    {
+        if ($trim) {
+            if (strlen($value) > $trim) {
+                $value = substr($value, 0, $trim);
+            }
+        }
+        if (preg_match('/[\x80-\xff]/', $value)) {
+            return '=?UTF-8?B?' . base64_encode($value) . '?=';
+        }
+        return $value;
+    }
+
+    private function decodeContent()
+    {
+        $encodings = Array('quoted-printable' => 'quoted_printable_decode',
+                           'base64'           => 'base64_decode',
+                           'x-uuencode'       => 'convert_uudecode');
+        foreach ($encodings as $encoding => $callback) {
+            if ($this->encoding == $encoding) {
+                $this->body     = $callback($this->body);
+                $this->encoding = '8bit';
+                break;
+            }
+        }
+        if (!$this->isType('text')) {
+            return;
+        }
+
+        if (!is_null($this->charset)) {
+            $body = iconv($this->charset, 'UTF-8//IGNORE', $this->body);
+            if (empty($body)) {
+                return;
+            }
+            $this->body = $body;
+        } else {
+            $this->body = utf8_encode($this->body);
+        }
+        $this->charset = 'utf-8';
+    }
+
+    public function send($force_inline = false)
+    {
+        $this->decodeContent();
+        if ($force_inline) {
+            $dispostion =  $this->disposition;
+            $this->disposition = 'inline';
+        }
+        $headers = $this->getHeaders();
+        foreach ($headers as $key => $value) {
+            header("$key: $value");
+        }
+        if ($force_inline) {
+            $this->disposition = $disposition;
+        }
+        echo $this->body;
+        exit;
+    }
+
+    private function setBoundary()
+    {
+        if ($this->isType('multipart') && is_null($this->boundary)) {
+            $this->boundary = '--banana-bound-' . time() . rand(0, 255) . '-';
+        }
+    }
+
+    public function getHeaders()
+    {
+        $headers = array();
+        $this->setBoundary();
+        $headers['Content-Type'] = $this->content_type . ";"
+            . ($this->filename ? " name=\"{$this->filename}\";" : '')
+            . ($this->charset ? " charset=\"{$this->charset}\";" : '')
+            . ($this->boundary ? " boundary=\"{$this->boundary}\";" : "");
+        if ($this->encoding) {
+            $headers['Content-Transfer-Encoding'] = $this->encoding;
+        }
+        if ($this->disposition) {
+            $headers['Content-Disposition'] = $this->disposition
+                . ($this->filename ? "; filename=\"{$this->filename}\"" : '');
+        }
+        return array_map(array($this, 'encodeHeader'), $headers);
+    }
+
+    public function hasBody()
+    {
+        if (is_null($this->content) && !$this->isType('multipart')) {
+            return false;
+        }
+        return true;
+    }
+
+    public function get($with_headers = false)
+    {
+        $content = "";
+        if ($with_headers) {
+            foreach ($this->getHeaders() as $key => $value) {
+                $content .= "$key: $value\n"; 
+            }   
+            $content .= "\n";
+        } 
+        if ($this->isType('multipart')) {
+            $this->setBoundary();
+            foreach ($this->multipart as &$part) {
+                $content .= "\n--{$this->boundary}\n" . $part->get(true);
+            }
+            $content .= "\n--{$this->boundary}--";
+        } else {
+            $content .= $this->body;
+        }
+        return $content;
+    }
+
+    public function getText()
+    {
+        if (!$this->isType('text')) {
+            return null;
+        }
+        $this->decodeContent();
+        return $this->body;
+    }
+
+    protected function getType()
+    {
+        return explode('/', $this->content_type);
+    }
+
+    protected function isType($type, $subtype = null)
+    {
+        list($mytype, $mysub) = $this->getType();
+        return ($mytype == $type) && (is_null($subtype) || $mysub == $subtype);
+    }
+
+    public function isFlowed()
+    {
+        return $this->format == 'flowed';
+    }
+
+    public function getFilename()
+    {
+        return $this->filename;
+    }
+
+    protected function getParts($type, $subtype = null)
+    {
+        $parts = array();
+        if ($this->isType($type, $subtype)) {
+            return array($this);
+        } elseif ($this->isType('multipart')) {
+            foreach ($this->multipart as &$part) {
+                $parts = array_merge($parts, $part->getParts($type, $subtype));
+            }
+        }
+        return $parts;
+    }
+
+    public function toPlainText()
+    {
+        $parts = $this->getParts('text', 'plain');
+        return (count($parts) ? $parts[0] : null);
+    }
+
+    public function toHtml()
+    {
+        $parts = $this->getParts('text', 'html');
+        return (count($parts) ? $parts[0] : null);
+    }
+
+    public function toRichText()
+    {
+        $parts = $this->getParts('text', 'enriched');
+        return (count($parts) ? $parts[0] : null);
+    }
+
+    public function getFile($filename)
+    {
+        if ($this->filename == $filename) {
+            return $this;
+        } elseif ($this->isType('multipart')) {
+            foreach ($this->multipart as &$part) {
+                $file = $part->getFile($filename);
+                if (!is_null($file)) {
+                    return $file;
+                }
+            }
+        }
+        return null;
+    }
+
+    public function getAttachments()
+    {
+        if (!is_null($this->filename)) {
+            return array($this);
+        } elseif ($this->isType('multipart')) {
+            $parts = array();
+            foreach ($this->multipart as &$part) {
+                $parts = array_merge($parts, $part->getAttachments());
+            }
+            return $parts;
+        }
+        return array();
+    }
+
+    public function getPartById($id)
+    {
+        if ($this->id == $id) {
+            return $this;
+        } elseif ($this->isType('multipart')) {
+            foreach ($this->multipart as &$part) {
+                $res = $part->getPartById($id);
+                if (!is_null($res)) {
+                    return $res;
+                }
+            }
+        }
+        return null;
+    }
+}
+
+// vim:set et sw=4 sts=4 ts=4:
+?>
diff --git a/banana/misc.inc.php b/banana/misc.inc.php
deleted file mode 100644 (file)
index 48c8081..0000000
+++ /dev/null
@@ -1,670 +0,0 @@
-<?php
-/********************************************************************************
- * include/misc.inc.php : Misc functions
- * -------------------------
- *
- * This file is part of the banana distribution
- * Copyright: See COPYING files that comes with this distribution
- ********************************************************************************/
-
-/********************************************************************************
- *  MISC
- */
-
-function _b_($str) { return utf8_decode(dgettext('banana', utf8_encode($str))); }
-
-function to_entities($str) {
-    require_once dirname(__FILE__).'/utf8.php';
-    return utf8entities(htmlentities($str, ENT_NOQUOTES, 'UTF-8'));
-}
-
-function is_utf8($s) { return iconv('utf-8', 'utf-8', $s) == $s; }
-
-function textFormat_translate($format)
-{
-    switch (strtolower($format)) {
-        case 'plain':       return _b_('Texte brut');
-        case 'richtext':    return _b_('Texte enrichi');
-        case 'html':        return _b_('HTML');
-        default:            return $format;
-    }
-}
-
-/** Redirect to the page with the given parameter
- * @ref makeLink
- */
-function redirectInBanana($params)
-{
-    header('Location: ' . makeLink($params));
-}
-
-/** Make a link using the given parameters
- * @param ARRAY params, the parameters with
- *       key => value
- * Known key are:
- *  - group       = group name
- *  - artid/first = article id the the group
- *  - subscribe   = to show the subscription page
- *  - action      = action to do (new, cancel, view)
- *  - part        = to show the given MIME part of the article
- *  - pj          = to get the given attachment
- *  - xface       = to make a link to an xface
- *
- * Can be overloaded by defining a hook_makeLink function
- */
-function makeLink($params)
-{
-    if (function_exists('hook_makeLink')
-            && $res = hook_makeLink($params)) {
-        return $res;
-    }
-    $proto = empty($_SERVER['HTTPS']) ? 'http://' : 'https://';
-    $host  = $_SERVER['HTTP_HOST'];
-    $file  = $_SERVER['PHP_SELF'];
-
-    if (isset($params['xface'])) {
-        $file = dirname($file) . '/xface.php';
-        $get  = 'face=' . urlencode(base64_encode($params['xface']));
-    } else if (count($params) != 0) {
-        $get = '?';
-        foreach ($params as $key=>$value) {
-            if (strlen($get) != 1) {
-                $get .= '&';
-            }
-            $get .= $key . '=' . $value;
-        }
-    } else {
-        $get = '';
-    }
-
-    return $proto . $host . $file . $get;
-}
-
-/** Format a link to be use in a link
- * @ref makeLink
- */
-function makeHREF($params, $text = null, $popup = null, $class = null, $accesskey = null)
-{
-    $link = makeLink($params);
-    if (is_null($text)) {
-        $text = $link;
-    }
-    if (!is_null($accesskey)) {
-        $popup .= ' (raccourci : ' . $accesskey . ')';
-    }
-    if (!is_null($popup)) {
-        $popup = ' title="' . $popup . '"';
-    }
-    if (!is_null($class)) {
-        $class = ' class="' . $class . '"';
-    }
-    $target = null;
-    if (isset($params['action']) && $params['action'] == 'view') {
-        $target = ' target="_blank"';
-    }
-    if (!is_null($accesskey)) {
-        $accesskey = ' accesskey="' . $accesskey . '"';
-    }
-
-    return '<a href="' . htmlentities($link) . '"' 
-          . $target . $popup . $class . $accesskey
-          . '>' . $text . '</a>';
-}
-
-/** Format tree images links
- * @param img STRING Image name (without extension)
- * @param alt STRING alternative string
- * @param width INT  to force width of the image (null if not defined)
- *
- * This function can be overloaded by defining hook_makeImg()
- */
-function makeImg($img, $alt, $height = null, $width = null)
-{
-    if (function_exists('hook_makeImg')
-            && $res = hook_makeImg($img, $alt, $height, $width)) {
-        return $res;
-    }
-
-    if (!is_null($width)) {
-        $width = ' width="' . $width . '"';
-    }
-    if (!is_null($height)) {
-        $height = ' height="' . $height . '"';
-    }
-    
-    $proto = empty($_SERVER['HTTPS']) ? 'http://' : 'https://';
-    $host  = $_SERVER['HTTP_HOST'];
-    $file  = dirname($_SERVER['PHP_SELF']) . '/img/' . $img;
-    $url   = $proto . $host . $file; 
-
-    return '<img src="' . $url . '"' . $height . $width . ' alt="' . $alt . '" />';
-}
-
-/** Make a link using an image
- */
-function makeImgLink($params, $img, $alt, $height = null, $width = null, $class = null, $accesskey = null)
-{
-    return makeHREF($params,
-                    makeImg($img, ' [' . $alt . ']', $height, $width),
-                    $alt,
-                    $class,
-                    $accesskey);
-}
-
-/********************************************************************************
- * HTML STUFF
- * Taken from php.net
- */
-
-/**
- * @return string
- * @param string
- * @desc Strip forbidden tags and delegate tag-source check to removeEvilAttributes()
- */
-function removeEvilTags($source)
-{
-    $allowedTags = '<h1><b><i><a><ul><li><pre><hr><blockquote><img><br><font><p><small><big><sup><sub><code><em>';
-    $source = preg_replace('|</div>|i', '<br />', $source);
-    $source = strip_tags($source, $allowedTags);
-    return preg_replace('/<(.*?)>/ie', "'<'.removeEvilAttributes('\\1').'>'", $source);
-}
-
-/**
- * @return string
- * @param string
- * @desc Strip forbidden attributes from a tag
- */
-function removeEvilAttributes($tagSource)
-{
-    $stripAttrib = 'javascript:|onclick|ondblclick|onmousedown|onmouseup|onmouseover|'.
-                   'onmousemove|onmouseout|onkeypress|onkeydown|onkeyup';
-    return stripslashes(preg_replace("/$stripAttrib/i", '', $tagSource));
-}
-
-/** Convert html to plain text
- */
-function htmlToPlainText($res)
-{
-    $res = trim(html_entity_decode(strip_tags($res, '<div><br><p>')));
-    $res = preg_replace("@</?(br|p|div)[^>]*>@i", "\n", $res);
-    if (!is_utf8($res)) {
-        $res = utf8_encode($res);
-    }   
-    return $res;
-}
-
-/** Match **, // and __ to format plain text
- */
-function formatPlainText($text)
-{
-    $formatting = Array('\*' => 'strong',
-                        '_' => 'u',
-                        '/' => 'em');
-    foreach ($formatting as $limit=>$mark) {
-        $text = preg_replace('@(^|\W)' . $limit . '(\w+)' . $limit . '(\W|$)@'
-                            ,'\1<' . $mark . '>\2</' . $mark . '>\3'
-                            , $text);
-    }
-    return $text;
-} 
-
-/********************************************************************************
- * RICHTEXT STUFF
- */
-
-/** Convert richtext to html
- */
-function richtextToHtml($source)
-{
-    $tags = Array('bold' => 'b',
-                  'italic' => 'i',
-                  'smaller' => 'small',
-                  'bigger' => 'big',
-                  'underline' => 'u',
-                  'subscript' => 'sub',
-                  'superscript' => 'sup',
-                  'excerpt' => 'blockquote',
-                  'paragraph' => 'p',
-                  'nl' => 'br'
-            );
-            
-    // clean unsupported tags
-    $protectedTags = '<signature><lt><comment><'.join('><', array_keys($tags)).'>';
-    $source = strip_tags($source, $protectedTags);
-    
-    // convert richtext tags to html
-    foreach (array_keys($tags) as $tag) {
-        $source = preg_replace('@(</?)'.$tag.'([^>]*>)@i', '\1'.$tags[$tag].'\2', $source);
-    }
-
-    // some special cases
-    $source = preg_replace('@<signature>@i', '<br>-- <br>', $source);
-    $source = preg_replace('@</signature>@i', '', $source);
-    $source = preg_replace('@<lt>@i', '&lt;', $source);
-    $source = preg_replace('@<comment[^>]*>((?:[^<]|<(?!/comment>))*)</comment>@i', '<!-- \1 -->', $source);
-    return removeEvilAttributes($source);
-}
-
-/********************************************************************************
- *  HEADER STUFF
- */
-
-function _headerdecode($charset, $c, $str) {
-    $s = ($c == 'Q' || $c == 'q') ? quoted_printable_decode($str) : base64_decode($str);
-    $s = iconv($charset, 'iso-8859-15', $s);
-    return str_replace('_', ' ', $s);
-}
-function headerDecode($value) {
-    $val = preg_replace('/(=\?[^?]*\?[BQbq]\?[^?]*\?=) (=\?[^?]*\?[BQbq]\?[^?]*\?=)/', '\1\2', $value);
-    return preg_replace('/=\?([^?]*)\?([BQbq])\?([^?]*)\?=/e', '_headerdecode("\1", "\2", "\3")', $val);
-}
-
-function headerEncode($value, $trim = 0) {
-    if ($trim) {
-        if (strlen($value) > $trim) {
-            $value = substr($value, 0, $trim) . "[...]";
-        }
-    }
-    return "=?UTF-8?B?".base64_encode($value)."?=";
-}
-
-function header_translate($hdr) {
-    switch ($hdr) {
-        case 'from':            return _b_('De');
-        case 'subject':         return _b_('Sujet');
-        case 'newsgroups':      return _b_('Forums');
-        case 'followup-to':     return _b_('Suivi-à');
-        case 'date':            return _b_('Date');
-        case 'organization':    return _b_('Organisation');
-        case 'references':      return _b_('Références');
-        case 'x-face':          return _b_('Image');
-        default:
-            if (function_exists('hook_headerTranslate')
-                    && $res = hook_headerTranslate($hdr)) {
-                return $res;
-            }
-            return $hdr;
-    }
-}
-
-function formatDisplayHeader($_header,$_text) {
-    global $banana;
-    if (function_exists('hook_formatDisplayHeader')
-            && $res = hook_formatDisplayHeader($_header, $_text)) {
-        return $res;
-    }
-
-    switch ($_header) {
-        case "date": 
-            return formatDate($_text);
-        
-        case "followup-to":
-            case "newsgroups":
-            $res = "";
-            $groups = preg_split("/[\t ]*,[\t ]*/",$_text);
-            foreach ($groups as $g) {
-                $res .= makeHREF(Array('group' => $g), $g) . ', ';
-            }
-            return substr($res,0, -2);
-
-        case "from":
-            return formatFrom($_text);
-
-        case "references":
-            $rsl     = "";
-            $ndx     = 1;
-            $text    = str_replace("><","> <",$_text);
-            $text    = preg_split("/[ \t]/",strtr($text,$banana->spool->ids));
-            $parents = preg_grep("/^\d+$/",$text);
-            $p       = array_pop($parents);
-            $par_ok  = Array();
-            
-            while ($p) {
-                $par_ok[]=$p;
-                $p = $banana->spool->overview[$p]->parent;
-            }
-            foreach (array_reverse($par_ok) as $p) {
-                $rsl .= makeHREF(Array('group' => $banana->spool->group,
-                                       'artid' => $p),
-                                 $ndx) . ' ';
-                $ndx++;
-            }
-            return $rsl;
-
-        case "x-face":
-            return '<img src="' . makeLink(Array('xface' => headerDecode($_text))) .'"  alt="x-face" />';
-    
-        case "subject":
-            $link = null;
-            if (function_exists('hook_getSubject')) {
-                $link = hook_getSubject($_text);
-            }
-            return formatPlainText($_text) . $link;
-
-        default:
-            return htmlentities($_text);
-    }
-}
-
-/********************************************************************************
- *  FORMATTING STUFF
- */
-
-function formatDate($_text) {
-    return strftime("%A %d %B %Y, %H:%M (fuseau serveur)", strtotime($_text));
-}
-
-function fancyDate($stamp) {
-    $today  = intval(time() / (24*3600));
-    $dday   = intval($stamp / (24*3600));
-
-    if ($today == $dday) {
-        $format = "%H:%M";
-    } elseif ($today == 1 + $dday) {
-        $format = _b_('hier')." %H:%M";
-    } elseif ($today < 7 + $dday) {
-        $format = '%a %H:%M';
-    } else {
-        $format = '%a %e %b';
-    }
-    return strftime($format, $stamp);
-}
-
-function formatFrom($text) {
-#     From: mark@cbosgd.ATT.COM
-#     From: mark@cbosgd.ATT.COM (Mark Horton)
-#     From: Mark Horton <mark@cbosgd.ATT.COM>
-    $mailto = '<a href="&#109;&#97;&#105;&#108;&#116;&#111;&#58;';
-
-    $result = htmlentities($text);
-    if (preg_match("/^([^ ]+)@([^ ]+)$/",$text,$regs)) {
-        $result="$mailto{$regs[1]}&#64;{$regs[2]}\">".htmlentities($regs[1]."&#64;".$regs[2])."</a>";
-    }
-    if (preg_match("/^([^ ]+)@([^ ]+) \((.*)\)$/",$text,$regs)) {
-        $result="$mailto{$regs[1]}&#64;{$regs[2]}\">".htmlentities($regs[3])."</a>";
-    }
-    if (preg_match("/^\"?([^<>\"]+)\"? +<(.+)@(.+)>$/",$text,$regs)) {
-        $result="$mailto{$regs[2]}&#64;{$regs[3]}\">".htmlentities($regs[1])."</a>";
-    }
-    return preg_replace("/\\\(\(|\))/","\\1",$result);
-}
-
-function makeTab($link, $text)
-{
-    return Array(makeHREF($link, $text),
-                 $text);
-}
-
-function displayTabs()
-{
-    global $banana;
-    extract($banana->state);
-    if (function_exists('hook_shortcuts') && $cstm = hook_shortcuts()) {
-        $res = $cstm;
-    }
-    $res['subscribe'] = makeTab(Array('subscribe' => 1), _b_('Abonnements'));
-    $res['forums']    = makeTab(Array(), _b_('Les forums'));
-
-    if (!is_null($group)) {
-        $res['group'] = makeTab(Array('group' => $group), $group);
-        if (is_null($artid)) {
-            if (@$action == 'new') {
-                $res['action'] = makeTab(Array('group'  => $group,
-                                               'action' => 'new'),
-                                         _b_('Nouveau Message'));
-            }
-        } else {
-            $res['message'] = makeTab(Array('group' => $group,
-                                            'artid' => $artid),
-                                      _b_('Message'));
-            if (!is_null($action)) {
-                if ($action == 'new') {
-                    $res['action'] = makeTab(Array('group'  => $group,
-                                                   'artid'  => $artid,
-                                                   'action' => 'new'),
-                                             _b_('Réponse'));
-                } elseif ($action == 'cancel') {
-                    $res['action'] = makeTab(Array('group'  => $group,
-                                                   'artid'  => $artid,
-                                                   'action' => 'cancel'),
-                                             _b_('Annuler'));
-                }
-            }
-        }
-    }
-    $ret = '<ul id="onglet">';
-    foreach ($res as $name=>$onglet) {
-        if ($name != @$page) {
-            $ret .= '<li>' . $onglet[0] . '</li>';
-        } else {
-            $ret .= '<li class="actif">' . $onglet[1] . '</li>';
-        }
-    }
-    $ret .= '</ul>';
-    return $ret;
-}
-
-function displayPages($first = -1)
-{
-    global $banana;
-    extract($banana->state);
-    $res = null; 
-    if (!is_null($group) && is_null($artid)
-            && sizeof($banana->spool->overview)>$banana->tmax) {
-        $res .= '<div class="pages">';
-        $n = intval(log(count($banana->spool->overview), 10))+1;
-        $i = 1;
-        for ($ndx = 1 ; $ndx <= sizeof($banana->spool->overview) ; $ndx += $banana->tmax) {
-            if ($first==$ndx) {
-                $res .= '<strong>' . $i . '</strong> ';
-            } else {
-                $res .= makeHREF(Array('group' => $group,
-                                       'first' => $ndx),
-                                 $i, 
-                                 $ndx . '-' . min($ndx+$banana->tmax-1,sizeof($banana->spool->overview)))
-                     . ' ';
-            }
-            $i++;
-        }
-        $res .= '</div>';
-    }
-    return $res;
-}
-
-function makeTable($text)
-{
-    $links = null;
-    if (function_exists('hook_browsingAction')) {
-        $links = hook_browsingAction();
-    }
-
-    return '<table class="cadre_a_onglet" cellpadding="0" cellspacing="0" width="100%">'
-         . '<tr><td>'
-         . displayTabs()
-         . '</td></tr>'
-         . '<tr><td class="conteneur_tab">'
-         . $links
-         . $text
-         . '</td></tr>'
-         . '</table>';
-}        
-
-/********************************************************************************
- *  FORMATTING STUFF : BODY
- */
-
-function autoformat($text, $force = 0)
-{
-    global $banana;
-    $length = $banana->wrap;
-    $force  = $force ? 1 : 0; 
-    $cmd = 'echo ' . escapeshellarg($text)
-         . ' | perl -MText::Autoformat -e \'autoformat {left=>1, right=>' . $length . ', all=>' . $force . ' };\'';
-
-    exec($cmd, $result, $ret);
-    if ($ret != 0) {
-        $result = split("\n", $text);
-    }
-    return $result;
-}                                
-
-function wrap($text, $_prefix="", $_force=false, $firstpass = true)
-{
-    $parts = preg_split("/\n-- ?\n/", $text);
-    if (count($parts)  >1) {
-        $sign = "\n-- \n" . array_pop($parts);
-        $text = join("\n-- \n", $parts);
-    } else {
-        $sign = '';
-    }
-
-    global $banana;
-    $url    = $banana->url_regexp;
-    $length = $banana->wrap;
-    $max    = $length + ($length/10);
-    $splits = split("\n", $text);
-    $result = array();
-    $next   = array();
-    $format = false;
-    foreach ($splits as $line) {
-        if ($_force || strlen($line) > $max) {
-            if (preg_match("!^(.*)($url)(.*)!i", $line, $matches)
-                    && strlen($matches[2]) > $length && strlen($matches) < 900) {
-                if (strlen($matches[1]) != 0) {
-                    array_push($next, rtrim($matches[1]));
-                    if (strlen($matches[1]) > $max) {
-                        $format = true;
-                    }
-                }
-                    
-                if ($format) {
-                    $result = array_merge($result, autoformat(join("\n", $next), $firstpass));
-                } else {
-                    $result = array_merge($result, $next);
-                }
-                $format = false;
-                $next   = array();
-                array_push($result, $matches[2]);
-                    
-                if (strlen($matches[6]) != 0) {
-                    array_push($next, ltrim($matches[6]));
-                    if (strlen($matches[6]) > $max) {
-                        $format = true;
-                    }
-                }
-            } else {
-                array_push($next, $line);
-                $format = true;
-            }
-        } else {
-            array_push($next, $line);
-        }
-    }
-    if ($format) {
-        $result = array_merge($result, autoformat(join("\n", $next), $firstpass));
-    } else {
-        $result = array_merge($result, $next);
-    }
-
-    $break = "\n";
-    $prefix = null;
-    if (!$firstpass) {
-        $break .= $_prefix;
-        $prefix = $_prefix;
-    }
-    $result = $prefix.join($break, $result).($prefix ? '' : $sign);
-    if ($firstpass) {
-        return wrap($result, $_prefix, $_force, false);
-    }
-    return $result;
-}
-
-function cutlink($link)
-{
-    global $banana;
-    
-    if (strlen($link) > $banana->wrap) {
-        $link = substr($link, 0, $banana->wrap - 3)."...";
-    }
-    return $link;
-}
-
-function cleanurl($url)
-{
-    $url = str_replace('@', '%40', $url);
-    return '<a href="'.$url.'" title="'.$url.'">'.cutlink($url).'</a>';
-}
-
-function catchMailLink($email)
-{
-    global $banana;
-    $mid = '<' . $email . '>';
-    if (isset($banana->spool->ids[$mid])) {
-        return makeHREF(Array('group' => $banana->state['group'],
-                              'artid' => $banana->spool->ids[$mid]),
-                        $email);
-    } elseif (strpos($email, '$') !== false) {
-        return $email;
-    }   
-    return '<a href="mailto:' . $email . '">' . $email . '</a>';
-}
-
-/** Remove quotation marks
- */
-function replaceQuotes($text)
-{
-    return stripslashes(preg_replace("@(^|<pre>|\n)&gt;[ \t\r]*@i", '\1', $text));
-}
-
-function formatbody($_text, $format='plain', $flowed=false)
-{
-    if ($format == 'html') {
-        $res = html_entity_decode(to_entities(removeEvilTags($_text)));
-    } else if ($format == 'richtext') {
-        $res = html_entity_decode(to_entities(richtextToHtml($_text)));
-    } else {
-        $res  = to_entities(wrap($_text, "", $flowed));
-        $res  = formatPlainText($res);
-    }
-
-    if ($format != 'html') {
-        global $banana;
-        $url  = $banana->url_regexp;
-        $res  = preg_replace("/(&lt;|&gt;|&quot;)/", " \\1 ", $res);
-        $res  = preg_replace("!$url!ie", "'\\1'.cleanurl('\\2').'\\3'", $res);
-        $res  = preg_replace('/(["\[])?(?:mailto:|news:)?([a-z0-9.\-+_\$]+@([\-.+_]?[a-z0-9])+)(["\]])?/ie',
-                             "'\\1' . catchMailLink('\\2') . '\\4'",
-                             $res);
-        $res  = preg_replace("/ (&lt;|&gt;|&quot;) /", "\\1", $res);
-
-        if ($format == 'richtext') {
-            $format = 'html';
-        }
-    }
-    if ($format == 'html') {
-        $res = preg_replace("@(</p>)\n?-- ?\n?(<p[^>]*>|<br[^>]*>)@", "\\1<br/>-- \\2", $res);
-        $res = preg_replace("@<br[^>]*>\n?-- ?\n?(<p[^>]*>)@", "<br/>-- <br/>\\2", $res);
-        $res = preg_replace("@(<pre[^>]*>)\n?-- ?\n@", "<br/>-- <br/>\\1", $res);
-        $parts = preg_split("@(:?<p[^>]*>\n?-- ?\n?</p>|<br[^>]*>\n?-- ?\n?<br[^>]*>)@", $res);
-        $sign  = '<hr style="width: 100%; margin: 1em 0em; " />';
-    } else {
-        while (preg_match("@(^|<pre>|\n)&gt;@i", $res)) {
-            $res  = preg_replace("@(^|<pre>|\n)((&gt;[^\n]*(?:\n|$))+)@ie",
-                "'\\1</pre><blockquote><pre>'"
-                ." . replaceQuotes('\\2')"
-                ." . '</pre></blockquote><pre>'",
-                $res);
-        }
-        $res = preg_replace("@<pre>-- ?\n@", "<pre>\n-- \n", $res);
-        $parts = preg_split("/\n-- ?\n/", $res);
-        $sign  = '</pre><hr style="width: 100%; margin: 1em 0em; " /><pre>';
-    }
-
-    return join($sign, $parts);
-}
-
-// vim:set et sw=4 sts=4 ts=4
-?>
diff --git a/banana/nntp.inc.php b/banana/nntp.inc.php
new file mode 100644 (file)
index 0000000..f1dde9d
--- /dev/null
@@ -0,0 +1,247 @@
+<?php
+/********************************************************************************
+* banana/nntp.inc.php : NNTP protocole handler
+* ------------------------
+*
+* This file is part of the banana distribution
+* Copyright: See COPYING files that comes with this distribution
+********************************************************************************/
+
+require_once dirname(__FILE__) . '/banana.inc.php';
+require_once dirname(__FILE__) . '/message.inc.php';
+require_once dirname(__FILE__) . '/nntpcore.inc.php';
+require_once dirname(__FILE__) . '/protocoleinterface.inc.php';
+
+class BananaNNTP extends BananaNNTPCore implements BananaProtocoleInterface
+{
+    private $groupname = null;
+    private $description = null;
+    private $ingroup = null;
+
+    private $mode = null;
+    private $boxes = null;
+
+    /** Build a protocole handler plugged on the given box
+     */
+    public function __construct($box = null)
+    {
+        $url = parse_url(Banana::$host);
+        if ($url['scheme'] == 'nntps' || $url['scheme'] == 'snntp') {
+            $url['host'] = 'ssl://' . $url['host'];
+        }
+        if (!isset($url['port'])) {
+            $url['port'] = 119;
+        }
+        if (!isset($url['user'])) {
+            parent::__construct($url['host'], $url['port']);
+        } else {
+            parent::__construct($url['host'], $url['port'], 120, false);
+            $this->authinfo($url['user'], $url['pass']);
+        }      
+        $this->groupname = $box;
+    }
+
+    /** Return the descript;ion of the current box
+     */
+    public function getDescription()
+    {
+        if ($this->description) {
+            return $this->description;
+        }
+        $descs = $this->xgtitle($this->groupname);
+        if (isset($descs[$this->groupname])) {
+            $this->description = $descs[$this->groupname];
+        }
+        return $this->description;
+    }
+
+    /** Return the list of the boxes
+     * @param mode Kind of boxes to list
+     * @param since date of last check (for new boxes and new messages)
+     * @return Array(boxname => array(desc => boxdescripton, msgnum => number of message, unread =>number of unread messages)
+     */
+    public function getBoxList($mode = Banana::BOXES_ALL, $since = 0, $withstats = false)
+    {
+        if (!is_array($this->boxes) || $this->mode != $mode) {
+            $descs = $this->xgtitle();
+            if ($mode == Banana::BOXES_NEW && $since) {
+                $list = $this->newgroups($since);
+            } else {
+                $list = $this->listGroups();
+                if ($mode == Banana::BOXES_SUB) {
+                    $sub = array_flip(Banana::$profile['subscribe']);
+                    $list = array_intersect_key($list, $sub);
+                }
+            }
+            $this->boxes = array();
+            foreach ($list as $group=>&$infos) {
+                if (isset($descs[$group])) {
+                    $desc = $descs[$group];
+                    if (!is_utf8($desc)) {
+                        $desc = utf8_encode($desc);
+                    }
+                    $this->boxes[$group] = array('desc' => $desc);           
+                } else {
+                    $this->boxes[$group] = array('desc' => null);
+                }    
+            }
+            ksort($this->boxes);
+        }
+        if ($withstats) {
+            foreach ($this->boxes as $group=>&$desc) {
+                list($msgnum, $first, $last, $groupname) = $this->group($group);
+                $this->ingroup = $group;
+                $new = count($this->newnews($group, $since));
+                $desc['msgnum'] = $msgnum;
+                $desc['unread'] = $new;
+            }
+        }
+        return $this->boxes;
+    }
+
+    /** Return a message
+     * @param id Id of the emssage (can be either an Message-id or a message index)
+     * @param msg_headers Headers to process
+     * @param is_msgid If is set, $id is en Message-Id
+     * @return A BananaMessage or null if the given id can't be retreived
+     */
+    public function getMessage($id, array $msg_headers = array(), $is_msgid = false)
+    {
+        if (!$is_msgid && $this->groupname != $this->ingroup) {
+            if (is_null(Banana::$spool)) {
+                $this->group($this->groupname);
+                $this->ingroup = $this->groupname;
+            } else {
+                $id = array_search($id, Banana::$spool->ids);
+            }
+        }
+        $data = $this->article($id);
+        if ($data !== false) {
+            return new BananaMessage($data);
+        }
+        return null;
+    }
+
+    /** Return the indexes of the messages presents in the Box
+     * @return Array(number of messages, MSGNUM of the first message, MSGNUM of the last message)
+     */
+    public function getIndexes()
+    {
+        list($msgnum, $first, $last, $groupname) = $this->group($this->groupname);
+        $this->ingroup = $this->groupname;
+        return array($msgnum, $first, $last);
+    }
+
+    /** Return the message headers (in BananaMessage) for messages from firstid to lastid
+     * @return Array(id => array(headername => headervalue))
+     */
+    public function &getMessageHeaders($firstid, $lastid, array $msg_headers = array())
+    {
+        $messages = array();
+        foreach ($msg_headers as $header) {
+            $headers = $this->xhdr($header, $firstid, $lastid);
+            array_walk($headers, array('BananaMimePart', 'decodeHeader'));
+            $header  = strtolower($header);
+            if ($header == 'date') {
+                $headers = array_map('strtotime', $headers);
+            }
+            foreach ($headers as $id=>&$value) {
+                if (!isset($messages[$id])) {
+                    $messages[$id] = array();
+                }
+                $messages[$id][$header] =& $value;
+            }
+        }
+        return $messages;
+    }
+
+    /** Add protocole specific data in the spool
+     */
+    public function updateSpool(array &$messages)
+    {
+        return true;
+    }
+
+    /** Return the indexes of the new messages since the give date
+     * @return Array(MSGNUM of new messages)
+     */
+    public function getNewIndexes($since)
+    {
+        return $this->newnews($this->groupname, $since);
+    }
+
+    /** Return true if can post
+     */
+    public function canSend()
+    {
+        return $this->isValid();
+    }
+
+    /** Return true if can cancel
+     */
+    public function canCancel()
+    {
+        return $this->isValid();
+    }
+
+    /** Return the list of requested header for a new post
+     */
+    public function requestedHeaders()
+    {
+        return Array('From', 'Subject', 'dest' => 'Newsgroups', 'reply' => 'Followup-To', 'Organization');
+    }
+
+    /** Send the message
+     */
+    public function send(BananaMessage &$message)
+    {
+        $sources = $message->get(true);
+        return $this->post($sources);
+    }
+
+    /** Cancel the message
+     */
+    public function cancel(BananaMessage &$message)
+    {
+        $headers = Array('From' => Banana::$profile['From'],
+                         'Newsgroups' => $this->groupname,
+                         'Subject'    => 'cmsg ' . $message->getHeaderValue('message-id'),
+                         'Control'    => 'cancel ' . $message->getHeaderValue('message-id'));
+        $headers = array_merge($headers, Banana::$custom_hdr);
+        $body   = 'Message canceled with Banana';
+        $msg    = BananaMessage::newMessage($headers, $body);
+        return $this->send($msg);
+    }
+
+    /** Return the protocole name
+     */
+    public function name()
+    {
+        return 'NNTP';
+    }
+}
+
+/*
+require_once dirname(__FILE__) . '/spool.inc.php';
+$time = microtime(true);
+$nntp = new BananaNNTP('xorg.promo.x2002');
+if (!$nntp->isValid()) {
+    echo "Beuh !\n";
+    exit;
+}
+Banana::$protocole =& $nntp;
+Banana::$spool =& BananaSpool::getSpool('xorg.promo.x2002');
+$msg = $nntp->getMessage(3424);
+echo '<html><head>
+        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+        <link rel="stylesheet" type="text/css" href="css/banana.css"/>
+</head><body><table class="banana_msg"><tr><td>';
+//echo $msg->getFormattedBody('plain');
+echo $msg->getFormattedBody();
+echo '</td></tr></table></body></html>', "\n";
+$end = microtime(true);
+echo ($end - $time) . "s\n";
+*/ 
+
+// vim:set et sw=4 sts=4 ts=4:
+?>
diff --git a/banana/nntpcore.inc.php b/banana/nntpcore.inc.php
new file mode 100644 (file)
index 0000000..1ab299e
--- /dev/null
@@ -0,0 +1,511 @@
+<?php
+/********************************************************************************
+* include/nntpcore.inc.php : NNTP subroutines
+* -------------------------
+*
+* This file is part of the banana distribution
+* Copyright: See COPYING files that comes with this distribution
+********************************************************************************/
+
+require_once dirname(__FILE__) . '/banana.inc.php';
+
+/** Class NNTPCore
+ *  implements some basic functions for NNTP protocol
+ */
+class BananaNNTPCore
+{
+    /** socket filehandle */
+    private $ns;
+    /** posting allowed */
+    private $posting;
+    /** last NNTP error code */
+    private $lasterrorcode;
+    /** last NNTP error text */
+    private $lasterrortext;
+    /** last NNTP result code */
+    private $lastresultcode;
+    /** last NNTP result */
+    private $lastresulttext;
+
+    /** debug mode */
+    private $debug = false;
+
+    /** constructor
+     * @param $host STRING NNTP host
+     * @parma $port STRING NNTP port
+     * @param $timeout INTEGER socket timeout
+     * @param $reader BOOLEAN sends a "MODE READER" at connection if true
+     */
+    public function __construct($host, $port = 119, $timeout = 120, $reader = true)
+    {
+        if (Banana::$debug_nntp) {
+            $this->debug = true;
+        }
+        $this->ns            = fsockopen($host, $port, $errno, $errstr, $timeout);
+        $this->lasterrorcode = $errno;
+        $this->lasterrortext = $errstr;
+        if (is_null($this->ns)) {
+            return;
+        }
+
+        $this->checkState();
+        $this->posting = ($this->lastresultcode == '200'); 
+        if ($reader && $this->posting) {
+            $this->execLine('MODE READER');
+            $this->posting = ($this->lastresultcode == '200');
+        }
+        if (!$this->posting) {
+            $this->quit();
+        }
+    }
+
+    public function __destruct()
+    {
+        $this->quit();
+    }
+
+# Accessors
+
+    public function isValid()
+    {
+        return !is_null($this->ns) && $this->posting;
+    }
+
+    public function lastErrNo()
+    {
+        return $this->lasterrorcode;
+    }
+
+    public function lastError()
+    {
+        return $this->lasterrortext;
+    }
+
+# Socket functions
+
+    /** get a line from server
+     * @return STRING 
+     */
+    private function getLine()
+    {
+        return rtrim(fgets($this->ns, 1200));
+    }
+
+    /** fetch data (and on delimitor)
+     * @param STRING $delim string indicating and of transmission
+     */
+    private function fetchResult($callback = null)
+    {
+        $array = Array();
+        while (($result = $this->getLine()) != '.') {
+            if (!is_null($callback)) {
+                list($key, $result) = call_user_func($callback, $result);
+                if (is_null($result)) {
+                    continue;
+                }
+                if (is_null($key)) {
+                    $array[] = $result;
+                } else {
+                    $array[$key] = $result;
+                }
+            } else {
+                $array[] = $result;
+            }
+        }
+        return $array;
+    }
+
+    /** puts a line on server
+     * @param STRING $line line to put
+     */
+    private function putLine($line, $format = false)
+    {
+        if ($format) {
+            $line = str_replace(array("\r", "\n"), '', $line);  
+            $line .= "\r\n";
+        }
+        if ($this->debug) {
+            $db_line = preg_replace('/PASS .*/', 'PASS *******', $line);
+            echo $db_line;
+        }
+        return fputs($this->ns, $line, strlen($line));
+    }
+
+    /** put a message (multiline)
+     */
+    private function putMessage($message = false)
+    {
+        if (is_array($message)) {
+            $message = join("\n", $_message);
+        }
+        if ($message) {
+            $this->putLine("$message\r\n", false);
+        }
+        return $this->execLine('.');
+    }
+
+
+    /** exec a command a check result
+     * @param STRING $line line to exec
+     */
+    private function execLine($line, $strict_state = true)
+    {
+        if (!$this->putLine($line, true)) {
+            return null;
+        }
+        return $this->checkState($strict_state);
+    }
+
+    /** check if last command was successfull (read one line)
+     * @param BOOL $strict indicate if 1XX codes are interpreted as errors (true) or success (false)
+     */
+    private function checkState($strict = true)
+    {
+        $result = $this->getLine();
+        if ($this->debug) {
+            echo "$result\n";
+        }
+        $this->lastresultcode = substr($result, 0, 3);
+        $this->lastresulttext = substr($result, 4);
+        $c = $this->lastresultcode{0};
+        if ($c == '2' || (($c == '1' || $c == '3') && !$strict)) {
+            return true;
+        } else {
+            $this->lasterrorcode = $this->lastresultcode;
+            $this->lasterrortext = $this->lastresulttext;
+            return false;
+        }
+    }
+
+# strict NNTP Functions [RFC 977]
+# see http://www.faqs.org/rfcs/rfc977.html
+
+    /** authentification
+     * @param $user STRING login
+     * @param $pass INTEGER password
+     * @return BOOLEAN true if authentication was successful
+     */
+    protected function authinfo($user, $pass)
+    {
+        if ($this->execLine("AUTHINFO USER $user", false)) {
+            return $this->execline("AUTHINFO PASS $pass");
+        }
+        return false;
+    }
+
+    /** retrieves an article
+     * MSGID is a numeric ID a shown in article's headers. MSGNUM is a
+     * server-dependent ID (see X-Ref on many servers) and retriving 
+     * an article by this way will change the current article pointer.
+     * If an error occur, false is returned. 
+     * @param $_msgid STRING MSGID or MSGNUM of article
+     * @return ARRAY lines of the article
+     * @see body
+     * @see head
+     */
+    protected function article($msgid = "")
+    {
+        if (!$this->execLine("ARTICLE $msgid")) {
+            return false;
+        }
+        return $this->fetchResult();
+    }
+
+    /** post a message
+     * if an error occur, false is returned
+     * @param $_message STRING message to post
+     * @return STRING MSGID of article 
+     */
+    protected function post($message)
+    {
+        if (!$this->execLine("POST ", false)) {
+            return false;
+        }
+        if (!$this->putMessage($message)) {
+            return false;
+        }
+        if (preg_match("/(<[^@>]+@[^@>]+>)/", $this->lastresulttext, $regs)) {
+            return $regs[0];
+        } else {
+            return true;
+        }
+    }
+
+    /** fetches the body of an article
+     * params are the same as article
+     * @param $_msgid STRING MSGID or MSGNUM of article
+     * @return ARRAY lines of the article
+     * @see article
+     * @see head
+     */
+    protected function body($msgid = '')
+    {
+        if ($this->execLine("BODY $msgid")) {
+            return false;
+        }
+        return $this->fetchResult();
+    }
+
+    /** fetches the headers of an article
+     * params are the same as article
+     * @param $_msgid STRING MSGID or MSGNUM of article
+     * @return ARRAY lines of the article
+     * @see article
+     * @see body
+     */
+    protected function head($msgid = '')
+    {
+        if (!$this->execLine("HEAD $msgid")) {
+            return false;
+        }
+        return $this->fetchResult();
+    }
+
+    /** set current group
+     * @param $_group STRING 
+     * @return ARRAY array : nb of articles in group, MSGNUM of first article, MSGNUM of last article, and group name
+     */
+    protected function group($group)
+    {
+        if (!$this->execLine("GROUP $group")) {
+            return false;
+        }
+        $array = explode(' ', $this->lastresulttext);
+        if (count($array) >= 4) {
+            return array_slice($array, 0, 4);
+        }
+        return false;
+    }
+
+    /** set the article pointer to the previous article in current group
+     * @return STRING MSGID of article
+     * @see next
+     */
+    protected function last()
+    {
+        if (!$this->execLine("LAST ")) {
+            return false;
+        }
+        if (preg_match("/^\d+ (<[^>]+>)/", $this->lastresulttext, $regs)) {
+            return $regs[1];
+        }
+        return false;
+    }
+
+    /** set the article pointer to the next article in current group
+     * @return STRING MSGID of article
+     * @see last
+     */
+
+    protected function next()
+    {
+        if (!$this->execLine('NEXT ')) {
+            return false;
+        }
+        if (preg_match("/^\d+ (<[^>]+>)/", $this->lastresulttext, $regs)) {
+            return $regs[1];
+        }
+        return false;
+    }
+
+    /** set the current article pointer
+     * @param $_msgid STRING MSGID or MSGNUM of article
+     * @return BOOLEAN true if authentication was successful, error code otherwise
+     * @see article
+     * @see body
+     */
+    protected function nntpstat($msgid)
+    {
+        if (!$this->execLine("STAT $msgid")) {
+            return false;
+        }
+        if (preg_match("/^\d+ (<[^>]+>)/", $this->lastresulttext, $regs)) {
+            return $regs[1];
+        }
+        return false;
+    }
+
+    /** filter group list
+     */
+    private function filterGroups()
+    {
+        $list = $this->fetchResult();
+
+        $groups = array();
+        foreach ($list as $result) {
+            list($group, $last, $first, $p) = explode(' ', $result, 4);
+            if (!is_null(Banana::$grp_pattern) || preg_match('@' .Banana::$grp_pattern . '@', $group)) {
+                $groups[$group] = array(intval($last), intval($first), $p);
+            }
+        }
+        return $groups;
+    }
+
+    /** gets information about all active newsgroups
+     * @return ARRAY group name => (MSGNUM of first article, MSGNUM of last article, NNTP flags)
+     * @see newgroups
+     */
+    protected function listGroups()
+    {
+        if (!$this->execLine('LIST')) {
+            return false;
+        }
+        return $this->filterGroups();
+    }
+
+    /** format date for news server
+     * @param since UNIX TIMESTAMP
+     */
+    protected function formatDate($since)
+    {
+        return gmdate("ymd His", $since) . ' GMT';
+    }
+
+    /** get information about recent newsgroups 
+     * same as list, but information are limited to newgroups created after $_since
+     * @param $_since INTEGER unix timestamp
+     * @param $_distributions STRING distributions 
+     * @return ARRAY same format as liste
+     * @see liste
+     */
+    protected function newgroups($since, $distributions = '')
+    {
+        if (!($since = $this->formatDate($since))) {
+            return false;
+        }
+        if (!$this->execLine("NEWGROUPS $since $distributions")) {
+            return false;
+        }    
+        return $this->filterGroups();
+    }
+
+    /** gets a list of new articles
+     * @param $_since INTEGER unix timestamp
+     * @parma $_groups STRING pattern of intersting groups 
+     * @return ARRAY MSGID of new articles
+     */
+    protected function newnews($groups = '*', $since = 0, $distributions = '')
+    {
+        if (!($since = $this->formatDate($since))) {
+            return false;
+        }
+        if (!$this->execLine("NEWNEWS $groups $since $distributions")) {
+            return false;
+        }
+        return $this->fetchResult();
+    }
+
+    /** Tell the remote server that I am not a user client, but probably another news server
+     * @return BOOLEAN true if sucessful
+     */
+    protected function slave()
+    {
+        return $this->execLine("SLAVE ");
+    }
+
+    /** implements IHAVE method
+     * @param $_msgid STRING MSGID of article
+     * @param $_message STRING article
+     * @return BOOLEAN 
+     */
+    protected function ihave($msgid, $message = false)
+    {
+        if (!$this->execLine("IHAVE $msgid ")) {
+            return false;
+        }
+        return $this->putMessage($message);
+    }
+
+    /** closes connection to server
+     */
+    protected function quit()
+    {
+        $this->execLine('QUIT');
+        fclose($this->ns);
+        $this->ns      = null;
+        $this->posting = false;
+    }
+
+# NNTP Extensions [RFC 2980]
+
+    /** Returns the date on the remote server
+     * @return INTEGER timestamp 
+     */
+
+    protected function date()
+    {
+        if (!$this->execLine('DATE ', false)) {
+            return false;
+        }
+        if (preg_match("/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})$/", $this->lastresulttext, $r)) {
+            return gmmktime($r[4], $r[5], $r[6], $r[2], $r[3], $r[1]);
+        }
+        return false;
+    }
+
+    /** returns group descriptions
+     * @param $_pattern STRING pattern of intersting groups
+     * @return ARRAY group name => description
+     */
+
+    protected function xgtitle($pattern = '*')
+    {
+        if (!$this->execLine("XGTITLE $pattern ")) {
+            return false;
+        }
+        $array  = $this->fetchResult();
+        $groups = array();
+        foreach ($array as $result) {
+            list($group, $desc) = split("[ \t]", $result, 2);
+            $groups[$group] = $desc;
+        }
+        return $groups;
+    }
+
+    /** obtain the header field $hdr for all the messages specified
+     * @param $_hdr STRING name of the header (eg: 'From')
+     * @param $_range STRING range of articles 
+     * @return ARRAY MSGNUM => header value
+     */
+    protected function xhdr($hdr, $first = null, $last = null)
+    {
+        if (is_null($first) && is_null($last)) {
+            $range = "";
+        } else {
+            $range = $first . '-' . $last;
+        }
+        if (!$this->execLine("XHDR $hdr $range ")) {
+            return false;
+        }
+        $array   = $this->fetchResult();
+        $headers = array();
+        foreach ($array as &$result) {
+            @list($head, $value) = explode(' ', $result, 2);
+            $headers[$head] = $value;
+        }
+        return $headers;
+    }
+
+    /** obtain the header field $_hdr matching $_pat for all the messages specified
+     * @param $_hdr STRING name of the header (eg: 'From')
+     * @param $_range STRING range of articles 
+     * @param $_pat STRING pattern
+     * @return ARRAY MSGNUM => header value
+     */
+    protected function xpat($_hdr, $_range, $_pat)
+    {
+        if (!$this->execLine("XPAT $hdr $range $pat")) {
+            return false;
+        }
+        $array   = $this->fetchResult();
+        $headers = array();
+        foreach ($array as &$result) {
+            list($head, $value) = explode(' ', $result, 2);
+            $headers[$head] = $result;
+        }
+        return $headers;
+    }
+}
+
+// vim:set et sw=4 sts=4 ts=4 
+?>
diff --git a/banana/page.inc.php b/banana/page.inc.php
new file mode 100644 (file)
index 0000000..b897146
--- /dev/null
@@ -0,0 +1,236 @@
+<?php
+/********************************************************************************
+* banana/page.inc.php : class for group lists
+* ------------------------
+*
+* This file is part of the banana distribution
+* Copyright: See COPYING files that comes with this distribution
+********************************************************************************/
+
+require_once 'smarty/libs/Smarty.class.php';
+
+class BananaPage extends Smarty
+{
+    private $error = array();
+    private $page  = null;
+
+    private $pages   = array();
+    private $actions = array();
+
+    public function __construct()
+    {
+        $this->Smarty();
+
+        $this->compile_check = Banana::$debug_smarty;
+        $this->template_dir  = dirname(__FILE__) . '/templates/';
+        $this->compile_dir   = dirname(dirname(__FILE__)) . '/spool/templates_c/';
+        $this->register_prefilter('banana_trimwhitespace');
+    
+    }
+
+    public function trig($message)
+    {
+        $this->error[] = $message;
+    }
+
+    public function kill($message)
+    {
+        $this->trig($message);
+        return $this->run();
+    }
+
+    public function setPage($page)
+    {
+        $this->page = $page;
+    }
+
+    public function registerAction($action_code, array $pages = null)
+    {
+        $this->actions[] = array('text' => $action_code, 'pages' => $pages);
+        return true;
+    }
+
+    public function registerPage($name, $text, $template = null)
+    {
+        $this->pages[$name] = array('text' => $text, 'template' => $template);
+        return true;
+    }
+
+    public function run()
+    {
+        $this->registerPage('subscribe', _b_('Abonnements'), null);
+        $this->registerPage('forums', _b_('Les forums'), null);
+        if (!is_null(Banana::$group)) {
+            $this->registerPage('thread', Banana::$group, null);
+            if (!is_null(Banana::$artid)) {
+                $this->registerPage('message', _b_('Message'), null);
+                if ($this->page == 'cancel') {
+                    $this->registerPage('cancel', _b_('Annulation'), null);
+                } elseif ($this->page == 'new') {
+                    $this->registerPage('new', _b_('Répondre'), null);
+                }
+            } elseif ($this->page == 'new') {
+                $this->registerPage('new', _b_('Nouveau'), null);
+            }
+        }
+        foreach ($this->actions as $key=>&$action) {
+            if (!is_null($action['pages']) && !in_array($this->page, $action['pages'])) {
+                unset($this->actions[$key]);
+            }
+        }
+        $this->assign('group',     Banana::$group);
+        $this->assign('artid',     Banana::$artid);
+        $this->assign('part',      Banana::$part);
+        $this->assign('first',     Banana::$first);
+        $this->assign('action',    Banana::$action);
+        $this->assign('profile',   Banana::$profile);
+        $this->assign('spool',     Banana::$spool);
+        $this->assign('protocole', Banana::$protocole);
+
+        $this->assign('errors',    $this->error);
+        $this->assign('page',      $this->page);
+        $this->assign('pages',     $this->pages);
+        $this->assign('actions',   $this->actions);
+
+        $this->register_function('url',     array($this, 'makeUrl'));
+        $this->register_function('link',    array($this, 'makeLink'));
+        $this->register_function('imglink', array($this, 'makeImgLink'));
+        $this->register_function('img',     array($this, 'makeImg'));
+        if (!Banana::$debug_smarty) {
+            $error_level = error_reporting(0);
+        }
+        $text = $this->fetch('banana-base.tpl');
+        $text = banana_utf8entities($text);
+        if (!Banana::$debug_smarty) {
+            error_reporting($error_level);
+        }
+        return $text;
+    }
+
+    public function makeUrl($params, &$smarty = null)
+    {
+        if (function_exists('hook_makeLink')
+                && $res = hook_makeLink($params)) {
+            return $res;
+        }   
+        $proto = empty($_SERVER['HTTPS']) ? 'http://' : 'https://';
+        $host  = $_SERVER['HTTP_HOST'];
+        $file  = $_SERVER['PHP_SELF'];
+    
+        if (count($params) != 0) {
+            $get = '?';
+            foreach ($params as $key=>$value) {
+                if (strlen($get) != 1) {
+                    $get .= '&';
+                }
+                $get .= $key . '=' . $value;
+            }
+        } else {
+            $get = '';
+        }     
+        return $proto . $host . $file . $get;
+    }
+
+    public function makeLink($params, &$smarty = null)
+    {
+        $catch = array('text', 'popup', 'class', 'accesskey');
+        foreach ($catch as $key) {
+            ${$key} = isset($params[$key]) ? $params[$key] : null;
+            unset($params[$key]);
+        }
+        $link = $this->makeUrl($params, &$smarty);
+        if (is_null($text)) {
+            $text = $link;
+        }
+        if (!is_null($accesskey)) {
+            $popup .= ' (raccourci : ' . $accesskey . ')';
+        }
+        if (!is_null($popup)) {
+            $popup = ' title="' . $popup . '"';
+        }
+        if (!is_null($class)) {
+            $class = ' class="' . $class . '"';
+        }
+        $target = null;
+        if (isset($params['action']) && $params['action'] == 'view') {
+            $target = ' target="_blank"';
+        }
+        if (!is_null($accesskey)) {
+            $accesskey = ' accesskey="' . $accesskey . '"';
+        }
+        return '<a href="' . htmlentities($link) . '"'
+              . $target . $popup . $class . $accesskey
+              . '>' . $text . '</a>';
+    }
+
+    public function makeImg($params, &$smarty = null)
+    {
+        $catch = array('img', 'alt', 'height', 'width');
+        foreach ($catch as $key) {
+            ${$key} = isset($params[$key]) ? $params[$key] : null;
+        }
+        $img .= ".gif";
+        if (function_exists('hook_makeImg')
+                && $res = hook_makeImg($img, $alt, $height, $width)) {
+            return $res;
+        }
+
+        if (!is_null($width)) {
+            $width = ' width="' . $width . '"';
+        }
+        if (!is_null($height)) {
+            $height = ' height="' . $height . '"';
+        }
+
+        $proto = empty($_SERVER['HTTPS']) ? 'http://' : 'https://';
+        $host  = $_SERVER['HTTP_HOST'];
+        $file  = dirname($_SERVER['PHP_SELF']) . '/img/' . $img;
+        $url   = $proto . $host . $file;
+
+        return '<img src="' . $url . '"' . $height . $width . ' alt="' . _b_($alt) . '" />';
+    }
+
+    public function makeImgLink($params, &$smarty = null)
+    {
+        $params['alt'] = _b_($params['alt']);
+        $params['popup'] = $params['alt'];
+        $params['text'] = $this->makeImg($params, $smarty);
+        return $this->makeLink($params, $smarty);
+    }
+
+    /** Redirect to the page with the given parameter
+     * @ref makeLink
+     */
+    public function redirect($params = array())
+    {
+        header('Location: ' . $this->makeUrl($params));
+    }
+}
+
+// {{{  function banana_trimwhitespace
+
+function banana_trimwhitespace($source, &$smarty)
+{
+    $tags = array('script', 'pre', 'textarea');
+
+    foreach ($tags as $tag) {
+        preg_match_all("!<{$tag}[^>]+>.*?</{$tag}>!is", $source, ${$tag});
+        $source = preg_replace("!<{$tag}[^>]+>.*?</{$tag}>!is", "&&&{$tag}&&&", $source);
+    }
+
+    // remove all leading spaces, tabs and carriage returns NOT
+    // preceeded by a php close tag.
+    $source = preg_replace('/((?<!\?>)\n)[\s]+/m', '\1', $source);
+
+    foreach ($tags as $tag) {
+        $source = preg_replace("!&&&{$tag}&&&!e",  'array_shift(${$tag}[0])', $source);
+    }
+
+    return $source;
+}
+
+// }}}
+
+
+// vim:set et sw=4 sts=4 ts=4:
+?>
diff --git a/banana/post.inc.php b/banana/post.inc.php
deleted file mode 100644 (file)
index c3e3402..0000000
+++ /dev/null
@@ -1,536 +0,0 @@
-<?php
-/********************************************************************************
-* include/posts.inc.php : class for posts
-* -----------------------
-*
-* This file is part of the banana distribution
-* Copyright: See COPYING files that comes with this distribution
-********************************************************************************/
-
-/** class for posts
- */
-
-class BananaPost
-{
-    var $id;
-    /** headers */
-    var $headers;
-    /** body */
-    var $body;
-    /** formating */
-    var $messages;
-    /** attachment */
-    var $pj;
-    /** poster name */
-    var $name;
-    /** test validity */
-    var $valid = true;
-
-    /** constructor
-     * @param $_id STRING MSGNUM or MSGID (a group should be selected in this case)  
-     */
-    function BananaPost($_id)
-    {
-        global $banana;
-        $this->id       = $_id;
-        $this->pj       = array();
-        $this->messages = array();
-        if (!$this->_header()) {
-            $this->valid = false; 
-            return null;
-        }
-
-
-        if ($body = $banana->nntp->body($_id)) {
-            $this->body = join("\n", $body);
-        } else {
-            $this->valid = false;
-            return null;
-        }
-        
-        if (isset($this->headers['content-transfer-encoding'])) {
-            if (preg_match("/base64/", $this->headers['content-transfer-encoding'])) {
-                $this->body = base64_decode($this->body);
-            } elseif (preg_match("/quoted-printable/", $this->headers['content-transfer-encoding'])) {
-                $this->body = quoted_printable_decode($this->body);
-            }
-        }
-
-        if ($this->_split_multipart($this->headers, $this->body)) {
-            $this->set_body_to_part(0);
-        } else {
-            if(isset($mpart_type)) {
-                $this->_split_multipart($mpart_type[1], $mpart_boundary[1]);
-            }
-            $this->_find_uuencode();
-            $this->_fix_charset();
-        }
-    }
-
-    /** find and add uuencoded attachments
-     */
-    function _find_uuencode()
-    {
-        if (preg_match_all('@\n(begin \d+ ([^\r\n]+)\r?(?:\n(?!end)[^\n]*)*\nend)@', $this->body, $matches, PREG_SET_ORDER)) {
-            foreach ($matches as $match) {
-                $mime = trim(exec('echo '.escapeshellarg($match[1]).' | uudecode -o /dev/stdout | file -bi -'));
-                if ($mime != 'application/x-empty') {
-                    $this->body = trim(str_replace($match[0], '', $this->body));
-                    $body = $match[1];
-                    $header['content-type'] = $mime.'; name="'.$match[2].'"';
-                    $header['content-transfer-encoding'] = 'x-uuencode';
-                    $header['content-disposition'] = 'attachment; filename="'.$match[2].'"';
-                    $this->_add_attachment(Array('headers' => $header, 'body' => $body));
-                }
-            }
-        }
-    }
-
-    /** split multipart messages
-     * @param $type STRING multipart type description
-     * @param $boundary STRING multipart boundary identification string
-     */
-    function _split_multipart($headers, $body)
-    {
-        if (!isset($headers['content-type'])
-                || !preg_match("@multipart/([^;]+);@", $headers['content-type'], $type)) {
-            return false;
-        }
-            
-        preg_match("/boundary=\"?([^ \"]+)\"?/", $headers['content-type'], $boundary);
-        $boundary = $boundary[1];
-        $type     = $type[1];
-        $parts    = preg_split("@\n--$boundary(--|\n)@", $body);
-        foreach ($parts as $part) {
-            $part         = $this->_get_part($part);
-            $local_header = $part['headers'];
-            $local_body   = $part['body'];
-            if (!$this->_split_multipart($local_header, $local_body)) {
-                $is_text = isset($local_header['content-type'])
-                         && preg_match("@text/([^;]+);@", $local_header['content-type'])
-                         && (!isset($local_header['content-disposition']) 
-                             || !preg_match('@attachment@', $local_header['content-disposition'])); 
-
-                // alternative ==> multiple formats for messages
-                if ($type == 'alternative' && $is_text) {
-                    array_push($this->messages, $part);
-
-                // !alternative ==> une body, others are attachments
-                } else if ($is_text) {
-                    if (count($this->messages) == 0) {
-                        $this->body = $local_body;
-                        foreach (array_keys($local_header) as $key) {
-                            $this->header[$key] = $local_header[$key];
-                        }
-                        array_push($this->messages, $part);
-                    } else {
-                        $this->_add_attachment($part);
-                    }
-                } else {
-                    $this->_add_attachment($part);
-                }
-            }
-        }
-        return true;
-    }
-
-    /** extract new headers from the part
-     * @param $part STRING part of a multipart message
-     */
-    function _get_part($part)
-    {
-        global $banana;
-
-        $local_headers = Array();
-        $lines = split("\n", $part);
-        while (count($lines)) {
-            $line = array_shift($lines);
-            if ($line != "") {
-                if (preg_match('@^[\t\r ]+@', $line) && isset($hdr)) {
-                    $local_headers[$hdr] .= ' '.trim($line);
-                } else {
-                    list($hdr, $val) = split(":[ \t\r]*", $line, 2);
-                    $hdr = strtolower($hdr);
-                    if (in_array($hdr, $banana->parse_hdr)) {
-                        $local_headers[$hdr] = $val;
-                    }
-                }
-            } else {
-                break;
-            }
-        }
-        $local_body = join("\n", $lines);
-        if (isset($local_headers['content-transfer-encoding'])
-                && preg_match("/quoted-printable/", $local_headers['content-transfer-encoding'])) {
-            $local_body = quoted_printable_decode($local_body);
-        }
-        return Array('headers' => $local_headers, 'body' => $local_body); 
-    }
-
-    /** add an attachment
-     */
-    function _add_attachment($part)
-    {
-        $local_header = $part['headers'];
-        $local_body = $part['body'];
-
-        if ((isset($local_header['content-disposition']) && preg_match('/filename="?([^"]+)"?/', $local_header['content-disposition'], $filename))
-            || (isset($local_header['content-type']) && preg_match('/name="?([^"]+)"?/', $local_header['content-type'], $filename))) {
-            $filename = $filename[1];
-        }           
-        if (!isset($filename)) {
-            $filename = "attachment" . count($this->pj);
-        }
-
-        if (isset($local_header['content-type'])
-                && preg_match('/^\s*([^ ;]+);/', $local_header['content-type'], $mimetype)) {
-            $mimetype = $mimetype[1];
-        } else {
-            return false;
-        }
-
-        if (isset($local_header['content-id'])
-                && preg_match('/^\s*<([^> ]+)>/', $local_header['content-id'], $cid)) {
-            $cid = $cid[1];
-        } else {
-            $cid = null;
-        }
-
-        array_push($this->pj, Array('MIME' => $mimetype,
-                                    'filename' => $filename,
-                                    'encoding' => strtolower($local_header['content-transfer-encoding']),
-                                    'data' => $local_body,
-                                    'cid' => $cid));
-        return true;
-    }
-
-    /** Fix body charset (convert body to utf8)
-     * @return false if failed
-     */
-    function _fix_charset()
-    {
-        if (isset($this->headers['content-type'])
-                && preg_match('!charset="?([^;"]*)"?\s*(;|$)?!', $this->headers['content-type'], $matches)) {
-            $body = iconv($matches[1], 'utf-8//IGNORE', $this->body);
-            if (strlen($body) == 0) {
-                return false;
-            }
-            $this->body = $body;
-        } else {
-            $this->body = utf8_encode($this->body);
-        }
-        return true;
-    }
-
-    /** return body in plain text (useful for messages without a text/plain part)
-     */
-    function get_body()
-    {
-        preg_match("@text/([^;]+);@", $this->headers['content-type'], $format);
-        if ($format[1] == 'plain') {
-            return $this->body;
-        }
-        if ($format[1] == 'richtext') {
-            return htmlToPlainText(richtextToHtml($this->body));
-        } else {
-            return htmlToPlainText($this->body);
-        }
-    }
-
-    /** return local url for the given cid
-     * @param cid STRING
-     */
-    function find_attachment($cid)
-    {
-        global $banana;
-        $i = 0;
-        foreach ($this->pj as $pj) {
-            if ($pj['cid'] == $cid) {
-                return htmlentities(makeLink(Array('group'  => $banana->state['group'],
-                                                   'artid'  => $this->id,
-                                                   'pj'     => $i,
-                                                   'action' => 'view')));
-            }
-            $i++;
-        }
-        return 'cid:' . $cid;;
-    }
-
-    /** decode an attachment
-     * @param pjid INT id of the attachment to decode
-     * @param action BOOL action to execute : true=view, false=download
-     */
-    function get_attachment($pjid, $action = false)
-    {
-        if ($pjid >= count($this->pj)) {
-            return false;
-        } else {
-            $file = $this->pj[$pjid];
-            header('Content-Type: '.$file['MIME'].'; name="'.$file['filename'].'"');
-            if (!$action) {
-                header('Content-Disposition: attachment; filename="'.$file['filename'].'"');
-            } else {
-                header('Content-Disposition: inline; filename="'.$file['filename'].'"');
-            }                
-            if ($file['encoding'] == 'base64') {
-                echo base64_decode($file['data']);
-            } else if ($file['encoding'] == 'x-uuencode') {                
-                passthru('echo '.escapeshellarg($file['data']).' | uudecode -o /dev/stdout');
-            } else {
-                header('Content-Transfer-Encoding: '.$file['encoding']);
-                echo $file['data'];
-            }
-            return true;
-        }
-    }
-
-    /** set body to represent the given part
-     * @param partid INT index of the part in messages
-     */
-    function set_body_to_part($partid)
-    {
-        global $banana;
-        
-        if (count($this->messages) == 0) {
-            return false;
-        }
-
-        $local_header = $this->messages[$partid]['headers'];
-        $this->body   = $this->messages[$partid]['body'];
-        foreach ($banana->parse_hdr as $hdr) {
-            if (isset($local_header[$hdr])) {
-                $this->headers[$hdr] = $local_header[$hdr];
-            }
-        }
-
-        $this->_fix_charset();
-        return true;
-    }
-
-    function _header()
-    {
-        global $banana;
-        $hdrs = $banana->nntp->head($this->id);
-        if (!$hdrs) {
-            return false;
-        }
-
-        // parse headers
-        foreach ($hdrs as $line) {
-            if (preg_match("/^[\t\r ]+/", $line)) {
-                $line = ($hdr=="X-Face"?"":" ").ltrim($line);
-                if (in_array($hdr, $banana->parse_hdr))  {
-                    $this->headers[$hdr] .= $line;
-                }
-            } else {
-                list($hdr, $val) = split(":[ \t\r]*", $line, 2);
-                $hdr = strtolower($hdr);
-                if (in_array($hdr, $banana->parse_hdr)) {
-                    $this->headers[$hdr] = $val;
-                }
-            }
-        }
-        // decode headers
-        foreach ($banana->hdecode as $hdr) {
-            if (isset($this->headers[$hdr])) {
-                $this->headers[$hdr] = headerDecode($this->headers[$hdr]);
-            }
-        }
-
-        $this->name = $this->headers['from'];
-        $this->name = preg_replace('/<[^ ]*>/', '', $this->name);
-        $this->name = trim($this->name);
-        return true;
-    }
-
-    function checkcancel()
-    {
-        if (function_exists('hook_checkcancel')) {
-            return hook_checkcancel($this->headers);
-        }
-        if (!isset($_SESSION)) {
-            return false;
-        }
-        return ($this->headers['from'] == $_SESSION['name'] . ' <' . $_SESSION['mail']. '>');
-    }
-
-    /** Make some links to browse the current newsgroup
-     */
-    function _browser()
-    {
-        global $banana;
-        $ret = '<div class="banana_menu">';
-        $actions = Array('nextUnread' => Array('next_unread', _b_('Message non-lu suivant'), 'u'),
-                         'prevPost'   => Array('prev',        _b_('Article précédent'), 'a'),
-                         'nextPost'   => Array('next',        _b_('Article suivant'), 'z'),
-                         'prevThread' => Array('prev_thread', _b_('Discussion précédente'), 'q'),
-                         'nextThread' => Array('next_thread', _b_('Discussion suivante'), 's'));
-        foreach ($actions as $method=>$params) {
-            $id = $banana->spool->$method($this->id);
-            if (!is_null($id)) {
-                $ret .= makeImgLink(Array('group' => $banana->state['group'],
-                                          'artid' => $id),
-                                    $params[0] . '.gif',
-                                    $params[1],
-                                    null, null, null,
-                                    $params[2]);
-            }
-        }
-        return $ret . '</div>';
-    }
-
-    /** convert message to html
-     * @param partid INT id of the multipart message that must be displaid
-     */
-    function to_html($partid = -1)
-    {
-        global $banana;
-
-        if (count($this->messages) > 1) {
-            if ($partid != -1) {
-                $this->set_body_to_part($partid);
-            } else {
-                // Select prefered text-format
-                foreach ($banana->body_mime as $mime) {
-                    for ($id = 0 ; $id < count($this->messages) ; $id++) {
-                        if (preg_match("@$mime@", $this->messages[$id]['headers']['content-type'])) {
-                            $partid = $id;
-                            $this->set_body_to_part($partid);
-                            break;
-                        }
-                    }
-                    if ($partid != -1) {
-                        break;
-                    }
-                }
-                if ($partid == -1) {
-                    $partid = 0;
-                }
-            }
-        } else {
-            $partid = 0;
-        }
-
-        $res  = '<table class="bicol banana_msg" cellpadding="0" cellspacing="0">';
-        $res .= '<tr><th colspan="2" class="subject">'
-             . $this->_browser()
-             . '<div class="banana_action">'
-             . makeImgLink(Array('group'  => $banana->state['group'],
-                                 'action' => 'new'),
-                           'post.gif',
-                           _b_('Nouveau message'), null, null, null, 'p') . '&nbsp;'
-             . makeImgLink(Array('group'  => $banana->state['group'],
-                                 'artid'  => $this->id,
-                                 'action' => 'new'),
-                           'reply.gif',
-                           _b_('Répondre'), null, null, null, 'r');
-        if ($this->checkCancel()) {
-            $res .= '&nbsp;'
-                  . makeImgLink(Array('group'  => $banana->state['group'],
-                                      'artid'  => $this->id,
-                                      'action' => 'cancel'),
-                                'cancel.gif',
-                                _b_('Annuler'), null, null, null, 'c');
-        }
-        $res .= '</div>'
-             . formatDisplayHeader('subject', $this->headers['subject'])
-             . '</th></tr>'
-             . '<tr class="pair"><td class="headers"><table cellpadding="0" cellspacing="0">';
-
-        $xface = null;
-        foreach ($banana->show_hdr as $hdr) {
-            if (isset($this->headers[$hdr])) {
-                $res2 = formatdisplayheader($hdr, $this->headers[$hdr]);
-                if ($res2 && ($hdr != 'x-face' || !$banana->formatxface)) {
-                    $res .= '<tr><td class="hdr">'.header_translate($hdr)."</td><td class='val'>$res2</td></tr>\n";
-                } else if ($res2) {
-                    $xface = $res2;
-                }
-            }
-        }
-        $res .= '</table></td><td class="xface">';
-
-        if ($xface) {
-            $res .= $xface;
-        }
-        $res .= '</td></tr>';
-
-        if (count($this->messages) > 1) {
-            $res .= '<tr><th colspan="2">';        
-            for ($i = 0 ; $i < count($this->messages) ; $i++) {
-                if ($i != 0) {
-                    $res .= ' . ';
-                }
-                preg_match("@text/([^;]+);@", $this->messages[$i]['headers']['content-type'], $format);
-                $format = textFormat_translate($format[1]);
-                if ($i != $partid) {
-                    $res .= makeHREF(Array('group' => $banana->state['group'],
-                                           'artid' => $this->id,
-                                           'part'  => $i),
-                                     $format);
-                } else {
-                    $res .= $format;
-                }
-            }
-            $res .= '</th></tr>';
-        }
-        if (isset($this->headers['content-type'])
-                && preg_match("@text/([^;]+);@", $this->headers['content-type'], $format)) {
-            $format = $format[1];
-        } else {
-            $format = 'plain';
-        }
-        $res .= '<tr class="impair"><td colspan="2" class="body"';
-        if ($format == 'html') {
-            if (preg_match('@<body[^>]*bgcolor="?([#0-9a-f]+)"?[^>]*>@i', $this->body, $bgcolor)) {
-                $res .= ' bgcolor="'.$bgcolor[1].'"';
-            }
-            $this->body = preg_replace('/cid:([^\'" ]+)/e', "find_attachment('\\1')", $this->body);
-            $res .= '>'.formatbody($this->body, $format); 
-        } else {
-            $res .= '><pre>'.formatbody($this->body).'</pre>';
-        }
-        $res .= '</td></tr>';
-
-        if (count($this->pj) > 0) {
-            $res .= '<tr><th colspan="2">'._b_('Pièces jointes').'</th></tr>';
-            $res .= '<tr><td colspan="2">';
-            $i = 0;
-            foreach ($this->pj as $file) {
-                $res .= makeImgLink(Array('group' => $banana->state['group'],
-                                          'artid' => $this->id,
-                                          'pj'    => $i),
-                                    'save.gif',
-                                    _b_('Télécharger')) . ' ';
-                $res .= makeImgLink(Array('group' => $banana->state['group'],
-                                          'artid' => $this->id,
-                                          'pj'    => $i,
-                                          'action'=> 'view'),
-                                    'preview.gif',
-                                    _b_('Aperçu'));
-                $res .= ' ' . $file['filename'].' ('.$file['MIME'].')';
-                $res .=  '<br/>';
-                $i++;
-            }
-            $res .= '</td></tr>';
-        }
-        
-        $ndx  = $banana->spool->getndx($this->id);
-        $res .= '<tr><td class="thrd" colspan="2">'
-             . $banana->spool->to_html($ndx-$banana->tbefore, $ndx+$banana->tafter, $ndx)
-             . '</td></tr>';
-        return $res.'</table>';
-    }
-}
-
-/** Wrapper for Post::find_attachment
- */
-function find_attachment($cid)
-{
-    global $banana;
-    return $banana->post->find_attachment($cid);
-}
-
-// vim:set et sw=4 sts=4 ts=4
-?>
diff --git a/banana/protocoleinterface.inc.php b/banana/protocoleinterface.inc.php
new file mode 100644 (file)
index 0000000..5962e5a
--- /dev/null
@@ -0,0 +1,102 @@
+<?php
+/********************************************************************************
+* banana/protocoleinterface.inc.php : interface for box access
+* ------------------------
+*
+* This file is part of the banana distribution
+* Copyright: See COPYING files that comes with this distribution
+********************************************************************************/
+
+require_once dirname(__FILE__) . '/banana.inc.php';
+
+interface BananaProtocoleInterface
+{
+    /** Build a protocole handler plugged on the given box
+     */
+    public function __construct($box = null);
+
+    /** Indicate if the Protocole handler has been succesfully built
+     */
+    public function isValid();
+    
+    /** Indicate last error n°
+     */
+    public function lastErrNo();
+    
+    /** Indicate last error text
+     */
+    public function lastError();
+
+    /** Return the description of the current box
+     */
+    public function getDescription();
+
+    /** Return the list of the boxes
+     * @param mode Kind of boxes to list
+     * @param since date of last check (for new boxes and new messages)
+     * @param withstats Indicated whether msgnum and unread must be set in the result
+     * @return Array(boxname => array(desc => boxdescripton, msgnum => number of message, unread =>number of unread messages)
+     */
+    public function getBoxList($mode = Banana::BOXES_ALL, $since = 0, $withstats = false);
+
+    /** Return a message
+     * @param id Id of the emssage (can be either an Message-id or a message index)
+     * @param msg_headers Headers to process
+     * @param is_msgid If is set, $id is en Message-Id
+     * @return A BananaMessage or null if the given id can't be retreived
+     */
+    public function getMessage($id, array $msg_headers = array(), $is_msgid = false);
+
+    /** Return the indexes of the messages presents in the Box
+     * @return Array(number of messages, MSGNUM of the first message, MSGNUM of the last message)
+     */
+    public function getIndexes();
+
+    /** Return the message headers (in BananaMessage) for messages from firstid to lastid
+     * @return Array(id => array(headername => headervalue))
+     */
+    public function &getMessageHeaders($firstid, $lastid, array $msg_headers = array());
+
+    /** Update the spool to add protocole specifics data
+     * @param Array(id => message headers)
+     */
+    public function updateSpool(array &$messages);
+
+    /** Return the indexes of the new messages since the give date
+     * @return Array(MSGNUM of new messages)
+     */
+    public function getNewIndexes($since);
+
+    /** Return wether or not the protocole can be used to add new messages
+     */
+    public function canSend();
+
+    /** Return wether or not the protocole allow message deletion
+     */
+    public function canCancel();
+
+    /** Return the list of requested headers
+     * @return Array('header1', 'header2', ...) with the key 'dest' for the destination header
+     * and 'reply' for the reply header, eg:
+     * * for a mail: Array('From', 'Subject', 'dest' => 'To', 'Cc', 'Bcc', 'reply' => 'Reply-To')
+     * * for a post: Array('From', 'Subject', 'dest' => 'Newsgroups', 'reply' => 'Followup-To')
+     */
+    public function requestedHeaders();
+
+    /** Send a message
+     * @return true if it was successfull
+     */
+    public function send(BananaMessage &$message);
+
+    /** Cancel a message
+     * @return true if it was successfull
+     */
+    public function cancel(BananaMessage &$message);
+
+    /** Return the protocole name
+     */
+    public function name();
+}
+
+// vim:set et sw=4 sts=4 ts=4:
+?>
index de4e598..610d192 100644 (file)
@@ -7,20 +7,9 @@
 * Copyright: See COPYING files that comes with this distribution
 ********************************************************************************/
 
-if(!function_exists('file_put_contents')) {
-    function file_put_contents($filename, $data)
-    {
-        $fp = fopen($filename, 'w');
-        if(!$fp) {
-            trigger_error('file_put_contents cannot write in file '.$filename, E_USER_ERROR);
-            return;
-        }
-        fputs($fp, $data);
-        fclose($fp);
-    }
-}
+require_once dirname(__FILE__) . '/banana.inc.php';
 
-function spoolCompare($a,$b) { return ($b->date>=$a->date); }
+define('BANANA_SPOOL_VERSION', '0.3');
 
 /** Class spoolhead
  *  class used in thread overviews
@@ -28,23 +17,26 @@ function spoolCompare($a,$b) { return ($b->date>=$a->date); }
 class BananaSpoolHead
 {
     /** date (timestamp) */
-    var $date;
+    public $date;
     /** subject */
-    var $subject;
+    public $subject;
     /** author */
-    var $from;
+    public $from;
     /** reference of parent */
-    var $parent;
+    public $parent = null;
     /** paren is direct */
-    var $parent_direct;
+    public $parent_direct;
     /** array of children */
-    var $children = Array();
+    public $children = Array();
     /** true if post is read */
-    var $isread;
+    public $isread;
     /** number of posts deeper in this branch of tree */
-    var $desc;
+    public $desc;
     /**  same as desc, but counts only unread posts */
-    var $descunread;
+    public $descunread;
+
+    /** storage data */
+    public $storage = array();
 
     /** constructor
      * @param $_date INTEGER timestamp of post
@@ -54,102 +46,99 @@ class BananaSpoolHead
      * @param $_read BOOLEAN true if read
      * @param $_descunread INTEGER descunread value (0 for a new post)
      */
-
-    function BananaSpoolHead($_date, $_subject, $_from, $_desc=1, $_read=true, $_descunread=0)
+    public function __construct(array &$message)
     {
-        $this->date       = $_date;
-        $this->subject    = $_subject;
-        $this->from       = $_from;
-        $this->desc       = $_desc;
-        $this->isread     = $_read;
-        $this->descunread = $_descunread;
+        $this->date       = $message['date'];
+        $this->subject    = stripslashes($message['subject']);
+        $this->from       = $message['from'];
+        $this->desc       = 1;
+        $this->isread     = true;
+        $this->descunread = 0;
     }
 }
 
-/** Class spool
- * builds and updates spool 
- */
-
-define("BANANA_SPOOL_VERSION", '0.2');
 
 class BananaSpool
 {
-    var $version;
-    /**  spool */
-    var $overview;
+    private $version;
+
     /**  group name */
-    var $group;
+    public $group;
+    /**  spool */
+    public $overview;
     /**  array msgid => msgnum */
-    var $ids;
+    public $ids;
     /** thread starts */
-    var $roots;
-    /** test validity */
-    var $valid = true;
+    public $roots;
+
 
     /** constructor
      * @param $_group STRING group name
      * @param $_display INTEGER 1 => all posts, 2 => only threads with new posts
      * @param $_since INTEGER time stamp (used for read/unread)
      */
-    function BananaSpool($_group, $_display=0, $_since="")
+    protected function __construct($group)
     {
-        global $banana;
-        $this->group = $_group;
-        $groupinfo   = $banana->nntp->group($_group);
-        if (!$groupinfo) {
-            $this->valid = false;
-            return null; 
-        }
-
-        $this->_readFromFile();
-
-        $do_save = false;
-        $first   = $banana->maxspool ? max($groupinfo[2] - $banana->maxspool, $groupinfo[1]) : $groupinfo[1];
-        $last    = $groupinfo[2]; 
+        $this->version    = BANANA_SPOOL_VERSION;
+        $this->group      = $group;
+    }
 
-        if ($this->version == BANANA_SPOOL_VERSION && is_array($this->overview)) {
-            $mids = array_keys($this->overview);
-            foreach ($mids as $id) {
-                if (($first <= $last && ($id < $first || $id > $last))
-                        || ($first > $last && $id < $first && $id > $last))
-                {
-                    $this->delid($id, false);
-                    $do_save = true;
-                }
-            }
-            if (!empty($this->overview)) {
-                $first = max(array_keys($this->overview))+1;
-            }
+    public static function getSpool($group, $since = 0)
+    {
+        if (!is_null(Banana::$spool) && Banana::$spool->group == $group) {
+            $spool = Banana::$spool;
         } else {
-            unset($this->overview, $this->ids);
-            $this->version = BANANA_SPOOL_VERSION;
+            $spool = BananaSpool::readFromFile($group);
+        }        
+        if (is_null($spool)) {
+            $spool = new BananaSpool($group);
         }
+        Banana::$spool =& $spool;
+        $spool->build();
+        $spool->updateUnread($since);
+        return $spool;
+    }
 
-        if ($first<=$last && $groupinfo[0]) {
-            $do_save = true;
-            $this->_updateSpool("$first-$last");
+    private static function spoolFilename($group)
+    {
+        $file = dirname(dirname(__FILE__));
+        $file .= '/spool/' . Banana::$protocole->name() . '/';
+        if (!is_dir($file)) {
+            mkdir($file);
         }
-
-        if ($do_save) { $this->_saveToFile(); }
-
-        $this->_updateUnread($_since, $_display);
+        $url  = parse_url(Banana::$host);
+        if (isset($url['host'])) {
+            $file .= $url['host'] . '_';
+        }
+        if (isset($url['port'])) {
+            $file .= $url['port'] . '_';
+        }
+        $file .= $group;
+        return $file;
     }
 
-    function _readFromFile()
+    private static function readFromFile($group)
     {
-        $file = $this->_spoolfile();
-        if (file_exists($file)) {
-            $temp = unserialize(file_get_contents($file));
-            foreach (get_object_vars($temp) as $key=>$val) {
-                $this->$key = $val;
-            }
+        $file = BananaSpool::spoolFilename($group);
+        if (!file_exists($file)) {
+            return null;
+        }
+        $spool =  unserialize(file_get_contents($file));
+        if ($spool->version != BANANA_SPOOL_VERSION) {
+            return null;
         }
+        return $spool;
+    }
+
+    private function compare($a, $b)
+    {
+        return ($b->date >= $a->date);
     }
 
-    function _saveToFile()
+    private function saveToFile()
     {
-        $file = $this->_spoolfile();
-        uasort($this->overview, "spoolcompare");
+        $file = BananaSpool::spoolFilename($this->group);
+        uasort($this->overview, array($this, 'compare'));
 
         $this->roots = Array();
         foreach($this->overview as $id=>$msg) {
@@ -161,95 +150,144 @@ class BananaSpool
         file_put_contents($file, serialize($this));
     }
 
-    function _spoolfile()
+    private function build()
     {
-        global $banana;
-        $url = parse_url($banana->host);
-        $file = $url['host'].'_'.$url['port'].'_'.$this->group;
-        return dirname(dirname(__FILE__)).'/spool/'.$file;
+        $threshold = 0;
+
+        // Compute the range of indexes
+        list($msgnum, $first, $last) = Banana::$protocole->getIndexes();
+        if ($last < $first) {
+            $threshold = $firt + $msgnum - $last;
+            $threshold = (int)(log($threshold)/log(2));
+            $threshold = (2 ^ ($threshold + 1)) - 1;
+        }
+        if (Banana::$maxspool && Banana::$maxspool < $msgnum) {
+            $first = $last - Banana::$maxspool;
+            if ($first < 0) {
+                $first += $threshold;
+            }
+        }
+        $clean  = $this->clean($first, $last, $msgnum);
+        $update = $this->update($first, $last, $msgnum);
+        
+        if ($clean || $update) {
+            $this->saveToFile();
+        }
+    }
+    
+    private function clean(&$first, &$last, $msgnum)
+    {
+        $do_save = false;
+        if (is_array($this->overview)) {
+            $mids = array_keys($this->overview);
+            foreach ($mids as $id) {
+                if (($first <= $last && ($id < $first || $id > $last))
+                        || ($first > $last && $id < $first && $id > $last))
+                {
+                    $this->delid($id, false);
+                    $do_save = true;
+                }
+            }
+            if (!empty($this->overview)) {
+                $first = max(array_keys($this->overview))+1;
+            }
+        }
+        return $do_save;
     }
 
-    function _updateSpool($arg)
+    private function update(&$first, &$last, $msgnum)
     {
-        global $banana;
-        $dates    = array_map('strtotime',    $banana->nntp->xhdr('Date',    $arg));
-        $subjects = array_map('headerdecode', $banana->nntp->xhdr('Subject', $arg));
-        $froms    = array_map('headerdecode', $banana->nntp->xhdr('From',    $arg));
-        $msgids   = $banana->nntp->xhdr('Message-ID', $arg);
-        $refs     = $banana->nntp->xhdr('References', $arg);
-
-        if (is_array(@$this->ids)) {
-            $this->ids = array_merge($this->ids, array_flip($msgids));
-        } else {
-            $this->ids = array_flip($msgids);
+        if ($first >= $last || !$msgnum) {       
+            return false;
         }
 
-        foreach ($msgids as $id=>$msgid) {
-            $msg                = new BananaSpoolHead($dates[$id], $subjects[$id], $froms[$id]);
-            $refs[$id]          = str_replace('><', '> <', @$refs[$id]);
-            $msgrefs            = preg_split("/[ \t]/", strtr($refs[$id], $this->ids));
-            $parents            = preg_grep('/^\d+$/', $msgrefs);
-            $msg->parent        = array_pop($parents);
-            $msg->parent_direct = preg_match('/^\d+$/', array_pop($msgrefs));
+        $messages =& Banana::$protocole->getMessageHeaders($first, $last,
+            array('Date', 'Subject', 'From', 'Message-ID', 'References', 'In-Reply-To'));
+
+        if (!is_array($this->ids)) {
+            $this->ids = array();
+        }
+        foreach ($messages as $id=>&$message) {
+            $this->ids[$message['message-id']] = $id;
+        }
 
-            if (isset($this->overview[$id])) {
-                $msg->desc     = $this->overview[$id]->desc;
-                $msg->children = $this->overview[$id]->children;
+        foreach ($messages as $id=>&$message) {
+            if (!isset($this->overview[$id])) {
+                $this->overview[$id] = new BananaSpoolHead($message);
             }
-            $this->overview[$id] = $msg;
+            $msg    =& $this->overview[$id];
+            $msgrefs = BananaMessage::formatReferences($message);
+            $parents = preg_grep('/^\d+$/', $msgrefs);
+            $msg->parent = array_pop($parents);
+            $msg->parent_direct = preg_match('/^\d+$/', array_pop($msgrefs));
 
-            if ($p = $msg->parent) {
+            if (!is_null($p = $msg->parent)) {
                 if (empty($this->overview[$p])) {
-                    $this->overview[$p] = new BananaSpoolHead($dates[$p], $subjects[$p], $froms[$p], 1);
+                    $this->overview[$p] = new BananaSpoolHead($messages[$p]);
                 }
                 $this->overview[$p]->children[] = $id;
 
-                while ($p) {
+                while (!is_null($p)) {
                     $this->overview[$p]->desc += $msg->desc;
-                    $p = $this->overview[$p]->parent;
+                    if ($p != $this->overview[$p]->parent) {
+                        $p = $this->overview[$p]->parent;
+                    } else {
+                        $p = null;
+                    }    
                 }
             }
         }
+        Banana::$protocole->updateSpool($messages);
+        return true;
     }
 
-    function _updateUnread($since, $mode)
+    private function updateUnread($since)
     {
-        global $banana;
-        if (empty($since)) { return; }
-
-        if (is_array($newpostsids = $banana->nntp->newnews($since, $this->group))) {
-            if (!is_array($this->ids)) { $this->ids = array(); }
-            $newpostsids = array_intersect($newpostsids, array_keys($this->ids));
-            foreach ($newpostsids as $mid) {
-                $this->overview[$this->ids[$mid]]->isread     = false;
-                $this->overview[$this->ids[$mid]]->descunread = 1;
-                $parentmid = $this->ids[$mid];
-                while (isset($parentmid)) {
-                    $this->overview[$parentmid]->descunread ++;
-                    $parentmid = $this->overview[$parentmid]->parent;
+        if (empty($since)) {
+            return;
+        }
+
+        $newpostsids = Banana::$protocole->getNewIndexes($since);
+        
+        if (empty($newpostsids)) {
+            return;
+        }
+
+        if (!is_array($this->ids)) {
+            $this->ids = array();
+        }
+        $newpostsids = array_intersect($newpostsids, array_keys($this->ids));
+        foreach ($newpostsids as $mid) {
+            $id = $this->ids[$mid];
+            if ($this->overview[$id]->isread) {
+                $this->overview[$id]->isread     = false;
+                $this->overview[$id]->descunread = 1;
+                while (isset($id)) {
+                    $this->overview[$id]->descunread ++;
+                    $id = $this->overview[$id]->parent;
                 }
             }
+        }
+    }
 
-            if (count($newpostsids)) {
-                switch ($mode) {
-                    case 1:
-                        foreach ($this->roots as $k=>$i) {
-                            if ($this->overview[$i]->descunread==0) {
-                                $this->killdesc($i);
-                                unset($this->roots[$k]);
-                            }
-                        }
-                        break;
+    public function setMode($mode)
+    {
+        switch ($mode) {
+          case Banana::SPOOL_UNREAD:
+            foreach ($this->roots as $k=>$i) {
+                if ($this->overview[$i]->descunread == 0) {
+                    $this->killdesc($i);
+                    unset($this->roots[$k]);
                 }
             }
+            break;
         }
     }
 
     /** kill post and childrens
      * @param $_id MSGNUM of post
      */
-
-    function killdesc($_id)
+    private function killdesc($_id)
     {
         if (sizeof($this->overview[$_id]->children)) {
             foreach ($this->overview[$_id]->children as $c) {
@@ -265,8 +303,7 @@ class BananaSpool
     /** delete a post from overview
      * @param $_id MSGNUM of post
      */
-
-    function delid($_id, $write=true)
+    public function delid($_id, $write = true)
     {
         if (isset($this->overview[$_id])) {
             if (sizeof($this->overview[$_id]->parent)) {
@@ -296,10 +333,29 @@ class BananaSpool
                 unset($this->ids[$msgid]);
             }
             
-            if ($write) { $this->_saveToFile(); }
+            if ($write) {
+                $this->saveToFile();
+            }
         }
     }
 
+    private function formatDate($stamp)
+    {
+        $today  = intval(time() / (24*3600));
+        $dday   = intval($stamp / (24*3600));
+
+        if ($today == $dday) {
+            $format = "%H:%M";
+        } elseif ($today == 1 + $dday) {
+            $format = _b_('hier')." %H:%M";
+        } elseif ($today < 7 + $dday) {
+            $format = '%a %H:%M';
+        } else {
+            $format = '%a %e %b';
+        }
+        return utf8_encode(strftime($format, $stamp));
+    }
+
     /** displays children tree of a post
      * @param $_id INTEGER MSGNUM of post
      * @param $_index INTEGER linear number of post in the tree
@@ -314,74 +370,75 @@ class BananaSpool
      * take the subject as a reference parameter, transform this subject to be displaid in the spool
      * view and return a string. This string will be put after the subject.
      */
-
-    function _to_html($_id, $_index, $_first=0, $_last=0, $_ref="", $_pfx_node="", $_pfx_end="", $_head=true)
+    private function _to_html($_id, $_index, $_first=0, $_last=0, $_ref="", $_pfx_node="", $_pfx_end="", $_head=true)
     {
-        $spfx_f   = makeImg('k1.gif',       'o', 21, 9); 
-        $spfx_n   = makeImg('k2.gif',       '*', 21, 9);
-        $spfx_Tnd = makeImg('T-direct.gif', '+', 21, 12);
-        $spfx_Lnd = makeImg('L-direct.gif', '`', 21, 12);
-        $spfx_snd = makeImg('s-direct.gif', '-', 21, 5);
-        $spfx_T   = makeImg('T.gif',        '+', 21, 12);
-        $spfx_L   = makeImg('L.gif',        '`', 21, 12);
-        $spfx_s   = makeImg('s.gif',        '-', 21, 5);
-        $spfx_e   = makeImg('e.gif',        '&nbsp;', 21, 12);
-        $spfx_I   = makeImg('I.gif',        '|', 21, 12);
-
-        if ($_index + $this->overview[$_id]->desc < $_first || $_index > $_last) {
-            return;
+        static $spfx_f, $spfx_n, $spfx_Tnd, $spfx_Lnd, $spfx_snd, $spfx_T, $spfx_L, $spfx_s, $spfx_e, $spfx_I;
+        if (!isset($spfx_f)) {
+            $spfx_f   = Banana::$page->makeImg(Array('img' => 'k1',       'alt' => 'o', 'height' => 21,  'width' => 9)); 
+            $spfx_n   = Banana::$page->makeImg(Array('img' => 'k2',       'alt' => '*', 'height' => 21,  'width' => 9));
+            $spfx_Tnd = Banana::$page->makeImg(Array('img' => 'T-direct', 'alt' => '+', 'height' => 21, 'width' => 12));
+            $spfx_Lnd = Banana::$page->makeImg(Array('img' => 'L-direct', 'alt' => '`', 'height' => 21, 'width' => 12));
+            $spfx_snd = Banana::$page->makeImg(Array('img' => 's-direct', 'alt' => '-', 'height' => 21, 'width' => 5));
+            $spfx_T   = Banana::$page->makeImg(Array('img' => 'T',        'alt' => '+', 'height' => 21, 'width' => 12));
+            $spfx_L   = Banana::$page->makeImg(Array('img' => 'L',        'alt' => '`', 'height' => 21, 'width' => 12));
+            $spfx_s   = Banana::$page->makeImg(Array('img' => 's',        'alt' => '-', 'height' => 21, 'width' => 5));
+            $spfx_e   = Banana::$page->makeImg(Array('img' => 'e',   'alt' => '&nbsp;', 'height' => 21, 'width' => 12));
+            $spfx_I   = Banana::$page->makeImg(Array('img' => 'I',        'alt' => '|', 'height' => 21, 'width' => 12));
         }
 
-        $res = '';
-
-        if ($_index>=$_first) {
-            $hc = empty($this->overview[$_id]->children);
+        $overview =& $this->overview[$_id];
+        if ($_index + $overview->desc < $_first || $_index > $_last) {
+            return '';
+        }
 
-            $res .= '<tr class="'.($_index%2?'pair':'impair').($this->overview[$_id]->isread?'':' new')."\">\n";
-            $res .= "<td class='date'>".fancyDate($this->overview[$_id]->date)." </td>\n";
-            $res .= "<td class='subj'>"
-                ."<div class='tree'>$_pfx_node".($hc?($_head?$spfx_f:($this->overview[$_id]->parent_direct?$spfx_s:$spfx_snd)):$spfx_n)
-                ."</div>";
-            $subject = $this->overview[$_id]->subject;
-            if (strlen($subject) == 0) {
+        $res = '';
+        if ($_index >= $_first) {
+            $hc = empty($overview->children);
+
+            $res .= '<tr class="' . ($_index%2 ? 'pair' : 'impair') . ($overview->isread ? '' : ' new') . "\">\n";
+            $res .= '<td class="date">' . $this->formatDate($overview->date) . " </td>\n";
+            $res .= '<td class="subj' . ($_index == $_ref ? ' cur' : '') . '">'
+                . $_pfx_node .($hc ? ($_head ? $spfx_f : ($overview->parent_direct ? $spfx_s : $spfx_snd)) : $spfx_n);
+            $subject = $overview->subject;
+            if (empty($subject)) {
                 $subject = _b_('(pas de sujet)');
             }
             $link = null;
             if (function_exists('hook_getSubject')) {
                 $link = hook_getSubject($subject);
             }
-            $subject = formatPlainText(htmlentities($subject));
-            if ($_index == $_ref) {
-                $res .= '<span class="cur">' . $subject . $link . '</span>';
-            } else {
-                $res .= makeHREF(Array('group' => $this->group,
-                                       'artid' => $_id),
-                                 $subject,
-                                 $subject)
-                     . $link;
+            $subject = banana_catchFormats($subject);
+            if ($_index != $_ref) {
+                $subject = Banana::$page->makeLink(Array('group' => $this->group, 'artid' => $_id,
+                                                    'text'  => $subject, 'popup' => $subject));
             }
-            $res .= "</td>\n<td class='from'>".formatFrom($this->overview[$_id]->from)."</td>\n</tr>";
+            $res .= '&nbsp;' . $subject . $link;
+            $res .= "</td>\n<td class='from'>" . BananaMessage::formatFrom($overview->from) . "</td>\n</tr>";
 
-            if ($hc) { return $res; }
+            if ($hc) {
+                return $res;
+            }
         } 
 
         $_index ++;
-
-        $children = $this->overview[$_id]->children;
+        $children = $overview->children;
         while ($child = array_shift($children)) {
-            if ($_index > $_last) { return $res; }
-            if ($_index+$this->overview[$child]->desc >= $_first) {
+            $overview =& $this->overview[$child];
+            if ($_index > $_last) {
+                return $res;
+            }
+            if ($_index + $overview->desc >= $_first) {
                 if (sizeof($children)) {
                     $res .= $this->_to_html($child, $_index, $_first, $_last, $_ref,
-                            $_pfx_end.($this->overview[$child]->parent_direct?$spfx_T:$spfx_Tnd),
-                            $_pfx_end.$spfx_I, false);
+                            $_pfx_end . ($overview->parent_direct ? $spfx_T : $spfx_Tnd),
+                            $_pfx_end . $spfx_I, false);
                 } else {
                     $res .= $this->_to_html($child, $_index, $_first, $_last, $_ref,
-                            $_pfx_end.($this->overview[$child]->parent_direct?$spfx_L:$spfx_Lnd),
-                            $_pfx_end.$spfx_e, false);
+                            $_pfx_end . ($overview->parent_direct ? $spfx_L : $spfx_Lnd),
+                            $_pfx_end . $spfx_e, false);
                 }
             }
-            $_index += $this->overview[$child]->desc;
+            $_index += $overview->desc;
         }
 
         return $res;
@@ -392,64 +449,38 @@ class BananaSpool
      * @param $_last INTEGER MSGNUM of last post
      * @param $_ref STRING MSGNUM of current/selectionned post
      */
-
-    function to_html($_first=0, $_last=0, $_ref = null)
+    public function toHtml($first = 0, $overview = false)
     {
-        $res  = '<table class="bicol banana_thread" cellpadding="0" cellspacing="0">';
-       
-        if (is_null($_ref)) {
-            $next = $this->nextUnread();
-            if (!is_null($next)) {
-                $next = '<div class="banana_menu">'
-                      . makeImgLink(Array('group' => $this->group,
-                                          'artid' => $next),
-                                    'next_unread.gif',
-                                    _b_('Message non-lu suivant'), null, null, null, 'u')
-                      . '</div>';
-            }
-            $new  = '<div class="banana_action">'
-                  . makeImgLink(Array('group'  => $this->group,
-                                      'action' => 'new'),
-                                'post.gif',
-                                _b_('Nouveau message'), null, null, null, 'p')
-                  . '</div>';
-
-            $res .= '<tr><th>' . $next . _b_('Date') . '</th>';
-            $res .= '<th>' .  _b_('Sujet') . '</th>';
-            $res .= '<th>' . $new . _b_('Auteur') . '</th></tr>';
+        $res = '';
+
+        if (!$overview) {
+            $_first = $first;
+            $_last  = $first + Banana::$tmax - 1;
+            $_ref   = null;
         } else {
-            $res .= '<tr><th colspan="3">' . _b_('Aperçu de ')
-                 . makeHREF(Array('group' => $this->group),
-                            $this->group)
-                 . '</th></tr>';
+            $_ref   = $this->getNdx($first);
+            $_last  = $_ref + Banana::$tafter;
+            $_first = $_ref - Banana::$tbefore;
+            if ($_first < 0) {
+                $_last -= $_first;
+            }
         }
-
         $index = 1;
-        if (sizeof($this->overview)) {
-            foreach ($this->roots as $id) {
-                $res   .= $this->_to_html($id, $index, $_first, $_last, $_ref);
-                $index += $this->overview[$id]->desc ;
-                if ($index > $_last) { break; }
+        foreach ($this->roots as $id) {
+            $res   .= $this->_to_html($id, $index, $_first, $_last, $_ref);
+            $index += $this->overview[$id]->desc ;
+            if ($index > $_last) {
+                break;
             }
-        } else {
-            $res .= '<tr><td colspan="3">'._b_('Aucun message dans ce forum').'</td></tr>';
-        }
-
-        global $banana;
-        if (is_object($banana->groups)) {
-            $res .= '<tr><td colspan="3" class="subs">'
-                 . $banana->groups->to_html()
-                 . '</td></tr>';
         }
-        return $res .= '</table>';
+        return $res;
     }
 
     /** computes linear post index
      * @param $_id INTEGER MSGNUM of post
      * @return INTEGER linear index of post
      */
-
-    function getndx($_id)
+    public function getNdX($_id)
     {
         $ndx    = 1;
         $id_cur = $_id;
@@ -478,8 +509,8 @@ class BananaSpool
     /** Return root message of the given thread
      * @param id INTEGER id of a message
      */
-     function root($id)
-     {
+    public function root($id)
+    {
         $id_cur = $id;
         while (true) {
             $id_parent = $this->overview[$id_cur]->parent;
@@ -492,7 +523,7 @@ class BananaSpool
     /** Returns previous thread root index
      * @param id INTEGER message number
      */
-    function prevThread($id)
+    public function prevThread($id)
     {
         $root = $this->root($id);
         $last = null;
@@ -508,7 +539,7 @@ class BananaSpool
     /** Returns next thread root index
      * @param id INTEGER message number
      */
-    function nextThread($id)
+    public function nextThread($id)
     {
         $root = $this->root($id);
         $ok   = false;
@@ -526,7 +557,7 @@ class BananaSpool
     /** Return prev post in the thread
      * @param id INTEGER message number
      */
-    function prevPost($id)
+    public function prevPost($id)
     {
         $parent = $this->overview[$id]->parent;
         if (is_null($parent)) {
@@ -545,7 +576,7 @@ class BananaSpool
     /** Return next post in the thread
      * @param id INTEGER message number
      */
-    function nextPost($id)
+    public function nextPost($id)
     {
         if (count($this->overview[$id]->children) != 0) {
             return $this->overview[$id]->children[0];
@@ -574,7 +605,7 @@ class BananaSpool
     /** Look for an unread message in the thread rooted by the message
      * @param id INTEGER message number
      */
-    function _nextUnread($id)
+    private function _nextUnread($id)
     {
         if (!$this->overview[$id]->isread) {
             return $id;
@@ -591,7 +622,7 @@ class BananaSpool
     /** Find next unread message
      * @param id INTEGER message number
      */
-    function nextUnread($id = null)
+    public function nextUnread($id = null)
     {
         if (!is_null($id)) {
             // Look in message children
diff --git a/banana/templates/banana-base.tpl b/banana/templates/banana-base.tpl
new file mode 100644 (file)
index 0000000..3db7416
--- /dev/null
@@ -0,0 +1,63 @@
+{* *}
+{* *}
+{* *}
+
+<table class="cadre_a_onglet" style="width: 100%" cellpadding="0" cellspacing="0">
+  <tr>
+    <td>
+      <ul id="onglet">
+        {foreach from=$pages item=pg key=name}
+          {if $name eq $page}
+            <li class="actif">{$pg.text}</li>
+            {assign var=current_page value=$pg}
+          {else}
+            <li>{if $name eq 'subscribe'}{link subscribe=1 text=$pg.text}
+            {elseif $name eq 'forums'}{link text=$pg.text}
+            {elseif $name eq 'thread'}{link group=$group text=$group}
+            {elseif $name eq 'message'}{link group=$group artid=$artid text=$pg.text}
+            {else}{link page=$name text=$pg.text}
+            {/if}</li>
+          {/if}
+        {/foreach}
+      </ul>
+    </td>
+  </tr>
+  <tr>
+    <td class="conteneur_tab banana">
+      {foreach from=$errors item=error}
+      <p class="error">{$error}</p>
+      {/foreach}
+      {foreach from=$actions item=act}
+      <p class="center" style="padding: 0; margin: 0 0 1em 0">{$act.text}</p>
+      {/foreach}
+      {if $page eq 'forums'}
+        {include file="banana-boxlist.inc.tpl" grouplist=$groups withstats=true}
+        {if $newgroups|@count}
+        <p>Les nouveaux groupes suivants ont été créés depuis votre dernière visite</p>
+        {include file="banana-boxlist.inc.tpl" grouplist=$newgroups withstats=true}
+        {/if}
+      {elseif $page eq 'subscribe'}
+        {include file="banana-boxlist.inc.tpl" grouplist=$groups withsubs=true}
+      {elseif $page eq 'thread'}
+        {include file="banana-thread.inc.tpl" withtitle=true}
+      {elseif $page eq 'message'}
+        {include file="banana-message.inc.tpl"}
+        {include file="banana-thread.inc.tpl" withtitle=false}
+      {elseif $page eq 'new'}
+        {include file="banana-newmessage.inc.tpl"}
+      {elseif $page eq 'cancel'}
+        <p class="error">Voulez-vous vraiment annuler ce message ?</p>
+        <form action="{url group=$group artid=$artid action=cancel}" method="post">
+          <p class="center">
+            <input type="submit" name="cancel" value="Annuler !" />
+          </p>
+        </form>
+        {include file="banana-message.inc.tpl" noactions=true}
+      {elseif $current_page.template}
+        {include file=$current_page.template}
+      {/if}
+    </td>
+  </tr>
+</table>
+
+{* vim:set et sw=2 sts=2 ts=2: *}
diff --git a/banana/templates/banana-boxlist.inc.tpl b/banana/templates/banana-boxlist.inc.tpl
new file mode 100644 (file)
index 0000000..0331838
--- /dev/null
@@ -0,0 +1,44 @@
+{if $groups|@count}
+{if $withsubs}
+<form action="{url subscribe=1}" method="post">
+<p style="text-align: center">
+  <input type="submit" name="validsubs" value="Valider" />
+</p>
+{/if}
+<table class="bicol">
+  <tr>
+    {if $withsubs}
+    <th></th>
+    {/if}
+    {if $withstats}
+    <th>Total</th>
+    <th>Nouveaux</th>
+    {/if}
+    <th>Nom</th>
+    <th>Description</th>
+  </tr>
+  {foreach from=$groups key=name item=grp}
+  <tr class="{cycle values="impair,pair"}">
+    {if $withsubs}
+    <td>
+      <input type="checkbox" name="subscribe[{$name}]" {if in_array($name, $profile.subscribe)}checked="checked"{/if} />
+    </td>
+    {/if}
+    {if $withstats}
+    <td style="text-align: center">{if $grp.msgnum eq 0}-{else}{$grp.msgnum}{/if}</td>
+    <td style="text-align: center">{if $grp.unread eq 0}-{else}{$grp.unread}{/if}</td>
+    {/if}
+    <td class="grp">{link group=$name text=$name}</td>
+    <td class="dsc">{$grp.desc}</td>
+  </tr>
+  {/foreach}
+</table>
+{if $withsubs}
+<p style="text-align: center">
+  <input type="submit" name="validsubs" value="Valider" />
+</p>
+</form>
+{/if}
+{/if}
+
+{* vim:set et sw=2 sts=2 ts=2: *}
diff --git a/banana/templates/banana-message.inc.tpl b/banana/templates/banana-message.inc.tpl
new file mode 100644 (file)
index 0000000..5dbc3f4
--- /dev/null
@@ -0,0 +1,62 @@
+<table class="bicol message">
+  <tr>
+    <th colspan="3" class="subject">
+      {if !$noactions}
+      <div class="menu">
+        {if $spool->nextUnread($artid)}
+        {imglink group=$group artid=$spool->nextUnread($artid) img=next_unread alt="Message non-lu suivant" accesskey=u}{/if}
+        {if $spool->prevPost($artid)}
+        {imglink group=$group artid=$spool->prevPost($artid) img=prev alt="Message précédent" accesskey=a}{/if}
+        {if $spool->nextPost($artid)}
+        {imglink group=$group artid=$spool->nextPost($artid) img=next alt="Message suivant" accesskey=z}{/if}
+        {if $spool->prevThread($artid)}
+        {imglink group=$group artid=$spool->prevThread($artid) img=prev_thread alt="Discussion précédente" accesskey=q}{/if}
+        {if $spool->nextThread($artid)}
+        {imglink group=$group artid=$spool->nextThread($artid) img=next_thread alt="Discussion suivante" accesskey=s}{/if}
+      </div>
+      <div class="action">
+        {if $message->canSend()}
+        {imglink group=$group action="new" img=post alt="Nouveau messasge" accesskey=p}
+        {imglink group=$group artid=$artid action="new" img=reply alt="Répondre" accesskey=r}
+        {/if}
+        {if $message->canCancel()}
+        {imglink group=$group artid=$artid action="cancel" img=cancel alt="Annuler" accesskey=c}
+        {/if}
+      </div>
+      {/if}
+      {$message->translateHeaderValue('subject')}
+    </th>
+  </tr>
+  {foreach from=$headers name=headers item=hdr}
+  <tr class="pair">
+    <td class="hdr">{$message->translateHeaderName($hdr)}</td>
+    <td>{$message->translateHeaderValue($hdr)}</td>
+    {if $smarty.foreach.headers.first}
+    <td class="xface" rowspan="{$headers|@count}">
+      {if $message->hasXFace()}
+      <img src="{url group=$group artid=$artid part="xface"}" alt="[ X-Face ]" />
+      {/if}
+    </td>
+    {/if}
+  </tr>
+  {/foreach}
+  {assign var=files value=$message->getAttachments()}
+  {if $files|@count}
+  <tr class="pair">
+    <td class="hdr">Fichiers joints</td>
+    <td colspan="2">
+      {foreach from=$files item=file name=attachs}
+      {$file->getFilename()|htmlentities}
+      {imglink img=save alt="Enregistrer" group=$group artid=$artid part=$file->getFilename()}{if !$smarty.foreach.attachs.last}, {/if}
+      {/foreach}
+    </td>
+  </tr>
+  {/if}
+  <tr>
+    <td colspan="3" class="body">
+      {$message->getFormattedBody()}
+    </td>
+  </tr>
+</table>
+
+{* vim:set et sw=2 sts=2 ts=2: *}
diff --git a/banana/templates/banana-newmessage.inc.tpl b/banana/templates/banana-newmessage.inc.tpl
new file mode 100644 (file)
index 0000000..dd15111
--- /dev/null
@@ -0,0 +1,42 @@
+<form {if $can_attach}enctype="multipart/form-data"{/if} action="{url group=$group artid=$artid action=new}" method="post" accept-charset="utf-8">
+  <table class="bicol">
+    <tr>
+      <th colspan="2">Composer un nouveau message</th>
+    </tr>
+    {foreach from=$headers key=header item=values}
+    <tr>
+      <td>{$values.name}</td>
+      <td>
+        {if $values.fixed}
+        {$values.fixed|htmlentities}
+        {else}
+        <input type="text" name="{$header}" value="{$values.user|default:$smarty.request[$header]}" size="50" />
+        {/if}
+      </td>
+    </tr>
+    {/foreach}
+    <tr class="pair">
+      <td colspan="2" class="center">
+        <textarea name="body" cols="74" rows="16">{$body|default:$smarty.request.body}</textarea>
+      </td>
+    </tr>
+    {if $can_attach}
+    <tr class="pair">
+      <td>Fichier joint</td>
+      <td>
+        {if $maxfilesize}
+        <input type="hidden" name="MAX_FILE_SIZE" value="{$maxfilesize}" />
+        {/if}
+        <input type="file" name="attachment" size="40" />
+      </td>
+    </tr>
+    {/if}
+    <tr class="pair">
+      <td colspan="2" class="center">
+        <input type="submit" name="sendmessage" value="Envoyer le message" />
+      </td>
+    </tr>
+  </table>
+</form>
+
+{* vim:set et sts=2 ts=2 sw=2: *}
diff --git a/banana/templates/banana-thread.inc.tpl b/banana/templates/banana-thread.inc.tpl
new file mode 100644 (file)
index 0000000..60630fa
--- /dev/null
@@ -0,0 +1,65 @@
+{if $withtitle}
+<div class="pages">
+{if $spool->overview|@count > $msgbypage}
+{section name=pages loop=$spool->overview step=$msgbypage}
+  {if $first >= $smarty.section.pages.index && $first < $smarty.section.pages.index_next}
+    <strong>{$smarty.section.pages.iteration}</strong>
+  {else}
+    {link group=$group first=$smarty.section.pages.index text=$smarty.section.pages.iteration}
+  {/if}
+{/section}
+{/if}
+</div>
+{/if}
+<table class="bicol thread">
+  <tr>
+    {if $withtitle}
+    <th>
+      {if $spool->nextUnread()}
+      <div class="menu">
+        {imglink group=$group artid=$spool->nextUnread() img=next_unread alt="Message non-lu suivant" accesskey=u}
+      </div>
+      {/if}
+      Date
+    </th>
+    <th>Sujet</th>
+    <th>
+      {if $protocole->canSend()}
+      <div class="action">
+        {imglink group=$group action=new img=post alt="Nouveau message" accesskey=p}
+      </div>
+      {/if}
+      Auteur
+    </th>
+    {else}
+    <th colspan="3">
+      {link group=$group text=$group}
+    </th>
+    {/if}
+  </tr>
+  {if $spool->overview|@count}
+  {if $artid}{$spool->toHtml($artid, true)}{else}{$spool->toHtml($first)}{/if}
+  {else}
+  <tr>
+    <td colspan="3">
+      Aucun message dans ce forum
+    </td>
+  </tr>
+  {/if}
+</table>
+{include file="banana-boxlist.inc.tpl" grouplist=$groups withstats=true}
+{if $withtitle}
+<div class="pages">
+{if $spool->overview|@count > $msgbypage}
+{section name=pages loop=$spool->overview step=$msgbypage}
+  {if $first >= $smarty.section.pages.index && $first < $smarty.section.pages.index_next}
+    <strong>{$smarty.section.pages.iteration}</strong>
+  {else}
+    {link group=$group first=$smarty.section.pages.index text=$smarty.section.pages.iteration}
+  {/if}
+{/section}
+{/if}
+</div>
+{/if}
+
+{* vim:set et sw=2 sts=2 ts=2: *}
similarity index 85%
rename from banana/utf8.php
rename to banana/text.func.inc.php
index 586bc31..00d0385 100644 (file)
@@ -1,13 +1,28 @@
 <?php\r
 /********************************************************************************\r
-* banana/utf8.php : utf8 to html entities\r
+* banana/text.php : text tools\r
 * ---------------\r
 *\r
 * This file is part of the banana distribution\r
 * Copyright: See COPYING files that comes with this distribution\r
 ********************************************************************************/\r
 \r
-function utf8entities($source)\r
+function _b_($str)\r
+{\r
+    if (!is_utf8($str)) {\r
+        $str = utf8_encode($str);\r
+    }\r
+    return dgettext('banana', $str);\r
+}\r
+\r
+if (!function_exists('is_utf8')) {\r
+    function is_utf8($s)\r
+    {\r
+        return @iconv('utf-8', 'utf-8', $s) == $s;\r
+    }\r
+}\r
+\r
+function banana_utf8entities($source)\r
 {\r
    // array used to figure what number to decrement from character order value \r
    // according to number of characters used to map unicode to ascii by utf-8\r
index dc9ac52..b02058d 100644 (file)
@@ -6,28 +6,12 @@
 * Copyright: See COPYING files that comes with this distribution
 ********************************************************************************/
 
-div.pages {
-    font-size: 80%;
-    text-align: center;
-    padding-top: 0.1em;
-    padding-bottom: 0.1em;
-}
-div.banana_action { float: right; }
-div.banana_menu { float: left; }
-
-
 /** GROUP LIST **/
-
-table.banana_group { width: 100%; }
-table.banana_group td.new { text-align: center; }
-table.banana_group td.all { text-align: center; }
-table.banana_group td.grp { text-align: left; }
-table.banana_group td.dsc { text-align: left; }
+.banana .grp { text-align: left; }
+.banana .dsc { text-align: left; }
 
 /** THREAD VIEW **/
-
-table.banana_thread { width: 100%; }
-table.banana_thread td {
+.banana table.thread td {
     white-space: nowrap;
     height: 20px;
     vertical-align: middle;
@@ -37,62 +21,57 @@ table.banana_thread td {
     margin-bottom: 0;
 }
 
-table.banana_thread tr.new { font-weight: bold; }
+.banana table.thread .new { font-weight: bold; }
 
-table.banana_thread td.date { width: 15%; text-align: center; }
-table.banana_thread td.subj { text-align: left; overflow: hidden; }
-table.banana_thread td.from { text-align: left; }
+.banana table.thread .date { width: 15%; text-align: center; }
+.banana table.thread .subj { text-align: left; overflow: hidden; }
+.banana table.thread .from { text-align: left; }
 
-table.banana_thread div.tree { float: left; padding-right: 0.3em; }
-table.banana_thread span.cur { font-style: italic; }
-table.banana_thread table { border: 0px; padding: 0px; margin: 0px; width: 100%; }
-table.banana_thread td.subs { padding: 0%; }
+.banana table.thread .cur { font-style: italic; }
+.banana table.thread .subs { padding: 0%; }
 
 /** MESSAGE VIEW **/
-
-table.banana_msg { width: 100%; }
-table.banana_msg td.headers { width: 100%; }
-table.banana_msg .hdr { width: 15%; text-align: right; font-weight: bold; padding-right: 1em; }
-table.banana_msg td.xface { text-align: right; }
-table.banana_msg th.subject { font-size: 120%; }
-table.banana_msg td.body { padding-top: 1em; padding-bottom: 0.5em; }
-
-table.banana_msg td.thrd { padding: 0px; }
-table.banana_msg table { border: 0px; padding: 0px; margin: 0px; width: 100%; }
-
-table.banana_msg blockquote {
+.banana table.message .hdr { width: 15%; text-align: right; font-weight: bold; padding-right: 1em; }
+.banana table.message .xface { text-align: right; }
+.banana table.message .subject { font-size: 120%; }
+.banana table.message .body { padding-top: 1em; padding-bottom: 0.5em; }
+.banana table.message blockquote {
     color: blue;
     font-style: italic;
     margin-left: 0;
-    padding-left: 1em;
+    padding-left: 1ex;
     border-left: solid 1px;
     border-color: blue;
     background: inherit;    
 }
-table.banana_msg blockquote blockquote {
+.banana table.message blockquote blockquote {
        color: green;
        border-color: green;
     background: inherit;
 }
-table.banana_msg blockquote blockquote blockquote {
+.banana table.message blockquote blockquote blockquote {
        color: #cc0000;
        border-color: #cc0000;
     background: inherit;
 }
-table.banana_msg blockquote blockquote blockquote blockquote {
+.banana table.message blockquote blockquote blockquote blockquote {
     color: #aaaa00;
     border-color: #aaaa00;
     background: inherit;
 }
 
 /** MISC **/
-
-div.center { text-align: center; padding: 1em; }
-p.error { color: red; background: inherit; }
-pre.error {
-    color: red;
+.banana .center { text-align: center; padding: 1em; }
+.banana .error { color: red; background: inherit; }
+.banana pre.error {
     margin-left: 2em;
     font-size: 90%;
-    background: inherit;
 }
-
+.banana .pages {
+    font-size: 80%;
+    text-align: center;
+    padding-top: 0.1em;
+    padding-bottom: 0.1em;
+}
+.banana div.action { float: right; }
+.banana div.menu { float: left; }