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 = [ '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 = []; foreach ($rsp as $buf) { if (preg_match('/^\.\s*$/', $buf)) { break; } list($group, $last, $first, $post) = preg_split('/\s+/', $buf, 4); $result[$group] = [ 'desc' => '', 'last' => trim($last), 'first' => trim($first), 'post' => strtolower(trim($post)) ]; } # Get groups descriptions $rsp = $this->write(['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 [ '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 = []; 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 = []; $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 = []; 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 = [], $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 = []; # 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 [ '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(); } }