<?php
# ***** BEGIN LICENSE BLOCK *****
# This file is part of Clearbricks.
# Copyright (c) 2003-2013 Olivier Meunier & Association Dotclear
# All rights reserved.
#
# Clearbricks is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
# 
# Clearbricks is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with Clearbricks; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# ***** END LICENSE BLOCK *****

class netNntp extends netSocket
{
	const SERVER_READY = 200;
	const SERVER_READY_NO_POST = 201;
	const GROUP_SELECTED = 211;
	const INFORMATION_FOLLOWS = 215;
	const ARTICLE_HEAD_BODY = 220;
	const ARTICLE_HEAD = 221;
	const ARTICLE_BODY = 222;
	const ARTICLE_OVERVIEW = 224;
	const NEW_ARTICLES = 230;
	const ARTICLE_POST_OK = 240;
	const ARTICLE_POST_READY = 340;
	const AUTH_ACCEPT = 281;
	const MORE_AUTH_INFO = 381;
	const AUTH_REQUIRED = 480;
	const AUTH_REJECTED = 482;
	const NOT_IMPLEMENTED = 500;
	const NO_PERMISSION = 502;
	
	protected $host;
	protected $port;
	protected $user;
	protected $password;
	
	protected $proxy_host;
	protected $proxy_port;
	protected $proxy_user;
	protected $proxy_pass;
	protected $use_proxy;
	
	public function __construct($host,$port=119,$user=null,$password=null,$timeout=10)
	{
		$this->host = $host;
		$this->port = (integer) $port;
		$this->user = $user;
		$this->password = $password;
		$this->_timeout = $timeout;
	}
	
	public function write($data)
	{
		if (!is_array($data)) {
			$data = $data."\r\n";
		}
		return parent::write($data);
	}
	
	public function close()
	{
		if ($this->isOpen()) {
			$this->sendRequest('quit');
			parent::close();
		}
	}
	
	public function open()
	{
		if ($this->isOpen()) {
			return true;
		}
		
		if ($this->use_proxy) {
			$this->_host = $this->proxy_host;
			$this->_post = $this->proxy_port;
		} else {
			$this->_host = $this->host;
			$this->_port = $this->port;
		}
		
		$rsp = parent::open();
		
		if ($this->isOpen())
		{
			if ($this->use_proxy)
			{
				$data[] = 'CONNECT '.$this->host.':'.$this->port.' HTTP/1.0';
				if ($this->proxy_user && $this->proxy_pass)
				{
					$data[] =
					'Proxy-Authorization: Basic '.
					base64_encode($this->proxy_user.':'.$this->proxy_pass);
				}
				
				foreach ($this->write($data) as $i => $v)
				{
					if ($i == 0)
					{
						if (stristr($v, '200 Connection established')) {
							continue;
						} else {
							$rsp = array(
								'status' => self::NO_PERMISSION, # Assign it to something dummy
								'message' => "No permission"
							);
							break;
						}
					}
					if ($i == 2) {
						$rsp = $this->parseResponse($v);
						break;
					}
				}
			}
			else
			{
				$rsp = $this->parseResponse($rsp->current());
			}
			
			if (($rsp['status'] == self::SERVER_READY) || ($rsp['status'] == self::SERVER_READY_NO_POST))
			{
				$this->sendRequest("mode reader");
				if ($this->user)
				{
					$rsp = $this->parseResponse($this->sendRequest('authinfo user '.$this->user));
					
					if ($rsp['status'] == self::MORE_AUTH_INFO)
					{
						$rsp = $this->parseResponse($this->sendRequest('authinfo pass '.$this->password));
						
						if ($rsp['status'] == self::AUTH_ACCEPT) {
							return true;
						}
					}
				}
				else
				{
					return true;
				}
			}
			
			throw new Exception($rsp['status'].' - '.$rsp['message']); 
		}
	}
	
	public function setUser($user,$password=null)
	{
		$this->user = $user;
		$this->password = $password;
		
		if ($this->isOpen()) {
			$this->close();
			$this->open();
		}
	}
	
	public function setProxy($proxy_host,$proxy_port=null,$proxy_user=null,$proxy_pass=null)
	{
		$this->proxy_host = $proxy_host;
		$this->proxy_port = $proxy_port;
		$this->proxy_user = $proxy_user;
		$this->proxy_pass = $proxy_pass;
		
		if ((strcmp($this->proxy_host, "") != 0) && (strcmp($this->proxy_port, "") != 0)) {
			$this->use_proxy = true;
		} else {
			$this->use_proxy = false;
		}
	}
	
	public function getGroupsList($group_pattern=null)
	{
		$rsp = $this->write('list active '.$group_pattern);
		$r = $this->parseResponse($rsp->current());
		
		if ($r['status'] == self::INFORMATION_FOLLOWS)
		{
			# List groups
			$result = array();
			foreach($rsp as $buf)
			{
				if (preg_match('/^\.\s*$/',$buf)) {
					break;
				}
				
				list($group, $last, $first, $post) = preg_split('/\s+/',$buf,4);
				$result[$group] = array(
					'desc' => '',
					'last' => trim($last),
					'first' => trim($first),
					'post' => strtolower(trim($post))
				);
			}
			
			# Get groups descriptions
			$rsp = $this->write(array('list newsgroups '.$group_pattern));
			$r = $this->parseResponse($rsp->current());
			
			if ($r['status'] == self::INFORMATION_FOLLOWS)
			{
				foreach ($rsp as $buf)
				{
					if (self::eot($buf)) {
						break;
					}
					
					list($group, $desc) = preg_split('/\s+/',$buf,2);
					if (isset($result[$group])) {
						$result[$group]['desc'] = text::toUTF8(trim($desc));
					}
				}
			}
			
			return $result;
		}
		
		throw new Exception($r['message'].' - ('.$r['status'].')');
	}
	
	public function joinGroup($group)
	{
		$rsp = $this->parseResponse($this->sendRequest('group '.$group));
		
		if ($rsp['status'] == self::GROUP_SELECTED)
		{
			$result = preg_split("/\s/", $rsp['message']);
			
			return array(
				'count' => $result[0],
				'start_id' => $result[1],
				'end_id' => $result[2],
				'group' => $result[3]
			);
		}
		
		throw new Exception($rsp['message'].' - ('.$rsp['status'].')');
	}
	
	public function getArticleList($group=null)
	{
		$rsp = $this->write('listgroup '.$group);
		$r = $this->parseResponse($rsp->current());
		
		if ($r['status'] == self::GROUP_SELECTED)
		{
			$res = array();
			foreach ($rsp as $i => $buf)
			{
				if (self::eot($buf)) {
					break;
				}
				$res[] = trim($buf);
			}
			return $res;
		}
		
		throw new Exception($r['message'].' - ('.$r['status'].')');
	}
	
	public function getNewArticles($ts,$group)
	{
		$ts = $ts + dt::getTimeOffset('UTC',$ts);
		
		
		# First try with newnews
		$rsp = $this->write('newnews '.$group.' '.dt::str('%y%m%d %H%M%S',$ts).' GMT');
		$r = $this->parseResponse($rsp->current());
		
		if ($r['status'] == self::NEW_ARTICLES)
		{
			$res = array();
			$rsp->current(); # we don't want first matched article
			foreach ($rsp as $buf)
			{
				if (self::eot($buf)) {
					break;
				}
				$res[] = trim($buf);
			}
			return $res;
		}
		else
		{
			# newnews is not implemented, use xhdr instead
			# First, we need to join the group
			$g = $this->joinGroup($group);
			
			if ($g['count'] > 1000) {
				$g['start_id'] = $g['end_id']-1000;
			}
			
			# Then, xhdr on all group messages
			$rsp = $this->write('xhdr date '.$g['start_id'].'-');
			$r = $this->parseResponse($rsp->current());
			
			if ($r['status'] == self::ARTICLE_HEAD)
			{
				$ts = $ts + dt::getTimeOffset('UTC',$ts);
				
				$res = array();
				foreach ($rsp as $buf)
				{
					if (self::eot($buf)) {
						break;
					}
					$buf = preg_split('/\s/',$buf,2);
					if (strtotime($buf[1]) > $ts) {
						$res[] = $buf[0];
					}
				}
				return $res;
			}
		}
		
		throw new Exception($r['message'].' - ('.$r['status'].')');
	}
	
	public function getHeader($message_id,&$header='')
	{
		$rsp = $this->write('head '.$message_id);
		$r = $this->parseResponse($rsp->current());
		
		if ($r['status'] == self::ARTICLE_HEAD || $r['status'] == self::ARTICLE_HEAD_BODY)
		{
			$header = '';
			foreach ($rsp as $buf)
			{
				if (self::eot($buf)) {
					break;
				}
				$header .= $buf;
			}
			
			return new nntpMessage($header);
		}
		
		throw new Exception($r['message'].' - ('.$r['status'].')');
	}
	
	public function getArticle($message_id,&$article='')
	{
		$rsp = $this->write('article '.$message_id);
		$r = $this->parseResponse($rsp->current());
		
		if ($r['status'] == self::ARTICLE_BODY || $r['status'] == self::ARTICLE_HEAD_BODY)
		{
			$article = '';
			foreach ($rsp as $buf)
			{
				if (self::eot($buf)) {
					break;
				}
				$article .= $buf;
			}
			return new nntpMessage($article);
		}
		
		throw new Exception($r['message'].' - ('.$r['status'].')');
	}
	
	public function postArticle($headers=array(),$content)
	{
		if (!is_array($headers)) {
			return false;
		}
		
		if (empty($headers['From'])) {
			throw new Exception('No "From" header in message');
		}
		
		$headers['Mime-Version'] = '1.0';
		$headers['Content-Type'] = 'text/plain; charset=UTF-8';
		$headers['Content-Transfer-Encoding'] = 'quoted-printable';
		
		$content = mailConvert::rewrap($content);
		$content = preg_replace('/^\./msu','..$1',$content);
		$content = text::QPEncode($content);
		
		$data = array();
		# Headers
		foreach ($headers as $k => $v) {
			$data[] = $k.': '.$v;
		}
		
		# Blank line
		$data[] = '';
		
		# Body
		foreach (preg_split("/\r?\n/msu",$content) as $l) {
			$data[] = $l;
		}
		
		# EOT
		$data[] = '.';
		
		$this->sendRequest('post');
		
		$rsp = $this->write($data);
		$r = $this->parseResponse($rsp->current());
		if ($r['status'] == self::ARTICLE_POST_OK) {
			return true;
		}
		
		throw new Exception($r['message'].' - ('.$r['status'].')');
	}
	
	protected static function eot($l)
	{
		return preg_match('/^\.\s*$/',$l);
	}
	
	protected function assertOpen()
	{
		if (!$this->isOpen()) {
			throw new Exception('NNTP connexion not available');
		}
	}
	
	protected function parseResponse($rsp)
	{
		return array(
			'status' => substr($rsp,0,3),
			'message' => rtrim(substr($rsp,4),"\r\n")
		);
	}
	
	protected function sendRequest($request)
	{
		$this->assertOpen();
		
		$rsp = $this->write($request);
		$this->flush();
		return $rsp->current();
	}
}
?>